/review-loop-author — Author-Side Review Loop
Hybrid architecture: the leader handles all Bash I/O (inbox, git, gh, polling), spawns a fix-only subagent (Read/Edit/Write — no Bash) to address findings, then the leader commits, pushes, and sends messages.
This works around runtime restrictions where the subagent's shell permissions can't be widened to match the leader's — the split keeps state-changing operations on the leader, where runtime permissions are configured.
Arguments
/review-loop-author <PR_NUMBER> --reviewer <name> [--project <name>] [--task-id <id>] [--timeout 15] [--max-rounds 3] [--poll] [--dry-run]
<PR_NUMBER>— required. The GitHub PR number.--reviewer <name>— required. Reviewer agent name (no default — caller must be explicit).--project <name>— optional. Auto-detected from.oacpif omitted.--task-id <id>— optional. Enables task-aware message metadata and task-state updates.--timeout <minutes>— optional. Poll timeout in minutes (default: 15).--max-rounds <N>— optional. Max review rounds before escalation (default: 3).--dry-run— optional. Show plan without spawning.--poll— optional. Enable in-session polling for reviewer feedback (Step 6). Without this flag, the skill exits after sending messages (single-pass mode for event-driven dispatch via/check-inbox).
Instructions
When the user runs /review-loop-author, do the following:
1. Parse arguments
Extract from the user's command:
PR_NUMBER— required, first positional arg. Error if missing.REVIEWER— required--reviewerflag. Error if missing with message: "Missing required --reviewer flag. Usage: /review-loop-author --reviewer "PROJECT— optional--projectflag (auto-detected in step 2 if omitted)TASK_ID— optional--task-idflag (default empty)TIMEOUT— optional--timeoutflag, default15MAX_ROUNDS— optional--max-roundsflag, default3DRY_RUN— optional--dry-runflagPOLL— optional--pollflag
2. Gather context
BRANCH=$(git -C <cwd> rev-parse --abbrev-ref HEAD)
REPO=$(gh repo view --json nameWithOwner -q .nameWithOwner 2>/dev/null || echo "unknown")
REPO_PATH=$(git -C <cwd> rev-parse --show-toplevel)
Detect project name (if --project was not provided):
PROJECT=$(python3 -c "import json; print(json.load(open('${REPO_PATH}/.oacp'))['project_name'])" 2>/dev/null || basename "${REPO_PATH}")
Set derived paths:
OACP_HOME="${OACP_HOME:-$HOME/oacp}"
INBOX_DIR="${OACP_HOME}/projects/${PROJECT}/agents/claude/inbox"
Define the terminal-archival helper here, before any routing — the LGTM-first route (Step 4 → Step 10) never passes through Step 9, so a definition placed later would leave Step 10's invocation undefined in a fresh shell. Every guard fails closed (a failed check returns without moving anything, and the message stays pending in inbox/); the archive/ directory is provisioned by workspace init/migration, never created during message processing:
oacp_archive() { # oacp_archive <inbox_dir> <filename> <accepted_sha256>
local d="$1" f="$2" want="$3" live arch
[ -d "$d/archive" ] && [ ! -L "$d/archive" ] \
|| { echo "RETAINED: archive/ missing or symlinked — provision via workspace migration"; return 1; }
[ -f "$d/$f" ] && [ ! -L "$d/$f" ] \
|| { echo "RETAINED: source missing or not a regular file"; return 1; }
live=$(shasum -a 256 "$d/$f" | awk '{print $1}') \
|| { echo "RETAINED: digest read failed"; return 1; }
[ "$live" = "$want" ] \
|| { echo "RETAINED: digest drift — re-verify before any further processing"; return 1; }
[ ! -e "$d/archive/$f" ] && [ ! -L "$d/archive/$f" ] \
|| { echo "RETAINED: destination exists — never overwrite history"; return 1; }
mv -n "$d/$f" "$d/archive/$f" \
|| { echo "RETAINED: move failed"; return 1; }
[ ! -e "$d/$f" ] && [ ! -L "$d/$f" ] \
|| { echo "ERROR: source path still present after move (skipped move or concurrent re-creation) — inspect before retry"; return 1; }
arch=$(shasum -a 256 "$d/archive/$f" 2>/dev/null | awk '{print $1}')
[ -f "$d/archive/$f" ] && [ ! -L "$d/archive/$f" ] && [ "$arch" = "$want" ] \
|| { echo "ERROR: archived copy missing or digest mismatch — inspect before retry"; return 1; }
echo "ARCHIVED: $d/archive/$f"
}
The post-move source check requires both ! -e and ! -L — a concurrent actor re-creating the original inbox pathname as a dangling symlink passes a bare ! -e test, and reporting success then would leave an untrusted pathname behind a claimed-clean archival.
3. Check preconditions
Verify all of the following. Report failures and stop:
- PR exists:
gh pr view <PR_NUMBER> --repo <REPO>succeeds - Current branch matches the PR's head branch
- PR is mergeable:
gh pr view <PR_NUMBER> --repo <REPO> --json mergeablemust showMERGEABLE, notCONFLICTING. If conflicting, rebase onto latest base branch (git fetch origin main && git rebase origin/main), resolve conflicts, and force-push (git push --force-with-lease) before requesting review. A post-review force-push dismisses the reviewer's approval and wastes a review cycle. - Inbox directory exists:
${INBOX_DIR} oacp sendis available:oacp send --helpsucceeds- gh is authenticated:
gh auth statussucceeds
4. Check for existing feedback (leader pre-scan)
Before sending a review request, check if the reviewer has already submitted feedback:
command ls -1 "${INBOX_DIR}/" 2>/dev/null | command grep '\.yaml$'
Look for YAML files where from matches <REVIEWER> and related_pr matches <PR_NUMBER>. Read matching files with the Read tool.
- If
review_lgtmfound → skip to Step 10 (handle LGTM) - If
review_feedbackfound → skip to Step 7 (parse feedback) - If nothing found → proceed to Step 5
Also check GitHub for out-of-band feedback:
gh api repos/<REPO>/issues/<PR_NUMBER>/comments --jq '.[].body' 2>/dev/null
gh api repos/<REPO>/pulls/<PR_NUMBER>/comments --jq '.[].body' 2>/dev/null
gh api repos/<REPO>/pulls/<PR_NUMBER>/reviews --jq '.[] | "\(.state): \(.body)"' 2>/dev/null
If GitHub shows new actionable feedback not in inbox, note it for later.
5. Show plan and send review request
Display to the user:
Review Loop — Author Side (Hybrid Split)
PR: #<PR_NUMBER> (<REPO>)
Branch: <BRANCH>
Reviewer: <REVIEWER>
Project: <PROJECT>
Task ID: <TASK_ID or empty>
Inbox: <INBOX_DIR>
Timeout: <TIMEOUT> minutes
Max rounds: <MAX_ROUNDS>
If --dry-run, stop here.
Generate a diff summary:
git -C <REPO_PATH> diff main...<BRANCH> --stat
Write a concise 2-4 line summary. Establish the review thread's explicit conversation identity — every message in this loop carries it, and continuation-grant matching (v0.4.3) binds to it. The value is validator-enforced: it must match the protocol schema conv-<YYYYMMDD>-<agent>-<seq> (regex ^conv-\d{8}-[A-Za-z0-9._-]{1,64}-\d{1,6}$) — UTC date, then the originating agent, then a numeric sequence unique to this thread. The PR number is a natural sequence; if a second thread for the same PR starts on the same UTC day, pick a fresh sequence instead of reusing it:
CONV_ID="conv-$(date -u +%Y%m%d)-claude-<PR_NUMBER>"
Then send the review request:
oacp send <PROJECT> \
--from claude --to <REVIEWER> --type review_request \
--subject "Review: PR #<PR_NUMBER>" \
--conversation-id "${CONV_ID}" \
--body "pr: <PR_NUMBER>
repo: <REPO>
branch: <BRANCH>
declared_head: $(git -C <REPO_PATH> rev-parse <BRANCH>)
diff_summary: |
<DIFF_SUMMARY>
task_id: <TASK_ID>
round: 1
review_round: 1" \
--related-pr <PR_NUMBER> --priority P1 \
--oacp-dir "${OACP_HOME}"
This round-1 message starts the review thread — record its msg-id as ROUND1_REQUEST_MSG_ID. The continuation evaluator matches same-thread evidence by equal conversation_id or by a direct parent equal to the audited round-1 request's msg-id — it does not traverse a chain of parents, and --in-reply-to cannot inherit a conversation from a message that has already been archived out of the live inbox. The explicit --conversation-id on every loop message is therefore the load-bearing thread identity; --in-reply-to is kept for logical reply threading. repo and the canonical round field are required for fail-closed grant matching; review_round is kept as a legacy alias.
declared_head is advisory context for the reviewer (they bind their verdict to the live ref they fetch, not to your declaration) — take it verbatim from rev-parse, never hand-typed.
Post a PR comment for human visibility (status only — no diff details):
COMMENT_FILE="$(mktemp)"
cat > "${COMMENT_FILE}" <<EOF
**Review requested** - claude -> <REVIEWER>
Round: 1
Scope: PR #<PR_NUMBER>
Details delivered via inbox review_request message.
EOF
gh pr comment <PR_NUMBER> --repo <REPO> --body-file "${COMMENT_FILE}"
rm -f "${COMMENT_FILE}"
Initialize: current_round = 1.
6. Poll for feedback (leader polling loop)
Single-pass mode (default): If
--pollwas NOT provided, skip this step entirely. The skill exits after Step 9 (commit/push/send messages). Subsequent invocations — dispatched by/check-inboxwhenreview_feedbackarrives — will detect the feedback in Step 4 and skip directly to Step 7.
If --poll was provided, poll the inbox for a response from the reviewer. On each iteration:
command ls -1 "${INBOX_DIR}/" 2>/dev/null | command grep '\.yaml$'
Look for YAML files where from matches <REVIEWER>, related_pr matches <PR_NUMBER>, and type is review_feedback or review_lgtm. Read matching files with the Read tool.
review_lgtmfound → Go to Step 10review_feedbackfound → Go to Step 7- Nothing found → Check if elapsed time exceeds
<TIMEOUT>minutes. If yes → Go to Step 12 (escalation). Otherwise sleep 30 seconds and repeat.
7. Parse review feedback (leader reads findings)
From the review_feedback message, extract from the body:
findings_packet— path to the findings YAML (relative to project workspace)round— current review round numberblocking_count— number of blocking findingstask_id— optional task identifier (fallback to<TASK_ID>)review_round— optional round field (fallback toround)
Check: if body contains escalation: max_rounds_exceeded → Go to Step 12.
Read the findings packet:
cat "${OACP_HOME}/projects/<PROJECT>/<findings_packet>"
Store the full findings YAML content as FINDINGS_CONTENT.
Retain the review_feedback message in the inbox for now — record its filename and accepted SHA (ACCEPTED_SHA=$(shasum -a 256 "${INBOX_DIR}/<message_filename>" | awk '{print $1}')). It is archived only in Step 9, after both outbound messages (review_addressed and the round-N+1 review_request) have been sent successfully — a crash before that leaves it pending for clean re-dispatch. Never plain-rm it.
If task_id is available, update task review fields:
review_round: parsed valuereview_status:needs_changesfindings_packets: append path
8. Spawn fix subagent (code edits only)
Use the Task tool to spawn a general-purpose subagent with run_in_background: true. Use model sonnet.
IMPORTANT: The subagent uses Read, Edit, Write, and Bash (for search only — command rg, command fd, command ls). Do not let it use Bash for git, gh, or anything that writes state — the leader handles commits, pushes, and messages.
The subagent prompt MUST be exactly the template below with variables substituted:
You are the fix subagent for PR #<PR_NUMBER> on repo <REPO>.
Your ONLY job: read the review findings, edit source files to address them, and output a summary of what you did. You receive all findings data pre-gathered.
CRITICAL: Use Read (to examine source files), Edit (to fix code), Write (to create new files if needed), and Bash only for search (`command rg`, `command fd`, `command ls`). Do NOT use Bash for git commits, pushes, gh operations, or sending messages — the leader handles all state changes. (Glob and Grep tools no longer exist on native Claude Code builds since v2.1.117.)
## Context
- PR number: <PR_NUMBER>
- Branch: <BRANCH>
- Repo: <REPO>
- Repo path: <REPO_PATH>
- Round: <current_round>
## Findings to Address
```yaml
<FINDINGS_CONTENT>
```
## Instructions
### Step 1: Prioritize findings
Extract all findings where `status: open`. Sort by priority:
1. P0 blocking — critical, must fix
2. P1 blocking — major, must fix
3. Non-blocking P0/P1 — important but not merge-blockers
4. P2/P3 — minor issues and nits
### Step 2: Address each finding
For each open finding, in priority order:
1. Read the referenced file using the Read tool
2. Determine action:
- **Fix** — if the finding is valid and the fix is clear, use Edit to apply it
- **Push back** — if the finding is incorrect or based on a misunderstanding, note it in your output (the leader will send a question to the reviewer)
- **Defer** — if out of scope for this PR, note it in your output
3. If fixing: use the Edit tool to modify the file. Make minimal, focused changes that address the finding without introducing new issues.
### Step 3: Output fix summary
As the LAST part of your output, print a structured summary block exactly like this:
```
---FIX_SUMMARY---
fixed:
- id: "F-001"
description: "<what was changed>"
files: ["<path/to/file>"]
- id: "F-003"
description: "<what was changed>"
files: ["<path/to/file>"]
deferred:
- id: "F-002"
reason: "<why deferred>"
pushed_back:
- id: "F-004"
reason: "<why this finding is incorrect>"
files_modified:
- "<path/to/file1>"
- "<path/to/file2>"
---END_FIX_SUMMARY---
```
This block is parsed by the leader. Do not deviate from the format. If there are no deferred or pushed_back items, use empty lists.
9. Commit, push, and send review_addressed (leader post-process)
After the subagent completes:
Authenticate for git/gh operations. If the repo requires GitHub App auth (e.g., to attribute commits to a bot identity), generate and use the short-lived app token exactly as that repo specifies. Export
GH_TOKENforghcommands and inject auth per-command viahttp.<host>.extraheaderforgit push— never persist the token in the remote URL. Fall back to verified human auth only after a concrete app failure. Example pattern:git remote set-url origin https://github.com/<ORG>/<REPO>.git AUTH=$(printf 'x-access-token:%s' "$TOKEN" | base64 | tr -d '\n') git -c http.https://github.com/.extraheader="AUTHORIZATION: basic ${AUTH}" \ push https://github.com/<ORG>/<REPO>.git <branch>Parse the fix summary from the subagent's output. Look for the
---FIX_SUMMARY---block and extractfixed,deferred,pushed_back,files_modified.Review changes — run
git diffin the repo to confirm the edits look correct.Stage and commit the modified files:
cd <REPO_PATH> git add <files_modified> git commit -m "$(cat <<'EOF' Address review findings (round <current_round>) <list of F-xxx: brief description for each fixed finding> Co-Authored-By: Claude <noreply@anthropic.com> EOF )"Push:
git push origin <BRANCH>(Optional) If
eval_fix.pyis available locally, run a quick heuristic check on the findings packet. If any findings are "unaddressed", investigate.Handle push-backs — if the subagent flagged any findings as
pushed_back, send a question to the reviewer for each:oacp send <PROJECT> \ --from claude --to <REVIEWER> --type question \ --subject "Question re: <finding_id> (#<PR_NUMBER>)" \ --body "<push_back_reason>" \ --related-pr <PR_NUMBER> --priority P2 \ --oacp-dir "${OACP_HOME}"Build changes summary from the fixed/deferred/pushed_back lists.
Send review_addressed:
oacp send <PROJECT> \ --from claude --to <REVIEWER> --type review_addressed \ --subject "Feedback addressed: round <current_round> (#<PR_NUMBER>)" \ --conversation-id "${CONV_ID}" \ --in-reply-to <FEEDBACK_MSG_ID> \ --body "commit_sha: <LATEST_COMMIT_SHA> changes_summary: | <CHANGES_SUMMARY> round: <current_round> task_id: <TASK_ID> review_round: <current_round>" \ --related-pr <PR_NUMBER> --priority P1 \ --oacp-dir "${OACP_HOME}"Post PR comment for human visibility (status only — no changes details):
COMMENT_FILE="$(mktemp)" cat > "${COMMENT_FILE}" <<EOF **Feedback addressed (round <current_round>)** - claude Status: updates pushed for re-review. Details delivered via inbox review_addressed message. EOF GH_TOKEN="${TOKEN}" gh pr comment <PR_NUMBER> --repo <REPO> --body-file "${COMMENT_FILE}" rm -f "${COMMENT_FILE}"Send review_request for round N+1: Send a new
review_requestto trigger re-review:oacp send <PROJECT> \ --from claude --to <REVIEWER> --type review_request \ --subject "Re-review: PR #<PR_NUMBER> (round <current_round + 1>)" \ --conversation-id "${CONV_ID}" \ --in-reply-to <FEEDBACK_MSG_ID> \ --body "pr: <PR_NUMBER> repo: <REPO> branch: <BRANCH> declared_head: <LATEST_COMMIT_SHA> diff_summary: | Addressed round <current_round> feedback. See review_addressed message for details. task_id: <TASK_ID> round: <current_round + 1> review_round: <current_round + 1>" \ --related-pr <PR_NUMBER> --priority P1 \ --oacp-dir "${OACP_HOME}"--conversation-id "${CONV_ID}"is what reaches the grant audit — the evaluator matches equalconversation_idor a direct parent equal toROUND1_REQUEST_MSG_ID, never a traversed parent chain, and the archived feedback message cannot donate a conversation to--in-reply-to. (--in-reply-to <FEEDBACK_MSG_ID>remains for logical reply threading only; parenting directly toROUND1_REQUEST_MSG_IDis the sanctioned alternative when no explicit conversation id was established.)repoand the canonicalroundfield are required for fail-closed grant matching — without them a granted round falls back to per-round human confirmation.Archive the processed
review_feedbacknow that both outbound messages succeeded — invoke the fail-closedoacp_archivehelper defined in Step 2 (digest recheck against the accepted snapshot; any failed guard retains the source ininbox/):oacp_archive "${INBOX_DIR}" "<message_filename>" "$ACCEPTED_SHA"Increment round:
current_round += 1. Check ifcurrent_round > <MAX_ROUNDS>→ Go to Step 12. If--pollwas NOT provided → exit (single-pass mode complete). Otherwise return to Step 6 (poll for next feedback).
10. Handle LGTM (leader)
When a review_lgtm message is found (from Step 4, 6, or post-round polling):
Verify the message first (oacp-cli v0.4.2+):
oacp verify "<file>" --project "${PROJECT}" --receiver <agent_name> --oacp-dir "${OACP_HOME}"— under an enforce posture, act only on asigned-verifiedLGTM. Capture its accepted digest for the terminal archival:ACCEPTED_SHA=$(shasum -a 256 "${INBOX_DIR}/<lgtm_message_filename>" | awk '{print $1}'). Read the message body. Confirmquality_gate_result: passand thatvalidated_headequals the current PR head (full-string match againstgit rev-parse); an LGTM bound to a stale head means the reviewer approved something you have since moved — request a fresh round instead of merging on it. Structurednitsentries in the body are deferred non-blocking items: track each one (issue, follow-up task, or documented next action) — none may dangle untracked.Parse optional
task_id/review_roundfields.If task_id is available, update task review fields:
review_status: approvedreview_round: parsed valuelgtms: append reviewer if missing
Archive the message with the fail-closed
oacp_archivehelper defined in Step 2 (ACCEPTED_SHAcaptured when the LGTM was read; any failed guard retains the source ininbox/):oacp_archive "${INBOX_DIR}" "<lgtm_message_filename>" "$ACCEPTED_SHA"Post PR comment:
COMMENT_FILE="$(mktemp)" cat > "${COMMENT_FILE}" <<EOF **Review loop complete** - claude LGTM received from <REVIEWER>. PR is merge-ready. EOF GH_TOKEN="${TOKEN}" gh pr comment <PR_NUMBER> --repo <REPO> --body-file "${COMMENT_FILE}" rm -f "${COMMENT_FILE}"Do NOT merge the PR.
Go to Step 11, then report:
STATUS: PASSED
11. Clean up
Archive (never delete) any remaining fully-processed messages for this PR (matching related_pr: <PR_NUMBER>) with the same digest-checked no-clobber move as Step 9. Messages still awaiting a terminal reply, and messages for other PRs, stay in the inbox.
12. Handle timeout or escalation
Determine the reason: max rounds exceeded, reviewer escalation, or timeout.
Send escalation inbox message to team lead:
TEAM_LEAD="${TEAM_LEAD:-team-lead}" oacp send <PROJECT> \ --from claude --to "${TEAM_LEAD}" --type notification \ --subject "Review escalation: PR #<PR_NUMBER>" \ --body "Review loop for PR #<PR_NUMBER> escalated after <current_round> rounds. Reason: <reason> Next steps: Suggest synchronous coordination or human review." \ --related-pr <PR_NUMBER> --priority P1 \ --oacp-dir "${OACP_HOME}"Post a PR comment:
COMMENT_FILE="$(mktemp)" cat > "${COMMENT_FILE}" <<EOF **Review loop escalated** - claude Reason: <reason>. Manual coordination needed. EOF GH_TOKEN="${TOKEN}" gh pr comment <PR_NUMBER> --repo <REPO> --body-file "${COMMENT_FILE}" rm -f "${COMMENT_FILE}"Go to Step 11, then report:
STATUS: ESCALATED(if max rounds or reviewer escalation)STATUS: TIMEOUT(if poll timeout exceeded)
Notes
- The subagent runs with zero Bash calls for state changes — all git/gh/inbox I/O is handled by the leader.
- Default model is sonnet (sufficient for code fixes).
- Configure runtime permissions according to local policy; the leader owns all state-changing shell operations.
- Idempotent sends: Before sending
review_request(Step 5) orreview_addressed(Step 9) after a crash/restart, check the reviewer's inbox and own outbox for an already-sent message matching this PR+round. Skip the send if a duplicate exists. - Single-pass is the default: The skill exits after Step 9 (commit/push/send messages). After sending
review_addressed, it also sends a newreview_request(round N+1) to trigger re-review via/check-inboxdispatch. Use--pollto revert to in-session polling (Step 6). - With
--poll, handles multi-round loops (up to max-rounds) autonomously. - Can be resumed by running the command again if timeout occurs (with or without
--poll). - The reviewer does not merge — the author (or human) decides when to merge.
PR Comment Data-Minimization Rule
- PR comments must stay concise and status-oriented.
- Store rich detail in protocol messages (
review_request,review_addressed) and findings packets, not in PR comments. - Never include command output, logs, stack traces, credentials, environment values, or local-only paths in PR comments.
- Always use
--body-filewith a temp file for PR comments — avoids shell expansion leaking sensitive data topsoutput.