Machine interface¶
Snodo's first committed machine interface (ADR 022). This is a contract, not a convenience: field names and exit codes are stable, and a breaking change bumps the schema version so a consumer can detect it before parsing.
Conventions¶
--jsonis additive. Human output is unchanged;--jsononly changes what is written to stdout.- The delayed CLI wave indicator writes only to stderr; it does not add output
to stdout or contaminate a
--jsondocument. - Every
--jsoncommand writes a single JSON object to stdout. Errors are emitted to stdout as that object, so stdout is always one parseable document. - Every payload carries a
schemafield of the formsnodo.<command>.v<N>. A consumer must check this field first; a mismatch means the payload shape changed and the consumer should refuse to parse rather than misread it. - Field names are asserted by the test suite. A rename fails the suite rather than a downstream consumer.
Commands¶
snodo config validate --json¶
Schema: snodo.config.validate.v1. Emits {schema, ok, findings}; each
finding contains path, section, message, and hint. Secret values are
never included. Exit code is 0 for valid configuration and 1 when findings
are present.
snodo status --json¶
Schema: snodo.status.v1
| Field | Type | Meaning |
|---|---|---|
schema |
string | snodo.status.v1 |
ok |
bool | always true on success |
project_root |
string | absolute project root |
protocol |
object | {id, name} from protocol.yml |
mode |
string | null | active mode id, or null |
active_session |
string | null | active session id, or null |
last_run |
object | null | {session_id, mode, updated_at, outcome} |
snodo mode show --json¶
Schema: snodo.mode.v1
| Field | Type | Meaning |
|---|---|---|
schema |
string | snodo.mode.v1 |
ok |
bool | always true |
mode |
string | null | active mode id, or null |
name |
string | null | display name, or null |
active_session |
string | null | active session id for this mode |
snodo session show <id> --json¶
Schema: snodo.session.v1
| Field | Type | Meaning |
|---|---|---|
schema |
string | snodo.session.v1 |
ok |
bool | true on success |
session_id |
string | session id |
mode |
string | session mode |
project_root |
string | project root |
project_id |
string | project identity |
created_at |
string | ISO timestamp |
updated_at |
string | ISO timestamp |
checkpoint |
object | {current_task, decisions, memory_summary} |
snodo task show <id> --json¶
Schema: snodo.task.v1
| Field | Type | Meaning |
|---|---|---|
schema |
string | snodo.task.v1 |
ok |
bool | true on success |
task_id |
string | task id |
session_id |
string | session holding the record |
mode |
string | session mode |
halt |
object | null | the halt payload, or null |
failure |
object | null | the failure context, or null |
spec |
string | null | the task spec |
For a task that is still running (no halt/failure record yet, a live run
record on disk) the command answers ok: true with status: "running", null
halt/failure, and a watch field pointing at the live surface rather than
a record. This distinguishes a running task from an unknown one, which keeps
returning the No record for task <id> error.
The halt payload carries an attempts summary alongside the final
validator_results: total, coder_dispatches, and a bounded history of
{attempt, outcome} entries (ADR 045). A consumer can therefore tell a
first-time pass from a hard-won one, and count coder dispatches, without
parsing prose. The summary is additive to the payload.
snodo worktree list --json¶
Schema: snodo.worktree.v1
| Field | Type | Meaning |
|---|---|---|
schema |
string | snodo.worktree.v1 |
ok |
bool | always true |
project_root |
string | project root |
worktrees |
array | [{task_id, path, age_days}] |
snodo queue remove <plan> --json¶
Schema: snodo.queue.remove.v1
| Field | Type | Meaning |
|---|---|---|
schema |
string | snodo.queue.remove.v1 |
ok |
bool | true when the plan was removed |
plan |
string | Removed plan name |
queue |
string | Queue the plan was removed from |
snodo validate <task_spec> [--phase pre_execute|post_execute] [--mode <m>]¶
Schema: snodo.validate.v1
Runs the phase's validators through the shared engine runner and returns the
validation-outcome result without running a coder. The shape mirrors the
engine's halt payload. The engine's canonical vocabulary is five (ADR 015);
environment_error is an execution halt, and because this command never
invokes a coder, it is not one of the outcomes returned here.
| Field | Type | Meaning |
|---|---|---|
schema |
string | snodo.validate.v1 |
ok |
bool | true on success |
status |
string | pass | escalate | blocker | validator_error |
task_id |
string | derived task id |
phase |
string | the phase validated |
mode |
string | the mode validated |
results |
array | [{validator_id, severity, justification}] |
policy_decision |
object | null | the policy decision |
instruction |
string | follow-up instruction |
The same command accepts --base <ref> --head <ref> to validate the git
three-dot diff, or --pr <number> to resolve that range through the installed
code-host provider. Ref ranges are resolved locally with git. JSON output and
the exit codes below are identical to task-spec validation.
See Running Snodo in CI for GitHub Actions and GitLab CI
examples that use this interface.
snodo ready --json¶
Schema: snodo.ready.v2
The readiness payload adds extensions, keyed by entry-point group
(snodo.providers, snodo.validators, snodo.predicates, snodo.coders).
Each value maps installed entry-point names to {status, error} records;
status is installed or failed, and failed entries include their load
error. The schema version is bumped because this is a payload-shape change.
snodo survey --json¶
Schema: snodo.survey.v1
Reports one repository in one of two shapes, depending on whether a protocol
already exists. The analysis object is the same in both; analysis.drift is
present only for a repository that already has a protocol.
| Field | Type | Meaning |
|---|---|---|
schema |
string | snodo.survey.v1 |
ok |
bool | true on success |
project_root |
string | absolute project root |
project_id |
string | project identity |
scope |
string | remote | local | override |
display_name |
string | repository directory name |
analysis |
object | discovered modules, languages, tooling, decision records, test command, findings, agent judgements and the judgements not made |
analysis.drift |
object | governed repositories only — what the protocol claims against what the code shows |
analysis.drift:
| Field | Type | Meaning |
|---|---|---|
protocol_id |
string | the protocol surveyed |
uses_modules |
bool | whether the protocol declares modules: at all |
shape |
string | which shape the report is reading, in prose |
summary |
string | one-line account of what was weighed |
checks_made |
int | comparisons that were actually carried out (agreements + divergences) |
has_divergences |
bool | whether any comparison diverged |
agreements |
array | [{check, statement, subject, evidence}] — comparisons that agreed |
divergences |
array | [{check, subject, claim, observation, evidence}] — where the two differ; claim restates the protocol, observation restates the code, and neither names a fault |
not_compared |
array | [{check, reason}] — comparisons this protocol's shape does not support, and why |
check is one of protocol-module-paths, module-coverage,
decision-records, test-commands, validator-tooling. A protocol that
declares no modules (the common shape) gets no module-level comparisons at all:
the three module checks appear in not_compared with their reason, and no
discovered boundary is reported as ungoverned.
snodo survey exits 0 whenever it produced a report, including a report
that found drift: a comparison that diverged is a true observation, not a
judgement that either side is at fault, and survey adjudicates nothing. A
caller distinguishes the outcomes from the payload, not the exit code:
| Situation | Exit | Payload |
|---|---|---|
| ungoverned repository | 0 | analysis without a drift field |
| governed, no divergence | 0 | analysis.drift.has_divergences: false |
| governed, divergence found | 0 | analysis.drift.has_divergences: true |
| no git repository, or a protocol that will not load | 4 | ok: false, error |
Code 4 therefore means "no report was produced" and nothing else. Surveying a
governed repository is not an error state, and 1/2/3 are validation
outcomes that survey does not emit: survey proposes and reports, it does not
judge work.
snodo intake --json¶
Schema: snodo.intake.v1
Reports the validator criteria snodo intake would offer, drawn from the
repository's decision records. It writes nothing and prompts for nothing —
a machine can see the proposals, but acceptance stays a human act; the write
path is entered only by the interactive command or an explicit --accept-all.
| Field | Type | Meaning |
|---|---|---|
schema |
string | snodo.intake.v1 |
ok |
bool | true on success |
project_root |
string | absolute project root |
protocol_id |
string | the protocol the criteria would be added to |
proposals |
array | [{criterion, record_path, record_title, record_section}] — each rule and the record it came from |
written |
bool | always false |
record_path is a repository-relative citation resolved against the
repository before the proposal is built; a criterion whose record cannot be
found is never proposed. A non-zero exit from intake --json means no
proposals could be produced (no git repository, no protocol, or an unloadable
one), and it writes nothing.
snodo models --json¶
Schema: snodo.models.v1
Lists discovered models without the human table. With no --provider, the
models array is empty and providers lists configured providers with usable
credentials. With a provider, models contains the discovery records; filters
are applied before the payload is emitted.
| Field | Type | Meaning |
|---|---|---|
schema |
string | snodo.models.v1 |
ok |
bool | true on success |
provider |
string | null | selected provider, or null when listing providers |
providers |
array | configured providers when no provider was selected |
models |
array | discovered model records, or [] when none match |
snodo models --stats --json¶
Schema: snodo.models-stats.v1
Returns recorded usage aggregates without the human tables. Raw duration arrays and call counts remain available so a consumer can choose its own aggregation.
| Field | Type | Meaning |
|---|---|---|
schema |
string | snodo.models-stats.v1 |
ok |
bool | true when the stats report was produced |
project_root |
string | null | project whose records were read |
provider |
string | null | provider filter, if supplied |
total_jobs |
int | jobs found in the project |
models |
object | model id to usage record, including calls, token totals, durations, costs and roles |
coders |
object | coder id to job totals and durations |
snodo runs --json¶
Schema: snodo.runs.v1
Emits the accumulated completed task run records. A task is one row; background job copies are not emitted as additional rows. Measurements that were not recorded are omitted, while measured zeroes remain zeroes.
| Field | Type | Meaning |
|---|---|---|
schema |
string | snodo.runs.v1 |
ok |
bool | true when the report was produced |
project_root |
string | absolute project root |
runs |
array | completed run records, each with task_id, completed_at, and recorded cost fields |
snodo models --benchmark --json¶
Schema: snodo.models-benchmark.v1
Runs the same fixed prompt and performs the same measurements as human mode,
but emits one JSON object instead of the intent and report prose. A failed API
call is still a sample, so attempted_runs can exceed succeeded_runs.
Aggregates in statistics cover successful samples only; consumers that need
to combine runs retain the individual samples.
| Field | Type | Meaning |
|---|---|---|
schema |
string | snodo.models-benchmark.v1 |
ok |
bool | true when at least one run succeeded |
model |
string | fully qualified model selected for the benchmark |
snodo_version |
string | snodo version that produced the result |
prompt |
object | {file, chars, sha256} identity of the measured prompt |
attempted_runs |
int | requested and attempted calls |
succeeded_runs |
int | calls that returned a measurement |
samples |
array | one {run, ok, ...metrics} object per attempt; failures have error |
statistics |
object | successful-sample-only median and mean for first-token and throughput metrics |
Each successful sample preserves output_tokens, prompt_tokens,
counts_basis, time_to_first_token, wall_seconds, decode_tok_per_sec,
and overall_tok_per_sec. --json without --benchmark therefore has the
separate listing or stats schema above; it never mixes those payloads with a
benchmark result.
Exit codes¶
snodo validate (and any command that returns a validation outcome) uses exit
codes that distinguish the validation outcomes, so a caller can branch without
parsing prose:
| Exit code | Outcome |
|---|---|
| 0 | pass |
| 1 | blocker |
| 2 | escalate |
| 3 | validator_error |
| 4 | internal_error |
No command adds a sixth code. environment_error is an engine execution halt
(ADR 015), not a validation outcome, so it has no code here. snodo survey deliberately uses none of the
judgement codes (1–3): it adjudicates nothing, so a governed repository whose
protocol has diverged from its code exits 0 like any other successful run
and reports the divergence in its payload
(analysis.drift.has_divergences). A non-zero exit from survey means no report
was produced at all — see snodo survey --json above.
Error shape¶
When a --json command cannot produce its normal payload (not inside a
project, missing argument, unknown id), it emits a uniform error object and a
non-zero exit code:
{
"schema": "snodo.<command>.v1",
"ok": false,
"error": "human-readable reason"
}
MCP tool reference¶
MCP tools are exposed according to the active mode's capability grant. The dispatch surface provides the following task loop:
validate_tasktakes requiredtask_id, with optionaltask_specandplan_name. It runs pre-execute validators and reports the quorum outcome without running a coder. A passing validation records a single-use token; the nextdispatch_taskconsumes it as the audit link. The caller does not hold a token, and no tool call is refused for lack of one.dispatch_tasktakes requiredtask_spec, with optionalcoding_modelandmodule. It returns a job id for background execution. Useget_job_status,get_job_logs, orwatch_jobto follow the job;list_jobslists project jobs.retry_jobtakes requiredjob_id. By default it keeps the recorded specification and continues from the previous attempt's worktree and changes.fresh_start: truestarts from main instead.append_specadds guidance to the existing specification;revised_specreplaces it, with the replaced specification audited asspec_replacedand recoverable.
The read-only project diagnostic tools are always available, independent of mode capabilities:
surveyreports a repository survey. Its optionalagent: trueallows configured agent boundary judgements and may call models; the default is a deterministic pass without a model call.intakereports proposed validator criteria and never writes or prompts.readyreports a project readiness assessment. Optionalmodefilters displayed findings; optionalprotocolselects the protocol file (default.snodo/protocol.yml). It does not append an audit event.protocol_schematakes no arguments and returns the versionedsnodo.protocol.v1JSON Schema publication also emitted bysnodo protocol schema --json. It does not read or write the project's.snodo/protocol.yml.
The pr capability exposes vendor-neutral change-request tools:
create_change_request creates one from branch, title, and body;
read_change_request_diff and read_change_request_discussion read by
change_request_id; post_change_request_comment takes that id and comment;
approve_change_request approves by id;
request_change_request_changes takes the id and a reason; and
merge_change_request merges by id. The older create_pr, read_pr_diff,
post_review_comment, approve_pr, reject_pr, and merge_pr names remain
available as deprecated aliases, respectively.