claw-code/SCHEMAS.md
YeonGyu-Kim ce352f4750 docs(#172): correct action-field inventory claim (4 → 3 verbs) + regression guard
Pinpoint #172: SCHEMAS.md v1.5 Emission Baseline documentation inaccuracy
discovered during cycle #98 probe.

The Phase 1 normalization targets section claimed:
  "unify where `action` field appears (only in 4 inventory verbs)"

But reality is only 3 inventory verbs have `action`:
  - mcp
  - skills
  - agents

list-sessions uses `command` instead (the documented 1-of-13 deviation
already captured elsewhere in v1.5 baseline).

This is a doc-truthfulness issue (same family as cycles #76, #79, #82).
Active misdocumentation leads downstream consumers to assume 4-verb
coverage when building adapters/dispatchers.

Changes:
1. SCHEMAS.md: 'only in 4 inventory verbs' → 'only in 3 inventory verbs: mcp, skills, agents'
2. Added regression test `v1_5_action_field_appears_only_in_3_inventory_verbs_172`
   - Asserts mcp/skills/agents HAVE action field
   - Asserts help/version/doctor/status/sandbox/system-prompt/bootstrap-plan/list-sessions do NOT have action field
   - Forces SCHEMAS.md + binary to stay synchronized

Test added:
- `v1_5_action_field_appears_only_in_3_inventory_verbs_172` (8 negative cases + 3 positive cases)

Tests: 227/227 pass (+1 from #172).

Related: #155 (doc parity family), #168c (emission baseline).
Doc-truthfulness family: #76, #79, #82, #172.
2026-04-23 08:32:59 +09:00

24 KiB

JSON Envelope Schemas — Clawable CLI Contract

⚠️ CRITICAL: This document describes the TARGET v2.0 envelope schema, not the current v1.0 binary behavior. The Rust binary currently emits a flat v1.0 envelope that does NOT include timestamp, command, exit_code, output_format, or schema_version fields. See FIX_LOCUS_164.md for the full migration plan and timeline. Do not build automation against the field shapes below without first testing against the actual binary output. Use claw <command> --output-format json to inspect what your binary version actually emits.

This document locks the target field-level contract for all clawable-surface commands. After the v1.0→v2.0 migration (FIX_LOCUS_164 Phase 2), every command accepting --output-format json will conform to the envelope shapes documented here.

Target audience: Claws planning v2.0 migration, reference implementers, contract validators.

Current v1.0 reality: See ERROR_HANDLING.md Appendix A for the flat envelope shape the binary actually emits today.


Common Fields (All Envelopes) — TARGET v2.0 SCHEMA

This section describes the v2.0 target schema. The current v1.0 binary does NOT emit these fields. See FIX_LOCUS_164.md for the migration timeline.

After v2.0 migration, every command response, success or error, will carry:

{
  "timestamp": "2026-04-22T10:10:00Z",
  "command": "list-sessions",
  "exit_code": 0,
  "output_format": "json",
  "schema_version": "2.0"
}
Field Type Required Notes
timestamp ISO 8601 UTC Yes Time command completed
command string Yes argv[1] (e.g. "list-sessions")
exit_code int (0/1/2) Yes 0=success, 1=error/not-found, 2=timeout
output_format string Yes Always "json" (for symmetry with text mode)
schema_version string Yes "1.0" (bump for breaking changes)

Turn Result Fields (Multi-Turn Sessions)

When a command's response includes a turn object (e.g., in bootstrap or turn-loop), it carries:

Field Type Required Notes
prompt string Yes User input for this turn
output string Yes Assistant response
stop_reason enum Yes One of: completed, timeout, cancelled, max_budget_reached, max_turns_reached
cancel_observed bool Yes #164 Stage B: cancellation was signaled and observed (#161/#164)

Error Envelope

When a command fails (exit code 1), responses carry:

{
  "timestamp": "2026-04-22T10:10:00Z",
  "command": "exec-command",
  "exit_code": 1,
  "error": {
    "kind": "filesystem",
    "operation": "write",
    "target": "/tmp/nonexistent/out.md",
    "retryable": true,
    "message": "No such file or directory",
    "hint": "intermediate directory does not exist; try mkdir -p /tmp/nonexistent"
  }
}
Field Type Required Notes
error.kind enum Yes One of: filesystem, auth, session, parse, runtime, mcp, delivery, usage, policy, unknown
error.operation string Yes Syscall/method that failed (e.g. "write", "open", "resolve_session")
error.target string Yes Resource that failed (path, session-id, server-name, etc.)
error.retryable bool Yes Whether caller can safely retry without intervention
error.message string Yes Platform error message (e.g. errno text)
error.hint string No Optional actionable next step

Not-Found Envelope

When an entity does not exist (exit code 1, but not a failure):

{
  "timestamp": "2026-04-22T10:10:00Z",
  "command": "load-session",
  "exit_code": 1,
  "name": "does-not-exist",
  "found": false,
  "error": {
    "kind": "session_not_found",
    "message": "session 'does-not-exist' not found in .claw/sessions/",
    "retryable": false
  }
}
Field Type Required Notes
name string Yes Entity name/id that was looked up
found bool Yes Always false for not-found
error.kind enum Yes One of: command_not_found, tool_not_found, session_not_found
error.message string Yes User-visible explanation
error.retryable bool Yes Usually false (entity will not magically appear)

Per-Command Success Schemas

list-sessions

Status: Implemented (closed #251 cycle #45, 2026-04-23).

Actual binary envelope (as of #251 fix):

{
  "command": "list-sessions",
  "sessions": [
    {
      "id": "session-1775777421902-1",
      "path": "/path/to/.claw/sessions/session-1775777421902-1.jsonl",
      "updated_at_ms": 1775777421902,
      "message_count": 0
    }
  ]
}

Aspirational (future) shape:

{
  "timestamp": "2026-04-22T10:10:00Z",
  "command": "list-sessions",
  "exit_code": 0,
  "output_format": "json",
  "schema_version": "1.0",
  "directory": ".claw/sessions",
  "sessions_count": 2,
  "sessions": [
    {
      "session_id": "sess_abc123",
      "created_at": "2026-04-21T15:30:00Z",
      "last_modified": "2026-04-22T09:45:00Z",
      "prompt_count": 5,
      "stopped": false
    }
  ]
}

Gap: Current impl lacks timestamp, exit_code, output_format, schema_version, directory, sessions_count (derivable), and the session object uses id/updated_at_ms/message_count instead of session_id/last_modified/prompt_count. Follow-up #250 Option B to align field names and add common-envelope fields.

delete-session

Status: ⚠️ Stub only (closed #251 dispatch-order fix; full impl deferred).

Actual binary envelope (as of #251 fix):

{
  "type": "error",
  "command": "delete-session",
  "error": "not_yet_implemented",
  "kind": "not_yet_implemented"
}

Exit code: 1. No credentials required. The stub ensures the verb does NOT fall through to Prompt/auth (the #251 fix), but the actual delete operation is not yet wired.

Aspirational (future) shape:

{
  "timestamp": "2026-04-22T10:10:00Z",
  "command": "delete-session",
  "exit_code": 0,
  "session_id": "sess_abc123",
  "deleted": true,
  "directory": ".claw/sessions"
}

load-session

Status: Implemented (closed #251 cycle #45, 2026-04-23).

Actual binary envelope (as of #251 fix):

{
  "command": "load-session",
  "session": {
    "id": "session-abc123",
    "path": "/path/to/.claw/sessions/session-abc123.jsonl",
    "messages": 5
  }
}

For nonexistent sessions, emits a local session_not_found error (NOT missing_credentials):

{
  "error": "session not found: nonexistent",
  "kind": "session_not_found",
  "type": "error",
  "hint": "Hint: managed sessions live in .claw/sessions/<hash>/ ..."
}

Aspirational (future) shape:

{
  "timestamp": "2026-04-22T10:10:00Z",
  "command": "load-session",
  "exit_code": 0,
  "session_id": "sess_abc123",
  "loaded": true,
  "directory": ".claw/sessions",
  "path": ".claw/sessions/sess_abc123.jsonl"
}

Gap: Current impl uses nested session: {...} instead of flat fields, and omits common-envelope fields. Follow-up #250 Option B to align.

flush-transcript

Status: ⚠️ Stub only (closed #251 dispatch-order fix; full impl deferred).

Actual binary envelope (as of #251 fix):

{
  "type": "error",
  "command": "flush-transcript",
  "error": "not_yet_implemented",
  "kind": "not_yet_implemented"
}

Exit code: 1. No credentials required. Like delete-session, this stub resolves the #251 dispatch-order bug but the actual flush operation is not yet wired.

Aspirational (future) shape:

{
  "timestamp": "2026-04-22T10:10:00Z",
  "command": "flush-transcript",
  "exit_code": 0,
  "session_id": "sess_abc123",
  "path": ".claw/sessions/sess_abc123.jsonl",
  "flushed": true,
  "messages_count": 12,
  "input_tokens": 4500,
  "output_tokens": 1200
}

show-command

{
  "timestamp": "2026-04-22T10:10:00Z",
  "command": "show-command",
  "exit_code": 0,
  "name": "add-dir",
  "found": true,
  "source_hint": "commands/add-dir/add-dir.tsx",
  "responsibility": "creates a new directory in the worktree"
}

show-tool

{
  "timestamp": "2026-04-22T10:10:00Z",
  "command": "show-tool",
  "exit_code": 0,
  "name": "BashTool",
  "found": true,
  "source_hint": "tools/BashTool/BashTool.tsx"
}

exec-command

{
  "timestamp": "2026-04-22T10:10:00Z",
  "command": "exec-command",
  "exit_code": 0,
  "name": "add-dir",
  "prompt": "create src/util/",
  "handled": true,
  "message": "created directory",
  "source_hint": "commands/add-dir/add-dir.tsx"
}

exec-tool

{
  "timestamp": "2026-04-22T10:10:00Z",
  "command": "exec-tool",
  "exit_code": 0,
  "name": "BashTool",
  "payload": "cargo build",
  "handled": true,
  "message": "exit code 0",
  "source_hint": "tools/BashTool/BashTool.tsx"
}

route

{
  "timestamp": "2026-04-22T10:10:00Z",
  "command": "route",
  "exit_code": 0,
  "prompt": "add a test",
  "limit": 10,
  "match_count": 3,
  "matches": [
    {
      "kind": "command",
      "name": "add-file",
      "score": 0.92,
      "source_hint": "commands/add-file/add-file.tsx"
    }
  ]
}

bootstrap

{
  "timestamp": "2026-04-22T10:10:00Z",
  "command": "bootstrap",
  "exit_code": 0,
  "prompt": "hello",
  "setup": {
    "python_version": "3.13.12",
    "implementation": "CPython",
    "platform_name": "darwin",
    "test_command": "pytest"
  },
  "routed_matches": [
    {"kind": "command", "name": "init", "score": 0.85, "source_hint": "..."}
  ],
  "turn": {
    "prompt": "hello",
    "output": "...",
    "stop_reason": "completed"
  },
  "persisted_session_path": ".claw/sessions/sess_abc.jsonl"
}

command-graph

{
  "timestamp": "2026-04-22T10:10:00Z",
  "command": "command-graph",
  "exit_code": 0,
  "builtins_count": 185,
  "plugin_like_count": 20,
  "skill_like_count": 2,
  "total_count": 207,
  "builtins": [
    {"name": "add-dir", "source_hint": "commands/add-dir/add-dir.tsx"}
  ],
  "plugin_like": [],
  "skill_like": []
}

tool-pool

{
  "timestamp": "2026-04-22T10:10:00Z",
  "command": "tool-pool",
  "exit_code": 0,
  "simple_mode": false,
  "include_mcp": true,
  "tool_count": 184,
  "tools": [
    {"name": "BashTool", "source_hint": "tools/BashTool/BashTool.tsx"}
  ]
}

bootstrap-graph

{
  "timestamp": "2026-04-22T10:10:00Z",
  "command": "bootstrap-graph",
  "exit_code": 0,
  "stages": ["stage 1", "stage 2", "..."],
  "note": "bootstrap-graph is markdown-only in this version"
}

Versioning & Compatibility

  • schema_version = "1.0": Current as of 2026-04-22. Covers all 13 clawable commands.
  • Breaking changes (e.g. renaming a field) bump schema_version to "2.0".
  • Additive changes (e.g. new optional field) stay at "1.0" and are backward compatible.
  • Downstream claws must check schema_version before relying on field presence.

Regression Testing

Each command is covered by:

  1. Fixture file (golden JSON snapshot under tests/fixtures/json/<command>.json)
  2. Parametrised test in test_cli_parity_audit.py::TestJsonOutputContractEndToEnd
  3. Field consistency test (new, tracked as ROADMAP #172)

To update a fixture after a intentional schema change:

claw <command> --output-format json <args> > tests/fixtures/json/<command>.json
# Review the diff, commit
git add tests/fixtures/json/<command>.json

To verify no regressions:

cargo test --release test_json_envelope_field_consistency

Design Notes

Why common fields on every response?

  • Downstream claws can build one error handler that works for all commands
  • Timestamp + command + exit_code give context without scraping argv or timestamps from command output
  • schema_version signals compatibility for future upgrades

Why both "found" and "error" on not-found?

  • Exit code 1 covers both "entity missing" and "operation failed"
  • found=false distinguishes not-found from error without string matching
  • error.kind and error.retryable let automation decide: retry a temporary miss vs escalate a permanent refusal

Why "operation" and "target" in error?

  • Claws can aggregate failures by operation type (e.g. "how many write ops failed?")
  • Claws can implement per-target retry policy (e.g. "skip missing files, retry networking")
  • Pure text errors ("No such file") do not provide enough structure for pattern matching

Why "handled" vs "found"?

  • show-command reports found: bool (inventory signal: "does this exist?")
  • exec-command reports handled: bool (operational signal: "was this work performed?")
  • The names matter: a command can be found but not handled (e.g. too large for context window), or handled silently (no output message)

Appendix: Current v1.0 vs. Target v2.0 Envelope Shapes

⚠️ IMPORTANT: Binary Reality vs. This Document

This entire SCHEMAS.md document describes the TARGET v2.0 schema. The actual Rust binary currently emits v1.0 (flat) envelopes.

Do not assume the fields documented above are in the binary right now. They are not.

Current v1.0 Envelope (What the Rust Binary Actually Emits)

The Rust binary in rust/ currently emits a flat v1.0 envelope without common metadata wrapper:

v1.0 Success Envelope Example

{
  "kind": "list-sessions",
  "sessions": [
    {"id": "abc123", "created": "2026-04-22T10:00:00Z", "turns": 5}
  ],
  "type": "success"
}

Key differences from v2.0 above:

  • NO timestamp, command, exit_code, output_format, schema_version fields
  • kind field contains the verb name (or is entirely absent for success)
  • type: "success" flag at top level
  • Verb-specific fields (sessions, turn, etc.) at top level

v1.0 Error Envelope Example

{
  "error": "session 'xyz789' not found in .claw/sessions",
  "hint": "use 'list-sessions' to see available sessions",
  "kind": "session_not_found",
  "type": "error"
}

Key differences from v2.0 error above:

  • error field is a STRING, not a nested object
  • NO error.operation, error.target, error.retryable structured fields
  • kind is at top-level, not nested
  • NO timestamp, command, exit_code, output_format, schema_version
  • Extra type: "error" flag

Migration Timeline (FIX_LOCUS_164)

See FIX_LOCUS_164.md for the full phased migration:

  • Phase 1 (Opt-in): claw <cmd> --output-format json --envelope-version=2.0 emits v2.0 shape
  • Phase 2 (Default): v2.0 becomes default; --legacy-envelope flag opts into v1.0
  • Phase 3 (Deprecation): v1.0 warnings, then removal

Building Automation Against v1.0 (Current)

For claws building automation today (against the real binary, not this schema):

  1. Check type field first (string: "success" or "error")
  2. For success: verb-specific fields are at top level. Use jq .kind for verb ID (if present)
  3. For error: access error (string), hint (string), kind (string) all at top level
  4. Do not expect: timestamp, command, exit_code, output_format, schema_version — they don't exist yet
  5. Test your code against claw <cmd> --output-format json output to verify assumptions before deploying

Example: Python Consumer Code (v1.0)

Correct pattern for v1.0 (current binary):

import json
import subprocess

result = subprocess.run(
    ["claw", "list-sessions", "--output-format", "json"],
    capture_output=True,
    text=True
)
envelope = json.loads(result.stdout)

# v1.0: type is at top level
if envelope.get("type") == "error":
    error_msg = envelope.get("error", "unknown error")  # error is a STRING
    error_kind = envelope.get("kind")  # kind is at TOP LEVEL
    print(f"Error: {error_kind}{error_msg}")
else:
    # Success path: verb-specific fields at top level
    sessions = envelope.get("sessions", [])
    for session in sessions:
        print(f"Session: {session['id']}")

After v2.0 migration, this code will break. Claws building for v2.0 compatibility should:

  1. Check schema_version field
  2. Parse differently based on version
  3. Or wait until Phase 2 default bump is announced, then migrate

Why This Mismatch Exists

SCHEMAS.md was written as the target design for v2.0. The Rust binary is still on v1.0. The migration (FIX_LOCUS_164) will bring the binary in line with this schema, but it hasn't happened yet.

This mismatch is the root cause of doc-truthfulness issues #78, #79, #165. All three docs were documenting the v2.0 target as if it were current reality.

Questions?

  • "Is v2.0 implemented?" No. The binary is v1.0. See FIX_LOCUS_164.md for the implementation roadmap.
  • "Should I build against v2.0 schema?" No. Build against v1.0 (current). Test your code with claw to verify.
  • "When does v2.0 ship?" See FIX_LOCUS_164.md Phase 1 estimate: ~6 dev-days. Not scheduled yet.
  • "Can I use v2.0 now?" Only if you explicitly pass --envelope-version=2.0 (which doesn't exist yet in v1.0 binary).

v1.5 Emission Baseline — Per-Verb Shape Catalog (Cycle #91, Phase 0 Task 3)

Status: 📸 Snapshot of actual binary behavior as of cycle #91 (2026-04-23). Anchored by controlled matrix /tmp/cycle87-audit/matrix.json + Phase 0 tests in output_format_contract.rs.

Purpose

This section documents what each verb actually emits under --output-format json as of the v1.5 emission baseline (post-cycle #89 emission routing fix, pre-Phase 1 shape normalization).

This is a reference artifact, not a target schema. It describes the reality that:

  1. --output-format json exists and emits JSON (enforced by Phase 0 Task 2)
  2. All output goes to stdout (enforced by #168c fix, cycle #89)
  3. Each verb has a bespoke top-level shape (documented below; to be normalized in Phase 1)

Emission Contract (v1.5 Baseline)

Property Rule Enforced By
Exit 0 + stdout empty (silent success) Forbidden Test: emission_contract_no_silent_success_under_output_format_json_168c_task2
Exit 0 + stdout contains valid JSON Required Test: same (parses each safe-success verb)
Exit != 0 + JSON envelope on stdout Required Test: same + error_envelope_emitted_to_stdout_under_output_format_json_168c
Error envelope on stderr under --output-format json Forbidden Test: #168c regression test
Text mode routes errors to stderr Preserved Backward compat; not changed by cycle #89

Per-Verb Shape Catalog

Captured from controlled matrix (cycle #87) and verified against post-#168c binary (cycle #91).

Verbs with kind top-level field (12/13)

Verb Top-level keys Notes
help kind, message Minimal shape
version git_sha, kind, message, target, version Build metadata
doctor checks, has_failures, kind, message, report, summary Diagnostic results
mcp action, config_load_error, configured_servers, kind, servers, status, working_directory MCP state
skills action, kind, skills, summary Skills inventory
agents action, agents, count, kind, summary, working_directory Agent inventory
sandbox active, active_namespace, active_network, allowed_mounts, enabled, fallback_reason, filesystem_active, filesystem_mode, in_container, kind, markers, requested_namespace, requested_network, supported Sandbox state (14 keys)
status config_load_error, kind, model, model_raw, model_source, permission_mode, sandbox, status, usage, workspace Runtime status
system-prompt kind, message, sections Prompt sections
bootstrap-plan kind, phases Bootstrap phases
export file, kind, message, messages, session_id Export metadata
acp aliases, discoverability_tracking, kind, launch_command, message, recommended_workflows, serve_alias_only, status, supported, tracking ACP discoverability

Verb with command top-level field (1/13) — Phase 1 normalization target

Verb Top-level keys Notes
list-sessions command, sessions Deviation: uses command instead of kind. Target Phase 1 fix.

Verbs with error-only emission in test env (exit != 0)

These verbs require external state (credentials, session fixtures, manifests) and return error envelopes in clean test environments:

Verb Error envelope keys Notes
bootstrap error, hint, kind, type Requires ANTHROPIC_AUTH_TOKEN for success path
dump-manifests error, hint, kind, type Requires upstream manifest source
state error, hint, kind, type Requires worker state file

Common error envelope shape (all verbs): {error, hint, kind, type} — this is the one consistently-shaped part of v1.5.

Standard Error Envelope (v1.5)

Error envelopes are the only part of v1.5 with a guaranteed consistent shape across all verbs:

{
  "type": "error",
  "error": "short human-readable reason",
  "kind": "snake_case_machine_readable_classification",
  "hint": "optional remediation hint (may be null)"
}

Classification kinds (from classify_error_kind in main.rs):

  • cli_parse — argument parsing error
  • missing_credentials — auth token/key missing
  • session_not_found — load-session target missing
  • session_load_failed — persisted session unreadable
  • no_managed_sessions — no sessions exist to list
  • missing_manifests — upstream manifest sources absent
  • filesystem_io_error — file operation failure
  • api_http_error — upstream API returned non-2xx
  • unknown — classifier fallthrough

How This Differs from v2.0 Target

Aspect v1.5 (this doc) v2.0 Target (SCHEMAS.md top)
Top-level verb ID 12 use kind, 1 uses command Common command field
Common metadata None (no timestamp, exit_code, etc.) timestamp, command, exit_code, output_format, schema_version
Error envelope {error, hint, kind, type} flat {error: {message, kind, operation, target, retryable}, ...} nested
Success shape Verb-specific (13 bespoke) Common wrapper with data field

Consumer Guidance (Against v1.5 Baseline)

For claws consuming v1.5 today:

  1. Always use --output-format json — text format has no stability contract (#167)
  2. Check type field first — "error" or absent/other (treat as success)
  3. For errors: access error (string), kind (string), hint (nullable string)
  4. For success: use verb-specific keys per catalog above
  5. Do NOT assume kind field exists on success path — list-sessions uses command instead
  6. Do NOT assume metadata fields (timestamp, exit_code, etc.) — they are v2.0 target only
  7. Check exit code for pass/fail; don't infer from payload alone

Phase 1 Normalization Targets (After This Baseline Locks)

Phase 1 (shape stabilization) will normalize these divergences:

  • list-sessions: commandkind (align with 12/13 convention)
  • Potentially: unify where message field appears (9/13 have it, inconsistently populated)
  • Potentially: unify where action field appears (only in 3 inventory verbs: mcp, skills, agents)

Phase 1 does not add common metadata (timestamp, exit_code) — that's Phase 2 (v2.0 wrapper).

Regenerating This Catalog

The catalog is derived from running the controlled matrix. Phase 0 Task 4 will add a deterministic script; for now, reproduce with:

for verb in help version list-sessions doctor mcp skills agents sandbox status system-prompt bootstrap-plan export acp; do
  echo "=== $verb ==="
  claw $verb --output-format json | jq 'keys'
done

This matches what the Phase 0 Task 2 test enforces programmatically.