Skip to content

/sdlc:task-ensure-ready

Generated from solutions/ontological/plugin/plugins/sdlc/skills/task-ensure-ready/task-ensure-ready.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 state to planning/needs-definition — except when the input is already in-progress or in-progress/blocked, in which case state 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 states 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}/lib/model/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}/lib/model/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}/lib/model/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: an Areas section is a category error 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}/lib/model/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}/lib/model/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 deterministic checks — parse-touchpoints (informational-only ## Areas shape), scan-placeholders, and check-claims — plus required-section presence in ONE call, and returns ONE structured object. 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 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, 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 separate calls used to drive:

    • Touchpoint shape → read touchpoints.areas_kind (∈ prose / table / missing / bulleted-legacy) and touchpoints.parse_errors[]. As of v7, ## Areas is advisory and ## Today is fully free-form prose — neither is a resolution gate. areas_kind and parse_errors are informational only: whatever shape or row-count defect they show, it is NEVER a gap. Do not add a hard fail for any *_kind value here; sections[] is the sole authority on what presence costs.
    • 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 disqualifiers → gaps[] ({category, code, section, line, message}) is the flat union of the deterministic disqualifiers (section / placeholder / claim categories — touchpoint no longer exists as of v7, since nothing about ## Today or ## Areas gates readiness), 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. Flag exactly what gaps[] flags — do not invent an additional hard fail from touchpoints.areas_kind (it is informational only, per the Touchpoint-shape bullet above); a verdict that flags MORE or FEWER gaps than gaps[] is the failure mode.
  • Subjective-AC disqualifiers: read the body (LLM judgment, not in gap-report).

  • Corpus-assumption disqualifier (LLM-judged, advisory scanner). See ${CLAUDE_PLUGIN_ROOT}/conventions/corpus-assumptions-scanner.md for what it checks and how to confirm a candidate; keep it its OWN separate call:

    ${CLAUDE_PLUGIN_ROOT}/cli/sdlc task scan-corpus-assumptions <path>

    Only a candidate you CONFIRM against the cited line becomes a gap — fold it into the gap description you compose in Step 5.

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 — placeholder/missing-section — plus any disqualifier-severity claim-resolver finding), and any confirmed corpus-assumption gap. Nothing about ## Today or ## Areas shape contributes a reason — see the Touchpoint-shape bullet above.

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 state: + 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 19) 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, state: in planning/*ONE promotion commit: state: 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 state 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 op. Two invocation shapes:

    Standalone (default) — commit on the current branch:

    ${CLAUDE_PLUGIN_ROOT}/cli/sdlc task ensure-ready-mutate \
    <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:

    ${CLAUDE_PLUGIN_ROOT}/cli/sdlc task ensure-ready-mutate \
    <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).

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 op. Two invocation shapes (same convention as Step 4):

    Standalone (default) — commit on the current branch:

    ${CLAUDE_PLUGIN_ROOT}/cli/sdlc task ensure-ready-mutate \
    <path> --mode fail --gap "<one-paragraph gap description>" --commit

    Invoked by /sdlc:task-work from a worktree — commit on origin/main:

    ${CLAUDE_PLUGIN_ROOT}/cli/sdlc task ensure-ready-mutate \
    <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: state is preserved when the task holds an ACTIVE LEASE, or when its state: 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 state-only carve-out would have downshifted live work. Otherwise state 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 — 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).

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), 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 13 (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 / unconfigured (lease). The marker is informational — the cleanup either succeeded or each target was already gone before the run started. unconfigured means this project does not lease at all (no lease_authority and no SDLC_LEASE_AUTHORITY), so no release was attempted.

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, composing scan-placeholders, check-claims, and a read of parse-touchpoints (informational ## Areas shape only) plus required-section presence into one structured object; this skill reads its fields (touchpoints.*, placeholders[], claims[], sections[], gaps[]) and applies the interpretation layer (thin-vs-present, severity weighting). gap-report is purely deterministic and runs NO LLM — the corpus-assumption advisory stays a SEPARATE call; see ${CLAUDE_PLUGIN_ROOT}/conventions/corpus-assumptions-scanner.md.

  • ## Today and ## Areas are advisory, never a resolution gate (v7). ## Today is fully free-form prose; ## Areas is an advisory list of packages/directories/modules. Neither is resolved against the working tree by any op, and no shape (table, prose, bulleted) or content of either ever produces a gaps[] entry. touchpoints.areas_kind / touchpoints.parse_errors[] exist for a caller that wants to narrate the shape, not to gate on it. This replaced the old v3–v6 ## Files to touch contract (a strict | Location | Kind | Change | table whose modify/delete rows had to resolve against the codebase, with a bulleted-legacy hard fail) — do not resurrect any of that machinery here.

  • 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