Skip to content

Implement .claude Config Tickets From an Operator Session #503

Description

@b-at-neu

Problem

Tickets that touch .claude/ cannot be implemented by impl-agent or fixed by revise-agent. The restriction is enforced by the harness, above project config — .claude/settings.json already grants Edit(**) and Write(**) with no .claude/ deny rule, so it cannot be granted away in this repo.

Today these silently become manual work. #451 was implemented by hand — both commits on PR #467 are authored by b-at-neu, not an agent. Also affected and currently idle or awkward: #397, #395, #396, #398, #402.

The constraint is also undocumented. .claude/docs/PIPELINE.md covers the permission model in detail — allowlist, deny surface, rationale, per-stage tool scopes — and never mentions it. The only related rule is that rebase conflicts in CLAUDE.md or .claude/docs/ must escalate rather than auto-resolve.

Key distinction

Dispatching impl-agent creates a subagent, and the harness restriction applies to that context.

Instructing an operator's main session to follow impl-agent.md spawns nothing — the session reads the file as content and acts on it with main-session permissions. No subagent, no restriction. This already works when asked directly in a prompt.

So the fix is not to grant permissions or re-implement the workflow. It is to route stages 2 and 4 to the operator's session for these tickets, reusing the existing agent instructions as-is.

Design

