Skip to content

review: re-review fingerprint anchors on cache memory (body stamp never survives gh-aw ingest) - #287

Closed
jwbron wants to merge 1 commit into
mainfrom
jwies/review-stamp-carrier
Closed

review: re-review fingerprint anchors on cache memory (body stamp never survives gh-aw ingest)#287
jwbron wants to merge 1 commit into
mainfrom
jwies/review-stamp-carrier

Conversation

@jwbron

@jwbron jwbron commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

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 from prior-reviews.json to decide depth. That channel has never worked: gh-aw's safe-output ingest sanitizer strips ALL XML/HTML comments (removeXmlComments in gh-aw-actions setup/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:

  • The raw MCP call in a run's safeoutputs.jsonl contains the stamp; the ingested item in safe-output-items.jsonl does not (run 29893634730).
  • None of Khan/webapp#40996's three production-posted reviews carries a stamp.
  • 40996's re-review runs planned "reasons": ["no-prior-fingerprint"] and escalated to full depth (run 29872581602's rereview-plan.json), at 758 and 925 AI credits.

Consequence: the re-review ROUTING 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 stores verdict, the reviewed hunk signature, and wasDraft on every run.

  • stampFromCacheMemory reconstructs a stamp from that record, validating every field (a gap returns null and the plan degrades to full, never cheaper). It prefers a new stampHunks field and falls back to reviewedHunks.
  • runRereviewPlanCli reads 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 as stampSource in rereview-plan.json for observability.
  • Step 9 (review.md) gains stampHunks: copied verbatim from rereview-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. reviewedHunks keeps serving comment scoping with Step 1's own hash on both sides.
  • Hash-regime mismatches inside a stale cache record cannot be detected and surface as full divergence, i.e. a full review; cache eviction likewise degrades to full. Both are the pre-fix steady state.

Verification

19 new vitest cases: stampFromCacheMemory field validation (verdict, wasDraft, hunk-map shapes, stampHunks preference and fallback), and the CLI fallback end to end (cache anchor reduces depth with stampSource: "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.

@changeset-bot

changeset-bot Bot commented Jul 22, 2026

Copy link
Copy Markdown

🦋 Changeset detected

Latest commit: 22455b1

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
review Patch

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

@khan-actions-bot
khan-actions-bot requested review from a team, jeresig and kevinb-khan and removed request for a team July 22, 2026 06:04
@github-actions

github-actions Bot commented Jul 22, 2026

Copy link
Copy Markdown
Contributor

Review live A/B

Baseline: origin/main (review.md ea89dc34d0eb); candidate: working tree (review.md 227df7bd7d67).

Ruler: matcher deterministic+arbiter; corpus cc11988c0918 (9 cases).

Metric Baseline Candidate Delta
Must-catch recall 100% 100% +0%
Verdict agreement 100% 100% +0%
Noise (unmatched posted) 61% 55% -6%
Clean false flags 0 0
Judge mean quality 0.89 0.91 +0.02
Cost $9.27 $9.38
Wall clock 1078s 1240s
Cases run / skipped 9 / 0 9 / 0
Misses found-but-dropped 0 0
Findings anchor-snapped 0 0

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 --repeats to resolve smaller effects.

jwbron added a commit that referenced this pull request Jul 28, 2026
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.
jwbron added a commit that referenced this pull request Jul 28, 2026
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.
jwbron added a commit that referenced this pull request Jul 29, 2026
#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)
@jwbron
jwbron force-pushed the jwies/review-stamp-carrier branch from 63930fd to 22455b1 Compare July 29, 2026 23:16
@github-actions

Copy link
Copy Markdown
Contributor

Review Guidance

github-actions (2 files)
File Reason
rereview-mode.ts Deterministic enforcement layer of the shared reviewer; this file now decides, on every consumer PR push, which carrier anchors re-review depth, so a validation bug changes how much review every consumer PR gets. The change fails toward full review, but the reviewedHunks fallback regime mismatch (see inline) affects the tripwire note/metric.
review.md Executable reviewer prompt in every consuming repo; the Step 9 stampHunks instruction makes a model-written cache field the production fingerprint carrier for all consumers. Risky direction (a wrong fingerprint) fails toward more review, not less.

// be detected here; it surfaces as full divergence, i.e. a full review.
const signature =
validSignature(record.stampHunks) ??
validSignature(record.reviewedHunks);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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") {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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,

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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?

@jwbron

jwbron commented Jul 30, 2026

Copy link
Copy Markdown
Contributor Author

Superseded by #279, which absorbed this change wholesale as part of deterministic-orchestrator slice 0.

Verified file-by-file against main:

  • .changeset/stamp-carrier-cache-memory.md and lib/rereview-mode.test.ts are identical.
  • lib/rereview-mode.ts on main is a superset: same stampSource / stampHunks / cache-memory fallback logic, plus a refactor swapping the local splitHunks helper for the shared splitPatchHunks from ./diff.
  • review.md carries the same semantics, reworded for the new pre-agent staging: the sanitizer/cache-fallback note now lives in the prior-reviews.json description and the stampHunks cache-record block is intact. The agent-facing "stage them anyway" instruction is gone only because staging prior reviews is deterministic code now (lib/stage-pr.ts).

Rebasing onto main drops this commit as already-applied, leaving a zero-diff branch. Closing.

@jwbron jwbron closed this Jul 30, 2026
@jwbron
jwbron deleted the jwies/review-stamp-carrier branch July 30, 2026 18:37
jwbron added a commit that referenced this pull request Jul 30, 2026
* [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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants