ProcessSubmitDecisionTool
`process_submit_decision` — resolves an open gate via {@see HumanGate::resolve()} then drives the process forward again via {@see ProcessRunner} (so a decision that reaches a terminal state fires `process.terminal` — see {@see ProcessRunner}'s own docblock — and a decision that loops back to a gated state re-opens a fresh gate, both in one call). {@see HumanGate::resolve()}'s failure modes are each surfaced as a distinct, clean {@see ToolResult::error()} instead of a generic catch-all — `SelfApprovalException` MUST be caught before the generic `\RuntimeException` arm since it extends it. Parameters are named `instance_id`/`gate_id` (snake_case), not `instanceId`/`gateId`, on purpose: {@see \Milpa\ToolRuntime\ToolScanner::buildSchema()} takes a tool's wire argument names directly from `ReflectionParameter::getName()` with no case conversion, so the PHP parameter name IS the JSON-RPC argument key a caller must send — this family's wire convention is snake_case throughout. The resolving principal is the AUTHENTICATED caller — the {@see ToolContext} the host injects through {@see self::setCurrentContext()} — never an argument. An earlier version took a `principal` argument and judged it against the gate's requester, so an agent that opened a gate could approve its own work by naming anyone else: author ≠ approver was only as strong as the caller's honesty. A caller acting for a human authenticates AS that human (the host builds that human's own context — e.g. {@see ToolContext::web()} or {@see ToolContext::authorizedBy()}) instead of asserting the name. With no authenticated principal the call fails closed (`UNAUTHENTICATED`), and the context is dropped after every call, so one caller's identity can never answer for the next. And the authenticated caller must be a VERIFIED actor, not a transport's placeholder (greenhouse decisions/0528). On a process-trusted transport the principal names the pipe, not a person: MCP over stdio is `stdio` for whoever holds it, and a terminal is `local-shell` for whoever has a shell. Judged only as author ≠ approver, an agent over MCP could approve a gate a human opened. Those placeholders ({@see self::UNVERIFIED}) cannot resolve a gate (`UNVERIFIED_APPROVER`); a signed call, a passkey session or a host-authenticated caller can. This tool touches NO domain entity — reaching a terminal state is surfaced purely via the `process.terminal` event {@see ProcessRunner} dispatches; a consumer subscribes to that event to run whatever domain effect its own process definition's terminal state should trigger.
ProcessSubmitDecisionTool::__construct()
public function __construct(Milpa\EventStore\EventStoreInterface $store, Milpa\Orchestrator\HumanGate $gate, Milpa\Orchestrator\ProcessRunner $runner, Milpa\Orchestrator\ProcessDefinitionRegistry $registry):Parameters
| Name | Type | Description |
|---|---|---|
| $store | Milpa\EventStore\EventStoreInterface | |
| $gate | Milpa\Orchestrator\HumanGate | |
| $runner | Milpa\Orchestrator\ProcessRunner | |
| $registry | Milpa\Orchestrator\ProcessDefinitionRegistry |
ProcessSubmitDecisionTool::setCurrentContext()
public function setCurrentContext(Milpa\ToolRuntime\Contracts\ToolContext $ctx): voidCaptures the calling {@see ToolContext} — the tool scanner injects it when this method exists.
Parameters
| Name | Type | Description |
|---|---|---|
| $ctx | Milpa\ToolRuntime\Contracts\ToolContext |
ProcessSubmitDecisionTool::submit()
public function submit(string $instance_id, string $gate_id, string $decision): Milpa\ToolRuntime\ToolResultResolves `$gate_id` for `$instance_id` with `$decision` on behalf of the authenticated caller and auto-advances the process again.
Parameters
| Name | Type | Description |
|---|---|---|
| $instance_id | string | |
| $gate_id | string | |
| $decision | string |
Returns
with data `{instance_id: string, current_state: string}` on success