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 on started and its pair.
  • phase: required. One of started, completed, failed.
  • at: required. UTC timestamp.
  • agent: required. The tool key passed via --agent.
  • mission_id: required. ULID for the mission next was driving.
  • wp_id: optional; populated when the action targets a specific work package.
  • reason: optional; populated for failed.

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 started without a pair is not silently overwritten by a subsequent started for the same canonical_action_id.
  • The doctor surface (existing) lists orphan started records. This makes mid-cycle agent crashes observable rather than silently lost.

Test matrix

TestAsserts
next issues action <step>::implementstarted record present with canonical_action_id = "<step>::implement".
Action advances on successPaired completed with same canonical_action_id.
Action explicitly failsPaired 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 stateTriggerstdout requirement
disabledSPEC_KITTY_ENABLE_SAAS_SYNC unset / 0Strict JSON.
unauthorizedSaaS reachable, no valid authStrict JSON. Diagnostic on stderr.
network-failedSaaS unreachableStrict JSON. Diagnostic on stderr.
authorized-successSaaS reachable, authorizedStrict 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 --json
  • spec-kitty agent mission setup-plan --json
  • spec-kitty agent mission branch-context --json
  • spec-kitty agent context resolve --json
  • spec-kitty charter context --action <action> --json
  • spec-kitty agent decision open|resolve|defer|cancel|verify --json
  • spec-kitty agent action implement --json (when --json is supported)
  • Any other CLI surface that accepts --json and 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.