Skip to content

Outcomes and recovery for MCP orchestrators

An MCP orchestrator receives validator results and asynchronous job records; it cannot use snodo validate or branch on that command's process exit-code table. Use the outcome payload and job state as evidence, then choose the next action below. A dispatch_task or retry_job acceptance is not a successful result: follow it with watch_job, and inspect get_job_status or get_job_logs for a specific follow-up or diagnosis.

The five canonical halts (ADR 015)

These are the engine's canonical halt names. pass is a successful validation result, not a halt. A validation result does not itself mean the dispatched task completed; confirm the terminal job status and exit code.

blocker

Means: A validator judged the task or its work and rejected it. The concern is a verdict, not an operational failure. In plan records the corresponding task status is blocked.

Next: Inspect the returned validator result and, when necessary, get_job_logs and the current repository/worktree evidence. If the concern is valid, create a focused corrective follow-up task and let validators judge that work. Change the specification only when evidence shows the specification itself is wrong. A reported stale premise can be false: check the premise against the current source before changing either implementation or spec.

Never: Override a blocker, including with propose_adjudicate; a blocker is non-overridable (INV3). Do not rewrite a spec merely to silence an unverified validator claim, or retry the same task blindly in a loop.

Operator: snodo authorize is not a blocker escape hatch. Use it only if a separate escalate outcome asks for a human decision.

escalate

Means: A validator concern requires a human adjudication. No authorization has been granted yet; the task is parked (plan task status blocked).

Next: Surface the validator, concern, and task ID to the operator and wait for the human to review it with snodo authorize <task_id>. The MCP tool propose_adjudicate can record an unsigned proposal when a proposal is useful: it writes an adjudicate entry to the active session's pending_decisions[task_id], containing the validator ID, proceed or halt, justification, proposed_by: "agent", and timestamp. It returns status: pending and the instruction to run snodo authorize <task_id>; that is a proposal, not an authorized decision. The human CLI reviews and signs it.

Never: Self-authorize, treat a pending proposal as a signed decision, route around the gate, or retry blindly while authorization is unresolved. Do not use propose_adjudicate to override a blocker.

Operator: Required. snodo authorize <task_id> is the human review and signing step.

validator_error

Means: A validator failed to produce a verdict. This says nothing adverse about the task's correctness and is not a decision for a human to adjudicate.

Next: Inspect get_job_logs and the error payload, resolve the validator or provider problem, then retry the same task with retry_job when the job is a task job and the issue is fixed. By default, retry_job uses the recorded spec unchanged. It dispatches under the same task ID and reuses that task's existing worktree, so prior attempt work remains available; inspect it rather than reproducing it. append_spec adds guidance without replacing the spec. Use revised_spec only when a diagnosed specification correction is actually needed.

Never: Turn an operational failure into a spec critique, retry blindly in a loop, or record a validator status yourself. propose_adjudicate and snodo authorize do not repair a validator failure.

Operator: Involve the operator when the validator/provider issue needs configuration, credentials, or runtime repair. snodo authorize is not needed for this outcome.

internal_error

Means: The engine encountered an internal fault; it is not a verdict on the task.

Next: Inspect get_job_logs and the halt details, preserve the job ID and evidence, and surface the fault for diagnosis. Once the cause is understood and resolved, retry_job can retry a task job with its original spec by default; the retry continues from the prior attempt's worktree, including uncommitted changes. Pass fresh_start: true to retry_job when a clean start from main is intentional. If the prior worktree and branch are unavailable, retry starts from main. The spec remains rooted in the original task; use append_spec only for additional guidance. The retry preserves the same task identity and available worktree contents. Use a follow-up task only for a deliberate code change, not to disguise the engine fault.

Never: Treat it as a blocker, rewrite the spec to address an engine fault, retry blindly in a loop, or record a validator status in the validators' place.

Operator: Escalate the engine fault to the operator for diagnosis or repair. snodo authorize is not an internal-error recovery action.

environment_error

Means: The coder could not be invoked because the execution environment is not ready (for example, a required executable or container runtime is missing). It is the fifth canonical halt, but not one of the validation results returned by validate_task, which does not invoke a coder.

Next: Inspect the error hint and get_job_logs; repair the environment where the job runs. Then retry the task with retry_job and the unchanged recorded spec. The task ID/worktree reuse keeps earlier work available for inspection. If the fault is not repaired, stop and report it rather than spawning more attempts.

Never: Classify it as a task blocker or validator failure, alter the spec to mask a missing runtime, retry blindly in a loop, or record a verdict/status for validators.

Operator: The operator is needed to install or configure the missing runtime/credential or otherwise repair the execution environment. This does not call for snodo authorize.

Job outcomes beyond the five halts

From a non-completed status to its cause

Start with the job ID and task ID shown by the run. The job record, task record, session checkpoint, and audit log answer different questions; use the CLI views below rather than treating the printed halt payload as the only copy.

