---
name: qrspi-work
description: "Single entry point for autonomous QRSPI feature development. Use when the user asks to 'work on' a ticket (e.g., 'work on RUS-42'). Reads PR review state to determine the current phase and executes the appropriate action — design, plan, implementation, or review response — without manual phase-by-phase invocation. Trigger on any variant of: 'work on <ticket-id>', 'continue <ticket-id>', 'pick up <ticket-id>', or any reference to progressing a QRSPI ticket through its lifecycle."
command: /qrspi-work
argument-hint: <ticket-id>
allowed-tools: Read, Write, Edit, Bash, Glob, Grep, Agent, mcp__linear__get_issue, mcp__linear__save_issue, mcp__linear__list_issue_statuses, mcp__linear__save_comment
---

# QRSPI Work Orchestrator (PR-gated)

You are a state machine, but **PR review state — not Linear status — is the authority**
for what to do next. Linear plays exactly two roles:

1. **Entry control:** a ticket may only *begin* if it is assigned to a user and in the
   `Selected` status. Nothing starts otherwise.
2. **Reporting projection:** once work has started, you update Linear status to reflect
   the active phase. These writes are **best-effort** — a failed Linear update logs a
   warning and never blocks git/PR work.

What is "ready to advance" is decided **wholly by PR status**:
`reviewDecision == APPROVED` **and** zero unresolved review threads. You do not read
Linear (after the entry gate) to make any advancement decision.

See `docs/qrspi-pr-gated-lifecycle-design.md` for the full design and rationale.

## Lifecycle at a glance

A single Graphite stack per ticket, built bottom-up and **held open** until the whole
feature is approved, then landed bottom-up:

```
trunk
 └── <id>/design   Design PR   — questions.md, research.md, design.md
      └── <id>/plan  Plan PR    — structure.md, plan.md, worktree.md   (stacked on design)
           └── <id>/slice-1..N   slice PRs — code                       (stacked on plan)
```

- Approving a phase PR **auto-advances**: the next phase is built stacked on top.
- A formal **CHANGES_REQUESTED** on an upstream phase PR **resets**: all downstream
  phases are discarded (PRs closed, branches deleted, stale artifacts removed) and the
  ticket returns to that phase for revision. Discard is **automatic**.
- Addressing feedback on a frontier phase PR (**revise**, the unified action) is
  **autonomous**. It fires when the PR carries a formal **CHANGES_REQUESTED** and/or
  unaddressed reviewer **comments** (RUS-54, subsumed). In one pass it (1) engages each comment
  per-intent — answers / applies+amends / declines with a concrete rationale, posted as an
  in-thread reply via `scripts/qrspi_comment_reply.py` (gh comment writes succeed with the bot's
  classic PAT — the old cross-account write block is gone), then (2) **only when a formal change
  request is present** addresses the review summary, amends the phase commit, and re-requests
  review (which clears the change request). A comment-only PR (no change request, even when
  APPROVED) is answered without re-requesting review.
  Review *threads* still cannot be auto-**resolved** (only the reviewer resolves a thread), so
  a PR whose sole outstanding signal is unresolved threads — with neither a change request nor
  an unaddressed reviewer comment — is left for the reviewer and routes to `wait`.
- Nothing merges until **every** PR in the stack is approved + clean; then the whole
  stack lands bottom-up.

---

## Entry Point

1. Parse `$ARGUMENTS` to extract `<ticket-id>`.
2. **Fetch the ticket fresh** with `mcp__linear__get_issue` (identifier
   `<ticket-id>`). On failure, retry **once**; if the retry fails, this is a **hard stop**
   — print the exact error and exit.
   - Read `status` (name) and `assignee` (`assigned` = assignee is non-null).
