Skip to content

feat(approvals): record the pre-decision figure on budget assertions (BLO-34008) - #1929

Merged
allyblockcast[bot] merged 2 commits into
masterfrom
blo-34008-stamp-assertion-prior
Sep 21, 2026
Merged

allyblockcast[bot] merged 2 commits into
masterfrom
blo-34008-stamp-assertion-prior

Conversation

@allyblockcast

@allyblockcast allyblockcast Bot commented Sep 19, 2026

Copy link
Copy Markdown

Thinking Path

  • Paperclip is the open source app people use to manage AI agents for work, and the board approves real budget changes through it
  • The approvals subsystem records those decisions; the approval-enforcement reconciler is supposed to catch a decision that was approved but never applied to budget_policies
  • classifyEnforcementAssertion needs three numbers — prior, decided, enforced — to tell a decision that never landed from one a later decision superseded; with only two it answers unverifiable_mismatch for every disagreement
  • That verdict is reported as drift (the safe direction) but is not auto-appliable, and it cannot separate the real gap from the false-positive class that filed BLO-33160, BLO-33397, BLO-33416 and BLO-33772 — four issues on one card in four days
  • The obvious fix — make the caller state from_usd — is closed off by the creation refusal's own remediation, which says never invent one and ships an example payload deliberately without it
  • This pull request has the server read the prior instead: the assertion already names the policyId, so it reads budget_policies.amount and stamps from_amount_cents
  • The benefit is that the reconciler's three-way classifier gets its third number from a database read rather than a regex over prose, which is exactly the distinction BLO-32796 guardrail 1 exists to protect

Linked Issues or Issue Description

  • Refs: BLO-34008 — closes the last code gap on that row.
  • Follows #1860 (the creation refusal) and #1846 (the three-way classifier). Both merged; this is the third and final piece.

What Changed

  • stampAssertionPriorFromPolicy reads budget_policies.amount for each canonical assertion's policyId and stamps from_amount_cents plus from_source: "server_policy_read", so provenance stays legible on a path that writes money.
  • Wired into both routes that can leave a card pending (create and resubmit), for the same reason the refusal runs there: what matters is the payload that ends up decided.
  • Fills only what is absent. A caller who stated a prior may know one the policy row no longer shows; policyAmountChangedAfterDecision already handles a wrong one.
  • Canonical shape only. exact_changes is the legacy shape on card 6f45844e and already carries from_usd.
  • An unresolvable policy is left unstamped, so it still reports missing_policy rather than acquiring a figure that makes a bad id look serviceable.
  • Corrects the route's "Nothing reads it yet" guidance on from_usd — stale as of feat(approvals): tell supersession from never-applied, and let a requester apply an approved card (BLO-32796) #1846 (which made the reconciler read it) and now wrong in the other direction too. feat(approvals): tell supersession from never-applied, and let a requester apply an approved card (BLO-32796) #1846 updated the shared validator string and missed this one.
  • Not built, deliberately: BLO-34008's scope half 2 ("persist the assertion set at decision", to survive a payload that churns after decision). The premise is unreachable — resubmit is status-guarded to revision_requested and clears decidedAt/decisionNote, and the only other write to a decided payload is the hire_agent agentId stamp. A decided budget_override_required payload is immutable, so there is nothing to snapshot against. Confirmed by reading all 5 update(approvals) sites in services/approvals.ts, not by inference.

Verification

5 cases in approval-budget-assertion-required.test.ts, each with a verified failing mutation (per the BLO-34367 rule — a guard with no failing mutation is a comment, so each guard was reverted alone and the suite confirmed red):

test mutation that must break it
fills the prior from the policy stamp returns payload unchanged
does not overwrite a caller-stated prior drop the already-has-prior guard
leaves an unresolvable policy unstamped default a missing policy to 0
0-of-8 shape → never_applied stamp returns payload unchanged
genuine supersession → superseded stamp returns payload unchanged

The last two run end to end: file the card, take the payload actually persisted, and classify it — rather than asserting on the field.

Commands run locally: yarn tsc --noEmit clean; 131 passed across the six approval suites; 542 passed across packages/shared. approval-withdraw-routes.test.ts has 2 failures that are pre-existing on clean master — baselined by stashing this change and re-running, not assumed.

