diff --git a/.claude/commands/test-pr.md b/.claude/commands/test-pr.md new file mode 100644 index 0000000..8f9fa5e --- /dev/null +++ b/.claude/commands/test-pr.md @@ -0,0 +1,34 @@ +--- +description: Prepare an open PR for local human testing — checks out its branch into an isolated, ready-to-run worktree (deps built) and hands you the launch command. +argument-hint: +--- + +You are preparing an OPEN pull request for the user to manually test locally, WITHOUT disturbing their main +working tree. The PR number is: **$ARGUMENTS** + +Do this: + +1. Run `bash .claude/scripts/prepare-pr.sh $ARGUMENTS`. It resolves the PR's head branch, fetches it, creates (or + refreshes) a **detached** worktree at `/pr-$ARGUMENTS` (default `.worktrees/pr-`), + and runs the project's `humanTest.prepare` command (install + build) inside it. The script is idempotent — + re-running it on the same PR just fast-forwards the worktree to the latest pushed commit and rebuilds. + +2. If the script fails, report the exact error and stop (common causes: the PR is closed, the bot token isn't set, + or `humanTest` isn't configured in `.claude/gates.json`). Do not guess. + +3. On success, report to the user, in this order: + - the PR number, its title, and the **exact head commit SHA** the worktree is now at (so they know they are + testing the latest pushed code, not a stale build — the usual reason "the fix doesn't work" for them); + - the ready-to-run path and the launch command the script printed, e.g. + `cd && `. + - Tell them to run the launch command **in their own terminal** (a dev server is long-running and needs a + browser), and to tear the worktree down with `git worktree remove ` when finished. + +4. Offer (do not assume) to start the launch command for them in the background if they'd rather you drive it. + +Notes: +- This never edits the PR and never touches `main` — the worktree is an isolated, detached checkout, so it can + coexist with the same branch checked out elsewhere (e.g. an agent worktree). +- The prepare/launch/worktree-dir commands are read from `.claude/gates.json` → `humanTest`, so this command is + project-agnostic. If `humanTest` is absent, the script still makes the worktree and tells the user to build/run + manually. diff --git a/.claude/scripts/prepare-pr.sh b/.claude/scripts/prepare-pr.sh new file mode 100755 index 0000000..f1bbd5c --- /dev/null +++ b/.claude/scripts/prepare-pr.sh @@ -0,0 +1,75 @@ +#!/usr/bin/env bash +# prepare-pr.sh +# Prepare a ready-to-run local checkout of an OPEN pull request so a human can +# manually test it, WITHOUT touching the main working tree. Idempotent. +# +# 1. Resolves the PR's head branch (via bot-gh.sh) and fetches it. +# 2. Creates or refreshes a DETACHED git worktree at /pr-. +# (Detached, not a named branch, so it never clashes with the same branch +# already checked out in an agent worktree — git forbids that.) +# 3. Runs humanTest.prepare inside the worktree (install/build) if configured. +# 4. Prints the worktree path + the humanTest.launch command to run. +# +# Config (.claude/gates.json → "humanTest"): +# prepare shell cmd run IN the worktree to make it runnable (optional) +# launch shell cmd to start the app for manual testing (printed, optional) +# worktreeDir parent dir for PR worktrees (optional, default ".worktrees") +# +# gh reads go through bot-gh.sh for consistent auth; git ops stay local. +set -uo pipefail + +pr="${1:?usage: prepare-pr.sh }" +case "$pr" in + ''|*[!0-9]*) echo "prepare-pr: PR number must be numeric (got '$pr')" >&2; exit 2 ;; +esac + +script_dir="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +root="$(cd "$script_dir/../.." && pwd)" +cd "$root" + +gates="$root/.claude/gates.json" +read_gate() { + node -e "try{const g=require('$gates');process.stdout.write((g.humanTest&&g.humanTest['$1'])||'')}catch(e){process.stdout.write('')}" 2>/dev/null +} +prepare_cmd="$(read_gate prepare)" +launch_cmd="$(read_gate launch)" +wt_parent="$(read_gate worktreeDir)"; wt_parent="${wt_parent:-.worktrees}" + +# Resolve the PR's head branch (gh through the bot wrapper for consistent auth). +branch="$("$script_dir/bot-gh.sh" pr view "$pr" --json headRefName -q .headRefName 2>/dev/null)" +if [ -z "$branch" ]; then + echo "prepare-pr: could not resolve head branch for PR #$pr (is it open? is the bot token set?)" >&2 + exit 1 +fi +echo "▶ PR #$pr → branch '$branch'" + +git fetch origin "$branch" || { echo "prepare-pr: git fetch origin $branch failed" >&2; exit 1; } +sha="$(git rev-parse FETCH_HEAD)" + +wt="$root/$wt_parent/pr-$pr" +if git worktree list --porcelain | grep -qxF "worktree $wt"; then + echo "▶ refreshing existing worktree $wt → $sha" + git -C "$wt" checkout -q --detach "$sha" || { echo "prepare-pr: could not update worktree" >&2; exit 1; } +else + echo "▶ creating worktree $wt (detached at $sha)" + mkdir -p "$root/$wt_parent" + git worktree add -f --detach "$wt" "$sha" || { echo "prepare-pr: git worktree add failed" >&2; exit 1; } +fi + +if [ -n "$prepare_cmd" ]; then + echo "▶ prepare: $prepare_cmd" + ( cd "$wt" && eval "$prepare_cmd" ) || { echo "prepare-pr: humanTest.prepare failed" >&2; exit 1; } +else + echo "▶ humanTest.prepare not configured in gates.json — skipping deps/build" +fi + +echo "" +echo "✅ PR #$pr is ready to test. In your terminal:" +echo " cd $wt" +if [ -n "$launch_cmd" ]; then + echo " $launch_cmd" +else + echo " (no humanTest.launch configured — start the app manually)" +fi +echo "" +echo "When done, tear it down with: git worktree remove $wt" diff --git a/docs/USAGE.md b/docs/USAGE.md index 54b3a25..00a7ab4 100644 --- a/docs/USAGE.md +++ b/docs/USAGE.md @@ -173,3 +173,27 @@ If workers stall or collide, the fix is almost always a sharper **module map** i ## When NOT to orchestrate Trivial or single-file changes: just do them directly. The 15× token multiplier isn't worth it. The orchestrator itself is told to use one worker and no parallelism for small tasks — hold it to that. + +## Testing a PR locally (`/test-pr `) + +The owner reviews bot PRs by actually running the change. Doing that by hand is error-prone — the classic failure +is testing from the main working tree (which does NOT contain the unmerged PR), concluding "the fix doesn't work", +and bouncing the PR back. `/test-pr ` removes that footgun: + +1. Resolves the PR's head branch and fetches the latest pushed commit. +2. Creates a **detached** git worktree at `humanTest.worktreeDir/pr-` (default `.worktrees/pr-`) — isolated + from your main checkout and from any agent worktree on the same branch. +3. Runs `humanTest.prepare` inside it (install + build) and prints `humanTest.launch` for you to run. + +Configure the commands once in `.claude/gates.json`: + +```json +"humanTest": { + "prepare": "pnpm install --prefer-offline && pnpm -r build", + "launch": "pnpm dev", + "worktreeDir": ".worktrees" +} +``` + +Add `humanTest.worktreeDir` to `.gitignore`. Re-running `/test-pr` on the same PR fast-forwards the worktree to the +latest commit (idempotent). Tear down with `git worktree remove `. diff --git a/examples/ts-solidity-foundry/gates.json b/examples/ts-solidity-foundry/gates.json index 33b812c..c7c2f34 100644 --- a/examples/ts-solidity-foundry/gates.json +++ b/examples/ts-solidity-foundry/gates.json @@ -1,12 +1,10 @@ { "_README": "WORKED EXAMPLE (not a live adapter) — a TypeScript (pnpm workspace) + Solidity (Foundry) monorepo. Copy this into YOUR repo's .claude/gates.json and adapt paths/commands. See examples/ts-solidity-foundry/README.md for the reasoning behind each choice. Empty string = gate skipped, not failed.", - "project": { "name": "acme-protocol", "language": "typescript+solidity", "packageManager": "pnpm" }, - "modules": [ { "name": "sdk", @@ -27,7 +25,6 @@ "owner": "" } ], - "gates": { "_note": "Run from repo root. Each mixed-stack gate runs the TS side then the Foundry side; a failure in either fails the gate. `(cd contracts && ...)` keeps forge in its own project dir without a persistent chdir.", "install": "pnpm install --frozen-lockfile && (cd contracts && forge install)", @@ -41,14 +38,17 @@ "e2e": "", "security": "(cd contracts && slither . || true)" }, - "review": { - "lenses": ["correctness", "tests", "security", "performance"], + "lenses": [ + "correctness", + "tests", + "security", + "performance" + ], "consensus": "all", "_consensus_note": "Solidity handles value — keep the `security` lens on. 'all' = every lens must approve.", "skills": [] }, - "budget": { "orchestrator_model": "opus", "worker_model": "sonnet", @@ -57,10 +57,15 @@ "max_parallel_workers": 3, "_note": "3 modules → up to 3 parallel workers. Drop to 2 if human review/merge is the bottleneck." }, - "merge": { "policy": "pr-per-agent", "_policy_options": "pr-per-agent | orchestrated-sequential-merge", "baseBranch": "main" + }, + "humanTest": { + "_note": "Consumed by /test-pr (.claude/scripts/prepare-pr.sh). 'prepare' runs INSIDE the PR worktree to make it runnable (install/build); 'launch' is printed for you to run in your own terminal; 'worktreeDir' is where PR test worktrees go (gitignore it).", + "prepare": "pnpm install --prefer-offline && pnpm -r build", + "launch": "pnpm dev", + "worktreeDir": ".worktrees" } }