WhaleCastle

3p

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.

WhaleCastle 0 Updated 1mo ago

Resources

7
GitHub

Install

npx skillscat add whalecastle/claude-code-3p

Install via the SkillsCat registry.

SKILL.md

/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 clink using cli_name=agy (PAL binds its output parser to a fixed set of cli names, so the PAL-facing name is agy even though 3p presents it as "Antigravity"). PAL's agy integration 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 from state.json of the most recent run with that slug (full <slug>-<timestamp> disambiguates)
  • /3p --list → call python3 ~/.claude/skills/3p/scripts/3p.py list and print
  • /3p --clean <task-slug-or-runid> → call python3 ~/.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 for high or low; after they choose, call 3PSH model-power <choice> and print the saved value
  • /3p --model-power high|low → call 3PSH model-power <value> directly and print the saved value
  • /3p --models → call 3PSH models list and print the reviewer model map
  • /3p --models set <codex|antigravity> <low|high> <reasoning|code> <model> → call 3PSH 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 → call 3PSH 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 at init time only. They are passed to 3p.py init once, which resolves the full configuration and persists it under state.resolvedConfig. All subsequent subcommands (snapshot, summary, etc.) read from state.resolvedConfig and do not accept these flags. This guarantees --resume uses 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)

  1. Never run git commit, git push, git tag, or any deploy/publish command during a /3p run.
  2. Never modify existing git branches/tags/refs except refs/3p/<run-id>/*.
  3. 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).
  4. Never claim a phase is "approved" unless the approving round was fully attended AND both reviewers explicitly emitted APPROVED.
  5. Never skip the user-facing transparency display (see Transparency §).

Phase 0: New run setup

When invoked with a task description (no --resume):

  1. Compute slug: slug=$(3PSH slug "<task description>")
  2. Compute timestamp: ts=$(date +%Y%m%d-%H%M)
  3. 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 to state.resolvedConfig, writes the stub state, and bootstraps .gitignore. Verify the constructed git ref path — if init aborts because git check-ref-format rejected it, surface the error to the user and stop. Capture the <run-id> from stdout on success.
  4. Save the original task: write user's task description verbatim to .3p/<run-id>/task.txt
  5. Announce in chat: → Started /3p run: <run-id>

When invoked with --resume:

  1. Find the run dir matching the slug (most recent timestamp if ambiguous)
  2. Load state: 3PSH state-read <run-id> phase
  3. Announce in chat: → Resuming /3p run: <run-id> from phase <phase>, round <round>
  4. 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 caller
    • Antigravity [Risk] broad except clause → ignored: out of scope for this task
      A reviewer with nothing to raise is reported as Codex ✓ 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 N or Phase 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

  1. Set phase: 3PSH state-write <run-id> phase '"plan"'
  2. 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, optional testCommand.
  3. Run review loop with plan-review.md template and broader severity bar. See Review Loop § below.
  4. On approved exit OR cap-reached exit, set phase=build and proceed to Phase B.

Phase B: Build (per step)

Before the first step: capture the pre-build baseline ONCE.

  1. Check state: 3PSH state-read <run-id> baselines. If pre-build is 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
  2. Set phase: 3PSH state-write <run-id> phase '"build"'

For each step N declared in the plan:

  1. Capture step baseline: check 3PSH state-read <run-id> baselines. If step-N is 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.
  2. Record current step: 3PSH state-write <run-id> currentStep '{"index": N, "description": "...", "testCommand": "..."}'
  3. Implement step N. Edit files per the plan. Use Read/Edit/Write tools as normal.
  4. 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 content no tests run for this step so Phase C can detect this uniformly. The test command runs in the anchor directory.
  5. Run review loop with step-review.md template, tighter severity bar, the step's diff (3PSH snapshot diff <run-id> step-N), and the test output (read step-N-test.txt).
  6. On exit:
    • Compute sub-summary (counts of findings/verdicts, files touched, notable items)
    • Write .3p/<run-id>/step-N-summary.md with the sub-summary
    • Display the sub-summary in chat
    • Continue automatically to step N+1 (no pause)

Phase C: Final review

  1. Set phase: 3PSH state-write <run-id> phase '"final"'
  2. Build the cumulative diff: 3PSH snapshot diff <run-id> pre-build. Save to .3p/<run-id>/final-diff.txt so the prompt and final-review.md can both reference it.
  3. Build the file list (changed + new): from the diff output, extract paths
  4. Consolidate per-step test output: read every step-N-test.txt file in run order. Concatenate them into one block prefixed with === step-N === headers and the per-step exit code. If a step has the literal no tests run for this step body, 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 line no tests run during build. Save this to .3p/<run-id>/final-test-output.txt and pass it to the reviewer template as {{test_output}}.
  5. Run review loop with final-review.md template (broader bar). Round count begins fresh.
  6. After loop exit, consolidate Phase C into final-review.md: call 3PSH consolidate-final <run-id>.
  7. Generate the run-wide summary: 3PSH summary <run-id>
  8. Set phase: 3PSH state-write <run-id> phase '"done"'
  9. 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:

  1. Increment state.currentRound. Round files for this scope are named per the scope: plan-round-N-<reviewer>.md, step-M-round-N-<reviewer>.md, or final-round-N-<reviewer>.md. At scope start, do 3PSH state-write <run-id> currentRound 0 AND 3PSH state-write <run-id> currentScope '"<scope-id>"', then begin round 1.

  2. 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."
  3. Run both reviewers in parallel:

    • Pick this phase's reviewType: reasoning for Phase A (plan) and Phase C (final); code for 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>) and agy_role=$(3PSH reviewer-role <run-id> antigravity <reviewType>).
    • Single message with two mcp__pal__clink calls. The PAL cli_name is static per reviewer: Codex → cli_name=codex; Antigravity → cli_name=agy. So call cli_name=codex, role=<codex_role> and cli_name=agy, role=<agy_role>.
    • Use the continuation_id from prior rounds to preserve cross-round context.

    Timeout handling: mcp__pal__clink does 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: read state.resolvedConfig.timeoutSeconds and note the start time of each call. If a clink call errors with a timeout/transport error from the framework, that reviewer is unavailable for the round. If a clink call returns successfully but the wall-clock time exceeded timeoutSeconds AND its response parses as unavailable (garbled), the same failure mode applies. The skill does NOT attempt to forcibly cancel an in-flight clink call. 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.

  4. 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 unavailable OR the clink call 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.
  5. Detect failures. Update state.reviewerHealth.<reviewer>.consecutiveFailures (increment on failure, reset to 0 on responded). If a reviewer hit consecutiveFailuresForDowngrade, pause and surface the downgrade menu (see Persistent-reviewer downgrade §).

  6. If both reviewers failed: pause and surface the both-down menu. Wait for user.

  7. Verify findings. For each available reviewer's findings, decide a verdict:

    • accepted — true, aligned with goal, not over-engineering → will fix
    • rejected — false positive, misreading, factually wrong → won't fix, one-line reason
    • ignored — true but out of scope / taste / hypothetical → won't fix, one-line reason
      For each finding, fill verdict and verdictReason fields 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 the rebuttals array on the verdicts JSON: [{originalRound, originalTitle, claudeReasonPrior, reviewerPushback, claudeReasonNow, outcome: "withdrawn"|"sustained"|"now-accepted"}, ...].
  8. 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.

  9. 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).

  10. 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 APPROVED token 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 accepted findings 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 accepted findings 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.
  11. After each round, display the transparency line in chat.

Persistent-reviewer downgrade (handle when triggered)

When reviewerHealth.<reviewer>.consecutiveFailures hits consecutiveFailuresForDowngrade (default 3):

  1. 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)
  2. 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 in summary.md.
  3. If user picks [3]: write pausedReason, exit gracefully.

Both reviewers down (handle when triggered)

When both reviewers failed in the same round:

  1. Pause. Increment consecutiveBothDownRounds. If it hits 2, force a re-pause even after a retry.
  2. 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)
  3. Apply user choice. State is checkpointed before pausing.

Resume semantics

On --resume <slug>:

  1. Read state.json. Use phase to decide which phase block to enter.
  2. If phase == "plan": read plan.md and continue the plan review loop from currentRound + 1.
  3. If phase == "build": read currentStep and continue from where it left off (re-snapshot the step baseline only if not yet captured; otherwise reuse).
  4. If phase == "final": continue the final review loop.
  5. If phase == "paused": read pausedReason, 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 and parse-response input)

When you finish

After Phase C summary is written:

  1. Display the path: Summary written to .3p/<run-id>/summary.md
  2. Stop. Wait for the user. Do not commit, push, deploy, or take any further action.