Skip to content

T-RDKI-extract-task-close-out-skill

Status: closed/done · Impact: high · Complexity: small

Pull Step 11 of /sdlc:task-work (post-merge close-out) into a standalone, callable skill /sdlc:task-close-out <task-slug>. The orchestrator invokes it when /sdlc:pr-check returns MERGED; /sdlc:task-work keeps its Step 11 prose but delegates to the new skill, so the close-out behavior lives in exactly one place. Today there is no entry point to run Step 11 against an already-merged task — the user has to re-enter the whole 13-step skill, which is wrong shape for an orchestrator dispatch.

plugin/skills/task-work/SKILL.md Step 11 documents the close-out procedure inline:

  • 11a: edit task file to status: closed/done, update last_reviewed, add completion_note: "Shipped via #<pr-number>.", drop relevance_note, clear readiness_verified_at; commit on main; push.
  • 11b: tear down worktree (git worktree remove --force .claude/worktrees/<basename> + prune).
  • 11c: delete local + remote branches.
  • 11d: verify (log shows close-out commit; git worktree list clean; branch gone).

The skill body says this should fire “whenever the user reports the PR is merged, or when you notice the merge as part of any later check.” There is no callable entry point — invoking /sdlc:task-work <slug> against an already-merged task would re-trigger the pre-flight checks and fail (Step 2 blocks if a worktree already exists for the task, etc.). Current evidence of the gap: 2026-05-19-add-epic-entity-task-depends-on-dependencies is status: in-progress despite PR #18 being merged on 2026-05-19; no automated path got it to closed/done.

New skill at plugin/skills/task-close-out/SKILL.md invocable as /sdlc:task-close-out <task-slug-or-path>. Inputs:

  • Task slug (basename without .md) OR absolute path to task file.
  • Optional --pr <N> to override PR detection (default: derive from branch feat/<basename> → most recent merged PR with that head).

