Three-party review workflow — Claude plans/builds, Codex and Antigravity independently review, Claude verifies and iterates until unanimous approval or round-cap exit. Use when invoked via `/3p <task>` or when the user explicitly asks to run a task "through 3p" or "with 3p review." Per-task only — not a session-wide mode.
Resources
7Install
npx skillscat add whalecastle/claude-code-3p Install via the SkillsCat registry.
/3p — Three-Party Review Workflow
You are running the /3p skill. Follow these instructions exactly. Do not skip steps.
What this skill does
For one task, you execute three phases — Plan, Build, Final — with Codex and Antigravity independently reviewing your work at each phase. You verify their findings, iterate, and produce a summary the user reviews before any commit/deploy. The skill is per-task (not session-wide) and never commits, pushes, or deploys.
Reviewer naming. The second reviewer is Antigravity. It is reached through PAL
clinkusingcli_name=agy(PAL binds its output parser to a fixed set of cli names, so the PAL-facing name isagyeven though 3p presents it as "Antigravity"). PAL'sagyintegration runs non-interactively and already injects--dangerously-skip-permissions(auto-approve), so no extra permission flag is configured by 3p.
Activation forms
Parse the invocation:
/3p <task description>→ start a new run/3p --resume <task-slug>→ resume fromstate.jsonof the most recent run with that slug (full<slug>-<timestamp>disambiguates)/3p --list→ callpython3 ~/.claude/skills/3p/scripts/3p.py listand print/3p --clean <task-slug-or-runid>→ callpython3 ~/.claude/skills/3p/scripts/3p.py clean <run-id>(resolve slug to run-id if needed)/3p --model-power→ show the user a numbered menu forhighorlow; after they choose, call3PSH model-power <choice>and print the saved value/3p --model-power high|low→ call3PSH model-power <value>directly and print the saved value/3p --models→ call3PSH models listand print the reviewer model map/3p --models set <codex|antigravity> <low|high> <reasoning|code> <model>→ call3PSH models set ..., print the result, and tell the user to restart Claude Code so PAL MCP reloads the updated reviewer role args. If PAL MCP is managed as a separate process, restart that process instead./3p --update→ call3PSH update, print the result, and tell the user to restart Claude Code so the updated skill instructions and PAL MCP reviewer roles are loaded. If PAL MCP is managed as a separate process, restart that process too.--config <path>and--exclude <pattern>are flags that are consumed atinittime only. They are passed to3p.py initonce, which resolves the full configuration and persists it understate.resolvedConfig. All subsequent subcommands (snapshot,summary, etc.) read fromstate.resolvedConfigand do not accept these flags. This guarantees--resumeuses the same exclusions/timeouts as the original run, even if the user does not re-supply them.
For brevity below, 3PSH refers to python3 ~/.claude/skills/3p/scripts/3p.py.
Model power
modelPower controls which reviewer PAL role is used for new /3p runs. Models are mapped on two axes: power (high/low) × reviewType (reasoning/code). The reviewType is chosen by the phase:
reasoning— Phase A (plan review) and Phase C (final integration/logic review).code— Phase B (per-step code review).
The installed PAL roles are generated by 3PSH pal-config install and map to model names in .3p/config.json:
| Power | Reviewer | reasoning (plan/final) |
code (per-step) |
|---|---|---|---|
| high | Codex | gpt-5.5 |
gpt-5.5 |
| high | Antigravity | Gemini 3.1 Pro (High) |
Gemini 3.5 Flash (High) |
| low | Codex | gpt-5.4-mini |
gpt-5.4-mini |
| low | Antigravity | Gemini 3.1 Pro (Low) |
Gemini 3.5 Flash (Low) |
Codex uses the same model for both review types; Antigravity uses the Pro model for reasoning and the Flash model for code review.
Advanced users can change those mappings with /3p --models set <codex|antigravity> <low|high> <reasoning|code> <model> or a config file. After changing model mappings, restart Claude Code so PAL MCP reloads the updated role args; if PAL MCP is managed as a separate process, restart that process instead. The current modelPower value and model names are persisted into each run's state.resolvedConfig at init. 3PSH reviewer-role <run-id> <reviewer> <reviewType> installs and returns a model-specific PAL role for that frozen run config, so later model changes affect future runs and do not silently change an in-progress run.
Hard safety guarantees (NEVER violate)
- Never run
git commit,git push,git tag, or any deploy/publish command during a /3p run. - Never modify existing git branches/tags/refs except
refs/3p/<run-id>/*. - Never include files matching the hardcoded secret patterns in any reviewer prompt (the helpers enforce this — but if you manually paste content into a prompt, double-check).
- Never claim a phase is "approved" unless the approving round was fully attended AND both reviewers explicitly emitted
APPROVED. - Never skip the user-facing transparency display (see Transparency §).
Phase 0: New run setup
When invoked with a task description (no --resume):
- Compute slug:
slug=$(3PSH slug "<task description>") - Compute timestamp:
ts=$(date +%Y%m%d-%H%M) - Initialize run, forwarding any user-supplied
--config <path>and--exclude <pattern>flags exactly once:3PSH init "$slug" "$ts" [--config <path>] [--exclude <pat>].... This creates.3p/<run-id>/, persists the resolved configuration tostate.resolvedConfig, writes the stub state, and bootstraps.gitignore. Verify the constructed git ref path — ifinitaborts becausegit check-ref-formatrejected it, surface the error to the user and stop. Capture the<run-id>from stdout on success. - Save the original task: write user's task description verbatim to
.3p/<run-id>/task.txt - Announce in chat:
→ Started /3p run: <run-id>
When invoked with --resume:
- Find the run dir matching the slug (most recent timestamp if ambiguous)
- Load state:
3PSH state-read <run-id> phase - Announce in chat:
→ Resuming /3p run: <run-id> from phase <phase>, round <round> - Jump to the matching phase below.
Transparency rules (apply throughout)
Keep the user continuously informed — they should never be left waiting without knowing what is happening. Surface these moments in chat, one short line each (concise, never raw dumps):
- Phase transitions:
→ Phase A: Plan/→ Phase B: Step 2/4 — implement auth middleware/→ Phase C: Final review - Round starts:
Round N: requesting reviews from codex + antigravity (parallel)… - Reviewer feedback (per finding). When a reviewer returns, list each finding it raised on its own line as
<Reviewer> [<Severity>] <title> → <verdict>[: <one-line reason>], e.g.:Codex [Critical] race in state lock → accepted (fixing)Antigravity [Important] missing input validation → rejected: input is already constrained by the callerAntigravity [Risk] broad except clause → ignored: out of scope for this task
A reviewer with nothing to raise is reported asCodex ✓ APPROVED.
- What Claude did in response. After verifying findings, say what you changed:
applied 2 fixes (state.py, lock.py); rejected 1, ignored 1. When you re-review, note it:re-running review so reviewers see the revised artifact. - Round results recap:
Round N: codex <count> findings (<accepted>/<rejected>/<ignored>) · antigravity <…> - Approval status:
Round N: codex ✓ approved · antigravity ✗ <N> findings remain · proceeding to round N+1 - Pushback events:
antigravity pushed back on R<n> finding "<title>" — reconsidering → <withdrawn|sustained|now-accepted> - File writes:
wrote .3p/<run-id>/<filename> - Exit conditions:
Phase A complete: unanimous approval at round NorPhase A: round cap reached, see summary - Reviewer availability:
⚠ <reviewer> timeout — continuing with <other> only this round - Both-down pauses: surface the full menu and wait for user input
Do NOT dump full reviewer outputs to chat; they go to round files. Do NOT dump diffs to chat. The per-finding lines above are short summaries (severity + title + your verdict + a one-line reason), NOT the reviewer's raw text.
Phase A: Plan
- Set phase:
3PSH state-write <run-id> phase '"plan"' - Write the plan to
.3p/<run-id>/plan.md. The plan MUST contain numbered Steps (Step 1, Step 2, …) — even a one-step task has Step 1. Each step has: goal, expected files touched, acceptance criteria, optionaltestCommand. - Run review loop with
plan-review.mdtemplate and broader severity bar. See Review Loop § below. - On approved exit OR cap-reached exit, set
phase=buildand proceed to Phase B.
Phase B: Build (per step)
Before the first step: capture the pre-build baseline ONCE.
- Check state:
3PSH state-read <run-id> baselines. Ifpre-buildis already present (resume case), SKIP this step entirely — re-capturing would overwrite the original starting state with a dirty state and invalidate the Phase C cumulative diff. Otherwise:3PSH snapshot capture <run-id> pre-build
- Set phase:
3PSH state-write <run-id> phase '"build"'
For each step N declared in the plan:
- Capture step baseline: check
3PSH state-read <run-id> baselines. Ifstep-Nis already present (resume after the step's baseline was captured but before implementation finished), SKIP this capture — overwriting it with a partially-implemented working tree would silently drop half the step's changes from the reviewer diff. Otherwise:3PSH snapshot capture <run-id> step-N. The same guard semantics apply as for pre-build: never re-capture an existing baseline; only capture if absent. - Record current step:
3PSH state-write <run-id> currentStep '{"index": N, "description": "...", "testCommand": "..."}' - Implement step N. Edit files per the plan. Use Read/Edit/Write tools as normal.
- Run the step's test command if declared. Capture stdout/stderr/exit-code into
.3p/<run-id>/step-N-test.txt. If no command, write the file with the literal contentno tests run for this stepso Phase C can detect this uniformly. The test command runs in the anchor directory. - Run review loop with
step-review.mdtemplate, tighter severity bar, the step's diff (3PSH snapshot diff <run-id> step-N), and the test output (readstep-N-test.txt). - On exit:
- Compute sub-summary (counts of findings/verdicts, files touched, notable items)
- Write
.3p/<run-id>/step-N-summary.mdwith the sub-summary - Display the sub-summary in chat
- Continue automatically to step N+1 (no pause)
Phase C: Final review
- Set phase:
3PSH state-write <run-id> phase '"final"' - Build the cumulative diff:
3PSH snapshot diff <run-id> pre-build. Save to.3p/<run-id>/final-diff.txtso the prompt andfinal-review.mdcan both reference it. - Build the file list (changed + new): from the diff output, extract paths
- Consolidate per-step test output: read every
step-N-test.txtfile in run order. Concatenate them into one block prefixed with=== step-N ===headers and the per-step exit code. If a step has the literalno tests run for this stepbody, mark that step as "no tests" in the header. If NO step declared any test command across the whole run, the consolidated block is the single literal lineno tests run during build. Save this to.3p/<run-id>/final-test-output.txtand pass it to the reviewer template as{{test_output}}. - Run review loop with
final-review.mdtemplate (broader bar). Round count begins fresh. - After loop exit, consolidate Phase C into
final-review.md: call3PSH consolidate-final <run-id>. - Generate the run-wide summary:
3PSH summary <run-id> - Set phase:
3PSH state-write <run-id> phase '"done"' - Display the summary path and stop. Do not commit, do not deploy. Wait for the user's response.
Review loop (shared by all three phases)
Scoped round counters. state.currentRound is per review scope, not per run. Reset it to 0 at the start of every review-loop scope:
- Start of Phase A plan review
- Start of every Phase B step (each step is its own scope)
- Start of Phase C final review
Also write state.currentScope to one of: "plan", "step-N", "final". On --resume, the skill reads currentScope and resumes that loop from currentRound + 1. This keeps the round cap (roundCap, default 10) applied per-scope per the spec, and keeps the rebuttal context scoped to the right loop.
For each round:
Increment
state.currentRound. Round files for this scope are named per the scope:plan-round-N-<reviewer>.md,step-M-round-N-<reviewer>.md, orfinal-round-N-<reviewer>.md. At scope start, do3PSH state-write <run-id> currentRound 0AND3PSH state-write <run-id> currentScope '"<scope-id>"', then begin round 1.Build each reviewer's prompt by filling the relevant template:
- For round 1: leave
{{rebuttal_section}}empty - For round ≥ 2: include any rejected/ignored findings from prior rounds that the reviewer raised (each reviewer only sees its own), formatted as: "Last round you raised these findings that were not addressed: . If you still believe any are blocker/critical/important, push back with stronger evidence (point to specific code, cite a concrete failure mode). Otherwise drop them and review the latest artifact."
- For round 1: leave
Run both reviewers in parallel:
- Pick this phase's reviewType:
reasoningfor Phase A (plan) and Phase C (final);codefor Phase B (per-step). - Resolve and install the PAL role names for this run, passing the reviewType:
codex_role=$(3PSH reviewer-role <run-id> codex <reviewType>)andagy_role=$(3PSH reviewer-role <run-id> antigravity <reviewType>). - Single message with two
mcp__pal__clinkcalls. The PALcli_nameis static per reviewer: Codex →cli_name=codex; Antigravity →cli_name=agy. So callcli_name=codex, role=<codex_role>andcli_name=agy, role=<agy_role>. - Use the
continuation_idfrom prior rounds to preserve cross-round context.
Timeout handling:
mcp__pal__clinkdoes not expose a per-call timeout parameter; it relies on the MCP framework's transport timeout (typically generous). The skill therefore treats timeout the practical way: readstate.resolvedConfig.timeoutSecondsand note the start time of each call. If aclinkcall errors with a timeout/transport error from the framework, that reviewer isunavailablefor the round. If aclinkcall returns successfully but the wall-clock time exceededtimeoutSecondsAND its response parses asunavailable(garbled), the same failure mode applies. The skill does NOT attempt to forcibly cancel an in-flightclinkcall. If a hang seems to be occurring (no progress for ≫timeoutSeconds × 3), surface the situation to the user and wait for input — this is the "both-down" pause unless one reviewer already returned cleanly.- Pick this phase's reviewType:
For each reviewer's response:
- Save the raw text to
.3p/<run-id>/<round-prefix>-<reviewer>.raw.txt - Parse it:
3PSH parse-response <raw-file>→ JSON{status, findings, raw?} - If status is
unavailableOR theclinkcall errored, mark the reviewer as having failed this round (record reason:timeout/error/garbled) - Append an entry to the historical availability log:
3PSH availability-append <run-id> '<entry-json>'. Entry shape:{phase, step, round, reviewer, status: "responded"|"unavailable", reason?, durationSeconds}. Call this for BOTH reviewers on every round, whether they responded or failed.
- Save the raw text to
Detect failures. Update
state.reviewerHealth.<reviewer>.consecutiveFailures(increment on failure, reset to 0 on responded). If a reviewer hitconsecutiveFailuresForDowngrade, pause and surface the downgrade menu (see Persistent-reviewer downgrade §).If both reviewers failed: pause and surface the both-down menu. Wait for user.
Verify findings. For each available reviewer's findings, decide a verdict:
accepted— true, aligned with goal, not over-engineering → will fixrejected— false positive, misreading, factually wrong → won't fix, one-line reasonignored— true but out of scope / taste / hypothetical → won't fix, one-line reason
For each finding, fillverdictandverdictReasonfields in the JSON. For rebuttal items (rejected/ignored findings that the reviewer pushed back on, OR new pushbacks the reviewer raised this round in response to prior rejections), populate therebuttalsarray on the verdicts JSON:[{originalRound, originalTitle, claudeReasonPrior, reviewerPushback, claudeReasonNow, outcome: "withdrawn"|"sustained"|"now-accepted"}, ...].
Write the round file:
3PSH round-write <run-id> <phase> <step-or-dash> <round> <reviewer> <verdicts-json>for each reviewer. The round-write helper renders both findings AND the rebuttals array into the markdown.Apply accepted-finding fixes. Edit the plan (Phase A) or code (Phase B) or fail-out (Phase C — apply post-hoc fixes to code if needed; rare).
Decide exit: Apply all
accepted-finding fixes to the artifact, then decide:- Approved exit: This round was fully attended (both reviewers responded with structured output) and both reviewers emitted the explicit
APPROVEDtoken for the current artifact (the version revised after this round's accepted findings, if any). Exit phase. Exception: if user-authorized downgrade mode is active for the run, the working reviewer's APPROVED is sufficient. - Continue: Otherwise — if any
acceptedfindings were applied this round (reviewers must see the revised artifact) OR any reviewer raised findings this round OR this round was not fully attended → proceed to the next round. - Cap reached: Round 10 finishes without an approved exit. Any remaining
acceptedfindings are applied in one final no-review revision pass before exit. Cap-reached exit is recorded explicitly in the round file and surfaced in the final summary.
- Approved exit: This round was fully attended (both reviewers responded with structured output) and both reviewers emitted the explicit
After each round, display the transparency line in chat.
Persistent-reviewer downgrade (handle when triggered)
When reviewerHealth.<reviewer>.consecutiveFailures hits consecutiveFailuresForDowngrade (default 3):
- Pause and display:
⚠ <reviewer> has failed 3 consecutive rounds. Last failure: <reason> How should I proceed? [1] Keep retrying every round [2] Downgrade to single-reviewer mode for the rest of this run [3] Abort and save current state (resume later) [4] Other (tell me) - If user picks [2]:
3PSH state-write <run-id> downgradeMode '{"disabledReviewer": "<r>", "authorizedAt": "<iso>", "reason": "<r>"}'. From then on, the fully-attended approving-round requirement is waived and only the working reviewer's APPROVED is required for phase approval. Record the downgrade in every subsequent round file and insummary.md. - If user picks [3]: write
pausedReason, exit gracefully.
Both reviewers down (handle when triggered)
When both reviewers failed in the same round:
- Pause. Increment
consecutiveBothDownRounds. If it hits 2, force a re-pause even after a retry. - Display:
⚠ Both codex and antigravity failed to respond this round. codex: <reason> antigravity: <reason> How should I proceed? [1] Retry both reviewers now (same round) [2] Continue solo for this round, try both again next round [3] Abort and save current state (resume later) [4] Other (tell me) - Apply user choice. State is checkpointed before pausing.
Resume semantics
On --resume <slug>:
- Read
state.json. Usephaseto decide which phase block to enter. - If
phase == "plan": readplan.mdand continue the plan review loop fromcurrentRound + 1. - If
phase == "build": readcurrentStepand continue from where it left off (re-snapshot the step baseline only if not yet captured; otherwise reuse). - If
phase == "final": continue the final review loop. - If
phase == "paused": readpausedReason, display, ask user how to proceed.
Output format reminders
- Round file names (per-reviewer, race-free):
plan-round-N-<reviewer>.md,step-M-round-N-<reviewer>.md,final-round-N-<reviewer>.md. The summary/consolidate helpers glob all matching files at read time. - Step summary:
step-M-summary.md - Final summary:
summary.md - Raw reviewer responses:
<round-prefix>-<reviewer>.raw.txt(for debugging andparse-responseinput)
When you finish
After Phase C summary is written:
- Display the path:
Summary written to .3p/<run-id>/summary.md - Stop. Wait for the user. Do not commit, push, deploy, or take any further action.