feat(reviewer-runtime): add shared review supervisor

This commit is contained in:
Stefano Fiorini
2026-03-05 22:54:50 -06:00
parent 526101fd23
commit 8720691135
4 changed files with 578 additions and 0 deletions

View File

@@ -13,6 +13,11 @@ Create structured implementation plans with milestone and story tracking, and op
- For Codex, native skill discovery must be configured:
- `~/.agents/skills/superpowers -> ~/.codex/superpowers/skills`
- For Cursor, skills must be installed under `.cursor/skills/` (repo-local) or `~/.cursor/skills/` (global)
- Shared reviewer runtime must be installed beside agent skills when using reviewer CLIs:
- Codex: `~/.codex/skills/reviewer-runtime/run-review.sh`
- Claude Code: `~/.claude/skills/reviewer-runtime/run-review.sh`
- OpenCode: `~/.config/opencode/skills/reviewer-runtime/run-review.sh`
- Cursor: `.cursor/skills/reviewer-runtime/run-review.sh` or `~/.cursor/skills/reviewer-runtime/run-review.sh`
If dependencies are missing, stop and return:
@@ -39,6 +44,8 @@ The reviewer CLI is independent of which agent is running the planning — e.g.,
```bash
mkdir -p ~/.codex/skills/create-plan
cp -R skills/create-plan/codex/* ~/.codex/skills/create-plan/
mkdir -p ~/.codex/skills/reviewer-runtime
cp -R skills/reviewer-runtime/* ~/.codex/skills/reviewer-runtime/
```
### Claude Code
@@ -46,6 +53,8 @@ cp -R skills/create-plan/codex/* ~/.codex/skills/create-plan/
```bash
mkdir -p ~/.claude/skills/create-plan
cp -R skills/create-plan/claude-code/* ~/.claude/skills/create-plan/
mkdir -p ~/.claude/skills/reviewer-runtime
cp -R skills/reviewer-runtime/* ~/.claude/skills/reviewer-runtime/
```
### OpenCode
@@ -53,6 +62,8 @@ cp -R skills/create-plan/claude-code/* ~/.claude/skills/create-plan/
```bash
mkdir -p ~/.config/opencode/skills/create-plan
cp -R skills/create-plan/opencode/* ~/.config/opencode/skills/create-plan/
mkdir -p ~/.config/opencode/skills/reviewer-runtime
cp -R skills/reviewer-runtime/* ~/.config/opencode/skills/reviewer-runtime/
```
### Cursor
@@ -62,6 +73,8 @@ Copy into the repo-local `.cursor/skills/` directory (where the Cursor Agent CLI
```bash
mkdir -p .cursor/skills/create-plan
cp -R skills/create-plan/cursor/* .cursor/skills/create-plan/
mkdir -p .cursor/skills/reviewer-runtime
cp -R skills/reviewer-runtime/* .cursor/skills/reviewer-runtime/
```
Or install globally (loaded via `~/.cursor/skills/`):
@@ -69,6 +82,8 @@ Or install globally (loaded via `~/.cursor/skills/`):
```bash
mkdir -p ~/.cursor/skills/create-plan
cp -R skills/create-plan/cursor/* ~/.cursor/skills/create-plan/
mkdir -p ~/.cursor/skills/reviewer-runtime
cp -R skills/reviewer-runtime/* ~/.cursor/skills/reviewer-runtime/
```
## Verify Installation
@@ -78,6 +93,10 @@ test -f ~/.codex/skills/create-plan/SKILL.md || true
test -f ~/.claude/skills/create-plan/SKILL.md || true
test -f ~/.config/opencode/skills/create-plan/SKILL.md || true
test -f .cursor/skills/create-plan/SKILL.md || test -f ~/.cursor/skills/create-plan/SKILL.md || true
test -x ~/.codex/skills/reviewer-runtime/run-review.sh || true
test -x ~/.claude/skills/reviewer-runtime/run-review.sh || true
test -x ~/.config/opencode/skills/reviewer-runtime/run-review.sh || true
test -x .cursor/skills/reviewer-runtime/run-review.sh || test -x ~/.cursor/skills/reviewer-runtime/run-review.sh || true
```
Verify Superpowers dependencies exist in your agent skills root:

View File

@@ -20,6 +20,11 @@ Execute an existing plan (created by `create-plan`) in an isolated git worktree,
- For Codex, native skill discovery must be configured:
- `~/.agents/skills/superpowers -> ~/.codex/superpowers/skills`
- For Cursor, skills must be installed under `.cursor/skills/` (repo-local) or `~/.cursor/skills/` (global)
- Shared reviewer runtime must be installed beside agent skills when using reviewer CLIs:
- Codex: `~/.codex/skills/reviewer-runtime/run-review.sh`
- Claude Code: `~/.claude/skills/reviewer-runtime/run-review.sh`
- OpenCode: `~/.config/opencode/skills/reviewer-runtime/run-review.sh`
- Cursor: `.cursor/skills/reviewer-runtime/run-review.sh` or `~/.cursor/skills/reviewer-runtime/run-review.sh`
If dependencies are missing, stop and return:
@@ -46,6 +51,8 @@ The reviewer CLI is independent of which agent is running the implementation —
```bash
mkdir -p ~/.codex/skills/implement-plan
cp -R skills/implement-plan/codex/* ~/.codex/skills/implement-plan/
mkdir -p ~/.codex/skills/reviewer-runtime
cp -R skills/reviewer-runtime/* ~/.codex/skills/reviewer-runtime/
```
### Claude Code
@@ -53,6 +60,8 @@ cp -R skills/implement-plan/codex/* ~/.codex/skills/implement-plan/
```bash
mkdir -p ~/.claude/skills/implement-plan
cp -R skills/implement-plan/claude-code/* ~/.claude/skills/implement-plan/
mkdir -p ~/.claude/skills/reviewer-runtime
cp -R skills/reviewer-runtime/* ~/.claude/skills/reviewer-runtime/
```
### OpenCode
@@ -60,6 +69,8 @@ cp -R skills/implement-plan/claude-code/* ~/.claude/skills/implement-plan/
```bash
mkdir -p ~/.config/opencode/skills/implement-plan
cp -R skills/implement-plan/opencode/* ~/.config/opencode/skills/implement-plan/
mkdir -p ~/.config/opencode/skills/reviewer-runtime
cp -R skills/reviewer-runtime/* ~/.config/opencode/skills/reviewer-runtime/
```
### Cursor
@@ -69,6 +80,8 @@ Copy into the repo-local `.cursor/skills/` directory (where the Cursor Agent CLI
```bash
mkdir -p .cursor/skills/implement-plan
cp -R skills/implement-plan/cursor/* .cursor/skills/implement-plan/
mkdir -p .cursor/skills/reviewer-runtime
cp -R skills/reviewer-runtime/* .cursor/skills/reviewer-runtime/
```
Or install globally (loaded via `~/.cursor/skills/`):
@@ -76,6 +89,8 @@ Or install globally (loaded via `~/.cursor/skills/`):
```bash
mkdir -p ~/.cursor/skills/implement-plan
cp -R skills/implement-plan/cursor/* ~/.cursor/skills/implement-plan/
mkdir -p ~/.cursor/skills/reviewer-runtime
cp -R skills/reviewer-runtime/* ~/.cursor/skills/reviewer-runtime/
```
## Verify Installation
@@ -85,6 +100,10 @@ test -f ~/.codex/skills/implement-plan/SKILL.md || true
test -f ~/.claude/skills/implement-plan/SKILL.md || true
test -f ~/.config/opencode/skills/implement-plan/SKILL.md || true
test -f .cursor/skills/implement-plan/SKILL.md || test -f ~/.cursor/skills/implement-plan/SKILL.md || true
test -x ~/.codex/skills/reviewer-runtime/run-review.sh || true
test -x ~/.claude/skills/reviewer-runtime/run-review.sh || true
test -x ~/.config/opencode/skills/reviewer-runtime/run-review.sh || true
test -x .cursor/skills/reviewer-runtime/run-review.sh || test -x ~/.cursor/skills/reviewer-runtime/run-review.sh || true
```
Verify Superpowers execution dependencies exist in your agent skills root:

View File

@@ -0,0 +1,303 @@
#!/usr/bin/env bash
set -euo pipefail
DEFAULT_POLL_SECONDS=10
DEFAULT_SOFT_TIMEOUT_SECONDS=600
DEFAULT_STALL_WARNING_SECONDS=300
DEFAULT_HARD_TIMEOUT_SECONDS=1800
COMMAND_FILE=""
STDOUT_FILE=""
STDERR_FILE=""
STATUS_FILE=""
POLL_SECONDS=$DEFAULT_POLL_SECONDS
SOFT_TIMEOUT_SECONDS=$DEFAULT_SOFT_TIMEOUT_SECONDS
STALL_WARNING_SECONDS=$DEFAULT_STALL_WARNING_SECONDS
HARD_TIMEOUT_SECONDS=$DEFAULT_HARD_TIMEOUT_SECONDS
CHILD_PID=""
USE_GROUP_KILL=0
INTERRUPTED=0
usage() {
cat <<'EOF'
Usage:
run-review.sh \
--command-file <path> \
--stdout-file <path> \
--stderr-file <path> \
--status-file <path> \
[--poll-seconds <int>] \
[--soft-timeout-seconds <int>] \
[--stall-warning-seconds <int>] \
[--hard-timeout-seconds <int>]
EOF
}
fail_usage() {
echo "Error: $*" >&2
usage >&2
exit 2
}
require_integer() {
local name=$1
local value=$2
[[ "$value" =~ ^[0-9]+$ ]] || fail_usage "$name must be an integer"
}
escape_note() {
local note=$1
note=${note//$'\n'/ }
note=${note//\"/\'}
printf '%s' "$note"
}
iso_timestamp() {
date -u +"%Y-%m-%dT%H:%M:%SZ"
}
elapsed_seconds() {
local now
now=$(date +%s)
printf '%s' $((now - START_TIME))
}
file_bytes() {
local path=$1
if [[ -f "$path" ]]; then
wc -c <"$path" | tr -d '[:space:]'
else
printf '0'
fi
}
append_status() {
local level=$1
local state=$2
local note=$3
local elapsed pid stdout_bytes stderr_bytes line
elapsed=$(elapsed_seconds)
pid=${CHILD_PID:-0}
stdout_bytes=$(file_bytes "$STDOUT_FILE")
stderr_bytes=$(file_bytes "$STDERR_FILE")
line="ts=$(iso_timestamp) level=$level state=$state elapsed_s=$elapsed pid=$pid stdout_bytes=$stdout_bytes stderr_bytes=$stderr_bytes note=\"$(escape_note "$note")\""
printf '%s\n' "$line" | tee -a "$STATUS_FILE"
}
ensure_parent_dir() {
local path=$1
mkdir -p "$(dirname "$path")"
}
kill_child_process_group() {
if [[ -z "$CHILD_PID" ]]; then
return 0
fi
if ! kill -0 "$CHILD_PID" 2>/dev/null; then
return 0
fi
if [[ "$USE_GROUP_KILL" -eq 1 ]]; then
kill -TERM -- "-$CHILD_PID" 2>/dev/null || kill -TERM "$CHILD_PID" 2>/dev/null || true
else
kill -TERM "$CHILD_PID" 2>/dev/null || true
fi
sleep 1
if kill -0 "$CHILD_PID" 2>/dev/null; then
if [[ "$USE_GROUP_KILL" -eq 1 ]]; then
kill -KILL -- "-$CHILD_PID" 2>/dev/null || kill -KILL "$CHILD_PID" 2>/dev/null || true
else
kill -KILL "$CHILD_PID" 2>/dev/null || true
fi
fi
}
handle_signal() {
local signal_name=$1
INTERRUPTED=1
append_status error failed "received SIG${signal_name}; terminating reviewer child"
kill_child_process_group
exit 130
}
parse_args() {
while [[ $# -gt 0 ]]; do
case "$1" in
--command-file)
COMMAND_FILE=${2:-}
shift 2
;;
--stdout-file)
STDOUT_FILE=${2:-}
shift 2
;;
--stderr-file)
STDERR_FILE=${2:-}
shift 2
;;
--status-file)
STATUS_FILE=${2:-}
shift 2
;;
--poll-seconds)
POLL_SECONDS=${2:-}
shift 2
;;
--soft-timeout-seconds)
SOFT_TIMEOUT_SECONDS=${2:-}
shift 2
;;
--stall-warning-seconds)
STALL_WARNING_SECONDS=${2:-}
shift 2
;;
--hard-timeout-seconds)
HARD_TIMEOUT_SECONDS=${2:-}
shift 2
;;
--help|-h)
usage
exit 0
;;
*)
fail_usage "unknown argument: $1"
;;
esac
done
[[ -n "$COMMAND_FILE" ]] || fail_usage "--command-file is required"
[[ -n "$STDOUT_FILE" ]] || fail_usage "--stdout-file is required"
[[ -n "$STDERR_FILE" ]] || fail_usage "--stderr-file is required"
[[ -n "$STATUS_FILE" ]] || fail_usage "--status-file is required"
require_integer "poll-seconds" "$POLL_SECONDS"
require_integer "soft-timeout-seconds" "$SOFT_TIMEOUT_SECONDS"
require_integer "stall-warning-seconds" "$STALL_WARNING_SECONDS"
require_integer "hard-timeout-seconds" "$HARD_TIMEOUT_SECONDS"
[[ "$POLL_SECONDS" -gt 0 ]] || fail_usage "poll-seconds must be > 0"
[[ "$SOFT_TIMEOUT_SECONDS" -gt 0 ]] || fail_usage "soft-timeout-seconds must be > 0"
[[ "$STALL_WARNING_SECONDS" -gt 0 ]] || fail_usage "stall-warning-seconds must be > 0"
[[ "$HARD_TIMEOUT_SECONDS" -gt 0 ]] || fail_usage "hard-timeout-seconds must be > 0"
[[ "$SOFT_TIMEOUT_SECONDS" -le "$HARD_TIMEOUT_SECONDS" ]] || fail_usage "soft-timeout-seconds must be <= hard-timeout-seconds"
[[ "$STALL_WARNING_SECONDS" -le "$HARD_TIMEOUT_SECONDS" ]] || fail_usage "stall-warning-seconds must be <= hard-timeout-seconds"
[[ -r "$COMMAND_FILE" ]] || fail_usage "command file is not readable: $COMMAND_FILE"
}
launch_child() {
if command -v setsid >/dev/null 2>&1; then
setsid bash "$COMMAND_FILE" >"$STDOUT_FILE" 2>"$STDERR_FILE" &
USE_GROUP_KILL=1
else
bash "$COMMAND_FILE" >"$STDOUT_FILE" 2>"$STDERR_FILE" &
USE_GROUP_KILL=0
fi
CHILD_PID=$!
}
main() {
parse_args "$@"
ensure_parent_dir "$STDOUT_FILE"
ensure_parent_dir "$STDERR_FILE"
ensure_parent_dir "$STATUS_FILE"
: >"$STDOUT_FILE"
: >"$STDERR_FILE"
: >"$STATUS_FILE"
START_TIME=$(date +%s)
export START_TIME
trap 'handle_signal INT' INT
trap 'handle_signal TERM' TERM
trap 'if [[ "$INTERRUPTED" -eq 0 ]]; then kill_child_process_group; fi' EXIT
launch_child
append_status info running-silent "reviewer child launched"
local last_stdout_bytes=0
local last_stderr_bytes=0
local last_output_change_time=$START_TIME
local soft_timeout_logged=0
local stall_warning_logged=0
while kill -0 "$CHILD_PID" 2>/dev/null; do
sleep "$POLL_SECONDS"
local now elapsed stdout_bytes stderr_bytes note state level
now=$(date +%s)
elapsed=$((now - START_TIME))
stdout_bytes=$(file_bytes "$STDOUT_FILE")
stderr_bytes=$(file_bytes "$STDERR_FILE")
if [[ "$stdout_bytes" -ne "$last_stdout_bytes" || "$stderr_bytes" -ne "$last_stderr_bytes" ]]; then
last_output_change_time=$now
stall_warning_logged=0
state=running-active
level=info
note="reviewer output changed"
else
local silent_for
silent_for=$((now - last_output_change_time))
if [[ "$silent_for" -ge "$STALL_WARNING_SECONDS" ]]; then
state=stall-warning
level=warn
note="no output growth for ${silent_for}s; process still alive"
stall_warning_logged=1
else
state=running-silent
level=info
note="reviewer process alive; waiting for output"
fi
fi
if [[ "$elapsed" -ge "$SOFT_TIMEOUT_SECONDS" && "$soft_timeout_logged" -eq 0 ]]; then
note="$note; soft timeout reached, continuing while reviewer is alive"
soft_timeout_logged=1
fi
append_status "$level" "$state" "$note"
last_stdout_bytes=$stdout_bytes
last_stderr_bytes=$stderr_bytes
if [[ "$elapsed" -ge "$HARD_TIMEOUT_SECONDS" ]]; then
append_status error needs-operator-decision "hard timeout reached; terminating reviewer child for operator intervention"
kill_child_process_group
trap - EXIT
exit 12
fi
done
local child_exit_code=0
set +e
wait "$CHILD_PID"
child_exit_code=$?
set -e
trap - EXIT
local final_stdout_bytes final_stderr_bytes
final_stdout_bytes=$(file_bytes "$STDOUT_FILE")
final_stderr_bytes=$(file_bytes "$STDERR_FILE")
if [[ "$child_exit_code" -eq 0 ]]; then
if [[ "$final_stdout_bytes" -gt 0 ]]; then
append_status info completed "reviewer completed successfully"
exit 0
fi
append_status error completed-empty-output "reviewer exited successfully with empty stdout"
exit 10
fi
append_status error failed "reviewer exited with code $child_exit_code"
exit "$child_exit_code"
}
main "$@"

View File

@@ -0,0 +1,237 @@
#!/usr/bin/env bash
set -euo pipefail
SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)
HELPER_PATH=$(cd "$SCRIPT_DIR/.." && pwd)/run-review.sh
fail() {
echo "FAIL: $*" >&2
exit 1
}
assert_file_contains() {
local file=$1
local pattern=$2
if ! rg -q --fixed-strings "$pattern" "$file"; then
echo "Expected pattern not found: $pattern" >&2
echo "--- $file ---" >&2
sed -n '1,200p' "$file" >&2 || true
fail "missing pattern in $file"
fi
}
assert_exit_code() {
local actual=$1
local expected=$2
if [[ "$actual" -ne "$expected" ]]; then
fail "expected exit code $expected, got $actual"
fi
}
assert_nonzero_exit() {
local actual=$1
if [[ "$actual" -eq 0 ]]; then
fail "expected non-zero exit code"
fi
}
make_command() {
local file=$1
local body=$2
cat >"$file" <<EOF
#!/usr/bin/env bash
set -euo pipefail
$body
EOF
chmod +x "$file"
}
run_helper() {
local command_file=$1
local stdout_file=$2
local stderr_file=$3
local status_file=$4
shift 4
set +e
"$HELPER_PATH" \
--command-file "$command_file" \
--stdout-file "$stdout_file" \
--stderr-file "$stderr_file" \
--status-file "$status_file" \
"$@"
local exit_code=$?
set -e
return "$exit_code"
}
test_delayed_success() {
local dir=$1
local command_file=$dir/delayed-success.sh
local stdout_file=$dir/delayed-success.stdout
local stderr_file=$dir/delayed-success.stderr
local status_file=$dir/delayed-success.status
make_command "$command_file" '
sleep 2
printf "VERDICT: APPROVED\n"
'
if run_helper "$command_file" "$stdout_file" "$stderr_file" "$status_file" \
--poll-seconds 1 \
--soft-timeout-seconds 5 \
--stall-warning-seconds 3 \
--hard-timeout-seconds 10; then
local exit_code=0
else
local exit_code=$?
fi
assert_exit_code "$exit_code" 0
assert_file_contains "$stdout_file" "VERDICT: APPROVED"
assert_file_contains "$status_file" "state=completed"
}
test_soft_timeout_continues() {
local dir=$1
local command_file=$dir/soft-timeout.sh
local stdout_file=$dir/soft-timeout.stdout
local stderr_file=$dir/soft-timeout.stderr
local status_file=$dir/soft-timeout.status
make_command "$command_file" '
sleep 3
printf "completed after soft timeout\n"
'
if run_helper "$command_file" "$stdout_file" "$stderr_file" "$status_file" \
--poll-seconds 1 \
--soft-timeout-seconds 1 \
--stall-warning-seconds 2 \
--hard-timeout-seconds 8; then
local exit_code=0
else
local exit_code=$?
fi
assert_exit_code "$exit_code" 0
assert_file_contains "$stdout_file" "completed after soft timeout"
assert_file_contains "$status_file" "state=completed"
}
test_nonzero_failure() {
local dir=$1
local command_file=$dir/nonzero-failure.sh
local stdout_file=$dir/nonzero-failure.stdout
local stderr_file=$dir/nonzero-failure.stderr
local status_file=$dir/nonzero-failure.status
make_command "$command_file" '
printf "boom\n" >&2
exit 7
'
if run_helper "$command_file" "$stdout_file" "$stderr_file" "$status_file" \
--poll-seconds 1 \
--soft-timeout-seconds 5 \
--stall-warning-seconds 3 \
--hard-timeout-seconds 10; then
local exit_code=0
else
local exit_code=$?
fi
assert_nonzero_exit "$exit_code"
assert_file_contains "$stderr_file" "boom"
assert_file_contains "$status_file" "state=failed"
}
test_empty_output_is_terminal() {
local dir=$1
local command_file=$dir/empty-output.sh
local stdout_file=$dir/empty-output.stdout
local stderr_file=$dir/empty-output.stderr
local status_file=$dir/empty-output.status
make_command "$command_file" '
sleep 1
exit 0
'
if run_helper "$command_file" "$stdout_file" "$stderr_file" "$status_file" \
--poll-seconds 1 \
--soft-timeout-seconds 5 \
--stall-warning-seconds 3 \
--hard-timeout-seconds 10; then
local exit_code=0
else
local exit_code=$?
fi
assert_nonzero_exit "$exit_code"
assert_file_contains "$status_file" "state=completed-empty-output"
}
test_signal_cleanup() {
local dir=$1
local command_file=$dir/signal-child.sh
local stdout_file=$dir/signal-child.stdout
local stderr_file=$dir/signal-child.stderr
local status_file=$dir/signal-child.status
local child_pid_file=$dir/child.pid
make_command "$command_file" "
printf '%s\n' \"\$\$\" > \"$child_pid_file\"
sleep 30
"
set +e
"$HELPER_PATH" \
--command-file "$command_file" \
--stdout-file "$stdout_file" \
--stderr-file "$stderr_file" \
--status-file "$status_file" \
--poll-seconds 1 \
--soft-timeout-seconds 5 \
--stall-warning-seconds 2 \
--hard-timeout-seconds 10 &
local helper_pid=$!
set -e
sleep 2
kill -TERM "$helper_pid"
set +e
wait "$helper_pid"
local exit_code=$?
set -e
assert_nonzero_exit "$exit_code"
[[ -f "$child_pid_file" ]] || fail "child pid file was not written"
local child_pid
child_pid=$(cat "$child_pid_file")
sleep 1
if kill -0 "$child_pid" 2>/dev/null; then
fail "child process is still alive after helper termination"
fi
}
main() {
[[ -x "$HELPER_PATH" ]] || fail "helper is not executable: $HELPER_PATH"
local tmp_dir
tmp_dir=$(mktemp -d)
trap "rm -rf '$tmp_dir'" EXIT
test_delayed_success "$tmp_dir"
test_soft_timeout_continues "$tmp_dir"
test_nonzero_failure "$tmp_dir"
test_empty_output_is_terminal "$tmp_dir"
test_signal_cleanup "$tmp_dir"
echo "PASS: reviewer runtime smoke tests"
}
main "$@"