Procedure (mirrors task-work Step 11 with one extension):

  1. Resolve task file via the same file-resolution rules as /sdlc:task-work (use plugin/entities/task/file-resolution.md).

  2. Verify task is eligible to close: status: in-progress OR status: in-progress/blocked. If closed/*, exit ALREADY-CLOSED. If ready or earlier, exit NOT-IN-PROGRESS error="task hasn't started".

  3. Detect the merged PR:

    gh pr list --search "head:feat/<basename> is:merged" --state merged --json number,mergedAt,mergeCommit --limit 1

    If none found and --pr not passed, exit NO-MERGED-PR.

  4. Run 11a: edit frontmatter (status: closed/done, last_reviewed: <today UTC>, add completion_note with PR number, drop relevance_note, clear readiness_verified_at); commit on main with docs(tasks): mark <basename> closed/done and a “Shipped via #.” body; pull —rebase —autostash; push.

  5. Run 11b: tear down worktree at .claude/worktrees/<basename> (skip silently if not present — supports “task closed manually but worktree forgotten” case).

  6. Run 11c: delete feat/<basename> locally (safe -d, fall back to logged warning if unmerged); delete origin/feat/<basename> if present; fetch —prune.

  7. Run 11d: verify and emit DONE pr=#<N> worktree=removed branch=deleted on success, or PARTIAL <what-couldn't-be-done> if any teardown step failed.

Refactor of /sdlc:task-work Step 11 to delegate: replace the inline prose with “invoke /sdlc:task-close-out <basename> and report its return.” This keeps the behavior in one place and means future close-out tweaks land in one file.

  1. Read plugin/skills/task-work/SKILL.md Step 11 carefully — capture every side effect.
  2. Scaffold plugin/skills/task-close-out/SKILL.md with allowed-tools: [Bash, Read, Edit].
  3. Port Step 11 prose, verbatim where possible, into the new skill. Add the PR-detection + pre-flight (steps 2–3 above).
  4. Edit plugin/skills/task-work/SKILL.md Step 11 to delegate. Keep the “when to invoke” guidance (“on user report of merge, or when noticed later”); replace the inline 11a–11d with “invoke /sdlc:task-close-out and report.”
  5. Add plugin/skills/task-close-out/invariants.yaml: “must verify PR is merged before editing task file”; “must clear readiness_verified_at on close”; “must commit on main, not on feature branch”; “must run pull —rebase —autostash before push to avoid race with concurrent orchestrator ticks.”
  6. Manually run /sdlc:task-close-out 2026-05-19-add-epic-entity-task-depends-on-dependencies to reconcile the existing stale state. (This is itself the first orchestrator-reachable cleanup.)
  • plugin/skills/task-close-out/SKILL.md (new) — the extracted close-out procedure.
  • plugin/skills/task-close-out/invariants.yaml (new) — lint invariants.
  • plugin/skills/task-work/SKILL.md — Step 11 prose replaced with delegation to /sdlc:task-close-out.
  • docs/planning/tasks/2026-05-19-add-epic-entity-task-depends-on-dependencies.md — closed out as part of acceptance verification (not in the same commit as the skill addition; treat as a follow-on dry-run).
  • AC-1: /sdlc:task-close-out <slug> exists and is invocable. (auto)
  • AC-2: Invoking against a known-merged task (2026-05-19-add-epic-entity-task-depends-on-dependencies, PR #18) flips frontmatter to status: closed/done with completion_note referencing #18, removes the worktree at .claude/worktrees/2026-05-19-add-epic-entity-task-depends-on-dependencies, deletes the feat/... branch local + remote, and emits DONE pr=#18 .... (agent-manual)
  • AC-3: Invoking against a non-merged task returns NO-MERGED-PR and makes no file edits, no commits, no branch deletions. (agent-manual: try on 2026-05-19-build-import-planning-skill which is in-progress without a merged PR)
  • AC-4: Invoking against an already-closed task returns ALREADY-CLOSED and is a no-op. (auto via fixture or agent-manual)
  • AC-5: /sdlc:task-work Step 11 prose delegates to /sdlc:task-close-out and no longer contains the inline edit/commit/teardown procedure. The behavior change is captured by following the delegation chain. (auto: grep task-work/SKILL.md for the literal git worktree remove and confirm it’s gone)
  • AC-6: lint_skill_prose.py passes for the new invariants. (auto)
  • Detecting closed/wontdo / closed/superseded paths (where there’s no PR to point at). The orchestrator only calls close-out on the MERGED verdict; the other terminal states are still set manually by the user or by task-review.
  • A “rollback” mode for accidentally closed-out tasks.
  • Migration of existing stale worktrees beyond the one we test on. The orchestrator (once shipped) handles the rest naturally.
  • none

Phase 1 of epic T-FQCN-self-driving-orchestrator-loop. Identified during conversation 2026-05-19: the orchestrator needs a callable close-out step, and task-work Step 11 is the natural home for the behavior but the wrong shape for orchestrator dispatch. Extraction is also cleanup — having close-out logic live in one place is good independently of the orchestrator landing.

Captured by /sdlc:task-work on 2026-05-20. PR: pending.

The AC fixtures named in the spec (the add-epic-entity-task-depends-on-dependencies and build-import-planning-skill tasks) were already closed/done before this run started — they were the motivating examples back when the spec was written, but the gap they pointed at has since been reconciled manually. Verification used synthetic-fixture traces instead.

  • AC-1: auto — plugin/skills/task-close-out/SKILL.md exists and is the documented invocation surface; lint_skill_prose.py confirms structural conformance.
  • AC-2 (happy path): agent-manual — synthetic trace against feat/2026-05-19-add-pr-check-skill. Step 3’s gh pr list --search "head:feat/... is:merged" returns PR #39 (mergedAt: 2026-05-20T03:36:41Z). With auto-detected --pr 39, Step 4 would edit frontmatter (already done by the prior close-out, no-op), Step 5 would skip worktree (already removed → worktree=absent), Step 6 would skip branch deletion (already gone → branch=deleted via the empty check). Final marker: DONE pr=#39 worktree=absent branch=deleted. The auto-detection query and the gate-on-no-changes-needed semantics are both demonstrated; the file-edit path itself is structurally identical to the manual close-out that just ran.
  • AC-3 (NO-MERGED-PR): agent-manual — synthetic trace against this very task’s branch (feat/2026-05-19-extract-task-close-out-skill). Query returns [] (PR isn’t open yet). Procedure emits NO-MERGED-PR reason="no merged PR found for head feat/2026-05-19-extract-task-close-out-skill" and exits. Crucially, this trace fires before any file edits, commits, or branch deletions — the invariants linter pins the literal “no file edits, no commits, no branch deletions” phrase in Step 3 to enforce this property going forward.
  • AC-4 (ALREADY-CLOSED): agent-manual — synthetic trace against 2026-05-19-add-pr-check-skill.md (status: closed/done). Step 2 gate trips on closed/* prefix, emits ALREADY-CLOSED reason="status is closed/done", exits without reaching Step 3. No state changes.
  • AC-5: auto — command grep "git worktree remove\|11a\.\|11b\.\|11c\.\|11d\." against plugin/skills/task-work/SKILL.md returns no matches. The inline 11a–11d substeps are gone; Step 11 is a one-paragraph delegation to /sdlc:task-close-out. docs/skills/task-work.md flowchart collapsed accordingly to a single S11 node.
  • AC-6: auto — lint_skill_prose.py plugin/skills/*/SKILL.md passes 18/18 (6 with invariants files: task-work, import-planning, milestones-from-file, pr-check, project-cleanup, task-close-out — all clean).
  • The forbidden_phrases mechanism that pinned pr-check’s read-only stance worked again here: invariants pin the verdict enum, the no-mutation property on NO-MERGED-PR, and the autostash race-mitigation as load-bearing prose claims.
  • The Step 11 delegation refactor in task-work collapsed cleanly — one paragraph of prose replaces four substeps with no behavior loss because the close-out skill carries all the detail.
  • AC verification by synthetic-fixture trace (gh queries + gate-logic walk) was faster and safer than invoking the skill live against the repo state. The traces are reproducible and don’t need real GitHub state mutation to demonstrate.
  • AC-2 and AC-3 fixture rot: the named tasks (add-epic-entity-task-depends-on-dependencies, build-import-planning-skill) were both already closed/done by the time this task was picked up, breaking the literal AC wording. Required a triage step and a user decision (synthetic fixtures) before any code was written. A task-review-style relevance check on AC fixture references would catch this earlier; broader fix is the orchestrator itself, which will keep fixture references fresh by closing tasks promptly. → T-IXV6-task-review-checks-ac-fixture-relevance
  • The skill cannot be exercised end-to-end without /reload-plugins after the worktree’s plugin/skills/task-close-out/ exists, and even then the skill’s mutations (branch delete, worktree teardown) are destructive enough that real live testing requires throwaway fixtures. A --dry-run flag that printed each side effect without executing would let close-out be exercised against any in-progress task safely. → T-Y81J-task-close-out-dry-run-flag
  • Step 4’s “race against concurrent orchestrator ticks via pull --rebase --autostash” remains spec-only. There’s no integration test for the race itself; we’re trusting git’s well-known semantics. Worth a small fixture once the orchestrator is shipping ticks. → T-LN0B-orchestrator-tick-race-fixture
  • gh pr list --search "head:feat/<basename> is:merged" can return false positives if the same feat/<basename> name was reused across PRs (rare but possible). The skill takes --limit 1 which is the most-recent — fine for now, but a sanity-check that the returned PR’s mergeCommit is actually reachable from origin/main would harden against this. → T-RNW9-task-close-out-verifies-pr-on-main

← Back to Tasks