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 indecisions/index.jsondecision_open_response.schema.json— JSON returned byspec-kitty agent decision opendecision_terminal_response.schema.json— JSON returned by resolve/defer/canceldecision_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 orcli). - 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, optionaldetails.
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 onopenidempotency 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"}}}} ] }