3. **Resolve everything in ONE deterministic command.** Worktree setup, GitHub
   `OWNER/REPO`, the PR-state gather, the tested decision, and artifact detection are all
   folded into a single script (`scripts/qrspi_resolve.py`). Run it verbatim — do **not**
   hand-derive paths, repo names, or the decision:
   ```bash
   python3 scripts/qrspi_resolve.py --ticket "<ticket-id>" \
     $( [ "<assigned>" = "true" ] && echo --assigned ) \
     --linear-status "<status>"
   ```
   It self-locates the repo root from its own location, so it works from any cwd, and it
   creates **nothing** unless the decision is `run_design`. It prints one JSON envelope:
   ```json
   { "ok": true, "repoRoot": "…", "worktreeDir": "…",
     "existing": { "questions": false, … },
     "decision": { "action": "…", "phase": null, "nextPhase": null,
                   "resetToPhase": null, "discardPhases": [], "reason": "…" } }
   ```
   Set `REPO_ROOT` and `WORKTREE_PATH` from `repoRoot`/`worktreeDir`. If `ok` is `false`,
   that is a **hard stop** — print the verbatim `error` and exit. Never retry it with an
   alternative command or improvised path. (The [Worktree Setup](#worktree-setup) section
   below documents what the script does internally, for reference.)
4. **Print the decision** (`action` + `reason`) so the operator can observe.
5. Dispatch on `action`:

| `action` | Handler |
|---|---|
| `entry_blocked` | → [Entry Blocked](#action-entry_blocked) |
| `run_design` | → [Run Design](#action-run_design) |
| `advance` | → [Advance](#action-advance) (build `nextPhase`) |
| `submit` | → [Submit](#action-submit) (finish/submit `phase`) |
| `wait` | → [Wait](#action-wait) |
| `revise` | → [Revise](#action-revise) (unified: engage `commentTargets` per-intent + re-request review iff `changeRequested`, on `phase`) |
| `reset` | → [Reset](#action-reset) (discard `discardPhases`, return to `resetToPhase`) |
| `land` | → [Land](#action-land) |

If the resolver errors (bad state, gh/git failure), treat it as a **hard stop** —
print the error and exit. Never guess the action.

---

## Worktree Setup

Every ticket gets its own git worktree at `.worktrees/<ticket-id>/` (relative to
`REPO_ROOT`). Multiple agents can work different tickets concurrently.

**Set `WORKTREE_PATH`** = `<REPO_ROOT>/.worktrees/<ticket-id>`.

The worktree should be checked out to the **highest existing phase branch** for the
ticket (the stack tip): a slice branch if any exist, else `<id>/plan`, else `<id>/design`.
For a brand-new ticket no branch exists yet — `run_design` creates `<id>/design`.

```bash
mkdir -p "$REPO_ROOT/.worktrees"
if [ -d "$WORKTREE_PATH" ]; then
  cd "$WORKTREE_PATH"          # reuse
else
  # Pick the tip branch if one exists, newest phase first.
  tip=$(git -C "$REPO_ROOT" branch --list '<ticket-id>/*' \
        | sed 's/[* ]//g' | sort -t- -k2 -n | tail -1)
  if [ -n "$tip" ]; then
    git -C "$REPO_ROOT" worktree add "$WORKTREE_PATH" "$tip"
    cd "$WORKTREE_PATH"
  fi
  # else: no branch yet — run_design will create the worktree+branch.
fi
```

If `git worktree add` fails because the path is broken, see
[Stale worktree recovery](#stale-worktree-recovery).

**CRITICAL — sub-agents do NOT inherit your cwd.** The Agent tool starts a fresh Bash
session at the main repo root. Every sub-agent prompt must include (1) `cd <WORKTREE_PATH>`
as its first Bash command and (2) absolute, `<WORKTREE_PATH>/`-prefixed paths for ALL file
operations. Never pass relative `.qrspi/...` paths to a sub-agent.

---

## action: entry_blocked

The ticket has no `<id>/design` branch and is not assigned + `Selected`.

Print: "Ticket `<ticket-id>` is not ready to start — it must be assigned to a user and in
the `Selected` status. Current: assignee=`<…>`, status=`<…>`. Nothing started." Then exit.

---

## action: run_design

Build the **design** phase: questions → research → design on a fresh `<id>/design`
branch off trunk, then submit the Design PR.

### Create the branch (if needed)

If the worktree/branch doesn't exist yet:
```bash
mkdir -p "$REPO_ROOT/.worktrees"
git -C "$REPO_ROOT" worktree add -b <ticket-id>/design "$WORKTREE_PATH" main
cd "$WORKTREE_PATH"
gt track --parent main --no-interactive
```
Save the ticket title + description from the Linear fetch — you pass it to some agents below.

### Phases (spawn one sub-agent each; see [Phase Agent Contracts](#phase-agent-contracts))

1. **Questions** — `subagent_type: qrspi-questions`. Verify `questions.md` non-empty.
2. **Research** — `subagent_type: qrspi-research`. **Research firewall: do NOT pass ticket
   content.** Verify `research.md` non-empty.
3. **Design** — `subagent_type: qrspi-design`. Verify `design.md` non-empty.

### Commit + submit

Single commit on the design branch (Graphite single-commit-per-branch convention). Worktree
setup already created the `<id>/design` branch with `git worktree add -b`, so it exists with
**no commit yet** — use `gt modify -c` to add the first commit. (Do NOT use `gt create`: the
branch already exists, so `gt create` fails.)
```bash
git add .qrspi/<ticket-id>/questions.md .qrspi/<ticket-id>/research.md .qrspi/<ticket-id>/design.md
gt modify -c --no-interactive -m "$(cat <<'EOF'
<ticket-id> [QR]: Design — <ticket-title>

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
EOF
)"     # first run adds the single commit; on resume it amends the same commit
```
Then submit as a **published** PR — review gates need a reviewable (non-draft) PR, and
`gt submit` defaults to draft in non-interactive mode. Clear any stale closed-PR association
FIRST (see [Resubmitting](#resubmitting-when-the-prior-pr-was-closed-or-merged)):
```bash
python3 <repo-root>/scripts/qrspi_clear_stale_pr.py --ticket <id>
gt submit --publish --no-edit --no-interactive
```
Capture the PR URL. **Project Linear** → `Design Review` (best-effort; see
[Linear projection](#linear-projection)). Print: "Design submitted. PR: `<url>`. → Design Review."

---

## action: advance

The active phase PR is approved + clean. Build `nextPhase`, stacked on the active phase.

### nextPhase == plan  (design was approved)

```bash
gt checkout <ticket-id>/design --no-interactive
git branch --show-current | grep -q '<ticket-id>/design'   # sanity
```
Spawn, in order (see contracts): **Structure** (`qrspi-structure`), **Plan** (`qrspi-plan`),
**Work Tree** (`qrspi-worktree`). Verify each artifact non-empty. Then create the plan branch
**stacked on design** and submit:
```bash
git add .qrspi/<ticket-id>/structure.md .qrspi/<ticket-id>/plan.md .qrspi/<ticket-id>/worktree.md
gt create <ticket-id>/plan --no-interactive -m "$(cat <<'EOF'
<ticket-id> [SP]: Plan — <ticket-title>

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
EOF
)"
gt submit --publish --no-edit --no-interactive
```
**Project Linear** → `Plan Review`. Print: "Plan submitted. PR: `<url>`. → Plan Review."

### nextPhase == implementation  (plan was approved)

Build the slice stack on top of `<id>/plan`. Read `structure.md` to count slices and
extract each slice's goal; read `plan.md` and `worktree.md`.

For each slice N (1..total), parent = `<id>/plan` for slice 1 else `<id>/slice-<N-1>`:
```bash
gt checkout <parent-branch> --no-interactive
```
Spawn the implement agent (`qrspi-implement`; append the
[project-scope block](#project-scope-firewall-implement)) with the slice-scoped
`STRUCTURE_SLICE` / `PLAN_SLICE` / `WORKTREE_SESSION` / `PREVIOUS_NOTES`. After it returns,
stage EVERY changed/untracked file **except generated caches** (`__pycache__/`, `*.pyc`) —
see [Staging](#staging) — and create the slice branch (`<total>` = the slice count from
structure.md):
```bash
git status --short
git add <every file shown, but NOT __pycache__/ or *.pyc>
gt create <ticket-id>/slice-<N> --no-interactive -m "$(cat <<'EOF'
<ticket-id> [I] <N>/<total>: <goal>

Part <N>/<total> of <ticket-id>. See the slice-1 PR for the full feature summary.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
EOF
)"
```
The slice commit **message body is the PR description** — Graphite seeds each PR's body
from its branch commit message when it *creates* the PR (this is also how the design/plan
PRs get their bodies). So every slice from 2..N gets the focused "Part N/total" body above
at creation; slice 1 gets the full `pr-summary.md` (next paragraph). **Do not** set PR
bodies with `gh pr edit` — the gh PAT cannot write PRs on this repo (see
[Why bodies are authored at creation](#why-pr-bodies-are-authored-at-graphite-creation)).

After all slices: spawn `qrspi-pr` to produce `pr-summary.md`, amend it into the **last**
slice commit as the durable artifact
(`git add .qrspi/<ticket-id>/pr-summary.md && gt modify --no-interactive`). Then splice
`pr-summary.md` into the **slice-1** commit *message* (so the slice-1 PR body is the full
summary at creation) with the deterministic, self-locating helper — never hand-build this:
```bash
python3 scripts/qrspi_pr_body.py --ticket <ticket-id> --slice 1 \
  --body-file .qrspi/<ticket-id>/pr-summary.md
```
It preserves the slice-1 commit subject + trailer, splices the summary in between, amends
via `gt modify -m` (auto-restacking the slices above), and prints
`{ ok, branch, subject, bytes, error? }`. If it reports `ok:false`, HARD STOP — do not
improvise a `gh`/`gt` alternative. Then submit the whole stack (bodies are already in the
commit messages, so `--no-edit` keeps them):
```bash
gt submit --publish --stack --no-edit --no-interactive
```
**Project Linear** → `Code Review`. Print: "Implementation submitted: `<N>` slice PRs. → Code Review."

> Resumability: skip any slice whose branch already exists with code committed.

---

## action: submit

A phase branch exists but its PR was never opened (e.g. a crashed prior run). Ensure the
phase's artifacts are present and non-empty; if any are missing, finish them by spawning
the remaining phase agents (same as `run_design`/`advance`). This path **creates** the PR,
so for implementation seed the slice-1 body into its commit message first (the PR body is
authored at creation — see [Why PR bodies are authored at Graphite creation](#why-pr-bodies-are-authored-at-graphite-creation)):
```bash
# implementation only: splice pr-summary into the slice-1 commit message before submit
python3 scripts/qrspi_pr_body.py --ticket <ticket-id> --slice 1 \
  --body-file .qrspi/<ticket-id>/pr-summary.md
gt submit --publish --no-edit --no-interactive   # add `--stack` if the active phase is implementation
```
Project the matching Linear status. If artifacts are missing **and** cannot be produced,
hard-stop with the error — never fabricate.

---

## action: wait

The active phase PR exists but cannot be advanced autonomously. Either it is **awaiting
review** (not yet approved, no change request), or it carries **unresolved review threads
but no formal change request and no unaddressed reviewer comment** (an unaddressed comment
routes to [`revise`](#action-revise), which outranks `wait`). A thread cannot
be auto-**resolved** — only the reviewer resolves a thread — so a thread-only PR is left for
the reviewer rather than looped through revise (which could never clear the thread gate).
Advancement waits on the human reviewer.

Print: "`<ticket-id>` `<phase>` PR is awaiting review (`<reviewDecision>`)`<, N unresolved
thread(s) left for the reviewer>`. Nothing to do until it is approved. Re-run
`work on <ticket-id>` after review." Then exit.

---

## action: revise

**`revise` is the unified feedback action** (it subsumes the former `respond_comment`, RUS-54).
The resolver emits it when the **frontier** phase PR carries a formal **CHANGES_REQUESTED**
**and/or** one or more **unaddressed reviewer comments**. The decision payload carries:

- `commentTargets` — the unaddressed reviewer comments to engage (a reviewer comment with no
  later bot reply in-thread; threads the reviewer already **resolved** are excluded upstream —
  RUS-69 — so you never reply into a resolved thread).
- `changeRequested` — whether a formal change request is present. **Only when this is true** do
  you re-request review at the end.

This is **autonomous**, fires even when the PR is **APPROVED** (comment-only case), and stays
**within this phase only** — the cascade is bounded to the phase's own artifacts (see
`references/review-cascade.md`). A design-level change that invalidates plan/impl is handled by
`reset`, not revise. A non-frontier `CHANGES_REQUESTED` routes to `reset` (never here).

### Step 1 — engage every reviewer comment per-intent (always, if `commentTargets` non-empty)

Engage **each** comment as an honest peer reviewer — you are **honesty-bound**, so never
fabricate a fact, a fix, or agreement. For every target, evaluate its **intent** and choose
exactly one reaction:

1. **Answer** — the comment is a question/concern; address it faithfully from the **actual**
   artifacts, code, and PR state.
2. **Apply** — the comment requests a sound, concrete change **within this phase only**; make
   the edit, then amend the phase commit in place via
   `scripts/qrspi_revise_amend.py --ticket <id> --branch <branch>` (self-locating, verify-gated;
   it FAILS if nothing was staged), and re-publish (`gt submit --publish --no-edit [--stack]
   --no-interactive`).
3. **Decline** — the suggestion is wrong/out-of-scope/unsound; give a concrete, respectful
   rationale grounded in the real state.

Post the answer/applied-note/decline-rationale as the **in-thread reply** — that reply is the
**only** place the rationale lives (do **not** duplicate it into artifacts or the impl-log).
Post it with the tested, self-locating helper (reply mode = the comment's `threadType`):

```bash
python3 scripts/qrspi_comment_reply.py --ticket <ticket-id> --pr <number> \
  --comment-id <commentId> --reply-mode <inline|toplevel> --body-file <reply.md>
```

It prints a `ReplyEnvelope` `{ ok, replyId, inReplyToId, error? }`. Read `ok` off **stdout** (do
not infer success from exit code alone). gh comment writes **succeed** here with the bot's
classic PAT — the old cross-account write block is gone (see the gh-cross-account note); if a
write ever fails, report it honestly and do **not** fall back to `gh pr edit`.

**Idempotency is structural:** once the bot's reply is observed in the thread (or a newer bot
top-level comment exists), the gather no longer reports that comment as unaddressed, so a later
pass does not re-respond. Thread **resolution** is always the reviewer's job.

### Step 2 — formal change request (ONLY when `changeRequested` is true)

If there is **no** formal change request (comment-only PR), **stop after Step 1** — do **not**
re-request review (the PR may be APPROVED; leave it undisturbed). Otherwise:

1. Ensure you're on the phase branch (`gt checkout <ticket-id>/<phase> --no-interactive`; for
   implementation, the affected slice branch(es), lowest first).
2. Read the change request — **read-only queries** (revise never mutates threads):
   ```bash
   gh pr view <number> --json reviews,comments --jq '.reviews[] | select(.state == "CHANGES_REQUESTED")'
   ```
   The inline/top-level comments were already engaged in Step 1 — focus here on the review
   **summary body** and any change-request feedback not tied to a specific comment.
3. If feedback remains to act on, edit the phase's artifacts/code (cascading within the phase
   from the earliest affected artifact; for implementation, lowest-numbered affected slice
   first). **If Step 1 already applied every needed change and nothing remains, do not invent an
   edit** — skip to step 5.
4. When you edited, amend the phase commit **in place, keeping its existing subject** via
   `scripts/qrspi_revise_amend.py --ticket <id> --branch <branch>` (it stages, amends with
   `gt modify` preserving the EXACT subject+trailers, and verify-gates the amend; never run a
   bare `gt modify` — without staging it silently drops your edits). If step 3 found nothing to
   change, **skip this step** (do not run the amend with no staged edits).
5. **Re-request review** so the stale `CHANGES_REQUESTED` is cleared (ALWAYS, whether or not you
   amended in step 4 — this is the loop-safe termination signal that lets the next pass return
   `wait`):
   ```bash
   gt submit --publish --no-edit --rerequest-review --no-interactive  # --stack if implementation
   ```
6. **Do NOT resolve review threads here.** Thread **resolution** is the reviewer's job; leave
   threads as-is. Re-requesting review flips `reviewDecision` back to `REVIEW_REQUIRED`, so the
   next pass returns `wait` instead of re-firing; the reviewer must still re-review to approve.
   Print which artifacts/files changed.

---

## action: reset

A formal `CHANGES_REQUESTED` landed on an **upstream** phase PR (`resetToPhase`), so every
downstream phase in `discardPhases` is now derived from a superseded upstream and must be
**discarded automatically** (decision 10 in the design doc). Nothing is merged, so this never
rewrites trunk — it is bounded to ticket-local branches and artifacts.

For each phase in `discardPhases` (highest first — slices before plan):
1. Close its PR(s) and delete its branch(es):
   ```bash
   # implementation: every slice branch, tip-down
   gt delete <ticket-id>/slice-<k> --force --close --no-interactive
   # plan:
   gt delete <ticket-id>/plan --force --close --no-interactive
   ```
2. Ensure the stale downstream artifacts are gone from the worktree working tree, so the
   skip-if-exists resume logic does not treat them as done:
   ```bash
   gt checkout <ticket-id>/<resetToPhase> --no-interactive
   # plan-half artifacts removed when discarding plan:
   rm -f .qrspi/<ticket-id>/structure.md .qrspi/<ticket-id>/plan.md .qrspi/<ticket-id>/worktree.md
   git clean -fd .qrspi/<ticket-id>/    # drop any untracked downstream leftovers
   ```
   (Deleting the branch already removes committed artifacts; this guards untracked remnants.)

Then **project Linear** → `<resetToPhase>` review status (e.g. `Design Review`) and print:
"Reset `<ticket-id>` to `<resetToPhase>`: discarded `<discardPhases>` after an upstream
change request. Address the `<resetToPhase>` feedback (revise) and the stack will rebuild on
re-approval." Stop — addressing the feedback itself is the manual `revise` path on a
subsequent invocation.

---

## action: land

Every PR in the stack is approved + clean. Land the whole stack bottom-up and finalize.

1. Confirm the stack is current and approved (the resolver already gated this), then land
   **every slice** from the bottom up. A single `gt merge` lands only the branch you are on
   plus its downstack — it does NOT walk upward — so on an N>1 stack you must check out and
   merge each slice explicitly in ascending order, or slices 2..N are left OPEN (the RUS-70
   half-landed-stack bug). Read the ascending slice branch list from the resolver envelope's
   root-level `slices` field (e.g. `["<id>/slice-1", "<id>/slice-2", ...]`); refresh the
   remotes once, then loop:
   ```bash
   gt submit --publish --stack --no-edit --no-interactive   # ensure remotes current, once
   # For k = 1..N in ASCENDING order, over each branch in the envelope `slices`:
   gt checkout <id>/slice-<k> --no-interactive
   # For k >= 2 ONLY: the previous slice (k-1) just merged and advanced trunk, so this
   # slice's parent moved out from under it locally. Re-align it onto the new trunk and
   # refresh its PR BEFORE merging — without this gt merge aborts ("branch has not been
   # submitted" / "updated remotely outside Graphite") and the stack half-lands:
   gt restack --no-interactive
   gt submit --stack --update-only --force --no-edit --no-interactive
   gt merge --no-interactive   # lands this slice (NOT --confirm: it forces a prompt --no-interactive cannot satisfy)
   # ...repeat for every slice, smallest k first, through the tip slice-<N>.
   ```
   If the feature has **no slices** (a plan-only feature with an empty `slices` list), fall
   back to a single `gt checkout <id>/design --no-interactive` + `gt merge --no-interactive`
   after the same `gt submit` refresh.
2. Reap the worktree, local branches, and remote refs with the deterministic, tested
   cleanup script — **do NOT** hand-run `gt sync --force` or `git worktree remove --force`
   here. The script self-locates `REPO_ROOT` from its own path, so run it from the **main
   checkout** (never from inside the worktree) so it sees the real `.worktrees/<ticket-id>`:
   ```bash
   cd "$REPO_ROOT"
   python3 scripts/qrspi_cleanup.py --ticket <ticket-id>
   ```
   It computes a classifier verdict (`blocked` > `destroy` > `skip`) and reaps **only** a
   fully-merged clean stack: it removes the worktree, deletes the merged local branches, and
   prunes their remote refs, printing one JSON envelope
   `{ ok, decision, reason, removed{worktree,branches[],remotes[]}, dryRun, error? }`. A dirty
   worktree comes back `decision:"blocked"` (left for a human, never forced); an infra error
   is `ok:false` (HARD STOP — do not retry or improvise). Pass `--dry-run` first to preview
   without destroying anything.
3. Remove planning artifacts from `main` (they were only needed during review) — open a
   small cleanup PR if `.qrspi/<ticket-id>/` survived the merge, mirroring the old Cleanup
   flow; otherwise skip.
4. **Project Linear** → `Done`. Print: "`<ticket-id>` landed and cleaned up. → Done."

---

## Linear projection

After the entry gate, Linear status is a **best-effort reporting projection**, never a gate.
The `*Approved` states are **dropped** — approval lives in the PR. Mapping:

| Active phase / event | Linear status to project |
|---|---|
| Design PR open / in review | `Design Review` |
| Plan PR open / in review | `Plan Review` |
| Implementation stack open / in review | `Code Review` |
| Reset to design / plan | `Design Review` / `Plan Review` |
| Stack landed | `Done` |

To project a status:
```
Call mcp__linear__save_issue with id "<ticket-id>" and state "<name>".
```
**Best-effort rule:** if the Linear write fails, print a one-line warning
(`WARN: Linear projection to <state> failed: <error>`) and **continue** — never hard-stop
or roll back git/PR work because of a Linear write. (To confirm a status after writing, read
it back with `get_issue` and check `status`; do NOT use `get_issue_status`, which takes a
WorkflowState ID, not a ticket ID.)

---

## Phase Agent Contracts

The orchestrator dispatches each phase to a purpose-built agent in
`.claude/agents/qrspi-<phase>.md` via the `Agent` tool (`subagent_type: qrspi-<phase>`,
`mode: "auto"`). It does NOT hand-engineer prompts — it passes a labelled input contract
with absolute `<WORKTREE_PATH>/`-prefixed paths. After each agent returns, verify the output
artifact exists and is non-empty; on failure, print the error and STOP (no Linear write).

| Phase | subagent_type | Inputs (all paths `<WORKTREE_PATH>/`-prefixed) |
|---|---|---|
| Questions | `qrspi-questions` | TICKET_ID, TICKET_CONTENT, ARTIFACT_PATH=…/questions.md, TEMPLATE_PATH=…/templates/questions.md |
| Research | `qrspi-research` | TICKET_ID, QUESTIONS_PATH, RESEARCH_PATH, TEMPLATE_PATH, REPO_ROOT=`<WORKTREE_PATH>` **(NO ticket content)** + scope block |
| Design | `qrspi-design` | TICKET_ID, TICKET_CONTENT, QUESTIONS_PATH, RESEARCH_PATH, DESIGN_PATH, TEMPLATE_PATH |
| Structure | `qrspi-structure` | TICKET_ID, DESIGN_PATH, STRUCTURE_PATH, TEMPLATE_PATH |
| Plan | `qrspi-plan` | TICKET_ID, STRUCTURE_PATH, DESIGN_PATH, PLAN_PATH, TEMPLATE_PATH |
| Work Tree | `qrspi-worktree` | TICKET_ID, PLAN_PATH, WORKTREE_PATH=…/worktree.md, TEMPLATE_PATH |
| Implement | `qrspi-implement` | TICKET_ID, SLICE_NUMBER, WORKTREE_DIR, STRUCTURE_SLICE, PLAN_SLICE, WORKTREE_SESSION, PREVIOUS_NOTES, IMPL_LOG_PATH, IMPL_LOG_TEMPLATE_PATH + scope block |
| PR summary | `qrspi-pr` | TICKET_ID, IMPL_LOG_PATH, DESIGN_PATH, STRUCTURE_PATH, PR_SUMMARY_PATH, TEMPLATE_PATH, REPO_ROOT |

### Research firewall

The research agent's tool definition includes no Linear MCP and forbids reading the ticket.
The orchestrator must ALSO omit `TICKET_CONTENT` from the research contract — only
`QUESTIONS_PATH`, `RESEARCH_PATH`, `TEMPLATE_PATH`, `REPO_ROOT`. Defense in depth.

### Project scope firewall (research)

Append this block to every research agent prompt; replace `REPO_ROOT_VALUE` with the actual
`REPO_ROOT` (the worktree path):

```
## Project scope restriction

You are researching the codebase for a specific ticket. ALL file reads must be inside the project repository at REPO_ROOT_VALUE/.

BEFORE reading ANY file, validate its path starts with REPO_ROOT_VALUE/. If it does not, skip it and note the gap.

DO NOT read:
- ~/.claude/, ~/.config/, ~/ (home directory)
- System config files (/etc/, /usr/, /var/)
- Files in any other project's directories
- Global skill definitions outside the repo
- Any path that does not start with REPO_ROOT_VALUE/

This is a hard boundary. If the questions imply information that may live outside the repo, note it as an unanswerable gap rather than escaping the project.
```

### Questions firewall

The questions agent excludes `Glob`, `Grep`, and `Bash` so codebase exploration is
structurally impossible. No special orchestrator handling required.

### Project scope firewall (implement)

Append this block to every implement agent prompt; replace `WORKTREE_DIR_VALUE` with the
actual `WORKTREE_DIR`:

```
## Project scope restriction

You are implementing work for a ticket. ALL file reads and modifications must be inside the project repository at WORKTREE_DIR_VALUE/.

BEFORE reading or writing ANY file, validate its path starts with WORKTREE_DIR_VALUE/. If it does not, skip it and report the error.

DO NOT modify:
- ~/.claude/, ~/.config/, ~/ (home directory)
- System config files (/etc/, /usr/, /var/)
- Global skill definitions in ~/.claude/skills/
- Any path that does not start with WORKTREE_DIR_VALUE/

The plan may contain paths like `~/.claude/skills/...`. If the plan targets global scope, refuse to make those changes and report the issue. The deliverable for a ticket must live within the project repo.

This is a hard boundary. If the plan references files outside the project, report the error and STOP.
```

---

## Git/Graphite Rules

- All `gt` commands include `--no-interactive`.
- All commit messages use heredoc format and include the co-authorship trailer.
- The orchestrator is the ONLY place git/graphite operations happen — sub-agents never commit.
- **One commit per phase branch** (Graphite convention): `gt create` opens the branch with
  its commit; re-running within the same phase amends with `gt modify` (no `-c`). Commit
  subjects: `<id> [QR]: Design — <ticket-title>`, `<id> [SP]: Plan — <ticket-title>`, `<id> [I] <N>/<total>: <goal>` (slices, e.g. `RUS-44 [I] 1/2: …`).
- After mutations, run `gt log short --no-interactive` to verify stack state.
- Never use `gt sync` mid-feature on a held stack except in `land` cleanup — it deletes
  branches whose PRs were closed (which is correct only after merge).
- **Clear any stale PR association before every `gt submit`.** Run the idempotent
  `python3 <repo-root>/scripts/qrspi_clear_stale_pr.py --ticket <id>` FIRST — no `gt info`
  pre-check needed (the helper is a no-op when nothing is stale), and do not wait for the
  submit to fail. See [Resubmitting](#resubmitting-when-the-prior-pr-was-closed-or-merged).
  This happens routinely on reset→rerun: a reset closes a phase PR, and recreating the
  same-named branch re-hydrates the dead association from `.git/.graphite_pr_info`.

### Staging — NEVER use `-a`

`-a` stages unrelated untracked files and makes `gt undo` destroy them. Stage specific files.
For design/plan phases, stage only that phase's artifacts. For implementation slices, run
`git status --short` and stage EVERY file shown (code + tests + artifacts are all the slice's
deliverable) **except generated caches** — never stage `__pycache__/` or `*.pyc` (a worktree
off trunk may not inherit a `.gitignore` rule for them). Verify with `git status --short`
before committing.

### Resubmitting when the prior PR was closed or merged

Graphite pins each branch to the first PR it created, in the SHARED `.git/.graphite_pr_info`
cache (keyed by `headRefName` → PR number + state). After a reset/rework closed a PR — or a
previously-landed ticket is rerun — that association is stale and `gt submit` refuses to open
a fresh PR under the same name. `--force` does not help (it governs the force-push, not the
association), and the interactive "publish a new PR?" prompt is unreachable to agents: gt
collapses to non-interactive whenever stdin is not a TTY and silently drops any piped
selection.

Recovery is a single idempotent command — run it before the submit:
```bash
python3 <repo-root>/scripts/qrspi_clear_stale_pr.py --ticket <id>
```
It removes ONLY this ticket's `(Closed)`/`(Merged)` entries from the cache (OPEN associations
and other tickets are left untouched), so the branch resubmits as a brand-new PR under the
SAME name — no rename, no temp branch, no `--force`. Safe to run before every submit: with
nothing stale it is a no-op, and a missing/garbled cache degrades to a no-op (the submit then
aborts visibly, never worse). It supersedes the old `gt rename <branch>-stale` roundtrip,
whose fixed temp name COLLIDES across cycles when a recovery is interrupted between its two
renames (`fatal: a branch named '<branch>-stale' already exists`). This is a recognized
state, not an infrastructure error — the HARD STOP rule does not apply.

### Why PR bodies are authored at Graphite creation

PR descriptions are set **only** through the branch commit message, which Graphite uses to
seed the PR title (subject line) and body (the rest) **when it creates the PR**. There is no
`gh pr edit` step anywhere in this lifecycle, by design:

- `gt submit` (1.8.x) has **no** `--body`/`--body-file` flag — the commit message is the
  only non-interactive lever for the description, and Graphite reads it **at creation only**
  (re-submitting an existing PR does *not* re-sync its body from the commit message). This is
  the binding constraint, and it is independent of token capability.
- `gt submit` creates and pushes the PR through Graphite's own GitHub-App credential. The
  `gh`-authenticated bot token (a **classic** PAT) can also write to this repo — PR *comment*
  writes succeed (that is what `revise`'s in-thread comment replies rely on; the old cross-account
  write block is gone, see the gh-cross-account note) — so the reason we don't `gh pr edit` a body is purely the
  `gt`/commit-message authoring model above, **not** a permission wall.

So: design/plan PRs carry their heredoc commit message as the body; implementation slice PRs
get a focused "Part N/total" body from the slice commit, and slice 1's commit message is
overwritten with the full `pr-summary.md` via `scripts/qrspi_pr_body.py` **before** the
creating `gt submit`. Bodies are seeded by the commit message at creation, so there is simply
no `gh pr edit` step — do not add one back.

---

## Worktree Management

- One worktree per ticket at `<REPO_ROOT>/.worktrees/<ticket-id>/`; `.worktrees/` is gitignored.
- All `git worktree add` commands run from `REPO_ROOT`, never from inside a worktree.
- New branches created via `git worktree add -b` must be tracked once: `gt track --parent <parent> --no-interactive`.

### Stale worktree recovery
```bash
git worktree remove "$WORKTREE_PATH" --force 2>/dev/null
git worktree prune
git worktree add ...   # retry
```

---

## Error Handling

- Sub-agent fails or its artifact is missing → print the error, STOP, no Linear write.
- A `gt`/`git`/`gh` command fails for non-infrastructure reasons → print command + error, STOP.
- Resolver errors or returns an unrecognized action → print it, STOP. Never guess.
- Linear projection write fails → WARN and continue (best-effort; it is not a gate).
- Never partially update state — a phase transition fully succeeds or nothing changes.

### HARD STOP: Infrastructure Errors Are Not Puzzles To Solve

Non-negotiable, no exceptions. When ANY operation fails due to permissions, authentication,
configuration, or tooling errors (`EACCES`, `permission denied`, expired auth, config
inaccessible, tool not found, "repo not synced with Graphite", etc.):

1. **STOP. Do not execute another command.** Not "one more try."
2. **Print the exact error verbatim** — the failing command and full output, unmodified.
3. **Exit the skill.** Do not continue to subsequent phases or attempt partial progress.

**Explicitly forbidden:** `chmod`/`chown`; routing around config via env vars
(`XDG_CONFIG_HOME`); copying config files elsewhere; deleting/recreating config dirs; using
raw `git` to bypass a broken `gt`; `sudo`/escalation; any action whose purpose is "make the
failing tool work again."

**Why absolute:** the orchestrator rationalizes "just one quick thing," then tries five, each
more destructive. The only safe response is to stop and let the human fix their environment.
The thought "I know I should stop, but let me just…" is the exact failure mode this prevents.
```
