From d9f31ae523b9578f664c49c140f4eeda41e27747 Mon Sep 17 00:00:00 2001 From: b-at-neu Date: Wed, 19 Aug 2026 18:44:03 -0400 Subject: [PATCH 1/5] #503 route config tickets to an operator session via /implement Co-Authored-By: Claude Opus 5 --- .claude/agents/plan-agent.md | 2 + .claude/docs/PIPELINE.md | 56 +++++++++--- .claude/skills/implement/SKILL.md | 114 +++++++++++++++++++++++++ .claude/skills/pipeline/SKILL.md | 58 ++++++++++--- .claude/skills/worktree-clean/SKILL.md | 2 +- CLAUDE.md | 1 + 6 files changed, 206 insertions(+), 27 deletions(-) create mode 100644 .claude/skills/implement/SKILL.md diff --git a/.claude/agents/plan-agent.md b/.claude/agents/plan-agent.md index 5da2d54f..7b5665b1 100644 --- a/.claude/agents/plan-agent.md +++ b/.claude/agents/plan-agent.md @@ -68,6 +68,8 @@ Construct the **full new issue body** (original ticket description preserved on gh issue edit N --repo SGAOperations/aplio --body-file .temp/plan-N.md ``` +**Route declaration (config tickets).** If any file in your **## Changes** section is `CLAUDE.md` or lives under `.claude/`, the plan body's **first line — directly under the `## Implementation Plan` heading, before `## Overview` — must be exactly `ROUTE: operator session`**, followed by one sentence naming why. The harness blocks a dispatched subagent from editing those paths, so the cockpit routes stages 2 and 4 to an operator session instead (`.claude/docs/PIPELINE.md` → "Config tickets"). Emit it in **revision mode** too — a revised plan can change route. Never emit it for a plan that touches neither. + **Use the fixed structure** in `.claude/docs/PIPELINE.md` → "Implementation plan" — the canonical section list, order, and writing style. Think through how the feature should actually work and look (it's a product/UX design, not just a file checklist), but write it tight: bullets, short sentences, **don't restate the ticket**, omit sections that don't apply. The plan must still _decide_ the substance — even though it's brief: - **Design each UX state** (happy + unhappy/edge), layout/hierarchy, key interactions, and the actual **copy** — in the **## UX states** section (only if there's UI). diff --git a/.claude/docs/PIPELINE.md b/.claude/docs/PIPELINE.md index b533dcdc..21c0c003 100644 --- a/.claude/docs/PIPELINE.md +++ b/.claude/docs/PIPELINE.md @@ -97,18 +97,19 @@ Rule: **every stage agent's first action is swapping its trigger label for its i ### Issue labels -| Label | Set by | Type | Meaning | -| ------------------------ | ---------------------------------------- | --------- | ------------------------------------------------- | -| `claude` | Cockpit (at opt-in) | marker | Claude is handling this ticket | -| `ready` | Cockpit (at opt-in) | trigger | Dispatch `plan-agent` | -| `planning` | `plan-agent` | in-flight | Plan being researched/written | -| `plan review` | `plan-agent` | gate | Plan written — awaiting human approval in cockpit | -| `plan changes requested` | Cockpit (human feedback) | trigger | Dispatch `plan-agent` in revision mode | -| `plan approved` | Cockpit (human approval, or `auto plan`) | trigger | Dispatch `impl-agent` | -| `auto plan` | Cockpit (at opt-in) | marker | Plan gate skipped: `plan review` auto-approved | -| `in progress` | `impl-agent` | in-flight | Implementation underway | -| `pr opened` | `impl-agent` | terminal | PR open; remaining state tracked on the PR | -| `blocked` | `impl-agent` | gate | Needs human decision; details in issue comment | +| Label | Set by | Type | Meaning | +| ------------------------ | ---------------------------------------- | --------- | ---------------------------------------------------------------------- | +| `claude` | Cockpit (at opt-in) | marker | Claude is handling this ticket | +| `ready` | Cockpit (at opt-in) | trigger | Dispatch `plan-agent` | +| `planning` | `plan-agent` | in-flight | Plan being researched/written | +| `plan review` | `plan-agent` | gate | Plan written — awaiting human approval in cockpit | +| `plan changes requested` | Cockpit (human feedback) | trigger | Dispatch `plan-agent` in revision mode | +| `plan approved` | Cockpit (human approval, or `auto plan`) | trigger | Dispatch `impl-agent` | +| `auto plan` | Cockpit (at opt-in) | marker | Plan gate skipped: `plan review` auto-approved | +| `operator route` | Cockpit (at the plan gate) | marker | Stages 2/4 run in an operator session (`/implement`); never dispatched | +| `in progress` | `impl-agent` | in-flight | Implementation underway | +| `pr opened` | `impl-agent` | terminal | PR open; remaining state tracked on the PR | +| `blocked` | `impl-agent` | gate | Needs human decision; details in issue comment | ### PR labels @@ -123,6 +124,7 @@ Rule: **every stage agent's first action is swapping its trigger label for its i | `needs human` | Cockpit / `revise-agent` | gate | 5 cycles without convergence, or an ambiguous rebase conflict needing the author; pipeline stops | | `refresh branch` | Cockpit / human | trigger | Dispatch `revise-agent` in refresh mode — rebase onto base and force-push, no code changes | | `refreshing` | `revise-agent` | in-flight | Branch refresh underway; the PR's other labels (e.g. `approved`) are left in place | +| `operator route` | `/implement` (at PR creation) | marker | Stages 2/4 run in an operator session; the cockpit never dispatches `revise-agent` for it | ## Stages and models @@ -137,6 +139,8 @@ Rule: **every stage agent's first action is swapping its trigger label for its i All four workers read `.claude/docs/ENGINEERING.md` before working; the review agent treats it as a review dimension. +**Stages 2 and 4 have an operator variant.** A ticket touching `CLAUDE.md` or `.claude/**` can't be implemented by a dispatched agent (the harness denies its `Edit`), so those two stages run in the operator's own session via `/implement`, following these same agent files — see "Config tickets". + ## Permission rationale The model is **broad allow + authoritative deny**: stage agents do real dev work (install packages, read CI logs, manage git in their worktree), so the allowlist grants broad categories and the `deny` list draws the safety line. **Any permission change must update this section.** @@ -161,6 +165,32 @@ Permission mode: every stage agent runs **`permissionMode: dontAsk`** (auto-deny **CI merge gate — `approval-check.yml`.** PRs into `dev` are gated on the `approved` label **only when the PR carries `claude`** (a job-level `if:`). Every other PR — human, Dependabot — gets a `skipped` check run, which GitHub counts as satisfied, so it merges on its own merits. This is deliberately **fail-open**: an unlabelled pipeline PR is indistinguishable in CI from a human one and simply loses its gate. Nothing in the workflow can close that, so the mitigations live upstream — `impl-agent` passes `--label "claude"` at `gh pr create` (so the gate is live on the PR's first event) and the cockpit's **ungated-PR sweep** reports any tracked PR missing it. Never narrow the workflow's trigger to exclude a PR: a workflow that never runs creates no check run, leaving the required check pending forever. +## Config tickets (`CLAUDE.md` / `.claude/**`) + +**The harness denies `Edit`/`Write` under `.claude/` to dispatched subagents, and `settings.json` cannot grant it back.** This repo already allows `Edit(**)`/`Write(**)` with no `.claude/` deny rule and the denial persists anyway — it sits above project config, so there is nothing to fix in the permission model. An operator's **main session** is unaffected: reading `.claude/agents/impl-agent.md` and acting on it spawns no subagent, so no subagent restriction applies. That asymmetry is the whole basis of this route. + +**The route** — `plan-agent` declares it, the cockpit marks it, an operator runs it: + +1. **Declare.** When the plan's **## Changes** touches `CLAUDE.md` or `.claude/**`, its first line is exactly `ROUTE: operator session` (see "Implementation plan"). +2. **Mark.** At the plan gate the cockpit reads the issue body for that sentinel and adds **`operator route`** in the same edit as `plan approved`. The marker is sticky and travels to the PR (`/implement` passes `--label "operator route"` to `gh pr create`), so every later cockpit decision is a pure label check. +3. **Run.** The operator opens a **named session** and runs `/implement `. That skill resolves the stage from the item's labels, creates a worktree under `.claude/worktrees/impl-`, and follows the **unmodified** `impl-agent.md` / `revise-agent.md` plus a short list of subagent-only overrides. The override list lives in the skill and nowhere else — one place to drift, one place to check. + +**What moves and what doesn't.** Only stages **2** and **4**. `plan-agent` and `review-agent` are read-only and work through `gh`, so stages 1 and 3 run unchanged. `refresh branch` is still dispatched to `revise-agent` — a rebase and force-push edit no files, and a conflict inside `CLAUDE.md` / `.claude/docs/**` is already on the never-touch list and escalates. + +**The invariant this deviates from.** Everywhere else a trigger label means something is dispatching. An `operator route` item **keeps** its trigger label (`plan approved` / `needs revision`) and is **never** dispatched — the cockpit announces the command instead. Recovery is unchanged (re-apply the trigger), but the label alone no longer implies motion. That is why `operator route` appears in the cockpit's gate queries, its unowned sweep, and its `status` output: an unowned config ticket is otherwise invisible in a way an unowned normal ticket isn't, because no cockpit will ever nag about it. + +**Session naming.** These sessions are long-lived and several run at once, so launch each with the issue number in its display name: + +```bash +claude -n "#503: operator config route" # then, in that session: /implement 503 +``` + +`-n/--name` sets the name shown in the prompt box, the `/resume` picker, and the terminal title. It is settable **only at launch** — a running session can neither read nor change its own name — so the cockpit's announcement hands over the command with the name pre-filled, and the skill re-states the expected string. The name always carries the **issue** number, even when the command takes a PR number. + +**Always in a worktree.** `/implement` never works in the main checkout, for two reasons: editing `.claude/` from the session that is _using_ it mutates your live configuration mid-task, and the ticket may be editing the very agent file the session is following. In a worktree the session reads its instructions by absolute path from the main checkout while every edit lands on the worktree copy, so the committed behaviour holds for the whole run. Reclaim the worktree with `/worktree-clean` once the PR merges. + +**Unverified:** whether the manual escape hatch `claude --agent impl-agent` carries the same restriction (its frontmatter forces `dontAsk` and worktree isolation). Untested — use `/implement` for config tickets regardless. If a future Claude Code version lifts the harness restriction, this whole route can be deleted; the sentinel and the label are the only things to unwind. + ## Pipeline output formats Defined once here; the stage agents follow these exactly. @@ -179,6 +209,7 @@ Every plan, review, summary, and comment is written for a human scanning fast: Appended below the ticket under a `---` then `## Implementation Plan`; revision mode replaces only that block. **Do not restate the ticket** — reference it. Fixed sections in this order; the conditional ones appear **only when they apply** (omit otherwise — no stub): +- **`ROUTE: operator session`** _(only when the plan touches `CLAUDE.md` or `.claude/**`)_ — a bare line before `## Overview`, exactly that text, plus one sentence naming why. It routes stages 2 and 4 to an operator session; see "Config tickets". - **## Overview** — 2–4 sentences: what, why, the approach. - **## Changes** — files to create/modify, one bullet each: `` `path` — one-line reason ``. - **## Implementation** — ordered `- [ ]` checkboxes, one line each; fold validation / states / error-model notes into the step they belong to. @@ -280,6 +311,7 @@ Closing the cockpit session also halts dispatch (it is the only dispatcher) but | Labels manually changed on GitHub | Fine — labels are the source of truth | The next tick acts on whatever the labels say | | Stale worktrees / orphan `node_modules` dirs accumulating under `.claude/worktrees/` | Agents cut off mid-run; on Windows the harness leaves dirs git can't delete | Run **`/worktree-clean`** from the main checkout — it prunes registrations and force-deletes orphan dirs (`git worktree remove` alone fails with `Invalid argument` once `node_modules` exists). The cockpit reports these but never auto-deletes them. | | An agent stopped with `BLOCKED:` or hit `maxTurns` | Clean stop by design (not a crash) | Resolve the blocker (or widen scope/permissions), then `retry #N` | +| An issue sits at `plan approved`, or a PR at `needs revision`, and nothing dispatches | It carries `operator route` — a config ticket (`CLAUDE.md` / `.claude/**`) the cockpit never dispatches for | Open a named session and run it yourself: `claude -n "#N: "`, then `/implement `. See "Config tickets" | ## Reading current state without the cockpit diff --git a/.claude/skills/implement/SKILL.md b/.claude/skills/implement/SKILL.md new file mode 100644 index 00000000..415a4206 --- /dev/null +++ b/.claude/skills/implement/SKILL.md @@ -0,0 +1,114 @@ +--- +name: implement +description: Run pipeline stage 2 or 4 yourself, in your own session, for a config ticket — one touching CLAUDE.md or .claude/**, which a dispatched agent cannot edit. Resolves the stage from the item's labels, works in a dedicated worktree, and follows the existing impl-agent / revise-agent definitions unchanged. Manual only. Usage: /implement +disable-model-invocation: true +allowed-tools: Read, Edit, Write, Glob, Grep, Bash, AskUserQuestion +--- + +# Implement — operator-run pipeline stage + +**Trigger:** manual, in an operator's own session. **Input:** an issue or PR number (``). **Repo:** `SGAOperations/aplio`. + +The harness denies `Edit`/`Write` under `.claude/` to **dispatched subagents**, and `settings.json` cannot grant it back — so `impl-agent` and `revise-agent` can't run for tickets that touch `CLAUDE.md` or `.claude/**`. Your session has no such restriction. This skill is a **thin wrapper**: it points you at the existing agent definition and overrides only the rules that exist because that agent is a subagent. Background and rationale: `.claude/docs/PIPELINE.md` → "Config tickets". + +**The agent files are the workflow. Do not modify them, and do not reimplement them here.** + +## 1. Name this session (first output line) + +Derive `#: <2–5 lowercase words>` from the issue title — e.g. `#503: operator config route` — and print it as your first line: + +> Name this session: `#503: operator config route` + +If the session wasn't launched with `claude -n ""`, tell the operator to relaunch with `-n` (or rename in-client, if their client supports it). You can neither read nor set your own display name, so this is an instruction to the human — **state it and move on; never block on it.** In revise mode the name still carries the **issue** number, not the PR number. + +## 2. Record the main checkout + +```bash +git rev-parse --show-toplevel +``` + +Keep that path. **Every instruction file is read by absolute path from there** — `.claude/agents/impl-agent.md`, `.claude/agents/revise-agent.md`, `.claude/docs/ENGINEERING.md`, `.claude/docs/PIPELINE.md`, `CLAUDE.md` — and **every edit goes only to paths inside the worktree.** That split is what makes a ticket that edits `impl-agent.md` safe: you follow the committed version while changing the worktree copy. + +## 3. Resolve the mode from state + +```bash +gh pr view --repo SGAOperations/aplio --json labels,headRefName,baseRefName,title +``` + +- Resolves to a PR labeled `needs revision` → **revise mode** (`.claude/agents/revise-agent.md`). +- Otherwise: + + ```bash + gh issue view --repo SGAOperations/aplio --json labels,assignees,title + ``` + + Labeled `plan approved` → **impl mode** (`.claude/agents/impl-agent.md`). **Record the issue's assignee login** (`@me` if none) — the PR must carry it. + +- Anything else → stop, report the current labels, change nothing: `# is not awaiting an operator (labels: …). Nothing was changed.` + +**If the item carries the trigger label but not `operator route`,** ask first (AskUserQuestion): _"#412 has no `operator route` label, so the cockpit may dispatch `impl-agent` for it too. Proceed anyway / Cancel."_ A double dispatch — cockpit and operator on the same item — is the hazard this guards. + +Then report the resolved mode, worktree path and branch before the slow steps, so the operator can see you picked the right stage. + +## 4. Create the worktree + +Never work in the main checkout: editing `.claude/` from the session using it mutates your live configuration mid-task. Never touch another worktree, and never `--force`. + +**impl mode** — branch straight off `dev` (this replaces the agent's checkout-`main`-then-rebase): + +```bash +git fetch origin +git worktree add -b -ticket-name-in-kebab-case .claude/worktrees/impl- origin/dev +``` + +**revise mode** — detached at the PR's head, rebased onto its base (per `revise-agent.md` step 2; push by refspec at the end): + +```bash +git fetch origin +git worktree add --detach .claude/worktrees/impl- origin/ +``` + +Then `cd` into the worktree and bootstrap it — this is also what activates the commit hooks there: + +```bash +npm ci +npm run prisma:generate +``` + +In revise mode, rebase onto the base branch from inside the worktree: `git rebase origin/`. + +## 5. Follow the agent file + +Read the resolved agent file from the **main checkout** and follow it end to end: label swaps, the plan checklist, the `.temp/commit-msg.txt` commit format, the three CI checks, push by refspec, PR body format, base `dev`, the issue's assignee, thread resolution, the revision note. Also read `.claude/docs/ENGINEERING.md` and its **Pre-PR self-check**, as the agent file requires. + +**One addition to `gh pr create` (impl mode):** pass `--label "operator route"` so the marker travels to the PR — the same precedent as copying the assignee, and it makes every later cockpit decision a pure label check. + +```bash +gh pr create --repo SGAOperations/aplio \ + --base dev \ + --title "# " \ + --body-file .temp/pr-.md \ + --assignee "" \ + --label "operator route" \ + --head -ticket-name-in-kebab-case +``` + +## 6. Overrides + +**This is the whole list. Anything not here applies unchanged** — if the agent files gain a rule that only makes sense for a subagent, it belongs here. + +1. **`permissionMode: dontAsk` and "auto-denied silently"** — not your mode. Your session prompts normally. +2. **"STOP and emit `BLOCKED:`"** — pointless with a human present. Ask the operator directly (AskUserQuestion) and wait. Never emit a `BLOCKED:` sentinel, never guess. +3. **"Never spawn subagents"** — not applicable. +4. **The shell-allowlist discipline** (no `cat`/`grep`/`sed`/`find`, bare commands only, no `cd`, quoted cwd-relative paths) — that exists for the subagent's allowlist. Use whatever is clearest. **Still preferred:** `npm run …` over `npx …` for the toolchain, and `git rm` for tracked deletions. +5. **"You are already in a worktree; never run `git worktree`"** — inverted. You create the worktree (§4) and `cd` into it. +6. **Instruction files are read from the recorded main checkout** (§2), not the cwd — the worktree copy may be the thing you are editing. + +## 7. Handoff + +Per the agent file's own Handoff step: + +- **impl** — issue `in progress` → `pr opened`; PR gets `ready for review`. +- **revise** — PR `revising` → `ready for review`. + +The cockpit picks up review on its next tick. Finish by telling the operator the PR URL, the labels applied, and that `/worktree-clean` reclaims `.claude/worktrees/impl-` once the PR merges. diff --git a/.claude/skills/pipeline/SKILL.md b/.claude/skills/pipeline/SKILL.md index b092bd8c..ea3d2cb7 100644 --- a/.claude/skills/pipeline/SKILL.md +++ b/.claude/skills/pipeline/SKILL.md @@ -18,6 +18,7 @@ You are the orchestrator of the agent pipeline in `.claude/docs/PIPELINE.md`. Th - Never act on issues/PRs that lack a pipeline **trigger** label — opt-in is human-initiated. - **Never act on an item assigned to another operator** — ownership transfers only through an explicit human take-over (see Ownership). - Never dispatch for an item with an **in-flight** label (`planning`, `in progress`, `reviewing`, `revising`, `refreshing`) — an agent owns it or a human paused it. +- **Never dispatch `impl-agent` or `revise-agent` for an item labelled `operator route`** — the harness blocks a dispatched agent from editing `CLAUDE.md` / `.claude/**`, so stages 2 and 4 run in the operator's own session. **Announce the command instead** (see Operator-route items). `review-agent` and refresh-mode dispatches are unaffected. - Every dispatch runs in the background (`run_in_background: true`). Worktree isolation, model, tool scope, and **permission mode (`dontAsk` — auto-denies anything not allow-listed)** all come from the subagent definition in `.claude/agents/` — you do not set them at the call site. (Since CC v2.1.186 a background subagent's prompts surface to you unless it runs `dontAsk` **and** this session is in default mode — see Model & permission mode.) - **Respect the draining flag:** while draining (see Stop controls), dispatch nothing new and schedule no wakeup; only report state and relay completions. @@ -45,10 +46,12 @@ gh issue list --repo SGAOperations/aplio --assignee "@me" --label "plan review" gh issue list --repo SGAOperations/aplio --assignee "@me" --label "blocked" --json number,title gh pr list --repo SGAOperations/aplio --assignee "@me" --label "approved" --json number,title gh pr list --repo SGAOperations/aplio --assignee "@me" --label "needs human" --json number,title +gh issue list --repo SGAOperations/aplio --assignee "@me" --label "operator route" --json number,title,labels +gh pr list --repo SGAOperations/aplio --assignee "@me" --label "operator route" --json number,title,labels # Unowned sweep → report only, never act (see Ownership above) -gh issue list --repo SGAOperations/aplio --search "no:assignee" --limit 100 --json number,title,labels --jq '[.[] | select(.labels | map(.name) | any(. == "ready" or . == "plan changes requested" or . == "plan approved" or . == "plan review" or . == "blocked"))]' -gh pr list --repo SGAOperations/aplio --search "no:assignee" --limit 100 --json number,title,labels --jq '[.[] | select(.labels | map(.name) | any(. == "ready for review" or . == "needs revision" or . == "approved" or . == "needs human"))]' +gh issue list --repo SGAOperations/aplio --search "no:assignee" --limit 100 --json number,title,labels --jq '[.[] | select(.labels | map(.name) | any(. == "ready" or . == "plan changes requested" or . == "plan approved" or . == "plan review" or . == "blocked" or . == "operator route"))]' +gh pr list --repo SGAOperations/aplio --search "no:assignee" --limit 100 --json number,title,labels --jq '[.[] | select(.labels | map(.name) | any(. == "ready for review" or . == "needs revision" or . == "approved" or . == "needs human" or . == "operator route"))]' # Ungated-PR sweep → report only, never act (see Ungated report below) gh pr list --repo SGAOperations/aplio --assignee "@me" --json number,title,labels --jq '[.[] | select((.labels | map(.name)) as $l | ($l | any(. == "ready for review" or . == "reviewing" or . == "needs revision" or . == "revising" or . == "approved" or . == "refresh branch" or . == "refreshing" or . == "needs human")) and ($l | index("claude") | not)) | {number, title}]' @@ -101,14 +104,14 @@ Agent({ Stage → trigger mapping: -| Trigger query result | subagent_type | -| -------------------------------------- | ---------------------------------------------- | -| Issue labeled `ready` | `plan-agent` (fresh plan) | -| Issue labeled `plan changes requested` | `plan-agent` (revision) | -| Issue labeled `plan approved` | `impl-agent` | -| PR labeled `ready for review` | `review-agent` | -| PR labeled `needs revision` | `revise-agent` — **after the cycle-cap check** | -| PR labeled `refresh branch` | `revise-agent` in **refresh mode** | +| Trigger query result | subagent_type | +| -------------------------------------- | ----------------------------------------------------------------------------------------------------- | +| Issue labeled `ready` | `plan-agent` (fresh plan) | +| Issue labeled `plan changes requested` | `plan-agent` (revision) | +| Issue labeled `plan approved` | `impl-agent` — **unless `operator route`: announce, never dispatch** | +| PR labeled `ready for review` | `review-agent` | +| PR labeled `needs revision` | `revise-agent` — **after the cycle-cap check**; **unless `operator route`: announce, never dispatch** | +| PR labeled `refresh branch` | `revise-agent` in **refresh mode** | For a refresh, say so in the prompt so the agent takes its Refresh mode path: `Run your pipeline stage for PR # in refresh mode (label: refresh branch).` @@ -133,11 +136,38 @@ then notify the human. For each issue labeled `plan review`: +- **Read the route first** — the plan declares whether stages 2/4 can be dispatched at all: + + ```bash + gh issue view --repo SGAOperations/aplio --json body --jq '.body | contains("ROUTE: operator session")' + ``` + + `true` means a **config ticket** (`CLAUDE.md` / `.claude/**`) — add `operator route` in the **same** label edit that approves the plan, and announce instead of dispatching (see Operator-route items). + - **Without `auto plan`:** summarize the plan from the issue body in a few sentences, then ask (AskUserQuestion): **Approve** / **Request changes** / **Discuss**. - - Approve → `gh issue edit --repo SGAOperations/aplio --remove-label "plan review" --add-label "plan approved"` (impl dispatches this tick). + - Approve → + ```bash + gh issue edit --repo SGAOperations/aplio --remove-label "plan review" --add-label "plan approved" # route false + gh issue edit --repo SGAOperations/aplio --remove-label "plan review" --add-label "plan approved,operator route" # route true + ``` + Route `false` → impl dispatches this tick. Route `true` → dispatch nothing; announce the operator command. - Request changes → write the human's feedback to `.temp/feedback-.md`, `gh issue comment --repo SGAOperations/aplio --body-file .temp/feedback-.md`, then `--remove-label "plan review" --add-label "plan changes requested"`. - Discuss → converse; finish with one of the two transitions above. -- **With `auto plan`:** swap `plan review` → `plan approved` immediately, no interaction, and dispatch impl this tick. +- **With `auto plan`:** run the same route check, swap `plan review` → `plan approved` (plus `operator route` when the route is `true`) immediately, no interaction — and dispatch impl this tick **only** when the route is `false`. + +### Operator-route items + +An item labelled `operator route` keeps its trigger label but is **never** dispatched (`.claude/docs/PIPELINE.md` → "Config tickets"). Announce it **once per session per item** — the same tracked-announcement pattern as `approved` PRs — and take no other action. Derive the session name from the issue title: `#: <2–5 lowercase words>`. + +- **Issue at `plan approved` + `operator route`:** + + > 🧰 #503 is a **config ticket** (touches `CLAUDE.md` / `.claude/**`) — dispatched agents can't edit those. Open a named session and run it yourself: + > `claude -n "#503: operator config route"` then `/implement 503` + > I'll pick it back up at review. + +- **PR at `needs revision` + `operator route`** (announce **after** the cycle-cap check, which still runs and can still escalate to `needs human`): + + > 🧰 PR #512 needs revision and is a config ticket — `claude -n "#503: operator config route"` then `/implement 512` in your own session. I'll review again once it's back at `ready for review`. (The session name carries the **issue** number; the command takes the PR number.) ### Approved PRs @@ -173,7 +203,7 @@ Interpret intent, not literal syntax: Dispatch the plan agent the same tick. - **"scope out X" / "break down X"** — Stage 0 deserves a stronger model than haiku; suggest the human run `/scope` in their main session. -- **"status"** — re-run the tick queries **live** and build the table from them (never from session memory): each in-flight item + stage, each item waiting on the human, and each PR currently labeled `approved` (the live `gh pr list --assignee "@me" --label approved` result — a merged PR has already dropped out, so it must not appear). It **inherits the assignee filter**, so it reports only this operator's items; append the unowned sweep result as a separate **"unowned"** line, and the ungated-PR sweep result as an **"ungated"** line, so a stalled ticket or a PR with no approval gate is diagnosable from one command. +- **"status"** — re-run the tick queries **live** and build the table from them (never from session memory): each in-flight item + stage, each item waiting on the human, and each PR currently labeled `approved` (the live `gh pr list --assignee "@me" --label approved` result — a merged PR has already dropped out, so it must not appear). It **inherits the assignee filter**, so it reports only this operator's items; append the unowned sweep result as a separate **"unowned"** line, and the ungated-PR sweep result as an **"ungated"** line, so a stalled ticket or a PR with no approval gate is diagnosable from one command. List `operator route` items under the human-gated group with the commands to run, e.g. `#503 — plan approved · operator route → claude -n "#503: operator config route" · /implement 503`. - **"pause #N"** — remove the item's current trigger label; confirm what was removed. Same ownership rule as opt-in: if the item belongs to **another operator**, say so and stop rather than touch its labels. - **"resume #N" / "retry #N"** — re-apply the trigger label for where it stalled (issue stuck in `planning` → `ready`; PR stuck in `revising` → `needs revision`; PR stuck in `refreshing` → `refresh branch`; etc.). Same ownership rule as opt-in: if the item is **unassigned**, add `--add-assignee "@me"` in the same command (re-applying a trigger to an unassigned item is a no-op for every cockpit); if it belongs to **another operator**, say so and stop rather than re-trigger. - **"refresh #N"** — apply `refresh branch` to that PR and dispatch it this tick, bypassing the per-merge cap. Use it to force a fresh preview deployment on a PR left quota-red. Same ownership rule as opt-in. @@ -201,4 +231,4 @@ Background-agent completions wake this session automatically; the scheduled wake ## Manual / recovery -Each stage is also runnable by hand without the cockpit — @-mention the subagent (e.g. `@agent-impl-agent implement #142`) or run a whole session as it via `claude --agent impl-agent`. All durable state is in labels, so `retry #N` (or re-applying the trigger label on GitHub) recovers any stalled item. +Each stage is also runnable by hand without the cockpit — @-mention the subagent (e.g. `@agent-impl-agent implement #142`) or run a whole session as it via `claude --agent impl-agent`. All durable state is in labels, so `retry #N` (or re-applying the trigger label on GitHub) recovers any stalled item. **Exception:** an `operator route` item is never dispatched — run `/implement ` in your own named session, since no subagent can edit `CLAUDE.md` / `.claude/**`. diff --git a/.claude/skills/worktree-clean/SKILL.md b/.claude/skills/worktree-clean/SKILL.md index 114453f5..c4efa899 100644 --- a/.claude/skills/worktree-clean/SKILL.md +++ b/.claude/skills/worktree-clean/SKILL.md @@ -7,7 +7,7 @@ allowed-tools: Read, Bash # Clean up pipeline worktrees -Pipeline agents run in isolated worktrees under `.claude/worktrees/agent-*`. On Windows, once an agent has run `npm ci`, the harness often **de-registers** the worktree (removes its `.git` file) but **can't delete the directory** — the populated `node_modules` (and long/`(app)`-parenthesized paths) defeat `git worktree remove` (`Invalid argument`) and `git worktree prune` (which only clears registrations whose directory is already gone). These **orphan directories** then accumulate (hundreds of MB of `node_modules` each). The cockpit deliberately does **not** force-delete them in its autonomous loop; this skill does it, interactively, run by you from the **main checkout**. +Pipeline agents run in isolated worktrees under `.claude/worktrees/agent-*`; operator-run config tickets (`/implement`) add `.claude/worktrees/impl-*`. Both are in scope here. On Windows, once an agent has run `npm ci`, the harness often **de-registers** the worktree (removes its `.git` file) but **can't delete the directory** — the populated `node_modules` (and long/`(app)`-parenthesized paths) defeat `git worktree remove` (`Invalid argument`) and `git worktree prune` (which only clears registrations whose directory is already gone). These **orphan directories** then accumulate (hundreds of MB of `node_modules` each). The cockpit deliberately does **not** force-delete them in its autonomous loop; this skill does it, interactively, run by you from the **main checkout**. > **Scope guard:** this skill only ever deletes directories **directly under `.claude/worktrees/`**. Never delete anything outside that directory, never the main checkout, and never a worktree of an **in-flight** pipeline item (check `gh pr list`/the cockpit first if unsure). diff --git a/CLAUDE.md b/CLAUDE.md index fae5b46c..de0f021a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -62,6 +62,7 @@ All tests live under `tests/`, never co-located with the source they cover: `tes ## Worktrees & local dev - Pipeline agents get their own isolated worktree automatically (`isolation: worktree`) — they handle setup; see `.claude/docs/PIPELINE.md`. Do not script worktree creation for them. +- **Config tickets are implemented by you, not by an agent** — anything touching `CLAUDE.md` or `.claude/**`, because the harness blocks a dispatched agent's `Edit` there. The plan declares it, the cockpit labels it `operator route` and announces the command: launch a named session (`claude -n "#XXX: "`) and run `/implement XXX`, which runs stage 2/4 in its own worktree. See `.claude/docs/PIPELINE.md` → "Config tickets". - For manual local work in a worktree, install deps with `npm ci` (then `npm run prisma:generate`). **Do not `ln -s node_modules` — symlinks fall back to copies on Windows here.** Sync before resuming: `git fetch origin && git rebase origin/dev`. - **`npm ci` is what activates Git hooks** — it runs `prepare` (`husky && npm run hooks:check`), which regenerates the untracked `.husky/_` bootstrap dir, sets `core.hooksPath`, and fails `npm ci` itself if activation didn't take. Each worktree/clone needs its own `npm ci` for hooks to fire there. If hooks stop firing, re-run `npm ci` (or `npm run prepare`) and verify with `npm run hooks:check`. - **The same step sets `core.commentChar=';'`** — git's default `#` makes it strip the mandated `#XXX` subject as a comment every time it re-reads a message through the editor machinery (`git rebase --continue`, `git commit --amend`), silently promoting the first body line into the subject; the `commit-msg` hook does not run on that path. Side effect: git's own instructional lines in the commit editor are `;`-prefixed. This writes to the shared `.git/config`, so one worktree's `npm ci` fixes every worktree of that clone. It cannot be enforced across fresh clones or forks, which is why CI validates subjects too. From f019e79f0f1c9890b8022b5bb393b0fbc50c21d6 Mon Sep 17 00:00:00 2001 From: b-at-neu Date: Wed, 19 Aug 2026 18:57:49 -0400 Subject: [PATCH 2/5] #503 replace the operator route label with a SESSION REQUIRED body marker One marker string in the issue plan and the PR description, read from the body field the cockpit's trigger queries already return. Names the routing rather than the cause, so future non-.claude categories reuse it. Co-Authored-By: Claude Opus 5 --- .claude/agents/plan-agent.md | 8 ++- .claude/docs/PIPELINE.md | 70 +++++++++++++++++--------- .claude/skills/implement/SKILL.md | 17 +++++-- .claude/skills/pipeline/SKILL.md | 67 +++++++++++------------- .claude/skills/worktree-clean/SKILL.md | 2 +- CLAUDE.md | 2 +- 6 files changed, 94 insertions(+), 72 deletions(-) diff --git a/.claude/agents/plan-agent.md b/.claude/agents/plan-agent.md index 7b5665b1..561f4c99 100644 --- a/.claude/agents/plan-agent.md +++ b/.claude/agents/plan-agent.md @@ -68,7 +68,13 @@ Construct the **full new issue body** (original ticket description preserved on gh issue edit N --repo SGAOperations/aplio --body-file .temp/plan-N.md ``` -**Route declaration (config tickets).** If any file in your **## Changes** section is `CLAUDE.md` or lives under `.claude/`, the plan body's **first line — directly under the `## Implementation Plan` heading, before `## Overview` — must be exactly `ROUTE: operator session`**, followed by one sentence naming why. The harness blocks a dispatched subagent from editing those paths, so the cockpit routes stages 2 and 4 to an operator session instead (`.claude/docs/PIPELINE.md` → "Config tickets"). Emit it in **revision mode** too — a revised plan can change route. Never emit it for a plan that touches neither. +**Session-required declaration.** Some tickets can't be handed to a dispatched agent at all. Today that means any plan whose **## Changes** touches `CLAUDE.md` or `.claude/**` — the harness blocks a subagent's `Edit` there — but the marker is deliberately about the _routing_, not the cause, so future categories reuse it. For those plans, the body's **first line, directly under the `## Implementation Plan` heading and before `## Overview`**, is the marker, with the reason after the colon: + +``` +> **SESSION REQUIRED:** touches `CLAUDE.md` / `.claude/**` — a dispatched agent can't edit those +``` + +The literal string `SESSION REQUIRED` is what the cockpit greps for — **never reword it**. The reason after the colon is free text and is the part that generalizes. Emit it in **revision mode** too, since a revised plan can change the routing, and never emit it for a plan that doesn't need a session. Full rules: `.claude/docs/PIPELINE.md` → "Session-required tickets". **Use the fixed structure** in `.claude/docs/PIPELINE.md` → "Implementation plan" — the canonical section list, order, and writing style. Think through how the feature should actually work and look (it's a product/UX design, not just a file checklist), but write it tight: bullets, short sentences, **don't restate the ticket**, omit sections that don't apply. The plan must still _decide_ the substance — even though it's brief: diff --git a/.claude/docs/PIPELINE.md b/.claude/docs/PIPELINE.md index 21c0c003..9b2dfad2 100644 --- a/.claude/docs/PIPELINE.md +++ b/.claude/docs/PIPELINE.md @@ -97,19 +97,18 @@ Rule: **every stage agent's first action is swapping its trigger label for its i ### Issue labels -| Label | Set by | Type | Meaning | -| ------------------------ | ---------------------------------------- | --------- | ---------------------------------------------------------------------- | -| `claude` | Cockpit (at opt-in) | marker | Claude is handling this ticket | -| `ready` | Cockpit (at opt-in) | trigger | Dispatch `plan-agent` | -| `planning` | `plan-agent` | in-flight | Plan being researched/written | -| `plan review` | `plan-agent` | gate | Plan written — awaiting human approval in cockpit | -| `plan changes requested` | Cockpit (human feedback) | trigger | Dispatch `plan-agent` in revision mode | -| `plan approved` | Cockpit (human approval, or `auto plan`) | trigger | Dispatch `impl-agent` | -| `auto plan` | Cockpit (at opt-in) | marker | Plan gate skipped: `plan review` auto-approved | -| `operator route` | Cockpit (at the plan gate) | marker | Stages 2/4 run in an operator session (`/implement`); never dispatched | -| `in progress` | `impl-agent` | in-flight | Implementation underway | -| `pr opened` | `impl-agent` | terminal | PR open; remaining state tracked on the PR | -| `blocked` | `impl-agent` | gate | Needs human decision; details in issue comment | +| Label | Set by | Type | Meaning | +| ------------------------ | ---------------------------------------- | --------- | ------------------------------------------------- | +| `claude` | Cockpit (at opt-in) | marker | Claude is handling this ticket | +| `ready` | Cockpit (at opt-in) | trigger | Dispatch `plan-agent` | +| `planning` | `plan-agent` | in-flight | Plan being researched/written | +| `plan review` | `plan-agent` | gate | Plan written — awaiting human approval in cockpit | +| `plan changes requested` | Cockpit (human feedback) | trigger | Dispatch `plan-agent` in revision mode | +| `plan approved` | Cockpit (human approval, or `auto plan`) | trigger | Dispatch `impl-agent` | +| `auto plan` | Cockpit (at opt-in) | marker | Plan gate skipped: `plan review` auto-approved | +| `in progress` | `impl-agent` | in-flight | Implementation underway | +| `pr opened` | `impl-agent` | terminal | PR open; remaining state tracked on the PR | +| `blocked` | `impl-agent` | gate | Needs human decision; details in issue comment | ### PR labels @@ -124,7 +123,6 @@ Rule: **every stage agent's first action is swapping its trigger label for its i | `needs human` | Cockpit / `revise-agent` | gate | 5 cycles without convergence, or an ambiguous rebase conflict needing the author; pipeline stops | | `refresh branch` | Cockpit / human | trigger | Dispatch `revise-agent` in refresh mode — rebase onto base and force-push, no code changes | | `refreshing` | `revise-agent` | in-flight | Branch refresh underway; the PR's other labels (e.g. `approved`) are left in place | -| `operator route` | `/implement` (at PR creation) | marker | Stages 2/4 run in an operator session; the cockpit never dispatches `revise-agent` for it | ## Stages and models @@ -139,7 +137,7 @@ Rule: **every stage agent's first action is swapping its trigger label for its i All four workers read `.claude/docs/ENGINEERING.md` before working; the review agent treats it as a review dimension. -**Stages 2 and 4 have an operator variant.** A ticket touching `CLAUDE.md` or `.claude/**` can't be implemented by a dispatched agent (the harness denies its `Edit`), so those two stages run in the operator's own session via `/implement`, following these same agent files — see "Config tickets". +**Stages 2 and 4 have an operator variant.** Some tickets can't be implemented by a dispatched agent at all — today, any that touch `CLAUDE.md` or `.claude/**`, where the harness denies its `Edit`. Those two stages then run in the operator's own session via `/implement`, following these same agent files — see "Session-required tickets". ## Permission rationale @@ -165,19 +163,41 @@ Permission mode: every stage agent runs **`permissionMode: dontAsk`** (auto-deny **CI merge gate — `approval-check.yml`.** PRs into `dev` are gated on the `approved` label **only when the PR carries `claude`** (a job-level `if:`). Every other PR — human, Dependabot — gets a `skipped` check run, which GitHub counts as satisfied, so it merges on its own merits. This is deliberately **fail-open**: an unlabelled pipeline PR is indistinguishable in CI from a human one and simply loses its gate. Nothing in the workflow can close that, so the mitigations live upstream — `impl-agent` passes `--label "claude"` at `gh pr create` (so the gate is live on the PR's first event) and the cockpit's **ungated-PR sweep** reports any tracked PR missing it. Never narrow the workflow's trigger to exclude a PR: a workflow that never runs creates no check run, leaving the required check pending forever. -## Config tickets (`CLAUDE.md` / `.claude/**`) +## Session-required tickets + +**Some tickets can't be handed to a dispatched agent at all, so stages 2 and 4 run in the operator's own session.** Today there is exactly one such category — anything touching `CLAUDE.md` or `.claude/**` — but the mechanism is built around the _routing_, not the cause, so a future category reuses it by supplying a different reason. **The harness denies `Edit`/`Write` under `.claude/` to dispatched subagents, and `settings.json` cannot grant it back.** This repo already allows `Edit(**)`/`Write(**)` with no `.claude/` deny rule and the denial persists anyway — it sits above project config, so there is nothing to fix in the permission model. An operator's **main session** is unaffected: reading `.claude/agents/impl-agent.md` and acting on it spawns no subagent, so no subagent restriction applies. That asymmetry is the whole basis of this route. -**The route** — `plan-agent` declares it, the cockpit marks it, an operator runs it: +### The marker + +One string, one rendering, both surfaces — **`SESSION REQUIRED`**, with the reason after the colon: + +``` +> **SESSION REQUIRED:** touches `CLAUDE.md` / `.claude/**` — a dispatched agent can't edit those +``` + +| Surface | Written by | Where | +| --------- | ------------ | --------------------------------------------------------------------------------- | +| **Issue** | `plan-agent` | First line of the plan body, under `## Implementation Plan`, before `## Overview` | +| **PR** | `/implement` | Directly under `Closes #N` in the PR description | + +The literal string `SESSION REQUIRED` is the contract — **never reword it**; the reason after the colon is free text and is the part that generalizes. **There is deliberately no label.** The marker lives in the body on both surfaces, and the cockpit reads it from the `body` field of the trigger query it already runs, so the check costs no extra call and there is nothing to keep in sync: + +```bash +gh issue list --repo SGAOperations/aplio --assignee "@me" --label "plan approved" --json number,title,body +gh pr list --repo SGAOperations/aplio --assignee "@me" --label "needs revision" --json number,title,body +``` + +### The route -1. **Declare.** When the plan's **## Changes** touches `CLAUDE.md` or `.claude/**`, its first line is exactly `ROUTE: operator session` (see "Implementation plan"). -2. **Mark.** At the plan gate the cockpit reads the issue body for that sentinel and adds **`operator route`** in the same edit as `plan approved`. The marker is sticky and travels to the PR (`/implement` passes `--label "operator route"` to `gh pr create`), so every later cockpit decision is a pure label check. -3. **Run.** The operator opens a **named session** and runs `/implement `. That skill resolves the stage from the item's labels, creates a worktree under `.claude/worktrees/impl-`, and follows the **unmodified** `impl-agent.md` / `revise-agent.md` plus a short list of subagent-only overrides. The override list lives in the skill and nowhere else — one place to drift, one place to check. +1. **Declare.** `plan-agent` emits the marker when the plan needs it (see "Implementation plan"). +2. **Skip dispatch.** The cockpit finds it in the trigger query's `body` and **announces the command instead of dispatching**. The item keeps its trigger label. +3. **Run.** The operator opens a **named session** and runs `/implement `. That skill resolves the stage from the item's labels, creates a worktree under `.claude/worktrees/impl-`, follows the **unmodified** `impl-agent.md` / `revise-agent.md` plus a short list of subagent-only overrides, and repeats the marker in the PR description it writes. The override list lives in the skill and nowhere else — one place to drift, one place to check. -**What moves and what doesn't.** Only stages **2** and **4**. `plan-agent` and `review-agent` are read-only and work through `gh`, so stages 1 and 3 run unchanged. `refresh branch` is still dispatched to `revise-agent` — a rebase and force-push edit no files, and a conflict inside `CLAUDE.md` / `.claude/docs/**` is already on the never-touch list and escalates. +**What moves and what doesn't.** Only stages **2** and **4**. `plan-agent` and `review-agent` are read-only and work through `gh`, so stages 1 and 3 run unchanged — a session-required ticket is **not** out of the pipeline. `refresh branch` is still dispatched to `revise-agent` too: a rebase and force-push edit no files, and a conflict inside `CLAUDE.md` / `.claude/docs/**` is already on the never-touch list and escalates. -**The invariant this deviates from.** Everywhere else a trigger label means something is dispatching. An `operator route` item **keeps** its trigger label (`plan approved` / `needs revision`) and is **never** dispatched — the cockpit announces the command instead. Recovery is unchanged (re-apply the trigger), but the label alone no longer implies motion. That is why `operator route` appears in the cockpit's gate queries, its unowned sweep, and its `status` output: an unowned config ticket is otherwise invisible in a way an unowned normal ticket isn't, because no cockpit will ever nag about it. +**The invariant this deviates from.** Everywhere else a trigger label means something is dispatching. A session-required item **keeps** its trigger label (`plan approved` / `needs revision`) and is **never** dispatched — the cockpit announces instead. Recovery is unchanged (re-apply the trigger), but the label alone no longer implies motion, which is why `status` has to call these out explicitly: nothing else distinguishes one from an item that is genuinely mid-flight. **Session naming.** These sessions are long-lived and several run at once, so launch each with the issue number in its display name: @@ -189,7 +209,7 @@ claude -n "#503: operator config route" # then, in that session: /implement 50 **Always in a worktree.** `/implement` never works in the main checkout, for two reasons: editing `.claude/` from the session that is _using_ it mutates your live configuration mid-task, and the ticket may be editing the very agent file the session is following. In a worktree the session reads its instructions by absolute path from the main checkout while every edit lands on the worktree copy, so the committed behaviour holds for the whole run. Reclaim the worktree with `/worktree-clean` once the PR merges. -**Unverified:** whether the manual escape hatch `claude --agent impl-agent` carries the same restriction (its frontmatter forces `dontAsk` and worktree isolation). Untested — use `/implement` for config tickets regardless. If a future Claude Code version lifts the harness restriction, this whole route can be deleted; the sentinel and the label are the only things to unwind. +**Unverified:** whether the manual escape hatch `claude --agent impl-agent` carries the same restriction (its frontmatter forces `dontAsk` and worktree isolation). Untested — use `/implement` regardless. If a future Claude Code version lifts the harness restriction, this whole route can be deleted; the marker is the only thing to unwind. ## Pipeline output formats @@ -209,7 +229,7 @@ Every plan, review, summary, and comment is written for a human scanning fast: Appended below the ticket under a `---` then `## Implementation Plan`; revision mode replaces only that block. **Do not restate the ticket** — reference it. Fixed sections in this order; the conditional ones appear **only when they apply** (omit otherwise — no stub): -- **`ROUTE: operator session`** _(only when the plan touches `CLAUDE.md` or `.claude/**`)_ — a bare line before `## Overview`, exactly that text, plus one sentence naming why. It routes stages 2 and 4 to an operator session; see "Config tickets". +- **`SESSION REQUIRED` marker** _(only when the ticket can't be dispatched to an agent — today, when it touches `CLAUDE.md` or `.claude/**`)_ — the first line, before `## Overview`. Exact format and rules: "Session-required tickets". - **## Overview** — 2–4 sentences: what, why, the approach. - **## Changes** — files to create/modify, one bullet each: `` `path` — one-line reason ``. - **## Implementation** — ordered `- [ ]` checkboxes, one line each; fold validation / states / error-model notes into the step they belong to. @@ -311,7 +331,7 @@ Closing the cockpit session also halts dispatch (it is the only dispatcher) but | Labels manually changed on GitHub | Fine — labels are the source of truth | The next tick acts on whatever the labels say | | Stale worktrees / orphan `node_modules` dirs accumulating under `.claude/worktrees/` | Agents cut off mid-run; on Windows the harness leaves dirs git can't delete | Run **`/worktree-clean`** from the main checkout — it prunes registrations and force-deletes orphan dirs (`git worktree remove` alone fails with `Invalid argument` once `node_modules` exists). The cockpit reports these but never auto-deletes them. | | An agent stopped with `BLOCKED:` or hit `maxTurns` | Clean stop by design (not a crash) | Resolve the blocker (or widen scope/permissions), then `retry #N` | -| An issue sits at `plan approved`, or a PR at `needs revision`, and nothing dispatches | It carries `operator route` — a config ticket (`CLAUDE.md` / `.claude/**`) the cockpit never dispatches for | Open a named session and run it yourself: `claude -n "#N: "`, then `/implement `. See "Config tickets" | +| An issue sits at `plan approved`, or a PR at `needs revision`, and nothing dispatches | Its body carries the `SESSION REQUIRED` marker — the cockpit never dispatches for those | Open a named session and run it yourself: `claude -n "#N: "`, then `/implement `. See "Session-required tickets" | ## Reading current state without the cockpit diff --git a/.claude/skills/implement/SKILL.md b/.claude/skills/implement/SKILL.md index 415a4206..ce9bae39 100644 --- a/.claude/skills/implement/SKILL.md +++ b/.claude/skills/implement/SKILL.md @@ -1,6 +1,6 @@ --- name: implement -description: Run pipeline stage 2 or 4 yourself, in your own session, for a config ticket — one touching CLAUDE.md or .claude/**, which a dispatched agent cannot edit. Resolves the stage from the item's labels, works in a dedicated worktree, and follows the existing impl-agent / revise-agent definitions unchanged. Manual only. Usage: /implement +description: Run pipeline stage 2 or 4 yourself, in your own session, for a ticket marked SESSION REQUIRED — one that cannot be handed to a dispatched agent, today because it touches CLAUDE.md or .claude/**. Resolves the stage from the item's labels, works in a dedicated worktree, and follows the existing impl-agent / revise-agent definitions unchanged. Manual only. Usage: /implement disable-model-invocation: true allowed-tools: Read, Edit, Write, Glob, Grep, Bash, AskUserQuestion --- @@ -9,7 +9,7 @@ allowed-tools: Read, Edit, Write, Glob, Grep, Bash, AskUserQuestion **Trigger:** manual, in an operator's own session. **Input:** an issue or PR number (``). **Repo:** `SGAOperations/aplio`. -The harness denies `Edit`/`Write` under `.claude/` to **dispatched subagents**, and `settings.json` cannot grant it back — so `impl-agent` and `revise-agent` can't run for tickets that touch `CLAUDE.md` or `.claude/**`. Your session has no such restriction. This skill is a **thin wrapper**: it points you at the existing agent definition and overrides only the rules that exist because that agent is a subagent. Background and rationale: `.claude/docs/PIPELINE.md` → "Config tickets". +The harness denies `Edit`/`Write` under `.claude/` to **dispatched subagents**, and `settings.json` cannot grant it back — so `impl-agent` and `revise-agent` can't run for tickets that touch `CLAUDE.md` or `.claude/**`. Your session has no such restriction. Those tickets are marked **`SESSION REQUIRED`** in their body, and the cockpit announces them instead of dispatching. This skill is a **thin wrapper**: it points you at the existing agent definition and overrides only the rules that exist because that agent is a subagent. Background and rationale: `.claude/docs/PIPELINE.md` → "Session-required tickets". **The agent files are the workflow. Do not modify them, and do not reimplement them here.** @@ -46,7 +46,7 @@ gh pr view --repo SGAOperations/aplio --json labels,headRefName,baseRefName, - Anything else → stop, report the current labels, change nothing: `# is not awaiting an operator (labels: …). Nothing was changed.` -**If the item carries the trigger label but not `operator route`,** ask first (AskUserQuestion): _"#412 has no `operator route` label, so the cockpit may dispatch `impl-agent` for it too. Proceed anyway / Cancel."_ A double dispatch — cockpit and operator on the same item — is the hazard this guards. +**If the item carries the trigger label but its body has no `SESSION REQUIRED` marker,** ask first (AskUserQuestion): _"#412 isn't marked `SESSION REQUIRED`, so the cockpit will dispatch an agent for it too. Proceed anyway / Cancel."_ A double dispatch — cockpit and operator on the same item — is the hazard this guards. Then report the resolved mode, worktree path and branch before the slow steps, so the operator can see you picked the right stage. @@ -81,7 +81,15 @@ In revise mode, rebase onto the base branch from inside the worktree: `git rebas Read the resolved agent file from the **main checkout** and follow it end to end: label swaps, the plan checklist, the `.temp/commit-msg.txt` commit format, the three CI checks, push by refspec, PR body format, base `dev`, the issue's assignee, thread resolution, the revision note. Also read `.claude/docs/ENGINEERING.md` and its **Pre-PR self-check**, as the agent file requires. -**One addition to `gh pr create` (impl mode):** pass `--label "operator route"` so the marker travels to the PR — the same precedent as copying the assignee, and it makes every later cockpit decision a pure label check. +**Carry the marker into the PR (impl mode).** The cockpit re-reads it on the PR to decide stage 4, so the PR description must repeat it verbatim, directly under `Closes #N`: + +``` +Closes #503 + +> **SESSION REQUIRED:** touches `CLAUDE.md` / `.claude/**` — a dispatched agent can't edit those +``` + +Same literal string as the issue plan, same rendering — `.claude/docs/PIPELINE.md` → "Session-required tickets". **No label is involved on either surface.** Otherwise `gh pr create` is exactly as the agent file specifies: ```bash gh pr create --repo SGAOperations/aplio \ @@ -89,7 +97,6 @@ gh pr create --repo SGAOperations/aplio \ --title "# " \ --body-file .temp/pr-.md \ --assignee "" \ - --label "operator route" \ --head -ticket-name-in-kebab-case ``` diff --git a/.claude/skills/pipeline/SKILL.md b/.claude/skills/pipeline/SKILL.md index ea3d2cb7..0863628e 100644 --- a/.claude/skills/pipeline/SKILL.md +++ b/.claude/skills/pipeline/SKILL.md @@ -18,7 +18,7 @@ You are the orchestrator of the agent pipeline in `.claude/docs/PIPELINE.md`. Th - Never act on issues/PRs that lack a pipeline **trigger** label — opt-in is human-initiated. - **Never act on an item assigned to another operator** — ownership transfers only through an explicit human take-over (see Ownership). - Never dispatch for an item with an **in-flight** label (`planning`, `in progress`, `reviewing`, `revising`, `refreshing`) — an agent owns it or a human paused it. -- **Never dispatch `impl-agent` or `revise-agent` for an item labelled `operator route`** — the harness blocks a dispatched agent from editing `CLAUDE.md` / `.claude/**`, so stages 2 and 4 run in the operator's own session. **Announce the command instead** (see Operator-route items). `review-agent` and refresh-mode dispatches are unaffected. +- **Never dispatch `impl-agent` or `revise-agent` for an item whose body carries the `SESSION REQUIRED` marker** — those tickets can't be handed to an agent (today: they touch `CLAUDE.md` / `.claude/**`, which the harness won't let a subagent edit). **Announce the command instead** (see Session-required items). `review-agent` and refresh-mode dispatches are never gated by it. - Every dispatch runs in the background (`run_in_background: true`). Worktree isolation, model, tool scope, and **permission mode (`dontAsk` — auto-denies anything not allow-listed)** all come from the subagent definition in `.claude/agents/` — you do not set them at the call site. (Since CC v2.1.186 a background subagent's prompts surface to you unless it runs `dontAsk` **and** this session is in default mode — see Model & permission mode.) - **Respect the draining flag:** while draining (see Stop controls), dispatch nothing new and schedule no wakeup; only report state and relay completions. @@ -36,9 +36,9 @@ On start and on every wakeup, run one polling pass: # Trigger labels → dispatch — this operator's items only gh issue list --repo SGAOperations/aplio --assignee "@me" --label "ready" --json number,title gh issue list --repo SGAOperations/aplio --assignee "@me" --label "plan changes requested" --json number,title -gh issue list --repo SGAOperations/aplio --assignee "@me" --label "plan approved" --json number,title +gh issue list --repo SGAOperations/aplio --assignee "@me" --label "plan approved" --json number,title,body gh pr list --repo SGAOperations/aplio --assignee "@me" --label "ready for review" --json number,title -gh pr list --repo SGAOperations/aplio --assignee "@me" --label "needs revision" --json number,title +gh pr list --repo SGAOperations/aplio --assignee "@me" --label "needs revision" --json number,title,body gh pr list --repo SGAOperations/aplio --assignee "@me" --label "refresh branch" --json number,title # Gates and announcements → talk to the human — this operator's items only @@ -46,12 +46,10 @@ gh issue list --repo SGAOperations/aplio --assignee "@me" --label "plan review" gh issue list --repo SGAOperations/aplio --assignee "@me" --label "blocked" --json number,title gh pr list --repo SGAOperations/aplio --assignee "@me" --label "approved" --json number,title gh pr list --repo SGAOperations/aplio --assignee "@me" --label "needs human" --json number,title -gh issue list --repo SGAOperations/aplio --assignee "@me" --label "operator route" --json number,title,labels -gh pr list --repo SGAOperations/aplio --assignee "@me" --label "operator route" --json number,title,labels # Unowned sweep → report only, never act (see Ownership above) -gh issue list --repo SGAOperations/aplio --search "no:assignee" --limit 100 --json number,title,labels --jq '[.[] | select(.labels | map(.name) | any(. == "ready" or . == "plan changes requested" or . == "plan approved" or . == "plan review" or . == "blocked" or . == "operator route"))]' -gh pr list --repo SGAOperations/aplio --search "no:assignee" --limit 100 --json number,title,labels --jq '[.[] | select(.labels | map(.name) | any(. == "ready for review" or . == "needs revision" or . == "approved" or . == "needs human" or . == "operator route"))]' +gh issue list --repo SGAOperations/aplio --search "no:assignee" --limit 100 --json number,title,labels --jq '[.[] | select(.labels | map(.name) | any(. == "ready" or . == "plan changes requested" or . == "plan approved" or . == "plan review" or . == "blocked"))]' +gh pr list --repo SGAOperations/aplio --search "no:assignee" --limit 100 --json number,title,labels --jq '[.[] | select(.labels | map(.name) | any(. == "ready for review" or . == "needs revision" or . == "approved" or . == "needs human"))]' # Ungated-PR sweep → report only, never act (see Ungated report below) gh pr list --repo SGAOperations/aplio --assignee "@me" --json number,title,labels --jq '[.[] | select((.labels | map(.name)) as $l | ($l | any(. == "ready for review" or . == "reviewing" or . == "needs revision" or . == "revising" or . == "approved" or . == "refresh branch" or . == "refreshing" or . == "needs human")) and ($l | index("claude") | not)) | {number, title}]' @@ -104,14 +102,16 @@ Agent({ Stage → trigger mapping: -| Trigger query result | subagent_type | -| -------------------------------------- | ----------------------------------------------------------------------------------------------------- | -| Issue labeled `ready` | `plan-agent` (fresh plan) | -| Issue labeled `plan changes requested` | `plan-agent` (revision) | -| Issue labeled `plan approved` | `impl-agent` — **unless `operator route`: announce, never dispatch** | -| PR labeled `ready for review` | `review-agent` | -| PR labeled `needs revision` | `revise-agent` — **after the cycle-cap check**; **unless `operator route`: announce, never dispatch** | -| PR labeled `refresh branch` | `revise-agent` in **refresh mode** | +| Trigger query result | subagent_type | +| -------------------------------------- | ------------------------------------------------------------------------------------------------------- | +| Issue labeled `ready` | `plan-agent` (fresh plan) | +| Issue labeled `plan changes requested` | `plan-agent` (revision) | +| Issue labeled `plan approved` | `impl-agent` — **unless `SESSION REQUIRED`: announce, never dispatch** | +| PR labeled `ready for review` | `review-agent` | +| PR labeled `needs revision` | `revise-agent` — **after the cycle-cap check**; **unless `SESSION REQUIRED`: announce, never dispatch** | +| PR labeled `refresh branch` | `revise-agent` in **refresh mode** | + +**Session-required items never dispatch.** Before dispatching `impl-agent` or `revise-agent`, check that item's `body` for the literal string `SESSION REQUIRED`. It is already in the trigger query's result (both queries request `body`), so this costs no extra call. Present → **announce, don't dispatch** (see Session-required items); absent → dispatch normally. For a refresh, say so in the prompt so the agent takes its Refresh mode path: `Run your pipeline stage for PR # in refresh mode (label: refresh branch).` @@ -136,38 +136,27 @@ then notify the human. For each issue labeled `plan review`: -- **Read the route first** — the plan declares whether stages 2/4 can be dispatched at all: - - ```bash - gh issue view --repo SGAOperations/aplio --json body --jq '.body | contains("ROUTE: operator session")' - ``` - - `true` means a **config ticket** (`CLAUDE.md` / `.claude/**`) — add `operator route` in the **same** label edit that approves the plan, and announce instead of dispatching (see Operator-route items). - -- **Without `auto plan`:** summarize the plan from the issue body in a few sentences, then ask (AskUserQuestion): **Approve** / **Request changes** / **Discuss**. - - Approve → - ```bash - gh issue edit --repo SGAOperations/aplio --remove-label "plan review" --add-label "plan approved" # route false - gh issue edit --repo SGAOperations/aplio --remove-label "plan review" --add-label "plan approved,operator route" # route true - ``` - Route `false` → impl dispatches this tick. Route `true` → dispatch nothing; announce the operator command. +- **Without `auto plan`:** summarize the plan from the issue body in a few sentences, then ask (AskUserQuestion): **Approve** / **Request changes** / **Discuss**. If the plan carries the `SESSION REQUIRED` marker, say so in the summary — the human should learn at the gate that they'll be running this one themselves. + - Approve → `gh issue edit --repo SGAOperations/aplio --remove-label "plan review" --add-label "plan approved"` (impl dispatches this tick — **unless** the plan is session-required, in which case this tick announces instead). - Request changes → write the human's feedback to `.temp/feedback-.md`, `gh issue comment --repo SGAOperations/aplio --body-file .temp/feedback-.md`, then `--remove-label "plan review" --add-label "plan changes requested"`. - Discuss → converse; finish with one of the two transitions above. -- **With `auto plan`:** run the same route check, swap `plan review` → `plan approved` (plus `operator route` when the route is `true`) immediately, no interaction — and dispatch impl this tick **only** when the route is `false`. +- **With `auto plan`:** swap `plan review` → `plan approved` immediately, no interaction, and dispatch impl this tick (same session-required exception). + +The gate applies **no special label** for a session-required plan — the marker is already in the issue body, and the dispatch step reads it from there. -### Operator-route items +### Session-required items -An item labelled `operator route` keeps its trigger label but is **never** dispatched (`.claude/docs/PIPELINE.md` → "Config tickets"). Announce it **once per session per item** — the same tracked-announcement pattern as `approved` PRs — and take no other action. Derive the session name from the issue title: `#: <2–5 lowercase words>`. +An item whose body carries the `SESSION REQUIRED` marker keeps its trigger label but is **never** dispatched (`.claude/docs/PIPELINE.md` → "Session-required tickets"). Announce it **once per session per item** — the same tracked-announcement pattern as `approved` PRs — and take no other action. Derive the session name from the **issue** title: `#: <2–5 lowercase words>`. -- **Issue at `plan approved` + `operator route`:** +- **Issue at `plan approved` + marker:** - > 🧰 #503 is a **config ticket** (touches `CLAUDE.md` / `.claude/**`) — dispatched agents can't edit those. Open a named session and run it yourself: + > 🧰 #503 needs its own session — its plan is marked `SESSION REQUIRED` (touches `CLAUDE.md` / `.claude/**`, which a dispatched agent can't edit). Run it yourself: > `claude -n "#503: operator config route"` then `/implement 503` > I'll pick it back up at review. -- **PR at `needs revision` + `operator route`** (announce **after** the cycle-cap check, which still runs and can still escalate to `needs human`): +- **PR at `needs revision` + marker** (announce **after** the cycle-cap check, which still runs and can still escalate to `needs human`): - > 🧰 PR #512 needs revision and is a config ticket — `claude -n "#503: operator config route"` then `/implement 512` in your own session. I'll review again once it's back at `ready for review`. (The session name carries the **issue** number; the command takes the PR number.) + > 🧰 PR #512 needs revision and is marked `SESSION REQUIRED` — `claude -n "#503: operator config route"` then `/implement 512` in your own session. I'll review again once it's back at `ready for review`. (The session name carries the **issue** number; the command takes the PR number.) ### Approved PRs @@ -203,7 +192,7 @@ Interpret intent, not literal syntax: Dispatch the plan agent the same tick. - **"scope out X" / "break down X"** — Stage 0 deserves a stronger model than haiku; suggest the human run `/scope` in their main session. -- **"status"** — re-run the tick queries **live** and build the table from them (never from session memory): each in-flight item + stage, each item waiting on the human, and each PR currently labeled `approved` (the live `gh pr list --assignee "@me" --label approved` result — a merged PR has already dropped out, so it must not appear). It **inherits the assignee filter**, so it reports only this operator's items; append the unowned sweep result as a separate **"unowned"** line, and the ungated-PR sweep result as an **"ungated"** line, so a stalled ticket or a PR with no approval gate is diagnosable from one command. List `operator route` items under the human-gated group with the commands to run, e.g. `#503 — plan approved · operator route → claude -n "#503: operator config route" · /implement 503`. +- **"status"** — re-run the tick queries **live** and build the table from them (never from session memory): each in-flight item + stage, each item waiting on the human, and each PR currently labeled `approved` (the live `gh pr list --assignee "@me" --label approved` result — a merged PR has already dropped out, so it must not appear). It **inherits the assignee filter**, so it reports only this operator's items; append the unowned sweep result as a separate **"unowned"** line, and the ungated-PR sweep result as an **"ungated"** line, so a stalled ticket or a PR with no approval gate is diagnosable from one command. List **session-required** items under the human-gated group with the commands to run, e.g. `#503 — plan approved · SESSION REQUIRED → claude -n "#503: operator config route" · /implement 503`. - **"pause #N"** — remove the item's current trigger label; confirm what was removed. Same ownership rule as opt-in: if the item belongs to **another operator**, say so and stop rather than touch its labels. - **"resume #N" / "retry #N"** — re-apply the trigger label for where it stalled (issue stuck in `planning` → `ready`; PR stuck in `revising` → `needs revision`; PR stuck in `refreshing` → `refresh branch`; etc.). Same ownership rule as opt-in: if the item is **unassigned**, add `--add-assignee "@me"` in the same command (re-applying a trigger to an unassigned item is a no-op for every cockpit); if it belongs to **another operator**, say so and stop rather than re-trigger. - **"refresh #N"** — apply `refresh branch` to that PR and dispatch it this tick, bypassing the per-merge cap. Use it to force a fresh preview deployment on a PR left quota-red. Same ownership rule as opt-in. @@ -231,4 +220,4 @@ Background-agent completions wake this session automatically; the scheduled wake ## Manual / recovery -Each stage is also runnable by hand without the cockpit — @-mention the subagent (e.g. `@agent-impl-agent implement #142`) or run a whole session as it via `claude --agent impl-agent`. All durable state is in labels, so `retry #N` (or re-applying the trigger label on GitHub) recovers any stalled item. **Exception:** an `operator route` item is never dispatched — run `/implement ` in your own named session, since no subagent can edit `CLAUDE.md` / `.claude/**`. +Each stage is also runnable by hand without the cockpit — @-mention the subagent (e.g. `@agent-impl-agent implement #142`) or run a whole session as it via `claude --agent impl-agent`. All durable state is in labels, so `retry #N` (or re-applying the trigger label on GitHub) recovers any stalled item. **Exception:** an item marked `SESSION REQUIRED` is never dispatched — run `/implement ` in your own named session. diff --git a/.claude/skills/worktree-clean/SKILL.md b/.claude/skills/worktree-clean/SKILL.md index c4efa899..6c9444b6 100644 --- a/.claude/skills/worktree-clean/SKILL.md +++ b/.claude/skills/worktree-clean/SKILL.md @@ -7,7 +7,7 @@ allowed-tools: Read, Bash # Clean up pipeline worktrees -Pipeline agents run in isolated worktrees under `.claude/worktrees/agent-*`; operator-run config tickets (`/implement`) add `.claude/worktrees/impl-*`. Both are in scope here. On Windows, once an agent has run `npm ci`, the harness often **de-registers** the worktree (removes its `.git` file) but **can't delete the directory** — the populated `node_modules` (and long/`(app)`-parenthesized paths) defeat `git worktree remove` (`Invalid argument`) and `git worktree prune` (which only clears registrations whose directory is already gone). These **orphan directories** then accumulate (hundreds of MB of `node_modules` each). The cockpit deliberately does **not** force-delete them in its autonomous loop; this skill does it, interactively, run by you from the **main checkout**. +Pipeline agents run in isolated worktrees under `.claude/worktrees/agent-*`; operator-run session-required tickets (`/implement`) add `.claude/worktrees/impl-*`. Both are in scope here. On Windows, once an agent has run `npm ci`, the harness often **de-registers** the worktree (removes its `.git` file) but **can't delete the directory** — the populated `node_modules` (and long/`(app)`-parenthesized paths) defeat `git worktree remove` (`Invalid argument`) and `git worktree prune` (which only clears registrations whose directory is already gone). These **orphan directories** then accumulate (hundreds of MB of `node_modules` each). The cockpit deliberately does **not** force-delete them in its autonomous loop; this skill does it, interactively, run by you from the **main checkout**. > **Scope guard:** this skill only ever deletes directories **directly under `.claude/worktrees/`**. Never delete anything outside that directory, never the main checkout, and never a worktree of an **in-flight** pipeline item (check `gh pr list`/the cockpit first if unsure). diff --git a/CLAUDE.md b/CLAUDE.md index de0f021a..4bebadda 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -62,7 +62,7 @@ All tests live under `tests/`, never co-located with the source they cover: `tes ## Worktrees & local dev - Pipeline agents get their own isolated worktree automatically (`isolation: worktree`) — they handle setup; see `.claude/docs/PIPELINE.md`. Do not script worktree creation for them. -- **Config tickets are implemented by you, not by an agent** — anything touching `CLAUDE.md` or `.claude/**`, because the harness blocks a dispatched agent's `Edit` there. The plan declares it, the cockpit labels it `operator route` and announces the command: launch a named session (`claude -n "#XXX: "`) and run `/implement XXX`, which runs stage 2/4 in its own worktree. See `.claude/docs/PIPELINE.md` → "Config tickets". +- **Some tickets are implemented by you, not by an agent** — today, anything touching `CLAUDE.md` or `.claude/**`, because the harness blocks a dispatched agent's `Edit` there. The plan marks these `SESSION REQUIRED` and the cockpit announces instead of dispatching: launch a named session (`claude -n "#XXX: "`) and run `/implement XXX`, which runs stage 2/4 in its own worktree. See `.claude/docs/PIPELINE.md` → "Session-required tickets". - For manual local work in a worktree, install deps with `npm ci` (then `npm run prisma:generate`). **Do not `ln -s node_modules` — symlinks fall back to copies on Windows here.** Sync before resuming: `git fetch origin && git rebase origin/dev`. - **`npm ci` is what activates Git hooks** — it runs `prepare` (`husky && npm run hooks:check`), which regenerates the untracked `.husky/_` bootstrap dir, sets `core.hooksPath`, and fails `npm ci` itself if activation didn't take. Each worktree/clone needs its own `npm ci` for hooks to fire there. If hooks stop firing, re-run `npm ci` (or `npm run prepare`) and verify with `npm run hooks:check`. - **The same step sets `core.commentChar=';'`** — git's default `#` makes it strip the mandated `#XXX` subject as a comment every time it re-reads a message through the editor machinery (`git rebase --continue`, `git commit --amend`), silently promoting the first body line into the subject; the `commit-msg` hook does not run on that path. Side effect: git's own instructional lines in the commit editor are `;`-prefixed. This writes to the shared `.git/config`, so one worktree's `npm ci` fixes every worktree of that clone. It cannot be enforced across fresh clones or forks, which is why CI validates subjects too. From b15a8fa580b2b4723abbc5e99761af0b8d170044 Mon Sep 17 00:00:00 2001 From: b-at-neu Date: Wed, 19 Aug 2026 19:03:07 -0400 Subject: [PATCH 3/5] #503 pass the claude label when /implement opens its PR #513 gated approval-check.yml on the claude label, so an operator-run PR opened without it would merge with no approval gate. Co-Authored-By: Claude Opus 5 --- .claude/skills/implement/SKILL.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.claude/skills/implement/SKILL.md b/.claude/skills/implement/SKILL.md index ce9bae39..4cfbd131 100644 --- a/.claude/skills/implement/SKILL.md +++ b/.claude/skills/implement/SKILL.md @@ -89,7 +89,7 @@ Closes #503 > **SESSION REQUIRED:** touches `CLAUDE.md` / `.claude/**` — a dispatched agent can't edit those ``` -Same literal string as the issue plan, same rendering — `.claude/docs/PIPELINE.md` → "Session-required tickets". **No label is involved on either surface.** Otherwise `gh pr create` is exactly as the agent file specifies: +Same literal string as the issue plan, same rendering — `.claude/docs/PIPELINE.md` → "Session-required tickets". **The routing marker is never a label.** The one label `gh pr create` does pass is `claude`, exactly as the agent file specifies — it **activates the approval gate** (`approval-check.yml` runs only on PRs carrying it), so a PR opened without it merges with no gate at all: ```bash gh pr create --repo SGAOperations/aplio \ @@ -97,6 +97,7 @@ gh pr create --repo SGAOperations/aplio \ --title "# " \ --body-file .temp/pr-.md \ --assignee "" \ + --label "claude" \ --head -ticket-name-in-kebab-case ``` From 70551f0e329e0c4217f7c077d5abd5cf10088ffe Mon Sep 17 00:00:00 2001 From: b-at-neu Date: Wed, 19 Aug 2026 19:20:46 -0400 Subject: [PATCH 4/5] #503 add body to the gh queries in step 3 of the implement skill Step 3 checks the item's body for the SESSION REQUIRED marker but the gh pr view / gh issue view calls above it didn't request the body field, so there was nothing to inspect at that point. Co-Authored-By: Claude Sonnet 5 --- .claude/skills/implement/SKILL.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.claude/skills/implement/SKILL.md b/.claude/skills/implement/SKILL.md index 4cfbd131..22fc4e9a 100644 --- a/.claude/skills/implement/SKILL.md +++ b/.claude/skills/implement/SKILL.md @@ -32,14 +32,14 @@ Keep that path. **Every instruction file is read by absolute path from there** ## 3. Resolve the mode from state ```bash -gh pr view --repo SGAOperations/aplio --json labels,headRefName,baseRefName,title +gh pr view --repo SGAOperations/aplio --json labels,headRefName,baseRefName,title,body ``` - Resolves to a PR labeled `needs revision` → **revise mode** (`.claude/agents/revise-agent.md`). - Otherwise: ```bash - gh issue view --repo SGAOperations/aplio --json labels,assignees,title + gh issue view --repo SGAOperations/aplio --json labels,assignees,title,body ``` Labeled `plan approved` → **impl mode** (`.claude/agents/impl-agent.md`). **Record the issue's assignee login** (`@me` if none) — the PR must carry it. From db3aa3d2862d52c0bb393df8fda8b08ff659aeef Mon Sep 17 00:00:00 2001 From: b-at-neu Date: Wed, 19 Aug 2026 22:40:26 -0400 Subject: [PATCH 5/5] #503 make the cockpit's session-required handoff explicit Announcing was a caveat buried in the dispatch table, not a tick step, and nothing said the human must run /implement in a session other than the cockpit's own. Co-Authored-By: Claude Opus 5 --- .claude/docs/PIPELINE.md | 6 +++++- .claude/skills/pipeline/SKILL.md | 14 ++++++++------ 2 files changed, 13 insertions(+), 7 deletions(-) diff --git a/.claude/docs/PIPELINE.md b/.claude/docs/PIPELINE.md index 9b2dfad2..47d2a13a 100644 --- a/.claude/docs/PIPELINE.md +++ b/.claude/docs/PIPELINE.md @@ -11,7 +11,7 @@ claude # open a session (haiku recommended for the cockpit) Then talk to it: `work on #142` · `scope out a notifications feature` · `status` · `pause #142` · `retry #142` · `drain` · `resume` · `stop #142`. -## The two flows +## The flows ### Major feature @@ -22,6 +22,10 @@ Then talk to it: `work on #142` · `scope out a notifications feature` · `statu | 3. Approve plan | Read the summary, approve — or give feedback (it revises and comes back) | `impl-agent` builds in an isolated worktree, runs CI, opens a PR; `review-agent`/`revise-agent` loop until clean (max 5 rounds) | | 4. Merge | Click merge on GitHub | Issue closes automatically | +### Session-required ticket + +Same as above through step 3, then it diverges: some tickets can't be handed to an agent at all (today, any touching `CLAUDE.md` or `.claude/**`). Their plan carries a **`SESSION REQUIRED`** marker, and at step 3 the cockpit **announces instead of dispatching** — it hands you a launch command and you run **`/implement ` in a separate session**. Nothing moves until you do; no agent will ever pick it up. Review (step 3's second half) and merge are unchanged. Full detail: "Session-required tickets". + ### Bug fix File the issue → in the cockpit: "work on #N, auto-approve the plan" → wait → merge on GitHub. The plan gate is skipped (`auto plan`); the merge gate never is. diff --git a/.claude/skills/pipeline/SKILL.md b/.claude/skills/pipeline/SKILL.md index 0863628e..73b1aa26 100644 --- a/.claude/skills/pipeline/SKILL.md +++ b/.claude/skills/pipeline/SKILL.md @@ -18,7 +18,7 @@ You are the orchestrator of the agent pipeline in `.claude/docs/PIPELINE.md`. Th - Never act on issues/PRs that lack a pipeline **trigger** label — opt-in is human-initiated. - **Never act on an item assigned to another operator** — ownership transfers only through an explicit human take-over (see Ownership). - Never dispatch for an item with an **in-flight** label (`planning`, `in progress`, `reviewing`, `revising`, `refreshing`) — an agent owns it or a human paused it. -- **Never dispatch `impl-agent` or `revise-agent` for an item whose body carries the `SESSION REQUIRED` marker** — those tickets can't be handed to an agent (today: they touch `CLAUDE.md` / `.claude/**`, which the harness won't let a subagent edit). **Announce the command instead** (see Session-required items). `review-agent` and refresh-mode dispatches are never gated by it. +- **Never dispatch `impl-agent` or `revise-agent` for an item whose body carries the `SESSION REQUIRED` marker** — those tickets can't be handed to an agent (today: they touch `CLAUDE.md` / `.claude/**`, which the harness won't let a subagent edit). **Announce it instead, and tell the human to run `/implement` in a separate session — never this one** (see Session-required items). `review-agent` and refresh-mode dispatches are never gated by it. - Every dispatch runs in the background (`run_in_background: true`). Worktree isolation, model, tool scope, and **permission mode (`dontAsk` — auto-denies anything not allow-listed)** all come from the subagent definition in `.claude/agents/` — you do not set them at the call site. (Since CC v2.1.186 a background subagent's prompts surface to you unless it runs `dontAsk` **and** this session is in default mode — see Model & permission mode.) - **Respect the draining flag:** while draining (see Stop controls), dispatch nothing new and schedule no wakeup; only report state and relay completions. @@ -55,7 +55,7 @@ gh pr list --repo SGAOperations/aplio --search "no:assignee" --limit 100 --json gh pr list --repo SGAOperations/aplio --assignee "@me" --json number,title,labels --jq '[.[] | select((.labels | map(.name)) as $l | ($l | any(. == "ready for review" or . == "reviewing" or . == "needs revision" or . == "revising" or . == "approved" or . == "refresh branch" or . == "refreshing" or . == "needs human")) and ($l | index("claude") | not)) | {number, title}]' ``` -Then, in order: **(1)** reconcile merged PRs (below), **(2)** handle human gates, **(3)** **unless draining,** dispatch for every actionable trigger item (all Agent calls in one message), **(4)** report the unowned and ungated-PR sweeps if their sets changed, **(5)** schedule the next wakeup (**skip while draining**). +Then, in order: **(1)** reconcile merged PRs (below), **(2)** handle human gates, **(3)** **announce every session-required item** — nothing will ever dispatch for these, so surfacing them is the only thing that moves them (below), **(4)** **unless draining,** dispatch for every remaining actionable trigger item (all Agent calls in one message), **(5)** report the unowned and ungated-PR sweeps if their sets changed, **(6)** schedule the next wakeup (**skip while draining**). **Merged-PR reconciliation (each tick):** the `approved` query above is open-only, so a merged PR silently drops out of it — never trust in-session memory for "awaiting merge." Diff the set of PRs you have **announced as approved** against the live `approved` result; for each announced PR no longer present, confirm and announce it **once**: @@ -146,17 +146,19 @@ The gate applies **no special label** for a session-required plan — the marker ### Session-required items -An item whose body carries the `SESSION REQUIRED` marker keeps its trigger label but is **never** dispatched (`.claude/docs/PIPELINE.md` → "Session-required tickets"). Announce it **once per session per item** — the same tracked-announcement pattern as `approved` PRs — and take no other action. Derive the session name from the **issue** title: `#: <2–5 lowercase words>`. +**Surfacing these is your job, and nothing else will do it.** An item whose body carries the `SESSION REQUIRED` marker keeps its trigger label but is **never** dispatched (`.claude/docs/PIPELINE.md` → "Session-required tickets"). No agent will ever pick it up, so if you don't tell the human it sits at `plan approved` / `needs revision` indefinitely — silently, because a trigger label normally means something is already moving. Announce it **once per session per item** (the same tracked-announcement pattern as `approved` PRs), then take no other action. + +**Say "separate session", and mean it.** The human runs `/implement` in a **new session — not this one.** This cockpit has no `Edit` in its tool scope and runs on haiku, so it cannot do the work; and it has to stay free to keep ticking, since a long implementation here would stall every other item in the pipeline. Hand over the launch command with the name pre-filled, derived from the **issue** title: `#: <2–5 lowercase words>`. - **Issue at `plan approved` + marker:** - > 🧰 #503 needs its own session — its plan is marked `SESSION REQUIRED` (touches `CLAUDE.md` / `.claude/**`, which a dispatched agent can't edit). Run it yourself: + > 🧰 #503 is marked **`SESSION REQUIRED`** — it touches `CLAUDE.md` / `.claude/**`, which a dispatched agent can't edit, so I won't be implementing this one. **Open a separate session and run it there** (not here — I need to keep ticking): > `claude -n "#503: operator config route"` then `/implement 503` - > I'll pick it back up at review. + > Nothing moves until you do. I'll pick it back up automatically at review. - **PR at `needs revision` + marker** (announce **after** the cycle-cap check, which still runs and can still escalate to `needs human`): - > 🧰 PR #512 needs revision and is marked `SESSION REQUIRED` — `claude -n "#503: operator config route"` then `/implement 512` in your own session. I'll review again once it's back at `ready for review`. (The session name carries the **issue** number; the command takes the PR number.) + > 🧰 PR #512 needs revision and is marked **`SESSION REQUIRED`** — I can't dispatch `revise-agent` for it. **In a separate session** (not here): `claude -n "#503: operator config route"` then `/implement 512`. Nothing moves until you do; I'll review again once it's back at `ready for review`. (The session name carries the **issue** number; the command takes the PR number.) ### Approved PRs