Contracts
invocation-lifecycle.md
Contract: Profile-Invocation Lifecycle Records
Issue: #843 FRs: FR-011, FR-012 · NFR: NFR-006 · SC: SC-005
Contract
When spec-kitty next --agent <name> issues a public action:
1. A started profile-invocation lifecycle record is written to the existing local invocation store before the action is exposed to the calling agent. 2. When the same action subsequently advances to success or explicit failure, a paired completed (or failed) record is written. 3. Both records share the same canonical action identifier — derived from the mission step/action that next actually issued.
Record shape
{
"canonical_action_id": "<mission_step>::<action>",
"phase": "started" | "completed" | "failed",
"at": "<ISO-8601 UTC>",
"agent": "<tool key, e.g. \"claude\">",
"mission_id": "<ULID>",
"wp_id": "<WPNN>" | null,
"reason": "<text>" | null
}
canonical_action_id: required. Same value onstartedand its pair.phase: required. One ofstarted,completed,failed.at: required. UTC timestamp.agent: required. The tool key passed via--agent.mission_id: required. ULID for the missionnextwas driving.wp_id: optional; populated when the action targets a specific work package.reason: optional; populated forfailed.
Pairing rule
For each canonical_action_id seen in the local store:
group_phases = sorted list of phases for that id
expected: ["started"] or ["started", "completed"] or ["started", "failed"]
any other shape is a defect (orphans, doubles, missing started)
Orphan behavior
- A
startedwithout a pair is not silently overwritten by a subsequentstartedfor the samecanonical_action_id. - The doctor surface (existing) lists orphan
startedrecords. This makes mid-cycle agent crashes observable rather than silently lost.
Test matrix
| Test | Asserts |
|---|---|
next issues action <step>::implement | started record present with canonical_action_id = "<step>::implement". |
| Action advances on success | Paired completed with same canonical_action_id. |
| Action explicitly fails | Paired failed with same canonical_action_id and non-null reason. |
| Mid-cycle crash (agent stops between started and completed) | Orphan started is listed by doctor; subsequent next does not overwrite it. |
| 5+ issued actions in a session | ≥ 95% pairing rate (NFR-006). |
Out of scope
- SaaS-side records / sync of these lifecycle records — local-first; SaaS sync is independent and governed by
SPEC_KITTY_ENABLE_SAAS_SYNC. - Schema migration for pre-existing local records (this is a new pair-aware shape; old single-shot records, if any, are tolerated by the doctor surface during a deprecation window).
json-envelope.md
Contract: Strict JSON Envelope for --json Commands
Issue: #842 FRs: FR-003, FR-004 · NFR: NFR-001 · SC: SC-002
Contract
For every --json command in the covered set:
- stdout: a single top-level JSON object.
json.loads(stdout)MUST succeed without preprocessing. - stderr: free-form text; receives all sync, auth, tracker, and other diagnostic output by default.
- exit code: as today (success / non-zero error).
The contract holds regardless of SaaS state:
| SaaS state | Trigger | stdout requirement |
|---|---|---|
disabled | SPEC_KITTY_ENABLE_SAAS_SYNC unset / 0 | Strict JSON. |
unauthorized | SaaS reachable, no valid auth | Strict JSON. Diagnostic on stderr. |
network-failed | SaaS unreachable | Strict JSON. Diagnostic on stderr. |
authorized-success | SaaS reachable, authorized | Strict JSON. No diagnostic required. |
Rules for diagnostics
1. Default: route all diagnostic prints (sync attempts, auth failures, network errors, tracker noise) to stderr. 2. In-envelope diagnostics are permitted ONLY when the consumer needs to programmatically observe the diagnostic. They go under a documented top-level key, proposed:
``json { "result": "success", "...": "...", "diagnostics": { "sync": { "status": "skipped", "reason": "not_authenticated" } } } ``
3. Forbidden: bare diagnostic lines on stdout outside the JSON object. This includes lines such as Not authenticated, skipping sync — these MUST NOT precede or follow the JSON envelope on stdout.
Covered commands
The contract surface is defined as: every --json command exercised by the strict-JSON parametrised integration test (Assumption A3). At minimum the test includes:
spec-kitty agent mission create --jsonspec-kitty agent mission setup-plan --jsonspec-kitty agent mission branch-context --jsonspec-kitty agent context resolve --jsonspec-kitty charter context --action <action> --jsonspec-kitty agent decision open|resolve|defer|cancel|verify --jsonspec-kitty agent action implement --json(when--jsonis supported)- Any other CLI surface that accepts
--jsonand is exercised in the test matrix
Adding a new --json flag to a command outside this set requires extending the test matrix in the same change.
Test matrix (NFR-001)
For each covered command × each of the four SaaS states:
def test_strict_json(command, saas_state):
set_saas_state(saas_state)
result = run_cli([*command, "--json"])
parsed = json.loads(result.stdout) # MUST succeed
assert isinstance(parsed, dict) # top-level object
# Diagnostics, if any, live in parsed["diagnostics"] or on stderr.
assert "Not authenticated" not in result.stdout
A bare-string scan on stdout for any text outside the JSON envelope is sufficient to detect the original bug class.
Versioning note
This contract is additive-only — new top-level keys MAY be added to the envelope but existing consumers MUST continue to parse successfully. If a future change renames a key, that requires a deprecation window, not in scope here.