From b1087f4556e0840262513159e8d06a711cafe4fd Mon Sep 17 00:00:00 2001 From: Roberto Cano <3525807+robercano@users.noreply.github.com> Date: Fri, 3 Jul 2026 19:44:24 +0200 Subject: [PATCH] feat(harness): /orchestrator:setup skill scaffolds non-distributable residue (#37) Co-Authored-By: Claude Opus 4.8 (1M context) Claude-Session: https://claude.ai/code/session_01WNHtY86twXHEKsLEucSJyY --- .claude/commands/setup-orchestrator.md | 103 +------------ .claude/skills/setup/SKILL.md | 145 ++++++++++++++++++ .claude/skills/setup/scaffold.sh | 119 ++++++++++++++ .claude/skills/setup/templates/CLAUDE.md | 37 +++++ .claude/skills/setup/templates/MANIFEST.md | 17 ++ .claude/skills/setup/templates/action.yml | 57 +++++++ .../skills/setup/templates/feature-fanout.js | 89 +++++++++++ .claude/skills/setup/templates/gates.json | 60 ++++++++ .claude/skills/setup/templates/gates.yml | 48 ++++++ 9 files changed, 577 insertions(+), 98 deletions(-) create mode 100644 .claude/skills/setup/SKILL.md create mode 100755 .claude/skills/setup/scaffold.sh create mode 100644 .claude/skills/setup/templates/CLAUDE.md create mode 100644 .claude/skills/setup/templates/MANIFEST.md create mode 100644 .claude/skills/setup/templates/action.yml create mode 100644 .claude/skills/setup/templates/feature-fanout.js create mode 100644 .claude/skills/setup/templates/gates.json create mode 100644 .claude/skills/setup/templates/gates.yml diff --git a/.claude/commands/setup-orchestrator.md b/.claude/commands/setup-orchestrator.md index 135e80e..6256dcb 100644 --- a/.claude/commands/setup-orchestrator.md +++ b/.claude/commands/setup-orchestrator.md @@ -2,102 +2,9 @@ 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. +This onboarding flow now lives in the `/orchestrator:setup` skill (`.claude/skills/setup/SKILL.md`), which +also scaffolds the files a plugin can't carry into the repo for you (`.claude/gates.json`, `CLAUDE.md`, +`.claude/workflows/feature-fanout.js`, and the CI gate workflow) via `.claude/skills/setup/scaffold.sh`. -`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_PLUGIN_ROOT:-.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_PLUGIN_ROOT:-.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_PLUGIN_ROOT:-.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_PLUGIN_ROOT:-.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`. +Run `/orchestrator:setup` — it does everything this command used to do, plus the scaffolding step. This +file is kept as a pointer so `/setup-orchestrator` still resolves for anyone used to the old name. diff --git a/.claude/skills/setup/SKILL.md b/.claude/skills/setup/SKILL.md new file mode 100644 index 0000000..0acddf0 --- /dev/null +++ b/.claude/skills/setup/SKILL.md @@ -0,0 +1,145 @@ +--- +name: setup +description: First-time onboarding for the orchestrator plugin in a NEW or unconfigured repo. Interviews the user, scaffolds the project adapter (.claude/gates.json), CLAUDE.md, the fan-out workflow, CI gate workflows, module:* labels, and .gitignore hygiene, verifies the bot identity, and offers to arm the PR loop and hardening. Use this whenever a repo has the orchestrator plugin installed but hasn't been set up yet, or when the user asks to "set up the orchestrator", "onboard this repo", or run `/orchestrator:setup`. +--- + +You are running **first-time setup** for the orchestrator plugin in the user's project. Goal: take a fresh +install from placeholder to a fully working autonomous state — 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. + +A Claude Code plugin can carry generic agents/scripts/hooks, but it CANNOT carry things that must live and be +version-controlled inside the consumer's own repo: the project-specific adapter, `CLAUDE.md`, the fan-out +workflow file, and GitHub Actions YAML. This skill's job is to scaffold exactly that non-distributable residue, +on top of the interview below. + +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_PLUGIN_ROOT:-.claude}/scripts/bot-gh.sh`, never bare `gh`. This skill never runs `gh` or +touches the network for the file-scaffolding part — that part is delegated to `scaffold.sh` (see step 4). + +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` (if present in this repo — a + downstream consumer may only have the plugin, not the template's docs; fall back to this skill's own + description of the model in step 2 if they're missing), 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 + skill 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_PLUGIN_ROOT:-.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. Scaffold the files (adapter, CLAUDE.md, workflow, CI, gitignore, state dir) +This is the part a plugin can't carry for you — it must land inside the consumer repo itself. Run it AFTER +the interview is confirmed, but note the script only writes files that don't already exist (user-owned) or +re-stamps a version-marked managed file — it never silently clobbers something you or a teammate hand-edited: + +``` +bash ${CLAUDE_PLUGIN_ROOT:-.claude}/skills/setup/scaffold.sh +``` + +Run it from the repo root (no argument needed there — it defaults to the current directory). It handles, all +idempotently: +- `.claude/gates.json` and `CLAUDE.md` — **user-owned from birth**. Created from templates only if absent. + Since these already exist as placeholders in a fresh checkout, immediately after scaffold.sh runs (or before, + your choice), you still need to **write the interview answers into `.claude/gates.json` and `CLAUDE.md` + yourself** (propose the complete files, get an explicit "yes", then write) — scaffold.sh only guarantees the + files exist to edit; it does not know the interview answers. +- `.claude/workflows/feature-fanout.js` — workflows aren't plugin-distributable, so it's scaffolded here, + stamped with an `@orchestrator-managed feature-fanout vN` marker comment. On a re-run, if the marker version + in the repo is older than the version scaffold.sh ships, it re-stamps (overwrites); if it's the same or + newer, it's left alone. This is the seam a future plugin-upgrade flow uses to push workflow fixes into + already-onboarded repos without touching hand-edited copies that opted out (by bumping their own marker). +- `.github/workflows/gates.yml` + `.github/actions/setup/action.yml` — the CI gate. Created if absent, left + untouched if present. +- `.gitignore` entries (append-if-missing, never duplicated): `.env`, `.env.*`, `!.env.example`, + `.claude/settings.local.json`, `.claude/state/`. +- `.claude/state/` directory (the notify-poll cursor lives here). + +Report the script's per-file summary (created / kept / restamped / up to date / appended) to the user. Then +write the interview answers into `.claude/gates.json` (validate with `node -e "require('./.claude/gates.json')"`) +and 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) — propose both files and get an explicit "yes" +before writing. Do **not** touch `.claude/settings.json` or any generic agent/script — only the adapter and +`CLAUDE.md` are project-specific here. + +## 5. Gitignore verification +`scaffold.sh` already appended the required entries in step 4. Spot-check with `git check-ignore ` for +`.env`, `.claude/settings.local.json`, and `.claude/state/` to confirm they actually resolve as ignored (e.g. a +repo-level override elsewhere in `.gitignore` could still un-ignore one). + +## 6. Create the module labels +For every module `name`: `bash ${CLAUDE_PLUGIN_ROOT:-.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_PLUGIN_ROOT:-.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 (scaffolded in step 4 if it wasn't already) and its jobs match + the gate commands just configured. If the commands differ from what's in `gates.json`, 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, if present). **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/kept/restamped by `scaffold.sh`, `gates.json`/`CLAUDE.md` filled, +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`. + +## Reference: file inventory this skill scaffolds +See `.claude/skills/setup/templates/MANIFEST.md` for the full template → destination map, and +`.claude/skills/setup/scaffold.sh` for the idempotent implementation (safe to re-run any time; it never +touches user-owned files that already exist, and only re-stamps the managed workflow when its version marker +is behind). diff --git a/.claude/skills/setup/scaffold.sh b/.claude/skills/setup/scaffold.sh new file mode 100755 index 0000000..24bd33d --- /dev/null +++ b/.claude/skills/setup/scaffold.sh @@ -0,0 +1,119 @@ +#!/usr/bin/env bash +# scaffold.sh — idempotent FILE materialization for `/orchestrator:setup` (issue #37). +# +# This script does ONLY the non-interactive, non-network file work: copy user-owned +# templates if absent, re-stamp the managed workflow file, install CI templates if +# absent, fix up .gitignore, and ensure .claude/state/ exists. The interview, gh +# calls (labels, bot verification), and PR-loop/harden offers stay in prose in +# SKILL.md — this script never shells out to `gh` and never touches the network. +# +# Usage: +# scaffold.sh [target-repo-root] +# +# target-repo-root defaults to the current working directory. Pass an explicit path +# (e.g. a $TMPDIR scratch dir) to dry-run against a throwaway target instead of a +# real checkout — this is how the idempotency demo in the setup skill is run. +# +# Exit code: 0 on success. Prints a per-file action summary (created / kept / +# restamped / up to date / appended). +set -euo pipefail + +# --- Resolve paths ---------------------------------------------------------------- +# Template dir is relative to THIS script, so it resolves correctly whether invoked +# from an installed plugin root ($CLAUDE_PLUGIN_ROOT/skills/setup/scaffold.sh) or a +# plain .claude/skills/setup/scaffold.sh checkout. +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +templates_dir="$script_dir/templates" + +target_root="${1:-$PWD}" +mkdir -p "$target_root" +target_root="$(cd "$target_root" && pwd)" + +# Single source of truth for the managed workflow's version. Bump this whenever +# templates/feature-fanout.js's behavior changes; scaffold.sh will then re-stamp any +# destination whose marker is older (see issue #38, which drives re-stamping on +# plugin upgrade). +MANAGED_VERSION=1 +MARKER_PREFIX="@orchestrator-managed feature-fanout v" + +echo "orchestrator setup: scaffolding into $target_root" + +# --- helpers ------------------------------------------------------------------------ +copy_if_absent() { + # $1 = template path, $2 = destination path, $3 = label for the summary line + local src="$1" dst="$2" label="$3" + if [ -e "$dst" ]; then + echo " kept: $label ($dst already exists — user-owned, left untouched)" + return 0 + fi + mkdir -p "$(dirname "$dst")" + cp "$src" "$dst" + echo " created: $label -> $dst" +} + +managed_version_of() { + # Prints the version number found in the marker line of $1, or empty if none. + local f="$1" + [ -f "$f" ] || { echo ""; return 0; } + grep -o "${MARKER_PREFIX}[0-9]\+" "$f" 2>/dev/null | head -1 | grep -o '[0-9]\+$' || true +} + +# --- 1. user-owned files: create only if absent, never overwritten ----------------- +copy_if_absent "$templates_dir/gates.json" "$target_root/.claude/gates.json" "project adapter (.claude/gates.json)" +copy_if_absent "$templates_dir/CLAUDE.md" "$target_root/CLAUDE.md" "CLAUDE.md" + +# --- 2. managed workflow: re-stamp when the destination's marker is older ---------- +fanout_dst="$target_root/.claude/workflows/feature-fanout.js" +existing_version="$(managed_version_of "$fanout_dst")" +if [ ! -f "$fanout_dst" ]; then + mkdir -p "$(dirname "$fanout_dst")" + cp "$templates_dir/feature-fanout.js" "$fanout_dst" + echo " created: managed workflow (.claude/workflows/feature-fanout.js) at v$MANAGED_VERSION" +elif [ -z "$existing_version" ]; then + # Present but carries no recognizable marker (e.g. hand-authored or pre-marker file) + # — treat as older than any managed version and re-stamp. + cp "$templates_dir/feature-fanout.js" "$fanout_dst" + echo " restamped: managed workflow (.claude/workflows/feature-fanout.js) — no marker found, now v$MANAGED_VERSION" +elif [ "$existing_version" -lt "$MANAGED_VERSION" ]; then + cp "$templates_dir/feature-fanout.js" "$fanout_dst" + echo " restamped: managed workflow (.claude/workflows/feature-fanout.js) v$existing_version -> v$MANAGED_VERSION" +elif [ "$existing_version" -eq "$MANAGED_VERSION" ]; then + echo " up to date: managed workflow (.claude/workflows/feature-fanout.js) already v$MANAGED_VERSION" +else + # Destination carries a NEWER version than this scaffold.sh ships — never clobber. + echo " kept: managed workflow (.claude/workflows/feature-fanout.js) is v$existing_version, newer than this installer's v$MANAGED_VERSION — left untouched" +fi + +# --- 3. CI templates: create only if absent ---------------------------------------- +copy_if_absent "$templates_dir/gates.yml" "$target_root/.github/workflows/gates.yml" "CI gate workflow (.github/workflows/gates.yml)" +copy_if_absent "$templates_dir/action.yml" "$target_root/.github/actions/setup/action.yml" "CI setup action (.github/actions/setup/action.yml)" + +# --- 4. .gitignore hygiene: append-if-missing, never duplicate --------------------- +gitignore="$target_root/.gitignore" +touch "$gitignore" +required_entries=( + ".env" + ".env.*" + "!.env.example" + ".claude/settings.local.json" + ".claude/state/" +) +appended=() +for entry in "${required_entries[@]}"; do + if grep -qxF -- "$entry" "$gitignore"; then + continue + fi + printf '%s\n' "$entry" >> "$gitignore" + appended+=("$entry") +done +if [ "${#appended[@]}" -gt 0 ]; then + echo " appended: .gitignore <- ${appended[*]}" +else + echo " kept: .gitignore already has all required entries" +fi + +# --- 5. runtime state dir ------------------------------------------------------------ +mkdir -p "$target_root/.claude/state" +echo " ensured: .claude/state/" + +echo "orchestrator setup: scaffold complete." diff --git a/.claude/skills/setup/templates/CLAUDE.md b/.claude/skills/setup/templates/CLAUDE.md new file mode 100644 index 0000000..348fb4f --- /dev/null +++ b/.claude/skills/setup/templates/CLAUDE.md @@ -0,0 +1,37 @@ +# + +> Fill this in per project. Keep it lean — project-WIDE context only. Task-specific detail belongs in the +> task prompt, not here. See `docs/PROMPTS.md` for a prompt that drafts this file for you. + +## What this project is +<1–3 sentences: domain, what it does, who uses it.> + +## Stack & layout +- Language / runtime: +- Package manager: +- Key directories (mirror `.claude/gates.json` → `modules`): + - `path/` — what lives here + +## Conventions +- Code style / lint rules of note: +- Testing approach (frameworks, where tests live): +- Definition of done: + +## Multi-agent orchestration (this template) +This repo is set up for orchestrated multi-agent development. See `docs/USAGE.md`. +- **Agents:** `.claude/agents/` — orchestrator, implementer (worktree-isolated), reviewer, test-runner. +- **Adapter:** `.claude/gates.json` — module map, gate commands, model routing. **This is the file to keep current.** +- **Gates run via** `.claude/scripts/gate.sh ` and the hooks in `.claude/settings.json`. +- **Workflow:** `.claude/workflows/feature-fanout.js` for deterministic fan-out. + +### Module boundaries (hard rule) +A worker assigned to a module MUST NOT edit files outside that module's `path`. Cross-module work is +re-scoped by the orchestrator, never reached across by a worker. + +### Merge policy + — base branch `main`. (Mirror in `gates.json` → `merge`.) + +## Don'ts +- Don't put secrets in the repo. +- Don't bypass the gates. +- diff --git a/.claude/skills/setup/templates/MANIFEST.md b/.claude/skills/setup/templates/MANIFEST.md new file mode 100644 index 0000000..b761403 --- /dev/null +++ b/.claude/skills/setup/templates/MANIFEST.md @@ -0,0 +1,17 @@ +# Template → destination map + +Read by humans (and `scaffold.sh`, informally) to see where each template lands in a +consumer repo. None of these paths are distributable inside the plugin package itself +(workflows and CI YAML aren't carried by a Claude Code plugin), so `/orchestrator:setup` +materializes them into the consumer repo on first run. + +| Template file | Destination in consumer repo | Ownership | Re-run behavior | +|-------------------|---------------------------------------|-----------|-------------------------------------------| +| `gates.json` | `.claude/gates.json` | user | created only if absent; never overwritten | +| `CLAUDE.md` | `CLAUDE.md` | user | created only if absent; never overwritten | +| `feature-fanout.js` | `.claude/workflows/feature-fanout.js` | managed | re-stamped when the `@orchestrator-managed feature-fanout vN` marker is older than the version scaffold.sh ships; left alone if same/newer | +| `gates.yml` | `.github/workflows/gates.yml` | ci | created only if absent; never overwritten | +| `action.yml` | `.github/actions/setup/action.yml` | ci | created only if absent; never overwritten | + +See `.claude/skills/setup/scaffold.sh` for the implementation, and `.claude/skills/setup/SKILL.md` +for the full onboarding flow this scaffold step is one part of. diff --git a/.claude/skills/setup/templates/action.yml b/.claude/skills/setup/templates/action.yml new file mode 100644 index 0000000..658b7b0 --- /dev/null +++ b/.claude/skills/setup/templates/action.yml @@ -0,0 +1,57 @@ +# INSTALL DESTINATION: .github/actions/setup/action.yml +# Installed by `/orchestrator:setup` (scaffold.sh) — created only if that path doesn't +# already exist; left untouched on re-runs (see .claude/skills/setup/scaffold.sh). +name: Adapter setup +description: > + Toolchain setup + dependency install, driven entirely by .claude/gates.json so the + same workflow works on any downstream repo without hand-edited YAML. Reads + project.packageManager / project.language / gates.coverage_threshold, sets up the + matching toolchain, runs the `install` gate, and exports COVERAGE_THRESHOLD. + +runs: + using: composite + steps: + # Read the adapter. `node` is preinstalled on GitHub runners, so this works even + # before any toolchain setup — same dependency gate.sh already relies on. + - id: adapter + shell: bash + run: | + gates=".claude/gates.json" + read_json() { node -e "try{const g=require('./$gates');process.stdout.write(String($1||''))}catch(e){process.stdout.write('')}"; } + pm="$(read_json "g.project&&g.project.packageManager")" + lang="$(read_json "g.project&&g.project.language")" + thr="$(read_json "g.gates&&g.gates.coverage_threshold")" + echo "package-manager=$pm" >> "$GITHUB_OUTPUT" + echo "language=$lang" >> "$GITHUB_OUTPUT" + # Export the threshold so coverage gate commands can read $COVERAGE_THRESHOLD. + [ -n "$thr" ] && echo "COVERAGE_THRESHOLD=$thr" >> "$GITHUB_ENV" + echo "adapter: packageManager='$pm' language='$lang' coverage_threshold='$thr'" + + # --- JavaScript/TypeScript toolchain (the common case) ------------------------- + - if: contains(fromJSON('["pnpm","npm","yarn"]'), steps.adapter.outputs.package-manager) + uses: actions/setup-node@v4 + with: + node-version: 20 + + - if: steps.adapter.outputs.package-manager == 'pnpm' + shell: bash + run: corepack enable + + # --- Extension point: add stack-specific toolchain setup here ------------------ + # The template ships the JS path above. Downstream repos on other stacks add their + # setup here (it's a config edit, like gates.json). Examples: + # + # - if: contains(steps.adapter.outputs.language, 'olidity') # Solidity / Foundry + # uses: foundry-rs/foundry-toolchain@v1 + # + # - if: contains(steps.adapter.outputs.language, 'ython') # Python + # uses: actions/setup-python@v5 + # with: { python-version: '3.12' } + # + # See issue #9 (per-worktree lifecycle) — the same bootstrap that lets isolated + # worktree workers run a gate is what CI needs here. + + # Install dependencies via the adapter's `install` gate (skips cleanly if empty). + - name: Install dependencies + shell: bash + run: bash .claude/scripts/gate.sh install diff --git a/.claude/skills/setup/templates/feature-fanout.js b/.claude/skills/setup/templates/feature-fanout.js new file mode 100644 index 0000000..804d202 --- /dev/null +++ b/.claude/skills/setup/templates/feature-fanout.js @@ -0,0 +1,89 @@ +// @orchestrator-managed feature-fanout v1 +// This file is installed and re-stamped by `/orchestrator:setup` (scaffold.sh). It is NOT +// user-owned: re-running setup will overwrite it whenever the marker version above is older +// than the version the installed plugin ships. Do not hand-edit if you want future setup runs +// to keep it in sync — fork it under a different name instead. +export const meta = { + name: 'feature-fanout', + description: 'Scope a task into independent sub-tasks, implement each in an isolated worktree, adversarially review every change through multiple lenses, and loop until all gates pass.', + phases: [ + { title: 'Scope', detail: 'decompose into non-overlapping sub-tasks' }, + { title: 'Implement', detail: 'one worker per sub-task, isolated worktrees' }, + { title: 'Review', detail: 'multi-lens adversarial review + iterate' }, + ], +} + +// Invoke with: Workflow({ name: 'feature-fanout', args: { task: "..." } }) +// or just type "ultracode " and ask to run this workflow. + +const TASK = (args && args.task) || 'No task provided. Pass args.task.' +const LENSES = ['correctness', 'tests', 'security'] +const MAX_ITERS = 3 + +phase('Scope') +const plan = await agent( + `Decompose this task into INDEPENDENT, non-overlapping sub-tasks. Each sub-task must be scoped to a + single module/path (read .claude/gates.json "modules") so no two workers touch the same files. + Merge or sequence anything that would overlap. Scale to complexity — a small task may yield ONE sub-task. + TASK: ${TASK}`, + { phase: 'Scope', schema: { + type: 'object', + properties: { subtasks: { type: 'array', items: { + type: 'object', + required: ['title', 'module', 'prompt'], + properties: { + title: { type: 'string' }, + module: { type: 'string' }, + prompt: { type: 'string', description: 'full self-contained instruction for the implementer' }, + }, + } } }, + required: ['subtasks'], + } } +) + +log(`Scoped into ${plan.subtasks.length} sub-task(s): ${plan.subtasks.map(s => s.title).join(', ')}`) + +// Each sub-task: implement (own worktree) -> review through all lenses -> iterate until approved or out of tries. +const results = await pipeline( + plan.subtasks, + async (st) => { + let attempt = 0 + let impl = await agent( + `${st.prompt}\n\nBoundary: stay within module "${st.module}". Run the gates in .claude/gates.json before reporting done.`, + { label: `impl:${st.module}`, phase: 'Implement', isolation: 'worktree' } + ) + while (attempt < MAX_ITERS) { + const reviews = await parallel(LENSES.map(lens => () => + agent( + `Adversarially review this change through the "${lens}" lens. Try to refute it. ` + + `Return verdict approve|reject with concrete, actionable findings.\n\nCHANGE:\n${impl}`, + { label: `review:${lens}:${st.module}`, phase: 'Review', schema: { + type: 'object', + required: ['verdict', 'findings'], + properties: { + verdict: { type: 'string', enum: ['approve', 'reject'] }, + findings: { type: 'array', items: { type: 'string' } }, + }, + } } + ) + )) + const rejects = reviews.filter(Boolean).filter(r => r.verdict === 'reject') + if (rejects.length === 0) { + return { subtask: st.title, module: st.module, status: 'approved', attempts: attempt + 1, impl } + } + attempt++ + log(`"${st.title}" rejected on attempt ${attempt} (${rejects.length}/${LENSES.length} lenses). Iterating.`) + const reasons = rejects.flatMap(r => r.findings).map(f => `- ${f}`).join('\n') + impl = await agent( + `Reviewers rejected your change to "${st.module}". Fix every finding, re-run the gates, report again:\n${reasons}`, + { label: `fix:${st.module}`, phase: 'Implement', isolation: 'worktree' } + ) + } + return { subtask: st.title, module: st.module, status: 'needs-human', attempts: attempt, impl } + } +) + +const approved = results.filter(r => r && r.status === 'approved') +const stuck = results.filter(r => r && r.status !== 'approved') +log(`Done. ${approved.length} approved, ${stuck.length} need human attention.`) +return { task: TASK, approved, stuck } diff --git a/.claude/skills/setup/templates/gates.json b/.claude/skills/setup/templates/gates.json new file mode 100644 index 0000000..cb04b16 --- /dev/null +++ b/.claude/skills/setup/templates/gates.json @@ -0,0 +1,60 @@ +{ + "_README": "PROJECT ADAPTER. This is the ONLY file you must fill in per project. The generic agents/hooks read it. Leave a value as an empty string to mark a gate 'not configured' (it will be skipped, not failed). See docs/GETTING_STARTED.md and docs/PROMPTS.md.", + + "project": { + "name": "", + "language": "", + "packageManager": "" + }, + + "modules": [ + { + "name": "example-core", + "path": "packages/core", + "description": "What lives here. Workers assigned to this module must not edit outside `path`.", + "owner": "" + } + ], + + "gates": { + "_note": "Exact shell commands for this repo. Run from repo root. Empty string = skip.", + "install": "", + "build": "", + "lint": "", + "typecheck": "", + "test": "", + "test_affected": "", + "coverage": "", + "coverage_threshold": 80, + "e2e": "", + "security": "" + }, + + "worktree": { + "_note": "Optional per-worktree lifecycle. `setup` runs right after an isolated implementer/reviewer worktree is created — bootstrap toolchain state that lives OUTSIDE the tree so every gate is runnable in-worktree (e.g. 'pnpm install', 'forge install', link shared caches). `teardown` runs before the worktree is removed. Empty string = skip. Run via .claude/scripts/worktree.sh setup|teardown.", + "setup": "", + "teardown": "" + }, + + "review": { + "lenses": ["correctness", "tests", "security", "performance"], + "consensus": "all", + "_consensus_note": "'all' = every lens must approve; or an integer = that many approvals required.", + "skills": [] + }, + + "budget": { + "orchestrator_model": "opus", + "worker_model": "sonnet", + "explorer_model": "haiku", + "reviewer_model": "opus", + "max_parallel_workers": 3, + "_note": "Opus to coordinate/review, Sonnet to build, Haiku to explore. Lower max_parallel_workers if review/merge is your bottleneck." + }, + + "merge": { + "policy": "pr-per-agent", + "_policy_options": "pr-per-agent | orchestrated-sequential-merge", + "baseBranch": "main" + } +} diff --git a/.claude/skills/setup/templates/gates.yml b/.claude/skills/setup/templates/gates.yml new file mode 100644 index 0000000..f2fbf44 --- /dev/null +++ b/.claude/skills/setup/templates/gates.yml @@ -0,0 +1,48 @@ +# INSTALL DESTINATION: .github/workflows/gates.yml +# Installed by `/orchestrator:setup` (scaffold.sh) — created only if that path doesn't +# already exist; left untouched on re-runs (see .claude/skills/setup/scaffold.sh). +# +# Server-side gate enforcement. Mirrors the local gates (settings.json hooks + +# gate.sh) at the GitHub PR level, so a merge is gated by CI — not only by the +# orchestrator remembering to run the gates before it opens a PR. +# +# Adapter-driven: each job just runs `gate.sh `, which reads the command from +# .claude/gates.json (and skips cleanly when a gate is left ""). You should not need +# to edit this file per project — configure gates.json instead. Toolchain setup lives +# in .github/actions/setup (also adapter-driven). See docs/PROMPTS.md to (re)generate. +# +# Enforcement teeth: turn these checks into REQUIRED status checks via branch +# protection on the base branch — see docs/GETTING_STARTED.md. +name: gates + +on: + pull_request: + workflow_dispatch: + +permissions: + contents: read + +# A newer push to the same PR cancels the in-flight run. +concurrency: + group: gates-${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + +jobs: + gates: + name: ${{ matrix.gate }} + runs-on: ubuntu-latest + strategy: + fail-fast: false # one red gate shouldn't hide the others + matrix: + # One job per gate → each surfaces as its own PR check (what visual + # front-ends like emdash monitor). `install` is not here — it runs once in + # the setup action as a prerequisite for every gate. + gate: [build, lint, typecheck, test, coverage, security] + steps: + - uses: actions/checkout@v4 + - uses: ./.github/actions/setup + - name: gate.sh ${{ matrix.gate }} + # An unconfigured gate ("") exits 0 (skip), so the check is green-but-trivial + # rather than red — same semantics as the local hooks. The coverage gate + # command can read $COVERAGE_THRESHOLD (exported by the setup action). + run: bash .claude/scripts/gate.sh ${{ matrix.gate }}