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_dirinside 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.
| Option | Behaviour |
|---|---|
--dry-run | Seed 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 byauto_discover_migrations(). - Version-key prefix sorts after the charter-fold migrations.
- Calls the same
cutover_missionper 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+mypyclean; 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
InnerStateChangedannotation (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:
| Group | Source | Fields |
|---|---|---|
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 →
authoredpopulated,resolvedempty; never the authored - subtasks authority: the snapshot
subtasksslot, nottasks.mdcheckbox 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:
WPResolvedBindingChangedshared event + a fan-out added toemit_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.