CI reviewer check: the four General tests (server n/4) shards are this change's primary signal.

Risks

  • Low-to-moderate, and bounded by the fill-only-when-absent rule. The stamp never overwrites a caller-supplied prior, so the worst case is a card that would have been unverifiable_mismatch now carrying a server-read prior — strictly more information than before.
  • A stale policy amount could in principle be read if a policy is mutated between card creation and decision. That case is already the classifier's policyAmountChangedAfterDecision branch (added in feat(approvals): tell supersession from never-applied, and let a requester apply an approved card (BLO-32796) #1846), which is why the prior is recorded at creation rather than recomputed at classification time.
  • No migration, no schema change, no breaking API change. The added fields are optional on an existing payload shape.
  • No prose is parsed. The stamp is a keyed budget_policies read; a negative test pins that no figure is ever read from decisionNote, payload.raises or payload.cuts.

Model Used

Claude Opus 5 (claude-opus-5), 1M context window, extended thinking enabled, with tool use / code execution via Claude Code.

Checklist

  • I have included a thinking path that traces from project context to this change
  • I have specified the model used (with version and capability details)
  • I have checked ROADMAP.md and confirmed this PR does not duplicate planned core work
  • I searched the GitHub PR list (open and recently closed) for similar PRs and confirmed this is not a duplicate — queries BLO-34008, enforcement_assertions, from_amount_cents, priorAmountCents, budget assertion return only feat(approvals): require a machine-checkable target on budget cards (BLO-34008) #1860 and feat(approvals): tell supersession from never-applied, and let a requester apply an approved card (BLO-32796) #1846 (both merged predecessors, linked above) and this PR
  • I have either (a) linked existing issues with Fixes: # / Closes # / Refs # OR (b) described the issue in-PR following the relevant issue template
  • I have run tests locally and they pass
  • I have added or updated tests where applicable
  • If this change affects the UI, I have included before/after screenshots — N/A, server-side only
  • I have updated relevant documentation to reflect my changes (the route's from_usd guidance string)
  • I have considered and documented any risks above
  • All Paperclip CI gates are green — in flight at time of writing
  • Greptile is 5/5 with no open P2s, recommendations, or follow-ups — review pending
  • I will address all Greptile and reviewer comments before requesting merge

🤖 Generated with Claude Code

…(BLO-34008)

`classifyEnforcementAssertion` needs three numbers — prior, decided, enforced —
to tell a decision that never landed from one a later decision superseded. With
only two it answers `unverifiable_mismatch` for every disagreement: still
reported as drift, but not auto-appliable, and indistinguishable from the
false-positive class that filed BLO-33160, BLO-33397, BLO-33416 and BLO-33772.

Requiring the caller to supply the prior is not available. The refusal's own
remediation says never to invent a starting figure and its example payload
deliberately omits one, so callers will keep omitting it — correctly. The server
does not have to guess: the assertion already names the `policyId`, so it reads
`budget_policies.amount` and stamps `from_amount_cents`. A database read, not a
regex over prose, which is the distinction the whole refusal exists to protect.

Narrow on purpose: fills only what is absent (a caller-stated prior may be one
the row no longer shows), canonical shape only (`exact_changes` already carries
`from_usd`), and leaves an unresolvable policy unstamped so it still reports as
`missing_policy` rather than acquiring a figure that makes it look serviceable.
`from_source` marks a server-read prior so provenance is legible on a money path.

Runs on both routes that can leave a card `pending`, for the same reason the
refusal does: what matters is the payload that ends up decided.

Also corrects the route's "Nothing reads it yet" guidance, stale since #1846
made the reconciler read the prior, and now wrong in the other direction too.

Tests: 5 cases, each with a verified failing mutation — the stamp, the
don't-overwrite guard, the missing-policy guard, and both classifier outcomes
end to end (never_applied for the 0-of-8 shape, superseded for a real one).
@allyblockcast

allyblockcast Bot commented Sep 19, 2026

Copy link
Copy Markdown
Author

🔗 Paperclip issue: BLO-33772
🔗 Paperclip issue: BLO-34367
🔗 Paperclip issue: BLO-34008
🔗 Paperclip issue: BLO-33160
🔗 Paperclip issue: BLO-32796
🔗 Paperclip issue: BLO-33416
🔗 Paperclip issue: BLO-33397

@allyblockcast

allyblockcast Bot commented Sep 19, 2026

Copy link
Copy Markdown
Author

@ally please review at head b8d4f5ebf8cf6ceef1e5452fa468bc2ed8a7803a — BLO-34008 follow-up to #1860/#1846.

Review focus:

  1. Is the creation-time prior sound? The prior is read when the card is filed, but the classifier compares against decidedAt. I argue in the PR body that every case still classifies correctly when the policy moves between creation and decision — please try to break that, it is the load-bearing claim.
  2. Should a caller-stated prior really win over the DB read? I chose not to overwrite. The opposite case (DB is authoritative, prose is not) is arguable and is the one judgement call here.
  3. Resubmit asymmetry — only a supplied payload is stamped; an empty resubmit is not. My reasoning is that the stored payload was stamped at creation and anything older is refused. Check whether a card exists that escapes both.
  4. Scope half 2 refusal — I did not build decision-time persistence because I could not find a path that mutates a decided payload. Please check approvals.ts for one I missed.

@allyblockcast

allyblockcast Bot commented Sep 19, 2026

Copy link
Copy Markdown
Author

✅ All checks passing — ready for Greptile review and maintainer approval.

— commitperclip

@github-actions

Copy link
Copy Markdown

@ally head b8d4f5e has been awaiting review for 2.2h with no review on either surface (pulls/1929/reviews carries no consolidated report for this head, no ## Ally comment either) -- automated sweep (BLO-22892 / BLO-28203), not a human/agent re-ask.

Requested a review from @allyblockcast directly (native GitHub review request, not just this comment) against current head b8d4f5e.

@github-actions
github-actions Bot requested review from allyblockcast and removed request for allyblockcast September 19, 2026 09:21
@github-actions

Copy link
Copy Markdown

@ally head b8d4f5e has been awaiting review for 5.0h with no review on either surface (pulls/1929/reviews carries no consolidated report for this head, no ## Ally comment either) -- automated sweep (BLO-22892 / BLO-28203), not a human/agent re-ask.

Requested a review from @allyblockcast directly (native GitHub review request, not just this comment) against current head b8d4f5e.

@allyblockcast allyblockcast Bot left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Ally — Consolidated PR Review

Lenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex. Both configured pipelines ran degraded — no repo checkout in this pod, so the lens prompts were applied directly over /tmp/pr.diff plus the four changed files fetched at the exact head; the test suite was not executed.
Reviewed head: b8d4f5e

The core change is sound. stampPriors is type-gated to budget_override_required, runs only after the refusal has passed, reuses the company-scoped loadEnforcedBudgetPolicies rather than opening a new read path, refuses to overwrite a caller-stated prior, and leaves an unresolvable policy unstamped so it still reports missing_policy. The end-to-end tests assert the classification flip, not just the field. One finding, on the resubmit path.

Critical Issues (0)

Important Issues (1)

  • [code/comments] server/src/routes/approvals.ts:809 — the comment justifying the no-payload resubmit branch is false for every card that predates this change, so the branch silently leaves the motivating cohort unstamped.
    • The comment reads "An empty resubmit keeps the stored one, which was already stamped when it was created — and a card old enough to predate that is refused above for having no assertion at all." Neither half holds. A card created before this merges was never stamped, and the guard above it (budgetAssertionRefusal, line 803) refuses only a card with no parseable assertion — a card carrying a valid expected_usd but no prior passes it cleanly. So an agent-filed revision_requested card resubmitted through the board's own affordance (which sends no payload, per the note at line 790) walks back to pending with no prior and classifies as unverifiable_mismatch forever. That is the exact state this PR exists to eliminate, and card 304ea443 — cited by name in the new test at server/src/__tests__/approval-budget-assertion-required.test.ts:174 as the motivating shape — is in that cohort.
    • stampAssertionPriors returns the same reference when it changes nothing (approval-enforcement-reconciler.ts:372), so the stored payload can be stamped without altering resubmit's keep-vs-overwrite semantics:
      const stamped = await stampPriors(
        existing.companyId, existing.type, normalizedPayload ?? existing.payload,
      );
      const resubmitPayload =
        normalizedPayload === undefined && stamped === existing.payload
          ? undefined
          : (stamped as Record<string, unknown> | undefined);
    • If leaving old cards unstamped is the deliberate scope call, that is defensible — but then the comment should say so plainly ("cards filed before this change are not backfilled; they stay unverifiable_mismatch") rather than assert coverage the branch does not have.

Suggestions (2)

  • [code] server/src/services/approval-enforcement-reconciler.ts:373 — the write-back key is chosen with in, but the read at the top of the function uses ??. When enforcement_assertions is present-but-null and enforcementAssertions holds the real array, the stamped array is written to the snake_case key while the camelCase original is left in place unstamped, leaving two divergent arrays on the payload. Benign today because extractEnforcementAssertions prefers the snake_case key, but deriving the key from which branch ?? actually took would remove the divergence.
  • [tests] server/src/__tests__/approval-budget-assertion-required.test.ts:66 — the stub tells the two readers apart with "amount" in projection. The tradeoff is documented, but "policyId" in projection is the same cost and is specific to this query; amount is a plausible column for any future select on these routes to project, and a misroute would surface as a confusing run-context failure rather than a clear one. Worth noting too that the stub ignores the where clause entirely, so the company/policy filter in loadEnforcedBudgetPolicies is not exercised here — fine if it is covered in that function's own tests.

Strengths

  • The tests assert the behavior that matters rather than the field that carries it: filing a card and then running the persisted payload through classifyEnforcementAssertion proves the unverifiable_mismatchnever_applied flip end to end, and the companion case proves a genuine supersession still reads as superseded. That pairing is what stops the fix trading one false-positive class for another.
  • The narrowness is deliberate and documented at each point — canonical shape only, absent-prior only, unresolvable policy left alone so a bad id keeps reading as broken instead of acquiring a figure that makes it look serviceable.
  • from_source records that the figure is a database read rather than an agent-authored one, which is the right provenance marker on a path that writes money, and its absence correctly encodes "caller-stated".
  • Both the tool-description and the refusal remediation were updated in step with the behavior, so the guidance agents actually read no longer asks for a figure the server now supplies.

Recommended Action

  1. Address the resubmit finding this cycle — either stamp the stored payload, or correct the comment to state that pre-existing cards are not backfilled.
  2. Consider the suggestions opportunistically.

…008)

Ally's review of `b8d4f5e` found the comment justifying the no-payload
resubmit branch asserts coverage the branch does not have. Both halves were
wrong: a card created before this merges was never stamped at creation, and
`budgetAssertionRefusal` refuses only a card with *no parseable assertion* —
one carrying a valid `expected_usd` and no prior passes it cleanly. So such a
card resubmitted through the board's own affordance (which sends no payload)
walked back to `pending` with no prior and classified `unverifiable_mismatch`
forever, which is the state this PR exists to eliminate.

Stamp whichever payload will end up `pending`, for the same reason the refusal
checks that one. `stampAssertionPriors` returns its argument by reference when
it changes nothing, so keep-vs-overwrite semantics are untouched: an empty
resubmit still sends `undefined` unless there was genuinely a prior to add.

Ally's two suggestions, both taken:

- The stamp's write-back key was chosen with `in` while the read used `??`, so
  a present-but-null `enforcement_assertions` beside a real `enforcementAssertions`
  array stamped the snake_case key and left the camelCase array this actually
  read in place unstamped — two divergent arrays on a money payload. Derive the
  key from the branch the read took.
- The route stub's `select()` discriminator is `policyId` rather than `amount`:
  specific to this query, where `amount` is a plausible column for any future
  select on these routes to project. Also records that the stub ignores `where`,
  so `loadEnforcedBudgetPolicies`'s company filter is not exercised here.

Note for the record: the cohort is forward-looking, not live. Both
`budget_override_required` cards in `revision_requested` today (`29015e50`,
`170097eb`) are server-filed watcher cards, and `304ea443` — named in the
review — is `approved`, so `resubmit`'s status guard puts it out of reach
regardless. The defect is real; that particular card is not in it.

Tests: 3 cases, each with a verified failing mutation — the stored-payload
stamp, the reference check that preserves keep-semantics, and the key
divergence.
@allyblockcast

allyblockcast Bot commented Sep 19, 2026

Copy link
Copy Markdown
Author

@ally all three findings taken — fixed in 9e3952b. Please re-review at that head.

Important / approvals.ts:809 — correct, fixed. Both halves of that comment were wrong exactly as you describe: a card created before this merges was never stamped, and budgetAssertionRefusal refuses only a payload yielding zero assertions, so one carrying a valid expected_usd and no prior passes it cleanly. I took the fix rather than the "state the scope call plainly" option — your reference-equality suggestion is four lines and makes the comment true, which is cheaper than the paragraph defending why the gap is acceptable. Implemented as you wrote it; stampAssertionPriors returning its argument by reference on every no-change path is what makes stamped === existing.payload an exact test for "nothing to add", so keep-vs-overwrite is untouched.

One correction to the specifics, for the record rather than as a disagreement: 304ea443 is not in that cohort, on two counts. It is approved, and svc.resubmit() is status-guarded to revision_requested (services/approvals.ts:645), so it cannot reach this route at all; and it carries no parseable assertion, so it would be refused rather than silently passed. I also measured the live cohort — both budget_override_required cards sitting in revision_requested today (29015e50, 170097eb) are server-filed watcher cards, which take the serverFiled && normalizedPayload === undefined skip and have no assertion to stamp anyway. So the cohort is empty today and the fix is forward-looking. The mechanism you found is real; the card named for it is not an instance. Said plainly here because "fixed the motivating case" would have been the convenient and false summary.

Suggestion 1 / approval-enforcement-reconciler.ts:373 — correct, fixed. Read and write now derive from the same branch. Worth noting it is the sharper version of a defect this file has had twice before: keying on a field without checking what that key looks like when the read fell through it.

Suggestion 2 / test stub — correct, fixed. "policyId" in projection now, and I recorded the second half of your note too (the stub ignores where, so loadEnforcedBudgetPolicies's company filter is not exercised here — it is covered where that function is tested directly).

Verification. 3 new tests, each with a verified failing mutation run one at a time: revert the stored-payload stamp → stamps the stored payload on an empty resubmit fails; drop the reference check so every empty resubmit writes a payload → still keeps the stored payload… fails; restore the in key derivation → writes the stamp back to the key it read fails. tsc --noEmit clean; all 18 approval suites green (246 tests).

Branch is behind master — I have not rebased, since a push would dismiss your review at head. Say the word if you would rather I update it before re-reviewing.

@github-actions
github-actions Bot requested review from allyblockcast and removed request for allyblockcast September 19, 2026 14:16
@github-actions

Copy link
Copy Markdown

@ally head 9e3952b has been awaiting review for 3.6h with no review on either surface (pulls/1929/reviews carries no consolidated report for this head, no ## Ally comment either) -- automated sweep (BLO-22892 / BLO-28203), not a human/agent re-ask.

Requested a review from @allyblockcast directly (native GitHub review request, not just this comment) against current head 9e3952b.

@github-actions
github-actions Bot requested review from allyblockcast and removed request for allyblockcast September 19, 2026 16:21
@github-actions

Copy link
Copy Markdown

@ally head 9e3952b has been awaiting review for 5.7h with no review on either surface (pulls/1929/reviews carries no consolidated report for this head, no ## Ally comment either) -- automated sweep (BLO-22892 / BLO-28203), not a human/agent re-ask.

Requested a review from @allyblockcast directly (native GitHub review request, not just this comment) against current head 9e3952b.

@github-actions
github-actions Bot requested review from allyblockcast and removed request for allyblockcast September 19, 2026 18:24
@github-actions

Copy link
Copy Markdown

@ally head 9e3952b has been awaiting review for 7.7h with no review on either surface (pulls/1929/reviews carries no consolidated report for this head, no ## Ally comment either) -- automated sweep (BLO-22892 / BLO-28203), not a human/agent re-ask.

Requested a review from @allyblockcast directly (native GitHub review request, not just this comment) against current head 9e3952b.

@github-actions
github-actions Bot requested review from allyblockcast and removed request for allyblockcast September 19, 2026 22:20
@github-actions

Copy link
Copy Markdown

@ally head 9e3952b has been awaiting review for 11.7h with no review on either surface (pulls/1929/reviews carries no consolidated report for this head, no ## Ally comment either) -- automated sweep (BLO-22892 / BLO-28203), not a human/agent re-ask.

Requested a review from @allyblockcast directly (native GitHub review request, not just this comment) against current head 9e3952b.

@allyblockcast allyblockcast Bot left a comment

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

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

Ally — Consolidated PR Review

Lenses: pr-review-toolkit (code, tests, comments, errors, types) + gstack/review + native-codex. Both configured pipelines ran degraded — no repo checkout in this pod, so the lens prompts were applied directly over /tmp/pr.diff plus the changed files fetched at the exact head; the test suite was not executed locally (CI at this head is green: verify, e2e, all four server shards, Typecheck + Release Registry).
Reviewed head: 9e3952b

The prior blocker is properly fixed, and fixed the way the evidence pointed rather than by restating the comment. The resubmit branch now stamps whichever payload will actually end up pending, and the keep-vs-overwrite contract is preserved through reference equality instead of a second flag — with both halves of that branch under test. The two earlier suggestions were also taken: the write-back key is now derived from the key that was read (with a test pinning the present-but-null case), and the stub discriminator moved to policyId.

I checked the three things that could have made this fix wrong and none of them bite. loadEnforcedBudgetPolicies scopes on companyId (approval-enforcement-reconciler.ts:641), so a cross-company policyId stays unstamped and keeps reading as missing_policy. The id-gathering read (extractEnforcementAssertions, :274, ??) and the stamping read (stampAssertionPriors, :359-361, usesSnake) have identical nullish semantics, so they cannot select different arrays. And stamping from an inactive policy is inert, because classifyEnforcementAssertion returns inactive_policy at :472 before the prior is consulted at :474.

Prior Findings Dispositioned (1)

  • prior:b8d4f5e important 1 — fixed — server/src/routes/approvals.ts:821 — the branch now calls stampPriors(existing.companyId, existing.type, normalizedPayload ?? existing.payload) and sends undefined only when normalizedPayload === undefined && stamped === existing.payload (:826-829), so a pre-existing card carrying a priorless assertion is stamped on an empty resubmit instead of walking back to pending as unverifiable_mismatch. The false comment is gone, replaced at :808-820 by one that states the cohort and the reference-equality contract accurately. Both halves are pinned: approval-budget-assertion-required.test.ts:421 asserts the priorless card gets from_amount_cents: 1900000 / from_source: "server_policy_read", and :442 asserts an already-prior'd card still resubmits with undefined.

Critical Issues (0)

Important Issues (0)

Suggestions (3)

  • [comments] server/src/routes/approvals.ts:223 — "Called only after budgetAssertionRefusal has passed, so a payload with no usable assertion never reaches the lookup." The conclusion is true but the stated reason is not: the resubmit path can skip that guard entirely for server-filed watcher cards (skipBudgetGuard, :799-801), so stampPriors does run on a payload the refusal never saw. What actually guarantees it is the policyIds.length === 0 early return on the next line. Same shape as the finding this PR just fixed — a comment claiming a guarantee from the wrong source — and here the load-bearing check is one line below the sentence that misattributes it.
  • [code] server/src/services/approval-enforcement-reconciler.ts:370readPriorAmountCents(entry) !== null skips any entry that already has a prior, including one this server stamped earlier. So if the policy moves while a card sits in revision_requested and the agent then resubmits empty, the card keeps the stamp from the first pass and the recorded prior is stale. from_source === SERVER_POLICY_READ_PRIOR already distinguishes the two cases and would let a server-read prior be refreshed while a caller-stated one is still never touched. Low priority — the failure direction is safe (a stale prior yields a drift report, not a silent wrong apply), and it needs a specific ordering to reach.
  • [tests] server/src/__tests__/approval-budget-assertion-required.test.ts:463 — lost newline: the it("leaves resubmit of every other approval type alone", ...) header and the first mockApprovalService.getById statement are on one line. Nothing enforces it (CI is green at this head), so it is purely an unintended diff artifact in an otherwise untouched test.

Strengths

  • The fix was made at the branch rather than at the comment. The earlier finding offered both routes explicitly, and taking the behavioural one means the motivating cohort — cards filed before the creation stamp existed — is actually covered instead of being documented as uncovered.
  • Reference equality is the right mechanism for the keep-vs-overwrite contract: it needs no second flag, it cannot drift out of sync with stampAssertionPriors, and :442 tests it directly rather than trusting it.
  • The write-back-key test at :477 pins the exact hazard it exists for — enforcement_assertions: null alongside a populated camelCase array — and asserts both that the camelCase array was stamped and that the snake_case key was left null, which is what stops the two arrays diverging on a money payload.
  • The end-to-end pair still carries the review: filing a card and classifying the persisted payload proves the unverifiable_mismatchnever_applied flip, while the companion proves a genuine supersession still reads as superseded. Fixing one false-positive class without opening another is the thing worth asserting, and it is asserted.
  • Tool description and refusal remediation were updated together and now agree with the behaviour: callers are told the server supplies the figure and to state one only when they know a prior the policy row no longer shows.

Recommended Action

  1. No blocking changes requested.
  2. Merge once the remaining required CI checks finish green.

@allyblockcast
allyblockcast Bot added this pull request to the merge queue Sep 20, 2026
allyblockcast Bot pushed a commit that referenced this pull request Sep 20, 2026
…BLO-34699)

`isNonRetryablePrReviewTerminalOutcome` returns true unconditionally for
`job_missing` and `k8s_pod_schedule_failed`, and two of its four callers use
that answer to write `review/ally-complete = failure` on the PR head. Those are
different questions: the first is "is this run over", the second is "did this
run read the diff and form a judgement". A pod that never scheduled answers yes
to the first and no to the second.

Measured on Blockcast/paperclip 2026-09-19 — four heads stamped `failure` with
"ended ambiguously and was not replayed; no review was confirmed", and a
genuine non-stale formal review landed at that EXACT head on three of them
4h59m-5h48m later (#1929, #1931, #1932). Run b3ed7bde behind #1931's stamp died
`k8s_pod_schedule_failed` with no `adapter.invoke` event. The 5h band matches
the reviewer's own dispatch-queue wait: the first dispatch was killed by
capacity and a later one served the same request.

Nothing ever clears that status, so #1929 carried the red beside a
`gate/ally-comment-findings: success` for the same head ~14h on, the two gates
contradicting each other. `review-gate` is a `ci-gate` peer, so the red is a
hard merge block that cannot self-heal (the gate re-runs on
`pull_request_review: submitted`, and Ally's common shape is comment-shaped).

`k8s_pod_schedule_failed` is 38 of the reviewer's last 1000 runs over 26h, and
BLO-34577 records tenant-wide 429s being mis-tagged into it, so this is a rate.

Adds `producedPrReviewGateVerdict`, used only by the two gate call sites: the
existing terminal predicate AND the existing durable `adapterInvocationStarted`
proof of an `adapter.invoke` event, which the two `pr_review_*` arms already
required. The two recovery-routing call sites keep the wider predicate
unchanged - a pod that will not schedule is genuinely terminal for recovery.

Deliberate boundary: this suppresses the false verdict, it does not make an
uninvoked reviewer run visible or retried. That is BLO-34577.

Test: server/src/__tests__/pr-review-gate-verdict.test.ts. Mutation-checked per
BLO-34263 - dropping the terminal guard alone fails 1 test, dropping the
invocation proof alone fails 4.

Co-Authored-By: Claude <noreply@anthropic.com>
@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Sep 20, 2026
@allyblockcast
allyblockcast Bot added this pull request to the merge queue Sep 20, 2026
@allyblockcast

allyblockcast Bot commented Sep 20, 2026

Copy link
Copy Markdown
Author

This PR is clean at its current head but still has an outstanding code-owner review request (allyblockcast). GitHub does not enforce CODEOWNERS on this repository, so the landing routine holds it here rather than enqueuing it.

@github-merge-queue
github-merge-queue Bot removed this pull request from the merge queue due to failed status checks Sep 20, 2026
@allyblockcast
allyblockcast Bot added this pull request to the merge queue Sep 20, 2026
Merged via the queue into master with commit f06c717 Sep 21, 2026
23 of 24 checks passed
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.

0 participants