Contracts

cutover-cli.md

Contract: Cutover CLI + Upgrade Migration (FR-001..FR-003, FR-010)

The behavioural contract for the two callers of the shared cutover orchestration helper. This is the acceptance surface for IC-01 and IC-02.

Shared orchestration helper — migration/runtime_state_cutover.py

cutover_mission(feature_dir, *, dry_run=False) -> CutoverResult
    1. backfill  = backfill_runtime_state(feature_dir, dry_run=dry_run)   # idempotent seeds
    2. verify    = verify_backfill(feature_dir)                           # count+value, fail-closed
    3. if not verify.ok:  return CutoverResult(flipped=False, verify=verify)   # NEVER flip
    4. if dry_run:        return CutoverResult(flipped=False, would_flip=True, verify=verify)
    5. write meta.json status_phase = "1"   (sole writer; only reached on ok verify)
    6. return CutoverResult(flipped=True, verify=verify)

verify; MigrationOrderingError guards it).

Path.cwd() path (INV-5 / #2815).

  • Ordering is by construction — verify is called on the un-stripped frontmatter (never strip before
  • Sole writer of status_phase — no other code path writes it; step 5 is unreachable on a failed verify.
  • Write target — resolved via canonicalize_feature_dir inside the library; the helper adds no

CutoverResult fields: slug, flipped: bool, would_flip: bool (dry-run), seeded_count: int, verify: VerifyResult, error: str | None.

Operator CLI — spec-kitty migrate backfill-runtime-state

Registered as @app.command(name="backfill-runtime-state") in cli/commands/migrate_cmd.py.

OptionBehaviour
--dry-runSeed nothing, flip nothing; report per-mission would-seed counts and would-flip.
--mission <handle>Scope to one mission (mission_id / mid8 / slug); default = whole corpus.
(default)Walk kitty-specs/, run cutover_mission per mission.

Exit semantics (per-mission best-effort — research D-03):

ever flipped.

  • Runs every mission independently; flips each that verifies; records failures.
  • Exit 0 iff every visited mission either flipped or was already migrated (idempotent skip).
  • Exit non-zero if any mission's verify failed; prints each mismatch. No unverified mission is
  • Idempotent: a second run seeds nothing and re-flips nothing.

Acceptance (maps to spec scenarios US1): 1. --dry-run on a corpus with legacy runtime state → reports counts, writes 0 events, 0 flips. 2. real run → snapshot == old reader (count+value) for every WP; status_phase="1" only for passed missions. 3. fault-injected corrupt deterministic seed row → run aborts that mission's flip, exits non-zero, names the mismatch; status_phase untouched for it. 4. re-run → 0 new seeds, 0 flips (idempotent).

Upgrade migration — upgrade/migrations/m_<version>_runtime_state_backfill.py

  • Self-registers via @MigrationRegistry.register; discovered by auto_discover_migrations().
  • Version-key prefix sorts after the charter-fold migrations.
  • Calls the same cutover_mission per mission over the project corpus.

Fail-closed abort semantics (stricter than CLI — research D-03, NFR-005):

naming the mission + mismatch; leaves no mission half-flipped (each mission's flip is atomic).

  • Any mission's verify failure aborts the migration step with an operator-actionable message
  • No-op when there is no kitty-specs/ or no mission carries legacy runtime state (fresh install).
  • Idempotent — a re-run (already-migrated corpus) seeds nothing and completes clean.

Acceptance (maps to spec scenarios US3): 1. legacy deployment → migration runs, corpus backfilled+verified+flipped. 2. fresh install → migration no-ops. 3. a mission fails verify → migration step aborts fail-closed with an actionable message; no partial flip.

Cross-cutting guards

event file) is created at repo root (#2815 co-constraint).

_CATEGORY_C_DEFERRED_RUNTIME_STATE_BACKFILL_CUTOVER frozenset in the same WP that first wires a caller.

  • INV-5 regression test: after backfill (CLI and migration), assert no status.events.jsonl (or any
  • Dead-symbol un-pin (C-006): IC-01 removes the 15-symbol
  • No suppression (NFR-004): ruff + mypy clean; complexity ≤15 (helper split into seed/verify/flip phases).

resolved-binding.md

Contract: Resolved-binding record + WP-view reconstruction (FR-012..FR-015)

Behavioural contract for the resolved-binding half (operator decision 2026-07-20). Acceptance surface for IC-07 (reconstruction reader), IC-08 (vocabulary + resolve seam), IC-09 (SaaS fan-out). Gated by the C-009 field-authority ADR (IC-08a) landing first.

Vocabulary (IC-08) — WPInnerStateDelta / claim StatusEvent.actor

Add resolved-binding fields (home decided by the C-009 ADR — delta slots vs structured actor):

resolved: { role, agent_profile, agent_profile_version, model, provider }

dispatch/Op path (invocation/executor.py RoutingRecommendation, registry.resolve; recorded in invocation/record.py, keyed by invocation_id) and threaded into the implement/review commands (new --model/--profile/--invocation-id on cli/commands/agent/workflow.py) — the claim seam alone has only the bare --agent string. NEVER a copy of the frontmatter agent_profile string (C-007 / INV-6).

folds latest-wins at BOTH implement-claim and review-claim — the policy_metadata claim fold fires only on planned→claimed), plus enriching the transition's structured actor for the IC-09 fan-out.

deterministic authored-derived seed rows are invalid and must be removed by exact seed identity.

  • Source (dispatch→claim linkage, operator decision Q1): the genuine model+profile are resolved on the
  • Written: at each pick-up/claim/reassign transition, as an InnerStateChanged annotation (so it
  • Reduced: latest-wins into snapshot resolved slots (_RUNTIME_SLOTS + _apply_annotation_delta).
  • Historical non-seed (C-011): authored recommendations are never copied into resolved actuals;
  • Absence is valid: a never-reclaimed WP has no resolved slot → reconstruction shows empty resolved.

Reconstruction reader (IC-07) — status/wp_view.py::reconstruct_wp_view(feature_dir, wp_id)

Single reader replacing the three hand-rolled gates (dashboard scanner, agent tasks status board, WorkPackage). Returns a view with two distinct identity groups:

GroupSourceFields
resolved (actual)snapshot (event-sourced)lane, agent, assignee, subtasks, review, resolved role/profile(+version)/model/provider
authored (recommended)frontmatter (static)authored role/agent_profile/model, owned_files, dependencies, requirement_refs, …

value returned in the resolved group (INV-7).

dashboard split-brain).

  • Contract: all three consumers call this reader; none hand-rolls a snapshot gate afterward (SC-007).
  • Tolerate-absent: absent resolved slots → authored populated, resolved empty; never the authored
  • subtasks authority: the snapshot subtasks slot, not tasks.md checkbox counting (resolves the

Acceptance (US6): 1. implement-claim (P1/M1) → resolved = P1/M1 from the event log; authored shown separately. 2. later review-claim (P2/M2) → resolved updates to P2/M2 (latest-wins), 0 bytes to tasks/WP##.md. 3. dashboard, status board, WorkPackage all agree for the same WP (one reader). 4. never-reclaimed WP → authored populated, resolved empty (no crash, no masquerade). 5. recorded resolved profile came from resolve_profile/resolved_agent(), not the frontmatter string.

SaaS fan-out (IC-09)

StatusEvent + its existing _saas_fan_out. spec_kitty_events 6.1.0 StatusTransitionPayload.actor is already Union[str, Dict]zero shared-package change. Feature-detect the dict actor defensively.

(none today), version-gated (hasattr(spec_kitty_events, "WPResolvedBindingChanged"), mirroring the genesis-lane gate). Land the local event now; enable fan-out when the package ships (never block-on-shared).

with an older spec_kitty_events, the new-event fan-out is skipped (logged) and local persistence is unaffected.

  • Preferred: enrich the structured actor ({role, profile, tool, model}) on the claim/review
  • Fallback: WPResolvedBindingChanged shared event + a fan-out added to emit_inner_state_changed
  • Acceptance: a claim transition's fan-out payload carries the resolved {role, profile, tool, model};

Cross-cutting

authored → static) is ratified before IC-08 lands.

same merge unit so IC-07 reads seeded resolved slots (else empty for the dogfood corpus).

  • C-009 ADR precedes vocabulary — the per-field authority (resolved role/profile/model → dynamic;
  • Dogfood re-seed (IC-08) — after extending the backfill vocabulary, re-run the corpus backfill in the
  • No suppression / complexity ≤15 / no repo-root write (NFR-004, C-003) apply as for the cutover half.