Nickalas Light @ nickalaslight.com

Code-Gated PR Review Loops for Multi-Agent Workflows

Jul 2 · 9min

Prompting an autonomous coding subagent to “always open a pull request, address review comments, and never merge your own branch” works until context pressure rises. Eventually an agent forgets, stops early with uncommitted edits sitting in a worktree, or skips a requested change after a compaction boundary.

Both fable-sonnet-orchestrator-kit and claude-upgrades solve this by moving the workflow gate out of the LLM prompt and into deterministic code: a PowerShell Stop hook (executor-stop-gate.ps1) inspects GitHub state via gh every time an executor tries to finish.

Asymmetric Roles: Orchestrator vs. Executors

Rather than giving every agent identical prompts, the operating model splits responsibility asymmetrically:

  • Orchestrator (main session): No custom agent file—configured automatically at boot by a SessionStart hook injecting the orchestration skill. Breaks requirements into single-PR-sized GitHub issues, dispatches one executor per ready issue, and acts as the sole reviewer and merger.
  • Executors (sonnet-executor, issue-triage): Spawned subagents with explicit frontmatter pinning (model: claude-sonnet-4-6, effort: max) and a dedicated Stop hook bound to their lifecycle.

Each executor works in an isolated git worktree (../repo-<issue#>-<slug>) on branch feat/<issue#>-<slug>, ships one focused PR with Closes #N, and never runs git merge on main.

How the Deterministic Stop Gate Works

Same-account GitHub disallows formal “Request changes” reviews on your own pull requests. To preserve a hard review loop without needing separate bot accounts, review feedback is round-tripped through a tagged PR comment ([ORCH-REVIEW] CHANGES-REQUESTED):

 orchestrator session
   | fan out: 1 sonnet-executor per ready GitHub issue
   v
 sonnet-executor (worktree feat/<issue#>-<slug>)
   |  1. implement + targeted tests
   |  2. open PR with "Closes #<issue#>"
   |  3. attempts to Stop
   v
 executor-stop-gate.ps1  (Stop hook, pure code)
   |
   +-- no open PR & no "BLOCKED:" -> BLOCK stop, keep working
   +-- open PR (or BLOCKED)        -> allow stop; orchestrator reviews
   v
 orchestrator reviews PR (sole reviewer)
   |
   +-- approves -> merge into main -> Closes #N
   +-- requests changes -> "[ORCH-REVIEW] CHANGES-REQUESTED"
         |
         v
       resume executor -> Stop hook injects review -> push fix -> loop

Because the gate executes as an external hook (exit 2 + stderr payload on block), the executor cannot rationalize its way past a missing PR or unaddressed review notes. At the same time, two safety valves keep the harness from wedging:

  1. Explicit blocker escape hatch: Emitting BLOCKED: <reason> lets an agent surface genuine missing prerequisites cleanly.
  2. Fail-open & loop protection: Transient gh network errors fail open rather than trapping a session, and Claude Code’s stop_hook_active guard prevents infinite self-blocking loops.

Package Structure and Model Cost Control

In claude-upgrades, the whole setup packs cleanly into four drop-in directories guarded by a CI workflow that checks hook syntax, JSON structure, and frontmatter integrity:

skills/
  fable-orchestrator/   main-agent operating model: issues-as-todo-list, fan-out
  sonnet-executor/       one issue -> one worktree -> one PR; never merges
  issue-triage/          context-efficient GitHub issue labeling & triage
  handoff/               rolling handoff.md curation across compactions
agents/
  sonnet-executor.md     pinned Sonnet 4.6 + effort max + Stop hook
hooks/
  executor-stop-gate.ps1 pure-code PR & review round-trip gate
settings/
  settings.json          SessionStart auto-injection

One subtle operational pitfall surfaced during production runs: some setups make subagents inherit the parent session’s model rather than honoring model: in agent frontmatter. When orchestrating from Opus while dispatching many concurrent worktrees, always verify executors actually land on Sonnet 4.6—passing the target model explicitly if your runtime overrides frontmatter pins.

> cd ..