worktree-pipeline-skill
Tracker-ticket-to-merged-PR pipeline via git worktrees — sync, plan, adaptive review, /run-plan, code review, PR merge. Triggers: run-worktree-pipeline, worktree pipeline, ticket to PR pipeline, track
SKILL.mdskills/worktree-pipeline-skill/SKILL.md
GitHubWhat I do
I run the full ticket-to-merged-PR pipeline, one ticket at a time, each in
its own git worktree so the main working tree stays free. I am the
orchestrator: heavy knowledge lives in the skills/subagents I drive
(ticketing-skill for new tickets, plan-execution-inline-skill by
default (plan-execution-skill --gate on explicit subagent opt-in) for
execution, skill civiltekk-pr-workflow-skill create route by default
(pr-workflow-subagent on the opt-in subagent arm) for the PR) — I own
sequencing, PLAN authoring, worktree lifecycle, and re-validation.
Usage: /run-worktree-pipeline [--dry-run] [base-branch] <ticket-refs...>
Step 1 — Parse arguments
- Leading
---flags are stripped before the first-token test (--dry-runis the only flag). - First token is a base-branch iff it fails the ticket regex
^(tracker#\d+|[\w.-]+/tracker#\d+|#\d+|[\w.-]+/[\w.-]+#\d+|[A-Z][A-Z0-9]+-\d+|[\w.-]+/[A-Z][A-Z0-9]+-\d+)$and is not purely numeric. Full first-token taxonomy, in test order:--flags → ticket forms (#NGitHub ·owner/repo#Ncross-repo GitHub · bare numericN(auto-#N) ·KEYtracker ·repo/KEYcross-repo tracker ·tracker#Ntracker-repo GitHub ·repo/tracker#Ncross-repo work + tracker-repo ticket) → base-branch fallthrough. Every accepted token shape is listed here — a variant that matches none of them is the base-branch, never silently dropped. Thetracker#…alternatives sit before the genericowner/repo#Nin the regex —trackeris a reserved repo-segment word in this position; a GitHub repo actually namedtrackermust be referenced by an explicit full-owner/repo#Nform that does not collide. - Bare numerics (
351) auto-normalize to GitHub issue refs (#351). repo/KEYcross-repo refs (acme-workflow-engine/ABC-123) name a foreign repo: resolve it to the sibling checkout../<repo>relative to the main checkout; it must exist and be a git repo — missing → one batched user ask for the local path, still unresolved → abort with a clear error. BareKEY/#N/owner/repo#Nbehave exactly as before (session repo or named GitHub repo). Everything repo-scoped downstream — base branch, its validation, the worktree root, and the gh context — resolves per ticket's repo (Steps 2–4 rungit -C <repo>andgh ... -R <owner/name>for foreign repos; the session repo is unchanged).tracker#N/repo/tracker#Ntracker-repo refs: the ticket lives in the org's issue-tracker GitHub repo —$TRACKER_REPO, resolved from theWORKTREE_PIPELINE_TRACKER_REPOenv override (owner/name), defaultbetekk/canvastekk-tracker.tracker#Nworks the session repo (branchfeat/N, KEYN);repo/tracker#Nworks the sibling../<repo>checkout (same resolution and abort rules asrepo/KEY). Ticket fetch/transition/close run against$TRACKER_REPO(gh issue view -R <TRACKER_REPO> N); everything work-scoped — base branch, validation, worktree root, PR gh context, merged-ticket check — resolves per the work repo, exactly as#N/repo/KEYdo. The normalized$TICKET_IDis the fully-qualified<owner/name>#N(e.g.betekk/canvastekk-tracker#3982) so the PR body'sCloses <TICKET_ID>resolves cross-repo in the work repo.- Zero ticket refs → print usage and stop.
- The base-branch sets both where feat branches are cut from AND the PR
target. Default (omitted): repo default branch via
git symbolic-ref --short refs/remotes/origin/HEAD(yieldsorigin/<base>; strip the prefix; fallbackmain). - Validate the base after resolving it (per repo for
repo/KEYtickets):git ls-remote --exit-code --heads origin <base>; non-zero exit → abort with a clear error naming the attempted base (fail-fast — never reach Step 2 with a typo'd base). --dry-run: print the resolved base (per repo), ticket execution order, per-ticket predictions — merged / held-on-blocked-by:/ held-on-open-PR-overlap — which PRs will get background merge watchers, and the would-befeat/<KEY>branch + worktree names (per repo), then stop before Step 2. Read-only: no writes, no branch/worktree/remote mutations.- Arm selection (default inline — capability binding per AGENTS.md
§Portability contract): every invocation runs the inline arm —
fully in-session, zero subagents — unless the subagent arm is opted
in, which requires ALL three: (1) the user explicitly requested
subagent orchestration, (2) the harness is OpenCode, (3) the subagent
deps resolve (preflight below). Any unmet condition → run inline with a
prominent note (requested-but-unavailable → say so and proceed inline;
never abort on a subagent-arm miss once the inline deps resolve).
- OpenCode: subagent tool + deployed agents — opt-in only
- Claude Code: subagents not wired for this skill — inline always
- Other/none: inline always
- Dependency preflight (per-skill installs, resolved per arm): the
inline arm (the default) hard-requires skill
plan-execution-inline-skill(Step 8) + skillcode-review-inline-skill(Step 9 — the wrapper resolves the deployedagents/code-review-subagent.mdchecklist itself) + skillciviltekk-pr-workflow-skill(Step 10, create route), resolved by the skill loader. The opt-in subagent arm hard-requires skillplan-execution-skill--gate (Step 8) and agentscode-review-subagent(Step 9) +pr-workflow-subagent(Step 10). Any missing dep for the arm actually running → abort (failed) with the install hintnpx github:darellchua2/civiltekk-opencode-claude-skills add <name>(sole exception: the opted-in subagent arm with unresolvable deps falls back inline per the arm-selection rule). Soft deps degrade with a note:ticketing-skill(only for new-work tickets, Step 3),architecture-review-subagent/uiux-reviewer-subagent/requirements-specialist-subagent(Step 7 skip-with-note rule),requirements-inline-skill(inline arm only — the Step 7 detection gate is a quality gate, not a correctness backstop; skip-with-note when absent). The inline arm routes architecture review to skillarchitecture-review-skill(reviewer-baseline-skill first) instead of the deployed agent file. - Execution model (pipelined): ticket order = authoring order, but only one implementation runs at a time. The next ticket's implementation starts once the active ticket has created its PR (Step 10a) — not once it merges — AND this ticket's own blockers (below) have merged; unblocked tickets never wait on CI. A held ticket (blocked-by, 6f, or 10a overlap) releases the implementation lane — the next implementable ticket starts immediately. Each PR ships with a background merge watcher (Step 10b), so any number of PRs may be awaiting merge concurrently while the next implementation proceeds.
blocked-by:hold, not skip: if a ticket's body containsblocked-by: <ref>naming a ticket that is not yet merged (an open PR counts as unmerged), hold it — report as held, keep it in run order. Evaluation point: the Step 3 body fetch (dry-run predicts it earlier); a blocked-by hold parks with whatever state exists — typically none, before branch/worktree/PLAN. Auto-resume when the blocker's merge notification arrives: re-enter at the first unexecuted step — rebasefeat/<KEY>onto the updated base only if the branch already exists (push--force-with-leaseafter a resume rebase). Contrast: 6f/10a overlap holds park AFTER PLAN authoring, so their resume continues at Step 7 / 10a. Tickets still held when nothing else is runnable are reported deferred at run end, not failed. (No tracker link traversal — body text only.)
Steps 2-10 — per ticket (in order)
Sync + branch (all git/gh run in the ticket's repo —
git -C <repo>andgh ... -R <owner/name>for foreign repos; session repo unchanged):git fetch origin <base>. Merged-ticket check:gh pr list --state merged --head feat/<KEY>non-empty → the ticket is already merged; report the skip with a note and advance to the next ticket. Otherwise cutgit branch feat/<KEY> origin/<base>. If the branch or worktree already exists (mid-pipeline failure leftovers OR a held ticket resuming), report state and ask: prune / resume / refuse — never clobber silently; resume is the held path (Step 1): rebasefeat/<KEY>onto the updatedorigin/<base>(push--force-with-leaseafter the rebase) and re-enter at the first unexecuted step.Ticket fetch/create: existing ref → fetch its description (
gh issue view [-R <owner/name>];tracker#N/repo/tracker#N→gh issue view -R <TRACKER_REPO> N; tracker tickets perticketing-skill§MCP Availability Guard — degrade with a clear report when the tracker is unavailable). New work → create the ticket first viaticketing-skill(/create-ticket), then continue.Worktree: locate the main checkout via
git worktree list --porcelain | sed -n 's/^worktree //p' | head -1(NOT$(git rev-parse --show-toplevel)— that nests when invoked from a worktree). Creategit worktree add <root>/<KEY> feat/<KEY>— always, even when the ticket is in this repo.<root>is$WORKTREE_PIPELINE_ROOTwhen set (applies to ALL tickets — foreign repos included, a documented asymmetry), else<ticket-repo>/../worktrees/— derived from the ticket's repo, sorepo/KEYtickets get a worktree root beside their own checkout. Pre-flightgit worktree listfor stale<KEY>entries. CodeGraph index (conditional): iff the ticket repo's checkout has.codegraph, rungit -C <root>/<KEY> check-ignore -q .codegraph/first — exit 0 (ignored on the ticket branch) → runnpx @colbymchenry/codegraph init -iinside the new worktree (before Step 5; 5–60s, index gitignored); exit 1 → skip init entirely with a one-line note (".codegraph/ not ignored in target repo — skipping init to keep commits clean") and continue on the rg/grep fallback (any othercheck-ignoreexit → treat as the same soft-skip path); CLI absent or init failure → one-line soft-skip note and continue on rg/grep. No.codegraph/in the ticket repo's checkout → skip silently. Never write ignore entries (tracked.gitignoreedits stage into per-phase commits; per-worktreeinfo/excludeis not honored by linked worktrees). Never symlink the main checkout's.codegraph/into the worktree — the index reflects the main checkout's branch state and paths (sharing undocumented). Ticket start transition (worktree exists = work began): once the worktree is created, tracker tickets get theticketing-skill§Start transition (check-first idempotent; GitHub issues = no-op with a note — no status field,Closes #Ncovers close-on-merge). The Atlassian §MCP Availability Guard applies — JIRA unavailable → report the transition skipped, never block the run. A ticket that later fails or stays held legitimately remains In Progress (ticketing-skill§Start honest-state rule).--dry-runand/worktree-pipeline-previewstay read-only: they stop before any mutation and never transition.Re-validate: cross-check the ticket description once more against the latest
origin/<base>content in the worktree; if stale, update the ticket and note deltas before proceeding.PLAN authoring (self-contained — this skill owns it; see §PLAN Authoring): adopt/generate the ticket-scoped PLAN in the worktree, run the atomicity self-check, commit and push it on
feat/<KEY>.Plan review (§Adaptive Review): you triage before delegating — from the ticket, the PLAN's Dependency & Consumer Map, and the touched paths, select reviewers, then issue parallel Task calls for the selected ones only (a selected reviewer absent from this session's agent list → skip it with a note; per-skill installs may not carry every reviewer):
architecture-review-subagentiff the Consumer Map has cross-module nodes (a consumer beyond the node itself). Default (inline arm): run the review in-session via skillarchitecture-review-skill(reviewer-baseline-skill first) — the Task-call route below is the opt-in subagent arm.uiux-reviewer-subagentiff frontend signal (tsx/jsx/vue/svelte/css files, components/pages/app paths, UI keywords in the diff). Requirements detection gate (before reviewer selection): run the inline skillrequirements-inline-skill— its decision tree inspects the ticket + PLAN acceptance criteria (thin set, ambiguity markers, conflicting ACs) plus the triage outcome (zero reviewers selected on new-work tickets) and either skips with a one-line reason or interrogates the ACs inline (grilling-shaped, answers applied to the ticket/PLAN before reviewers run). This is data-driven AC interrogation, NOT a PLAN review by requirements-specialist — the stage-mismatch decision stands. Inline arm: the skill degrades soft with a note when absent (quality gate, not a correctness backstop); subagent arm: skip the gate. No proactive requirements review — requirements coverage is reviewer-owned: each selected reviewer verifies the PLAN against the ticket's stated requirements and emits Requirements Gaps for anything missing or ambiguous (never a silent assumption). A thin-map backend ticket may select zero reviewers — Step 9 code review (unconditional) backstops, and the detection gate above covers AC quality when no reviewer runs. Requirements Gaps relay: any reviewer (here or Step 9) returning a non-emptyRequirements Gapsarray → relay it (inline arm: invokerequirements-inline-skillMode R — it loads the deployedrequirements-specialist-subagent.mdas checklist; subagent arm: delegate torequirements-specialist-subagentMode R) and apply the answers to the PLAN before proceeding (max 2 relay rounds — agent contract bound; skill/agent absent → surface the gaps to the user directly and proceed on their answers). Triage assumptions (stated, not hidden): a thin Consumer Map may skip architecture review, so author the map honestly at Step 6.coverage-subagentis NOT part of plan review — it is a coverage reporting agent, so reviewing a pre-implementation PLAN is a stage mismatch (nothing measurable exists yet). Apply findings to the PLAN; re-review only when findings were structural. Zero selected reviewers → skip delegation entirely.
Execute: run
plan-execution-inline-skillwithPLANS/PLAN-${KEY}.mdinside the worktree — always pass the explicit PLAN path, never rely on branch-name auto-detect (opt-in subagent arm:plan-execution-skill--gate with the same explicit path). Plan review happened upstream in Step 7 — the executor must not re-review. Gate sequence, tier selection (light default per phase; full perverification-loop-skill§Tiered gating), pass semantics, and memo format come fromverification-loop-skill§The gate contract (this skill defines none of them); the executor commits + pushes per phase and writes the gate memo, and the run's last gate — the ticket exit gate — is full.Code review (default — inline arm: invoke
code-review-inline-skill; it owns baseline-first, checklist resolution, and the review loop; the remainder of this section describes the opt-in subagent arm):code-review-subagenthasedit: deny(bash is allowlisted to read-only git, and its cwd is the session checkout, not the worktree) — you compute the diff (git diff origin/<base>...feat/<KEY>and--stat) and embed it (file list + hunks) in the Task prompt. Fix findings: severity ≥ Major mandatory; Minor by judgment. Re-gate after review fixes: fix commits land after the exit gate, so before pushing a fix commit re-run the full gate once on the fixed tree and append itstier=fullmemo line — the final pushed SHA must carry a greentier=fullmemo (a review-fix push without one breaks Step 10's citation). Relay any non-emptyRequirements Gapsarray per Step 7's relay rule before fixing. LEARNINGS capture is yours, not the reviewer's: reviewers have no write access — they return LEARNINGS candidates as report content (aLEARNINGS candidates:block). LEARNINGS writes stay working-tree only through the run — Step 8 phase commits never stage them (canonical rule:continuous-learning-skillstep 6). For each candidate, writeLEARNINGS/<category>/<slug>.mdin the worktree (skip if the file already exists; suffix-2on a genuine distinct-entry collision) and append its_index.mdentry. At end of ticket — after the bounded review loop, before Step 10a — land one dedicatedchore(learnings)commit sweeping every LEARNINGS write of the run (phase-time captures plus review candidates: all bodies +_index.md), never folded into a review-fix commit. In repos that ignoreLEARNINGS/**/*.md, that same commit also appends each new body's!LEARNINGS/<category>/<slug>.mdnegation to.gitignore— the add otherwise errors on / silently drops the ignored body file. Any PLAN re-ticks / gate-memo appends from review fixes fold into this same commit — its tree is docs-only and its memo names the gated implementation SHA Step 10a cites; anything code-shaped riding it is a fix commit and takes the re-gate rule. Refresh any tracked_index.mdentry restating the learnings-timing rule (e.g. the #445 single-writer row) in this same commit. Bounded loop: max 2 fix-and-re-review iterations — exhaustion → halt per §Failure Policy.PR + merge watching — split: 10a foreground, 10b background. 10a — PR creation (foreground). Default (inline arm): invoke skill
civiltekk-pr-workflow-skill(create route) — it owns framework detection, the gate-memo check, the PR body, and the semver label; the pipeline pins below (target<base>, thetier=fullmemo citation,Closes <TICKET_ID>) still apply, and the merge watcher is 10b's background shell (never a subagent); the remainder of this section describes the subagent arm. First the authoritative overlap re-check (the §6f early leg is advisory only):comm -12ofgit -C <ticket-repo> diff --name-only origin/<base>...feat/<KEY> | sortagainst each earlier in-run ticket's still-open PR diff (per repo, likewise sorted) — non-empty → hold ticket N pre-PR (worktree kept); auto-resume on that PR's merge notification: rebase, re-run the full gate (the SHA changes), push--force-with-lease, then create the PR. A PR that would show merge conflicts because an earlier in-run PR merged inside the 6e→10a window (stale base) classifies the same way — overlap-hold, never a failed ticket. Clear →pr-workflow-subagentcreates the PR target<base>— the Task prompt MUST state gates are green by citing the finalGATE <short-sha> tier=fullmemo line for the pushed SHA from the PLAN's## Tracesection (that citation IS the pipeline-mode gate memo perverification-loop-skill§Gate memo; atier=lightline is phase evidence and never satisfies this citation) and instruct it to skip its steps 2 / 2.5 / 3 / 4: run-plan verified the gate per phase, docstrings were filled before the gate, coverage badges stay out (README must not change after Step 9 review — CI carries the coverage signal), and the PLAN is ticked and committed; the CI gate below is the merge decision. The Task prompt MUST instruct it to includeCloses <TICKET_ID>in the PR body (keep the#—Closes #366, notCloses 366; fortracker#Ntickets<TICKET_ID>is already the fully-qualifiedowner/name#Nform — use it verbatim; must predate the merge). On PR creation the orchestrator advances to the next implementable ticket — it does NOT wait for CI (Step 1 execution model). 10b — merge watcher (background). Spawn a background shell (harness binding below) that:timeout 1800 gh pr checks -R <owner/name> <num> --watch(GNU coreutils; macOS:gtimeout) — 30-minute timeout; merge only when green withgh pr merge -R <owner/name> <num> --squash— thefeat/<KEY>head is short-lived, so squash is the classifier verdict (civiltekk-pr-workflow-skillmerge route,references/merge.md§Phase 1 head-class rule); capture the merge SHA viagh pr view -R <owner/name> <num> --json mergeCommit; report the outcome to the main session (merge SHA on success; the failing check names on red). Red-verdict guard: a red verdict matches onlystatusCheckRollupconclusionsFAILURE/TIMED_OUT/CANCELLED(plus the pending-at-timeout rule below) —SKIPPEDandNEUTRALare never red (GitHub marks not-applicable jobsSKIPPED, so docs-only PRs carry them in the rollup and stay green). A green watch (exit 0) proceeds to merge even when the rollup containsSKIPPEDentries — exit 0 remains the primary green signal. When the watch exits non-zero with checks reported, classify before declaring red:jq '[.statusCheckRollup[] | select(.conclusion == "FAILURE" or .conclusion == "TIMED_OUT" or .conclusion == "CANCELLED")] | length'Nonzero count → red; zero → not red — report for manual verdict rather than auto-merge (the green watch, not the count, authorizes merging). The
-R <owner/name>flags are mandatory — the watcher runs unattended, so prose scoping rules elsewhere never reach it (session repo ≠ ticket repo forrepo/KEYtickets). Zero configured checks (exits non-zero with "no checks reported") → merge directly with a "no CI configured" note. Red or pending-at-timeout → report and stop. The watcher performs gh-side operations only — watch → merge → SHA capture → report; it performs no local git mutations (worktree/ref mutations are the main session's, below, keeping them serialized away from concurrentgit worktree addcalls). Notification handling — main session, at step/ticket boundaries only (never mid-Task: a Step 8 run-plan or Step 9 review may run many minutes), in arrival order, each notification exactly once. On a merge notification: report the merge SHA, then run the cleanup the watcher must not —git -C <ticket-repo> worktree remove <root>/<KEY>, purge the worktree from the opencode registry (below), delete the remote branch, andgit -C <ticket-repo> fetchin the ticket repo's main checkout (fetch-only — neverpullin the user's main worktree; uncommitted state may conflict); opencode registry purge — OpenCode keeps aworktreerow plus a phantomprojectrow per worktree directory in~/.local/share/opencode/opencode.dband never garbage-collects them; any dangling path makesopencode reloadfail whole-command withENOENT ... FileSystem.realPath <dir>(seen 2026-09-26 with 56 stale rows from prior pipelines), so run this in the same cleanup step:node -e 'const{DatabaseSync}=require("node:sqlite");const p=process.argv[1];const db=new DatabaseSync(process.env.HOME+"/.local/share/opencode/opencode.db");db.prepare("DELETE FROM worktree WHERE directory=?").run(p);db.prepare("DELETE FROM project WHERE worktree=?").run(p)' "<root>/<KEY>"Requires Node ≥23.4 (
node:sqlitebuilt in); older Node fallback:sqlite3 ~/.local/share/opencode/opencode.db "DELETE FROM worktree WHERE directory='<root>/<KEY>';DELETE FROM project WHERE worktree='<root>/<KEY>'". Tracker tickets: ensure exactly oneticketing-skill§Close transition to Done — check the ticket status first, transition only if still open. On a red notification: the fix is queued for the next boundary (immediate if idle), bounded at 2 fix-and-re-watch rounds per ticket; red-fix pushes ride Step 9's re-gate rule — run the full gate once on the fixed tree and append its greentier=fullmemo for the new final SHA before re-watch (the 10a citation names the final pushed SHA). Harness binding (§Portability contract) for the background mechanism:- OpenCode: background shell (
background: true) with completion notification. - Claude Code: background Bash (run_in_background).
- Other/none: foreground
timeout 1800 gh pr checks <num> --watchbefore advancing (the pre-#560 behavior). Requires bash (git-bash/WSL on Windows).
- OpenCode: background shell (
PLAN Authoring (Step 6 detail)
All commands run in the worktree (worktrees/<KEY>), on feat/<KEY>.
$TICKET_ID is the normalized ref (#123 or PROJ-123; <owner/name>#N
for tracker#N forms — the close-keyword-safe fully-qualified form);
$KEY is its alphanumeric form (123 or PROJ-123).
6a. Adopt or rename an existing PLAN draft
Before generating from scratch, check whether an existing draft should be
adopted (avoids duplicate plans, preserves git history). Canonical filename:
PLANS/PLAN-${KEY}.md — Step 8 invokes this exact path. Drafts named
PLAN-GIT-<issue-number>.md or other variants are git mv'd to the
canonical form on adoption.
Search candidates in
PLANS/only (never repo root — a rootPLAN.mdmay belong to unrelated active work):ls PLANS/PLAN.md PLANS/PLAN-DRAFT-*.md PLANS/TODO-*.md 2>/dev/nullAlso prior-iteration canonical names (PLANS/PLAN-GIT-*.mdetc.).Already adopted? Canonical name exists → skip to 6d.
Single candidate → auto-adopt via
git mv(preserves history):git mv "PLANS/PLAN-DRAFT-<slug>.md" "PLANS/PLAN-${KEY}.md". Before auto-adopting a genericPLANS/PLAN.md, verify its**Issue:**header matches this ticket; mismatch → non-candidate + warn.Multiple candidates → prompt the user which to adopt, via the
questiontool with this payload shape (instantiate options from the actual drafts — best three matches plus the decline option, keeping within the 2-4 option cap; keep payloads small per deployedAGENTS.md§Question Tool Payloads):{ "questions": [ { "question": "Multiple PLAN drafts match this ticket. Which should be adopted as PLANS/PLAN-<KEY>.md?", "header": "PLAN draft adoption", "multiple": false, "options": [ { "label": "Adopt <draft-name>", "description": "git mv the draft to the canonical PLANS/PLAN-<KEY>.md form and continue with it." }, { "label": "Keep drafts in place", "description": "Adopt nothing now; generate a fresh PLAN from the ticket and leave the drafts for manual cleanup." } ] } ] }Non-adopted candidates → left in place with a warning (user cleans up).
No candidate / no
PLANS/dir →mkdir -p PLANS, continue to 6b.
Note: 6a searches relative to the worktree cwd — drafts must be committed to
<base>to be adoptable here; uncommitted main-worktree drafts are invisible by design.
6b. BRD/SRS draft linking
Document-ladder order: BRD first, then SRS. For each:
ls docs/brd/BRD-draft-*.md 2>/dev/null # then docs/srs/SRS-draft-*.md
If drafts found, ask the user (via question — harness binding, §Portability contract: OpenCode question · Claude Code AskUserQuestion · Other/none — plain-reply ask, skip linking if unanswered) whether to link one, using this
payload shape (instantiate <BRD|SRS>, <key>, and the draft name per ladder
order):
{
"questions": [
{
"question": "Found <BRD|SRS> draft(s). Link one to this ticket's PLAN?",
"header": "Draft linking",
"multiple": false,
"options": [
{
"label": "Link <draft-name>",
"description": "Rename the draft to the <BRD|SRS>-<key> form, repoint its **PLAN**: header, and record the path for 6c header injection."
},
{
"label": "Skip — no link",
"description": "Leave drafts in place; continue with an empty doc path (backward-compatible)."
}
]
}
]
}
On link:
- Rename:
git mv docs/brd/BRD-draft-{slug}.md docs/brd/BRD-{key}.md(plainmv+git addif untracked); same for SRS. - Update the doc header
**PLAN**:placeholder toPLANS/PLAN-{key}.md. - Record
BRD_PATH/SRS_PATHfor header injection in 6c. - Declined/absent → empty path (skip — backward-compatible).
6c. Generate the PLAN
Write PLANS/PLAN-${KEY}.md using this template:
# PLAN: <title>
**Branch**: feat/<KEY>
**Issue**: <ticket URL> ← + `**BRD**: <path>` / `**SRS**: <path>` lines when linked
**Base**: <base>
## Acceptance Criteria
- [ ] <checkable criteria from the ticket>
## Dependency & Consumer Map
_Before writing steps, list each touched file/module and who consumes it. Use `codegraph_callers` (code) or `tofu graph` + grep (IaC)._
| Node (file/module) | Depends on (must precede) | Consumers (who depends on this) | Change risk |
|---------------------|---------------------------|---------------------------------|-------------|
| `path/to/file` | — | caller-A, module-B | low/med/high |
## Implementation Phases
_Every step MUST be atomic and carry rationale. Reject any step missing a "Why"._
### Canonical step format
- [ ] **N.M** <single atomic action — verb + target + outcome>
— **Why:** <what this unblocks / why it must precede others>
— **Done when:** <objective, checkable completion signal>
— **Consumers affected:** <who depends on this; none if N/A>
### Phase 1: <name>
- [ ] **1.1** <atomic action> (per canonical format)
…
## Technical Notes
<from ticket>
## Dependencies
<external dependencies / blocked-by tickets>
## Risks & Mitigation
<risks + mitigations>
Step authoring rules (enforced by 6d):
- Atomic: one reversible concern per step; two concerns → split.
- Rationale mandatory: every step has Why; a step without it is malformed.
- Completion signal: objective Done when, never subjective "done".
- Consumers explicit: blast radius visible to reviewers; "none" if isolated.
6d. Atomicity self-check (commit gate)
- Read the PLAN back from disk.
- For every
- [ ] **N.M**/- [x] **N.M**step, confirm the three rationale lines follow it:— **Why:**,— **Done when:**,— **Consumers affected:**. - Any step missing any field → do NOT commit. Surface malformed steps (line number + text), fix, re-check. Gate must pass with zero malformed steps.
- Also verify: Dependency & Consumer Map section exists; phase ordering matches the map's constraints; every Acceptance Criterion is addressed by ≥1 implementation step (catches silently forgotten requirements at authoring time — belt for thin-map tickets that select zero reviewers).
6e. Commit and push the PLAN
git add "PLANS/PLAN-${KEY}.md" docs/brd/ docs/srs/ 2>/dev/null
git commit -m "docs(plan): add PLAN-${KEY}.md for ${TICKET_ID}"
git push -u origin "feat/${KEY}"
/run-plan commits implementation phases, not an untracked PLAN — an
untracked PLAN file would be lost on worktree removal, which is why this
step pushes it.
Skipped by design in pipeline context: initial ticket progress comment (execution follows immediately; ticket updates flow through Step 5 re-validation and pr-workflow) and the branch-workflow setup signal (pipeline runs assume an established repo; run
/create-ticketstandalone if you want that signal).
6f. Overlap hold gate (between 6e and Step 7)
Early leg (advisory): while any earlier in-run ticket still has an open
PR, intersect that PR's branch diff with THIS ticket's PLAN Dependency &
Consumer Map touch-set (materialize the map's first-column file paths,
normalize, sort — comm -12 needs sorted input; the map 6d just
validated — at this boundary the branch diff contains only the PLAN commit,
so a comm -12 on branch diffs would be vacuous here). Non-empty
intersection → hold ticket N: keep the worktree, report held, and
auto-resume when that PR's merge notification arrives — rebase feat/<KEY>
onto the updated base (push --force-with-lease after the rebase) and
continue at Step 7, re-running the full gate iff the rebase touched
implementation commits (at this boundary the tree is PLAN-only — Step 8's
exit gate re-gates the real tree). Advisory default: an empty or missing
Consumer Map skips the early leg — worst case is a late hold at the 10a
authoritative check, never a wrong merge.
Failure Policy
- Ticket-failure triggers (per ticket — nothing aborts the remaining
run): the executor's
[goal:blocked]terminal marker on the active ticket (Step 8), review-fix exhaustion after 2 iterations (Step 9), PR creation failure (Step 10a), or watcher exhaustion (Step 10b) — a pending-at-timeout watch fails the ticket immediately (pending CI has no concluded failure to fix; keep the worktree, report under Issues), while a concluded-red watch enters the 2 fix-and-re-watch rounds and fails the ticket once exhausted. CI red is not a run-level abort: a failed watcher fails that ticket only. - Keep the scene: the failed ticket's worktree +
feat/<KEY>branch stay in place for inspection (Step 2's prune/resume/refuse ask handles clean reruns); a red watcher never cleans up. - Tickets in flight when a ticket fails: independent tickets proceed;
dependent (held) tickets stay held and are reported deferred at run
end. A
[goal:blocked]on the active ticket pauses the implementation lane — background watchers keep running and notifications keep draining at boundaries. - Final report waits for outstanding watchers (each bounded by the 30-minute cap plus up to 2 red-fix rounds) before the Return Contract.
- Return Contract semantics:
partialfor any halt after a ticket has started OR any failed-red ticket;successallows deferred-held tickets (listed under Issues);failedis reserved for pre-execution failures (invalid base branch, zero tickets resolved, missing hard dependency from Step 1's preflight, unresolvable foreign repo).
Guarantees
- One active implementation at a time; any number of background merge watchers. A ticket's worktree lives until its PR resolves — merge → cleaned up by the main session's notification handler; red → kept for fixes. Every merge is green-only; the watcher performs no local git mutations.
- Every ticket re-validated against latest
origin/<base>before execution. - The main working tree is never checked out on a feat branch.
- Every PLAN passes the atomicity self-check before commit.
- No standalone tick/progress commits at any step — PLAN updates ride the phase's atomic commit (Step 8) or fold into the review-fix commit (Step 9); the squash merge keeps PLAN noise out of release notes.
- Delegation is hub-and-spoke from the primary session (build agent allows
task: {"*": allow}); delegates whose cwd is the session checkout (not the worktree) receive precomputed diffs. - Each worktree gets a CodeGraph index when the main checkout has one (skipped with a note when the index would be unignored, the CLI is absent, or init fails).
Return Contract
Status: success | partial | failed — deferred-held tickets alone do not
downgrade to partial; any failed-red ticket does
Output: per ticket — PR URL + merge SHA (watcher-reported) + final state
(merged / failed-red / deferred-held); one line each
Summary: 2-3 sentences max
Issues: blockers, held/deferred tickets, red-watcher outcomes, or "None"