Contracts

dispatch-parity.md

Contract — Dispatch parity (workstream B, NFR-001 / FR-005 / C-002)

Canonical command

spec-kitty dispatch <request> [--profile <id>] [--json]

Routes through the single mechanism ProfileInvocationExecutor.invoke(). Mode derives from the entry command via invocation/modes.py::_ENTRY_COMMAND_MODE (add dispatchtask_execution).

Retained first-class aliases (NOT deprecated)

commandargument shape (UNCHANGED)modeprofile resolution
spec-kitty do <request> [--profile] [--json]optional --profiletask_executionrouter if no profile; fail-closed
spec-kitty advise <request> [--profile/-p] [--json]optional --profileadvisoryhint or router
spec-kitty ask <profile> <request> [--json]mandatory positional profiletask_executiondirect lookup
spec-kitty dispatch <request> [--profile] [--json]optional --profiletask_executionhint or router

All four call one shared _dispatch_impl(request, profile_hint, mode, json_output).

Parity assertions (pinned by tests)

For equivalent inputs, the Op record JSONL at kitty-ops/<invocation_id>.jsonl (the path returned by invocation/writer.py::invocation_path — the test must source the path from there, not hard-code it) MUST be byte/contract-identical across the canonical command and its alias, field-for-field except the unique invocation_id and timestamps:

governance_context_available

advise → advisory)

unchanged (advisory/query reject evidence promotion — pre-existing invocation/ behavior, not introduced or altered by this mission)

  • same event ("started"/"completed") shape and required v2 fields
  • same profile_id, action, request_text, actor, governance_context_hash,
  • same mode_of_work for equivalent verbs (do/ask/dispatch → task_execution;
  • identical JSON envelope (--json): status, close_contract, glossary observations
  • identical exit codes: 0 success; 1 routing/profile/write error; mode-enforcement behavior

Binding constraint (C-002)

The alias entry points land in the same change as dispatch. There is never a commit where spec-kitty do --profile … (which the governed-ops workflow itself depends on) is broken. No router/executor/record/modes-semantics change beyond adding the dispatch entry.

drg-curation.md

Contract — DRG curation (workstream C, FR-008/009, NFR-003, C-003)

Stale-reference repair (FR-008)

…/java-implementer.agent.yaml (non-existent) → repaint to …/java-jenny.agent.yaml (real Java specialist profile; already specializes_from implementer-ivan).

non-existent artifact); repair (repaint to a real target) or prune the reference (never the target artifact) with a per-fix one-line rationale.

  • src/doctrine/styleguides/built-in/java-conventions.styleguide.yaml: references entry
  • Sweep for other references of the same class (path/id/casing/retired-id pointing at a

Orphan triage (FR-009, C-003) — wire-or-document, never bulk-delete

For each genuinely-orphaned valid doctrine artifact (no inbound/outbound edge):

1. Prefer wiring a real inbound edge when a natural referent exists (e.g., cite a refactoring tactic from the refactoring procedure / a coding directive). 2. Else document it as an accepted residual with a per-orphan rationale (in-mission), and file a curation follow-up ticket if the residual set is non-empty. 3. Prune only genuinely-retired artifacts (superseded/dead), each individually justified. Bulk deletion of valid-but-unreferenced doctrine is explicitly rejected (D-C2).

Deterministic regen + regression pin (NFR-003)

sorted by URN, edges by (source,target,relation), generated_at="STATIC").

matches a fresh regen (freshness gate).

cannot silently grow; existing byte-identical-twice determinism test must stay green.

  • Regenerate via spec-kitty doctrine regenerate-graph (emit already deterministic: nodes
  • spec-kitty doctrine regenerate-graph --check exits 0 iff the committed graph.yaml
  • Add a regression test pinning the reduced orphan count (<= documented residual) so it

Closure (#1863)

#1863 closes once: java-implementer (+ same-class) refs resolved; orphan count reduced to the documented residual; residual rationale recorded (+ follow-up ticket if non-empty); regen deterministic and freshness/orphan-count tests green.

mission-lifecycle-commands.md

Contract — Post-mission lifecycle commands (workstream A, FR-001/002, NFR-004)

spec-kitty mission reopen <handle> --reason "<text>" [--json]

mission_id; ambiguous → structured MISSION_AMBIGUOUS_SELECTOR, no silent fallback).

from meta.json. The mission becomes actionable because derive_mission_lifecycle honors the MissionReopened event (new reopened surface_state) — NOT merely because merged_* was cleared (clearing alone is a no-op for the classifier, which reads WP lanes + age).

reached completionis_mission_completed(feature_dir) is true iff merged_at is present in meta.json OR derive_mission_lifecycle classifies it recently_completed/archived (all WPs terminal). Checked before any metadata mutation or event emission: a re-open of a not-yet-completed mission exits non-zero with a structured error (cannot re-open: mission has not completed/merged); no merged_ is cleared, no event written. (Self-correcting: after a re-open clears merged_ and the state flips to reopened/active, the mission is no longer completed, so a second re-open is blocked until it is re-completed.)

(a) meta.json absent/corrupt (no resolvable mission_id), OR (b) the mission branch resolves in neither the local repo nor any configured remote (via the core/vcs/git_ops lookup the resolver uses). A missing worktree directory alone is recoverable (re-materializable from the branch) and does NOT fail closed. On unrecoverable: exit non-zero with a structured error + remediation hint; no event written, no metadata change.

  • <handle>: mission_id (ULID) | mid8 | mission_slug (resolver disambiguates by
  • --reason is required (mirrors WP force-exit actor+reason discipline).
  • Effect: appends a MissionReopened lifecycle event (actor detected); clears merged_*
  • Does not mutate WP lanes (operator repositions WPs explicitly afterward).
  • Completion precondition (#1926, fail-closed): a mission can only be re-opened once it has
  • Fail-closed — concrete unrecoverable predicate: "unrecoverable" =
  • Reversible: a later spec-kitty merge re-stamps merged_*.
  • Exit: 0 on success; non-zero structured error on unresolved/unrecoverable mission.

spec-kitty mission follow-up <handle> (--commit <sha> | --pr <n>) [--json]

only valid once the mission has reached completion (is_mission_completed(feature_dir)merged_at present OR derive_mission_lifecycle reports recently_completed/archived). A follow-up against a not-yet-completed mission exits non-zero with a structured error (cannot record follow-up: mission has not completed/merged); no event is written. Checked before emission. (Replaces the earlier "allowed in any mission state" behaviour.)

reference is a no-op (no duplicate event).

not-yet-completed mission.

  • Exactly one of --commit <40-hex> / --pr <int> (validated).
  • Effect: appends a FollowUpRecorded lifecycle event attributed to mission_id.
  • Completion precondition (#1926, fail-closed): a follow-up is a post-mission fact and is
  • Idempotent: dedup key (mission_id, commit_sha | pr_number) — re-recording the same
  • Surfaced in the mission lifecycle/history view (post_mission_events).
  • Exit: 0 on success (including idempotent no-op); non-zero on invalid ref / unresolved handle /

History surface

spec-kitty mission status/history (and the derived lifecycle view) renders post_mission_events chronologically with actor, reason (re-open), and commit/PR (follow-up).