Contracts

README.md

Contracts — CLI Interview Decision Moments

Planning-phase sketches of CLI command surfaces and response schemas. These are NOT the committed runtime schemas — the runtime source of truth is Pydantic (or dataclass) models in src/specify_cli/decisions/models.py and the spec-kitty-events 4.0.0 event schemas (vendored).

Files:

  • cli-contracts.md — subcommand surfaces (args, env, exit codes, output shape)
  • index_entry.schema.json — shape of an entry in decisions/index.json
  • decision_open_response.schema.json — JSON returned by spec-kitty agent decision open
  • decision_terminal_response.schema.json — JSON returned by resolve/defer/cancel
  • decision_verify_response.schema.json — JSON returned by verify (empty findings array on success)

cli-contracts.md

CLI Contracts — spec-kitty agent decision ...

All commands:

  • accept --mission <handle> (mission_id, mid8, or mission_slug; uses existing context resolver).
  • accept --actor <id> (defaults to git-configured user email or cli).
  • accept --dry-run (validate + report would-have-been output, no side effects). Default false except where noted.
  • return JSON to stdout. Exit 0 on success, non-zero on structured error. Errors are JSON with error, code, optional details.

spec-kitty agent decision open

Purpose: Open a Decision Moment at ask time.

Required args: --flow {charter|specify|plan}, --input-key <str>, --question <str>, exactly one of --step-id <str> OR --slot-key <str>.

Optional: --options '["a","b",...]' (JSON array), --actor <id>, --dry-run.

Behavior: 1. Resolve mission via context resolver. 2. Build logical key. Look up in index. If matching non-terminal entry exists, return its decision_id with idempotent=true; do NOT emit event. 3. If matching terminal entry exists, return error DECISION_ALREADY_CLOSED. 4. Else: mint ULID, append index entry (status=open), create DM-<id>.md, emit DecisionPointOpened event.

Success output:

{
  "decision_id": "01J2A...",
  "idempotent": false,
  "mission_id": "01KPWT8P...",
  "artifact_path": "kitty-specs/<mission>/decisions/DM-01J2A....md",
  "event_lamport": 42
}

Error (already_closed):

{"error": "Decision already closed", "code": "DECISION_ALREADY_CLOSED",
 "details": {"decision_id": "01J2A...", "status": "resolved"}}

spec-kitty agent decision resolve <decision_id>

Required: --final-answer <str> (non-empty). Optional: --other-answer (flag), --rationale <str>, --resolved-by <id>, --actor <id>, --dry-run.

Behavior: Idempotent on exact payload match. Emits DecisionPointResolved(terminal_outcome=resolved). Updates index + artifact. Charter path additionally writes to answers.yaml via existing charter persistence helper.

Success output:

{
  "decision_id": "01J2A...",
  "status": "resolved",
  "terminal_outcome": "resolved",
  "idempotent": false,
  "event_lamport": 57
}

spec-kitty agent decision defer <decision_id>

Required: --rationale <str> (non-empty). Optional: --resolved-by <id>, --actor <id>, --dry-run.

Behavior: Emits DecisionPointResolved(terminal_outcome=deferred). No DecisionInputAnswered. No answers.yaml write.

spec-kitty agent decision cancel <decision_id>

Required: --rationale <str> (non-empty). Optional: --resolved-by <id>, --actor <id>, --dry-run.

Behavior: Emits DecisionPointResolved(terminal_outcome=canceled). No DecisionInputAnswered. No answers.yaml write.

spec-kitty agent decision verify

Required: --mission <handle>.

Optional: --format {json|text} (default json), --fail-on-stale (default true).

Behavior: Load index, scan spec.md and plan.md for inline markers, cross-reference. Return structured findings.

Success (clean):

{"status": "clean", "deferred_count": 3, "marker_count": 3, "findings": []}

Finding types:

  • DEFERRED_WITHOUT_MARKER — deferred decision has no matching inline marker.
  • MARKER_WITHOUT_DECISION — inline marker references an unknown decision_id.
  • STALE_MARKER — marker references a decision that's no longer deferred.

