review: re-review fingerprint anchors on cache memory (body stamp never survives gh-aw ingest) - #287
review: re-review fingerprint anchors on cache memory (body stamp never survives gh-aw ingest)#287jwbron wants to merge 1 commit into
Conversation
🦋 Changeset detectedLatest commit: 22455b1 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
Review live A/BBaseline: Ruler: matcher deterministic+arbiter; corpus cc11988c0918 (9 cases).
Adversarial hard gate: PASSED on the candidate arm. Single-run-stable rows: recall, verdict agreement, regressions, adversarial gate. Judge quality and noise are not: they jitter run-to-run at this corpus size, and a regressed reviewer can score HIGHER on judge quality (fewer, surer comments each read better). Recall against the labeled specs is the load-bearing metric. Measured noise floor (identical arms, run 29069228968, 2026-07-10, 6 arm-samples, full corpus x3, pre-arbiter; budget skips left the samples on unequal case sets, so these v1 bands also carry case-mix variance): must-catch recall 54%-86% (sd 10%), verdict agreement 75%-100% (sd 9%), noise (unmatched posted) 50%-60% (sd 3%), judge mean quality 82%-86% (sd 2%). A single-run delta whose arms both sit inside a band is indistinguishable from run-to-run wobble; use |
lib/stage.ts fetches everything the plan needs and writes it before the agent starts, so staging costs zero assistant turns and fails before any credits are spent. Follows the reviewer's orchestrator slice 1 (#280). Turns are what this workflow costs. The first live run: 131 turns, 460 AIC, ~93 KB of tool output total. 61% of the bill was cache reads at a 40.7:1 read-to-write ratio, so caching was already near-optimal and only the turn count could move. Staging was ~15 of those turns. Also records two findings from #287: gh-aw's ingest strips all HTML comments, so the reviewer's fingerprint never reaches a posted review (making autofix's degraded path the normal one, since cache memory is per-workflow and not reachable from here), and this workflow's own summary marker was being silently deleted too.
lib/stage.ts fetches everything the plan needs and writes it before the agent starts, so staging costs zero assistant turns and fails before any credits are spent. Follows the reviewer's orchestrator slice 1 (#280). Turns are what this workflow costs. The first live run: 131 turns, 460 AIC, ~93 KB of tool output total. 61% of the bill was cache reads at a 40.7:1 read-to-write ratio, so caching was already near-optimal and only the turn count could move. Staging was ~15 of those turns. Also records two findings from #287: gh-aw's ingest strips all HTML comments, so the reviewer's fingerprint never reaches a posted review (making autofix's degraded path the normal one, since cache memory is per-workflow and not reachable from here), and this workflow's own summary marker was being silently deleted too.
#292) ## Summary Fixes the two recurring live A/B eval failures that have shown up on the eval comments of PRs 279, 282, 283, 284, and 287 (for example [this comment](#287 (comment))): - `incident-cache-missing-key: correctness-reviewer / claim-validator: malformed output: Expected property name or '}' in JSON at position 1` - `Judge scoring failed: Bad escaped character in JSON at position ~3xx` ## Root causes Every live seam (producer finders/validator, judge scoring, match arbiter) shared the same extraction rule: slice `/\{[\s\S]*\}/` (the FIRST `{` through the LAST `}`) out of the model's final text and strict-`JSON.parse` it. 1. **Prose braces poison the slice.** The `incident-cache-missing-key` diff is full of template literals (`` `user-profile:${tenantId}:${userId}` ``). When an agent quotes one in prose before its JSON payload, the slice starts at `{tenantId...` and parsing dies with exactly "Expected property name or '}' at position 1 (line 1 column 2)". The malformed-output retry re-quotes the same snippet, so it never recovers; that is why it is always this case that fails. 2. **One invalid string escape kills the judge.** The judge occasionally emits an invalid escape (for example `\'`) in its rationale string; strict parsing then drops that arm's entire quality score. The match arbiter had the same flaw with a worse failure mode: on any parse failure it silently returns `false`, so a correct `{"match": true}` verdict preceded by prose braces was counted as a miss (inflating the "unmatched posted" noise metric). ## Fix New shared `workflows/review/eval/extract-json.ts`: - walks `{` candidates left to right and takes each candidate's balanced, string-aware extent (braces inside JSON strings do not confuse it); - a slice that fails to parse is retried once with invalid string escapes repaired (drop the backslash, the lenient-parser rule; valid escapes are consumed pairwise so `\\` is never corrupted); - of the top-level slices that parse, the last one wins, since every caller instructs its agent to end with the JSON object. All three call sites now use it: `live-producer.ts` (`parseJsonObject` removed), `judge-live-model.ts`, and `match-arbiter.ts`. ## Testing - 14 new unit tests in `extract-json.test.ts`, including reproductions of both production failure shapes. - Producer-level regression test: a payload preceded by quoted template-literal braces parses with no retry. - Arbiter-level regression test: a yes verdict past prose braces is read as a match (previously a silent no). - `pnpm test` (1073 tests), `pnpm typecheck`, and eslint on the touched files all pass. Author: jwbron Reviewers: github-actions[bot], jeresig, jaredly, somewhatabstract Required Reviewers: Approved By: github-actions[bot], jeresig Checks: ✅ 10 checks were successful Pull Request URL: #292
… to cache memory (the body stamp never survives gh-aw ingest)
63930fd to
22455b1
Compare
Review Guidancegithub-actions (2 files)
|
| // be detected here; it surfaces as full divergence, i.e. a full review. | ||
| const signature = | ||
| validSignature(record.stampHunks) ?? | ||
| validSignature(record.reviewedHunks); |
There was a problem hiding this comment.
suggestion (non-blocking): The reviewedHunks fallback anchors on a regime-mismatched signature, so on a pre-upgrade cache record (has reviewedHunks, no stampHunks) the run posts a false "divergence tripwire re-armed" note. In production reviewedHunks is Step 1's 64-char SHA over added lines only, but the CLI hashes 16-char truncations over added+removed lines (HUNK_HASH_CHARS), so they can never string-match. validSignature does no length/regime check, so the anchor validates, computeDivergence matches nothing (share 1.0), and decideReReviewDepth returns tripwire-divergence/tripwireRearmed: true — posting divergence tripwire re-armed a full review (unreviewed share 1.00) when nothing diverged, plus a phantom re-arm in the cost metric. Review coverage stays correct (still full), so this is observability, not blocking.
Two supporting notes: the code comment here claims a "scripted-mode staging layer" writes reviewedHunks in the CLI regime, but no such writer exists in this repo; and the new test seeds reviewedHunks: CURRENT (the CLI-computed signature), so the production mismatch is never exercised. Accepting only the CLI regime here also subsumes the stampHunks: "overflow" case (both degrade to the honest no-prior-fingerprint):
if (
!Array.isArray(hashes) ||
hashes.some(
(hash) => typeof hash !== "string" || !/^[0-9a-f]{16}$/.test(hash),
)
) {
return null;
}| the review body, Step 6, which is exactly why the stamp exists: cache memory can | ||
| be evicted, the review body cannot.) | ||
| only; both sides of that comparison are Step 1's own added-lines hash.) | ||
| - `stampHunks`: copy **verbatim** from `rereview-plan.json`'s `stampHunks` field (the |
There was a problem hiding this comment.
suggestion (non-blocking): This makes a model-transcribed field the production fingerprint carrier, with no size bound or checksum — unlike the old body-stamp carrier, which was code-rendered with an overflow cap. Every miscopy fails toward full (safe) but silently, and the large/long-lived PRs where the dial pays most are the likeliest to be miscopied; the symptom is then indistinguishable from the pre-fix bug except via stampSource. The Step 6 stamp subcommand already runs with the decided verdict and reads rereview-plan.json, so it holds stampHunks/stampAnchorDraft/verdict and could write those cache fields deterministically, leaving the model only the fields it genuinely owns.
| reviewedHunks?: unknown; | ||
| wasDraft?: unknown; | ||
| }; | ||
| if (record.verdict !== "APPROVE" && record.verdict !== "REQUEST_CHANGES") { |
There was a problem hiding this comment.
suggestion (non-blocking): No test asserts stampFromCacheMemory accepts a REQUEST_CHANGES record and carries that verdict — the reconstruct test only uses APPROVE, and the REQUEST_CHANGES record seeded elsewhere is the body-wins case where the cache is never consumed. Fails toward full so it's low severity, but a one-line case would cover the accept-branch:
it("reconstructs a REQUEST_CHANGES anchor", () => {
const stamp = stampFromCacheMemory(
JSON.parse(cacheRecord({verdict: "REQUEST_CHANGES"})),
);
expect(stamp?.verdict).toBe("REQUEST_CHANGES");
});| schemaVersion: STAMP_SCHEMA_VERSION, | ||
| depth: "full", | ||
| verdict: record.verdict, | ||
| anchorDraft: record.wasDraft, |
There was a problem hiding this comment.
note (non-blocking): anchorDraft is reconstructed from wasDraft, which is the last run's draft status, whereas anchorDraft means the draft status when the anchor fingerprint was taken. Tracing both directions this is conservative-only (it can only add a full review, never miss one), but the docstring documents the depth: "full" approximation and not this one — worth a line so a future reader doesn't "fix" it by carrying stampAnchorDraft into the cache record without re-deriving the analysis.
| @@ -0,0 +1,7 @@ | |||
| --- | |||
| "review": patch | |||
There was a problem hiding this comment.
question (non-blocking): This ships as "review": patch, but the fix changes production re-review depth behavior (re-reviews that escalated to full will now anchor and run scoped/flip-gated/fast per the ROUTING dial). The two prior behavior-changing changesets (the payload seam and NOTIFIED) both shipped minor, while patch has been used for internal/eval-only fixes. Is patch intended here, or should this be minor?
|
Superseded by #279, which absorbed this change wholesale as part of deterministic-orchestrator slice 0. Verified file-by-file against
Rebasing onto main drops this commit as already-applied, leaving a zero-diff branch. Closing. |
* [jwies/review-dispatch-gate] review: block review submission when the dispatched sub-agent outputs are missing (dispatch-conformance gate) * [jwies/review-dispatch-gate] review: harden the gate's fail-open ordering; sentinel-gated job failure; per-line disclosure match (review feedback) * [jwies/review-dispatch-gate] review: protect the sentinel/pre-gate writes so the rewrite always runs once blocked (re-review feedback) * [jwies/review-dispatch-gate] review: pin keep-list survivors, Step 6 template coupling, and the Conformant summary branch (third-round nits) * [jwies/review-pre-agent-staging] review: move the staging layer into a deterministic pre-agent step (orchestrator slice 1) * [jwies/review-pre-agent-staging] review: fix the added-lines hash for ++ content lines; paginate reviews; shared hunk splitter; feedback hardening * [jwies/review-pre-agent-staging] review: tighten ghGet retry semantics; warn on a missing scoped.diff; eval roster-shrink assertion (re-review feedback) * [jwies/review-stamp-carrier] review: re-review fingerprint falls back to cache memory (the body stamp never survives gh-aw ingest) * [jwies/review-dispatch-gate-local] review: shared lenient sub-agent JSON extraction; the gate reads out-files with the dispatcher's leniency (trial run 29893634730) * [jwies/review-pre-agent-staging-local] review: the staged plan artifact mirrors stampSource (carrier-fix follow-through) * [jwies/review-pre-agent-staging-local] review: retry the secondary-rate-limit 403 (Retry-After) in the staging fetch (re-review feedback) * [jwies/autofix-v1] autofix: opt-in, one-shot fixing of the PR reviewer's feedback Label a PR `autofix: blocking` or `autofix: nits` (they union) and the run fixes the reviewer's open threads in that scope, pushes one commit, replies in each thread, posts a summary, and removes the label. One run per arming. The deterministic half (workflows/autofix/lib) decides everything before the agent edits anything: scope resolution, review-currency gating, the work list, and the commit trailer. The plan is final; the prompt may execute it or stop, never widen it. Currency is checked per file against the reviewer's own fingerprint stamp, so a PR whose author pushed one unrelated fix after the review still gets its other findings fixed rather than being refused wholesale. The push uses KHAN_ACTIONS_BOT_TOKEN, not GITHUB_TOKEN: GitHub creates no workflow runs for GITHUB_TOKEN-triggered events, and the re-review of the autofix commit is the only verification the fix gets. * [jwies/autofix-v1] autofix: add /autofix as a peer arming surface alongside the label Both surfaces resolve through one function over a shared token vocabulary, so a value cannot mean one thing as a label and another as a command. The axis model now describes tokens rather than labels. The trigger decides and the two never union: a stale `autofix: nits` label must not widen an explicit `/autofix blocking`. A command-armed run removes no labels, since a comment is self-clearing and any label present was not what armed it. The command gate is spelled out in the workflow's `if:` rather than using gh-aw's `slash_command` trigger, whose compiled gate misses a trailing CRLF and silently killed /review in Khan/webapp#40943. * [jwies/autofix-v1] autofix: fix the two faults the first live trial run exposed The bash allowlist used gh-aw's documented `"npx *"` form, which compiles to `Bash(npx)` and matches only a bare `npx` with no arguments, so the plan CLI was never executable and the run hand-simulated it instead. `"npx:*"` compiles to `Bash(npx:*)`, the form gh-aw's own defaults use. The prompt now treats a non-executable CLI as a hard stop. The currency guard collapsed "no reviews" with "reviews exist but carry no fingerprint", so a PR with real blocking feedback was told no feedback had been posted. They are now distinct, and the unstamped state degrades to the per-thread anchor check with a note in the summary rather than refusing. * [jwies/autofix-v1] autofix: work around gh-aw's unbounded PR-branch fetch on large monorepos The safe-outputs job gets a depth-1 shallow clone of refs/pull/N/merge, then push_to_pull_request_branch.cjs:935 fetches the PR branch with no --depth. The branch tip is absent and its history never reaches the shallow boundary, so git walks it all the way back. Against Khan/webapp that ran >14 min and 5.6 GB before being cancelled, which is what killed the first trial push. Depth cannot be bounded from outside: git has no fetch.depth config, no gh-aw option touches the safe-outputs checkout, and no step can be injected into that job. Injecting a partial-clone filter via GIT_CONFIG_* does reach the fetch and removes the bulk: same command, 91 s and ~557 MB, exit 0. Composes with gh-aw's own GIT_CONFIG_* use, which appends rather than overwrites (git_helpers.cjs:64-76). * [jwies/autofix-v1] autofix: move to Opus 5; settle the tagOpt question with a measurement Pin the autofixer at claude-opus-5, deliberately ahead of the reviewer's claude-opus-4-8: the reviewer's pin is tied to its eval calibration, autofix has no such tie, and writing the fix is the harder half of the pair. gh-aw v0.83.4's firewall (0.27.42) already knows Claude 5 pricing, so no sandbox.agent.version or models: override is needed. Do NOT add remote.origin.tagOpt=--no-tags. Measured back to back against Khan/webapp from identical checkouts: 85 s to completion without it, still transferring past 20 minutes with it. Also drop the em dash from the commit-message template, which is how one reached the bot's first real commit. * [jwies/autofix-v1] autofix: self-contained commit messages, exception-driven summary, fewer turns The commit subject must now name the change; `autofix: address reviewer feedback` was identical on every run and made git log useless. The body states the problem as a fact about the code, not a pointer to a thread a later reader cannot see. The summary comment is exception-driven. A clean run is already told by the thread reply, the commit, and the engine's own pushed-commit comment; a fourth notification saying the same thing trains people to ignore the bot. It still always posts a refusal, a no-op, anything left unfixed, or a degraded check. The first run spent 131 turns and $4.61 on a six-line fix while moving only ~93 KB of tool output, so the cost was turns, not payload: 7 turns to make one directory, 3 reading this workflow's own lib source, 5 hand-assembling staged JSON, 3 on --help. The prompt now closes each of those. * [jwies/autofix-v1] autofix: stage deterministically in a pre-agent step lib/stage.ts fetches everything the plan needs and writes it before the agent starts, so staging costs zero assistant turns and fails before any credits are spent. Follows the reviewer's orchestrator slice 1 (#280). Turns are what this workflow costs. The first live run: 131 turns, 460 AIC, ~93 KB of tool output total. 61% of the bill was cache reads at a 40.7:1 read-to-write ratio, so caching was already near-optimal and only the turn count could move. Staging was ~15 of those turns. Also records two findings from #287: gh-aw's ingest strips all HTML comments, so the reviewer's fingerprint never reaches a posted review (making autofix's degraded path the normal one, since cache memory is per-workflow and not reachable from here), and this workflow's own summary marker was being silently deleted too. * [jwies/autofix-v1] autofix: fix the currency guard failing open; one changeset per PR Review feedback on #298, both blocking findings and one suggestion. assessReviewCurrency returned `current` with no stale paths and no degraded note whenever computeHunkSignature produced an empty signature, which happens for an empty diff and for raw get_files patches (no diff --git/---/+++ headers for splitUnifiedDiff to recognise). The guard reported a clean full check having performed none: the one failure direction this module must not have. An unreadable diff now degrades like any other unusable input. buildWorkList never checked who opened a thread, so the reviewer-feedback-only restriction lived solely in prose. A human thread whose first line quotes `**issue (blocking):**` would have become a work item an agent edits code for. Ownership is now enforced where the decision is made. Also collapses six changesets into one, which is what a PR should carry. * [jwies/autofix-v1] autofix: reuse the stack's diff builder; close the review's remaining gaps Rebased onto the orchestrator's staging slice (#280), so the local buildUnifiedDiff copy is gone. The shared one is also more correct: mine emitted `diff --git a/<name> b/<name>` from `filename` alone, wrong for a rename and for an add or delete. A cross-package test now pins that what the reviewer's builder emits is what our currency check can still parse. Remaining #298 feedback: - The head SHA now comes from the job's checkout rather than the API. Comparing an API read against an earlier checkout left a window where a push landed between the two and both reads agreed while the tree was already stale. - Trailer parsing is scoped to the final paragraph. A revert or doc commit quoting `Autofix-Version: 1` parsed as an autofix commit and inflated the cycle count; harmless while it is only reported, load-bearing once a cadence axis caps on it. - Nothing checks the agent's own edits before the push, so the prompt now requires the run to say so rather than let an unverified fix read as a verified one. - `diffText` is documented as what it actually is (the full diff, not the stripped copy) and why that is harmless: stalePaths is consulted only for paths carrying a finding, and findings are not raised on generated files. * [jwies/autofix-v1] autofix: price Opus 5 for the proxy; match bot logins across REST and GraphQL Both faults from the second trial run (Khan/webapp#41140, run 30416237794). The agent died at turn 1 with `API Error: 400 Model "claude-opus-5" has no AI credits pricing`. claude-opus-5 is in no gh-aw-firewall release's pricing table and no sandbox.agent.version pin can fix that, so the model needs `models.default-ai-credits-pricing` (gh-aw >= v0.83.0), exactly as #294 does for the reviewer roster. The comment this replaces asserted the opposite, reasoning that the firewall was past the release which priced Fable 5; Fable 5 being priced says nothing about Opus 5. Staging reported threadCount: 0 on a PR carrying five reviewer threads. REST spells an App's login `github-actions[bot]` and GraphQL spells the same actor `github-actions`, and staging reads threads over GraphQL but reviews over REST, so one configured spelling cannot match both. Comparison is now suffix-insensitive in staging and in the worklist's ownership guard. The unit tests could not have caught this: every fixture was written in the REST spelling. * [jwies/autofix-v1] autofix: pin the firewall to the version that honours the pricing fallback Adding models.default-ai-credits-pricing was necessary but not sufficient: the proxy still 400'd claude-opus-5 for want of pricing. The staged awf-config.json from Khan/webapp#41140 run 30421726630 carried `apiProxy.defaultAiCreditsPricing: {input: 5, output: 25}` and the request was rejected anyway, so the field reaches the config and the compiler's default firewall (v0.27.42) ignores it. #294 verified awf v0.27.27 accepts and maps that field, which is why the reviewer pins it. Autofix inherited the default instead. Pinning to the version with evidence behind it. * [jwies/autofix-v1] autofix: hold at Opus 4.8; the Opus 5 pricing fallback does not work anywhere Three live runs on Khan/webapp#41140, each isolating one variable: 30416237794 no fallback configured 400 30421726630 fallback + firewall v0.27.42 (compiler default) 400 30422315631 fallback + firewall v0.27.27 (the version #294 names) 400 In both of the last two the staged awf-config.json carried `apiProxy.defaultAiCreditsPricing: {input: 5, output: 25}`, and for the third the job log confirms api-proxy:0.27.27 was the image pulled. v0.27.42 is the newest firewall release, so there is no version left to try. So `models.default-ai-credits-pricing` does not suppress the credits guard for claude-opus-5 on any currently available firewall, contrary to #294's description. Holding the model at claude-opus-4-8, which is what the first successful autofix run used, so the trial can produce the cost and comment data it exists to produce. The frontmatter records the evidence and says to restore the model, its models: block and the sandbox pin together. This blocks #294 the same way: its whole roster would 400. * [jwies/autofix-v1] autofix: correct the Opus 5 note; the cause is unestablished, not proven The previous note claimed the pricing fallback "does not work on any firewall release that currently exists". That overstates the evidence. What the three runs show is that it did not work in the configuration tried; the spec documents the mechanism, and config-mapper.ts maps the field in both v0.27.27 and v0.27.42, so version is not the variable and the pin has been dropped. Records the untried combination instead: those runs supplied the fallback AND a models.providers cost entry, and spec 10.7.1 applies the fallback only to a model that cannot be resolved, so the providers entry may be short-circuiting it. Also records that #294's own lock carries no claude-opus-5, so its verified claim is a reading of the spec rather than a run. * [jwies/review-pre-agent-staging] review: don't tell the orchestrator to read the whole staging directory Step 1 said "Read these files" above a list of a dozen staged inputs, most of which only the sub-agents consume. Taken literally that pulls the whole change's diff into the orchestrator's context, which is the waste the same sentence is trying to prevent. Say which two files are the orchestrator's and leave the rest to the steps that use them. * [jwies/autofix-v1] autofix: actually implement the guards the command path claimed Blocking finding on #298. The issue_comment branch of the workflow's `if:` carries neither the fork check nor the skip-ai-review check, because that event has no github.event.pull_request to read them from. Both this file's comment and the README said the checks "move into the plan"; the plan never implemented them, so a command-armed run reached the agent with neither. Staging now records whether the head is a fork, and plan.ts refuses a fork or a skip-ai-review PR before any work is planned. Enforced on every path rather than only the command one: a duplicated guard is cheap and a missing one authorises a code push. Unknown fork status refuses, so a staging gap cannot open the hole back up. Also records why nobody has been able to test /autofix: issue_comment is a repository-level event, so GitHub reads the workflow from the default branch and never from a PR head. Every run of the Khan/webapp#41140 trial was pull_request, and a reviewer's /autofix comment there did nothing. The command surface is only reachable once the workflow is on the consuming repo's default branch. Threads AUTOFIX_BOT_LOGIN through to the worklist's ownership guard, which was re-filtering against the hardcoded default while staging honoured the override. * [jwies/autofix-v1] autofix: lead the description with the command, not the label jeresig on #298: for a single run the /autofix convention reads better than a label. The description named only the label, which both undersold the command surface and made it look like an afterthought. They are peers; the description now says so and leads with the command. * [jwies/autofix-v1] autofix: `skip-ai-review` no longer disarms autofix The label stops the reviewer's next run; it does not withdraw a review already posted, so a labelled PR can still be carrying current findings. The reviewer even suggests the label from inside a review body it just posted, which makes "labelled, with live findings" a state the workflow steers people into rather than a corner case. Reading the label as "no AI may act on this PR" silently swallowed an explicit `autofix:` label from someone with write access, and that opt-in IS the authorisation while autofix only ever runs when a human arms it. Removed from the label path's `if:` (shared source, this repo's install, and the recompiled lock) and from `plan.ts`, where the refusal was justified as "with no review there is nothing to fix" while the guard that actually checks for a review sits ten lines below it and covers exactly that case. Tests pin the new behaviour on both surfaces: a labelled PR with a current review arms, and a labelled PR with no review still refuses on review currency. Revisit when autofix runs automatically rather than only when armed. That is when a push nobody asked for becomes possible, and autofix should get its own opt-out then rather than borrowing the reviewer's. Surfaced by the Khan/webapp#41177 docs trial: suppressing the repo's installed reviewer with the label also disarmed the autofix arm, so no trial could isolate one reviewer and exercise autofix at the same time. * autofix: fail closed when the thread fetch fails Blocking finding on #298. The GraphQL port threw only on `!res.ok`, but GraphQL does not signal failure by status: GitHub answers RATE_LIMITED, node-access failures, and partial field failures with HTTP 200 and an `errors` array. A throttled run therefore read as a successful one, `threadsConnectionOf` found no connection, `collectThreads` treated that as the end of the pages, and staging wrote an empty threads.json for a PR with open reviewer threads. Everything downstream then behaves correctly on false input, which is what makes it worth fixing rather than logging. Reviews come over REST on a separate quota, so the currency check still passes; the plan sees no actionable threads and emits a no-op; the no-op populates labelsToRemove. The label comes off and the author is told there is nothing to fix while the findings it was armed for sit open, with nothing left to re-arm from. That is the fail-open direction this module's contract rules out and the REST paths' own `throw` already rules out for them. Any `errors` entry is fatal, including alongside partial data: fixing whichever threads happened to arrive and then clearing the label is worse than refusing, because it reports success over the ones that did not. A body with no reviewThreads connection is likewise fatal rather than "no threads". Throwing fails the staging step before any AI spend and leaves the label in place. Guarded at both the transport and the reader. Duplicated on purpose, the same reasoning as the fork guard in plan.ts: the check is cheap and its absence clears an arming label on a PR with live findings. The reader is also the layer the unit tests can reach, since the real port is built inside the `require.main` block. The test harness defaulted to `{}` for unsupplied pages, which the old code read as "no threads"; it now defaults to a well-formed empty page, so the fail-closed tests are the only ones exercising the error paths. Verified the three new guards are load-bearing by reverting them: 3 fail, 27 pass. * [jwies/autofix-v1] autofix: the re-review is best-effort verification, and the summary says so The docs claimed the reviewer's re-review is the verification an autofix commit gets, in the indicative, and Step 7 made the summary comment promise it: "The reviewer will re-review this push." On Khan/webapp#41194 that promise was false. Autofix pushed ad8da8d4, the shim posted /review, and the re-review died in gh-aw's "Install AWF binary" step before the model ran. The commit sat unverified for 36 minutes until a human noticed and re-triggered. Nothing on the PR said so. That repo's reviewer is an issue_comment local override, so the run's head SHA is a default-branch merge commit and it never joins the PR's check suite; status-comment is false, so it posts nothing; and gh-aw's own fallback tried to file a failure issue and got "410 Issues has been disabled in this repository". The only trace was the eyes reaction the activation job had already added, which is indistinguishable from "still running". Of that repo's last 100 reviewer runs, 15 of the 53 that started ended in failure, so this is a shape to expect rather than a one-off. The gap is not really autofix's: where the reviewer is push-triggered, as the shared workflow ships it, a failed re-review is already a red X on the commit autofix pushed. What autofix owed the human re-arming loop was the truth, so the fix is prose in the four places that overstated it, plus one line of output. Step 7 now posts exactly one comment on every path, and states that nothing has checked the commit. The quiet-clean-run branch is retracted deliberately, and the section says so and says why: its rationale was that a clean run is already told in three other places, and all three record that something changed while none records that nothing has checked it. It was unreachable anyway, because it required an empty degradedNote and the reviewer's fingerprint stamp is stripped from every posted review. Both mandated lines have to survive the re-review landing, so neither is a conditional instruction: "if no review appears, comment /review" would leave every verified PR permanently carrying an instruction to go and trigger a review. The same objection is why verification state stays out of the commit trailer, where nothing in v1 could ever flip it. No new trigger, no detector, no trailer field. A detector would need a home that runs later than a one-shot run, and the README records the query it would ask so it need not be re-derived.
Plan-of-record: The deterministic orchestrator. Production bug surfaced by the two-arm live trial of the orchestrator stack (Khan/webapp#41006/#41007); independent of the stack, so it targets main directly. The stack merges this branch in so both trial arms exercise it.
The bug
Every full-depth review renders a hidden HTML-comment fingerprint stamp into its review body (
renderRereviewStamp), and the re-review plan CLI reads it back fromprior-reviews.jsonto decide depth. That channel has never worked: gh-aw's safe-output ingest sanitizer strips ALL XML/HTML comments (removeXmlCommentsin gh-aw-actionssetup/js/sanitize_content_core.cjs, a depth-tracking scan with no allowlist), so the stamp is deleted before the review posts.Evidence, end to end, from the trial:
safeoutputs.jsonlcontains the stamp; the ingested item insafe-output-items.jsonldoes not (run 29893634730)."reasons": ["no-prior-fingerprint"]and escalated to full depth (run 29872581602'srereview-plan.json), at 758 and 925 AI credits.Consequence: the
re-reviewROUTING dial (scoped/flip-gated/fast), the flip gate, and the divergence tripwire are all silently inert in production; every repeat review pays the full roster. The dial was measured and shipped as the runs-per-PR cost lever; it has been buying nothing.The fix
The fingerprint gets a second carrier that demonstrably works: the Step 9 cache-memory record (
/tmp/gh-aw/cache-memory/pr-<n>.json), which already storesverdict, the reviewed hunk signature, andwasDrafton every run.stampFromCacheMemoryreconstructs a stamp from that record, validating every field (a gap returns null and the plan degrades to full, never cheaper). It prefers a newstampHunksfield and falls back toreviewedHunks.runRereviewPlanClireads body stamps first (unchanged priority; they cost nothing and become load-bearing again if the sanitizer ever allows them through), then the cache record, and records which carrier anchored the plan asstampSourceinrereview-plan.jsonfor observability.stampHunks: copied verbatim fromrereview-plan.json's code-computed value, never hand-computed. Step 1's added-lines-only hash is a different regime from the CLI's added-and-removed hash; mixing them would read as full divergence.reviewedHunkskeeps serving comment scoping with Step 1's own hash on both sides.Verification
19 new vitest cases:
stampFromCacheMemoryfield validation (verdict, wasDraft, hunk-map shapes, stampHunks preference and fallback), and the CLI fallback end to end (cache anchor reduces depth withstampSource: "cache-memory", body stamp wins when present, no-anchor plans full, ready-for-review guard applies to a draft cache anchor, missing PR number and unparseable cache records degrade to full). Full review suite green (873 tests), typecheck clean, changed files lint clean.