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-shaped NegativeInvariant; zero hand-edited JSON. Routes via write_target(ACCEPTANCE_MATRIX).
  • Execute (FR-008): runs the invariant's verification via the existing enforce_negative_invariants engine; records result (pass/fail).
  • Diagnose hardening (FR-009): spec-kitty accept --diagnose wraps per-invariant load so a malformed NegativeInvariant.from_dict is reported (id + reason) and the command exits non-zero — no unhandled TypeError at load.
  • Fresh verdict (FR-010): canonical acceptance persists the recomputed overall_verdict on the all-pass/no-invariant branch (samuelgoff: pass, not stale pending).
  • Prompt (FR-011): src/doctrine/missions/mission-steps/**/<accept step>/prompt.md (SOURCE only) drives acceptance-verdict — asserted to DRIVE the CLI, not merely mention it. Never edit the 12 generated agent copies (propagation via spec-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: IssueReference gains source_file; to_dict/from_dict carry 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.mdconsolidate canonical 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 LifecyclePhase internally from durable meta.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_LOG share 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.consolidated is populated per phase (was always None).
  • translate_surface already has the total CONSOLIDATED arm — no change to the translation map; only the location is now supplied.
  • None (genuinely unroutable) → existing ValueError guard → 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 is stage() invoked (materialize), then commit_for_mission.
  • Refused write ⇒ stage() never called ⇒ zero untracked residue (SC-003).
  • Composed writers pass a thunk; write_acceptance_matrix stays standalone (wrapped, not broken).

Acceptance scenarios → contract tests

  • SC-001 red-first: safe-commit on 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_artifact for issue-matrix/tracer/acceptance → committed (not refused).
  • 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_LOG pre-consolidation resolution unchanged (non-regression).
  • SC-009: review --mode post-merge on an E2 mission exits 0 end-to-end.