Resolve issue
Drive one ticket through the full issue-to-PR pipeline, from fact-checking the issue to opening the pull request, by invoking the already-built component skills in order and holding the human gates the flow needs. This skill is a sequencer, not a re-implementation: it never re-derives what a component skill does, and it never threads a component's verdict, risk table, or test selection into the next as a program argument — each component reads its own input (the issue text, the plan file, the git diff) and produces its own output behind its own gates. The orchestrator's whole job is ordering, the human gates it owns, and the durable handoff artifacts that make the pipeline resumable.
Run this in the main conversation loop, never as a subagent. The component skills spawn their own fresh verifier subagents, and the gates here are interactive — a subagent context loses the human gates and ExitPlanMode, and eats the depth budget those verifiers need. If this skill finds itself running inside a subagent, it stops (see the preamble).
The pipeline splits at plan approval — its Phase A→B pivot — into Phase A (diagnose and plan) and Phase B (build and open the PR), and ends at PR-created. Phase B adds conditional review checkpoints (b-security-review and the b-code-risk→b-open-pr checkpoint) that pause only when a review surfaces something the human must disposition, and otherwise pass through. Plan approval is not the last stop, though: b-open-pr pauses unconditionally for the open-PR confirmation, so every run waits on the human at least twice — and a-elicit-decisions adds a third whenever the ticket leaves a load-bearing decision open. Approving the plan does not hand the rest off — the full touchpoint list is under Degradation and safety below, and SKILL_DIR/README.md groups them by why they exist. Addressing the eventual review comments is Phase C (resolve-pr-comments), which the human invokes later — it is deliberately not part of this automated run and there is no polling loop.
How resume works (read this before the steps)
The orchestrator is stateless between steps. Every invocation reconstructs "where am I" from .claude/resolve/<ticket>/state.md and the observable git facts, then continues from the first unfinished step. Staying in one session and resuming days later in a fresh session at a different effort are the same code path — only the size of the gap differs. There is no auto model/effort switch; "each phase at its own effort" simply means the human re-invokes at the effort they want and the pipeline picks up where it left off.
Pick the model and effort before you invoke — Phase A (diagnose and plan) is the reasoning-critical part they drive. Bias toward a stronger model and higher effort the more complex, ambiguous, or high-risk the issue is. The pipeline and every subagent it spawns follow the session's model and effort; nothing here pins, caps, or silently downgrades them — silently swapping the model you chose for a cheaper one would trade your output quality for cost without consent. If you are already running and wish you had gone higher, revise at the approval gate, or stop and re-invoke at a higher setting — the run is resumable and nothing is lost.
Where the cost concentrates, so the choice is not made blind. b-write-tests dominates a run: it spawns a writer and then an independent verifier, and the review passes spawn their own subagents on top of that. Even the smallest unit-test target pays for that pair. Nothing here reports cost back to you — the orchestrator cannot see its own token usage — so resolve-issue-dashboard is where the running totals live, and timings.md holds only per-step wall-clock. (A specific token figure used to sit here; it was measured once, by a method later found to double-count, and is withdrawn. Shipped guidance should carry the shape of the cost, not a number nobody can re-derive.)
Resume is same-working-tree. The handoff artifacts (state.md, plan.md, fact-check.md, decisions.md) are gitignored working files that live on the local disk, so a new session on the same machine sees them. A fresh clone on another machine has none of these in-flight artifacts (by design — they deliberately stay out of git), so it does not resume an in-flight pipeline; it starts fresh. That is an accepted trade-off, not a bug: keeping the artifacts out of git is what stops them polluting the PR.
The handoff artifacts — .claude/resolve/<ticket>/
Five persistent markdown files — four handoff artifacts plus the append-only timings.md observability log — and nothing else persistent. One transient sixth exists only inside a-harden-plan: plan.baseline.md, the write-once snapshot that step hands to review-plan-risk and deletes once the cursor advances (see PLANNING.md). Do not read "five" as licence to delete it mid-step. Do not invent a machine-readable side-channel for verdicts or risk tables — those live inside the component skills' own output and gates. timings.md is not such a channel: no pipeline step reads it (only the read-only dashboard does), so it carries per-step wall-clock about the run, never a component's verdict threaded into the next step. (Separately, the generic-learning dead-drop that the cross-cutting capture step appends to is a user-global file outside this per-ticket directory — not a resume artifact and not a verdict side-channel; see "Capturing generic learnings" below.)
state.md — the resume cursor, written only by this skill. Fields: next-step, ticket, base-branch, work-branch, plan-approved, pr-url, attention, started, ended. attention is an optional one-line note, set only while the run is waiting on the human — either to disposition something (see the b-security-review and b-code-risk→b-open-pr checkpoints) or to confirm the PR draft (the b-open-pr open-PR confirmation) — and cleared the moment that wait ends and the run resumes (not deferred to the next step; a bare clarifying question that leaves the run still waiting does not clear it — the same question-versus-change distinction the a-gate-approve loop makes) — the dashboard renders the current step as blocked while it is set, so clearing attention only at the next step transition would leave the step showing blocked while it is in fact already running again. This is the single definition of attention's set/clear timing; the steps below set it with a step-specific reason and point here rather than restating the rule. started and ended are UTC ISO 8601 timestamps (with a trailing Z) for the run's start and finish, so the dashboard can show an active duration anchored to the run itself rather than to whichever Claude session is later tailed. It is a convenience cursor; git history plus the artifacts are the deeper truth. (No effort field — there is no runtime switch to drive, and re-invoking does not set effort; effort is the human's choice at invocation.)
- Timestamp rules for
started / ended (read once, they bind every state.md write). Capture the time only with a system command, in exactly UTC ISO 8601 with Z — never guess the clock, and never change the format (the dashboard parses it): date -u +%Y-%m-%dT%H:%M:%SZ (POSIX / Git Bash), or python -c "import datetime;print(datetime.datetime.now(datetime.timezone.utc).strftime('%Y-%m-%dT%H:%M:%SZ'))" (most portable). started is write-once and create-only: set it only when this invocation is a genuinely fresh-from-top run — the same condition the State reconcile calls fresh (no state.md and no plan.md and no fix commit, so the cursor is the first step a-fact-check). Never set started when the cursor was reconstructed from git at a later step (b-write-tests from an existing fix commit, done from an open PR) with no prior state.md: stamping now there would record a false late start — leave started absent and the dashboard falls back to the first transcript event. ended is set once at the done transition. Preserve both on every rewrite: state.md is rewritten at several points (P4 per step, the a-gate-approve approval, the b-code-risk→b-open-pr checkpoint, the b-open-pr open-PR confirmation's attention set/clear, done, and reconcile) — at each, read back the existing started/ended and carry them through unchanged; dropping started on any write defeats the run-scoped duration entirely.
fact-check.md — written by review-issue-fact when a resolve context exists (which is why the preamble creates the directory before Step a-fact-check). The issue's per-claim verdict table and the overall HALT / PROCEED / RESOLVE recommendation.
decisions.md — written by a-elicit-decisions when it elicits the human's open design decisions before planning; each entry marked resolved or open. It is the pre-plan scratch: a-draft-plan folds the resolved entries into plan.md and stays plan.md's sole author, so plan.md is the authoritative copy the a-gate-approve approves and review-code-risk reads — on any divergence the folded plan.md wins. When a-elicit-decisions no-ops or is skipped (non-interactive) it still writes a one-line sentinel (none — … or skipped (non-interactive)) — so a thin ticket cannot pass by silently self-declaring; it is absent only before a-elicit-decisions has run (e.g. a fresh clone). A real (non-sentinel) decisions.md is overwritten fresh only on a genuinely fresh-from-top run.
plan.md — drafted by this skill in a-draft-plan, hardened in place by review-plan-risk in a-harden-plan, appended to in b-security-review with one-line records of any approved scope expansion and, when the security review did not positively run, a line beginning SECURITY REVIEW DID NOT RUN (that line is a run record, not intent — review-code-risk should read it as context, never as something the fix was meant to satisfy), and read from the working-tree disk by review-code-risk in b-code-risk as its intent oracle. It is not committed by default (see the git-handling note at the end).
timings.md — an append-only per-step timing log the orchestrator writes, one markdown line - <UTC-ISO-Z> <step-id> per step entry (the boundary write in P4). It is pure run observability: only the read-only resolve-issue-dashboard reads it, no pipeline step does, so a missing or malformed timings.md costs nothing but the dashboard's per-step durations. A re-entered step — a gate revise re-running a-harden-plan / a-elicit-decisions, or a future rework loop — appends another line rather than overwriting, so the log measures re-runs instead of hiding them: the reason it is a separate append-only file and not state.md fields, whose flat one-value-per-field shape and wholesale rewrite could neither hold the repeats nor survive the carry-forward. Not committed (the .claude/resolve/.gitignore * covers it).
next-step is a closed set, matching the steps below: a-fact-check, a-elicit-decisions, a-draft-plan, a-harden-plan, a-gate-approve, b-implement, b-write-tests, b-security-review, b-code-risk, b-open-pr, done. These ids — with their labels, phase, component, and order — are registered canonically in this plugin's resources/resolve-issue-steps.json, which the resolve-issue-dashboard dashboard renders from; when you add, rename, or reorder a step, update that file too (and write the new value into state.md) so the dashboard stays in sync. The behaviour of each step lives in this skill's prose — a disclosed step's in its cluster file (PLANNING.md / REVIEW-PASSES.md), a spine-resident step's (a-gate-approve and the leaf steps) inline below — never in that registry file; a rename must sweep the cluster files too, not only this one.
Superseded runs are archived under the same ticket, not overwritten. The files above are the live run, at the ticket top-level. Because the directory is keyed on the ticket alone, a later branch reusing the same ticket number would otherwise inherit a previous run's files; the P4 staleness gate prevents that by moving a superseded run into a timestamp subdir .claude/resolve/<ticket>/<stamp>/ before the fresh run starts at the top-level. So a ticket directory may hold the live run plus past runs in dated subdirs. The .gitignore * covers the subdirs too, so none of it is committed, and the dashboard lists each archived run alongside the live one.
Preamble — run on every invocation
Resolve this skill's own directory once at load, so the pointers to its sibling reference files resolve to absolute paths:
!echo "${CLAUDE_SKILL_DIR}"
Call that SKILL_DIR. If the line above did not expand to a real absolute path (it still shows a literal ${CLAUDE_SKILL_DIR}), run echo "$CLAUDE_SKILL_DIR" with the Bash tool. Sibling files live at SKILL_DIR/<name>, in two kinds:
Cluster files — PLANNING.md (the Phase A authoring steps) and REVIEW-PASSES.md (the two Phase B review steps). These hold the per-step how-to that a normal run does need. The spine below carries only a one-line contract per step; load the cluster file before executing any step it covers — including a resume or re-entry that lands the cursor on a middle step, as the Phase A and Phase B sections direct. Executing a disclosed step from its one-line contract alone would drop the step's safety rails — do not.
Conditional refs — ARCHIVING.md, GIT-HANDLING.md, DASHBOARD.md, LEARNINGS.md. These hold branch-conditional or rationale detail; read one only when a step's branch points to it, so a normal run never needs them.
P0 — guard the context. If running inside a subagent, stop and tell the user to run resolve-issue from the main conversation (reason above). If running non-interactively such that the human gates below cannot be reached, see Degradation.
P1 — bring up the dashboard first (once per invocation), so the run is observable from step zero. This is the first action after the P0 guard, and it runs before any pipeline work — before the ticket is even resolved — so the human is watching from the start and the launch can never slip behind fact-checking or a later step. Performing the launch is a required preamble action on any interactive run; what is best-effort is only the outcome, not the attempt — the dashboard merely observes, so if Python is missing or the launch fails, say so in one line and carry on (a failed or absent dashboard must never block the pipeline), but silently not attempting the launch is a different failure and is not allowed. Invoke /resolve-issue-dashboard and treat it as fire-and-return: whichever outcome that skill reaches — server started, an already-running one reused, or could-not-launch — control returns here and the preamble continues (P2 onward), and the dashboard skill's closing stop ends only the launch sub-task, never this run. The dashboard is global and needs no ticket or state.md to come up — this repo shows as a placeholder and this run fills in the moment P4 writes state.md. It is idempotent across resumes (its singleton reuses a running server, no second tab) and skipped only in a non-interactive run. Mechanics and rationale: SKILL_DIR/DASHBOARD.md.
P2 — resolve ticket, base, and work-branch. Ticket: take it from an explicit argument or Jira reference, else from the current branch (git branch --show-current, extract a tracker key — the shape the repo's own branches and history use, e.g. [A-Z][A-Z0-9]+-\d+ for a Jira-style key, not a literal project prefix), else from a pasted issue (ask for a ticket, or use a stable ad-hoc-<slug>; never invent a fake acme- number). Two different numbers across argument / branch / Jira → ask which. Normalise the ticket twice — lowercase for the resolve directory (this skill's own namespace), uppercase (ACME-…) only for the Jira URL; the casing of a commit prefix is not this skill's to pick, it comes from the repo's history per b-implement. Base/target: ask the remote, do not guess an order — git symbolic-ref --short refs/remotes/origin/HEAD gives this repo's default branch (strip the origin/); only if that is unset ask the remote directly (git ls-remote --symref origin HEAD), and ask the human if that does not answer either — do not try main and master as candidates, since a default branch is often neither. If the work targets a long-lived branch other than the default (a maintenance or release line), base is that branch — take it from the request or the ticket, and ask if it is genuinely ambiguous. Do not expect to derive it: open-pr cannot either, which is why this step records it and b-open-pr passes it on. Record base in state.md so b-code-risk's git merge-base and b-open-pr's target stay consistent. Then compare the current branch against that base, and where they are the same, say so here. git branch --show-current is all it takes, and the point of saying it at P2 is when: nothing has been invested yet, whereas the Work-branch guard below stops the run for the same reason at b-implement — immediately after the human approved the plan, which is the most expensive moment to be interrupted. Name the branch they will need (feature/<ticket>-<slug>) and carry on: this is a voiced notice, not a gate, because Phase A never writes anything git tracks, so running it on the base costs nothing, and the guard is the enforcement. Do not create the branch, and do not write work-branch here. That field is written only at the guard, and the staleness gate reads its absence as "Phase B has not started" — record it early and a resume from any other branch looks like a different run's residue, which archives a live run.
P3 — create the resolve directory before Step a-fact-check. Make .claude/resolve/<ticket>/ (lowercase ticket, or ad-hoc-<slug>) if it does not exist, so review-issue-fact detects the resolve context and persists fact-check.md there instead of only printing it. Also, if .claude/resolve/.gitignore is absent, write it with a single line * — so the handoff artifacts never land in a commit no matter what the consumer repo's root .gitignore says. This scopes the skill's own output directory and does not touch the user's config.
P4 — resolve the cursor. Read state.md, run the staleness gate below, then reconcile against the observable git facts and continue from next-step. The first write of a run records ticket (the value P2 resolved) alongside next-step and started, and every later rewrite carries it forward — the dashboard reads it for the run header and shows the run untitled without it. Immediately after each step completes, write the next step into state.md so any interruption resumes cleanly, and — only when next-step changes to a new value (a real transition, not a same-step rewrite such as setting or clearing attention) — append one line - <ts> <new-step> to .claude/resolve/<ticket>/timings.md, where <ts> is a UTC timestamp captured the same way as started (the date -u command in the timestamp rules) and <new-step> is the value just written — reusing the started value for the t0 a-fact-check line and the ended value for the done line, so no extra clock read is needed at those two. This append is append-only (>>, never a read-rewrite) and best-effort / non-fatal — a failed append prints one line and is skipped, never blocking the step — the same discipline as the generic-learning dead-drop. Its only consumer is the dashboard's per-step timing; the timings.md artifact bullet above owns the format and rationale. On a genuinely fresh-from-top run, write state.md once here — still before Phase A begins, but not a cue to jump into a-fact-check now: P5 (load conventions) still runs first, then Phase A. Write it with next-step: a-fact-check and started set — so this run now surfaces in the dashboard that P1 already brought up, showing Fact-check from its first moment, and so started is genuinely recorded while the cursor is a-fact-check, as the timestamp rules require. On this fresh-from-top start, before Phase A, also print one non-blocking line: the run is starting on the current model and effort and will not switch mid-flight, so if the human wants a stronger model or higher effort they should stop now and re-invoke (nothing is lost), otherwise it continues — do not pause for an answer, and do not claim to detect the current effort (the skill cannot read it). Thereafter, on every write carry the existing started/ended through unchanged (see the timestamp rules above). This t0 write is why the staleness gate and the reconcile below treat an a-fact-check-only state.md (no work-branch, no plan.md, no fix commit) as a fresh / resumable run, not an unrecognised one.
P5 — load learned conventions (best-effort, read-only). Read $HOME/.claude/resolve-learnings/conventions.md if it exists and honor its preferences for the rest of this run only when they are consistent with current behaviour (honored preferences, not hard rules); skip entries past their ttl, and honor nothing if the file's top-line schema_version marker is unrecognised. Do not write the file (the physical prune belongs to /resolve-issue-learnings, the single writer). Non-fatal: if it is absent, unreadable, or unparseable, honor zero conventions and carry on. Scope (what a convention may cover), the per-file schema rule, and rationale: SKILL_DIR/LEARNINGS.md.
Staleness gate — before the reconcile, whenever a top-level state.md already exists. A ticket directory is keyed on the ticket alone, so a later branch reusing the same ticket number would otherwise inherit the previous run's state.md / plan.md — and a plan-approved: yes that git cannot refute — and mistake it for the current change. Before trusting the cursor, decide whether the top-level state.md belongs to this working tree, comparing the branch it recorded against the current branch (git branch --show-current):
work-branch recorded and different from the current branch — a different run's residue → archive it (below) and start fresh. (If the current branch is empty — detached HEAD — do not count that as a mismatch; fall to the confirm case.)
work-branch recorded and equal to the current branch — the legitimate resume of this run → do not archive; continue into the reconcile.
- No
work-branch recorded, but state.md shows in-flight progress (plan-approved: yes, a cursor already in Phase B / done, a drafted plan.md not yet approved, or a decisions.md carrying real resolved / open decisions — an interrupted a-elicit-decisions) and the current branch carries no matching fix commit — branch alone cannot separate "continue this plan" from "a new change on the same ticket", so ask the human (continue / new) and archive only on new, else resume or report done (on continue with an a-elicit-decisions cursor, resume a-elicit-decisions and re-present the open entries, keeping decisions.md). work-branch is written only at the Phase B guard, so a run abandoned after planning but before b-implement has none; this one confirm closes that gap without mis-killing the legitimate "approved on base, just switched to the feature branch for b-implement" handoff.
- Genuinely fresh —
state.md and plan.md both absent with no fix commit, or a state.md whose cursor is still a-fact-check with nothing approved (no plan.md, no work-branch, no fix commit), or an a-elicit-decisions cursor (no plan.md, no work-branch, no fix commit — as for the forms above) whose decisions.md is absent or holds only a none / skipped sentinel. The a-fact-check case is the normal residue of P4's t0 write; a sentinel-only or absent a-elicit-decisions residue is its equivalent — nothing the human answered is at stake. Both are tree-safe to re-run (a-fact-check is read-only; a no-op a-elicit-decisions re-asks nothing), so both take the fresh-from-top path: overwrite started fresh (do not preserve the orphan's value, so a stale start never bleeds in) and overwrite any leftover sentinel decisions.md; there is nothing approved to archive. An a-elicit-decisions residue whose decisions.md carries real resolved / open decisions is NOT fresh — the human already answered some, so it is in-flight Phase A work and takes the no-work-branch in-flight path above (continue → resume a-elicit-decisions, re-presenting the open entries and keeping decisions.md; new → archive), never a decisions.md-wiping fresh restart.
Archive is a move, never a delete. When the staleness gate above routes to archive, follow the full procedure in SKILL_DIR/ARCHIVING.md — it moves the superseded run's files into a timestamp subdir .claude/resolve/<ticket>/<stamp>/ (stamp derived from the old started, state.md moved last so an interrupted archive is idempotent), never deleting anything, then proceeds fresh-from-top.
Work-branch guard — before any Phase B commit. The current branch must be a feature branch distinct from the base P2 recorded, whether that base is the default branch or a maintenance line. If still on the base branch (including a maintenance line), stop and ask the user to create or switch to feature/<ticket>-<slug> — do not create it silently, and never commit the fix or tests onto the base. Record work-branch in state.md only once a valid feature branch (not the base) is confirmed. Step a-fact-check is read-only and may run on any branch; this guard binds only the Phase B commits.
Invocation discipline. Invoke each component by its explicit slash form (/disconfirm-first:…, /test-authoring:…, /pr-lifecycle:…) and confirm it actually ran and produced its expected output (its result table or verdict) — do not approximate a component's behaviour in place of running it (there is no skill-to-skill API; the whole design rests on faithfully delegating). Note that review-code-risk correctly spawns no verifier when it auto-fixed nothing, so "a verifier ran" is not a reliable did-it-run signal. If a component cannot be loaded or invoked, stop and say so rather than imitating it. b-security-review invokes the built-in security-review by bare name (built-ins have no plugin namespace); confirm the built-in itself answered — the R4-bis preflight in b-security-review guards against a same-named plugin shadowing it — and degrade per the Degradation section if it did not: a shadowed, missing, or unconfirmable security review is treated as not having run and surfaces loudly.
State reconcile (self-heal). Reconcile state.md against git on every start, not only when it is missing. The git-observable facts are only what git can see — fix/test commits and an open PR; the Phase A approval evidence lives in the local gitignored state.md / plan.md (readable in the same working tree). Cursor judgement: plan-approved in state.md or an existing fix commit → Phase A done; a fix commit but no test commit → resume at b-write-tests; a fix commit and a test commit but no PR, with no trustworthy state.md, → resume at b-security-review (git cannot tell whether the review passes already ran — their fixes are ordinary commits — so re-run from the first review pass; re-running is tree-safe, which is why a present state.md cursor is trusted over this fallback); an open PR for the branch → done. A legacy state.md cursor still pointing at a removed step is redirected: b-code-review is advanced to b-security-review, and b-commit-tests — whose commit is now the tail of b-write-tests — resumes at b-write-tests, the same place the git-derived rule above sends a run with a fix commit but no test commit. When state.md disagrees with git (it says b-implement but a fix is already committed), trust git and say so — never re-run b-implement from a stale cursor and create a duplicate or conflicting change. If both state.md and plan.md are absent (a fresh clone) and there is no fix commit — or state.md carries only an a-fact-check or a sentinel-only a-elicit-decisions cursor with nothing approved (the pre-plan residue described in the staleness gate — a decisions.md with real answers is in-flight, routed by that gate, not fresh) — treat it as a fresh run and say so plainly; do not misjudge Phase A as done. When the P4 staleness gate has just archived a prior run, the top-level is empty, so this same fresh path applies.
Capturing generic learnings (cross-cutting, best-effort)
At each step boundary (the same point P4 writes the next step to state.md), take one beat: did anything about the pipeline itself misfire, surprise, or prove awkward? Bias to silence — the default is to write nothing. Append a candidate only when the observation is all of: generic and orchestration-scoped (about resolve-issue's own gate flow / sequencing / state / a-draft-plan drafting / component-choice — not a delegated component's internals, which belong to that component's own loop), grounded in a concrete signal (a gate interaction, a SKILL.md instruction that misfired, a wrong verdict), and novel. When it clears that bar, append it — a true append (>>), not a read-rewrite — to the user-global dead-drop $HOME/.claude/resolve-learnings/candidates.md in the entry shape /resolve-issue-learnings documents (that skill owns the format). This is capture only (nothing is trusted until /resolve-issue-learnings verifies it against the current skill), append-only, silent, and non-fatal, and kept off state.md and every hot path. Full criteria, the dead-drop rationale, and why capture is per-step: SKILL_DIR/LEARNINGS.md.
Phase A — diagnose and plan
The four planning steps share decisions.md / plan.md authoring and the revise loop, so their how-to lives in one cluster file. Load SKILL_DIR/PLANNING.md before executing any of them. On a fresh run that is before a-fact-check, but a resume or a gate-revise re-entry can land the cursor directly on a-elicit-decisions, a-draft-plan, or a-harden-plan — the load is per-step, not once-per-phase, so each of those must load PLANNING.md first too. The one-line contract per step below is a non-normative spine index, not the executable detail: each step's safety rails (the decision no-op sentinel, a-draft-plan's sole-author invariant, a-harden-plan's write-once baseline snapshot) live in PLANNING.md, and where a one-liner and PLANNING.md differ, PLANNING.md wins. Contract per step (id · what it produces):
a-fact-check — invoke /disconfirm-first:review-issue-fact; advisory HALT / PROCEED / RESOLVE (never a hard stop); writes fact-check.md. Runs only after the full preamble; read-only, any branch.
a-elicit-decisions — settle the load-bearing open decisions the plan will rest on (facts looked up; decisions asked one-at-a-time as end-of-turn prose with a recommendation; voiced none sentinel if there are none); writes decisions.md (resolved / open); does not create plan.md.
a-draft-plan — sole author of plan.md: folds the resolved decisions in, then drafts Summary, Verification / Acceptance (the oracle, on the ground-truth > differential > metamorphic > human-judgment hierarchy), Out of Scope, and Testing approach.
a-harden-plan — snapshot plan.baseline.md (write-once), then invoke /disconfirm-first:review-plan-risk naming that baseline copy so it auto-fixes the untracked plan in place; delete the baseline once the cursor advances. When the returned result table carries a proposed (needs decision) row saying the plan would reverse a resolved entry in decisions.md, do not pass it to the gate as context: mark that entry open and re-enter planning at a-elicit-decisions — the same path a gate revise takes when it invalidates a recorded decision — so a reversed decision never rides through approval, whichever side invalidated it, the plan's own auto-fix or a human revise.
a-gate-approve — the one plan-approval gate (an approve / revise / question loop). Present .claude/resolve/<ticket>/plan.md and confirm with the human directly — this is a bespoke present-and-confirm loop, not the built-in plan mode / ExitPlanMode (which would write the plan to ~/.claude/plans/, a different file review-code-risk cannot read).
Present the gate as end-of-turn prose, and MUST NOT render it as an AskUserQuestion picker. Render the full current plan.md — its leading summary and its body both — as ordinary text in the turn, then end the turn so control returns to the main prompt. A summary, an excerpt, or a bare file-path reference does not satisfy this: the human has to be able to review the whole artifact they are approving without opening a file. If the plan is long enough that a full render risks truncation, say so and make sure the tail is not silently dropped — never let a truncated render pass as the whole plan (a genuinely bloated plan is a signal to tighten it, not to hide it). review-plan-risk's result table may be shown alongside for reference (the risk view), but it is optional context, not the artifact under approval. The prose form is mandatory, not merely preferred, for two reasons: a picker captures keyboard input, which blocks the very /model / /effort change this gate invites (below); and this section already read as a "present-and-confirm loop" yet was still rendered as a picker, so the prohibition has to be explicit rather than implied.
The a-gate-approve is its own turn, reached only after a-harden-plan (review-plan-risk, including its Step 3.6 opt-in) has fully returned, and it never absorbs or re-draws that opt-in. The edge-case opt-in is review-plan-risk's own interaction at a-harden-plan; by the a-gate-approve it is already settled, so the a-gate-approve presents only the final plan.md for approval and never re-surfaces the enhancement choices — folding the opt-in and the approval into one prompt is exactly what produced the picker.
When presenting the plan, add one line noting this pause is also the moment to change model or effort for Phase B: its reasoning runs at the current session setting, so if the human wants a different level they change their model/effort now (e.g. via /effort or /model) and then reply to approve — the approval starts Phase B on the new setting. This never blocks: only an explicit approval advances the cursor.
- A question that does not change the plan → answer it; the cursor stays at
a-gate-approve and plan.md is untouched.
- A minor adjustment (wording, scope trim, dropping a step) → edit
plan.md (refresh the leading summary to match), re-present the full updated plan; no need to re-run review-plan-risk.
- A substantive change (different approach, a new component, a changed contract) → edit
plan.md (refresh the leading summary). The risk surface changed, so review-plan-risk must re-run either way: if the change invalidates a decision recorded in decisions.md, mark that decision open and re-enter planning at a-elicit-decisions (load PLANNING.md — its Re-entry from a gate revise note owns the targeted re-elicit → re-fold → re-harden, which ends in that re-run), so a stale decision never rides through approval; otherwise return straight to a-harden-plan and re-run review-plan-risk. Then re-present the full plan. Judge minor vs substantive; ask if unsure.
- Only an explicit approval advances
next-step to b-implement and records plan-approved: yes. plan.md is not committed. During the loop the cursor stays at a-gate-approve (or briefly back at a-harden-plan, or a-elicit-decisions while a decision-invalidating revise re-elicits); a fresh session re-presents the current plan.md in full and continues the loop.
Phase B — build and open the PR
b-implement — implement the fix. Implement the fix directly (no skill for this step), then commit it. Commit message: take the convention from this repo's own history, do not assume one — git log --format=%s -n 30 shows whether subjects carry a ticket prefix and in what form (bracketed, bare, a Conventional Commits type, or none) and how long they run. Match what you find, using the ticket from the work-branch when the history prefixes with one. Only where the history is empty or inconsistent, fall back to a plain <summary> (≤50 chars) and say so. No AI co-author trailer on any commit this skill makes — b-implement, b-write-tests, and the review passes: do not add a Co-authored-by line naming Claude or any other agent unless the invoking request explicitly asked to mark this work as AI-assisted — the same single opt-in that turns on open-pr's provenance footer and ai-assisted label, and off by default here too. The repo's own history does not decide this one: the trailer names who wrote this commit, so it is a claim about this run, not a shape to copy.
b-write-tests — write tests for the fix. Invoke the scoped test writer for the change just made — /test-authoring:add-unit-test or add-integration-test (Mode A, git-diff scoped), or the matching update-*-test when the changed file already has a sibling test. Choose the type from what changed: pure logic / service / handler methods → unit; endpoint / persistence / cross-component behaviour → integration; if the right type or scope is genuinely unclear, ask one targeted question rather than guessing — and prefer the type the plan's Testing approach named; if the test you actually write diverges from it, record that in one line in plan.md, so review-code-risk (which reads plan.md) sees a reconciled record rather than a false mismatch. These run scoped to the change and carry their own writer and verifier subagents; they have no open-ended "select what to implement" gate. Do not use scan-test-gaps here — it is built for broad gap-hunting, surfaces untested code outside the fix, and has no clear end inside an automated flow. Gherkin scenario coverage is not something test-authoring writes, so if it looks needed, say so and leave it to the human rather than improvising it here. Close the step by committing the tests as their own commit, staging only the test files. That commit must land before the review passes (b-security-review onward): they read the committed diff (git diff $(git merge-base origin/<base> HEAD)..HEAD) and each needs a clean working tree so that pass's own edits are the only uncommitted ones — uncommitted tests would be invisible to them. Writing and committing the tests before the review passes is deliberate: they are an independent regression oracle for the code b-security-review and b-code-risk then modify.
Discrimination check (bounded, anti-tautology). Because b-write-tests runs after b-implement has committed the fix, a test written now can accidentally encode the fix's behaviour rather than the correct behaviour. Confirm the new test actually discriminates the bug: revert only the fixed files in the working tree (git checkout <fix-commit>~ -- <files>, where <fix-commit> is b-implement's commit), run the new test, and confirm it goes red for the right reason, then restore (git checkout HEAD -- <files>) and confirm it is green. One red/green demonstration, not a TDD loop. The assertion must encode a-draft-plan's oracle value, not whatever the fix currently returns. A build / compile failure on revert is NOT a valid red — it means the test references symbols the fix introduced, so the revert broke the build instead of exercising the
…(truncated)
1---2name: resolve-issue3description: Orchestrate the whole issue-to-PR pipeline for one ticket: fact-check the issue, draft a plan, harden it, implement the fix, write tests, review the fix, and open the PR — behind a plan-approval gate, pausing again wherever a decision is yours. Use whenever someone wants an issue / bug / Jira ticket taken from diagnosis all the way to a pull request, even if they don't name the skill. Resumable across sessions. Do NOT trigger when the user wants only ONE stage — route those to the component skill directly: "just fact-check this issue" is review-issue-fact; "review this plan" is review-plan-risk; "review my fix / diff before the PR" is review-code-risk; "add tests" is test-authoring; "open / raise a PR" is open-pr; "address / resolve the PR comments" is resolve-pr-comments. Also do NOT trigger for merging / completing a PR, or plain git operations. Trigger phrases: "resolve this issue", "run the issue-to-PR pipeline", "take this Jira from diagnosis to PR", "/resolve-issue".4---56# Resolve issue78Drive one ticket through the full issue-to-PR pipeline, from fact-checking the issue to opening the pull request, by invoking the already-built component skills in order and holding the human gates the flow needs. This skill is a **sequencer, not a re-implementation**: it never re-derives what a component skill does, and it never threads a component's verdict, risk table, or test selection into the next as a program argument — each component reads its own input (the issue text, the plan file, the git diff) and produces its own output behind its own gates. The orchestrator's whole job is ordering, the human gates it owns, and the durable handoff artifacts that make the pipeline resumable.910Run this in the **main conversation loop**, never as a subagent. The component skills spawn their own fresh verifier subagents, and the gates here are interactive — a subagent context loses the human gates and `ExitPlanMode`, and eats the depth budget those verifiers need. If this skill finds itself running inside a subagent, it stops (see the preamble).1112The pipeline splits at plan approval — its Phase A→B pivot — into **Phase A (diagnose and plan)** and **Phase B (build and open the PR)**, and ends at PR-created. Phase B adds conditional review checkpoints (b-security-review and the b-code-risk→b-open-pr checkpoint) that pause only when a review surfaces something the human must disposition, and otherwise pass through. **Plan approval is not the last stop, though**: `b-open-pr` pauses unconditionally for the open-PR confirmation, so every run waits on the human at least twice — and a-elicit-decisions adds a third whenever the ticket leaves a load-bearing decision open. Approving the plan does not hand the rest off — the full touchpoint list is under **Degradation and safety** below, and `SKILL_DIR/README.md` groups them by why they exist. Addressing the eventual review comments is **Phase C** (`resolve-pr-comments`), which the human invokes later — it is deliberately not part of this automated run and there is no polling loop.1314## How resume works (read this before the steps)1516The orchestrator is **stateless between steps**. Every invocation reconstructs "where am I" from `.claude/resolve/<ticket>/state.md` and the observable git facts, then continues from the first unfinished step. Staying in one session and resuming days later in a fresh session at a different effort are the **same code path** — only the size of the gap differs. There is no auto model/effort switch; "each phase at its own effort" simply means the human re-invokes at the effort they want and the pipeline picks up where it left off.1718**Pick the model and effort before you invoke — Phase A (diagnose and plan) is the reasoning-critical part they drive.** Bias toward a stronger model and higher effort the more complex, ambiguous, or high-risk the issue is. The pipeline and every subagent it spawns follow the session's model and effort; nothing here pins, caps, or silently downgrades them — silently swapping the model you chose for a cheaper one would trade your output quality for cost without consent. If you are already running and wish you had gone higher, revise at the approval gate, or stop and re-invoke at a higher setting — the run is resumable and nothing is lost.1920**Where the cost concentrates, so the choice is not made blind.** `b-write-tests` dominates a run: it spawns a writer and then an independent verifier, and the review passes spawn their own subagents on top of that. Even the smallest unit-test target pays for that pair. Nothing here reports cost back to you — the orchestrator cannot see its own token usage — so `resolve-issue-dashboard` is where the running totals live, and `timings.md` holds only per-step wall-clock. (A specific token figure used to sit here; it was measured once, by a method later found to double-count, and is withdrawn. Shipped guidance should carry the shape of the cost, not a number nobody can re-derive.)2122Resume is **same-working-tree**. The handoff artifacts (`state.md`, `plan.md`, `fact-check.md`, `decisions.md`) are gitignored working files that live on the local disk, so a new session on the same machine sees them. A fresh clone on another machine has none of these in-flight artifacts (by design — they deliberately stay out of git), so it does not resume an in-flight pipeline; it starts fresh. That is an accepted trade-off, not a bug: keeping the artifacts out of git is what stops them polluting the PR.2324## The handoff artifacts — `.claude/resolve/<ticket>/`2526Five persistent markdown files — four handoff artifacts plus the append-only `timings.md` observability log — and nothing else persistent. One transient sixth exists only inside `a-harden-plan`: `plan.baseline.md`, the write-once snapshot that step hands to `review-plan-risk` and deletes once the cursor advances (see PLANNING.md). Do not read "five" as licence to delete it mid-step. Do not invent a machine-readable side-channel for verdicts or risk tables — those live inside the component skills' own output and gates. `timings.md` is not such a channel: no pipeline step reads it (only the read-only dashboard does), so it carries per-step wall-clock about the run, never a component's verdict threaded into the next step. (Separately, the generic-learning *dead-drop* that the cross-cutting capture step appends to is a **user-global** file outside this per-ticket directory — not a resume artifact and not a verdict side-channel; see "Capturing generic learnings" below.)2728- **`state.md`** — the resume cursor, written only by this skill. Fields: `next-step`, `ticket`, `base-branch`, `work-branch`, `plan-approved`, `pr-url`, `attention`, `started`, `ended`. `attention` is an optional one-line note, set only while the run is waiting on the human — either to disposition something (see the b-security-review and b-code-risk→b-open-pr checkpoints) or to confirm the PR draft (the b-open-pr open-PR confirmation) — and cleared the moment that wait ends and the run resumes (not deferred to the next step; a bare clarifying question that leaves the run still waiting does not clear it — the same question-versus-change distinction the a-gate-approve loop makes) — the dashboard renders the current step as blocked while it is set, so clearing `attention` only at the next step transition would leave the step showing blocked while it is in fact already running again. **This is the single definition of `attention`'s set/clear timing; the steps below set it with a step-specific reason and point here rather than restating the rule.** `started` and `ended` are UTC ISO 8601 timestamps (with a trailing `Z`) for the run's start and finish, so the dashboard can show an active duration anchored to the run itself rather than to whichever Claude session is later tailed. It is a convenience cursor; git history plus the artifacts are the deeper truth. (No `effort` field — there is no runtime switch to drive, and re-invoking does not set effort; effort is the human's choice at invocation.)29- **Timestamp rules for `started` / `ended` (read once, they bind every `state.md` write).** Capture the time only with a system command, in **exactly** UTC ISO 8601 with `Z` — never guess the clock, and never change the format (the dashboard parses it): `date -u +%Y-%m-%dT%H:%M:%SZ` (POSIX / Git Bash), or `python -c "import datetime;print(datetime.datetime.now(datetime.timezone.utc).strftime('%Y-%m-%dT%H:%M:%SZ'))"` (most portable). **`started` is write-once and create-only**: set it only when this invocation is a genuinely *fresh-from-top* run — the same condition the State reconcile calls fresh (no `state.md` and no `plan.md` and no fix commit, so the cursor is the first step `a-fact-check`). **Never** set `started` when the cursor was reconstructed from git at a later step (`b-write-tests` from an existing fix commit, `done` from an open PR) with no prior `state.md`: stamping `now` there would record a false late start — leave `started` absent and the dashboard falls back to the first transcript event. `ended` is set once at the `done` transition. **Preserve both on every rewrite**: `state.md` is rewritten at several points (P4 per step, the a-gate-approve approval, the b-code-risk→b-open-pr checkpoint, the b-open-pr open-PR confirmation's `attention` set/clear, `done`, and reconcile) — at each, read back the existing `started`/`ended` and carry them through unchanged; dropping `started` on any write defeats the run-scoped duration entirely.30- **`fact-check.md`** — written by `review-issue-fact` when a resolve context exists (which is why the preamble creates the directory before Step a-fact-check). The issue's per-claim verdict table and the overall HALT / PROCEED / RESOLVE recommendation.31- **`decisions.md`** — written by `a-elicit-decisions` when it elicits the human's open design decisions before planning; each entry marked `resolved` or `open`. It is the pre-plan **scratch**: `a-draft-plan` folds the `resolved` entries into `plan.md` and stays `plan.md`'s sole author, so `plan.md` is the authoritative copy the a-gate-approve approves and `review-code-risk` reads — on any divergence the folded `plan.md` wins. When `a-elicit-decisions` no-ops or is skipped (non-interactive) it still writes a one-line sentinel (`none — …` or `skipped (non-interactive)`) — so a thin ticket cannot pass by silently self-declaring; it is absent only before `a-elicit-decisions` has run (e.g. a fresh clone). A real (non-sentinel) `decisions.md` is overwritten fresh only on a genuinely fresh-from-top run.32- **`plan.md`** — drafted by this skill in a-draft-plan, hardened in place by `review-plan-risk` in a-harden-plan, appended to in b-security-review with one-line records of any approved scope expansion **and, when the security review did not positively run, a line beginning `SECURITY REVIEW DID NOT RUN`** (that line is a run record, not intent — `review-code-risk` should read it as context, never as something the fix was meant to satisfy), and read from the working-tree disk by `review-code-risk` in b-code-risk as its intent oracle. It is **not committed** by default (see the git-handling note at the end).33- **`timings.md`** — an append-only per-step timing log the orchestrator writes, one markdown line `- <UTC-ISO-Z> <step-id>` per step *entry* (the boundary write in P4). It is pure run observability: only the read-only `resolve-issue-dashboard` reads it, no pipeline step does, so a missing or malformed `timings.md` costs nothing but the dashboard's per-step durations. A re-entered step — a gate revise re-running a-harden-plan / a-elicit-decisions, or a future rework loop — appends another line rather than overwriting, so the log measures re-runs instead of hiding them: the reason it is a separate append-only file and not `state.md` fields, whose flat one-value-per-field shape and wholesale rewrite could neither hold the repeats nor survive the carry-forward. Not committed (the `.claude/resolve/.gitignore` `*` covers it).3435`next-step` is a closed set, matching the steps below: `a-fact-check`, `a-elicit-decisions`, `a-draft-plan`, `a-harden-plan`, `a-gate-approve`, `b-implement`, `b-write-tests`, `b-security-review`, `b-code-risk`, `b-open-pr`, `done`. These ids — with their labels, phase, component, and order — are registered canonically in this plugin's `resources/resolve-issue-steps.json`, which the `resolve-issue-dashboard` dashboard renders from; when you add, rename, or reorder a step, update that file too (and write the new value into `state.md`) so the dashboard stays in sync. The behaviour of each step lives in this skill's prose — a disclosed step's in its cluster file (`PLANNING.md` / `REVIEW-PASSES.md`), a spine-resident step's (`a-gate-approve` and the leaf steps) inline below — never in that registry file; a rename must sweep the cluster files too, not only this one.3637**Superseded runs are archived under the same ticket, not overwritten.** The files above are the *live* run, at the ticket top-level. Because the directory is keyed on the ticket alone, a later branch reusing the same ticket number would otherwise inherit a previous run's files; the P4 staleness gate prevents that by moving a superseded run into a timestamp subdir `.claude/resolve/<ticket>/<stamp>/` before the fresh run starts at the top-level. So a ticket directory may hold the live run plus past runs in dated subdirs. The `.gitignore` `*` covers the subdirs too, so none of it is committed, and the dashboard lists each archived run alongside the live one.3839## Preamble — run on every invocation4041Resolve this skill's own directory once at load, so the pointers to its sibling reference files resolve to absolute paths:4243!`echo "${CLAUDE_SKILL_DIR}"`4445Call that `SKILL_DIR`. If the line above did not expand to a real absolute path (it still shows a literal `${CLAUDE_SKILL_DIR}`), run `echo "$CLAUDE_SKILL_DIR"` with the Bash tool. Sibling files live at `SKILL_DIR/<name>`, in two kinds:4647- **Cluster files — `PLANNING.md` (the Phase A authoring steps) and `REVIEW-PASSES.md` (the two Phase B review steps).** These hold the per-step how-to that a normal run **does** need. The spine below carries only a one-line contract per step; **load the cluster file before executing any step it covers — including a resume or re-entry that lands the cursor on a middle step**, as the Phase A and Phase B sections direct. Executing a disclosed step from its one-line contract alone would drop the step's safety rails — do not.48- **Conditional refs — `ARCHIVING.md`, `GIT-HANDLING.md`, `DASHBOARD.md`, `LEARNINGS.md`.** These hold branch-conditional or rationale detail; read one only when a step's branch points to it, so a normal run never needs them.4950- **P0 — guard the context.** If running inside a subagent, stop and tell the user to run `resolve-issue` from the main conversation (reason above). If running non-interactively such that the human gates below cannot be reached, see Degradation.51- **P1 — bring up the dashboard first (once per invocation), so the run is observable from step zero.** **This is the first action after the P0 guard, and it runs before any pipeline work — before the ticket is even resolved — so the human is watching from the start and the launch can never slip behind fact-checking or a later step.** Performing the launch is a required preamble action on any interactive run; what is best-effort is only the *outcome*, not the attempt — the dashboard merely observes, so if Python is missing or the launch fails, say so in one line and carry on (a failed or absent dashboard must never block the pipeline), but silently not attempting the launch is a different failure and is not allowed. Invoke `/resolve-issue-dashboard` and treat it as **fire-and-return**: whichever outcome that skill reaches — server started, an already-running one reused, or could-not-launch — control returns here and the preamble continues (P2 onward), and the dashboard skill's closing *stop* ends only the launch sub-task, never this run. The dashboard is global and needs no ticket or `state.md` to come up — this repo shows as a placeholder and this run fills in the moment P4 writes `state.md`. It is **idempotent across resumes** (its singleton reuses a running server, no second tab) and **skipped only in a non-interactive run**. Mechanics and rationale: `SKILL_DIR/DASHBOARD.md`.52- **P2 — resolve ticket, base, and work-branch.** Ticket: take it from an explicit argument or Jira reference, else from the current branch (`git branch --show-current`, extract a tracker key — the shape the repo's own branches and history use, e.g. `[A-Z][A-Z0-9]+-\d+` for a Jira-style key, not a literal project prefix), else from a pasted issue (ask for a ticket, or use a stable `ad-hoc-<slug>`; never invent a fake `acme-` number). Two different numbers across argument / branch / Jira → ask which. Normalise the ticket twice — lowercase for the resolve directory (this skill's own namespace), uppercase (`ACME-…`) only for the Jira URL; the casing of a **commit** prefix is not this skill's to pick, it comes from the repo's history per b-implement. Base/target: **ask the remote, do not guess an order** — `git symbolic-ref --short refs/remotes/origin/HEAD` gives this repo's default branch (strip the `origin/`); only if that is unset ask the remote directly (`git ls-remote --symref origin HEAD`), and ask the human if that does not answer either — do not try `main` and `master` as candidates, since a default branch is often neither. If the work targets a long-lived branch other than the default (a maintenance or release line), base is that branch — take it from the request or the ticket, and **ask** if it is genuinely ambiguous. Do not expect to derive it: `open-pr` cannot either, which is why this step records it and b-open-pr passes it on. Record base in `state.md` so b-code-risk's `git merge-base` and b-open-pr's target stay consistent. **Then compare the current branch against that base, and where they are the same, say so here.** `git branch --show-current` is all it takes, and the point of saying it at P2 is *when*: nothing has been invested yet, whereas the Work-branch guard below stops the run for the same reason at `b-implement` — immediately after the human approved the plan, which is the most expensive moment to be interrupted. Name the branch they will need (`feature/<ticket>-<slug>`) and carry on: this is a **voiced notice, not a gate**, because Phase A never writes anything git tracks, so running it on the base costs nothing, and the guard is the enforcement. **Do not create the branch, and do not write `work-branch` here.** That field is written only at the guard, and the staleness gate reads its absence as "Phase B has not started" — record it early and a resume from any other branch looks like a different run's residue, which archives a live run.53- **P3 — create the resolve directory before Step a-fact-check.** Make `.claude/resolve/<ticket>/` (lowercase ticket, or `ad-hoc-<slug>`) if it does not exist, so `review-issue-fact` detects the resolve context and persists `fact-check.md` there instead of only printing it. Also, if `.claude/resolve/.gitignore` is absent, write it with a single line `*` — so the handoff artifacts never land in a commit no matter what the consumer repo's root `.gitignore` says. This scopes the skill's own output directory and does not touch the user's config.54- **P4 — resolve the cursor.** Read `state.md`, run the staleness gate below, then reconcile against the observable git facts and continue from `next-step`. The first write of a run records `ticket` (the value P2 resolved) alongside `next-step` and `started`, and every later rewrite carries it forward — the dashboard reads it for the run header and shows the run untitled without it. Immediately after each step completes, write the next step into `state.md` so any interruption resumes cleanly, **and — only when `next-step` changes to a new value (a real transition, not a same-step rewrite such as setting or clearing `attention`) — append one line `- <ts> <new-step>` to `.claude/resolve/<ticket>/timings.md`**, where `<ts>` is a UTC timestamp captured the same way as `started` (the `date -u` command in the timestamp rules) and `<new-step>` is the value just written — reusing the `started` value for the t0 `a-fact-check` line and the `ended` value for the `done` line, so no extra clock read is needed at those two. This append is append-only (`>>`, never a read-rewrite) and best-effort / non-fatal — a failed append prints one line and is skipped, never blocking the step — the same discipline as the generic-learning dead-drop. Its only consumer is the dashboard's per-step timing; the `timings.md` artifact bullet above owns the format and rationale. On a genuinely fresh-from-top run, write `state.md` **once here** — still before Phase A begins, but **not** a cue to jump into a-fact-check now: P5 (load conventions) still runs first, then Phase A. Write it with `next-step: a-fact-check` and `started` set — so this run now surfaces in the dashboard that P1 already brought up, showing Fact-check from its first moment, and so `started` is genuinely recorded while the cursor is `a-fact-check`, as the timestamp rules require. On this fresh-from-top start, before Phase A, also print one **non-blocking** line: the run is starting on the current model and effort and will **not** switch mid-flight, so if the human wants a stronger model or higher effort they should stop now and re-invoke (nothing is lost), otherwise it continues — do not pause for an answer, and do not claim to detect the current effort (the skill cannot read it). Thereafter, on every write carry the existing `started`/`ended` through unchanged (see the timestamp rules above). This t0 write is why the staleness gate and the reconcile below treat an `a-fact-check`-only `state.md` (no `work-branch`, no `plan.md`, no fix commit) as a fresh / resumable run, not an unrecognised one.55- **P5 — load learned conventions (best-effort, read-only).** Read `$HOME/.claude/resolve-learnings/conventions.md` if it exists and honor its preferences for the rest of this run **only when they are consistent with current behaviour** (honored preferences, not hard rules); skip entries past their `ttl`, and honor nothing if the file's top-line `schema_version` marker is unrecognised. **Do not write the file** (the physical prune belongs to `/resolve-issue-learnings`, the single writer). **Non-fatal**: if it is absent, unreadable, or unparseable, honor zero conventions and carry on. Scope (what a convention may cover), the per-file schema rule, and rationale: `SKILL_DIR/LEARNINGS.md`.5657**Staleness gate — before the reconcile, whenever a top-level `state.md` already exists.** A ticket directory is keyed on the ticket alone, so a later branch reusing the same ticket number would otherwise inherit the previous run's `state.md` / `plan.md` — and a `plan-approved: yes` that git cannot refute — and mistake it for the current change. Before trusting the cursor, decide whether the top-level `state.md` belongs to *this* working tree, comparing the branch it recorded against the current branch (`git branch --show-current`):5859- **`work-branch` recorded and different from the current branch** — a different run's residue → archive it (below) and start fresh. (If the current branch is empty — detached HEAD — do not count that as a mismatch; fall to the confirm case.)60- **`work-branch` recorded and equal to the current branch** — the legitimate resume of this run → do not archive; continue into the reconcile.61- **No `work-branch` recorded, but `state.md` shows in-flight progress** (`plan-approved: yes`, a cursor already in Phase B / `done`, a drafted `plan.md` not yet approved, or a `decisions.md` carrying real `resolved` / `open` decisions — an interrupted a-elicit-decisions) **and the current branch carries no matching fix commit** — branch alone cannot separate "continue this plan" from "a new change on the same ticket", so ask the human (continue / new) and archive only on *new*, else resume or report `done` (on *continue* with an `a-elicit-decisions` cursor, resume a-elicit-decisions and re-present the `open` entries, keeping `decisions.md`). `work-branch` is written only at the Phase B guard, so a run abandoned after planning but before `b-implement` has none; this one confirm closes that gap without mis-killing the legitimate "approved on base, just switched to the feature branch for b-implement" handoff.62- **Genuinely fresh** — `state.md` and `plan.md` both absent with no fix commit, **or** a `state.md` whose cursor is still `a-fact-check` with nothing approved (no `plan.md`, no `work-branch`, no fix commit), **or** an `a-elicit-decisions` cursor (no `plan.md`, no `work-branch`, no fix commit — as for the forms above) whose `decisions.md` is absent or holds only a `none` / `skipped` sentinel. The `a-fact-check` case is the normal residue of P4's t0 write; a sentinel-only or absent `a-elicit-decisions` residue is its equivalent — nothing the human answered is at stake. Both are tree-safe to re-run (a-fact-check is read-only; a no-op a-elicit-decisions re-asks nothing), so both take the fresh-from-top path: overwrite `started` fresh (do **not** preserve the orphan's value, so a stale start never bleeds in) and overwrite any leftover sentinel `decisions.md`; there is nothing approved to archive. **An `a-elicit-decisions` residue whose `decisions.md` carries real `resolved` / `open` decisions is NOT fresh** — the human already answered some, so it is in-flight Phase A work and takes the no-`work-branch` in-flight path above (continue → resume a-elicit-decisions, re-presenting the `open` entries and keeping `decisions.md`; new → archive), never a `decisions.md`-wiping fresh restart.6364**Archive is a move, never a delete.** When the staleness gate above routes to *archive*, follow the full procedure in `SKILL_DIR/ARCHIVING.md` — it moves the superseded run's files into a timestamp subdir `.claude/resolve/<ticket>/<stamp>/` (stamp derived from the old `started`, `state.md` moved **last** so an interrupted archive is idempotent), never deleting anything, then proceeds fresh-from-top.6566**Work-branch guard — before any Phase B commit.** The current branch must be a feature branch distinct from the base P2 recorded, whether that base is the default branch or a maintenance line. If still on the base branch (including a maintenance line), stop and ask the user to create or switch to `feature/<ticket>-<slug>` — do not create it silently, and never commit the fix or tests onto the base. Record `work-branch` in `state.md` only once a valid feature branch (not the base) is confirmed. Step a-fact-check is read-only and may run on any branch; this guard binds only the Phase B commits.6768**Invocation discipline.** Invoke each component by its explicit slash form (`/disconfirm-first:…`, `/test-authoring:…`, `/pr-lifecycle:…`) and confirm it actually ran and produced its expected output (its result table or verdict) — do not approximate a component's behaviour in place of running it (there is no skill-to-skill API; the whole design rests on faithfully delegating). Note that `review-code-risk` correctly spawns no verifier when it auto-fixed nothing, so "a verifier ran" is not a reliable did-it-run signal. If a component cannot be loaded or invoked, stop and say so rather than imitating it. b-security-review invokes the **built-in** `security-review` by bare name (built-ins have no plugin namespace); confirm the built-in itself answered — the R4-bis preflight in b-security-review guards against a same-named plugin shadowing it — and degrade per the Degradation section if it did not: a shadowed, missing, or unconfirmable security review is treated as **not having run** and surfaces *loudly*.6970**State reconcile (self-heal).** Reconcile `state.md` against git on every start, not only when it is missing. The git-observable facts are only what git can see — fix/test commits and an open PR; the Phase A approval evidence lives in the local gitignored `state.md` / `plan.md` (readable in the same working tree). Cursor judgement: `plan-approved` in `state.md` or an existing fix commit → Phase A done; a fix commit but no test commit → resume at `b-write-tests`; a fix commit and a test commit but no PR, with no trustworthy `state.md`, → resume at `b-security-review` (git cannot tell whether the review passes already ran — their fixes are ordinary commits — so re-run from the first review pass; re-running is tree-safe, which is why a present `state.md` cursor is trusted over this fallback); an open PR for the branch → `done`. A legacy `state.md` cursor still pointing at a removed step is redirected: `b-code-review` is advanced to `b-security-review`, and `b-commit-tests` — whose commit is now the tail of `b-write-tests` — resumes at `b-write-tests`, the same place the git-derived rule above sends a run with a fix commit but no test commit. When `state.md` disagrees with git (it says `b-implement` but a fix is already committed), trust git and say so — never re-run `b-implement` from a stale cursor and create a duplicate or conflicting change. If both `state.md` and `plan.md` are absent (a fresh clone) and there is no fix commit — or `state.md` carries only an `a-fact-check` or a sentinel-only `a-elicit-decisions` cursor with nothing approved (the pre-plan residue described in the staleness gate — a `decisions.md` with real answers is in-flight, routed by that gate, not fresh) — treat it as a fresh run and say so plainly; do not misjudge Phase A as done. When the P4 staleness gate has just archived a prior run, the top-level is empty, so this same fresh path applies.7172## Capturing generic learnings (cross-cutting, best-effort)7374At each step boundary (the same point P4 writes the next step to `state.md`), take one beat: did anything about the pipeline *itself* misfire, surprise, or prove awkward? **Bias to silence — the default is to write nothing.** Append a candidate only when the observation is all of: *generic and orchestration-scoped* (about resolve-issue's own gate flow / sequencing / state / a-draft-plan drafting / component-choice — **not** a delegated component's internals, which belong to that component's own loop), *grounded* in a concrete signal (a gate interaction, a `SKILL.md` instruction that misfired, a wrong verdict), and *novel*. When it clears that bar, append it — a true append (`>>`), not a read-rewrite — to the user-global dead-drop `$HOME/.claude/resolve-learnings/candidates.md` in the entry shape `/resolve-issue-learnings` documents (that skill owns the format). This is **capture only** (nothing is trusted until `/resolve-issue-learnings` verifies it against the current skill), **append-only, silent, and non-fatal**, and kept off `state.md` and every hot path. Full criteria, the dead-drop rationale, and why capture is per-step: `SKILL_DIR/LEARNINGS.md`.7576## Phase A — diagnose and plan7778The four planning steps share `decisions.md` / `plan.md` authoring and the revise loop, so their how-to lives in one cluster file. **Load `SKILL_DIR/PLANNING.md` before executing any of them.** On a fresh run that is before `a-fact-check`, but a resume or a gate-revise re-entry can land the cursor directly on `a-elicit-decisions`, `a-draft-plan`, or `a-harden-plan` — the load is **per-step, not once-per-phase**, so each of those must load `PLANNING.md` first too. The one-line contract per step below is a **non-normative spine index**, not the executable detail: each step's safety rails (the decision no-op sentinel, `a-draft-plan`'s sole-author invariant, `a-harden-plan`'s write-once baseline snapshot) live in `PLANNING.md`, and **where a one-liner and `PLANNING.md` differ, `PLANNING.md` wins**. Contract per step (id · what it produces):7980- **`a-fact-check`** — invoke `/disconfirm-first:review-issue-fact`; advisory HALT / PROCEED / RESOLVE (never a hard stop); writes `fact-check.md`. Runs only after the full preamble; read-only, any branch.81- **`a-elicit-decisions`** — settle the load-bearing open decisions the plan will rest on (facts looked up; decisions asked one-at-a-time as end-of-turn prose with a recommendation; voiced `none` sentinel if there are none); writes `decisions.md` (`resolved` / `open`); does **not** create `plan.md`.82- **`a-draft-plan`** — **sole author** of `plan.md`: folds the `resolved` decisions in, then drafts Summary, Verification / Acceptance (the oracle, on the ground-truth > differential > metamorphic > human-judgment hierarchy), Out of Scope, and Testing approach.83- **`a-harden-plan`** — snapshot `plan.baseline.md` (write-once), then invoke `/disconfirm-first:review-plan-risk` naming that baseline copy so it auto-fixes the untracked plan in place; delete the baseline once the cursor advances. When the returned result table carries a `proposed (needs decision)` row saying the plan would reverse a `resolved` entry in `decisions.md`, do not pass it to the gate as context: mark that entry `open` and re-enter planning at `a-elicit-decisions` — the same path a gate revise takes when it invalidates a recorded decision — so a reversed decision never rides through approval, whichever side invalidated it, the plan's own auto-fix or a human revise.8485**a-gate-approve — the one plan-approval gate (an approve / revise / question loop).** Present `.claude/resolve/<ticket>/plan.md` and confirm with the human directly — this is a bespoke present-and-confirm loop, **not** the built-in plan mode / `ExitPlanMode` (which would write the plan to `~/.claude/plans/`, a different file `review-code-risk` cannot read).8687Present the gate as end-of-turn prose, and **MUST NOT render it as an `AskUserQuestion` picker.** Render the *full* current `plan.md` — its leading summary and its body both — as ordinary text in the turn, then end the turn so control returns to the main prompt. A summary, an excerpt, or a bare file-path reference does not satisfy this: the human has to be able to review the whole artifact they are approving without opening a file. If the plan is long enough that a full render risks truncation, say so and make sure the tail is not silently dropped — never let a truncated render pass as the whole plan (a genuinely bloated plan is a signal to tighten it, not to hide it). `review-plan-risk`'s result table may be shown alongside for reference (the risk view), but it is optional context, not the artifact under approval. The prose form is mandatory, not merely preferred, for two reasons: a picker captures keyboard input, which blocks the very `/model` / `/effort` change this gate invites (below); and this section already read as a "present-and-confirm loop" yet was still rendered as a picker, so the prohibition has to be explicit rather than implied.8889The a-gate-approve is its own turn, reached only after a-harden-plan (`review-plan-risk`, including its Step 3.6 opt-in) has fully returned, and it **never absorbs or re-draws that opt-in.** The edge-case opt-in is `review-plan-risk`'s own interaction at a-harden-plan; by the a-gate-approve it is already settled, so the a-gate-approve presents only the final `plan.md` for approval and never re-surfaces the enhancement choices — folding the opt-in and the approval into one prompt is exactly what produced the picker.9091When presenting the plan, add one line noting this pause is also the moment to change model or effort for Phase B: its reasoning runs at the current session setting, so if the human wants a different level they change their model/effort now (e.g. via `/effort` or `/model`) and then reply to approve — the approval starts Phase B on the new setting. This never blocks: only an explicit approval advances the cursor.9293- A question that does not change the plan → answer it; the cursor stays at `a-gate-approve` and `plan.md` is untouched.94- A minor adjustment (wording, scope trim, dropping a step) → edit `plan.md` (refresh the leading summary to match), re-present the full updated plan; no need to re-run `review-plan-risk`.95- A substantive change (different approach, a new component, a changed contract) → edit `plan.md` (refresh the leading summary). **The risk surface changed, so `review-plan-risk` must re-run either way:** if the change invalidates a decision recorded in `decisions.md`, mark that decision `open` and re-enter planning at `a-elicit-decisions` (load `PLANNING.md` — its **Re-entry from a gate revise** note owns the targeted re-elicit → re-fold → re-harden, which ends in that re-run), so a stale decision never rides through approval; otherwise return straight to `a-harden-plan` and re-run `review-plan-risk`. Then re-present the full plan. Judge minor vs substantive; ask if unsure.96- Only an explicit approval advances `next-step` to `b-implement` and records `plan-approved: yes`. `plan.md` is not committed. During the loop the cursor stays at `a-gate-approve` (or briefly back at `a-harden-plan`, or `a-elicit-decisions` while a decision-invalidating revise re-elicits); a fresh session re-presents the current `plan.md` in full and continues the loop.9798## Phase B — build and open the PR99100**b-implement — implement the fix.** Implement the fix directly (no skill for this step), then commit it. Commit message: **take the convention from this repo's own history, do not assume one** — `git log --format=%s -n 30` shows whether subjects carry a ticket prefix and in what form (bracketed, bare, a Conventional Commits type, or none) and how long they run. Match what you find, using the ticket from the work-branch when the history prefixes with one. Only where the history is empty or inconsistent, fall back to a plain `<summary>` (≤50 chars) and say so. **No AI co-author trailer on any commit this skill makes** — b-implement, b-write-tests, and the review passes: do not add a `Co-authored-by` line naming Claude or any other agent unless the invoking request explicitly asked to mark this work as AI-assisted — the same single opt-in that turns on `open-pr`'s provenance footer and `ai-assisted` label, and off by default here too. The repo's own history does not decide this one: the trailer names *who wrote this commit*, so it is a claim about this run, not a shape to copy.101102**b-write-tests — write tests for the fix.** Invoke the scoped test writer for the change just made — `/test-authoring:add-unit-test` or `add-integration-test` (Mode A, git-diff scoped), or the matching `update-*-test` when the changed file already has a sibling test. Choose the type from what changed: pure logic / service / handler methods → unit; endpoint / persistence / cross-component behaviour → integration; if the right type or scope is genuinely unclear, ask one targeted question rather than guessing — and prefer the **type the plan's `Testing approach` named**; if the test you actually write diverges from it, record that in one line in `plan.md`, so `review-code-risk` (which reads `plan.md`) sees a reconciled record rather than a false mismatch. These run scoped to the change and carry their own writer and verifier subagents; they have no open-ended "select what to implement" gate. Do **not** use `scan-test-gaps` here — it is built for broad gap-hunting, surfaces untested code outside the fix, and has no clear end inside an automated flow. Gherkin scenario coverage is not something `test-authoring` writes, so if it looks needed, say so and leave it to the human rather than improvising it here. **Close the step by committing the tests as their own commit, staging only the test files.** That commit must land before the review passes (b-security-review onward): they read the committed diff (`git diff $(git merge-base origin/<base> HEAD)..HEAD`) and each needs a clean working tree so that pass's own edits are the only uncommitted ones — uncommitted tests would be invisible to them. Writing *and committing* the tests before the review passes is deliberate: they are an **independent regression oracle** for the code b-security-review and b-code-risk then modify.103104**Discrimination check (bounded, anti-tautology).** Because b-write-tests runs *after* b-implement has committed the fix, a test written now can accidentally encode the fix's behaviour rather than the correct behaviour. Confirm the new test actually discriminates the bug: revert only the fixed files in the working tree (`git checkout <fix-commit>~ -- <files>`, where `<fix-commit>` is b-implement's commit), run the new test, and confirm it goes **red for the right reason**, then restore (`git checkout HEAD -- <files>`) and confirm it is **green**. One red/green demonstration, not a TDD loop. The assertion must encode a-draft-plan's oracle value, not whatever the fix currently returns. A **build / compile failure on revert is NOT a valid red** — it means the test references symbols the fix introduced, so the revert broke the build instead of exercising the105106…(truncated)