From 3fc1cb0eb870d1494af4783e48a026d3a9dcf397 Mon Sep 17 00:00:00 2001 From: Roberto Cano <3525807+robercano@users.noreply.github.com> Date: Wed, 1 Jul 2026 20:26:03 +0200 Subject: [PATCH] chore(agents,loop): route all gh through the bot identity + add /setup-orchestrator - Agents pinned to bot via bot-gh.sh (orchestrator broadened to ALL gh; implementer/reviewer/test-runner get GitHub-identity rules). - Loop scripts (notify-poll, merge-ready, pr-feedback) shim gh -> bot-gh.sh; merge runs as the bot, gate is still the owner's APPROVED review. - bot-gh.sh header updated to the broadened "all gh via bot" policy. - New /setup-orchestrator command: interactive full onboarding (adapter, gitignore, module:* labels, bot verify, CI gates, arm /pr-loop, offer /harden) with a non-blocking warn-then-ask on re-runs. Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_011HosUeuSvhetARboEfDW6K --- .claude/agents/implementer.md | 3 + .claude/agents/orchestrator.md | 3 + .claude/agents/reviewer.md | 3 + .claude/agents/test-runner.md | 3 + .claude/commands/setup-orchestrator.md | 103 +++++++++++++++++++++++++ .claude/scripts/bot-gh.sh | 5 +- .claude/scripts/merge-ready.sh | 10 ++- .claude/scripts/notify-poll.sh | 3 + .claude/scripts/pr-feedback.sh | 3 + 9 files changed, 132 insertions(+), 4 deletions(-) create mode 100644 .claude/commands/setup-orchestrator.md diff --git a/.claude/agents/implementer.md b/.claude/agents/implementer.md index d6ad96f..c1b3d4c 100644 --- a/.claude/agents/implementer.md +++ b/.claude/agents/implementer.md @@ -8,6 +8,9 @@ isolation: worktree You own ONE sub-task end-to-end, on your own branch, in your own worktree. +## GitHub identity (hard rule) +Never call bare `gh`. EVERY `gh` invocation (PR create/update, comments, `gh api`, any query) MUST go through `.claude/scripts/bot-gh.sh` so it runs as the bot. Only `git` commits/pushes use the owner's auth. If `GH_BOT_TOKEN` is missing, stop and report it — do not fall back to owner `gh`. + ## Read first - `.claude/gates.json` — for the exact gate commands (`build`, `lint`, `typecheck`, `test_affected`, `coverage`) and your module boundary. - `CLAUDE.md` — conventions, style, definition of done. diff --git a/.claude/agents/orchestrator.md b/.claude/agents/orchestrator.md index 3cdd27d..7831e35 100644 --- a/.claude/agents/orchestrator.md +++ b/.claude/agents/orchestrator.md @@ -13,6 +13,9 @@ Before anything else, read these and treat them as ground truth: - `CLAUDE.md` — project context and conventions. If `.claude/gates.json` has empty `gates`, STOP and tell the user the project hasn't been adapted yet (point them at `docs/GETTING_STARTED.md`). +## GitHub identity (hard rule) +EVERY `gh` invocation — by you and by every agent you spawn — MUST go through the bot account via `.claude/scripts/bot-gh.sh`; never call bare `gh`. This covers reads and writes alike: issue creation, issue/PR comments, PR creation, PR merging, and all queries (`gh pr list`, `gh issue view`, `gh api`, …). Only `git` commits and pushes stay on the owner's auth, so the owner can formally review and approve (GitHub blocks a PR's author from approving it). If `GH_BOT_TOKEN` is missing, STOP and point the user at the setup notes in `.claude/scripts/bot-gh.sh` rather than falling back to owner `gh`. When you delegate, tell each worker this same rule. + ## Your loop 1. **Scope.** Decompose the task into sub-tasks that are *independent* and *non-overlapping at the file level*. Use the `modules` map in `gates.json` to assign each sub-task to exactly one module/path. If two sub-tasks would touch the same files, either merge them into one sub-task or sequence them (declare the dependency). Scale effort to complexity: a trivial task gets ONE worker and no parallelism — do not fan out for its own sake. 2. **Present the plan and WAIT.** Output the plan: each sub-task's title, target module/path, owner boundary, dependencies, and which reviewers will gate it. Enter plan mode and wait for human approval before any code is written. This is the planning checkpoint. diff --git a/.claude/agents/reviewer.md b/.claude/agents/reviewer.md index 0187e4f..0804e14 100644 --- a/.claude/agents/reviewer.md +++ b/.claude/agents/reviewer.md @@ -7,6 +7,9 @@ model: opus You are an ADVERSARIAL reviewer. Your default posture is skepticism: try to find the reason this change is wrong, not reasons it's fine. A change you cannot refute is one you approve. +## GitHub identity (hard rule) +If you touch GitHub at all (e.g. `gh pr diff`, `gh pr view`, `gh api`), route it through `.claude/scripts/bot-gh.sh` — never bare `gh`. You remain read-only; this only changes the identity the query runs under. + ## Read first - `.claude/gates.json` — review lenses and any project review skills. - `CLAUDE.md` — the project's definition of done and conventions. diff --git a/.claude/agents/test-runner.md b/.claude/agents/test-runner.md index b85a435..576154e 100644 --- a/.claude/agents/test-runner.md +++ b/.claude/agents/test-runner.md @@ -7,6 +7,9 @@ model: haiku You run the project's gates and report results. You do not fix code — you report what's red so others can. +## GitHub identity (hard rule) +If any gate or check needs `gh` (e.g. fetching CI status via `gh api`/`gh run`), call it through `.claude/scripts/bot-gh.sh`, never bare `gh`, so it runs as the bot. + ## What to run Read `.claude/gates.json` and run the requested gates (or all configured ones) using `.claude/scripts/gate.sh ` when available, else the raw command from the file. Typical order: `install` (if needed) → `build` → `lint` → `typecheck` → `test` (or `test_affected`) → `coverage` → `e2e` → `security`. diff --git a/.claude/commands/setup-orchestrator.md b/.claude/commands/setup-orchestrator.md new file mode 100644 index 0000000..20b5290 --- /dev/null +++ b/.claude/commands/setup-orchestrator.md @@ -0,0 +1,103 @@ +--- +description: Interactive full onboarding — interview the user, then write .claude/gates.json + CLAUDE.md, fix gitignore, create module:* labels, verify the bot, confirm CI gates, and offer to arm the PR loop and hardening. Brings a fresh project to a working autonomous state. +--- + +You are running **first-time setup** for this orchestrator template in the user's project. Goal: take a fresh +clone from placeholder to a fully working autonomous state — the same configuration a mature project here has: +a filled adapter, module labels, a working bot identity, server-side gates, the PR loop armed, and (optionally) +hardened hands-off mode. You *interview* the user, then materialize the files and GitHub state the loop depends on. + +`docs/GETTING_STARTED.md`, `docs/USAGE.md`, and `docs/HARDENING.md` are the sources of truth — read them first +and defer to them on any detail. + +Be conversational but efficient. Use the `AskUserQuestion` tool for discrete choices; ask for free-text +(names, paths, shell commands) in plain prose. **Never invent values** — if you don't know a command or path, +ask. **Propose the final files and get an explicit "yes" before writing.** All `gh` runs through +`bash .claude/scripts/bot-gh.sh`, never bare `gh`. + +Do these in order. Stop and report if a step genuinely can't proceed. + +## 1. Preconditions & orientation +- Read `docs/GETTING_STARTED.md`, `docs/USAGE.md`, `docs/HARDENING.md`, the current `.claude/gates.json`, and `CLAUDE.md`. +- **Redundant-setup check (warn, then ask — don't hard-abort).** If `gates.json.gates` already has non-empty + commands, the project looks already configured. **Warn clearly**: show the current `project`/`modules`/`gates`, + and say that continuing will re-interview and, on your confirmation, overwrite the adapter files and reconcile + labels. Then **ask the user whether to continue or stop** (use `AskUserQuestion`). If they choose stop, end the + command cleanly with no changes. If they continue, proceed with the flow. (You still confirm before each file + write in later steps, so a re-run can't clobber silently.) +- Resolve the repo: `bash .claude/scripts/bot-gh.sh repo view --json nameWithOwner -q .nameWithOwner`. + +## 2. Explain the model up front (so answers are informed) +Briefly tell the user how the loop decides what to build: +- The loop only builds issues **labelled `module:`**, one at a time, when no PRs are open — an explicit opt-in queue. +- Each `module` maps to exactly one filesystem `path`, the **hard boundary** a worker may edit within. So the + module list you define here is both the isolation boundary and the set of labels the loop understands. Include + any non-code area you want automatable (e.g. `docs`, `.claude`, `examples`). +- Nothing merges until the **owner approves the bot's PR** on GitHub. + +## 3. Interview +Collect, confirming back as you go: +1. **Project basics** — `project.name`, `language`, `packageManager`. +2. **Modules** (the important one) — for each ownable area: `name` (label-safe: lowercase/kebab), `path` + (repo-relative, **non-overlapping / non-nested** with siblings), one-line `description`, optional `owner`. + Push back on overlapping or nested paths — the isolation guarantee needs disjoint paths. Offer to include + `docs`/infra modules if relevant. +3. **Gates** — exact shell commands (run from repo root) for `install`, `build`, `lint`, `typecheck`, `test`, + `test_affected`, `coverage`, `e2e`, `security`. Empty = "skip" (fine, and the right default when a gate + doesn't exist yet). Warn that a gate pointed at a command that can't pass will block the Stop hook. Ask + `coverage_threshold` (default 80). If unsure on `test_affected`, default it to the full `test` command. +4. **Review** — `review.lenses` (default `["correctness","tests","security","performance"]`) and + `review.consensus` (`all`, or an integer). +5. **Budget/routing** — `orchestrator_model`/`worker_model`/`explorer_model`/`reviewer_model` + (defaults opus/sonnet/haiku/opus) and `max_parallel_workers` (default 3; advise 2–4). +6. **Merge** — `merge.policy` (`pr-per-agent` | `orchestrated-sequential-merge`) and `merge.baseBranch` + (default the repo's default branch). + +## 4. Write the adapter (after confirmation) +- Produce the complete `.claude/gates.json`, show it, and on approval write it. It MUST be valid JSON — + validate with `node -e "require('./.claude/gates.json')"`; fix and re-validate if it throws. +- Fill `CLAUDE.md` from its template sections (What this project is / Stack & layout mirroring the module map / + Conventions / Merge policy mirroring `gates.json` / Don'ts). Keep it lean — project-WIDE context only. +- Do **not** touch `.claude/settings.json` or any generic agent/script — only the adapter. + +## 5. Gitignore hygiene +Ensure these are gitignored (append if missing, don't duplicate): `.env` (holds `GH_BOT_TOKEN`), +`.claude/settings.local.json` (per-machine hardening/bypass — must never be inherited by a clone), and +`.claude/state/` (the notify-poll cursor). Verify with `git check-ignore `. + +## 6. Create the module labels +For every module `name`: `bash .claude/scripts/bot-gh.sh label create "module:" --description "" --force`. +Report created vs already-existing. Remind: **an issue is only loop-eligible once it carries a `module:*` label.** + +## 7. Verify the bot account +- Confirm `.env` has `GH_BOT_TOKEN` and the bot can see the repo: + `bash .claude/scripts/bot-gh.sh api user --jq .login` and a `repo view` on the resolved repo. +- If missing/no access, DON'T fail the whole setup — point at the one-time setup notes in + `.claude/scripts/bot-gh.sh` (create machine account → add as **write** collaborator → classic `repo`-scope + token → `.env`) and mark this step "action needed". + +## 8. Server-side gates (CI) +- Confirm `.github/workflows/gates.yml` exists and its jobs match the gate commands just configured (see #8). + If the commands differ, tell the user which to reconcile. Note that **branch protection / required checks** + (making CI a hard merge gate) is an owner action in repo Settings — flag it as a manual step. + +## 9. Arm the PR loop +- Explain `/pr-loop` (session-scoped cron; adaptive cadence). **Offer to run it now** (ask; don't auto-run). + If they decline, note they can run `/pr-loop` anytime — and must re-arm it each session. + +## 10. Hardening (offer LAST — order matters) +- Explain `/harden`: it writes `bypassPermissions` + a strict OS sandbox into `.claude/settings.local.json` for + hands-off autonomous runs (`docs/HARDENING.md` is the source of truth). **Offer to run `/harden` now** (ask). +- Sequencing the user must know: hardening only takes effect after a **restart**, once hardened the agent can no + longer edit `.claude/settings*.json` (by design), and the restart drops the in-session PR-loop cron. So the + correct order is: finish setup → `/harden` → restart Claude Code → **re-run `/pr-loop`** in the hardened + session. Do not harden before the rest of setup is done. + +## 11. Hand off +Summarize what changed (files written, gitignore entries, labels created, bot status, CI status, loop armed?, +hardened?). Restate the two control points in one line each: **label an issue `module:*` to queue it; approve +the bot's PR to ship it.** Finish with an ordered checklist of everything only the human can complete, e.g.: +- add `GH_BOT_TOKEN` to `.env` / add the bot as a write collaborator (if step 7 flagged it), +- set branch protection / required status checks (if wanted), +- OS-level isolation from `docs/HARDENING.md` Step 2 (sudo / VM / WSL interop) if hardening, +- restart, then re-run `/pr-loop`. diff --git a/.claude/scripts/bot-gh.sh b/.claude/scripts/bot-gh.sh index 200fab9..8f80080 100755 --- a/.claude/scripts/bot-gh.sh +++ b/.claude/scripts/bot-gh.sh @@ -14,7 +14,10 @@ # reliably target repos owned by ANOTHER personal account.) # 4. Put it in the project's .env (gitignored) as GH_BOT_TOKEN=... # -# Only PR creation needs the bot; commits and pushes stay on the owner's auth. +# Policy: ALL agent `gh` interaction (issue/PR creation, comments, merging, and +# even reads/queries) goes through this wrapper so it runs as the bot. Only `git` +# commits and pushes stay on the owner's auth — that keeps the owner eligible to +# formally approve bot-authored PRs (GitHub blocks a PR author from approving it). # # Usage: .claude/scripts/bot-gh.sh pr create --title "..." --body "..." set -euo pipefail diff --git a/.claude/scripts/merge-ready.sh b/.claude/scripts/merge-ready.sh index 0d74b2e..752f5df 100644 --- a/.claude/scripts/merge-ready.sh +++ b/.claude/scripts/merge-ready.sh @@ -15,9 +15,11 @@ # - every CI check is green (none failing, none still pending) # Anything else is SKIPPED with a reason. Output is JSON lines a cron summarizes. # -# Auth: uses ambient `gh` auth (the owner's `gh auth login`), same as -# notify-poll.sh — merging is an owner action. Only PR *creation* uses the bot -# (bot-gh.sh). Repo is derived from the git remote; override with $1 (owner/repo). +# Auth: ALL `gh` calls (listing, viewing, and the merge itself) run as the bot via +# bot-gh.sh — the bot is a write collaborator, so it can merge. The merge GATE is +# still the human OWNER's APPROVED review (detected below); running the merge as the +# bot does not change who authorized it. Repo is derived from the git remote; +# override with $1 (owner/repo). # The approver defaults to the repo owner; override with $MERGE_APPROVER. # Pre-approve `bash .claude/scripts/merge-ready.sh` in .claude/settings.json. @@ -25,6 +27,8 @@ set -euo pipefail export PATH="$HOME/.local/bin:$PATH" root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +# Route EVERY gh call (list/view/merge) through the bot identity (see bot-gh.sh). +gh() { bash "$root/.claude/scripts/bot-gh.sh" "$@"; } repo="${1:-$(gh repo view --json nameWithOwner -q .nameWithOwner)}" owner="${MERGE_APPROVER:-${repo%%/*}}" # the approver whose APPROVED review authorizes a merge gates="$root/.claude/gates.json" diff --git a/.claude/scripts/notify-poll.sh b/.claude/scripts/notify-poll.sh index abeda1d..133afc8 100755 --- a/.claude/scripts/notify-poll.sh +++ b/.claude/scripts/notify-poll.sh @@ -12,6 +12,9 @@ set -euo pipefail root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +# Route EVERY gh call through the bot identity (see bot-gh.sh). Defined before the +# first gh use below so the repo-derivation call already runs as the bot. +gh() { bash "$root/.claude/scripts/bot-gh.sh" "$@"; } repo="${1:-$(gh repo view --json nameWithOwner -q .nameWithOwner)}" owner="${MERGE_APPROVER:-${repo%%/*}}" # the human whose APPROVED review gates a merge state_dir="$root/.claude/state" # add .claude/state/ to .gitignore diff --git a/.claude/scripts/pr-feedback.sh b/.claude/scripts/pr-feedback.sh index 36ead53..67acac9 100644 --- a/.claude/scripts/pr-feedback.sh +++ b/.claude/scripts/pr-feedback.sh @@ -15,6 +15,9 @@ # Invoke as `bash .claude/scripts/pr-feedback.sh` (pre-approve that exact command). set -euo pipefail +root="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +# Route EVERY gh call through the bot identity (see bot-gh.sh). +gh() { bash "$root/.claude/scripts/bot-gh.sh" "$@"; } repo="${1:-$(gh repo view --json nameWithOwner -q .nameWithOwner)}" bot="${BOT_LOGIN:-robercano-ghbot}" marker=""