Skip to content

/sdlc:task-ensure-ready

Generated from solutions/ontological/skills/task-ensure-ready/SKILL.md.

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.

  • Read
  • Edit
  • Bash
  • Grep
  • Glob
  • Skill
  • Agent

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 against docs/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.

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.

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.

  1. Read the task file end-to-end.
  2. Read ${CLAUDE_PLUGIN_ROOT}/entities/task/implementation-ready.md.

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 sections gap-report reports under sections[] 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 json

    task gap-report is the deterministic NO-LLM composite that runs the four former separate checks — parse-touchpoints, scan-placeholders, check-claims, and resolve-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 and sdlc task gap-report --help for the full output shape. A missing or unreadable file is INVALID_INPUT (exit 1); the single-line marker is GAP-REPORT gaps=N sections=N placeholders=N touchpoints=N claims=N parent=<bool> (exit 0). The op is read-only. The parenthood slice ({ 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_kind and touchpoints.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 *_kind tokens.
    • 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 into gaps[] under category: "touchpoint".
    • Touchpoint EXISTENCE/RESOLUTION → read touchpoints.unresolved[] ({section, location, kind, reason}): every entry is a disqualifier. The op reports EXISTENCE only — each reason is a machine token (file-missing, symbol-missing, directory-missing, glob-no-match, …); relay its location and reason into 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 historical command grep-vs-grep BSD-flag aliasing — see the edge-lesson note in §Notes); directory resolves iff the directory exists; glob resolves iff it expands to ≥1 match; kind: new rows fire NO existence check (resolved: true, never in unresolved[]); each ## Today row resolves as a modify row (its kind comes back modify). Rows the parser already rejected are NOT double-listed here — the op drops reason: "parse-error: …" rows from unresolved[] because they are already counted in parse_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 with present == false is a missing-section disqualifier.
    • Claim resolvers → read claims[] ({resolver, line, severity, message}): each severity: "disqualifier" finding is a fail reason — relay its message (and line, if helpful) into the gap description. severity: "warning" findings are informational only — never a gap (no resolver emits warning today, but the tier exists). The resolver contracts (what each claim kind checks) are documented in code under solutions/ontological/lib/model/entities/task/claims/.
    • Aggregated disqualifiersgaps[] ({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). Use gaps[] 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 place gaps[] and your verdict can diverge is the interpretation layer: a today_kind == "prose" Today is greenfield-ACCEPTED (NOT a gap) even though the op reported the kind; a bulleted-legacy Today/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.
  • 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 / ## Proposed with 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 through tail/head when 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:

    1. Dispatch /sdlc:task-auto-define <absolute-path-to-task-file> (via the Skill / Agent tool). It synthesizes the missing/thin sections from the task’s prose + codebase context, stamps a discoverable machine-authored AUTO-DEFINED: marker, and commits the body edits on the current branch. Its terminal marker is one of TASK-AUTO-DEFINE-DEFINED: / TASK-AUTO-DEFINE-NO-CHANGES: / TASK-AUTO-DEFINE-INSUFFICIENT: (or the shared ERROR reason=...) — all slug-namespaced, so do not mistake them for this skill’s own ENSURE-READY- verdict.
    2. 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.)
    3. Decide on the re-verify result:
      • Re-verify passes → proceed to Step 4 (stamp + ENSURE-READY-OK:). The AUTO-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-define a second time in this run (loop guard against a deterministically-failing definer). One attempt, then park.
  • 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 via autonomous/pr only.

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.

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.

SituationWhat happensplane:
No active lease, status: in planning/*ONE promotion commit: status: open/ready + readiness_verified_at:frontmatter-promotion
No active lease, already open/readyNothing. 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 taskCAS-write the lease’s gates object. Zero commits on main, no frontmatter writelease-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.

  1. 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 --commit

    Refuses when an active lease holds the task (Step 3b) — a run in flight owns the file.

    Invoked by /sdlc:task-work from 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 main

    When --commit-on main is passed and a commit is actually due, the script lands it on origin/main through an EPHEMERAL worktree off origin/main — NEVER the author’s checkout ([[D-WK7T-agent-git-writes-worktree-isolated]]). It fetches origin/main, applies the mutation against a throwaway worktree off that tip, validates, regenerates the derived docs there, commits, and pushes HEAD: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 on main; 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 gates and no commit is due at all.

    --commit and --commit-on are 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).

  2. Validation and the staged-then-committed step happen inside the script when --commit or --commit-on is 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.

  3. (Skip if --commit / --commit-on was used.) Stage only the task file: git add <path>.

  4. (Skip if --commit / --commit-on was used.) Commit on the current branch, using the promotion subject the lifecycle template renders.

  5. 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 | none

    Exit with success. Keep the ENSURE-READY-OK: prefix slug-namespaced verbatim (the bare READY: form collided with caller context and caused premature exits at the readiness gate; PR #176–#179).

If the evaluation failed:

  1. 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.

  2. 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>" --commit

    Invoked by /sdlc:task-work from 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 main

    A 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 legacy in-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 reads open/ready and the old status-only carve-out would have downshifted live work. Otherwise status is set to planning/needs-definition. The script also clears readiness_verified_at: and leaves last_reviewed: unchanged. The mutation logic lives in exactly that one place — do not re-encode it here.

    When --commit-on main is used (the task-work-invoked path), the downshift lands on origin/main through an EPHEMERAL worktree off origin/main — never the author’s checkout ([[D-WK7T-agent-git-writes-worktree-isolated]]) — so origin/main holds the canonical state. The accompanying --cleanup-on-fail flag (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 --commit or --commit-on is passed, the script ALSO emits the two-line FAIL marker block on stdout — ENSURE-READY-NEEDS- DEFINITION: <basename> followed by definition_gap: <one-line>. Relay whatever the script wrote; do NOT re-print the marker on top of it.

  3. (Skip if --commit / --commit-on was used.) Run the validator:

    ${CLAUDE_PLUGIN_ROOT}cli/sdlc entities validate <path>

    If it fails, fix the frontmatter and re-run.

  4. (Skip if --commit / --commit-on was used.) Stage only the task file: git add <path>.

  5. (Skip if --commit / --commit-on was used.) Commit on the current branch:

    docs(tasks): flag <basename> as needs-definition
    <one-line gap summary>
  6. 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 bare NEEDS-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:

  1. Removes the worktree at <main-repo>/.sdlc/worktrees/<basename> via git worktree remove --force (no-op if absent).
  2. Deletes the branch task/<basename> (and the legacy feat/<basename> defensively) via git branch -D (no-op if absent).
  3. 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-define or the user. The one exception is the Step 3a autonomy gate: for an autonomy: autonomous/pr task 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/pr tasks trigger the one-shot auto-define-then-re-verify branch; supervised, human-only, and absent autonomy park at planning/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 ls for files, but do not modify them.

  • Deterministic gap detection is one call. Step 3 makes ONE task gap-report call 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-legacy hard fail, severity weighting). gap-report is purely deterministic and runs NO LLM — the LLM-judged corpus-assumption advisory stays a SEPARATE call (confirm-before -gap) and is deliberately not in gap-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. ## Today and ## Files to touch must be | Location | … | v3 tables (or a pure-prose ## Today for greenfield); touchpoints.today_kind / touchpoints.files_to_touch_kind == "bulleted-legacy" is a hard fail. task gap-report is 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-report already de-dups this: rows whose resolve reason starts with parse-error are dropped from touchpoints.unresolved[] (they are counted once in touchpoints.parse_errors[]). Do not re-report.

  • Edge-lesson (command grep BSD-flag aliasing), now historical / N/A. The symbol-existence find used to be a hand-rolled shell grep that broke when a shell aliased grep to an rg rewrite rejecting BSD flags. That probe now runs inside gap-report in Bun/TS (no shell grep), 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-rolled grep loop.

  • Terminal stdout markers (the contract surface for callers):

    MarkerMeaningMutation
    ENSURE-READY-OK: <basename>Contract satisfiedPer the plane: line — see Step 4
    ENSURE-READY-NEEDS-DEFINITION: <basename>Contract failedDownshift commit, unless a lease is held
    ENSURE-READY-PARENT-ROLLUP: <basename>Parent task; contract does not applyNone
    ENSURE-READY-LEASED: <basename>--commit refused: a run holds this taskNone
    ENSURE-READY-NO-TASK-FOUNDArgument resolved to nothingNone
    ENSURE-READY-AMBIGUOUS: <arg>Argument matched several tasksNone