Field report: a migration-contract step with no owning WP
Date: 2026-07-19 · Agent run: implement-review orchestration (Claude Code, Opus,
per-WP subagents) · Human-in-command: maintainer (operator) · Mission:
wp-runtime-state-eviction-01KXWN13 (#2684 — evict runtime-mutable WP state into the
append-only event log) · Outcome so far: 9/10 WPs approved; the 10th (closeout) surfaced
a mission-scoping gap that the operator resolved by re-scoping (ship dual-write, defer the
corpus cutover).
This note is for maintainers weighing what the planning gates do and do not catch. It records one gap — a step in the mission's own declared migration contract that no work package owned — and proposes a cheap structural check that would have caught it at the post-tasks point-cut instead of at closeout.
The contract, and the gap
The mission declared a strict, ordered migration contract in
contracts/migration.md:
backfill → verify(FAIL-CLOSED) → reader cutover → writer cutover → delete fallbacks → hash guard
The work-package decomposition (10 WPs) covered the two ends well:
- The event infrastructure, reducer fold, emit API, and per-field readers/writers were
each owned (WP01–WP09), and each landed its reader/writer behind a phase-1 flag
(
_phase1_dual_write_enabled, default OFF → legacy frontmatter stays authoritative). This is the correct shape for a safe additive increment: dual-write, nothing flips yet. - The terminal steps — "delete fallbacks" and "verify no shim survives" — were owned by the closeout WP10 (T037 / T036).
But the middle of the contract had no owner:
- No WP wired the runtime-state
backfill → verify → cutoverCLI. The backfill library existed (WP03), but nothing invoked it:migrate_cmd.pyonly had identity/topology backfills, andmigration/runner.pyis the.kittify-layout migration, not the event backfill. - No WP flipped the flag unconditional. WP10's T036 was written assuming each field vertical "tears down its own dual-write" as part of its atomic switch — but the verticals landed dual-write behind the flag and none removed the OFF branch. The assumption was plausible-but-wrong, and never cross-checked against the verticals' actual scopes.
Result: WP10 was told to delete the fallbacks (the last contract step) while the steps that must precede it (a wired, verified backfill; an unconditional cutover) were produced by no one. The precondition chain was broken in the middle.
Why every gate passed anyway
The gap survived finalize-tasks, three adversarial squads (post-spec, post-plan, post-tasks), and /analyze. None of them is wrong; none of them checks the thing that was missing:
- finalize-tasks validates
owned_filesnon-overlap and requirement coverage — everyFR-###maps to ≥1 WP. FR-005 ("reader cutover") was referenced by WP05 and WP10, so coverage was satisfied. Coverage counts references to a requirement, not completion of that requirement's own multi-step contract. - /analyze checks FR→task coverage, duplication, ambiguity, and charter alignment. Its
coverage table (
FR-005 reader cutover → WP05, WP10) was green. It has no notion of "this requirement is delivered by an ordered contract; does every step have an owner?" - The dependency graph encoded WP10 depends on WP04–WP09, but nothing asserted WP10's contract preconditions (a merged+verified backfill) were produced by an ancestor. A dep edge says "runs after," not "precondition satisfied by."
So a requirement can read as "covered" while the terminal step of its contract silently has no owner — or is placed in a WP whose precondition no WP produces. The blind spot is orthogonal to the mission's own theme (single canonical authority); it's about migration sequencing ownership.
Caught at closeout (the loop working, expensively)
The WP10 implementer, instructed to delete the fallbacks, investigated the base first, found the precondition chain broken, and STOPPED — refusing to force an unsafe cross-lane cascade across ~12 files in already-approved WPs, and refusing to fake green (both STOP-authorized in its prompt). It filed a structured blocker. The operator then made the scoping call: ship the dual-write end-state now, defer the corpus cutover (wire backfill, flip the flag, delete fallbacks) to a follow-up mission — which is what the staged contract already implied: deleting a migration's fallbacks belongs after the corpus migration, in the same bucket as the already-deferred IC-08 reduction.
That is the right outcome — but it was reached at the most expensive point (a full closeout WP dispatched, investigated, and re-scoped), when the same conclusion was available at post-tasks from the artifacts alone.
Recommendation: a post-tasks contract-step-ownership lint
When a mission declares a sequenced contract — an ordered step list in contracts/*.md,
a lifecycle/state-machine, or a migration order — add a structural check (finalize-tasks
validate pass, or a post-tasks squad lens) that asserts:
- Every declared step maps to an owning WP. Flag an orphan contract step — a step in
the ordered contract that no WP's scope/subtasks claim. ("
backfill → verify → cutoveris named in migration.md but owned by no WP.") - Every step's declared precondition is produced by a dependency-graph ancestor. Flag a precondition inversion — a WP performs step N but no ancestor produces step N-1's output. ("WP10 deletes fallbacks; its precondition backfill merged + verify passed has no producing ancestor.")
Both are cheap and purely structural (parse the ordered steps; intersect with WP scopes; walk the dep graph). Both would have fired at post-tasks — where re-scoping is a one-paragraph edit — instead of at closeout. This complements, not replaces, FR-coverage: FR-coverage asks "is the requirement referenced?"; this asks "is the requirement's contract wholly owned and correctly ordered?"
A lighter, doctrine-only version: make "does every step of every declared contract have an
owner, and is its precondition produced upstream?" an explicit lens in the post-tasks
anti-laziness squad checklist. The squad already reads contracts/ and tasks.md; it just
wasn't pointed at step-ownership.
Takeaway
FR-coverage ≠ contract-step-coverage. A staged-migration mission can pass every existing planning gate while a middle step of its own contract has no owner, because the gates count requirement references and validate file ownership, not ordered-contract completeness. The dual-write increment the verticals shipped was correct and safe; what was missing was an owner for the cutover that turns it on, and a check that would have named that absence during planning. The blocker was caught — by an implementer that stopped instead of forcing green, and an operator who owned the scope call — but one structural lint would move this whole class of finding from "discovered at closeout" to "flagged at post-tasks."