Contracts
authoring-commands.md
Contract — Authoring finish-work (Lane D) + terminology foundation
Acceptance authoring — extend agent mission acceptance-verdict (#2318, C-008)
No new command; the NegativeInvariant dataclass already carries all fields (no schema migration).
- Register (FR-007):
spec-kitty agent mission acceptance-verdict --negative-invariant <id> --description <…> --verification-command <…> [--expect-absent …](exact flags per the dataclass) → persists a well-shapedNegativeInvariant; zero hand-edited JSON. Routes viawrite_target(ACCEPTANCE_MATRIX). - Execute (FR-008): runs the invariant's verification via the existing
enforce_negative_invariantsengine; recordsresult(pass/fail). - Diagnose hardening (FR-009):
spec-kitty accept --diagnosewraps per-invariant load so a malformedNegativeInvariant.from_dictis reported (id + reason) and the command exits non-zero — no unhandledTypeErrorat load. - Fresh verdict (FR-010): canonical acceptance persists the recomputed
overall_verdicton the all-pass/no-invariant branch (samuelgoff:pass, not stalepending). - Prompt (FR-011):
src/doctrine/missions/mission-steps/**/<accept step>/prompt.md(SOURCE only) drivesacceptance-verdict— asserted to DRIVE the CLI, not merely mention it. Never edit the 12 generated agent copies (propagation viaspec-kitty upgrade).
Tests: SC-006 (full accept pass incl. a negative invariant, 0 hand-edited JSON; malformed reported, 0 exceptions); SC-007 (all-pass persists pass).
Issue-reference discovery (#1738, C-011)
- FR-012: widen the SINGLE
_GH_ISSUE_PATTERN(tasks/issue_matrix.py:61) with a same-repo GitHub-issue-URL alternation (repo from the canonical constant/remote). Cross-repo URLs NOT matched. No second matcher. - FR-013:
IssueReferencegainssource_file;to_dict/from_dictcarry it; multi-file dedup preserves first-occurrence provenance.
Tests: SC-008 — a same-repo URL in spec.md's Input field (samuelgoff #320) is discovered + produces a matrix row + records source_file; an unrelated cross-repo URL in prose does NOT newly block the completeness gate; every discovered ref has a non-empty source_file.
Terminology foundation (#3080, IC-01)
- ADR (FR-014):
docs/adr/3.x/2026-07-30-1-consolidated-write-surface-and-consolidate-terminology.md— design (CONSOLIDATED wiring) + canonical-term decision; extends 2026-07-23-2, reaffirms C-006. - Glossary (FR-015):
docs/context/orchestration.md—consolidatecanonical for the lane-consolidation sense; that sense of "merge" → legacy alias + "do NOT use when" guards. - Drift guard (FR-016):
tests/architectural/test_no_legacy_terminology.py— curated forbidden lane-consolidation-sense phrasings + baseline allow-set (shrink-only). Bite fixture proves a NEW forbidden phrasing fails (SC-011); green on git-merge/publish uses + grandfathered occurrences.
Boyscouting (C-012): any lane-consolidation-sense "merge" in code/docs this mission touches → canonical in new symbols/touched prose/comments. Existing public merge-named symbols (spec-kitty merge, MergeState, baseline_merge_commit) and git-merge/publish senses stay (→ #3080).
consolidated-write-surface.md
Contract — CONSOLIDATED write surface (Lane A)
The single write resolver, phase-aware, CONSOLIDATED-wired. No parallel resolver (C-006); phase derived internally (C-001).
resolve_placement_only(mission_slug, kind, …) — phase-aware (internal)
- Derives
LifecyclePhaseinternally from durablemeta.json+ git state (D2). Callers pass NO phase. - PRE_CONSOLIDATION → behaviour unchanged from #3076 (regression floor).
- CONSOLIDATED (E1) → for a kind that should follow the integrated tree, resolves through the CONSOLIDATED surface = Target Ref tree.
- PUBLISHED (E2) → resolves through the CONSOLIDATED surface = repo-root checkout, gated by the content-presence predicate (D1); if content absent → structured refusal carrying the FR-006 recovery action.
STATUS_STATE/DECISION_LOGshare this resolver: their pre-consolidation resolution is unchanged (SC-005); their post-consolidation resolution shifts as the shared-resolver consequence (C-005) — NOT via per-kind branching in the resolver.
Invariants: single resolver (no second path); probe and commit derive identical phase; get_feature_target_branch stays unrouted.
SurfaceLocations.consolidated + translate_surface(CONSOLIDATED, …)
SurfaceLocations.consolidatedis populated per phase (was alwaysNone).translate_surfacealready has the total CONSOLIDATED arm — no change to the translation map; only the location is now supplied.None(genuinely unroutable) → existingValueErrorguard → surfaced as FR-006 refuse-with-recovery, never a fabricated ref.
Content-presence predicate (D1)
consolidated_location_valid(mission_slug, HEAD) :=
exists(HEAD:kitty-specs/<mission_slug>/meta.json) # squash-robust; NOT commit-ancestry
Anchored by baseline_merge_commit (phase signal) — together they replace the non-existent "recorded merge target."
Write-seam staging thunk (FR-005 / #3073)
write_artifact(kind, stage: Callable[[], tuple[Path, ...]], …) -> WriteSeamResult
_probe_write_target(...)runs FIRST; only on OK isstage()invoked (materialize), thencommit_for_mission.- Refused write ⇒
stage()never called ⇒ zero untracked residue (SC-003). - Composed writers pass a thunk;
write_acceptance_matrixstays standalone (wrapped, not broken).
Acceptance scenarios → contract tests
- SC-001 red-first:
safe-commiton a PRIMARY artifact, E2 mission, Target-Ref deleted → today refuses at HEAD-guard; after IC-05 → exit 0, committed to CONSOLIDATED, clean tree. - SC-002: E2
write_artifactfor issue-matrix/tracer/acceptance →committed(notrefused). - SC-003: unroutable write → 0 untracked files.
- SC-004: checkout lacking consolidated content → refuse-with-recovery naming the checkout, non-zero, no checkout performed.
- SC-005:
STATUS_STATE/DECISION_LOGpre-consolidation resolution unchanged (non-regression). - SC-009:
review --mode post-mergeon an E2 mission exits 0 end-to-end.