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 dispatch → task_execution).
Retained first-class aliases (NOT deprecated)
| command | argument shape (UNCHANGED) | mode | profile resolution |
|---|---|---|---|
spec-kitty do <request> [--profile] [--json] | optional --profile | task_execution | router if no profile; fail-closed |
spec-kitty advise <request> [--profile/-p] [--json] | optional --profile | advisory | hint or router |
spec-kitty ask <profile> <request> [--json] | mandatory positional profile | task_execution | direct lookup |
spec-kitty dispatch <request> [--profile] [--json] | optional --profile | task_execution | hint 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_workfor 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:referencesentry- 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 --checkexits 0 iff the committedgraph.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 completion — is_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--reasonis required (mirrors WP force-exit actor+reason discipline).- Effect: appends a
MissionReopenedlifecycle event (actor detected); clearsmerged_* - 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 mergere-stampsmerged_*. - 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
FollowUpRecordedlifecycle event attributed tomission_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).