T-7RST-ensure-ready-flags-mid-migration-corpus-assumptions
Status: closed/done · Impact: medium · Complexity: small
The readiness gate accepts tasks whose Approach implicitly assumes a uniform corpus shape even when the actual corpus is mid-migration. An implementer then has to design the strictness/tolerance split on the spot. Surfacing the assumption at the readiness gate turns it into a design conversation before implementation.
From the originating task’s post-mortem:
“T0011’s design needed a discover()/from_dir() strictness split absorbing the pre-D0004 corpus shape. The readiness gate should flag tasks whose Approach implicitly assumes a uniform corpus when the corpus is mid-migration, so the design conversation happens before implementation.”
| Location | Role today |
|---|---|
plugin/lib/model/entities/task/implementation-ready.md | The readiness contract. Its ## Disqualifiers list enumerates every condition that makes a doc NOT implementation-ready (subjective ACs, design-deferred-to-implementer, surviving placeholders). It has no entry for “Approach assumes a uniform corpus mid-migration.” |
plugin/skills/task-ensure-ready/SKILL.md | The skill spec. Step 3 (“Evaluate the contract”) runs the mechanical disqualifier scanners (parse_touchpoints.ts, scan_placeholders.ts) and then an LLM evaluation of the contract. There is no corpus-assumption check in either half. |
plugin/skills/task-ensure-ready/scan_placeholders.ts | The mechanical placeholder scanner: walks the required body sections (skipping fenced/inline code), emits one JSON line per hit ({section, phrase, line, snippet}), exit 0 with empty stdout when clean. The structural sibling a new corpus-assumption scanner is modelled on — same section-walk, same JSON-line output contract. |
plugin/skills/task-ensure-ready/scan_placeholders.ts#REQUIRED_SECTIONS | The canonical map of required-section H2 headings the scanner restricts itself to. A corpus-assumption scanner reuses the same section-scoping (it only inspects Approach/Proposed). |
plugin/skills/task-ensure-ready/ensure_ready_mutate.ts | The pass/fail mutation tail (stamps readiness_verified_at, or records definition_gap and downshifts status). Unchanged by this task — a corpus-assumption hit is just another disqualifier the LLM folds into the gap description it already composes in SKILL.md Step 5. |
plugin/skills/task-ensure-ready/tests/ensure_ready.test.ts | The skill’s test suite, including per-scanner behavioural cases (scan_placeholders, parse_touchpoints). New scanner cases land here. |
Proposed
Section titled “Proposed”Add “Approach assumes a uniform corpus shape while the corpus is
mid-migration” to the readiness contract’s disqualifier list, and back it
with a heuristic scanner (scan_corpus_assumptions.ts) modelled on
scan_placeholders.ts. Because whether an assumption is wrong is a
semantic judgment (the corpus may legitimately be uniform), the scanner is
advisory: it surfaces candidate signals — uniform-corpus phrasing in
## Approach/## Proposed with no accompanying tolerance/strictness
language — and the existing LLM evaluation in SKILL.md Step 3 makes the final
call. This mirrors the gate’s existing split: mechanical scanners narrow
where to look; the LLM step decides. A pure regex cannot be the gate here
(it would false-positive on genuinely-uniform corpora), so the contract
entry is phrased as an LLM-judged disqualifier with the scanner as its
candidate-finder, exactly as the subjective-AC disqualifier is LLM-judged
with the body as its input.
Approach
Section titled “Approach”- Add the disqualifier to the contract. In
plugin/lib/model/entities/task/implementation-ready.md’s## Disqualifierslist, add a bullet: a doc is not implementation-ready when its Approach assumes a single uniform corpus shape while the corpus it operates on is mid-migration (different instances carry different shapes), without specifying the strictness/tolerance split the implementer would otherwise have to invent on the spot. State explicitly that this is an LLM-judged disqualifier (like the subjective-AC entry), not a purely mechanical one, and that the corpus-assumption scanner only surfaces candidates. - Build
scan_corpus_assumptions.tsnext toscan_placeholders.ts, reusing its frontmatter-strip, fenced/inline-code masking, and required-section walk (restricted to## Approachand## Proposed). Emit one JSON line per candidate ({section, signal, line, snippet}), exit 0 with empty stdout when none, exit 2 on arg/IO failure — the same output contract asscan_placeholders.ts. - Define the candidate heuristic. A line is a candidate when it carries
uniform-corpus phrasing — e.g. “every entity”, “all instances”, “all
definition.md”, “the corpus”, “uniform shape”, “each*.mdhas the same”, “assume … same shape” — AND the surrounding section contains no tolerance/strictness signal (“legacy”, “mid-migration”, “tolerate”, “strict vs”, “both shapes”, “pre-D0004”, “discover()/from_dir()”, “escape hatch”, “version skew”). The patterns are documented in the module header so the heuristic is auditable and tunable. The scanner is a candidate-finder, not a verdict — false positives are expected and resolved by the LLM step. - Wire the scanner into SKILL.md Step 3 as an advisory input to the LLM
evaluation (NOT a hard mechanical disqualifier). Add a sub-bullet under
the disqualifier checks: run
bun run ${CLAUDE_PLUGIN_ROOT}/skills/task-ensure-ready/scan_corpus_assumptions.ts <path>; each emitted candidate is a prompt for the LLM to inspect that line and decide whether the Approach genuinely assumes a uniform corpus the corpus does not have. If the LLM confirms, it folds the finding into the gap description it already composes in Step 5; if the corpus is genuinely uniform (or the Approach already handles the split), it is not a gap. Keep the pipe-tail-safe shape (no| tail) perplugin/skills/CLAUDE.md. - Add behavioural test cases to
plugin/skills/task-ensure-ready/tests/ensure_ready.test.ts: an Approach with bare uniform-corpus phrasing → one candidate; an Approach that names the strictness/tolerance split → zero candidates; phrasing inside a fenced code block → zero candidates (masking works); a non-required section carrying the phrasing → zero candidates (section-scoping works). - Keep the mutation tail untouched.
ensure_ready_mutate.tsalready records whatever gap description the LLM composes; a confirmed corpus-assumption finding rides the existingdefinition_gap+ status downshift path with no code change there.
Files to touch
Section titled “Files to touch”| Location | Kind | Change |
|---|---|---|
plugin/skills/task-ensure-ready/scan_corpus_assumptions.ts | new | The heuristic candidate-finder modelled on scan_placeholders.ts: same section-walk and JSON-line output, restricted to Approach/Proposed, emitting uniform-corpus candidates with no nearby tolerance signal. |
plugin/lib/model/entities/task/implementation-ready.md | modify | Add the corpus-assumption disqualifier to the ## Disqualifiers list, phrased as an LLM-judged check with the scanner as candidate-finder. |
plugin/skills/task-ensure-ready/SKILL.md | modify | Add the scanner invocation as an advisory input to Step 3’s LLM evaluation; document that candidates are LLM-confirmed, not auto-failed. |
plugin/skills/task-ensure-ready/tests/ensure_ready.test.ts | modify | Add behavioural cases for the new scanner (candidate, tolerance-present, fenced-code, non-required-section). |
Acceptance criteria
Section titled “Acceptance criteria”- AC-1:
plugin/lib/model/entities/task/implementation-ready.md’s## Disqualifierslist contains an entry for an Approach that assumes a uniform corpus shape mid-migration, and that entry states it is LLM-judged (not a purely mechanical disqualifier). - AC-2:
scan_corpus_assumptions.tsexists and, given a task whose## Approachcarries uniform-corpus phrasing with no nearby tolerance/strictness signal, emits at least one JSON candidate line on stdout and exits 0. - AC-3: Given a task whose
## Approachnames the strictness/tolerance split (e.g. mentions “legacy”, “both shapes”, or a discover/from_dir split),scan_corpus_assumptions.tsemits zero candidates and exits 0. - AC-4: The scanner skips fenced code blocks and inline-code spans and restricts itself to the
## Approach/## Proposedsections (proven: uniform-corpus phrasing inside a fence, or under a non-required H2, yields zero candidates) — same scoping contract asscan_placeholders.ts. - AC-5:
plugin/skills/task-ensure-ready/SKILL.mdStep 3 invokes the scanner as an advisory input to the LLM evaluation, states each candidate is LLM-confirmed before becoming a gap, and uses a pipe-tail-safe invocation (no unguarded| tail/| head). - AC-6:
bun test plugin/skills/task-ensure-ready/tests/ensure_ready.test.tspasses, including the new scanner behavioural cases (candidate / tolerance-present / fenced-code / non-required-section). - AC-7: No change is made to
ensure_ready_mutate.ts’s mutation logic — a confirmed corpus-assumption finding rides the existingdefinition_gap+ status-downshift path.
Out of scope
Section titled “Out of scope”- A pure-regex hard-fail disqualifier — corpus uniformity is context-dependent; a regex alone would false-positive on genuinely-uniform corpora, so the gate stays LLM-judged with the scanner as candidate-finder only.
- Detecting which specific corpus is mid-migration (D-0004 column drift, schema-version skew, etc.) — the scanner flags the assumption pattern in the spec text; it does not inspect the live corpus.
- Auto-rewriting the Approach to add the strictness/tolerance split — that is
/sdlc:task-define’s interactive job; this task only surfaces the gap. - Changing
ensure_ready_mutate.ts,parse_touchpoints.ts, orscan_placeholders.ts— the new scanner is a sibling, not a modification of the existing ones.
Dependencies
Section titled “Dependencies”- none
Discovery context
Section titled “Discovery context”Spawned by /sdlc:spawn-task-pr on 2026-05-31 UTC from T-0011 in git@github.com:sksizer/dev.git.
Post-mortem
Section titled “Post-mortem”Captured by /sdlc:task-work on 2026-06-04. PR: pending.
Acceptance criteria coverage
Section titled “Acceptance criteria coverage”- AC-1: auto — contract entry present and flagged LLM-judged;
grep "LLM-judged disqualifier" plugin/lib/model/entities/task/implementation-ready.md. - AC-2: auto —
bun test ensure_ready.test.ts -t "scan corpus assumptions"casebare-uniform-approach-flagged(candidate emitted, exit 0). - AC-3: auto — same suite, case
tolerance-named-not-flagged(zero candidates). - AC-4: auto — same suite, cases
fenced-uniform-not-flaggedandnon-required-section-not-flagged(masking + section-scoping). - AC-5: agent-manual — read
plugin/skills/task-ensure-ready/SKILL.mdStep 3: scanner invoked as advisory input, candidates LLM-confirmed before becoming a gap, pipe-tail-safe invocation (no| tail/| head). - AC-6: auto —
bun test plugin/skills/task-ensure-ready/tests/ensure_ready.test.ts: 42 pass, 4 fail; the 4 failures are the pre-existingmarker emission + cleanuplease-transition tests (reproduced identically on a cleanorigin/maincheckout) and are unrelated to this task’s scope. - AC-7: auto —
git diff origin/main..HEAD -- ensure_ready_mutate.tsis empty (mutation logic untouched).
What worked
Section titled “What worked”- The new scanner cloned
scan_placeholders.ts’s structure cleanly — frontmatter strip, fenced/inline-code masking, and section-scoped walk all transferred, so the four behavioural cases (candidate / tolerance-present / fenced / non-required-section) passed first try. - Baseline-gated quality checks correctly subtracted the 4 pre-existing lease-transition test
failures, leaving a clean
OK 5/5.
Friction and automation gaps
Section titled “Friction and automation gaps”- The quality baseline captured from the main repo’s working tree was polluted by deliberate
uncommitted task-file WIP, so
audit_entities.tsreported phantomnew-drifton- OKenumeration lines; capturing the baseline from a clean detachedorigin/mainworktree fixed it —quality_baseline.ts captureshould warn (or capture from a clean tree) when the cwd working tree is dirty under audited paths. → T-TWZD-normalize-baseline-diff-nondeterministic-output - Resume after a killed run left the task branch based on a stale local
main; thecheck_ancestry.tshelper reportedcleanwhile a foreigndocs(tasks): verify <other>commit still sat inorigin/main..HEAD— the helper only matcheschore(tasks): start <other>subjects, so it misses foreign verify-stamp commits; widening it to also matchdocs(tasks): verify <other> implementation-readywould catch this class. → T-61OI-check-ancestry-flags-stale-base origin/mainadvanced three times during the run (parallel orchestration), forcing repeated re-fetch + re-baseline; a baseline keyed to a movingorigin/mainSHA is inherently racy — the gate could pin the baseline SHA at Step 3a and reuse it rather than re-resolving the tip. → T-TWZD-normalize-baseline-diff-nondeterministic-output
Spawned follow-up tasks
Section titled “Spawned follow-up tasks”- T-TWZD-normalize-baseline-diff-nondeterministic-output — linked (existing open/ready tracker
for the baseline-diff false-positive class; appended this task to its
related:and a dedup trail to its Discovery context). - T-61OI-check-ancestry-flags-stale-base — linked (existing in-flight task, PR #284, already
scoped to exactly the foreign-commit-in-
origin/main..HEADgap; referenced inline only, not edited, because it carries an open PR).