Status or signal Where to look What to do next
Job is failed, or its exit code is non-zero snodo job status <job_id> for status/exit code and snodo job logs <job_id> for stdout/stderr; then snodo task show <task_id> for the halt and failure context Read final_decision, halt_type, phase, reason/hint, and each validator result's severity. Repair the cause before retrying.
A structured halt was printed The job's .snodo/jobs/<job_id>/state.json stores it under halt; the task's .snodo/tasks/<task_id>/state.json also retains local halt details. snodo task show <task_id> presents the task halt and failure record Use the halt type and phase to distinguish a validator decision from execution/environment or engine trouble. For environment_error, no synthetic failure context is created, so use the halt/error details and logs to diagnose the environment.
Task context or the session record is needed snodo task show <task_id> reads the active session checkpoint, including decisions.halt and decisions.task_failure; session details are available through snodo session show <session_id> Use the recorded spec, attempt, branch, and validator context to decide whether to retry, repair, adjudicate, or create a separate follow-up.
You need historical/audit evidence .snodo/audit.log records append-only events such as spec_replaced and task_unmerged; snodo meta <job_id> shows the job's metadata Correlate the task/job IDs and event history with the current job and task records.
Task is resolved but not on the base branch snodo job status <job_id>, snodo job logs <job_id>, snodo task show <task_id>, and the task_unmerged audit event; the run reports the reason and branch/worktree Preserve the work and merge the printed branch by hand when there is a branch (git merge <branch>); for degraded isolation, inspect and commit the changes in the working tree instead. Do not rerun the task just to merge it.

The terminal CLI run exit codes are 0 for completed, 1 for failed (including preflight or isolation failure), and 2 for completed but not merged. A job wrapper reports code 2 as unmerged; use both the status and exit code when diagnosing a record.

Job status and validator outcome are different fields. Check both get_job_status's status and exit_code; for plan jobs also inspect per-task statuses (for example with get_plan). get_job_logs provides stdout/stderr context. A job's terminal status alone does not prove that all work passed or landed.

unmerged

Means: The job wrapper assigns unmerged when the run exits with code 2: the work finished, but did not land on the base branch. It is not a new halt or a validator verdict. In a plan, unmerged can also be the task status when an eligible auto-merge attempt failed. A plan task marked completed may also have work that is not on the base branch when auto-merge was disabled; check the plan/job detail rather than infer merge from that word alone.

Resolved work can remain unmerged when the configured delivery mode does not use local auto-merge, the closure did not resolve, isolation degraded or no task worktree was created, the task branch has no changes, or SNODO_BENCHMARK=1. When a branch exists, the run prints its name and git merge <branch>; when isolation degraded, it says the changes are in the working tree and to commit them there. A plan task may also be unmerged after an eligible auto-merge attempt fails.

Next: Inspect job status, logs, and the plan/task details. Keep the branch and work available for review; arrange a corrective follow-up task if the merge conflict or remaining work needs code changes. If the task branch itself needs resolution, have the operator resolve it and then re-evaluate/merge the work under the normal process.

Never: Claim the change landed, rerun the task just to merge it, discard the unmerged work, or mark it completed in place of actual validator/merge evidence. Do not retry blindly; a retry is for re-running a task, not merging an existing branch.

Operator: The operator is needed when resolving a merge conflict or deciding how to land the retained branch. snodo authorize applies only if a separate validator escalate requires adjudication.

Retrying a task

Retry only after inspecting the corresponding halt, logs, and task context and addressing the diagnosed cause. For the CLI task retry (snodo run --retry <task_id> or snodo job retry <job_id> [description]), the spec choices are:

Retry form Spec behavior
Bare --retry <task_id> Keeps the recorded spec unchanged.
Positional description or --append-spec <guidance> on snodo run --retry Adds guidance for this retry while retaining the existing spec. A positional description on snodo job retry has the same guidance behavior.
--replace-spec <new_spec> Replaces the existing spec. The superseded spec is retained in failure context when present and recorded as a spec_replaced audit event. snodo job retry also supports this option.

The CLI refuses --append-spec together with --replace-spec, and refuses a positional description together with --replace-spec; append/replace flags without a retry are also rejected on snodo run. A retry for a task that has no failure context but has a verified unmerged branch attempts to merge that existing work instead of rerunning the coder.

For MCP retry_job, the existing spec is used by default, and the retry continues from the previous attempt's worktree so its changes remain available. Pass fresh_start: true when a clean start is intended. append_spec adds guidance; revised_spec is for an actual specification correction.

A completed job with a non-zero exit_code

Means: This is an inconsistent success signal. The job wrapper in the current code writes completed for exit code 0, unmerged for exit code 2, and failed for other non-zero exit codes. Therefore it does not normally produce completed with a non-zero code. If an orchestrator sees that combination (for example in a mixed or stale record), it must not infer success from completed alone.

Next: Re-read get_job_status and inspect get_job_logs; for plan jobs, inspect each task's recorded result too. Preserve and report the conflicting signals. After identifying the actual failure and fixing its cause, use retry_job for a retryable task failure, or create a focused follow-up task when additional judged work is required.

Never: Declare success, set a status to completed on behalf of validators, or repeatedly retry without understanding the non-zero exit. Do not use propose_adjudicate to resolve a process exit code.

Operator: Ask the operator to investigate when the status and exit code remain inconsistent or the cause requires environment/engine repair. snodo authorize is needed only if independent validator output is escalate, never just because the exit is non-zero.

Keep status and judgement separate

record_task_status records an operator's account outside the engine loop; it does not create a validator verdict, prove that work ran, or prove that it merged. Never use it to manufacture a pass or to replace the validators' status. Use a focused, judged follow-up task for new corrective work, and do not call retry_job repeatedly without first inspecting the outcome and resolving its cause.