chore(pr): dev-wrapper opt-in — advisory subcommands may dogfood modified wrappers (#111656)

* chore(pr): dev-wrapper opt-in — advisory subcommands may dogfood modified wrappers

* fix(pr): hermetic dev-wrapper test fixture and type narrowing
This commit is contained in:
Peter Steinberger
2026-07-19 22:58:15 -07:00
committed by GitHub
parent 6e19c36d41
commit 104128dd32
3 changed files with 273 additions and 10 deletions
+1 -1
View File
@@ -20,7 +20,7 @@ This directory owns local tooling, script wrappers, and generated-artifact helpe
## PR Prepare Gates
- `scripts/pr` serializes review, prepare, and merge operations per PR across linked worktrees; `scripts/pr gc` skips active or indeterminate locks. A successful command return is the trusted synchronous-completion contract: every PR-state-mutating child must be joined before returning, and such work must never daemonize or explicitly escape both the operation group and lock-notification FD. A failed command auto-releases only while its explicit pre-side-effect validation marker remains active; failures after mutation/tool launch, interruptions, and controller loss stay locked because detached children cannot be disproved. After verifying no child tools remain, use the reported exact-OID `scripts/pr lock-recover` command. Never bypass or delete these refs manually.
- `scripts/pr` serializes review, prepare, and merge operations per PR across linked worktrees; `scripts/pr gc` skips active or indeterminate locks. Its subcommand classification table is the canonical wrapper trust boundary: a mismatched local wrapper may run only a classified `advisory` subcommand with `--dev-wrapper` or `OPENCLAW_PR_DEV_WRAPPER=1`; classified `landing` subcommands always require canonical/origin-main wrapper code. A successful command return is the trusted synchronous-completion contract: every PR-state-mutating child must be joined before returning, and such work must never daemonize or explicitly escape both the operation group and lock-notification FD. A failed command auto-releases only while its explicit pre-side-effect validation marker remains active; failures after mutation/tool launch, interruptions, and controller loss stay locked because detached children cannot be disproved. After verifying no child tools remain, use the reported exact-OID `scripts/pr lock-recover` command. Never bypass or delete these refs manually.
- `scripts/pr prepare-gates` holds the heavy-check lock for its whole local gate block (`scripts/pr-gates-lock.mjs`), so concurrent gate runs across `.worktrees` queue as units instead of dying on child lock timeouts or vitest no-output watchdog kills.
- `OPENCLAW_PR_GATES_REMOTE=testbox` runs the full-suite `pnpm test` gate on a Blacksmith Testbox through `scripts/crabbox-wrapper.mjs` (same delegation as `check:changed`); `pnpm build`/`pnpm check` stay local. The `tbx_` lease id and Actions run URL land in `.local/gates.env` (`REMOTE_GATES_*`) and `.local/prep.md`. Use it for reviewed trusted code when a loaded host makes the local 88-shard run stall-kill; contributor/fork code stays on secretless CI or sanitized AWS unless a maintainer explicitly approves credentialed execution.
+62 -8
View File
@@ -9,6 +9,36 @@ export CLICOLOR_FORCE=0
export FORCE_COLOR=0
unset COLORTERM
# This is the single source of truth for the canonical-wrapper trust boundary.
# Advisory commands may run a mismatched local wrapper only with the explicit
# developer opt-in; landing commands must always use canonical/origin-main code.
# Classification is independent of serialization: ci-dispatch remains locked
# because GitHub exposes neither dispatch deduplication nor a correlation ID.
# PR_SUBCOMMAND_CLASSIFICATIONS_BEGIN
pr_subcommand_classification() {
case "$1" in
ls | ci-dispatch)
printf 'advisory\n'
;;
gc | lock-recover | review-init | review-checkout-main | review-checkout-pr | review-claim | review-guard | review-artifacts-init | review-validate-artifacts | review-tests | prepare-init | prepare-validate-commit | prepare-gates | prepare-push | prepare-sync-head | prepare-run | merge-verify | merge-run)
printf 'landing\n'
;;
*) return 1 ;;
esac
}
# PR_SUBCOMMAND_CLASSIFICATIONS_END
dev_wrapper_opt_in=0
if [ "${OPENCLAW_PR_DEV_WRAPPER:-}" = "1" ]; then
dev_wrapper_opt_in=1
fi
if [ "${1-}" = "--dev-wrapper" ]; then
dev_wrapper_opt_in=1
export OPENCLAW_PR_DEV_WRAPPER=1
shift
fi
requested_subcommand="${1-}"
# If invoked from a linked worktree copy of this script, re-exec the canonical
# script from the repository root so behavior stays consistent across worktrees.
script_self="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)/$(basename "${BASH_SOURCE[0]}")"
@@ -60,19 +90,39 @@ if common_git_dir=$(git -C "$script_parent_dir" rev-parse --path-format=absolute
if [ -z "$linked_wrapper_revision" ] ||
[ -z "$anchor_wrapper_revision" ] ||
[ "$linked_wrapper_revision" != "$anchor_wrapper_revision" ]; then
echo "scripts/pr implementation differs between this worktree and the canonical checkout, and does not match origin/main." >&2
echo "Refusing to silently substitute canonical wrapper code from: $canonical_repo_root" >&2
echo "Run scripts/pr from a checkout whose wrapper matches the canonical checkout or a fetched origin/main." >&2
exit 1
requested_classification=$(pr_subcommand_classification "$requested_subcommand" 2>/dev/null || true)
if [ "$dev_wrapper_opt_in" = "1" ] && [ "$requested_classification" = "advisory" ]; then
if [ "${OPENCLAW_PR_DEV_WRAPPER_BANNER_SHOWN:-}" != "1" ]; then
local_head_revision=$(git -C "$script_parent_dir" rev-parse HEAD 2>/dev/null || printf 'unknown')
echo "WARNING: running local scripts/pr revision $local_head_revision via dev-wrapper opt-in." >&2
echo "subcommand '$requested_subcommand' is classified advisory." >&2
echo "The local wrapper differs from the canonical checkout and origin/main; landing subcommands remain refused." >&2
export OPENCLAW_PR_DEV_WRAPPER_BANNER_SHOWN=1
fi
else
if [ "$dev_wrapper_opt_in" = "1" ] && [ -n "$requested_classification" ]; then
echo "subcommand '$requested_subcommand' is classified $requested_classification; dev-wrapper opt-in is unavailable." >&2
fi
echo "scripts/pr implementation differs between this worktree and the canonical checkout, and does not match origin/main." >&2
echo "Refusing to silently substitute canonical wrapper code from: $canonical_repo_root" >&2
echo "Run scripts/pr from a checkout whose wrapper matches the canonical checkout or a fetched origin/main." >&2
exit 1
fi
fi
fi
fi
is_locked_pr_command() {
case "$1" in
review-init | review-checkout-main | review-checkout-pr | review-claim | review-guard | review-artifacts-init | review-validate-artifacts | review-tests | prepare-init | prepare-validate-commit | prepare-gates | prepare-push | prepare-sync-head | prepare-run | ci-dispatch | merge-verify | merge-run) return 0 ;;
*) return 1 ;;
esac
# Advisory trust classification permits local dogfood, but dispatches still
# serialize with landing operations because the remote mutation is not atomic.
if [ "$1" = "ci-dispatch" ]; then
return 0
fi
local classification
classification=$(pr_subcommand_classification "$1") || return 1
[ "$classification" = "landing" ] || return 1
# gc manages per-PR locks itself; lock-recover performs an exact-OID CAS.
[ "$1" != "gc" ] && [ "$1" != "lock-recover" ]
}
is_main_only_pr_command() {
@@ -107,6 +157,7 @@ source "$script_parent_dir/lib/plain-gh.sh"
usage() {
cat <<USAGE
Usage:
scripts/pr [--dev-wrapper] <subcommand> ...
scripts/pr ls
scripts/pr gc [--dry-run]
scripts/pr lock-recover <PR> <OWNER_OID> --confirmed-no-running-tools
@@ -128,6 +179,9 @@ Usage:
scripts/pr merge-verify <PR>
scripts/pr merge-run <PR>
OPENCLAW_PR_MERGE_METHOD=merge|rebase preserves the PR commit series.
--dev-wrapper permits a mismatched local wrapper only for subcommands
classified advisory. OPENCLAW_PR_DEV_WRAPPER=1 is equivalent.
USAGE
}