Rasen Agent Contract
Machine-readable surfaces of the rasen CLI, verified against src/ (capstone audit, 2026-06-11). Every shape below is documented from the emitting code.
1. General conventions
- One JSON document per invocation. In
--jsonmode, stdout carries exactly one JSON document (2-space pretty-printed). Human prose, spinners, and the store banner go to stderr. - Store banner. In human mode, a store-selected root prints
Using Rasen root: <id> (<path>)to stderr. Never printed in JSON mode. - Key casing is surface-dependent (see Known inconsistencies): store/doctor/context payloads use
snake_case; workflow payloads (status,instructions,new change,validate,list) usecamelCase, except the embeddedrootobject, which always usesstore_id. - Optional keys are omitted, not null, in most payloads (e.g.
root.store_id,member.path). Exceptions that use explicitnullare called out per shape (store doctorgit.*, failure payloads).
2. The diagnostic envelope
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.
3. Root selection and RootOutput
All root-resolving commands (list, show, validate, status, instructions, instructions apply, new change, archive, doctor, context) resolve one Rasen root with one precedence:
--store <id>→ the registered store's root (source: "store").- Otherwise, nearest ancestor with
rasen/: planning shape →source: "nearest"(astore:pointer is ignored with a stderr warning); config-only dir with a validstore:pointer → that store,source: "declared". - No nearest root + registered stores exist → error
no_root_with_registered_stores. - No root, no stores: scaffolding commands treat the cwd as
source: "implicit"; diagnostic commands (doctor,context) fail withno_openspec_rootinstead — they inspect, never scaffold.
Successful JSON payloads embed the root:
"root": { "path": "/abs/path", "source": "store" | "declared" | "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.
4. Command JSON shapes
4.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" }.
4.2 show <item> --json
Change: { "id", "title", "deltaCount", "deltas": [...], "root" }. Spec: { "id", "title", "overview", "requirementCount", "requirements": [...], "metadata": { "version", "format", "sourcePath"? }, "root" }.
4.3 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.
4.4 status --json
{ "changeName", "schemaName", "planningHome"?: { "kind", "root", "changesDir", "defaultSchema" }, "changeRoot", "artifactPaths": { "<id>": {outputPath, resolvedOutputPath, existingOutputPaths} }, "nextSteps": ["..."], "actionContext": { "mode": "repo-local", "sourceOfTruth": "repo", "planningArtifacts", "linkedContext", "version": 1|2, "planningWriteRoots", "codeWriteRoots", "readRoots", "allowedEditRoots"?, "requiresAffectedAreaSelection", "constraints" }, "isComplete", "applyRequires", "artifacts": [ {id, outputPath, status: "done"|"ready"|"blocked", missingDeps?} ], "root" }. No active changes: { "changes": [], "message", "root" }, exit 0.
4.5 instructions <artifact> --json
{ "changeName", "artifactId", "schemaName", "changeDir", "planningHome"?, "outputPath", "resolvedOutputPath", "existingOutputPaths", "description", "instruction"?, "context"?, "rules"?, "references"?: ReferenceIndexEntry[], "template", "dependencies": [{id,done,path,description}], "unlocks", "root" }.
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).
4.6 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"?, "root" }.
4.7 new change <name> --json
Success: { "change": { "id", "path", "metadataPath", "schema" }, "root" }. Failure: { "change": null, "status": [d] }, exit 1.
4.8 archive <name> --json
Success: { "archive": { "change", "archivedAs": "YYYY-MM-DD-name", "path", "specsUpdated", "totals"? }, "root" }. Failure: { "archive": null, "root"?, "status": [d] }, exit 1. JSON mode is strictly non-interactive: every prompt point becomes an archive_* code.
4.9 doctor --json
{ "root": { "path", "source", "store_id"?, "healthy", "status": [] }, "store": { "id", "metadata": {present,valid,remote?}, "origin_url"?, "status": [] } | null, "references": [...], "status": [] }. Health findings of any severity exit 0. Failure payload: { "root": null, "store": null, "references": [], "status": [d] }, exit 1.
4.10 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.
4.11 store ... --json
setup/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.
4.11a Store membership
Membership is roster and eligibility only. It says which projects belong to
a store and whether each one plans in it, shares knowledge with it, or both. It
does not determine, imply, or stand in for the decision of where a change is
implemented — no agent may read roles.planning (or any future role) as an
instruction about where to do work. Where a project plans is its own store:
declaration; where a change is implemented is a separate question this contract
does not answer.
Authority is the store's own record, one file per member project:
<store>/.rasen-store/projects/<projectId>.yaml, keyed by the project's
permanent identity. The project's storeMemberships: list is a locator only
and never confers membership; a hint that disagrees with the record is drift.
doctor --json carries membership: { project_id?, stores: [ { uid?, id?, sources: ["hint"|"record"], roles?: {planning, knowledge}, provenance?, unavailable?: {reason, repair} } ] }. A declared store that is not available on
this machine appears with unavailable set — never omitted. Absent from the
list must not be read as "not a member".
store add-project --json carries membership (per-repository store_writes /
project_writes, repair_needed, suggested_commits) and planning_binding
(requested, changed, refused, already_bound, bound_to,
requested_store, rebind_command) as two SEPARATE blocks, because they are
two separate relations. store migrate-membership --json carries
{ store, applied, converted[], unresolved[], store_writes, legacy_manifest_removed, legacy_manifest_path, suggested_commits, status }.
No membership command stages, commits, pushes, fetches, or pulls. Each renders a path-scoped commit suggestion per repository for the user to run.
4.11b Session runtime context and the action context
A supervised session records what it resolved — the planning space, the project
it works on, and the exact checkout of that project on this machine — and hands
its child process RASEN_SESSION_CONTEXT, the absolute path to a
machine-local sessions/<sessionId>/context.json under the global data dir.
The path, never the document. The file carries
{ version, sessionId, planning: {type:'project'|'store', …, root}, execution: {kind:'planning-only'} | {kind:'project', projectId, root, home?} }.
A file that is missing, unparseable, or names a different session is REPORTED;
no reader falls back to deriving context from the working directory.
Context resolution order for a first command: an explicit selector, then the session context, then the working directory and its nearest pointer. A resumed frozen run uses a different rule: the frozen identity is the authority for WHICH project, the session context (or the current checkout) is only the local locator, and an explicit selector only cross-checks. A frozen/checkout disagreement FAILS and never continues in another clone.
The action context is a capability, and agents consume it as one. It states
separately planningWriteRoots (the planning directories, never a repository
root), codeWriteRoots (exactly the session's own checkout — never another
member checkout of the same store), and readRoots, plus constraints.
No user home directory appears in any of the three lists. Making a root visible
to the agent process (--add-dir) is process visibility, NOT authorization: a
root that is readable by the process is not writable by the work unless it
appears in a write list.
version identifies the contract. Version 1 additionally carries
allowedEditRoots, the compatibility view for the older single-list form, and
it appears only when the newer capability projects into it without granting
anything the older form would not have granted — the projection can narrow,
never widen. When it cannot (a store-planning session with a project checkout
needs two roots), version is 2 and allowedEditRoots is ABSENT; a consumer
that knows only the older form must stop rather than proceed.
A planning-only session reports "codeWriteRoots": [] — empty as a stated
fact — and its constraints say that no code write root is available and that no
project-scoped materialization occurs.
4.12 schemas --json / templates --json
schemas: bare array [ {name, description, artifacts, source} ]. templates: keyed object { "<artifactId>": {path, source} }. Both cwd-based, no root/status keys.
5. Exit-code contract
| 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 |
6. Diagnostic code catalog
Resolution
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.
Rasen-root health (error, no fix)
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. (specs/changes/archive are optional directories — they only fail when something non-directory exists at that path, not when absent.)
Store registry/identity/state
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/register/remove
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_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
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).
References (warning)
reference_invalid_id, reference_registry_unreadable, reference_unresolved, reference_root_unhealthy, reference_index_truncated.
Relationships (warning; doctor; context keeps only the registry one)
relationship_registry_unreadable, root_pointer_invalid, pointer_declarations_inert.
Store membership (doctor and store doctor; read-only)
store_project_record_missing (error), project_membership_locator_missing (warning), project_membership_unverified (warning), shared_metadata_contains_local_path (warning), store_project_record_key_mismatch (error), store_legacy_reference_unresolved (warning), project_identity_unrecordable (error), store_membership_legacy_manifest (warning), store_membership_roles_inferred (info), project_planning_binding_refused (warning, store add-project --set-primary). Human and JSON render the same code, message, and repair. Pass-through from a refused write: invalid_store_project_record, store_project_record_unverified, membership_base_commit_moved, eject_destination_required, migrate_membership_verify_failed.
Archive (JSON mode)
archive_change_name_required, archive_change_not_found, archive_validation_failed, archive_confirmation_required, archive_tasks_incomplete, archive_spec_update_failed, archive_spec_validation_failed, archive_target_exists, archive_error.
Session runtime context and frozen resume
project_binding_mismatch (error; the frozen project is not the project the
session executes in — fails closed, never falls back to another clone),
project_binding_ambiguous (error; several registered checkouts carry the
frozen project's identity, all listed), project_binding_missing (error; no
checkout of the frozen project on this machine),
project_binding_selector_conflict (error; an explicit selector named a
different project than the frozen one — a selector cross-checks, it cannot
retarget), session_context_broken (error; the file RASEN_SESSION_CONTEXT
points at is missing, unparseable, of an unknown version, or names a different
session).
Context writes
context_file_exists, context_output_dir_missing.
Fallbacks
doctor_failed, context_failed, store_error, change_error, archive_error.
Known inconsistencies
Recorded by the capstone audit; published-key renames are product decisions deferred past this release:
InFixed in the capstone gauntlet round:--jsonmode, several failure paths printed stderr only with no JSON document.show/validateunknown and ambiguous items emit{status:[{code: unknown_item | ambiguous_item, ...}]}; thrown errors instatus/instructions/list/show/validateroute through the JSON-aware failure helper (the command's null-shape +status);store <unknown subcommand> --jsonemits{status:[{code: unknown_store_subcommand}]};listcarries its{changes|specs: [], root: null}null-shape on resolution failures.store_root_missingis emitted with two severities (warning in remove, error in store doctor) — context-dependent, documented above.- snake_case (store family) vs camelCase (workflow family) key casing;
root.store_idis snake_case everywhere. - Four parallel envelope type declarations exist in src; archive diagnostics never carry
target. list --jsonreuses thestatuskey as a string enum per change.- Only
validateoutput carries aversionfield. schemas/templatesignore root selection (cwd-based, no--store).- Deprecated noun forms (
change/specsubcommands) emit unenveloped payloads withoutroot/status.