1. The plan declares it. plan-agent already reads the codebase to write its plan. When the work touches CLAUDE.md or .claude/**, it emits an explicit machine-readable declaration — matching the existing QUESTIONS FOR HUMAN: sentinel pattern, rather than making the cockpit grep prose for paths.

2. The cockpit does not dispatch. On plan approved carrying that declaration, it skips impl-agent and tells the operator to open a session, naming the command and the issue. Same at needs revision — it skips revise-agent.

3. The operator runs a command. A new skill that is a thin wrapper: it instructs the session to read and follow .claude/agents/impl-agent.md (or revise-agent.md), overriding only the rules that are specific to being a subagent:

  • permissionMode: dontAsk and the "auto-denied silently" model — a main session prompts normally
  • "STOP and emit BLOCKED:" — pointless when a human is present; just ask
  • "Never spawn subagents"
  • The allowlist discipline about avoiding cat / grep / sed / find — that exists because those are not allow-listed for the subagent

Everything else applies unchanged: branch naming, .temp/commit-msg.txt commit format, base dev, assignee, the CI gate, rebase-onto-dev, and the Pre-PR self-check in ENGINEERING.md.

No extraction or refactoring of the agent files. They remain the single definition of the workflow, and the working pipeline path is untouched.

4. The pipeline resumes. review-agent is read-only and operates through gh, so it reviews these PRs unchanged. Only stages 2 and 4 move.

The command must work in a worktree

Two reasons, and the second only appeared once the design settled on reusing the agent files:

  1. Editing .claude/ in the session that is using it mutates your own live configuration mid-task — the hazard behind the existing rule that agent definitions are never hot-patched. A worktree keeps the edits inert until merge, so the running session keeps the committed behaviour throughout.
  2. Self-reference. The session reads .claude/agents/impl-agent.md to know how to work, and the ticket may be editing that very file. In a worktree it reads the committed version while editing the worktree copy, so its own instructions cannot shift underneath it.

Acceptance criteria

  • plan-agent emits an explicit declaration when the plan touches CLAUDE.md or .claude/**
  • The cockpit does not dispatch impl-agent or revise-agent for those tickets; it prompts the operator with the command and the issue number
  • A new skill implements the operator path in a worktree, producing a normal PR against dev with house branch, commit and assignee conventions
  • The skill instructs the session to follow the existing agent file, overriding only the subagent-specific rules — the agent files themselves are unchanged
  • The override list is explicit and short, so it is obvious when it drifts
  • review-agent handles these PRs unchanged
  • Agent definitions still route through a PR — no in-place editing from any session
  • The harness constraint is documented in PIPELINE.md, including that it is not settable via settings.json
  • The label state machine documents the skip path for both stage 2 and stage 4
  • The affected open tickets (Document the Permissions Model #395, Document the Official User Workflows #396, Add a Full Review Claude Command #397, Design the Position Edit Policy #398, Document Logging Standards in ENGINEERING.md #402) are re-planned or labelled

Note

Detection is by plan declaration rather than a label, so it cannot be forgotten at filing time — but a ticket's route is then only known after planning, which the cockpit's status output should reflect.

Worth a single test either way: claude --agent impl-agent, documented in PIPELINE.md as the manual escape hatch, may or may not carry the same restriction, since the frontmatter forces dontAsk and worktree isolation. Not worth designing around, but cheap to confirm.


Implementation Plan

SESSION REQUIRED: touches CLAUDE.md / .claude/** — a dispatched agent can't edit those

The route check this marker feeds does not exist yet — #503 bootstraps it and is therefore hand-routed (see Risks).

Superseded during implementation: the plan below specifies an operator route label as the durable signal. On review that was replaced by this body marker, carried identically in the issue plan and the PR description, since the PR-side label had exactly one consumer and the cockpit's trigger queries already return body. The marker also names the routing rather than the cause, so future non-.claude categories reuse it. The label has been deleted. As-built design: PR #520 and .claude/docs/PIPELINE.md → "Session-required tickets".

Overview

Add a third route through the pipeline for config tickets (anything touching CLAUDE.md or .claude/**), where the harness blocks a dispatched agent's Edit. plan-agent declares the route with a sentinel line; the cockpit converts that sentinel into a sticky operator route marker label at the plan gate and then never dispatches stages 2/4 for a labelled item — it announces the command instead. A new /implement skill runs those two stages in the operator's own session, in a dedicated worktree, by reading and following the existing, unmodified impl-agent.md / revise-agent.md plus a short list of subagent-only overrides. Stage 3 is untouched.

The trigger label stays on the item. plan approved / needs revision remain in place and the skill performs the same label swaps the agent file already specifies — so no new trigger semantics, no new in-flight labels, and recovery is still "re-apply the trigger". The cockpit gains one rule instead: operator route suppresses dispatch. That is a deliberate deviation from "a trigger label always means dispatch", and it is documented as such.

Operator sessions are named. Every session that runs /implement is launched with claude -n "#XXX: <2–5 word name>" (-n, --name sets the display name shown in the prompt box, the /resume picker, and the terminal title). The cockpit's announcement hands over the exact command with the name pre-filled from the issue title, and the skill re-states the expected name in its first line. With several long-lived sessions open, this is the only thing that makes "which window is #395?" answerable at a glance.

Changes

  • .claude/skills/implement/SKILL.mdnew. The operator path: resolve input → mode, name the session, create the worktree, follow the agent file, apply the overrides.
  • .claude/agents/plan-agent.md — add the route check and the sentinel to the "Write the plan" step.
  • .claude/skills/pipeline/SKILL.md — route check at the plan gate; operator route suppresses stage 2/4 dispatch; announcement copy (including the named-launch command); status reports the route; sweep/gate queries include it.
  • .claude/docs/PIPELINE.md — new "Config tickets" section (harness constraint + why settings.json can't grant it + the route + the skip path for both stages + the session-naming convention); operator route row in the issue and PR label tables; sentinel in the "Implementation plan" format; a Stages-table note; a recovery-runbook row.
  • .claude/skills/worktree-clean/SKILL.md — one line: impl-* worktrees are in scope alongside agent-*.
  • CLAUDE.md — one line under "Worktrees & local dev" pointing at the new PIPELINE.md section.

Repo state (no files): create the operator route label; pre-label #395, #396, #397.

Implementation

  • Create the label: gh label create "operator route" --repo SGAOperations/aplio --description "Stages 2/4 run in an operator session — touches CLAUDE.md or .claude/**" --color "5319E7".
  • plan-agent.md — in "Write the plan", add: after ## Implementation Plan, if any file in ## Changes is CLAUDE.md or under .claude/, the first line of the plan body is exactly ROUTE: operator session, followed by one sentence naming why. Emit it in revision mode too (it is regenerated with the plan, so a route can change). Never emit it otherwise.
  • PIPELINE.md → "Implementation plan" — document the sentinel as an optional first line of the plan block, before ## Overview, with its exact text and the condition.
  • PIPELINE.md → new "Config tickets (CLAUDE.md / .claude/**)" section, placed after "Permission rationale": the harness denies Edit/Write under .claude/ for dispatched subagents regardless of permissions.allow; it is not settable via settings.json — this repo already grants Edit(**)/Write(**) with no .claude/ deny rule and the denial persists; an operator's main session is unaffected because reading an agent file and acting on it spawns no subagent. Then the route: sentinel → operator route/implement for stages 2 and 4, stage 3 unchanged, refresh branch still dispatched to revise-agent (a rebase + force-push edits nothing; a conflict inside .claude/docs/ is already on the never-touch list and escalates). Include the session-naming convention (claude -n "#XXX: <2–5 word name>") and the worktree path (.claude/worktrees/impl-<n>). State the invariant deviation explicitly: an operator route item keeps its trigger label but is never dispatched.
  • PIPELINE.md label tables — add operator route to both the issue and PR tables: set by Cockpit (issue, at the plan gate) / /implement (PR, at creation), type marker, meaning "stages 2/4 run in an operator session; the cockpit never dispatches for it". Add a runbook row: "An issue sits at plan approved and nothing dispatches" → it carries operator route → run /implement <n> in your own named session. Add a one-line note under the Stages table that stages 2 and 4 have an operator variant.
  • pipeline/SKILL.md — plan gate. Before swapping plan reviewplan approved (both the interactive Approve branch and the auto plan path), read the route: gh issue view <n> --repo SGAOperations/aplio --json body --jq '.body | contains("ROUTE: operator session")'. If true, include operator route in the same edit: --remove-label "plan review" --add-label "plan approved,operator route".
  • pipeline/SKILL.md — safety rails + Dispatching. Add an absolute rule: never dispatch impl-agent or revise-agent for an item labelled operator route. In the stage→trigger table, mark the plan approved and needs revision rows "unless operator route → announce, don't dispatch". ready for reviewreview-agent and refresh branchrevise-agent (refresh mode) stay unconditional. Announce once per session per item (the same tracked-announcement pattern as approved PRs) using the copy in UX states; the cycle-cap check is unchanged and still runs before any announcement of a needs revision PR.
  • pipeline/SKILL.md — visibility. Add --label "operator route" queries to the gate block for issues and PRs, include operator route in the two unowned-sweep --jq filters, and make status list these under items waiting on the human, tagged operator route, with the exact commands to run (named launch + /implement <n>).
  • .claude/skills/implement/SKILL.md — new skill. Frontmatter: name: implement; disable-model-invocation: true; allowed-tools: Read, Edit, Write, Glob, Grep, Bash, AskUserQuestion; description ending Usage: /implement <issue-or-pr-number>. Body:
    • Name the session first. Derive #<n>: <2–5 word name> from the issue title (lowercase, no punctuation — e.g. #503: operator config route) and state it as the first output line: if the session wasn't launched with claude -n "<that string>", give the operator the exact string so they can set it (relaunch with -n, or rename in-client if their client supports it). The skill can neither read nor set its own display name, so this is an instruction to the human — do not block on it.
    • Resolve the mode from state (/release-style, so one command covers both stages): gh pr view <n> --json labels,headRefName,baseRefName — if it resolves to a PR labelled needs revisionrevise mode; else gh issue view <n> --json labels,assignees,title — labelled plan approvedimpl mode. Anything else → stop and report the labels, change nothing. If the item carries the trigger label but not operator route, warn that the cockpit may dispatch an agent for it too and confirm via AskUserQuestion before proceeding — the generic command name invites manual use, and a double dispatch is the hazard.
    • Record the main checkout (git rev-parse --show-toplevel) before anything else. Every instruction file (impl-agent.md / revise-agent.md, ENGINEERING.md, PIPELINE.md, CLAUDE.md) is read by absolute path from there; every edit goes only to paths inside the worktree. This is what makes a ticket that edits impl-agent.md safe.
    • Worktree. impl mode: git worktree add -b <n>-ticket-name-in-kebab-case .claude/worktrees/impl-<n> origin/dev after git fetch origin (branching straight off dev replaces the agent's checkout-main-then-rebase). Revise mode: add the worktree detached at origin/<headRefName> and rebase onto origin/<baseRefName>, per revise-agent.md step 2. Then cd into it, npm ci, npm run prisma:generate. Never touch another worktree; never --force.
    • Follow the agent file for everything else — checklist, commit format via .temp/commit-msg.txt, the three CI checks, push by refspec, PR body format, base dev, --assignee <issue assignee>, label swaps, thread resolution, revision note.
    • gh pr create adds --label "operator route" so the marker travels to the PR (same precedent as copying the assignee) — every later cockpit decision is then a pure label check.
    • Overrides (the whole list — anything not here applies unchanged): (1) permissionMode: dontAsk / "auto-denied silently" — this session prompts normally; (2) BLOCKED: → ask the operator and wait; (3) "never spawn subagents" — not applicable; (4) the shell-allowlist discipline (cat/grep/sed/find, bare commands, cd, quoted cwd-relative paths, npm run over npx) — that exists for the subagent allowlist, so use whatever is clearest, though npm run remains preferred for the toolchain; (5) "you are already in a worktree, never run git worktree" — inverted: you create it, and you cd into it; (6) instruction files are read from the recorded main checkout, not the cwd.
    • Handoff — impl: issue pr opened, PR ready for review (the cockpit picks up review on its next tick). Revise: PR ready for review. Then tell the operator the PR URL and that the worktree can be reclaimed with /worktree-clean after merge.
  • worktree-clean/SKILL.md — note that impl-* worktrees (operator-run config tickets) are in scope alongside agent-*.
  • CLAUDE.md — one line under "Worktrees & local dev": tickets touching CLAUDE.md / .claude/** are implemented from a named operator session via /implement; see PIPELINE.md → "Config tickets".
  • Backlog — label Document the Permissions Model #395, Document the Official User Workflows #396, Add a Full Review Claude Command #397 operator route (each unambiguously produces only .claude/docs/** or .claude/skills/**), so the cockpit routes them correctly even before their plans exist. Design the Position Edit Policy #398 is a design epic with no files — leave it; its sub-issues get routed by their own plans. Document Logging Standards in ENGINEERING.md #402 is closed; nothing to do.

UX states

Operator-facing copy is the whole interface here.

  • Cockpit, plan approved + operator route (announce once, in place of dispatch):

    🧰 Implement .claude Config Tickets From an Operator Session #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.

  • Cockpit, needs revision + operator route (after the cycle-cap check):

    🧰 PR #510 Fix tsc Broken On Dev: Markdown Field Tooltip #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.)

  • Cockpit status — a row per routed item under the human-gated group: #503 — plan approved · operator route → claude -n "#503: operator config route" · /implement 503.
  • Skill, wrong state (no matching trigger label): #503 is not awaiting an operator (labels: ready, claude). Nothing was changed. — and stop.
  • Skill, missing operator routeAskUserQuestion: "Preview Builds Fail Opaquely When The Neon Branch Limit Is Exceeded #412 has no operator route label, so the cockpit may dispatch impl-agent for it too. Proceed anyway / Cancel."
  • Skill, start — two lines: the session name to use (Name this session: #503: operator config route), then the resolved mode, worktree path and branch, so the operator can see it picked the right stage before npm ci runs.
  • Skill, blocked — ask the operator directly (AskUserQuestion) and wait; never a BLOCKED: sentinel, never a guess.
  • Skill, done — the PR URL, the labels applied, and that /worktree-clean reclaims .claude/worktrees/impl-<n> after merge.

Testing

  • gh label list --repo SGAOperations/aplio --search "operator route" shows the new label.
  • In the cockpit: work on #395 (interactive gate). Plan agent's plan starts with ROUTE: operator session; a source-only ticket's plan has no such line (negative control).
  • Approve Document the Permissions Model #395's plan in the cockpit → the issue gets plan approved and operator route, no impl-agent is dispatched (TaskList shows none), and the announcement gives both claude -n "#395: …" and /implement 395.
  • Next cockpit tick re-reports nothing for Document the Permissions Model #395 (announced once) and status lists it as operator route with the commands.
  • Launch claude -n "#395: <short name>" in a separate session and run /implement 395: the display name appears in the prompt box / terminal title, the skill's first line re-states the expected name, the worktree appears at .claude/worktrees/impl-395 on branch 395-…, the main checkout's working tree stays clean throughout (git status there), and labels go plan approvedin progresspr opened.
  • The PR: base dev, title #395 …, body has Closes #395 + Testing plan, assignee = the issue's assignee, labels operator route + ready for review; commit subject matches #395 <lowercase imperative> with the Co-Authored-By trailer.
  • The cockpit dispatches review-agent for that PR unchanged and posts a real review.
  • Force the revise path: label the PR needs revision → the cockpit announces /implement <pr> and dispatches nothing. Run it; the PR goes revisingready for review and the addressed threads are resolved.
  • Negative controls: a normal source ticket at plan approved (no operator route) still auto-dispatches impl-agent; running /implement on it raises the double-dispatch confirmation first.
  • Self-reference check: a config ticket that edits .claude/agents/impl-agent.md — confirm the session's own behaviour doesn't change mid-run and the edit lands only in the worktree copy.
  • One-off harness probe: run claude --agent impl-agent and ask it to edit .claude/docs/PIPELINE.md; record whether the edit is denied, and note the observed result in the new PIPELINE.md section.
  • /worktree-clean lists and reclaims .claude/worktrees/impl-395 after the PR merges.

Risks / notes

  • Bootstrap. Implement .claude Config Tickets From an Operator Session #503 itself is a config ticket and /implement doesn't exist yet, so it must be implemented by hand from an operator session following impl-agent.md — the exact workflow being codified. Same for its own revision cycles.
  • A generic command name invites manual use. /implement reads like "run stage 2 on anything", so the skill doesn't hard-stop on a missing operator route — it confirms first. The hazard it guards is a double dispatch (cockpit and operator on the same item), which the confirmation names explicitly.
  • Session naming is advisory, not enforceable. -n is set at launch and a running session can neither read nor change its own display name, so the skill can only tell the operator the string. The cockpit announcement carrying the pre-filled command is what makes it actually happen.
  • Override drift is the real long-term risk: the agent files can change without the override list noticing. Mitigation is that the list is short, quotes each rule it overrides, and lives in exactly one place — the skill — with PIPELINE.md pointing at it rather than restating it.
  • The suppressed-trigger deviation weakens the "trigger label ⇒ something is dispatching" reading of the state machine. It is why operator route must appear in status, in the gate queries, and in the unowned sweep — an unowned config ticket is otherwise invisible in a way an unowned normal ticket isn't (no cockpit will ever nag about it).
  • npm ci in the worktree costs a few minutes and a few hundred MB even for a markdown-only change. Kept anyway: it makes the CI gate real for mixed tickets and removes an override. /worktree-clean is the reclamation path.
  • The harness constraint is unverified as to mechanism — we know dispatched-subagent Edit under .claude/ is denied and that settings.json cannot grant it. If a future Claude Code version lifts it, the whole route can be deleted; the sentinel and the label are the only things that would need unwinding.

Metadata

Metadata

Assignees

Labels

claudeWill be worked on by ClaudeenhancementNew feature or requestpr openedPull request has been opened

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions