Freeze a finished change, collect two or four independent review seats by change class, and evaluate one fail-closed final-gate report.
Install
npx skillscat add taloncjones/dotfiles/codex-skills-co-review Install via the SkillsCat registry.
Co-Review
This Codex adapter follows the canonical gate extracted fromclaude/skills/co-review/gate-policy.md. It is a final gate, not an advisory
development review. It produces one report and stops; use review-change for
development feedback. The canonical policy's bounded co-review --fix
coordinator is the sole exception: an explicit user invocation supplies its
scoped repair authorization, and existing development authorization also covers
that handoff.
Use plain co-review for this one read-only gate. --fix names the bounded
coordinator invocation; it is not an option for review.py orgate_report.py. Follow the canonical policy to validate a reusable currentCHANGES report, repair only confirmed blockers in one separate implementer
batch, run regression tests and one independent review-change, and then run
one follow-up verification on the repaired head using the canonical
carried-coverage rules. All outcomes stop the entire calling workflow; only
new explicit user direction after a stop can authorize another cycle. It never
auto-edits advisory findings, recurses, or converts incomplete or stale
evidence into a repair budget.
Resolve this installed adapter and its helpers before entering the target
repository. Never resolve paths relative to the repository being reviewed.
REVIEW_ROOT=$(uv run --no-project python - "${REVIEW_SKILL_FILE:-}" "${DOTFILEDIR:-}" <<'PYROOT'
from pathlib import Path
import sys
source, fallback = sys.argv[1:]
if source and (not Path(source).is_absolute() or Path(source).name != "SKILL.md"):
raise SystemExit("Use the absolute SKILL.md path supplied by the skill loader")
root = Path(source).resolve(strict=True).parents[3] if source else (
Path(fallback).expanduser().resolve(strict=True) if fallback else None
)
required = (
"claude/skills/co-review/scripts/review.py",
"claude/skills/co-review/scripts/gate_report.py",
"claude/hooks/agent_runtime.py",
)
if root is None or not all((root / name).is_file() for name in required):
raise SystemExit("Installed final-gate helpers are unavailable")
print(root)
PYROOT
) || exit 2
REVIEW_HELPER="$REVIEW_ROOT/claude/skills/co-review/scripts/review.py"
GATE_REPORT="$REVIEW_ROOT/claude/skills/co-review/scripts/gate_report.py"
RUNNER="$REVIEW_ROOT/claude/hooks/agent_runtime.py"The policy and schema commands are authoritative. Do not embed another
policy, infer an approval from comments, or continue if policy extraction or
schema inspection fails.
The coordinator independently creates and retains a fresh expected-identity JSON
separate from the report before the snapshot. It gets repository, PR number, full head,
full base, base branch, committed tree (the manifest's source.source_tree, the head merged with the live base), and CI for the actual target. Verify
that the PR target repository is the fetch target before resolving --base-ref.
For a PR, read the live base tip from git ls-remote origin refs/heads/<base> (not baseRefOid, which can lag) before prepare, write it asexpected.base, and pass it as --pr-base with --base-ref. A head behind the live base is gated on its merge result: prepare
freezes git's merge of the head with the live base (git merge-tree --write-tree), so source.source_tree and the reviewed tree are what the
squash merge lands, and the frozen diff base..codex_tree holds only the
PR's change. prepare refuses only a merge conflict: merge the base into
the branch, resolve the conflict, and re-run. Merging the base cleanly
needs no new round (carry-forward). When the prepare output's behind_by
is nonzero, add one line to each seat prompt and the final summary:
"Behind by commits; the reviewed tree is git's merge of the
head with the live base." It is never a finding.
Prepare and verify one frozen snapshot with review.py prepare andreview.py verify; a dirty source is still frozen for findings, but a committed
tree mismatch makes approval incomplete. A local no-PR review cannot fabricate a
PR number or claim APPROVE. Capture and hash the frozen diff and exact-head
CI payload as precondition artifacts:
git -C "$REPO" -c core.quotePath=true diff --no-color --no-ext-diff --no-textconv --no-renames --binary \
"$BASE" "$CODEX_TREE" >"$RUN_DIR/frozen.diff" || exit 2
# A failed capture can leave a partial diff that classifies light; never use it.
CLASS=$(uv run --no-project python "$GATE_REPORT" classify --diff "$RUN_DIR/frozen.diff" \
--small-max-files "${SMALL_MAX_FILES:-3}" --small-max-lines "${SMALL_MAX_LINES:-40}") || exit 2
# `co-review --full` sets CLASS=full here regardless of the classifier.
# A herdr brief's `herdr-ship-small-sensitive:` prefixes add one
# `--small-sensitive PREFIX` flag each; see "Small tier" below.Write CLASS into the expected identity's class field before resolving any
route; set report.class to the same value. A small class also writessmall: {max_files, max_lines, sensitive_paths} there, with the exact values
passed to classify.
Use an empty helper-owned SNAPSHOT_DIR only for review.py prepare output and
parse MANIFEST from that command's JSON result. Keep the expected identity,
prompts, native completion artifacts, frozen diff, normalized CI envelope, andreport.json in RUN_DIR. This lets review.py cleanup --manifest remove only
the verified snapshots while retaining the active gate evidence.
For a follow-up, also supply each seat the retained initial report and actual
artifact paths/digests, prior head/base/tree, repair delta and affected callers.
Follow the canonical Follow-up evidence section: inspect affected contracts,
identify invalidated prior coverage, and cite evidence for carried entries.
The full frozen change remains available as context; it is not an instruction
to restart an unrestricted search. The current report binds the current tree
and CI; prior evidence cannot supply current approval authority. A full review
required by scope/coverage changes stops for a new user decision.
Load the canonical material into the active report directory, then resolve each
seat before launch. A route that is unavailable or unsupported stops the gate.
PR_REPO is the stored origin's owner/repo; write it as the expected
identity's repository and pass it to every gh call with--repo "$PR_REPO", since the scratch RUN_DIR is not a checkout.
uv run --no-project python "$REVIEW_HELPER" github-repo --repo "$REPO" >"$RUN_DIR/github-repo.json" || exit 2
PR_REPO=$(uv run --no-project python -c 'import json,sys; print(json.load(open(sys.argv[1]))["repository"])' "$RUN_DIR/github-repo.json") || exit 2
POLICY=$(uv run --no-project python "$GATE_REPORT" policy --section POLICY) || exit 2
uv run --no-project python "$GATE_REPORT" schema >"$RUN_DIR/gate-schema.json" || exit 2
# Full tier:
uv run --no-project python "$RUNNER" route --runtime claude --role reviewer --risk normal --provisional >"$RUN_DIR/claude.route.json"
uv run --no-project python "$RUNNER" route --runtime codex --role reviewer --risk normal --provisional >"$RUN_DIR/codex.route.json"
uv run --no-project python "$RUNNER" route --runtime codex --role skeptic --risk normal --provisional >"$RUN_DIR/breaker.route.json"
uv run --no-project python "$RUNNER" route --runtime codex --role skeptic --risk normal --provisional >"$RUN_DIR/verifier.route.json"
# Light tier: only the codex reviewer route and a Claude-runner verifier route.
uv run --no-project python "$RUNNER" route --runtime claude --role skeptic --risk normal --provisional >"$RUN_DIR/verifier.route.json"Delta tier
co-review --delta asks for the delta round: the claude reviewer seat
and the verifier, both on Claude routes, scoped to a small follow-up
after a full or small APPROVE on this branch, including one from an earlier
head. The first round on a branch is full or small. The prior report and expected identity come with their SHA-256
digests from exactly one source: the pinned ship.json a herdr ship brief
names in herdr-ship-prior-handoff:, or this same uninterrupted workflow's
own retained full or small gate. Caps default to MAX_FILES=5 and MAX_LINES=150;
a herdr brief's herdr-ship-delta-caps: <files>/<lines> replaces them.
After the snapshot verifies and before the expected identity exists, ask
for the recommendation. HEAD is the manifest's source.head:
DELTA=0
uv run --no-project python "$GATE_REPORT" delta-class --repo "$REPO" \
--report "$PRIOR_REPORT" --expected "$PRIOR_EXPECTED" \
--report-sha256 "$PRIOR_REPORT_SHA256" --expected-sha256 "$PRIOR_EXPECTED_SHA256" \
--head "$HEAD" --max-files "$MAX_FILES" --max-lines "$MAX_LINES" \
--diff-out "$RUN_DIR/delta.diff" >"$RUN_DIR/delta-class.json" && DELTA=1DELTA=0 recommends full: keep the classifier's CLASS and report the
recommendation's reasons. A herdr brief whose herdr-ship-delta-head:
differs from $HEAD also keeps it. Otherwise approve the delta once. A
personal repository (claude/skills/lib/workflow_context.py account-scope --cwd "$REPO" --runtime claude reports personal_repository true)
proceeds. A work repository, or an unavailable account-scope, asks the
owner with AskUserQuestion, options "Delta round (Recommended)" and
"Full round", naming the PR, head, caps and the recommendation's stats.
A herdr brief line herdr-ship-brief: tier=delta is that approval. The
approval covers this head in this workflow only; "Full round" keepsCLASS.
On approval set CLASS=delta, then write the prior full run and the anchor
proof into RUN_DIR, in this order after delta.diff:
PRIOR_FULL_REPORT=$(uv run --no-project python -c 'import json,sys; print(json.load(open(sys.argv[1]))["prior_report"])' "$RUN_DIR/delta-class.json") || exit 2
PRIOR_FULL_EXPECTED=$(uv run --no-project python -c 'import json,sys; print(json.load(open(sys.argv[1]))["prior_expected"])' "$RUN_DIR/delta-class.json") || exit 2
PRIOR_FULL_REPORT_SHA256=$(uv run --no-project python -c 'import json,sys; print(json.load(open(sys.argv[1]))["prior_report_sha256"])' "$RUN_DIR/delta-class.json") || exit 2
PRIOR_FULL_EXPECTED_SHA256=$(uv run --no-project python -c 'import json,sys; print(json.load(open(sys.argv[1]))["prior_expected_sha256"])' "$RUN_DIR/delta-class.json") || exit 2
uv run --no-project python "$GATE_REPORT" copy-prior --report "$PRIOR_FULL_REPORT" \
--expected "$PRIOR_FULL_EXPECTED" --report-sha256 "$PRIOR_FULL_REPORT_SHA256" \
--expected-sha256 "$PRIOR_FULL_EXPECTED_SHA256" --out "$RUN_DIR/prior" >"$RUN_DIR/prior.json" || exit 2
uv run --no-project python - "$RUN_DIR" <<'PY' || exit 2
import json
import sys
from pathlib import Path
run = Path(sys.argv[1])
record = json.loads((run / "delta-class.json").read_text())["carry_forward"]
if record is not None:
(run / "carry-forward.json").write_text(json.dumps(record, sort_keys=True))
PYThen write class: "delta" and delta: {prior_run, prior_head, anchor_head, max_files, max_lines}, copied from delta-class.json,
into the expected identity before any seat runs. An interruption anywhere
in this sequence invalidates the run.
The report's delta block takes prior_run, prior_head andanchor_head from delta-class.json; prior_report and prior_expected
from prior.json; diff = delta.diff and carry_forward =carry-forward.json (null when absent), each with its SHA-256; andblast_radius from the verifier's reconciliation. The schema
subcommand lists every field.
Each delta seat prompt adds delta.diff as the review scope with the
frozen diff as context, the prior report path and digest (its advisories
are carried by reference, not re-listed), and this instruction: reportblast_radius: unbounded when the change's effect cannot be bounded to
this delta, such as a constant, default, tolerance, or fixture value read
elsewhere. Coverage follows the policy's Follow-up evidence rules.
An evaluation carrying escalate: "full" ends the delta gate. In an
interactive workflow, start one fresh full gate (new run_id,CLASS=full) on the same head; its verdict is final and never escalates.
A herdr ship launch runs exactly one gate: write ship.json with the delta
verdict and stop, and the director dispatches the full gate.
Small tier
The classifier returns small for a code or config diff of at most 3 files
and 40 changed lines (added plus removed) that adds or deletes no file,
touches at least one test, and touches no gate, CI, auth, hook, submodule,
skill or agent-instruction text (SKILL.md, AGENTS.md, CLAUDE.md), no
path with a security segment and no configured sensitive prefix. The
flags --small-max-files and --small-max-lines (at most 5 and 150) and one--small-sensitive PREFIX per prefix tune it; a herdr brief'sherdr-ship-small-caps: <files>/<lines> and herdr-ship-small-sensitive:
lines supply them as SMALL_MAX_FILES, SMALL_MAX_LINES and the prefixes.
A diff outside any cap stays full, and co-review --full forces full.
A small gate runs like the light tier: the native codex reviewer, then theverifier through the Claude runner, with the same Codex substitute rule.ci.json is its test evidence: no seat runs a suite or builds a disposable
copy. Each seat has a 270-second bound (SEAT_SECS); the native child's
deadline is 270 seconds. Every seat prompt appends the ## Small tier
section the Claude entrypoint's prompt loop writes: run no suite, cite the
captured CI check that runs the suite owning the touched test infunctional_behavior (a material gap when none does), list only read-only
inspection commands in tests, and finish within 270 seconds.
Any small result other than APPROVE carries escalate: "full". In an
interactive workflow, start one fresh full gate (new run_id,CLASS=full) on the same head; its verdict is final and never escalates.
A herdr ship launch runs exactly one gate: write ship.json with the small
verdict and stop, and the director dispatches the full gate. A small APPROVE
posts a tier=small marker.
In every tier, append probes <epoch> to RUN_DIR/seat-timing.txt before the
probes and collected <epoch> after the verifier is collected (date +%s).
The file only measures seat time; no decision reads it.
Capture CI before seats
A PR gate waits for the head's CI to finish and freezes it as ci.json
before any probe or seat runs. A Codex shell call is bounded, so start the
watch under nohup and check for $RUN_DIR/ci-watch.done in short calls;ci-capture is the only step that writes ci.json; never write it from
your own gh output. It reads the PR number, head and repository from the
expected identity, runs gh pr view with --repo from that identity, and
refuses a frozen head (--manifest) that is not the expected head, before
any gh call or seat. A local no-PR review skips this
section; a lessons class there falls back to CLASS=light and a small
class falls back to CLASS=full, removing expected.small.
nohup sh -c 'gh pr checks "$1" --repo "$3" --watch --interval 30 >"$2/ci-watch.txt" 2>&1; echo done >"$2/ci-watch.done"' \
sh "$PR" "$RUN_DIR" "$PR_REPO" >/dev/null 2>&1 &test -s "$RUN_DIR/ci-watch.done" || exit 3
uv run --no-project python "$GATE_REPORT" ci-capture --repo "$REPO" --expected "$EXPECTED_IDENTITY" --manifest "$MANIFEST" --out "$RUN_DIR/ci.json" >"$RUN_DIR/ci-envelope.json"Exit 3 means the watch is still running; check again. The ci-capture
exit rules match the Claude entrypoint: 0 continues; reasons exactly["CI evidence is missing"] take the no-CI path, a lessons class falls
back to CLASS=light and a small class to CLASS=full (rewrite the expected
identity's class and remove its small block before any probe); any other nonzero exit stops INCOMPLETE before any
probe. Then a lessons class runsuv run --no-project python "$GATE_REPORT" lessons-check --file "$CODEX_ROOT/claude/rules/personal/agent-lessons.md";
a nonzero exit stops INCOMPLETE before any probe.
Run gate_report.py schema before report assembly. Resolve each fresh seat
with the shared runner and --provisional; this records unknown availability
honestly and stops if the route is actually unavailable or unsupported.
Full tier only: Run four fresh, independent read-only 1200-second seats:claude reviewer, codex reviewer, breaker skeptic, then verifier
skeptic after every finder artifact is complete.
The light tier runs codex and verifier: first the native codex reviewer
seat, then, after its artifact is complete, the verifier through the Claude
runner, so the light gate keeps one seat per model. The small tier runs the
same two seats under the "Small tier" rules:
Delta tier: the claude reviewer seat and the verifier both run through the
Claude runner blocks below (the probe block already probes both roles for a
non-light class); no native Codex child runs.
Lessons tier: only the Claude-runner verifier runs, through the light verifier
block below, with no finder artifact and the Lessons fact-check section in its
prompt; the probe block probes only the skeptic role; no native Codex child
runs.
Probe the Claude runner route before spending a seat on it; the Codex runtime
needs no probe, because the controller running this skill is that runtime.
The probe is a 60-second Reply ok run on $CLAUDE_ROOT, written underRUN_DIR. The light tier probes the skeptic role (the verifier it will
launch); the full tier also probes the reviewer role (the claude seat). A
non-success probe stops co-review with the existing incomplete report,
quoting the probe's status, observation and errors. This is one
60-second shell call, within a Codex shell call's limit.
printf 'Reply ok\n' >"$RUN_DIR/probe.prompt"
nohup uv run --no-project python "$RUNNER" run \
--runtime claude --role skeptic --risk normal --provisional \
--cwd "$CLAUDE_ROOT" --sandbox read-only --timeout-secs 60 \
--prompt-file "$RUN_DIR/probe.prompt" >"$RUN_DIR/probe-claude-skeptic.json" &
if [ "$CLASS" = "full" ] || [ "$CLASS" = "delta" ]; then
nohup uv run --no-project python "$RUNNER" run \
--runtime claude --role reviewer --risk normal --provisional \
--cwd "$CLAUDE_ROOT" --sandbox read-only --timeout-secs 60 \
--prompt-file "$RUN_DIR/probe.prompt" >"$RUN_DIR/probe-claude-reviewer.json" &
fi
wait
uv run --no-project python - "$RUN_DIR"/probe-*.json <<'PY' || exit 2
import json
import sys
failed = []
for path in sys.argv[1:]:
try:
with open(path) as handle:
result = json.load(handle)
except (OSError, ValueError):
failed.append(f"{path}: no runner JSON")
continue
if result.get("status") != "success":
failed.append(f"{path}: {result.get('status')} {result.get('observation')} {result.get('errors')}")
if failed:
print("\n".join(failed))
sys.exit(1)
PYSEAT_SECS=1200
[ "$CLASS" = "small" ] && SEAT_SECS=270
nohup uv run --no-project python "$RUNNER" run \
--runtime claude --role skeptic --risk normal --provisional \
--cwd "$CLAUDE_ROOT" --sandbox read-only --timeout-secs "$SEAT_SECS" \
--prompt-file "$RUN_DIR/verifier.prompt" >"$RUN_DIR/verifier.runtime.json" 2>"$RUN_DIR/verifier.runtime.err" &
echo $! >"$RUN_DIR/verifier.pid"Launch the Claude runner seat first (full tier: before spawning the native
seats), then check $RUN_DIR/<seat>.runtime.json in short shell calls
until it is non-empty or the $SEAT_SECS deadline (1200 seconds, 270 under the
small tier) passes; never hold one
shell call open in a blocking wait. nohup keeps the seat alive after the
launching call ends, and the runner leaves an inherited ignored SIGHUP alone.
After the $SEAT_SECS deadline plus a 60-second grace, only when the JSON is
still empty, kill -0 "$(cat "$RUN_DIR/<seat>.pid")" succeeds andps -o command= -p for that pid still shows this seat's prompt file, send it SIGTERM; the
runner reaps the seat and writes a runner-interrupted result. Remove the
pid file once the seat is collected. An empty JSON is an incomplete seat.
The light verifier receives the one codex artifact path and digest and
every known blocker.
Every prompt includes the
extracted policy, frozen diff, manifest identity, the complete ## Classes
section of the failure-class rubric, and
declared threat model. Every prompt also names the frozen diff path
($RUN_DIR/frozen.diff) and the seat's own snapshot root as the frozen
worktree for reading files outside the diff. The verifier also receives finder artifact
paths/digests and known blockers. Every native packet and runner prompt
appends claude/skills/co-review/references/seat-report.md verbatim after the rubric.
Every native packet and runner prompt also
carries a ## CI evidence section: the lineci.json: $RUN_DIR/ci.json sha256=<sha256 from ci-envelope.json> and the
file's content; a local no-PR review, which has no CI files, writesNone: a local no-PR review captures no CI. there instead. A lessons
verifier prompt adds the ## Lessons fact-check section with the Claude
entrypoint's exact text. Save each runner JSON response as that seat's
nonempty artifact and preserve requested and observed route metadata. The
controller never stands in for a seat.
Full and delta tiers: the Claude seat uses the resolved original-account route
through the bounded runner and preserves its raw response as the claude
artifact:
SEAT_SECS=1200
nohup uv run --no-project python "$RUNNER" run \
--runtime claude --role reviewer --risk normal --provisional \
--cwd "$CLAUDE_ROOT" --sandbox read-only --timeout-secs "$SEAT_SECS" \
--prompt-file "$RUN_DIR/claude.prompt" >"$RUN_DIR/claude.runtime.json" 2>"$RUN_DIR/claude.runtime.err" &
echo $! >"$RUN_DIR/claude.pid"Launch this seat first, before spawning the native seats, then check$RUN_DIR/claude.runtime.json the same way as the light verifier.
From Codex, the full tier's codex, breaker, and verifier seats and the
light tier's codex seat are fresh native children after route resolution;
the light verifier is the Claude runner call above. The native spawn API has
no timeout or sandbox
arguments: give each an explicit read-only, disposable-fixture task and use the
coordinator's 1200-second deadline to interrupt an unfinished child. Record its
actual completion artifact. Do not invoke codex exec, a generic /code-review
plugin, or a nested partner route. The Claude seat preserves the
original repository's account scope through the runner. Every seat is
adversarial and read-only; it uses only disposable fixtures and does not fix,
publish, comment, or merge.
For each native seat, build a complete rendered review packet from actual
frozen values, never placeholders: repository identity; snapshot.codex_root;
base, head, expected tree, and reviewed tree; frozen diff path and digest;
target repository/PR/base-branch identity; the extracted policy; full rubric;
declared threat model; and changed-symbol callers. State the requested finding
format (including concrete impact for any material allegation),
disposable-fixture/read-only limits, and permission to consult relevant
reference skills under the canonical policy. Require the reviewer to do the
review itself, without another review workflow, delegated reviewers, partners,
network actions, comments, fixes or merge actions.
Full tier: dispatch the codex reviewer and breaker through the nativespawn_agent API with fork_turns: "none", model and reasoning_effort
taken from their exact parsed RUNNER route JSON. The resulting messages
contain the complete packet above and no controller history or other seat
output. Keep the returned handles. The existing Claude seat uses the same
full packet without either native report.
Light tier: dispatch only the codex reviewer through native spawn_agent
with the same packet rules. The small tier dispatches the same single seat.
Full tier: wait for Claude, Codex, and breaker to complete and collect their
actual native or runner results before continuing. The coordinator useswait_agent for each native handle and a 1200-second deadline; it usesinterrupt_agent only after a deadline. A failed, timed-out, empty, or
malformed completion stops as incomplete. Store each successful native completion as
the report-relative <seat>.native.md and record its digest; a runner seat
keeps its .json runner response.
The runner reports a usage-limit refusal (a result that opens with a notice
such as "You've hit your session limit") as status: error, with the notice
in errors and a null result, so its probe fails the probe check. The
evaluator re-reads every seat artifact: a .json artifact must be runner
JSON whose status is success, and no seat artifact may be a usage-limit
refusal. A limit-refused seat is not retried, because the limit holds until
its reset; the gate is INCOMPLETE.
Light tier: wait for the codex seat only, under the same deadline and
incomplete rules.
Full tier: only after those three artifacts and digests exist, render the
verifier packet. It includes the same full frozen-value packet, the named
Claude/Codex/breaker artifact paths and digests, and the expected known
blockers. Light tier: only after the codex artifact and digest exist,
render the verifier packet with that one artifact path and digest and the
expected known blockers, run it with the Claude runner command above, then
collect, validate, and hash its runner JSON under the same deadline rules. It
includes no other controller history. The full-tier verifier is dispatched
with native spawn_agent, fork_turns: "none", and its parsed skeptic-routemodel and reasoning_effort; then wait, collect, validate, and hash its
completion using the same deadline rules. The verifier independently tests
material claims, accounts for every blocker and reconciles the combined
coverage ledger. It does not start another unrestricted search.
Create report.json from the exact gate_report.py schema example. Fill all
fields from the verified manifest, independent expected identity, SHA-256
digests, frozen diff/CI artifact digests, findings, prior blocker
dispositions, and required coverage. Copy each seat's Structure fit findings
into findings with category: "structure" and their proposed_layout, asschema lists.
Fill the seat entries from the actual runtime artifacts for CLASS; the
light verifier entry records runtime claude and the codex entry records
runtime codex, as the evaluator requires. Paths are report relative. Copy each seat's findings and
coverage from its co-review-seat block. The coordinator never writes a co-review-seat block for a seat. Do not
substitute coordinator prose for a missing or unsuccessful runtime
completion. Then run one dry evaluate before the finalize block, which
removes the snapshot:
cp "$RUN_DIR/report.json" "$RUN_DIR/report.pre-dry.json" || exit 2
uv run --no-project python "$GATE_REPORT" evaluate --report "$RUN_DIR/report.json" --expected "$EXPECTED_IDENTITY" >"$RUN_DIR/evaluation-dry.json" || trueThe dry result grants nothing. The dry evaluate runs once per gate, and the
coordinator makes at most one correction after it, then runs the finalize
block with no second dry run. A correction is allowed only for a
transcription error: a report.json field that differs from the on-disk file
it was copied from (the manifest, the expected identity, an artifact's path
or SHA-256, or the schema example). Never edit a seat artifact, and
never add, drop, or re-disposition a finding: findings and prior_blockers stay
byte-equal to report.pre-dry.json. A mismatch between authoritative inputs
(the manifest, the expected identity, ci.json, or a seat artifact
disagreeing with another), or any APPROVE or CHANGES dry verdict, goes
straight to the finalize block uncorrected. An interruption after the cp
line and before the finalize block prints its output ends the gate; recovery
is a fresh gate with a new RUN_DIR and run_id.
Then verify, evaluate, and clean up:
# co-review-finalize:start
uv run --no-project python "$REVIEW_HELPER" verify --manifest "$MANIFEST" || exit 2
if uv run --no-project python "$GATE_REPORT" evaluate \
--report "$RUN_DIR/report.json" --expected "$EXPECTED_IDENTITY" \
>"$RUN_DIR/evaluation.json"; then
evaluation_status=0
else
evaluation_status=$?
fi
if uv run --no-project python "$REVIEW_HELPER" cleanup --manifest "$MANIFEST" \
>"$RUN_DIR/cleanup.json"; then
cleanup_status=0
else
cleanup_status=$?
fi
if [ "$cleanup_status" -ne 0 ]; then
uv run --no-project python "$GATE_REPORT" invalidate --expected "$EXPECTED_IDENTITY"
printf '%s\n' '{"verdict":"INCOMPLETE","approve_allowed":false,"reasons":["snapshot cleanup failed; evidence preserved"]}' >&2
exit 2
fi
cat "$RUN_DIR/evaluation.json"
exit "$evaluation_status"
# co-review-finalize:endThis marked block finalizes one plain gate only. A --fix coordinator runs it
as a per-gate subprocess, retains its structured evaluation and cleanup output,
and validates them before proceeding. A nonzero exit is not permission to
ignore an evaluator, verification, or cleanup failure.
Only APPROVE permits a merge-ready result. CHANGES returns concrete
blockers to development. INCOMPLETE reports missing evidence. Human approval
and explicit merge permission remain separate; cleanup refusal preserves the
snapshot.
Carry-forward
When the only commits since an APPROVE gate merge the target branch, the
verdict carries to the new head without seats. The prior report and
expected identity come with their SHA-256 digests from exactly one source:
the pinned herdr ship.json, or this same uninterrupted workflow's own
retained gate. Fetch the target, then run the proof:
git -C "$REPO" fetch origin "$BASE_REF" || exit 2
uv run --no-project python "$GATE_REPORT" carry-forward --repo "$REPO" \
--report "$PRIOR_REPORT" --expected "$PRIOR_EXPECTED" \
--report-sha256 "$PRIOR_REPORT_SHA256" --expected-sha256 "$PRIOR_EXPECTED_SHA256" \
>"$RUN_DIR/carry-forward.json"Exit 0 means every proof holds: no branch-authored commit since the gated
head, no merge-tree conflict anywhere in git's merge of the gated head with
the new base, and the head's whole tree byte-equal to that merge result. Exit 1 means the head needs a gate;
the record's reasons say why. The record grants nothing beyond naming the
prior run it extends.
On exit 0, post the record's audit_comment as ship step 5 does (dedupe onco-review-audit head=<head>), then the carry-forward marker under the
Publish rules below. Its first line is<!-- co-review: sha=<head> base=<base> base_ref=<base_ref> verdict=APPROVE round=<n> tier=carry-forward prior_run=<prior_run> prior_sha=<prior_head> -->,
its verdict line isCo-review verdict: APPROVE (carry-forward of <prior_run> at <prior_head>),
and the proof block from audit_comment follows. Before asking for the go,
skip the marker when any PR comment already has a line containing both<!-- co-review: sha=<head> and tier=carry-forward prior_run=<prior_run>;
that read is for dedupe only, never authority.
Publish
The only publishable item is one marker comment on the reviewed PR, for a PR
gate only -- never for a local no-PR review. A --fix coordinator publishes
at most once, after its final gate.
APPROVE: post the marker without asking, run the supersede step, and
print[INFO] posted co-review marker on #<PR>: APPROVEin the same turn.CHANGES: do not post. The marker stays inRUN_DIRand the report. If
the owner asks for it on the PR, register it as a draft (below) and wait
for the go.INCOMPLETE: never posted; the marker grammar has no such verdict.
The herdr post gate enforcing this is a momentum guardrail, not a security
boundary: it stops a well-meaning agent posting through gh as found on
PATH. In a work repository the gh shim lets an APPROVE marker through on a
PR this account authored and refuses it on anyone else's PR; supersede
deletes never need a go, and the shim runs a delete only on this account's
own co-review marker. A personal repository needs no go at all.
Marker comment shape: first line is the marker, then one verdict line, then
one line per blocker (<id>: <title>), then at most oneStructure: <id>: <proposed layout> line for the report's most useful
Structure fit finding, nothing else. Marker fields: sha =
expected head, base = expected base (the live git ls-remote tip, not baseRefOid), base_ref = expected base_ref,verdict = evaluator verdict, round = 1 + the highest round= among our
own valid markers already on the PR (1 when none), tier = expected class; a delta marker adds prior_run = expecteddelta.prior_run and prior_sha = expected delta.prior_head, in that order.
No target_tip. A carry-forward marker also carries its proof block after
the verdict line (see Carry-forward).
Before posting, read the PR's comments once (the gh api --paginate --slurp
call below, run before gh pr comment) and compute round from it.
gh pr comment "$PR" --repo "$PR_REPO" --body-file "$RUN_DIR/marker.md" || exit 2
gh api --paginate --slurp "repos/$PR_REPO/issues/$PR/comments" >"$RUN_DIR/comments.json"
ME=$(gh api user --jq .login)
uv run --no-project python "$REVIEW_ROOT/claude/skills/co-review/scripts/pr_ready_gate.py" supersede \
--comments "$RUN_DIR/comments.json" --author "$ME" --expect-marker "$MARKER_LINE"
# Delete each printed id with its own call; no loop, no $(...):
# gh api -X DELETE "repos/$PR_REPO/issues/comments/<id>"Only on the comment's exit 0 does the supersede step run. If supersede did
not finish, re-read the comments and rerun only the supersede step. If gh pr comment did not exit 0, let that Bash call return first, then read the
comments: post again only when the marker is absent.
A draft is the owner's view of one gated post. When the shim refuses a post
(a CHANGES marker the owner asked for, or a marker on a PR this account did
not author), register it with the exact argv it names:
python3 ~/.claude/hooks/pr_post_guard.py draft -- gh pr comment "$PR" --repo "$PR_REPO" --body-file "$RUN_DIR/marker.md"Show the printed draft <hash> line and the body. Only a Claude session
mints the go: its AskUserQuestion answer to a post option starting Post plus a space and ending (draft <hash>), with no negation, hedge or dash aside (refusal words in references/posting.md)
approves that one draft through a PostToolUse hook, and a skip option
ending (skip draft <hash>) dismisses it.
A typed message never approves a post. A Codex session cannot mint one.
Leave the draft in RUN_DIR and report it as ready, so the Claude director
can ask the owner. Never ask the owner to type a phrase.
An approved draft stays approved across later messages until it is posted.
If an approved post fails, let the Bash call return, read the PR, and post
again only when the text is absent. Re-register it and tell the owner the
earlier attempt failed and a duplicate is possible.
Never reply to, resolve, or react to a reviewer thread on your own. Draft
any reply as above and leave it for the director to ask.
Edit the title or body of a PR this account authored without asking, and
print [INFO] edited PR #<PR> body: <why> in the same turn.