# Resolve Issue

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

- Skill: `softwareone-platform/resolve-issue` (Agent Skill, multi-file: 8 files)
- Install (CLI): `npx skillmds@latest add softwareone-platform/resolve-issue`
- Raw SKILL.md: https://api.skillmd.com/api/skills/softwareone-platform/resolve-issue/raw
- Safety review: pending
- Works with: Claude Code, Claude.ai, OpenAI Codex
- Category: Web & Frontend
- Author: softwareone-platform (https://skillmd.com/u/softwareone-platform)
- Updated: 2026-09-17
- Page: https://skillmd.com/skills/softwareone-platform/resolve-issue

---


# 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)
