docs/agent-contract.md
Machine-readable surfaces of the openspec CLI, verified against src/ (capstone audit, 2026-06-11). Every shape below is documented from the emitting code.
--json mode, stdout carries exactly one JSON document (2-space pretty-printed). Human prose, spinners, and the store banner go to stderr.Using OpenSpec root: <id> (<path>) to stderr. Never printed in JSON mode.snake_case; workflow payloads (status, instructions, new change, validate, list) use camelCase, except the embedded root object, which always uses store_id.root.store_id, member.path). Exceptions that use explicit null are called out per shape (store doctor git.*, failure payloads).One envelope shape is shared by every machine-readable diagnostic (StoreDiagnostic):
{
"severity": "error" | "warning" | "info",
"code": "snake_case_string",
"message": "human sentence",
"target": "dotted.surface (optional)",
"fix": "one actionable sentence/command (optional)"
}
Diagnostics appear in two positions: status arrays (status: StoreDiagnostic[] at top level or per entry) for health findings, and thrown errors converted to a single-element status array on command failure.
RootOutputAll root-resolving commands (list, show, validate, status, instructions, instructions apply, instructions archive, new change, archive, doctor, context) resolve one OpenSpec root with one precedence:
--store <id> → the registered store's root (source: "store").openspec/: planning shape → source: "nearest" (a store: pointer is ignored with a stderr warning); config-only dir with a valid store: pointer → that store, source: "declared".defaultStore set (openspec config set defaultStore <id>) → that store, source: "global_default"; a stale id fails with the underlying store error and a fix naming openspec config unset defaultStore.no_root_with_registered_stores.source: "implicit"; diagnostic commands (doctor, context) fail with no_openspec_root instead — they inspect, never scaffold.Successful JSON payloads embed the root:
"root": { "path": "/abs/path", "source": "store" | "declared" | "global_default" | "nearest" | "implicit", "store_id": "id (only when store-selected)" }
Root-failure contract: in JSON mode a resolution failure prints { ...commandNullShape, "status": [diagnostic] } on stdout and exits 1.
list --json{ "changes": [ { "name", "completedTasks", "totalTasks", "lastModified", "status": "no-tasks"|"complete"|"in-progress" } ], "root": RootOutput } — note the per-change status is a string enum here. --specs: { "specs": [ { "id", "requirementCount" } ], "root" }.
show <item> --jsonChange: { "id", "title", "deltaCount", "deltas": [...], "root" }. Spec: { "id", "title", "overview", "requirementCount", "requirements": [...], "metadata": { "version", "format", "sourcePath"? }, "root" }.
validate --json{ "items": [ { "id", "type": "change"|"spec", "valid", "issues": [ { "level", "path", "message", "line"?, "column"? } ], "durationMs" } ], "summary": { "totals": {items,passed,failed}, "byType": {...} }, "version": "1.0", "root" }. Exit 1 when any item fails.
status --json{ "changeName", "schemaName", "planningHome"?: { "kind", "root", "changesDir", "defaultSchema" }, "changeRoot", "artifactPaths": { "<id>": {outputPath, resolvedOutputPath, existingOutputPaths} }, "nextSteps": ["..."], "actionContext": { "mode": "repo-local", "sourceOfTruth": "repo", "planningArtifacts", "linkedContext", "allowedEditRoots", "requiresAffectedAreaSelection", "constraints" }, "isPlanningComplete", "isComplete", "applyRequires", "artifacts": [ {id, outputPath, status: "done"|"skipped"|"ready"|"blocked", requires, missingDeps?} ], "root" }. isPlanningComplete means every non-skipped planning artifact exists; skipped artifacts count as satisfied without being created. It does not mean implementation tasks are complete. isComplete is retained as a compatibility alias with the same value. Each artifact's requires is its direct dependency ids (present for every status, so the transitive required set is computable even when the artifact is done); missingDeps appears only when blocked. The artifacts array is in dependency order, with the schema's artifacts: declaration order breaking ties between artifacts that become ready at the same time (never alphabetical), so the first ready entry is the artifact to write next; missingDeps uses that same order. "skipped" marks an artifact whose generates path is under specs/ in a change whose .openspec.yaml declares skip_specs: true; it satisfies dependencies but must not be created. No active changes: { "changes": [], "message", "root" }, exit 0.
instructions <artifact> --json{ "changeName", "artifactId", "schemaName", "changeDir", "planningHome"?, "outputPath", "resolvedOutputPath", "existingOutputPaths", "description", "instruction"?, "context"?, "rules"?, "references"?: ReferenceIndexEntry[], "skipped"?, "warning"?, "template", "dependencies": [{id,done,path,description,skipped?}], "unlocks", "root" }. unlocks lists the artifacts this one makes ready, in the schema's declaration order (the same order status recommends them). "skipped": true (with "warning") appears when the change declares skip_specs: true and this artifact is skipped — do not create its files. A dependency entry with skipped: true is satisfied without files — do not try to read its paths.
ReferenceIndexEntry: { "store_id", "root"?, "specs"?: [{id,summary}], "fetch"?, "status": [] } — resolved entries carry root/specs/fetch; unresolved carry store_id + warning status. Index capped at 50KB (reference_index_truncated).
instructions apply --json{ "changeName", "changeDir", "schemaName", "contextFiles": { "<artifactId>": ["/abs", ...] }, "progress": {total,complete,remaining}, "tasks": [{id,description,done}], "state": "blocked"|"all_done"|"ready", "missingArtifacts"?, "instruction", "references"?, "context"?, "operationGuidance"?, "root" }. Both optional fields are read from the selected root on every invocation. context is a required prompt-level input whose relevant project facts, conventions, and constraints must be applied; operationGuidance is advisory input whose entries are followed only when applicable and compatible with the built-in workflow. Both remain separate from state, tasks, progress, context files, and the built-in instruction.
instructions archive --json{ "changeName", "context"?, "operationGuidance"?, "root" }. Requires a valid --change in the resolved repo/store root and uses the same required-context/advisory-guidance semantics as apply. This is a read-only runtime-input surface: it does not return the static archive workflow, inspect or merge delta specs, write main specs, or move the change.
new change <name> --jsonSuccess: { "change": { "id", "path", "metadataPath", "schema" }, "root" }. Failure: { "change": null, "status": [d] }, exit 1.
archive <name> --jsonSuccess: { "archive": { "change", "archivedAs": "YYYY-MM-DD-name", "path", "specsUpdated", "totals"?, "warnings"? }, "root" }. Failure: { "archive": null, "root"?, "status": [d] }, exit 1. specsUpdated is true only when at least one spec file was written or retired (a capability whose last requirement the change removed has its spec deleted, which requires retire_capabilities: true in the change's .openspec.yaml; every retirement is named in warnings, with a pasteable Git recovery command only when the spec lived in the caller's checkout); an already-synced change archives with all-zero totals and the skips listed in warnings. JSON mode is strictly non-interactive: every prompt point becomes an archive_* code.
doctor --json{ "root": { "path", "source", "store_id"?, "healthy", "status": [] }, "store": { "id", "metadata": {present,valid,remote?}, "origin_url"?, "drift"?: {ahead,behind}, "status": [] } | null, "references": [...], "status": [] }. drift (present only for a git-backed store checkout that has an upstream tracking ref) is ahead/behind counts against the last-fetched upstream, not the live remote. Health findings of any severity exit 0. Failure payload: { "root": null, "store": null, "references": [], "status": [d] }, exit 1.
context --json{ "root": { "path", "source", "store_id"?, "role": "openspec_root" }, "members": [ { "role": "referenced_store", "id", "path"?, "remote"?, "fetch"?, "status": [] } ], "status": [] }. AVAILABLE = path present AND status empty. --code-workspace <path> writes {folders:[{name,path}]} (available referenced stores only, ref: prefixes); in JSON mode the write runs before printing so stdout holds exactly one document even on write failure. Failure: { "root": null, "members": [], "status": [d] }, exit 1.
store ... --jsonsetup/register: { "store": {id, root, metadata_path?}, "registry": {path, registered, already_registered}, "git": {is_repository, initialized, committed}, "created_files": [], "status": [] }. unregister/remove: { "store", "registry": {path, removed}, "files": {deleted, deleted_path, left_on_disk}, "status": [] }. list: { "stores": [{id, root}], "status": [] }. doctor: { "stores": [ { id, root, metadata_path?, openspec_root: {...healthy, status}, metadata: {present, valid, id?, remote}, git: {is_repository, has_commits, has_uncommitted_changes, has_remote, origin_url}, status } ], "status": [] } (null = unknown/not probed). Health findings exit 0; failures exit 1 with the matching null-shape. Prompt cancellation exits 130.
schemas --json / templates --jsonschemas: bare array [ {name, description, artifacts, source} ]. templates: keyed object { "<artifactId>": {path, source} }. Both cwd-based, no root/status keys.
| Situation | Exit | Stdout |
|---|---|---|
| Success, incl. health findings (doctor/context/store doctor) | 0 | the payload |
Command failure in --json mode | 1 | one JSON document with status: [d] and the command's null-shape |
validate with failing items | 1 | full report |
Prompt cancellation (store group, human mode) | 130 | stderr only |
no_openspec_root, no_root_with_registered_stores, no_registered_stores, unknown_store, store_identity_mismatch, unhealthy_store_root, store_path_not_supported, invalid_store_pointer, initiative_option_removed, areas_option_removed; pass-through: invalid_store_id, invalid_store_registry, invalid_store_metadata.
openspec_store_root_missing, openspec_store_root_not_directory, openspec_root_missing, openspec_root_not_directory, openspec_config_missing, openspec_config_not_file, openspec_specs_not_directory, openspec_changes_not_directory, openspec_archive_not_directory. During the stores beta, openspec/specs/, openspec/changes/, and openspec/changes/archive/ may be absent in a healthy root; they are only health errors when present but not directories.
invalid_store_id, invalid_store_registry, invalid_store_metadata, store_registry_busy, store_not_found, no_store_registry, store_registry_changed, store_metadata_missing, store_metadata_id_mismatch, store_metadata_invalid, store_id_conflict, store_path_conflict, store_already_registered (info).
store_setup_id_required, store_setup_path_required, store_setup_path_not_directory, store_setup_inside_git_repo, store_setup_non_empty_directory, store_setup_cancelled, store_path_required, store_path_missing, store_path_not_directory, store_root_pointer_declared, store_register_root_unhealthy, store_register_identity_confirmation_required, store_register_cancelled, store_remote_empty, store_remote_requires_hand_edit, store_remove_confirmation_required, store_remove_cancelled, store_remove_path_not_directory, store_remove_metadata_missing, store_root_missing (warning in remove, error in doctor), store_root_not_directory.
store_git_init_failed, store_git_identity_missing, store_git_commit_failed, store_git_no_commits (warning), store_clone_fragile_directories (warning), store_remote_divergence (info, doctor), store_checkout_drift (info, doctor).
reference_invalid_id, reference_registry_unreadable, reference_unresolved, reference_root_unhealthy, reference_index_truncated.
relationship_registry_unreadable, root_pointer_ignored, root_pointer_invalid, pointer_declarations_inert.
archive_change_name_required, archive_change_not_found, archive_change_symlink, archive_validation_failed, archive_confirmation_required, archive_tasks_incomplete, archive_spec_update_failed, archive_spec_validation_failed, archive_target_exists, archive_error.
context_file_exists, context_output_dir_missing.
doctor_failed, context_failed, store_error, change_error, archive_error.
Recorded by the capstone audit; published-key renames are product decisions deferred past this release:
--json mode, several failure paths printed stderr only with no JSON document.show/validate unknown and ambiguous items emit {status:[{code: unknown_item | ambiguous_item, ...}]}; thrown errors in status/instructions/list/show/validate route through the JSON-aware failure helper (the command's null-shape + status); store <unknown subcommand> --json emits {status:[{code: unknown_store_subcommand}]}; list carries its {changes|specs: [], root: null} null-shape on resolution failures.store_root_missing is emitted with two severities (warning in remove, error in store doctor) — context-dependent, documented above.root.store_id is snake_case everywhere.target.list --json reuses the status key as a string enum per change.validate output carries a version field.schemas/templates ignore root selection (cwd-based, no --store).change/spec subcommands) emit unenveloped payloads without root/status.