Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
34 changes: 34 additions & 0 deletions .claude/commands/test-pr.md
Original file line number Diff line number Diff line change
@@ -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: <pr-number>
---

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 `<humanTest.worktreeDir>/pr-$ARGUMENTS` (default `.worktrees/pr-<n>`),
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 <path> && <humanTest.launch>`.
- 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 <path>` 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.
75 changes: 75 additions & 0 deletions .claude/scripts/prepare-pr.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
#!/usr/bin/env bash
# prepare-pr.sh <pr-number>
# 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 <worktreeDir>/pr-<n>.
# (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 <pr-number>}"
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"
24 changes: 24 additions & 0 deletions docs/USAGE.md
Original file line number Diff line number Diff line change
Expand Up @@ -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 <n>`)

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 <pr-number>` 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-<n>` (default `.worktrees/pr-<n>`) — 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 <path>`.
19 changes: 12 additions & 7 deletions examples/ts-solidity-foundry/gates.json
Original file line number Diff line number Diff line change
@@ -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",
Expand All @@ -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)",
Expand All @@ -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",
Expand All @@ -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"
}
}
Loading