Exit code: 0 on clean, non-zero if findings != [].

Terminal command error codes

  • DECISION_NOT_FOUND — decision_id unknown in this mission's index.
  • DECISION_TERMINAL_CONFLICT — already terminal with different payload (e.g., resolve after defer, or resolve with a different final_answer).
  • DECISION_ALREADY_CLOSED — (used on open idempotency miss, not on terminal commands retried identically).

decision_open_response.schema.json

{ "$schema": "https://json-schema.org/draft/2020-12/schema", "title": "DecisionOpenResponse", "type": "object", "additionalProperties": false, "required": ["decision_id", "idempotent", "mission_id", "artifact_path", "event_lamport"], "properties": { "decision_id": {"type": "string", "pattern": "^[0-9A-HJKMNP-TV-Z]{26}$"}, "idempotent": {"type": "boolean"}, "mission_id": {"type": "string"}, "artifact_path": {"type": "string"}, "event_lamport": {"type": ["integer", "null"], "minimum": 0} } }

decision_terminal_response.schema.json

{ "$schema": "https://json-schema.org/draft/2020-12/schema", "title": "DecisionTerminalResponse", "type": "object", "additionalProperties": false, "required": ["decision_id", "status", "terminal_outcome", "idempotent"], "properties": { "decision_id": {"type": "string", "pattern": "^[0-9A-HJKMNP-TV-Z]{26}$"}, "status": {"enum": ["resolved", "deferred", "canceled"]}, "terminal_outcome": {"enum": ["resolved", "deferred", "canceled"]}, "idempotent": {"type": "boolean"}, "event_lamport": {"type": ["integer", "null"], "minimum": 0} } }

decision_verify_response.schema.json

{ "$schema": "https://json-schema.org/draft/2020-12/schema", "title": "DecisionVerifyResponse", "type": "object", "additionalProperties": false, "required": ["status", "deferred_count", "marker_count", "findings"], "properties": { "status": {"enum": ["clean", "drift"]}, "deferred_count": {"type": "integer", "minimum": 0}, "marker_count": {"type": "integer", "minimum": 0}, "findings": { "type": "array", "items": { "type": "object", "additionalProperties": false, "required": ["kind", "decision_id_or_ref"], "properties": { "kind": {"enum": ["DEFERRED_WITHOUT_MARKER", "MARKER_WITHOUT_DECISION", "STALE_MARKER"]}, "decision_id_or_ref": {"type": "string"}, "location": {"type": ["string", "null"]}, "detail": {"type": ["string", "null"]} } } } } }

index_entry.schema.json

{ "$schema": "https://json-schema.org/draft/2020-12/schema", "title": "DecisionIndexEntry", "type": "object", "additionalProperties": false, "required": ["decision_id", "origin_flow", "input_key", "question", "status", "created_at", "mission_id", "mission_slug"], "properties": { "decision_id": {"type": "string", "pattern": "^[0-9A-HJKMNP-TV-Z]{26}$"}, "origin_flow": {"enum": ["charter", "specify", "plan"]}, "step_id": {"type": ["string", "null"]}, "slot_key": {"type": ["string", "null"]}, "input_key": {"type": "string", "minLength": 1}, "question": {"type": "string", "minLength": 1}, "options": {"type": "array", "items": {"type": "string"}}, "status": {"enum": ["open", "resolved", "deferred", "canceled"]}, "final_answer": {"type": ["string", "null"]}, "rationale": {"type": ["string", "null"]}, "other_answer": {"type": "boolean"}, "created_at": {"type": "string", "format": "date-time"}, "resolved_at": {"type": ["string", "null"], "format": "date-time"}, "resolved_by": {"type": ["string", "null"]}, "mission_id": {"type": "string"}, "mission_slug": {"type": "string"} }, "anyOf": [ {"required": ["step_id"], "not": {"properties": {"step_id": {"type": "null"}}}}, {"required": ["slot_key"], "not": {"properties": {"slot_key": {"type": "null"}}}} ] }