/sdlc:task-ensure-ready
Generated from solutions/ontological/skills/task-ensure-ready/SKILL.md.
Description
Section titled “Description”Verify a single task document against the implementation-ready contract (solutions/ontological/lib/model/entities/task/implementation-ready.md). Writes frontmatter only, leaving body content to /sdlc:task-define. On pass, stamps readiness_verified_at on frontmatter. On fail, clears that stamp, records a definition_gap, and downshifts status to planning/needs-definition — except when the input is already in-progress or in-progress/blocked, in which case status is preserved (the task is mid-flight and a downshift would corrupt task-work state). When the task is autonomy: autonomous/pr and the deterministic verify finds a gap, it dispatches /sdlc:task-auto-define once to best-effort fill the gap, then re-verifies, self-readying the task. Dispatched by /sdlc:task-work Step 5a after the readiness gate, and runnable standalone. Idempotent and safe to re-run.
Allowed tools
Section titled “Allowed tools”ReadEditBashGrepGlobSkillAgent
Source
Section titled “Source”Usage:
/sdlc:task-ensure-ready <slug-or-filename>— verify the named task. Matching rules are the same as/sdlc:task-work(filename match, then glob match againstdocs/planning/tasks/)./sdlc:task-ensure-ready <absolute-path-to-task-file>— verify a task by absolute path. Use this form when invoking from another skill.
When the doc has gaps, record the gap and exit. To drive the user
through filling gaps, callers (or the user) invoke /sdlc:task-define
separately.
Input contract — accepted statuses
Section titled “Input contract — accepted statuses”Allowed input statuses are the four pre-implementation stages —
draft, proposed, backlog, ready — plus the two legacy
execution-plane values, in-progress and in-progress/blocked.
A task in flight now reads open/ready: under
[[D-S30G-task-state-plane-split]] the run’s phase lives on the lease
and in-progress* is never written to frontmatter again. The two
values stay ACCEPTED (never written) so files that predate the split
are still gate-able through the deprecation window, and so
sdlc lease reconcile can heal them. readiness_verified_at is
likewise still read — it is the promotion stamp, not a per-run one.
planning/needs-definition and any closed/* value are not valid
inputs — surface that as an error and exit without modifying the file.
That makes the downshift a one-way door until someone edits the spec and
moves it back into planning/{draft,proposed,backlog}, which matters more
now that promotion is the ONLY route to open/ready. Re-opening a
downshifted task is deliberate human work, which is the point.
References:
${CLAUDE_PLUGIN_ROOT}/entities/task/implementation-ready.md— the contract this skill checks against.
1. Resolve the task file
Section titled “1. Resolve the task file”Resolve the argument by running
${CLAUDE_PLUGIN_ROOT}cli/sdlc task resolve <argument> per
${CLAUDE_PLUGIN_ROOT}/entities/task/file-resolution.md (the op’s spec
doc — invocation, marker grammar, and exit codes live there). This skill
is non-interactive: on ambiguous match exit immediately with
ENSURE-READY-AMBIGUOUS: <candidates>; on no match exit with
ENSURE-READY-NO-TASK-FOUND <argument>. On success, capture the absolute
path and the basename (filename without .md) and continue to Step 2.
2. Read inputs
Section titled “2. Read inputs”- Read the task file end-to-end.
- Read
${CLAUDE_PLUGIN_ROOT}/entities/task/implementation-ready.md.
3. Evaluate the contract
Section titled “3. Evaluate the contract”Run task gap-report first (the invocation is below) and read
parenthood.is_parent before anything else.
Parent short-circuit. When parenthood.is_parent is true, stop
here. Emit the marker and exit success:
ENSURE-READY-PARENT-ROLLUP: <basename>Apply NO mutation on this path — no stamp, no promotion commit, no
downshift, no lease write. A parent task is an organizational rollup,
not a dispatchable work order: Files-to-touch tables and touchpoint
resolution are category errors against it, and its readiness is its
children’s readiness. The contract binds LEAVES
([[D-VSLI-distributed-work-runner-architecture]] settled leaf-only
dispatch; see the ## Applicability section of
${CLAUDE_PLUGIN_ROOT}/entities/task/implementation-ready.md).
Parenthood is a deterministic fact the op computes — a task is a parent
iff another task’s parent_key resolves to it — so this is a
mechanical branch, not a judgment call. The op still reports every
other slice for a parent; it does not suppress them. Ignore them here.
Otherwise apply the contract from
${CLAUDE_PLUGIN_ROOT}/entities/task/implementation-ready.md
literally:
-
Body sections → mark each present / missing / thin. (“Thin” means header exists but body is empty, unchanged
<...>template text, or too vague to inform the implementer.) Only the sectionsgap-reportreports undersections[]are required; an absent section outside that list costs nothing. -
Required frontmatter → check each field.
-
Deterministic disqualifiers → run the single composite op, then read the structured object:
${CLAUDE_PLUGIN_ROOT}cli/sdlc task gap-report <path> --output jsontask gap-reportis the deterministic NO-LLM composite that runs the four former separate checks —parse-touchpoints,scan-placeholders,check-claims, andresolve-touchpoints— plus required-section presence in ONE call, and returns ONE structured object. It replaces the four separate op invocations the gate used to make. See the op module doc-comment andsdlc task gap-report --helpfor the full output shape. A missing or unreadable file isINVALID_INPUT(exit 1); the single-line marker isGAP-REPORT gaps=N sections=N placeholders=N touchpoints=N claims=N parent=<bool>(exit 0). The op is read-only. Theparenthoodslice ({ is_parent, children[] }) drives the short-circuit above;parent=is its marker form.This op reports deterministic FACTS only — it does NOT make the interpretation calls below (thin-vs-present, greenfield prose-Today vs bulleted-legacy, severity weighting). That interpretation is THIS skill’s job (the reference interpretation layer, S-0004 spectrum). Map the composite’s fields onto the same judgments the four separate calls drove:
- Touchpoint shape → read
touchpoints.today_kindandtouchpoints.files_to_touch_kind(each ∈prose/table/missing/bulleted-legacy). The gate is one parsing path; there is NO fallback to the v2 bulleted shape:files_to_touch_kind == "bulleted-legacy"→ hard fail. The v3 verifier is table-only.today_kind == "bulleted-legacy"→ also rejected. v3 admits a table or pure prose, not the bulleted shape.today_kind == "prose"→ accepted (greenfield work).files_to_touch_kind == "missing"→ accepted. The section is optional in presence; depth is the routed process’s business, not the universal contract’s.sections[]is the authority on what a missing section costs — read it rather than deciding from the*_kindtokens.
- Touchpoint parse errors → read
touchpoints.parse_errors[]({section, location, error}): each is a disqualifier (invalid kind, symbol on glob, malformed location). The composite has already folded these intogaps[]undercategory: "touchpoint". - Touchpoint EXISTENCE/RESOLUTION → read
touchpoints.unresolved[]({section, location, kind, reason}): every entry is a disqualifier. The op reports EXISTENCE only — eachreasonis a machine token (file-missing,symbol-missing,directory-missing,glob-no-match, …); relay itslocationandreasoninto the gap description. The per-form semantics the gate relies on are unchanged and applied inside the op: file/line resolve iff the file exists; symbol resolves iff the file exists AND the symbol token is found in it (the token find runs in Bun/TS, sidestepping the historicalcommand grep-vs-grepBSD-flag aliasing — see the edge-lesson note in §Notes); directory resolves iff the directory exists; glob resolves iff it expands to ≥1 match;kind: newrows fire NO existence check (resolved: true, never inunresolved[]); each## Todayrow resolves as amodifyrow (itskindcomes backmodify). Rows the parser already rejected are NOT double-listed here — the op dropsreason: "parse-error: …"rows fromunresolved[]because they are already counted inparse_errors[](keep this dedup; do not re-report). Any drift narration (“this path moved to …”) and any migrate recommendation is yours to compose in the gap description, not the op’s. - Placeholders → read
placeholders[]({section, phrase, line, snippet}): each match (TBD, pick-one, angle-bracket<...>, empty table cell) is a disqualifier. Name the section, the matched phrase, and (if helpful) the line in the gap description. No further filtering — the op already scoped these to required sections. - Missing required sections → read
sections[]({section, present}): every entry withpresent == falseis a missing-section disqualifier. - Claim resolvers → read
claims[]({resolver, line, severity, message}): eachseverity: "disqualifier"finding is a fail reason — relay itsmessage(andline, if helpful) into the gap description.severity: "warning"findings are informational only — never a gap (no resolver emitswarningtoday, but the tier exists). The resolver contracts (what each claim kind checks) are documented in code undersolutions/ontological/lib/model/entities/task/claims/. - Aggregated disqualifiers →
gaps[]({category, code, section, line, message}) is the flat union of the deterministic disqualifiers across all four slices, in category order.has_gaps == (gaps.length > 0). Usegaps[]as the iterable union when composing the gap paragraph; the per-category fields above carry the same facts with the detail each judgment needs. The ONE placegaps[]and your verdict can diverge is the interpretation layer: atoday_kind == "prose"Today is greenfield-ACCEPTED (NOT a gap) even though the op reported the kind; abulleted-legacyToday/Files-to-touch is a hard fail you add. Flag exactly what the four separate calls used to flag — a verdict that flags FEWER gaps than this composite is the failure mode.
- Touchpoint shape → read
-
Subjective-AC disqualifiers: read the body (LLM judgment, not in
gap-report). -
Corpus-assumption disqualifier (LLM-judged, advisory scanner) → keep its OWN separate call. This scanner is confirm-before-gap; it is deliberately NOT folded into
gap-report(which is purely deterministic). Shell out to the co-located candidate-finder:bun run ${CLAUDE_PLUGIN_ROOT}/skills/task-ensure-ready/scan_corpus_assumptions.ts <path>The scanner emits one JSON line per candidate (
{"section": ..., "signal": ..., "line": ..., "snippet": ...}) on stdout; exit 0 with empty stdout means no candidates. This scanner is advisory: a candidate becomes a gap only after you confirm it (below), never mechanically. Each candidate is uniform-corpus phrasing (e.g. “every entity”, “all instances”, “the corpus”) in## Approach/## Proposedwith no nearby tolerance/strictness signal. For each candidate, inspect the cited line and decide whether the Approach genuinely assumes a single uniform corpus shape the corpus does not have (it is mid-migration: different instances carry different shapes). Only when you CONFIRM the assumption is wrong does it become a gap — fold it into the gap description you compose in Step 5. If the corpus is genuinely uniform, or the Approach already names the strictness/tolerance split, the candidate is a false positive and is NOT a gap. (Run the command directly, capturing stdout into a variable if you need to trim it — do not pipe it throughtail/headwhen gating, per${CLAUDE_PLUGIN_ROOT}/skills/CLAUDE.md.)
Compile the result: either “pass” or “fail with reasons”. Reasons
are the union of missing/thin sections, frontmatter shortfalls, the
deterministic disqualifiers gap-report aggregated into gaps[]
(structural — touchpoint/placeholder/missing-section — plus any
disqualifier-severity claim-resolver finding), the bulleted-legacy
hard fail you add from the touchpoint kinds, and any confirmed
corpus-assumption gap.
Do not re-interpret the contract — if the reference doc says something is required, treat it as required.
If the evaluation passed, skip Step 3a and go straight to Step 4. If it failed, go to Step 3a (the autonomy gate) before Step 5.
3a. Autonomy gate — best-effort auto-define for autonomous/pr
Section titled “3a. Autonomy gate — best-effort auto-define for autonomous/pr”Reached only when Step 3 evaluated to fail. Read the task’s
autonomy: frontmatter value:
-
autonomy: autonomous/pr→ make exactly one best-effort attempt to fill the gap, then re-verify:- Dispatch
/sdlc:task-auto-define <absolute-path-to-task-file>(via theSkill/Agenttool). It synthesizes the missing/thin sections from the task’s prose + codebase context, stamps a discoverable machine-authoredAUTO-DEFINED:marker, and commits the body edits on the current branch. Its terminal marker is one ofTASK-AUTO-DEFINE-DEFINED:/TASK-AUTO-DEFINE-NO-CHANGES:/TASK-AUTO-DEFINE-INSUFFICIENT:(or the sharedERROR reason=...) — all slug-namespaced, so do not mistake them for this skill’s ownENSURE-READY-verdict. - Re-run Step 3’s evaluation against the (possibly edited) task
file: re-read it, re-run
task gap-report(the single deterministic composite), re-apply the interpretation layer, and re-check the required frontmatter. (The corpus advisory may be re-run too, but auto-define does not touch corpus phrasing, so a re-run is usually unnecessary.) - Decide on the re-verify result:
- Re-verify passes → proceed to Step 4 (stamp +
ENSURE-READY-OK:). TheAUTO-DEFINED:marker left in the body signals the human to review the synthesized spec before merge. - Re-verify still fails (auto-define returned
-INSUFFICIENT/-NO-CHANGES/ERROR, or the synthesized content still doesn’t satisfy the contract) → proceed to Step 5 with the re-verify’s gap reasons. Do NOT dispatch/sdlc:task-auto-definea second time in this run (loop guard against a deterministically-failing definer). One attempt, then park.
- Re-verify passes → proceed to Step 4 (stamp +
- Dispatch
-
Any other autonomy value (
supervised,human-only, or absent) → do NOT auto-define. Proceed directly to Step 5 (the downshift). The autonomy gate is opt-in viaautonomous/pronly.
The gate fires at most once per /sdlc:task-ensure-ready run.
3b. Precondition: the standalone --commit promotion must be frontmatter-only
Section titled “3b. Precondition: the standalone --commit promotion must be frontmatter-only”--commit also refuses (a distinct exit code, marker
ENSURE-READY-LEASED: <basename>) when an ACTIVE lease holds the task.
Post-split a task in flight reads open/ready, so without this check a
standalone invocation against live work would commit over a run in
progress. The lease is authoritative about who holds the task; the gate
defers to it.
When --commit is passed (the standalone path that commits on the
author’s CURRENT branch, in place), the mutator commits ONLY frontmatter
(the promotion’s status: + readiness_verified_at:). Before it stages
anything, it inspects the target task file’s uncommitted diff against
HEAD — both staged and unstaged — and refuses (exit 4) if any hunk
lands in the body (any line strictly after the closing --- of the
frontmatter block). The error names the file, the offending hunk line
ranges, and the frontmatter close line.
The reason is to keep the stamp commit isolated: on a dirty checkout, a
naive git add <task-file> would bundle the author’s uncommitted body
edits INTO the stamp commit. The gate refuses to bundle rather than
create that mixed commit. See
[[T-XBJY-ensure-ready-refuses-with-unstaged-body-edits]].
The fix when the gate refuses is mechanical: commit the body edits in
their own commit first (git add <task-file> && git commit), then
re-invoke the gate — now the only pending change is the in-memory
stamp, and the commit is frontmatter-only as intended. A
frontmatter-only uncommitted edit (e.g. a hand-bumped impact:) does
NOT trip the precondition; only body edits are refused.
The precondition does NOT apply to the --commit-on main path: that
path lands the stamp on origin/main through an EPHEMERAL worktree off
origin/main — never the author’s checkout
([[D-WK7T-agent-git-writes-worktree-isolated]]) — so the tree it stages
is always clean and the author’s uncommitted body edits can never be
bundled. Nor to the no-flag (in-place-edit) path: there the caller owns
the commit boundary.
4. Pass — promote, or memo the lease
Section titled “4. Pass — promote, or memo the lease”If the evaluation passed, the outcome depends on which PLANE owns the result. Under [[D-S30G-task-state-plane-split]] the lease substrate holds execution state and frontmatter holds semantic state, so a pass means different things in the two situations and writes to different places. The script decides; you invoke it and relay what it printed.
| Situation | What happens | plane: |
|---|---|---|
No active lease, status: in planning/* | ONE promotion commit: status: open/ready + readiness_verified_at: | frontmatter-promotion |
No active lease, already open/ready | Nothing. No commit; the file stays byte-identical. (A stale definition_gap: is still cleared — that edit is semantic and keeps its commit.) | none |
| An active lease holds the task | CAS-write the lease’s gates object. Zero commits on main, no frontmatter write | lease-gates |
Promotion IS the readiness claim. That is why the status flip and
the stamp ride one commit: there is no window in which a task is
open/ready without a verified spec, and no separate stamp commit to
land, crash between, or resume from. It is one of D-S30G’s four
semantic transitions — the only ones that commit to main.
Re-verifying an already-promoted task is a no-op. The stamp answers
“was this ready when it was PROMOTED”, which does not change on
re-verification. A per-run answer is what the lease’s gates is for.
-
Apply the pass mutation by shelling out to the co-located script. Two invocation shapes:
Standalone (default) — commit on the current branch:
bun run ${CLAUDE_PLUGIN_ROOT}/skills/task-ensure-ready/ensure_ready_mutate.ts \<path> --mode pass --commitRefuses when an active lease holds the task (Step 3b) — a run in flight owns the file.
Invoked by
/sdlc:task-workfrom a worktree — commit on origin/main:bun run ${CLAUDE_PLUGIN_ROOT}/skills/task-ensure-ready/ensure_ready_mutate.ts \<path-inside-the-worktree> --mode pass --commit-on mainWhen
--commit-on mainis passed and a commit is actually due, the script lands it onorigin/mainthrough an EPHEMERAL worktree offorigin/main— NEVER the author’s checkout ([[D-WK7T-agent-git-writes-worktree-isolated]]). It fetchesorigin/main, applies the mutation against a throwaway worktree off that tip, validates, regenerates the derived docs there, commits, and pushesHEAD:main(re-applying onto the fresh tip on a push race — never rebasing). The author’s checkout and the calling worktree are left untouched. This keeps task-state frontmatter commits onmain; see [[T-SIHV-task-state-frontmatter-commits-on-main-not-worktree-branch]].In the task-work case a lease is normally held, so this path usually writes the lease
gatesand no commit is due at all.--commitand--commit-onare mutually exclusive. Without either, the script edits the file in place but does not stage or commit — the caller owns the commit. The mutation logic lives in exactly that one place — do not re-encode it here.The script emits the PASS marker block on stdout. Relay whatever it wrote; do NOT re-print the marker on top of it (re-printing drifted under parallel dispatch).
-
Validation and the staged-then-committed step happen inside the script when
--commitor--commit-onis passed. If you invoked without either flag, run the validator manually:${CLAUDE_PLUGIN_ROOT}cli/sdlc entities validate <path>If it fails, fix the frontmatter and re-run. Do not commit a doc that fails validation.
-
(Skip if
--commit/--commit-onwas used.) Stage only the task file:git add <path>. -
(Skip if
--commit/--commit-onwas used.) Commit on the current branch, using the promotion subject the lifecycle template renders. -
The script’s stdout carries the marker block (see sub-step 1). The shape is:
ENSURE-READY-OK: <basename>readiness_verified_at: <timestamp-or-->plane: frontmatter-promotion | lease-gates | noneExit with success. Keep the
ENSURE-READY-OK:prefix slug-namespaced verbatim (the bareREADY:form collided with caller context and caused premature exits at the readiness gate; PR #176–#179).
5. Fail — record gap and commit
Section titled “5. Fail — record gap and commit”If the evaluation failed:
-
Compose the gap description first: a one-paragraph (2-6 sentences) description that names each missing/thin section and each triggered disqualifier in plain language. Be specific enough that a reader knows exactly what to fix.
-
Apply the fail mutation by shelling out to the co-located script. Two invocation shapes (same convention as Step 4):
Standalone (default) — commit on the current branch:
bun run ${CLAUDE_PLUGIN_ROOT}/skills/task-ensure-ready/ensure_ready_mutate.ts \<path> --mode fail --gap "<one-paragraph gap description>" --commitInvoked by
/sdlc:task-workfrom a worktree — commit on origin/main:bun run ${CLAUDE_PLUGIN_ROOT}/skills/task-ensure-ready/ensure_ready_mutate.ts \<path-inside-the-worktree> --mode fail --gap "<gap>" --commit-on mainA readiness FAIL is semantic — it changes what work the document needs — so it keeps its commit. The script handles the carve-out automatically: status is preserved when the task holds an ACTIVE LEASE, or when its
status:is the legacyin-progress/in-progress/blocked. Both mean a run is mid-flight and downshifting would corrupt it; the lease arm is the one that matters post-split, since a task in flight now readsopen/readyand the old status-only carve-out would have downshifted live work. Otherwise status is set toplanning/needs-definition. The script also clearsreadiness_verified_at:and leaveslast_reviewed:unchanged. The mutation logic lives in exactly that one place — do not re-encode it here.When
--commit-on mainis used (the task-work-invoked path), the downshift lands onorigin/mainthrough an EPHEMERAL worktree offorigin/main— never the author’s checkout ([[D-WK7T-agent-git-writes-worktree-isolated]]) — soorigin/mainholds the canonical state. The accompanying--cleanup-on-failflag (see “When called by /sdlc:task-work” below) then tears down the abandoned worktree + branch + lease deterministically. See [[T-SIHV-task-state-frontmatter-commits-on-main-not-worktree-branch]].When
--commitor--commit-onis passed, the script ALSO emits the two-line FAIL marker block on stdout —ENSURE-READY-NEEDS- DEFINITION: <basename>followed bydefinition_gap: <one-line>. Relay whatever the script wrote; do NOT re-print the marker on top of it. -
(Skip if
--commit/--commit-onwas used.) Run the validator:${CLAUDE_PLUGIN_ROOT}cli/sdlc entities validate <path>If it fails, fix the frontmatter and re-run.
-
(Skip if
--commit/--commit-onwas used.) Stage only the task file:git add <path>. -
(Skip if
--commit/--commit-onwas used.) Commit on the current branch:docs(tasks): flag <basename> as needs-definition<one-line gap summary> -
The script’s stdout carries the marker block (see sub-step 2). The shape is:
ENSURE-READY-NEEDS-DEFINITION: <basename>definition_gap: <one-line summary>Exit with success — recording an incomplete spec is the correct outcome, not a failure. Keep the
ENSURE-READY-prefix slug-namespaced verbatim (the bareNEEDS-DEFINITION:form collided with orchestrator verdict parsers and with callers that read it as a terminal verdict; PR #176–#179).
When called by /sdlc:task-work — the --cleanup-on-fail contract
Section titled “When called by /sdlc:task-work — the --cleanup-on-fail contract”/sdlc:task-work’s Step 5a invokes this skill from a worktree with
--commit-on main --cleanup-on-fail. The two flags are paired:
--commit-on main lands the downshift commit on origin/main through an
ephemeral worktree off origin/main (never the author’s checkout,
[[D-WK7T-agent-git-writes-worktree-isolated]]), and --cleanup-on-fail
extends the fail path so the script ALSO tears down the abandoned
task-work scaffolding before exiting.
After the downshift commit lands on origin/main, the script:
- Removes the worktree at
<main-repo>/.sdlc/worktrees/<basename>viagit worktree remove --force(no-op if absent). - Deletes the branch
task/<basename>(and the legacyfeat/<basename>defensively) viagit branch -D(no-op if absent). - Releases the lease ref
refs/sdlc/tasks/<basename>by shelling out to${CLAUDE_PLUGIN_ROOT}cli/sdlc lease release refs/sdlc/tasks/<basename>— exit code 5 (REF-NOT-FOUND) is tolerated as already-released.
The script then prints a third stdout line:
cleaned-up: worktree=<state> branch=<state> lease=<state>where each <state> is one of removed / absent (worktree),
deleted / absent (branch), or released / absent (lease).
The marker is informational — the cleanup either succeeded or each
target was already gone before the run started.
Standalone /sdlc:task-ensure-ready callers do NOT set
--cleanup-on-fail — the operator typically wants the worktree
preserved for inspection. The flag is task-work’s opt-in into
deterministic teardown.
-
Edit only frontmatter (the stamp / gap / downshift); leave body gaps to
/sdlc:task-defineor the user. The one exception is the Step 3a autonomy gate: for anautonomy: autonomous/prtask it dispatches/sdlc:task-auto-define, which performs the body edit; this skill itself still writes only frontmatter. -
Autonomy gate (Step 3a). Only
autonomy: autonomous/prtasks trigger the one-shot auto-define-then-re-verify branch;supervised,human-only, and absent autonomy park atplanning/needs-definition. See${CLAUDE_PLUGIN_ROOT}skills/task-auto-define/SKILL.md. -
Commit exactly one commit per run, touching only the task file.
-
Treat the codebase outside the task file as read-only: grep for symbols and
lsfor files, but do not modify them. -
Deterministic gap detection is one call. Step 3 makes ONE
task gap-reportcall where it formerly made four (parse-touchpoints+scan-placeholders+check-claims+resolve-touchpoints). The composite runs the same four checks plus required-section presence and returns one structured object; this skill reads its fields (touchpoints.*,placeholders[],claims[],sections[],gaps[]) and applies the interpretation layer (thin-vs-present, greenfield prose-Today acceptance,bulleted-legacyhard fail, severity weighting).gap-reportis purely deterministic and runs NO LLM — the LLM-judged corpus-assumption advisory stays a SEPARATE call (confirm-before -gap) and is deliberately not ingap-report. The whole change is call-shape (4 → 2), not behavior: flag exactly what the four separate calls flagged. -
Touchpoint shape is table-or-prose, never bulleted.
## Todayand## Files to touchmust be| Location | … |v3 tables (or a pure-prose## Todayfor greenfield);touchpoints.today_kind/touchpoints.files_to_touch_kind == "bulleted-legacy"is a hard fail.task gap-reportis the single parsing+existence path; it reports existence only (touchpoints.unresolved[]) — drift narration and any migrate recommendation are composed in the gap description, not by the op. -
Edge-lesson (parse-error dedup), now N/A inside the op. A touchpoint row the parser rejected used to risk double-reporting (once as a parse error, once as an existence failure).
gap-reportalready de-dups this: rows whose resolvereasonstarts withparse-errorare dropped fromtouchpoints.unresolved[](they are counted once intouchpoints.parse_errors[]). Do not re-report. -
Edge-lesson (
command grepBSD-flag aliasing), now historical / N/A. The symbol-existence find used to be a hand-rolled shell grep that broke when a shell aliasedgrepto anrgrewrite rejecting BSD flags. That probe now runs insidegap-reportin Bun/TS (no shellgrep), so the aliasing class is structurally gone for this gate. The lesson is retained here as historical context: when a gate’s existence check moves into TS, the shell-grep hazard disappears — do not reintroduce a hand-rolledgreploop. -
Terminal stdout markers (the contract surface for callers):
Marker Meaning Mutation ENSURE-READY-OK: <basename>Contract satisfied Per the plane:line — see Step 4ENSURE-READY-NEEDS-DEFINITION: <basename>Contract failed Downshift commit, unless a lease is held ENSURE-READY-PARENT-ROLLUP: <basename>Parent task; contract does not apply None ENSURE-READY-LEASED: <basename>--commitrefused: a run holds this taskNone ENSURE-READY-NO-TASK-FOUNDArgument resolved to nothing None ENSURE-READY-AMBIGUOUS: <arg>Argument matched several tasks None