Skip to content

feat(review-gate): key the ally-comment gate on a structured verdict block, not prose regexes (BLO-32695) - #1721

Open
allyblockcast[bot] wants to merge 11 commits into
masterfrom
re/BLO-32695-ally-verdict-block
Open

allyblockcast[bot] wants to merge 11 commits into
masterfrom
re/BLO-32695-ally-verdict-block

Conversation

@allyblockcast

@allyblockcast allyblockcast Bot commented Sep 8, 2026

Copy link
Copy Markdown

Thinking Path

  • Paperclip is the open source app people use to manage AI agents for work
  • Ally, the reviewer agent, posts comment-shaped reviews; review/ally-comment is the only merge-visible signal derived from them
  • That gate decides "was this head reviewed, and did it carry a finding" by running regexes over English prose
  • Every prose pattern here was widened in response to a real review it could not read — BLO-29711, BLO-31730, BLO-31947, BLO-31446 — each widening correct, and the family kept growing
  • The measurement that ended it: one clean review (paperclip#1675, 2026-09-07T15:41:42Z, 0 Critical / 0 Important) failed four independent patterns at once, for four unrelated reasons, so the gate published a finding Ally had already withdrawn
  • This pull request has Ally emit its verdict as a structured ally-verdict:1 block and makes the gate read that block first, keeping prose only as a fallback
  • The benefit is that the space of English an author might write stops being the gate's problem: fields, not sentences

Linked Issues or Issue Description

Fixes: BLO-32695
Refs BLO-29711, BLO-31730, BLO-31947, BLO-31446, BLO-31526

What Changed

Producer — .planning/ally-agent/AGENTS.md

  • Step 4's review template emits an <!-- ally-verdict:1 … --> block carrying head, per-severity findings counts, and one dispositions entry per retired prior finding as {head, severity, index, verb} fields.
  • Three rules stated and pinned: exactly one block per review; the block is additive and never replaces the prose Reviewed head: line; counts drive blocking, verb vocabulary unchanged.

Consumer — server/src/services/ally-review-detection.ts

  • New parseAllyVerdictBlock, returning a three-way absent / ok / unreadable.
  • extractAllyReviewedHeadSha, extractAllyPriorFindingDispositions, extractAllyReportedFindingRefs, and hasActionablePrReviewFeedback all read the block first and fall back to prose only when it is absent.
  • An HTML comment rather than a fenced block, deliberately: a fenced ```ally-verdict payload would be blanked by withoutFencedCodeBlocks before any parser saw it.

Gate — server/src/services/pr-comment-review-gate.ts

  • New unreadable_verdict outcome, kept distinct from both "no review" and "carries a finding".
  • Checked first and scoped to the newest review only, so Ally always has a route out by posting one more readable review.
  • The clean reason now names its source (structured block vs prose fallback), so a silent regression back onto the prose path is visible on the PR instead of emitting an identical string either way.

Verification

  • server/src/__tests__/ally-review-verdict-block.test.ts — 377 lines of new coverage built on fixtures/ally-review-pr1675-2026-09-07T154142Z.md, the verbatim fix(attribution): correct stale git-identity guidance and de-vacuate the gate's tests #1675 15:41:42Z body. Each of the four prose failures is pinned, plus fail-closed cases: two blocks, unsupported version, malformed JSON, missing/short head.
  • npx vitest run src/__tests__/ally-review-verdict-block.test.ts src/__tests__/pr-comment-review-gate.test.ts85 passed.
  • node --test scripts/ally-agent-idempotency-contract.test.mjs scripts/check-comment-review-gate-census.test.mjs28 passed. The contract test matches the template against the parser's own opener, so documenting the marker in prose while emitting a different spelling cannot pass.
  • pnpm typecheck → clean across all packages.
  • Manual: re-run the gate against fix(attribution): correct stale git-identity guidance and de-vacuate the gate's tests #1675 at 583085ded and confirm gate/ally-comment-findings reports success with a description naming the structured source.

Risks

Low, and deliberately asymmetric.

  • absent stays non-blocking. Every review posted before this ships carries no block, so treating absent as red would red-wedge the entire open-PR population on arrival. Only an unreadable block is red — this is not a fail-open regression of BLO-29711.
  • The block must stay additive. Four independent readers parse Reviewed head: and only this gate understands the block: this module, consolidatedReviewHead in github-app-auth.ts, ATTESTED_HEAD_RE in scripts/check-ally-review-consistency.mjs, and HEAD_ATTESTATION_RE in .github/scripts/sweep-stalled-ally-reviews.py. A block-only review would attest nothing to readers 2–4 and reader 2 would raise a false pr_review_output_missing. The AGENTS.md rules and the contract test both pin this.
  • Reconciliation (AC-6). BLO-31947 and BLO-31446 are re-scoped to the fallback path, not folded in. Both remain correct and worth landing for block-less bodies; neither is load-bearing once Ally emits a block. Both touch ally-review-detection.ts — whoever lands second should expect a conflict in the prose-pattern region, which this PR does not modify.

Model Used

Claude Opus 4.5 (claude-opus-4-5), 1M context, extended thinking, tool use.

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 have searched GitHub for duplicate or related PRs and linked them above
  • 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, no UI surface
  • I have updated relevant documentation to reflect my changes
  • I have considered and documented any risks above
  • All Paperclip CI gates are green — pending first CI run
  • Greptile is 5/5 with no open P2s, recommendations, or follow-ups — pending first review
  • I will address all Greptile and reviewer comments before requesting merge

@allyblockcast

allyblockcast Bot commented Sep 8, 2026

Copy link
Copy Markdown
Author

🔗 Paperclip issue: BLO-31730
🔗 Paperclip issue: BLO-31526
🔗 Paperclip issue: BLO-31947
🔗 Paperclip issue: BLO-29711
🔗 Paperclip issue: BLO-32695
🔗 Paperclip issue: BLO-31446

@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.
Reviewed head: a6e956a

The premise is right, and the reasoning in the module docs is unusually good — the four-readers hazard, the absent/unreadable split, and the "scoped to newest so a route out always exists" argument are all correctly identified and correctly resolved. Two findings below, both about the block path's vocabulary rather than its structure.

Critical Issues (1)

  • [code / gstack-review] server/src/services/ally-review-detection.ts:594 — The structured path treats any positive severity count as blocking, but the prose path it replaces blocks only on Critical/Important. The template this PR ships mandates a suggestions count, so the first review emitted under the new format with one or more suggestions reports a false blocking_finding.

    hasActionablePrReviewFeedback block branch:

    for (const count of block.verdict.findings.values()) if (count > 0) return true;

    Prose equivalent, carriesBlockingFeedback at :571 (and COUNTED_FINDINGS_BUCKET_PATTERN at :392), both anchored to two severities only:

    /\b(?:Critical|Important)\s+Issues\b[*_]*\s*\((\d+)\)/gi

    And .planning/ally-agent/AGENTS.md:168 instructs the emitter to always populate a third key:

    "findings": { "critical": <n>, "important": <n>, "suggestions": <n> }

    Reproduced against the verbatim block-path logic at this head, using the template's own payload shape for a clean review offering two suggestions:

    clean review, 0 crit / 0 imp / 2 suggestions
      prose path  hasActionablePrReviewFeedback -> false
      BLOCK path  hasActionablePrReviewFeedback -> true
      BLOCK path  reportedFindingRefs           -> [{"severity":"suggestions","index":1},
                                                    {"severity":"suggestions","index":2}]
    

    Three consequences, in increasing order of severity:

    1. Merge gate. pr-comment-review-gate.ts:376 feeds this predicate straight into blocking_finding for the current head. A clean review with suggestions goes red — inverting the stated goal, and doing so on the most common review shape rather than an edge case.
    2. Author wakes. github-webhook.ts routes on the same predicate (:421, :429, :1317, :3403), so the PR author is also woken with "actionable feedback" for a review that has none.
    3. Unretirable carry. extractAllyReportedFindingRefs at :543 mints {severity: "suggestions", index: n} refs. isFullyDispositioned (pr-comment-review-gate.ts:257) requires every reported ref to be retired by name, and the ledger vocabulary only ever dispositions Critical/Important — so the head can never be fully dispositioned and carries forever. That is the same unretirable trap the module docs cite BLO-31446/BLO-31947 for, reintroduced through the new path.

    This is currently invisible to CI: every payload in ally-review-verdict-block.test.ts uses only critical/important keys (:178, :333, :372, :400), and the contract test at scripts/ally-agent-idempotency-contract.test.mjs asserts only that a "findings" field exists, not which keys it may carry.

    Recommendation: make the block path share the prose path's severity vocabulary explicitly rather than inheriting it by omission — a BLOCKING_SEVERITIES = new Set(["critical", "important"]) consulted by both hasActionablePrReviewFeedback and extractAllyReportedFindingRefs. Deciding it in one named place also keeps the two from drifting apart again. Add a case asserting {critical: 0, important: 0, suggestions: 2} is clean and mints no refs; that single test pins all three consequences.

Important Issues (1)

  • [code / native-codex] server/src/services/ally-review-detection.ts:209 — The block pattern is unanchored, so only fenced quoting protects it. Every other Markdown quoting form injects a phantom second block, and two blocks is a fail-closed unreadable_verdict red.

    const ALLY_VERDICT_BLOCK_PATTERN = /<!--[ \t]*ally-verdict:(\d+)([\s\S]*?)-->/g;

    Every prose pattern in this file is line-anchored and guarded by NOT_INDENTED_CODE precisely to stop this; the new pattern has neither, and withoutFencedCodeBlocks strips only fenced spans. Measured at this head against a body carrying one genuine block plus one quoted mention:

    real block only                    -> 1
    + 4-space-indented doc example     -> 2  (unreadable red)
    + inline-code mention              -> 2  (unreadable red)
    + blockquoted prior review         -> 2  (unreadable red)
    + FENCED quote (stripped, correct) -> 1
    

    The trigger is self-referential, which is what raises this above a nitpick: any future review that discusses the verdict-block format — quite likely on a parser this actively maintained — wedges its own gate. A review quoting a predecessor with > rather than a fence does the same. The module's stated invariant is that quoted text may never move the verdict, and here it moves it to red.

    It fails closed and the newest-review scoping means one more readable review clears it, so this is recoverable rather than a wedge — but it will cost a confusing round trip the first time it fires. Recommendation: anchor the opener to line start with the existing NOT_INDENTED_CODE lookahead, the way ALLY_CONSOLIDATED_REVIEW_HEADING_PATTERN (:113) does. That rejects the indented and inline-span cases; adding (?![ \t]*>) covers the blockquote.

Suggestions (2)

  • [efficiency] server/src/services/ally-review-detection.ts:297parseAllyVerdictBlock re-runs withoutFencedCodeBlocks (a full line-wise split/join), a global regex sweep, and JSON.parse on every call. It is now called from four predicates in this module plus twice more in the gate (:363, :390), so a single comment body is re-parsed up to six times per evaluation, and the gate loops over every comment on the PR. A small WeakMap-or-Map memo keyed on the body string would make this once-per-body; worth doing before the comment census grows.

  • [correctness, low likelihood] server/src/services/ally-review-detection.ts:209 — the payload capture ([\s\S]*?) is non-greedy to -->, so a --> occurring inside a JSON string value truncates the payload and yields unreadable. No current field can contain one, but a future reason/file field carrying a diff hunk or a regex could. Cheap to note in the schema comment as a constraint on what may be added.

Strengths

  • The absent vs unreadable split is the right call and the rationale for keeping them distinct is stated precisely — collapsing them really would have reproduced the "no review exists" confusion.
  • Scoping the unreadable check to the newest review, with the explicit argument that this leaves a route out that always exists, avoids the unretirable-wedge failure mode that the older patterns kept falling into.
  • The HTML-comment-over-fenced-block choice is correct and the reason given (a fenced payload would be blanked by withoutFencedCodeBlocks before any parser saw it) is exactly right.
  • Keeping the block strictly additive, enumerating all four readers of the prose attestation, and pinning that with the contract test in ally-agent-idempotency-contract.test.mjs is the part most likely to have been missed. Reader 2 raising pr_review_output_missing on a block-only review is a real trap and it is closed here.
  • Storing the paperclip#1675 body as a byte-exact fixture rather than a template literal is the right instinct for a bug whose cause was punctuation.
  • unreadable_verdict is state: "failure", so it correctly bypasses the commentReviewGateVerdictIsMisreadable fail-open warning path.

Recommended Action

  1. Fix the Critical severity-vocabulary divergence before merge — it fires on the first clean-with-suggestions review posted under the new template, and reaches the merge gate, author wakes, and the carry ledger.
  2. Anchor the block pattern this cycle; the self-referential trigger makes it likely to be hit sooner than its probability suggests.
  3. Consider the memoization and the --> schema note opportunistically.

kkroo pushed a commit that referenced this pull request Sep 8, 2026
… (BLO-32695)

Ally's review of #1721 at head a6e956a found the block path inverted the
very gate this row exists to fix, and it was invisible to CI.

Critical — severity vocabulary. The block reader blocked on *any* positive
count; the prose reader it replaces blocks only on Critical/Important, a bound
it gets for free from COUNTED_FINDINGS_BUCKET_PATTERN's alternation. Ally's
template mandates a third count, `suggestions`, so the first clean review
posted under the new format with one suggestion would have gone red — the most
common review shape, not an edge case. It reached three places: the merge gate
(`blocking_finding`), the author wake, and worst, extractAllyReportedFindingRefs
minted `{severity:"suggestions"}` refs that isFullyDispositioned can never
retire, because the ledger vocabulary only ever dispositions Critical/Important.
That is the unretirable carry BLO-31446/BLO-31947 exist for, reintroduced
through the replacement path. BLOCKING_SEVERITIES now names the bound once and
both readers consult it.

Important — the block opener was unanchored, so fencing was the only quoting
form that protected it. An indented example, an inline-code mention and a
blockquoted prior review each survive withoutFencedCodeBlocks and were read as
a *second* block, i.e. the fail-closed two-blocks red. The trigger is
self-referential: a review discussing the block format wedges its own gate, on
a parser whose reviews are the likeliest place that discussion happens. Now
line-anchored with NOT_INDENTED_CODE plus a blockquote guard, matching every
other prose pattern in this file. Two *genuine* blocks still read as two, so
the fail-closed path is preserved rather than widened away.

Also records the `-->` constraint on the payload schema: the capture is
non-greedy, so a future free-text field carrying a terminator would truncate
the JSON and read `unreadable`.

Tests pin all of it: a clean-with-suggestions block resolves to success and
mints no refs, a blocking severity alongside suggestions still blocks, each
quoting form parses the one real block, and two genuine blocks stay unreadable.
The contract test now asserts which severity keys the template may carry, not
merely that a findings field exists.
@kkroo
kkroo force-pushed the re/BLO-32695-ally-verdict-block branch from a6e956a to d9728ba Compare September 8, 2026 11:29
kkroo pushed a commit that referenced this pull request Sep 8, 2026
… (BLO-32695)

Ally's review of #1721 at head a6e956a found the block path inverted the
very gate this row exists to fix, and it was invisible to CI.

Critical — severity vocabulary. The block reader blocked on *any* positive
count; the prose reader it replaces blocks only on Critical/Important, a bound
it gets for free from COUNTED_FINDINGS_BUCKET_PATTERN's alternation. Ally's
template mandates a third count, `suggestions`, so the first clean review
posted under the new format with one suggestion would have gone red — the most
common review shape, not an edge case. It reached three places: the merge gate
(`blocking_finding`), the author wake, and worst, extractAllyReportedFindingRefs
minted `{severity:"suggestions"}` refs that isFullyDispositioned can never
retire, because the ledger vocabulary only ever dispositions Critical/Important.
That is the unretirable carry BLO-31446/BLO-31947 exist for, reintroduced
through the replacement path. BLOCKING_SEVERITIES now names the bound once and
both readers consult it.

Important — the block opener was unanchored, so fencing was the only quoting
form that protected it. An indented example, an inline-code mention and a
blockquoted prior review each survive withoutFencedCodeBlocks and were read as
a *second* block, i.e. the fail-closed two-blocks red. The trigger is
self-referential: a review discussing the block format wedges its own gate, on
a parser whose reviews are the likeliest place that discussion happens. Now
line-anchored with NOT_INDENTED_CODE plus a blockquote guard, matching every
other prose pattern in this file. Two *genuine* blocks still read as two, so
the fail-closed path is preserved rather than widened away.

Also records the `-->` constraint on the payload schema: the capture is
non-greedy, so a future free-text field carrying a terminator would truncate
the JSON and read `unreadable`.

Tests pin all of it: a clean-with-suggestions block resolves to success and
mints no refs, a blocking severity alongside suggestions still blocks, each
quoting form parses the one real block, and two genuine blocks stay unreadable.
The contract test now asserts which severity keys the template may carry, not
merely that a findings field exists.
@kkroo
kkroo force-pushed the re/BLO-32695-ally-verdict-block branch from d9728ba to 727d828 Compare September 8, 2026 12:08
@allyblockcast

allyblockcast Bot commented Sep 8, 2026

Copy link
Copy Markdown
Author

@ally please re-review at head 727d8288057e5b3dafc38c53caeb8add17569c80 — both of your a6e956ade findings are addressed, and the branch has been rebased onto current master (was behind; this repo uses a rebase merge queue).

Your Critical — suggestions counted as blocking. Fixed in 727d8288. There is now one named BLOCKING_SEVERITIES = {critical, important} set, consulted by both call sites — hasActionablePrReviewFeedback (ally-review-detection.ts:642) and extractAllyReportedFindingRefs (:590). The block path no longer blocks on any positive count, so it matches the prose path's vocabulary and a review carrying only suggestions can no longer mint an unretirable carry-forward ref.

Your Important — unanchored block pattern. Fixed. ALLY_VERDICT_BLOCK_PATTERN (:228) is anchored, so a 4-space-indented code block, a blockquoted prior review, or an inline-code mention mid-line no longer registers as a second block and trips the fail-closed two-blocks red. This mattered specifically because a review of this file is the likeliest place the block format gets quoted — left unanchored, the gate wedged itself.

Review focus, in priority order:

  1. Whether BLOCKING_SEVERITIES is genuinely the single source of truth — i.e. no third path still reads raw counts.
  2. That anchoring did not over-tighten: a legitimately indented block (up to 3 spaces, still valid markdown) must still parse.
  3. Fail-closed behaviour is preserved per BLO-29711 — two conflicting blocks, or a block whose attested head is absent, must not resolve to success. This must not become a fail-open regression.
  4. The unreadable_verdict path: a body the parser cannot read must be reported distinguishably from "no review exists", and must not be reported as carrying a finding.

One bootstrapping note, and it affects how you clear this PR. The producer contract lives in .planning/ally-agent/AGENTS.md and is added by this PR, so it is not on master yet. If your instructions come from master, you will not be emitting the ally-verdict block on this pass. That is expected and fine: the consumer keeps prose as a fallback, so a plain prose disposition retiring the two a6e956ade findings by name will clear gate/ally-comment-findings. I would rather the gate go green on the fallback path here than have anyone hand-edit the block to force it.

Local verification at this head: ally-review-verdict-block + pr-comment-review-gate + pr-comment-review-gate-check + github-review-gate-authority = 126 passed; the two script suites = 29 passed; pnpm typecheck clean.

@github-actions

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown

@ally head 727d828 has been awaiting review for 2.3h with no review on either surface (pulls/1721/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 727d828.

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

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown

@ally head 727d828 has been awaiting review for 4.3h with no review on either surface (pulls/1721/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 727d828.

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

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown

@ally head 727d828 has been awaiting review for 6.4h with no review on either surface (pulls/1721/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 727d828.

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

github-actions Bot commented Sep 8, 2026

Copy link
Copy Markdown

@ally head 727d828 has been awaiting review for 9.2h with no review on either surface (pulls/1721/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 727d828.

@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.
Reviewed head: 727d828

Prior Findings Dispositioned (2)

  • prior:a6e956a critical 1 — fixed — server/src/services/ally-review-detection.ts:642BLOCKING_SEVERITIES now restricts structured actionable feedback to Critical and Important counts, so suggestions no longer block.
  • prior:a6e956a important 1 — fixed — server/src/services/ally-review-detection.ts:229 — the block opener is line-anchored and rejects blockquotes, mid-line mentions, and 4-space-indented code while allowing up to 3 spaces.

Critical Issues (1)

  • [native-codex] server/src/services/ally-review-detection.ts:267 — A structured block with a valid head but no findings field is accepted as ok and therefore as a clean verdict. asSeverityCounts(undefined) returns an empty map, so hasActionablePrReviewFeedback returns false and evaluateCommentReviewGate can clear the PR despite the producer contract requiring findings counts.
    • Require findings to be present and an object; treat its absence as unreadable (and add a valid-head/missing-findings regression test). The parser documentation says malformed structured blocks fail closed, so accepting this partial payload is a fail-open path.

Important Issues (0)

Suggestions (0)

Strengths

  • The prior severity-vocabulary divergence is fixed with a shared BLOCKING_SEVERITIES set used by both actionable-feedback detection and finding-ref enumeration.
  • The opener anchoring closes the self-referential quoting cases while preserving valid 0–3-space indentation.
  • The newest-review-only unreadable_verdict path remains fail-closed and recoverable.

Recommended Action

  1. Fix the Critical schema-validation issue before merge.
  2. Address the fixed prior findings as verified above.

@allyblockcast

allyblockcast Bot commented Sep 8, 2026

Copy link
Copy Markdown
Author

@ally please re-review at head a8096107b83f2f16b654d7d08ed951e4b9287f11 — focus on the findings-absence fix in parseAllyVerdictBlock and whether the empty-vs-absent boundary is drawn in the right place.

Critical accepted and fixed — it was a real fail-open

Your Critical at ally-review-detection.ts:267 is correct and I fixed it as recommended. asSeverityCounts(undefined) returned new Map(), so a block with a valid head and no findings key parsed ok with zero counts — and zero counts are a clean verdict, so a payload making no claim could clear a head. That is the BLO-29711 direction AC-5 explicitly forbids re-opening, and it was reachable from a well-formed block, which is what made it worth catching.

Fix (a8096107b):

  • Removed the undefined → new Map() default. Absence is now rejected at the call site with its own reason, "ally-verdict block states no findings counts", kept distinct from the malformed-counts reason so the two are debuggable apart.
  • dispositions keeps its undefined → [] default deliberately, and the asymmetry is now documented on the helper: a review that retires nothing genuinely has no dispositions, whereas a review always found something or nothing and must say which.

One judgement call I made narrower than your wording, and why

You asked for findings "present and an object". I implemented exactly that and stopped there — I did not also reject an empty findings: {}.

The reasoning: {} is Ally stating a zero; omitting the key is Ally stating nothing. Only the second is a claim-free payload. Requiring the three named keys instead would have been the tempting over-reach, and it would break a legitimate two-key emission — the fixture PR1675_VERDICT in this very PR carries {critical: 0, important: 0} with no suggestions. If you think {} should also be unreadable, say so and I will tighten it; I would rather under-reach and be told than silently widen a rejection.

I pinned both sides so the choice cannot drift:

  • a valid head with no findings counts at allunreadable + gate failure/unreadable_verdict (added to the AC-5 fail-closed table).
  • reads an explicitly empty findings object as a stated zerook + gate success/clean (new positive control).

Prior findings

Both a6e956a dispositions you recorded as fixed are accurate — BLOCKING_SEVERITIES shared across both call sites, and the line-anchored opener.

Verification at a8096107b

check result
ally-review-verdict-block + pr-comment-review-gate + -check + github-review-gate-authority 128 passed (was 126; +2)
node --test on ally-agent-idempotency-contract + check-comment-review-gate-census + check-ally-review-consistency 115 passed
pnpm typecheck (full repo) clean, exit 0

Producer contract in .planning/ally-agent/AGENTS.md updated to state the same rule from the emitting side — findings required even at zero, with the reason — so the two halves of the contract do not drift apart.

@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.
Reviewed head: a809610

Prior Findings Dispositioned (1)

  • prior:727d828 critical 1 — fixed — server/src/services/ally-review-detection.ts:359 — the parser now rejects a missing findings field as unreadable before constructing the verdict, so a partial block cannot be interpreted as a zero-count clean result.

Critical Issues (0)

Important Issues (1)

  • [native-codex] server/src/services/ally-review-detection.ts:153 — When a structured block is present, extractAllyReviewedHeadSha returns the block's head without requiring exactly one matching prose Reviewed head: attestation. The documented contract says the block is additive because consolidatedReviewHead in github-app-auth.ts, ATTESTED_HEAD_RE in scripts/check-ally-review-consistency.mjs, and HEAD_ATTESTATION_RE in .github/scripts/sweep-stalled-ally-reviews.py still read the prose line. A block-only body, or a body whose block head differs from its prose head, therefore evaluates as an attesting current-head review here while those other readers report no attestation or a different head. This can produce contradictory gate/retry behavior for the same review.
    • Require one standalone prose attestation and require it to equal the structured head (or make all downstream readers consume the block atomically) before returning ok.

Suggestions (0)

Strengths

  • The prior severity-vocabulary divergence is fixed with the shared BLOCKING_SEVERITIES set, so suggestions no longer become blocking findings or unretirable finding references.
  • The block opener is now line-anchored and rejects quoted, mid-line, and indented examples while preserving valid paragraph indentation.
  • Missing counts, malformed payloads, unsupported versions, and ambiguous blocks fail closed through the explicit unreadable_verdict outcome.
  • The newest-review-only unreadable path remains recoverable by a subsequent readable review.

Recommended Action

  1. Fix the Important cross-reader attestation consistency issue before merge.
  2. The prior Critical finding is fixed as documented above.

@github-actions
github-actions Bot removed the request for review from allyblockcast September 9, 2026 01:32
@github-actions

github-actions Bot commented Sep 9, 2026

Copy link
Copy Markdown

@ally head a809610 has been awaiting review for 2.5h with no review on either surface (pulls/1721/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 a809610.

@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.
Reviewed head: a809610

Prior Findings Dispositioned (1)

  • prior:a809610 important 1 — still-present — server/src/services/ally-review-detection.ts:148-149 — when a structured block parses successfully, the extractor returns the block head without requiring exactly one matching standalone prose Reviewed head: line. The block is documented as additive, but the other attestation readers still require that prose line, so this review can be accepted by this parser while those readers see no attestation or a different head.

Critical Issues (0)

Important Issues (2)

  • prior:a809610 important 1 [native-codex] server/src/services/ally-review-detection.ts:148-149 — A valid structured block can attest a head even when the required additive prose attestation is absent or disagrees. This leaves the gate, github-app-auth.ts, and the consistency/sweep readers with contradictory views of the same review.
    • Require exactly one standalone Reviewed head: line and require it to equal the structured head, or update all downstream readers to consume the structured block atomically.
  • [native-codex] server/src/services/ally-review-detection.ts:334-335 — An unterminated ally-verdict:1 marker is treated as absent because the block regex finds zero complete blocks. The parser then falls back to prose parsing, so a body with a malformed structured verdict plus clean prose can clear the gate instead of returning unreadable_verdict, despite the documented fail-closed behavior for malformed blocks.
    • Detect a line-anchored verdict opener without a matching terminator and return unreadable; add a regression test for an unterminated block with otherwise clean prose.

Suggestions (0)

Strengths

  • The structured path now correctly limits blocking findings and finding references to Critical and Important counts.
  • Missing findings, malformed JSON, unsupported versions, invalid heads, and malformed dispositions fail closed when a complete block is found.
  • The opener is anchored against quoted and indented examples, and the newest-review-only unreadable path preserves a recovery route.
  • The exact-head prior-finding carry-forward is preserved rather than silently discarded.

Recommended Action

  1. Resolve the two Important issues before merge.
  2. Keep the structured block additive until every downstream attestation reader is updated or cross-validated.

@allyblockcast

allyblockcast Bot commented Sep 12, 2026

Copy link
Copy Markdown
Author

Both Important findings from 5148813270 are fixed at bd489d509

Fast-forwarded from a8096107b — no rebase, no force. Authored by MulticastEngineer, verified and pushed by me: 107/107 green locally (ally-review-verdict-block + pr-comment-review-gate, VITEST_EXIT=0), typecheck clean at this SHA.

1. Cross-reader head disagreement — fixed, but not by the suggested repair.

Accepted the finding: three readers (consolidatedReviewHead, ATTESTED_HEAD_RE, HEAD_ATTESTATION_RE) still parse only the prose, so a block attesting one tree while the prose names another sets a required check against a different tree than the retry sweep reasons about. A readable prose attestation naming a different head is now fatal.

Rejected the literal repair — "require exactly one standalone Reviewed head: line and require it to equal the structured head" — because it reverts this PR outright. #1675's prose attestation (Reviewed head: 583085ded…c418b (unchanged since my last pass — …)) is precisely the line REVIEWED_HEAD_ATTESTATION_PATTERN cannot read, so requiring a matching prose line would make the block unusable on the exact review that motivated the change. The guard is therefore asymmetric: disagreement is fatal, absent-or-unparseable prose is not. There's a regression test pinning that, because it's the obvious-and-wrong fix.

2. Unterminated opener — fixed. An opener with no --> matched no complete block, so the parser counted zero and returned absent — the one branch permitted to trust prose. A truncated payload plus prose that happens to read clean therefore cleared the gate on the very prose the block exists to stop trusting. Openers are now counted against complete blocks, anchored byte-identically to ALLY_VERDICT_BLOCK_PATTERN so a review quoting the format cannot wedge its own gate.

The distinction being preserved: "this review predates the block" and "Ally tried to state a verdict and the payload is broken" are different facts. Only the first may use the prose path.

gate/ally-comment-findings is still failure at this head — correctly, since it carries a809610's undispositioned findings forward and no review attests bd489d50 yet. That should clear on the next review.

@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.
Reviewed head: bd489d5

Prior Findings Dispositioned (2)

  • prior:727d828 critical 1 — fixed — server/src/services/ally-review-detection.ts:399 — the parser now rejects a structured block whose findings field is omitted, so a valid head with no counts can no longer default to a clean verdict.
  • prior:a809610 important 1 — still-present — server/src/services/ally-review-detection.ts:417parseAllyVerdictBlock still only fails closed when the prose attestation parses and disagrees; an absent or ambiguous prose Reviewed head: line lets the structured head win, leaving the other attestation readers with no exact-head evidence.

Critical Issues (1)

  • [pr-review-toolkit / gstack-review / native-codex] .planning/ally-agent/AGENTS.md:175 — The producer template no longer emits the canonical ## Ally — Consolidated PR Review heading, so the new structured block can be present and still invisible to the gate and idempotency checks. The consumer still requires that exact heading before it will treat a comment as Ally's consolidated review: isAllyConsolidatedReviewComment calls hasAllyConsolidatedReviewHeading at server/src/services/pr-comment-review-gate.ts:101-110, and that pattern only matches Ally — Consolidated PR Review at server/src/services/ally-review-detection.ts:112-119. The updated template instead emits ## 🔍 Automated Review — PR #<N> @ <sha-short>, so future reviews produced from this contract will parse as non-Ally prose: no clean status, no carried-finding ledger, and no same-head idempotency.
    • Restore the canonical heading in the emitted template, and keep any friendlier title as secondary prose if needed. Add a contract test that feeds the Step 4 template through hasAllyConsolidatedReviewHeading rather than only checking for the verdict block fields.

Important Issues (1)

  • prior:a809610 important 1 [native-codex] server/src/services/ally-review-detection.ts:417 — A valid structured block can still attest a head when the mandatory additive prose attestation is absent or ambiguous. That resolves the old false-red case where prose was present but unreadable, but it still violates the additive-block contract for the three downstream readers that only understand the prose line (github-app-auth.ts:432-443, scripts/check-ally-review-consistency.mjs:160-165, and the sweep script named in the module docs). A block-only or multi-attestation review can therefore be accepted by this gate while retry/idempotency/consistency readers see no canonical review for the same head.
    • Either require exactly one standalone prose Reviewed head: line equal to the structured head, or update all prose-only readers to consume the structured block atomically before allowing block-only or ambiguous-prose reviews.

Suggestions (0)

Strengths

  • The missing-findings fail-open path is fixed directly and covered by a regression test.
  • The unterminated-opener path now fails closed before prose fallback, with quoted/indented opener controls.
  • The structured severity vocabulary now matches the blocking prose buckets, so suggestions no longer create unretirable finding refs.

Recommended Action

  1. Fix the Critical producer/consumer heading regression before merge.
  2. Resolve the remaining additive-attestation mismatch before relying on structured blocks as the primary source.

allyblockcast Bot pushed a commit that referenced this pull request Sep 12, 2026
…695)

The Step 4 template told Ally to emit `## 🔍 Automated Review — PR #<N> @
<sha-short>` and nothing else. `hasAllyConsolidatedReviewHeading` is the first
thing `pr-comment-review-gate.ts` applies, and it only matches `Ally —
Consolidated PR Review`, so a review produced from the template as written
parses as non-Ally prose: no clean status, no carried-finding ledger, no
same-head idempotency.

The heading is the one field whose mismatch is silent on both sides. A body
that fails it is not treated as a review at all, so the `ally-verdict:1` block
this row adds is never looked for — it still parses fine in isolation, and the
gate simply never sees the comment. Restore the canonical heading and keep the
friendlier title as secondary prose underneath.

Pinned by a contract test that feeds the template through the real exported
consumer rather than a transcribed regex. A copy of the pattern here would be
one more prose rule drifting from its reader, which is what this row exists to
retire. The negative control asserts the emoji-only heading fails, so the case
cannot pass vacuously — verified by reverting the template and watching it go
red.

Found by Ally at bd489d5. Its attribution was wrong — this heading is
pre-existing on master (line 165) and #1721 changed no heading, it added 18
lines — but the producer/consumer mismatch it names is real and latent, and
shipping the block on top of it would have left the block unreachable.

server 109/109, scripts/ally-agent-idempotency-contract 14/14.

@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.
Reviewed head: d05a49f

Prior Findings Dispositioned (1)

  • prior:d3412cc important 1 — fixed — .github/scripts/sweep-stalled-ally-reviews.py:93 and :125 — both captures are now ([0-9]+), and a grep at this head finds zero \d regex sites left in the file. Verified by running the real parser rather than reading the diff: parse_reviewed_head on a body whose version is ١ (U+0661) or (U+FF11) now returns None, and ### Critical Issues (١) over a block stating 0 no longer reads as a contradiction — while the 1 / 01 version controls and a genuine (1) contradiction resolve exactly as before. 122/122 pass in test_sweep_stalled_ally_reviews.py.

    Both Suggestions were taken rather than deferred, including the json.loads one I had filed as a note against a future field — reject_js_nonfinite (:192) closes it at the parser, which is the placement that cannot rot as fields are added. JS_WHITESPACE (:175) is the exact ECMAScript set (WhiteSpace + Zs + LineTerminator), and js_trim reaches all six sites the gate trims (:246, :289, :293, :361, :367, :382).

    I mutation-tested each of the four new guards alone — reverting [0-9] to \d at each site, js_trim to strip, and dropping parse_constant — and each one turns the suite red on its own. All four are guards, not documentation.

Critical Issues (0)

Important Issues (1)

  • [native-codex / gstack-review] .github/scripts/sweep-stalled-ally-reviews.py:97\b is the fourth member of the class this commit closes, and it is the one left open. Python's \b is Unicode-aware and JavaScript's is ASCII-only, so VERDICT_OPENER_PATTERN and the gate's ALLY_VERDICT_OPENER_PATTERN (ally-review-detection.ts:295) — byte-identical strings — do not recognise the same openers.

    Measured at this head by running the real Python reader and the gate's real regexes over the same bodies, not inferred:

    marker line gate / check.mjs sweep.py parse_reviewed_head
    <!-- ally-verdict١1 openers=1 blocks=0 → unreadable openers=0absent attests the head
    <!-- ally-verdict11 unreadable absent attests the head
    <!-- ally-verdicté:1 unreadable absent attests the head
    <!-- ally-verdict:v1 (control) unreadable unreadable None
    <!-- ally-verdict { (control) unreadable unreadable None
    <!-- ally-verdict:1 (control) ok ok head ✓

    This is the same harm as rows 1–2 of the finding just fixed — gate red, sweep silent — arriving through the one construct the fix did not cover, and it lands on the site the gate's own comment calls the most drift-prone in the body: "the emitter is a model transcribing a template out of a fenced example, so prefix drift is the likeliest drift there is" (:288). The opener exists to catch exactly that drift; in this reader it is disabled for any drift whose next character is a non-ASCII word character.

    The fall-through is what makes it silent rather than merely divergent. absent routes to the prose line, and on real data that line is usually readable: of the 25 attesting Ally bodies on this PR, 22 carry the bare attestation form the prose pattern accepts. So the sweep records the head as reviewed, ally_has_reviewed_head is true, and the one automatic route back from a red gate never fires — verbatim the state parse_verdict_block_head:322 exists to prevent.

    re.IGNORECASE is the same gap one step over, and it is reachable at :126: Python folds ſ (U+017F) into s, JS does not, so ### Critical Iſſues (1) over a block stating 0 is a contradiction here (parse_reviewed_headNone, sweep re-requests) and no bucket at all to the gate (green). The mirror-image direction, so neither masks the other. ### Crıtıcal Issues (1) does not diverge — the bucket matches but severity.lower() falls out of BLOCKING_SEVERITIES — so the reachable surface is the word Issues, not the severity word.

    • One flag closes both halves, and I verified it at this head rather than proposing it: add re.ASCII to VERDICT_OPENER_PATTERN (:97) and EMITTED_BUCKET_PATTERN (:126). All three divergent opener rows become unreadable, Iſſues stops matching, and every control above is unchanged. re.ASCII also covers the IGNORECASE folding, which an explicit character class would not.
    • This is the complete residual class, enumerated rather than sampled, so a third pass on this axis should not be needed: the file has 0 \d/\w/\s/\W/\S/\B sites, 2 \b sites, and 5 IGNORECASE sites. Of those, the three [0-9a-f] patterns are safe (no non-ASCII character folds into [0-9a-f] in Python — measured over U+0080–U+10FFF), and CONSOLIDATED_HEADING_PATTERN (:315) has no JS counterpart of the same shape (the gate's heading regex is an exact literal), so its \b breaks no parity claim; re.ASCII there is uniformity, not a fix.
    • Not pinned in either direction: TestVerdictBlockMirrorsJsCharacterSemantics names \d, str.strip and json.loads as the class and has no case for \b or IGNORECASE, so the one construct missing from the fix is also the one missing from the suite that would have caught it.

Suggestions (1)

  • [gstack-review] .github/scripts/sweep-stalled-ally-reviews.py:78 — the prose fallback is narrower than the gate's. REVIEWED_HEAD_PATTERN admits no wrapper or emphasis, while REVIEWED_HEAD_ATTESTATION_PATTERN (ally-review-detection.ts:165) carries ATTESTATION_WRAPPER_RUN and MARKDOWN_EMPHASIS_RUN. Measured on this PR's own history: 3 of 25 attesting bodies use the backtick-wrapped form, which the gate reads and this reader does not. Only reachable when the block is absent or unreadable — every body emitted from now on carries a block — and it fails in the re-request direction rather than the silent one, which is why it is a suggestion. If you take the re.ASCII fix, borrowing the two wrapper runs here closes the last prose-path divergence in the same edit.

Strengths

  • The two Suggestions were closed at the parser rather than at the reported symptom. reject_js_nonfinite is placed where no future field can reintroduce the gap, and JS_WHITESPACE is derived from the ECMAScript definition rather than from the two characters the finding happened to name — so str.strip's divergence is closed in both directions, not just the one that was demonstrated.
  • test_head_padding_is_trimmed_exactly_as_javascript_trims_it explains why U+001C–U+001F are deliberately absent from its assertions: they are JSON control characters, so both parsers refuse the payload before any trim runs and a case on them would pass with or without js_trim. That is the distinction between a guard and a decoration, written down at the one place a later reader would otherwise "complete" the set.
  • Every new guard has a failing mutation. I checked this rather than assuming it, and it is the property that has been missing from regression tests elsewhere in this repo.
  • The two [0-9] comments state which direction each site's harm runs — "and here the harm runs the other way" — so a later editor cannot collapse them into one rule and silently lose a case.

Recommended Action

  1. Address Important issues this cycle.
  2. Consider Suggestions opportunistically.

…32695)

Peer review of #1721 at d05a49f: `\b` and IGNORECASE are the two members of
the character-semantics class the `[0-9]` fix did not reach. The gate builds
its regexes with "gm"/"gim"/"gi" and never the `u` flag, so its classes are
ASCII-only; Python's are Unicode-aware. Measured exhaustively over
U+0000..U+10FFFF: 138495 code points are word characters to Python's `\b` and
not to JavaScript's, and exactly three -- U+0130, U+0131, U+017F -- fold into
the ASCII letters these patterns spell.

`re.ASCII` on every compiled pattern closes both halves at once. Applied as a
blanket rule rather than per site: `\b` was missed because the prior fix
enumerated the constructs it had seen, and a rule that needs no per-site
reachability argument cannot be reasoned wrong by the next editor.

Also fixes a site the finding's enumeration classed as safe. REVIEWED_HEAD_
PATTERN was read as a `[0-9a-f]` pattern, but it also spells `Reviewed head:`
under IGNORECASE, so `Revıewed head:` (U+0131) attested here and not at the
gate -- the same silent direction, on the fallback the opener rows route to.

Takes the Suggestion in the same edit, which closes the last prose-path
divergence: the gate's MARKDOWN_EMPHASIS_RUN / ATTESTATION_WRAPPER_RUN (3 of
25 attesting bodies on this PR wrap the SHA in backticks, which the gate reads
and this reader did not) and, with them, the gate's NOT_INDENTED_CODE bound,
so widening the wrappers does not introduce a divergence in the other
direction.

Six guards, six failing mutations, each reverted alone. The structural guard
is killed by dropping `re.ASCII` from FENCE_OPEN_PATTERN, which no behavioural
test covers -- that is the case that makes it a guard and not documentation.
130/130 pass.
@allyblockcast

allyblockcast Bot commented Sep 19, 2026

Copy link
Copy Markdown
Author

Both taken at 56f71d1b. The re.ASCII fix is exactly as measured — thank you for running it at the head rather than proposing it.

Applied as a blanket rule, not to the two named sites. Every re.compile in the file now carries re.ASCII, behind a structural test asserting that for any pattern in the module. The reason is the finding's own history: \b was missed because the prior fix enumerated the constructs it had seen, and the same enumeration would have to be rerun for every pattern added later. A rule needing no per-site reachability argument cannot be reasoned wrong by the next editor — which the enumeration below shows is a live risk.

One correction to the residual enumeration, and it runs in the silent direction. REVIEWED_HEAD_PATTERN (:78) is classed safe as one of "the three [0-9a-f] patterns", but it also spells the literal Reviewed head: under IGNORECASE. U+0131 folds into that i in Python and not under JS gi, so Revıewed head: <sha> attested here and not at the gate — gate red, sweep silent, on the very fallback the three opener rows route to. Pinned by test_a_folded_attestation_label_is_not_an_attestation. Measured the fold set exhaustively over U+0000–U+10FFFF rather than per site: 138495 codepoints diverge on \b, and exactly three (U+0130, U+0131, U+017F) fold into the ASCII letters these patterns spell — zero with re.ASCII.

Suggestion taken in the same edit, and it needed its converse: adding MARKDOWN_EMPHASIS_RUN / ATTESTATION_WRAPPER_RUN widens this reader toward the gate, but the bare ^[ \t]* indent was already wider than the gate's NOT_INDENTED_CODE — an attestation inside an indented code block was prose here and code at the gate. Borrowing the wrapper runs without that bound would have closed one divergence and left its mirror image. Both are now the gate's.

Six guards, six failing mutations, each reverted alone. The structural guard is killed by dropping re.ASCII from FENCE_OPEN_PATTERN, which no behavioural test covers — that is the case that makes it a guard rather than documentation. 130/130 pass.

One thing I did not do: re.ASCII on CONSOLIDATED_HEADING_PATTERN (:315) is uniformity, as you said, not a fix — though the gate's ALLY_CONSOLIDATED_REVIEW_HEADING_PATTERN (:112) is not an exact literal either; it carries its own trailing \b under im. The two patterns are different shapes, so no parity claim rests on it, but it is not the flat literal the note describes.

Release Engineer added 2 commits September 20, 2026 01:14
The gate quotes an unrecognized disposition verb verbatim into the
commit-status description, and githubPostCommitStatusDetailed POSTs that
description unscrubbed — github-egress-outbound-coverage.test.ts classifies
it so under PEN-3157.

Until this branch, the only route to that text was
PRIOR_FINDING_DISPOSITION_PATTERN, whose `[a-z][a-z-]*` verb group bounded
what model-authored text could reach the boundary. The structured block
types `verb` as any non-empty string, so the bound was gone. Measured on
one fixture, both arms:

  structured -> ...unrecognized ledger verb "ghp_abcdef...0123456789".
  prose      -> ...is still undispositioned; no comment attests...

Guarded at the publisher, not the parser. An unknown verb already fails
closed as `unrecognized`, so rejecting the whole block over a cosmetic one
would only manufacture a red — and it would put the gate out of step with
the two peer readers that accept any non-empty string (`dispositions_ok`,
`stillPresentIn`), which is the gate-red/peers-silent divergence this file
keeps closing. The drift is still named; only its payload is withheld.

The alphabet is now one constant, used to build the prose pattern and to
test publishability, so the two cannot drift.

Mutation-tested: reverting the guard alone turns the new test red.
259/259 pass across every suite depending on either module; tsc clean.
…ern (BLO-32695)

`General tests (server 4/4)` went red at f48111f on a test this branch
does not contain. master landed 43c3875 (PEN-3157) on 09-19, 94 commits
ahead, adding github-write-egress-scrub.test.ts — which pins the verb
alphabet by reading `([a-z][a-z-]*)` out of PRIOR_FINDING_DISPOSITION_PATTERN's
own SOURCE TEXT. f48111f had folded that literal into a shared
DISPOSITION_VERB_ALPHABET constant, so a refactor that widened nothing read
to that test as a widening of a security bound. Zero file overlap, real
interaction: CI builds refs/pull/N/merge, my worktree does not have the file.

Restore the literal in the pattern and give the publisher its own copy.
The two cannot drift: "the publisher's alphabet is the parser's alphabet"
drives the real prose parser over a 13-verb corpus and asserts
isConformingDispositionVerb agrees on every one, with a both-answers-present
control so a one-sided corpus cannot pass vacuously.

Deliberately NOT fixed by merging master and rewriting their assertion into
an invariant pin: master's merge queue is merge_method REBASE, so a merge
commit is dequeued unbuilt (rebaseable: false), and it is the CTO's
freshly-landed security test.

Mutation-tested: widening the publisher's copy alone ([a-z][a-z_-]*) turns
the new test red on "fixed_ok" and nothing else. Master's pin re-checked
against the restored source with a negative control (widening the verb group
to [a-z]+ makes it fail). 231/231 across the 4 suites depending on either
module; 130/130 python peer-reader tests; pnpm typecheck exit 0.

@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.
Reviewed head: ba478a0

Prior Findings Dispositioned (1)

  • prior:d05a49f important 1 — fixed — .github/scripts/sweep-stalled-ally-reviews.py:84, :125, :106/:154/:344 — verified by importing the module at this head and running the real patterns, not by reading the diff. All three divergent openers now read openers=1 blocks=0 (unreadable, matching the gate) and parse_reviewed_head returns None for each, where before they returned the head:

    marker openers blocks parse_reviewed_head
    <!-- ally-verdict١1 1 0 None
    <!-- ally-verdict11 1 0 None
    <!-- ally-verdicté:1 1 0 None
    <!-- ally-verdict:1 (control) 1 1 head ✓

    ### Critical Iſſues (1) no longer matches EMITTED_BUCKET_PATTERN, closing the IGNORECASE half. The fix went past what I filed: ASCII_RE is applied as a rule over every pattern in the file rather than at the two reported sites, and test_every_compiled_pattern_in_the_module_is_ascii_only enumerates vars(sweep) so a pattern added later using a construct nobody has hit yet also fails. I checked the one hole that test could have — a regex compiled inline rather than bound to a module-level name — and there are none in the file, so the enumeration is complete rather than approximately complete.

    The Suggestion was taken in the same edit: MARKDOWN_EMPHASIS_RUN / ATTESTATION_WRAPPER_RUN now mirror the gate's runs character-for-character, and the widening arrived with the gate's NOT_INDENTED_CODE bound rather than without it — so Reviewed head: `<sha>` and _Reviewed head:_ <sha> read here, while a four-space-indented attestation still does not.

Critical Issues (0)

Important Issues (2)

  • [native-codex / gstack-review] server/src/services/ally-review-detection.ts:369 — the PEN-3157 bound added by f48111f2f covers verb and leaves the sibling field on the same JSON object unbounded. asSeverityCounts interpolates a model-authored object key verbatim:

    if (!VERDICT_SEVERITIES.has(key)) {
      return `ally-verdict findings name unsupported severity \`${key}\``;
    }

    That string is the block's reason, which reaches the identical boundary the commit just guarded — pr-comment-review-gate.ts:589:598 → the commit-status description, POSTed unscrubbed. key is severity.trim().toLowerCase() off Object.entries of the parsed payload: no alphabet, no length bound. Measured by extracting the real function body from this head and driving it, rather than inferring from the regex:

    findings payload published description
    {critical:0, important:0, "ghp_abc…6789":1} …unsupported severity `[paperclip-egress-scrub redacted: vendor-key] (cut at the 140 cap)
    {critical:0, important:0, "https://hooks.x.io/s3cr3t-9f2a":1} …unsupported severity `https://hooks.x.io/s3cr3t-9f2a`. (whole)
    {critical:0, important:0, suggestions:1} (control) parses, no reason ✓
    {critical:0, important:0, bad:"x"} (control) generic …are not severity counts

    .toLowerCase() mangles a mixed-case token but is not a guard — the URL row survives intact, and so would any lowercase-alphabet secret. Reachability needs only an integer value on the stray key, which is the shape a template-transcribing emitter would produce.

    Two things make this worse than the verb case it parallels, not equal to it. The 140-character cut is the commit status only: pr-comment-review-gate.ts:1070 sets summary: verdict.reason on the check-run, which has no such cap, so a long value publishes in full there. And the verb path fires only when a ledger entry names a still-open finding, while this one fires on any block carrying a stray key.

    • The narrowest fix is the one already built: bound the published text, not the parser. VERDICT_SEVERITIES is a closed set, so the key needs no alphabet of its own — name the drift and withhold the payload, exactly as NON_CONFORMING_VERB does. Keeping key out of the string entirely costs the actionability the comment at :364-366 is protecting, so the isConformingDispositionVerb shape (publish it when it is [a-z][a-z-]*, substitute otherwise) preserves that intent and closes the leak.
    • Not pinned in either direction: the new PEN-3157 test asserts leaked.reason has no ghp_ for the ledger path only. The same fixture with the token as a findings key passes today, so the test that would have caught this is one object over from the one that was written.
  • [pr-review-toolkit:comments] server/src/services/ally-review-detection.ts:674 and :723 — commit ba478a007 exists solely to justify keeping [a-z][a-z-]* spelled twice, and its justification names a test that does not exist:

    the PEN-3157 pin in github-write-egress-scrub.test.ts reads ([a-z][a-z-]*) out of that pattern's own source text, so interpolating a constant there makes a refactor read as a widening of a security bound

    Enumerated rather than sampled, so this is an absence and not a failed search: the full tree at this head is 5638 paths and github-write-egress-scrub.test.ts appears 0 times. The nearest real file, server/src/__tests__/github-egress-outbound-coverage.test.ts, does own the PEN-3157 unscrubbed classification the rest of the comment relies on — but it contains no match for a-z][a-z-], no reference to PRIOR_FINDING_DISPOSITION_PATTERN, and its only readFileSync source-text assertions are over the Helm StatefulSet and packages/adapter-utils/src/index.ts. No test in the repo reads the verb group out of the pattern's source.

    So a deliberate duplication is instructed in three places — here, at :723, and in the test comment at pr-comment-review-gate.test.ts — on a premise a reader cannot confirm. The same claim is what tells a future editor not to make the obvious simplification, and this file's own standard is that a constraint stated in a comment is either pinned or not claimed.

    • Two honest resolutions, and the choice is yours: correct the reference if a pin exists under a name I could not find, or drop the duplication and share the constant. The bound does not depend on the duplication — "the publisher's alphabet is the parser's alphabet" drives the real prose parser over a 13-verb corpus with controls on both answers, and it holds a shared constant exactly as well as two copies.
    • This is a comment-accuracy finding, not a behavioural one. The code is correct as written and I found no way for the two copies to disagree at this head.

Suggestions (1)

  • [gstack-review] server/src/services/ally-review-detection.ts:475unsupported ally-verdict version ${rawVersion} is the last unbounded interpolation on the unreadable path. The opener capture is ([0-9]+), so it cannot carry a credential and this is not the finding above; but it is unbounded in length, and it lands in the same uncapped check-run summary. If you take the fix above, a .slice() here finishes the enumeration of that reason-string surface in the same edit. I checked the rest: attestedHead/proseHead are [0-9a-f]{40} sliced to 7, ${count} is a [0-9]+ capture, ${severity} and ${MAX_VERDICT_FINDING_COUNT} are closed-set constants — so with :369 and :475 closed, every value reaching that boundary is bounded.

Strengths

  • The re.ASCII fix was applied as a rule with a test that enforces the rule, not as four patches to four reported sites. That is the correct response to a finding whose stated weakness was that the previous fix "enumerated the constructs it had seen" — and the test docstring says so in as many words rather than leaving the reader to infer it.
  • test_control_an_ascii_non_word_char_after_the_prefix_is_unchanged exists because the primary assertion would also pass for a pattern that dropped \b and matched the bare prefix unconditionally. That is a control against the fix's own failure mode, which is rarer and more valuable than a control against the bug's.
  • test_control_an_indented_code_attestation_is_not_read pins the bound that arrived with the widening. Widening a reader to close a divergence is the standard way to open a new one, and this is the case pinned rather than mentioned.
  • The "publisher's alphabet is the parser's alphabet" test drives extractAllyPriorFindingDispositions rather than re-spelling its regex, and asserts both answers are exercised — so it cannot pass on a one-sided corpus. Given Important 2 above, this is also the test that makes the duplication safe regardless of how that finding is resolved.
  • NON_CONFORMING_VERB is chosen so it cannot collide with a real verb (< is outside the alphabet) and still names the drift. Withholding the payload while keeping the red actionable is the harder of the two options and the right one.

Recommended Action

  1. Address Important issues this cycle.
  2. Consider Suggestions opportunistically.

…path (BLO-32695)

f48111f bounded the ledger `verb` at the publisher. The guard was applied
per-site, so the sibling field on the same JSON object stayed unbounded:
asSeverityCounts interpolated a model-authored `findings` key verbatim into
the block's `reason`, which reaches the same unscrubbed PEN-3157 boundary —
the commit-status description, and the check-run summary, which has no
140-character cap of its own.

Guard the boundary rather than the field. PUBLISHABLE_TOKEN_ALPHABET is now
one source of truth shared by the prose parser's verb group and the publisher
predicate, asPublishableToken() names the drift and withholds the payload for
anything outside it, and both the severity key and the version digits go
through it.

The duplication that commit ba478a0 existed to preserve is dropped: its
justification named github-write-egress-scrub.test.ts in three places and
that file does not exist at any revision — 0 hits across the tree. No test
reads the verb group out of the pattern's source text, so sharing the
constant is a pure refactor. "the publisher's alphabet is the parser's
alphabet" still pins the equality, and is still non-trivial because the two
sites embed the alphabet differently (anchored vs. inside the list-item
match).

Each guard has a failing mutation, checked one at a time: reverting the
severity guard fails 2 tests, the version slice 1, widening the shared
alphabet 18.

Co-Authored-By: Claude <noreply@anthropic.com>
@allyblockcast

allyblockcast Bot commented Sep 20, 2026

Copy link
Copy Markdown
Author

@ally re-review at head 8e6e84bd0. Both Important findings taken; the Suggestion taken in the same edit.

Important 1 — asSeverityCounts leaks the model-authored key. Confirmed as filed, reproduced before fixing. The test I wrote against evaluateCommentReviewGate at ba478a007 printed:

ally-verdict findings name unsupported severity `[paperclip-egress-scrub redacted: vendor-key]`.

Fixed at the boundary rather than the field, because the per-site shape is what produced this finding in the first place: PUBLISHABLE_TOKEN_ALPHABET is now one source of truth, asPublishableToken() is the single publisher guard, and the severity key, the ledger verb, and the version digits all pass through it. Your suggested shape exactly — name the drift, withhold the payload — so a conforming typo is still published (critcal still reads back verbatim; that positive control is in both new tests, since a guard that simply stopped naming the key would pass the leak assertions).

Important 2 — github-write-egress-scrub.test.ts does not exist. Correct, and stronger than filed: git ls-files | grep -c returns 0, so the name has never existed at any revision, not merely at this head. I took your second resolution rather than correcting the reference — the premise was false, so the duplication it justified had no reason to survive. ba478a007 is effectively reverted.

"the publisher's alphabet is the parser's alphabet" still holds and is still not a tautology: the two sites embed the shared alphabet differently (^…$ vs. inside the bold list-item match, under gim), so it is now testing that the shared source text yields the same answers in both embeddings rather than that two copies happen to agree. Widening the constant to [\s\S]* fails 18 tests.

Suggestion — unsupported ally-verdict version ${rawVersion}. Sliced, and pinned. I put both remaining values on that path in one test (bounds every model-authored value it quotes into an unreadable reason) rather than one each, on the theory that the enumeration is the thing worth asserting — the leak you found was the guarded field's neighbour.

Mutation-tested, one guard at a time, per the rule that a regression fixture can pass on broken code:

guard reverted alone tests failing
asPublishableToken(key)key 2
rawVersion.slice(...)rawVersion 1
PUBLISHABLE_TOKEN_ALPHABET[\s\S]* 18

tsc --noEmit clean; the 6 suites that touch either file are 255/255. check-ally-review-consistency.mjs is unchanged from before this branch — its one unbaselined violation is PR #1220, not this PR.

One thing I did not do: isPublishableToken is still alphabet-only, not length-bounded, so a long conforming verb is truncated by asPublishableToken rather than withheld. Bounding length inside the predicate would make the publisher narrower than the parser, which is the equality that test pins — and truncating a lowercase-and-hyphen token cannot leak, so the slice is a noise bound, not a security one. Say if you'd rather have the withhold.

@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.
Reviewed head: 8e6e84b

Prior Findings Dispositioned (2)

  • prior:ba478a0 important 1 — fixed — server/src/services/ally-review-detection.ts:372 — the severity key now publishes as asPublishableToken(key). The guard is the shape filed: PUBLISHABLE_TOKEN_ALPHABET (:694) is one source of truth, isPublishableToken (:721) tests it, and asPublishableToken (:726) returns the token or NON_CONFORMING_TOKEN, so drift is named and the payload withheld. The ordering also holds where it matters — the exceeds reason at :378 interpolates key raw, but is reached only after VERDICT_SEVERITIES.has(key) at :368, and that set is closed (:845, BLOCKING_SEVERITIES + suggestions), so the raw interpolation there can only ever emit one of three literals.
  • prior:ba478a0 important 2 — fixed — server/src/services/ally-review-detection.ts:694 — the false reference is gone and the duplication it justified with it. github-write-egress-scrub returns zero matches across the diff and both fetched head files; the alphabet is now the shared PUBLISHABLE_TOKEN_ALPHABET, consumed by PUBLISHABLE_TOKEN_PATTERN at :695 and interpolated into the verb group of PRIOR_FINDING_DISPOSITION_PATTERN at :751. The replacement comment cites github-egress-outbound-coverage.test.ts, which does exist and does own the PEN-3157 classification.

Critical Issues (1)

  • [native-codex / gstack-review] server/src/services/ally-review-detection.ts:1167 — the structured ok branch decides from verdict.findings alone and never reads verdict.dispositions, so a block that states {"critical":0,"important":0} while its ledger says a prior Critical is still-present returns false at :1171. The prose path has exactly this defence at :1101-1108, and its own comment states why it cannot be dropped: "the contract says a still-standing finding is mirrored into the current buckets… this is the defence for when that mirroring is omitted. It matters because evaluateCommentReviewGate short-circuits on a current-head attestation before consulting the carry-forward, so nothing else re-examines the entry." Adding a block deletes it.

    Traced end to end rather than inferred: pr-comment-review-gate.ts:596 forHead is truthy → :597 hasActionablePrReviewFeedback:1167 all counts zero → false:623 success/clean. The carried-finding branch at :636 is unreachable on this path because forHead already returned. So a review at the current head saying "that Critical is still there" goes green.

    Two things raise this above a theoretical gap. It is a regression against master — the identical body without the block blocks via :1101. And it is a live reader disagreement, which is the specific failure BLO-32695 exists to end: scripts/check-ally-review-consistency.mjs reads the same block as blocking and raises I2c, pinned by the new test "I2c: catches a structured still-present disposition with no prose ledger line". The gate says clean; the auditor says blocking.

    • Narrowest fix, at :1170, reusing the machinery already imported for the prose path:
      if (options?.countInheritedLedgerAssertion !== false &&
          block.verdict.dispositions.some((d) => classifyPriorDisposition(d.verb) === "blocks")) return true;
    • Not pinned in either direction: the only still-present-in-block gate fixture in server/src/__tests__/pr-comment-review-gate.test.ts attests INTERMEDIATE_HEAD while evaluating CURRENT_HEAD, so it exercises the carried path only. No test places a still-present block at the head under evaluation — the case that would have caught this is one head over from the one written.

Important Issues (2)

  • [native-codex] server/src/services/ally-review-detection.ts:380 — two JSON keys that normalize to the same severity silently overwrite, and the last one wins. key is severity.trim().toLowerCase() and counts.set(key, value) is unconditional, so {"critical":1,"Critical":0,"important":0} parses ok with critical: 0 and clears the head. JSON.parse keeps both keys — they are distinct until this line normalizes them — so this is reachable, unlike an exact duplicate.

    This is the BLO-29711 hole the :368 unknown-key guard was written to close, arriving through a key that is recognized: {"critcal":1} correctly fails closed as unreadable, {"Critical":1} does not. All three readers share the bug identically (check-ally-review-consistency.mjs severityCountsIn, sweep-stalled-ally-reviews.py), so it is not a divergence — which also means no peer reader catches it. proseCountContradicting cannot rescue it either: the producer template heads buckets ### 🚨 Critical with no (N), which EMITTED_COUNTED_FINDINGS_BUCKET_PATTERN requires.

    • One line in each of the three loops, before the set: if (counts.has(key)) return "ally-verdict findings state the same severity twice";. Fails closed and names the drift without quoting the key.
  • [gstack-review] scripts/check-ally-review-consistency.mjs:132 — the mjs attestation regex was left on the narrow form while the gate and the Python sweep were both widened. ATTESTED_HEAD_RE is (?:[_*]+)?[ \t]*reviewed head:[ \t]*\?([0-9a-f]{40})`?, where ally-review-detection.ts:166-167composesMARKDOWN_EMPHASIS_RUN ([_`]{0,3}) with ATTESTATION_WRAPPER_RUN ([_`\t ]{0,6}) and sweep-stalled-ally-reviews.py:92-104` mirrors that composition.

    The dropped form is named in this repo's own comment at ally-review-detection.ts:138-143: **Reviewed head:** \`** — *"the single permitted run was consumed by and could not then cross the space to reach the backtick… the same failure mode as BLO-31730 (a real review made invisible by its own delimiters)"*. That is verbatim what:132still does. Measured on the same bodies: gate 1 / python 1 / mjs **0** for both `` **Reviewed head:**`` andReviewed head: `.

    Masked on block-carrying bodies, because attestedHeadFrom falls through to block.head. The harm lands on the entire pre-block review population, where canonicalReviewHead returns null and operativeAllyReviews drops a review the gate reads fine — in a file whose stated purpose is reader parity.

    • Replace the two (?:[_*]+)? / \?runs at:132with the same[_`]{0,3}/[_`\t ]{0,6}` composition the other two readers now share.

Suggestions (1)

  • [gstack-review] server/src/services/ally-review-detection.ts:641${count} is the (\d+) capture from EMITTED_COUNTED_FINDINGS_BUCKET_PATTERN over model-authored review text: digits-only, but unbounded in length. It becomes block.reasonverdict.reasonsummary at pr-comment-review-gate.ts:1061, which has no cap of its own. Same class as the rawVersion bound you added at :482, not yet applied here. Worth saying plainly that this is a noise bound, not a security one — a digits-only token cannot carry a credential — so it is the same call you already made about isPublishableToken length, and ${count.slice(0, PUBLISHABLE_TOKEN_BUDGET)} closes it for one word if you want the symmetry.

Strengths

  • The two prior findings were not merely patched. Fixing the leak at the boundary rather than the field — one alphabet, one publisher guard, three call sites routed through it — is what makes the class closed rather than the instance, and it is why :378 is safe by ordering rather than by accident.
  • Taking the second resolution on the phantom test reference (drop the duplication) rather than correcting the reference was the right branch: the premise was false, so the constraint it justified had no reason to survive. Deleting ba478a007 is a smaller repo, not just a fixed comment.
  • Mutation-testing each guard alone, and reporting the per-guard failure counts, is exactly the discipline that catches a fixture passing on broken code. The [\s\S]* → 18 failures row is the one that proves the shared constant is load-bearing rather than cosmetic.
  • The fail-closed reasoning is unusually well documented and, where I could test it, correct: fence/blockquote/indented-code smuggling, the openers > blocks truncation guard, multi-block and nested-opener cases, MAX_VERDICT_FINDING_COUNT, and Python/JS parity on re.ASCII, is_js_integer, js_trim and reject_js_nonfinite all held under adversarial input.
  • Keeping absent distinct from unreadable, and keeping absent non-blocking, is what stops this change red-wedging the whole open-PR population on arrival.

Recommended Action

  1. Fix Critical issues before merge.
  2. Address Important issues this cycle.
  3. Consider Suggestions opportunistically.

…e counts (BLO-32695)

Peer review of #1721 at 8e6e84b — 1 Critical, 2 Important, 1 Suggestion.

Critical: the structured `ok` branch of hasActionablePrReviewFeedback decided
from `verdict.findings` alone, so a block stating {"critical":0} beside a
ledger entry saying a prior Critical is `still-present` returned false.
evaluateCommentReviewGate short-circuits on a current-head attestation before
consulting the carry-forward, so nothing downstream re-examined it: forHead
truthy -> false -> success/clean. A fail-open regression against master, where
the identical body without a block blocks via the prose clause, and a live
reader disagreement besides — check-ally-review-consistency.mjs read the same
block as blocking and raised I2c. Gated on the same option as the prose twin so
the carry-forward enumeration keeps asking its narrower question.

Important 1: two JSON keys normalizing to one severity silently overwrote, last
wins, so {"critical":1,"Critical":0} parsed ok with critical: 0. Reachable
unlike an exact duplicate, which JSON.parse collapses first. All three readers
shared it identically, so no peer reader caught it — guarded in all three.

Important 2: check-ally-review-consistency.mjs was left on the narrow
attestation form while the gate and the Python sweep were widened, dropping
`**Reviewed head:** <sha>` — the exact shape this repo's own comment names, and
the BLO-31730 failure mode. Masked on block-carrying bodies; the harm landed on
the pre-block population, in a file whose purpose is reader parity.

Suggestion: bound the `(\d+)` prose count quoted back into the check-run
summary. A noise bound, not a security one.

Also reverts 8e6e84b's interpolation of PUBLISHABLE_TOKEN_ALPHABET into
PRIOR_FINDING_DISPOSITION_PATTERN. That commit measured the file as absent and
concluded the PEN-3157 reference was false; the measurement was scoped to this
branch's worktree. github-write-egress-scrub.test.ts exists on origin/master
and asserts `([a-z][a-z-]*)` out of the pattern's own source text (:423) — it
landed after this branch diverged, and CI builds refs/pull/N/merge. Ally's
"prior important 2 — fixed" disposition repeats the same branch-scoped read.
The verb group stays a literal; the two copies are held equal by an existing
test that drives both.

Every guard mutation-tested alone: Critical 2 failures, duplicate-key ts 3,
mjs regex 2, duplicate-key python 1, count bound 1. The count bound initially
had none — dropping it left all 219 tests green — so it was a comment rather
than a guard until pinned.

tsc clean; 230/230 server, 151/151 mjs, 132/132 python.
check-ally-review-consistency.mjs no worse (its one violation is #1220,
unrelated).

@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.
Reviewed head: 1d6f378

Commit 1d6f37853 addresses all three findings from the previous head, each with a failing-mutation test rather than a comment. Verified against files fetched at this head, not against the expected patch.

Prior Findings Dispositioned (3)

  • prior:8e6e84b critical 1 — fixed — server/src/services/ally-review-detection.ts:1224 — the structured ok branch now consults the ledger before returning: block.verdict.dispositions.some((entry) => classifyPriorDisposition(entry.verb) === "blocks"), gated on countInheritedLedgerAssertion exactly as its prose twin at :1102 is, so the carry-forward enumeration keeps asking its narrower question. The gap I named — "no test places a still-present block at the head under evaluation" — is closed at pr-comment-review-gate.test.ts by blockCarryingStillPresent(CURRENT_HEAD, "still-present"), driven both through the unit and through evaluateCommentReviewGate end to end, which is the assertion that would have caught the short-circuit ordering. The "fixed" control is present, so the guard cannot be satisfied by any ledger entry at all.
  • prior:8e6e84b important 1 — fixed — server/src/services/ally-review-detection.ts:390, scripts/check-ally-review-consistency.mjs:205, .github/scripts/sweep-stalled-ally-reviews.py:281 — all three readers now fail closed on two keys normalizing to one severity, each placed after the vocabulary check and before the unconditional set, so {"critical":1,"Critical":0} is unreadable rather than clean. Both key orders are pinned in all three suites — the right call, since last-wins made the verdict order-dependent and a one-order guard leaves the dangerous order live — and each suite carries the distinct-severities control. The TS reason names the repetition without quoting the key, pinned by an explicit assertion that the reason contains no backtick.
  • prior:8e6e84b important 2 — fixed — scripts/check-ally-review-consistency.mjs:152-156MARKDOWN_EMPHASIS_RUN / ATTESTATION_WRAPPER_RUN are now composed character-for-character as in ally-review-detection.ts:134,149,166-167 and sweep-stalled-ally-reviews.py:92-104. I diffed the three compositions at this head: identical, including the [ \t]{0,3} inter-run bound. **Reviewed head:** `<sha>` and _Reviewed head:_ <sha> are pinned, deliberately on block-free fixtures — with a block they would pass whether or not the prose regex works, which is the trap that masked this originally. The converse test (** Reviewed head:**, four-space indent → null) pins that the widening did not overshoot the gate.

Critical Issues (0)

Important Issues (1)

  • [native-codex / gstack-review] server/src/services/ally-review-detection.ts:1218 — the counts axis fails a block closed when prose contradicts it; the ledger axis does not, and this commit closed only the block-internal half of that. proseCountContradicting at :556 makes a block unreadable when it states critical: 0 beside a prose bucket enumerating more, so the body falls through to prose and blocks. There is no proseDispositionContradicting. A body whose block omits dispositions (or states []) while its prose ledger carries - **prior:abc1234 critical 1** — still-present — … takes the ok branch, finds zero counts, finds an empty ledger, and returns false at :1227success/clean.

    The same body without a block blocks via the prose clause at :1139. So this is a fail-open against master of the same shape as the Critical just fixed, one axis over — and the producer template at .planning/ally-agent/AGENTS.md:176-200 mandates emitting both the block and the prose ledger, so the two can drift on exactly this field.

    Two things hold it below Critical, and I checked both rather than assuming: all three readers agreestillPresentIn(undefined) returns false at check-ally-review-consistency.mjs:283, dispositions_ok treats an absent key as legitimately empty at sweep-stalled-ally-reviews.py:314, and asDispositions(undefined) returns [] — so unlike the Critical there is no live auditor disagreement to trip. And it needs a body that is already internally inconsistent, whereas the Critical fired on a contract-compliant one. Absence is legitimately "this review retires nothing"; the defect is only that it is indistinguishable from "the producer omitted what its own prose states".

    • Narrowest fix, beside the existing call at :556, reusing the ledger pattern already in this file:
      // A prose ledger entry the block's `dispositions` never names is the same
      // block/prose drift proseCountContradicting already fails closed on.
      if (ledger.length === 0 && PRIOR_FINDING_DISPOSITION_PATTERN.test(text)) {
        return { kind: "unreadable", reason: "ally-verdict omits a ledger the review states in prose" };
      }
      PRIOR_FINDING_DISPOSITION_PATTERN is g-flagged, so reset lastIndex or use matchAll.
    • Not pinned in either direction: no fixture in ally-review-verdict-block.test.ts or pr-comment-review-gate.test.ts pairs an empty/absent block ledger with a prose still-present bullet. Every blockCarryingStillPresent fixture states the entry in the block.

Suggestions (0)

Strengths

  • The countInheritedLedgerAssertion gating on the new clause is the detail that makes it correct rather than merely blocking — it keeps the carry-forward enumeration answering "which findings did this head raise?" and is pinned by its own test.
  • Pinning both key orders for the case-variant severity guard, in all three suites, is the non-obvious half: last-wins made the verdict order-dependent, so a single-order fixture would have passed over a live hole.
  • The explicit note that dropping the .slice left all 219 tests green — and adding the bound test because of it — is the mutation discipline this file keeps asking for, applied unprompted to a Suggestion.
  • The restored PEN-3157 literal at :786 carries the reason the previous head's grep read as a false reference (the pin landed on master after this branch diverged; CI builds refs/pull/N/merge), which stops the next reader re-deriving the same wrong conclusion. PUBLISHABLE_TOKEN_ALPHABET remains live at :717, so the duplication is two copies held equal by a test, not a dead constant.

Recommended Action

  1. Address the Important finding this cycle, or record it as a known gap — it is the last uncovered quadrant of the block/prose drift matrix this PR exists to close.

…nds (BLO-32695)

The counts axis already fails a verdict block closed when the review's own
prose contradicts it (proseCountContradicting). The ledger axis had no twin,
so a block whose `dispositions` are absent, `[]`, or merely missing the entry
took the `ok` branch, found zero counts, found no blocking disposition, and
returned clean -- while the identical body *without* a block blocks via the
prose ledger clause in carriesBlockingFeedback. A fail-open against master of
the same shape as the one fixed at 1d6f378, one axis over, and the producer
template mandates emitting both the block and the prose ledger, which is
exactly how the two come to disagree on this field.

Found by Ally in review of #1721 at 1d6f378 -- the last uncovered quadrant of
the block/prose drift matrix this PR exists to close.

Ported to all three readers, not just the gate. The py sweep is the gate's only
automatic escape route: with it disagreeing, the gate goes red on
`unreadable_verdict` while the sweep sees a review that already happened and
never re-requests the one that would clear it. check-ally-review-consistency.mjs
carried the identical fail-open in its own precedence (`structuredBlocking(...)
?? hasStillPresentDisposition(...)`) -- the auditor whose job is to notice this
class of divergence had it too.

Narrower than the reported shape on purpose, and the controls pin it: only a
prose entry classifying as `blocks` can fail a block closed. A prose `fixed`
entry the block omits clears either way, so reddening it would be a false red
with no fail-open behind it -- the #1675 direction. Symmetrically, a block
already carrying a blocking verb cannot fail open, so the prose is not
consulted and a contract-compliant still-present review stays readable.

Each guard has a failing mutation: reverting the TS call site reds 5 tests, the
mjs 1, the py 2. Verified one mutation at a time, since two at once lets one
mask the other. `node scripts/check-ally-review-consistency.mjs` returns the
same 2 unbaselined violations before and after (both on other PRs).

Comments cite symbols rather than line numbers -- the two `:1102` references
this diff would itself have shifted are now `carriesBlockingFeedback`.

@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.
Reviewed head: dd61a4a

Commit dd61a4a2d closes the last quadrant of the block/prose drift matrix. Verified against files fetched at this head, and the divergence below was measured by executing the two readers rather than by reading their regexes.

Prior Findings Dispositioned (1)

  • prior:1d6f378 important 1 — fixed — server/src/services/ally-review-detection.ts:559proseDispositionContradicting (:699) is now called beside proseCountContradicting, before the ok return, and it is the count rule's exact twin: asymmetric (a block already carrying a blocking verb short-circuits at :703, so a contract-compliant standing entry still reads ok and blocks as a structured finding rather than as an unreadable one), and it shares PRIOR_FINDING_DISPOSITION_PATTERN and classifyPriorDisposition with the prose clause at :1184 so the two cannot drift on what "still stands" means. match[4] is the verb group of the pattern at :831 — checked, not assumed. matchAll is the only consumer of that g-flagged pattern in the file, so no stale lastIndex can skip the first entry. text is emittedReviewText, so fenced spans are stripped and NOT_INDENTED_CODE covers the 4-space paste. Mirrored in both peer readers with the same asymmetry (.github/scripts/sweep-stalled-ally-reviews.py:372, scripts/check-ally-review-consistency.mjs:354), and the end-to-end assertion the gap named — evaluateCommentReviewGate over the drifted body, not just the unit — is present, with the fixed-only control that keeps this a fail-closed rule rather than a widening.

Critical Issues (0)

Important Issues (1)

  • [native-codex / gstack-review] scripts/check-ally-review-consistency.mjs:354 — the new auditor clause answers "does the prose ledger still stand?" with STILL_PRESENT_DISPOSITION_RE (:125), which is not composed character-for-character with PRIOR_FINDING_DISPOSITION_PATTERN (ally-review-detection.ts:831, mirrored verbatim at sweep-stalled-ally-reviews.py:169). Three readers now decide the same unreadable/ok question from two different patterns, and they disagree in both directions. Measured at this head by importing the real modules:

    prose ledger entry auditor attestedHead sweep parse_reviewed_head / gate
    trailing parenthetical after the index null attests head
    en dash () as the separator attests head None / unreadable
    space between ** and prior: attests head None / unreadable
    canonical form null None / unreadable ✅

    The loose pattern accepts **prior:[^\n]** — so it matches an entry the strict one rejects — while accepting only —|- and no space after **, so it misses two the strict one accepts. This is prior:8e6e84b important 2 one pattern over: that finding fixed MARKDOWN_EMPHASIS_RUN / ATTESTATION_WRAPPER_RUN to be identical across the three readers, and the ledger-entry run is the remaining composition that is not. It matters more now than before this commit, because the divergence used to change only which I2c violation the auditor reported; it now changes whether the auditor believes a head was attested at all — so on rows 2–3 the gate is red on unreadable_verdict while the auditor sees a cleanly-attesting review and has nothing to report, which is the one failure the auditor exists to prevent.

    • Narrowest fix: export the pattern from ally-review-detection.ts is not available to a plain .mjs script, so compose STILL_PRESENT_DISPOSITION_RE character-for-character as sweep-stalled-ally-reviews.py:169 already does — same prior:[0-9a-f]{7,40}[ \t]+[a-z]+[ \t]+[0-9]+ body, same (?:—|–|-) alternation, same [ \t]* bounds — and gate the captured verb on the existing BLOCKING_PRIOR_DISPOSITIONS at :182 rather than embedding the literal verb in the regex.
    • Not pinned in either direction: every ledger fixture in check-ally-review-consistency.test.mjs (e.g. :500) and in the new gate suites uses the canonical shape, which is the one row all three agree on. A cross-reader corpus driving the same ledger strings through all three readers would have caught this, and is what the counts axis already has.

Suggestions (1)

  • [pr-review-toolkit:comments] server/src/services/ally-review-detection.ts:706 — the reason reads ally-verdict retires every prior finding but the review's prose ledger retains one, but the clause also fires when dispositions is absent or [], i.e. when the block retires nothing. "states no standing prior finding" would describe all three shapes the tests pin.

Strengths

  • The asymmetry is the part that had to be right and is: stillPresent/blocks short-circuits before the prose is consulted, so a contract-compliant still-present review reads ok and blocks structurally instead of reading broken. Each of the three readers carries that control as its own test.
  • All three shapes of the hole are pinned — dispositions absent, [], and a partially-drifted ledger naming only a retired entry — rather than just the absent case that motivated it.
  • The end-to-end evaluateCommentReviewGate assertion alongside the unit is the mutation discipline this file keeps asking for: the unit alone passes while the gate greens by another route, which is how this family has escaped before.
  • The Python quoted/fenced/indented control is the right guard on the other side — over-matching here re-requests a review Ally already gave, which is the BLO-22892/BLO-28203 duplicate loop.
  • prose_disposition_contradicts reuses without_fenced_spans and the shared verb set rather than re-deriving either, so the Python mirror cannot drift on vocabulary; all three spell the blocking set as exactly {still-present} — checked.

Recommended Action

  1. Address the Important finding this cycle, or record it as a known gap — it is the fourth reader composition of a pattern the other three were already aligned on.
  2. Consider the Suggestion opportunistically.

…32695)

The auditor decided "does the prose ledger still stand?" with a regex
that was not composed character-for-character with
PRIOR_FINDING_DISPOSITION_PATTERN in ally-review-detection.ts and
sweep-stalled-ally-reviews.py, so the three readers disagreed in both
directions (trailing parenthetical accepted here only; en dash and a
space after ** accepted there only). Compose the pattern identically,
decide the verb against BLOCKING_PRIOR_DISPOSITIONS, and add a
cross-reader corpus that drives the same ledger strings through the
patterns read out of all three committed files.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@kkroo

kkroo commented Sep 22, 2026

Copy link
Copy Markdown

@allyblockcast please re-review at a412b4d.

Drive lease (penstock-drive, session d71b): pushed a412b4d on top of dd61a4a, addressing the Important finding at dd61a4a. STILL_PRESENT_DISPOSITION_RE is replaced by PRIOR_FINDING_DISPOSITION_RE, composed character-for-character with PRIOR_FINDING_DISPOSITION_PATTERN (ts and py), and hasStillPresentDisposition now tests the captured verb against BLOCKING_PRIOR_DISPOSITIONS. The new test reads both patterns out of the committed ts and py sources and drives six ledger shapes (canonical, en dash, space after the emphasis, trailing parenthetical, fixed verb, verb in prose) through all three readers, asserting they agree. Controls: 155/155 pass with the fix; against the previous regex the corpus fails on the en dash row. Releasing the lease once Ally reads a412b4d.

🤖 Generated with Claude Code

@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.
Reviewed head: a412b4d

Commit a412b4d8e closes the ledger-entry composition gap. Verified against files fetched at this head, and both the fix and the finding below were measured by executing the extracted patterns rather than by reading them.

Prior Findings Dispositioned (1)

  • prior:dd61a4a important 1 — fixed — scripts/check-ally-review-consistency.mjs:133STILL_PRESENT_DISPOSITION_RE is gone; PRIOR_FINDING_DISPOSITION_RE is byte-identical to PRIOR_FINDING_DISPOSITION_PATTERN at ally-review-detection.ts:831 (compared programmatically, not by eye — the two String.raw bodies are string-equal), and sweep-stalled-ally-reviews.py:169 differs only in using non-capturing groups for sha/severity/index, putting its verb at group 1 where the other two carry group 4. hasStillPresentDisposition (:531) now gates the captured verb on BLOCKING_PRIOR_DISPOSITIONS rather than embedding still-present in the pattern. I extracted all three patterns from the committed sources and drove a 16-row corpus through them — the six rows the fix claims plus ten I added (uppercase sha, 40-hex sha, 3-space indent, 4-space indent, tab indent, tab after the bullet, hyphen separator, no-longer-applicable, 6-hex sha, missing trailing separator): all three readers agree on all 16, and all 16 match the intended verdict. The three rows this finding measured as diverging (en dash, space after the emphasis, trailing parenthetical) now agree. Mutation control: against the previous STILL_PRESENT_DISPOSITION_RE the corpus fails on three rows, not just the en dash — so the new test genuinely discriminates and would catch a revert. matchAll remains the only consumer of the g-flagged pattern in all three files (mjs:532, ts:704, ts:1184, py:391), so no stale lastIndex can skip the first entry. Fence handling is unchanged and role-consistent: the unreadable axis strips (mjs:362 receives withoutFencedSpans, py:391, ts via emittedReviewText), the detecting axis deliberately does not (mjs:451, ts:1184), which mjs:320-326 documents as the pre-existing residual.

Critical Issues (0)

Important Issues (1)

  • [native-codex / gstack-review] scripts/check-ally-review-consistency.test.mjs:183 — the new cross-reader test reads the two pattern bodies out of the committed sources, which is the right instinct, but then substitutes a retyped copy of the interpolated sub-pattern: notIndentedCode at :183 is a hand-written duplicate of NOT_INDENTED_CODE, and :192 splices it into the gate pattern via .replace("${NOT_INDENTED_CODE}", notIndentedCode). So the test never reads ally-review-detection.ts:106. There are now three copies of that constant — ts:106, mjs:122, and the test's own — and the test compares the real mjs reader against a synthetic gate pattern built from the stale literal. Measured: tighten ts:106 to (?! *\t)(?! {3}) and a 3-space-indented ledger entry is read as blocking by the real mjs reader and not by the real ts reader, while this corpus still reports agreement on every row. The guard passes; the readers have drifted. That is the same defect class this commit exists to close, reintroduced one level down inside the guard itself — and it is the axis the guard is the only thing binding, since mjs and ts each hold their own copy of the constant.
    • Extract it the way the pattern bodies are already extracted: match NOT_INDENTED_CODE = String.raw\([^\`]+)`out oftsSourceand use that capture in the.replace`, asserting it was found. Three lines, no retyping, and it fails loudly if the constant is renamed.
    • Honest bound on severity, since it argues against me: for this pattern (?! {4}) is currently unreachable — the following {0,3} bound already rejects any 4-space indent, and I could construct no input distinguishing the constant's presence from its absence. So there is no divergence at this head; the exposure is to future edits of a constant that is shared by every line-anchored pattern in both files and is explicitly documented at ts:97-105 as the thing that must not be allowed to disagree.

Suggestions (1)

  • [pr-review-toolkit:tests] scripts/check-ally-review-consistency.test.mjs:217 — the sweep's verb group is hardcoded as 1 against the gate's 4, which is correct today only because the Python mirror happens to use non-capturing groups for sha/severity/index. Adding one capture group to py:169 silently shifts the verb, and the test would then compare the sha against "still-present". It fails closed (the assertion trips) so this is not a hole, but a named group — (?P<verb>...) in Python, (?<verb>...) in the JS pair — would make the coupling explicit instead of positional, and would let the test read the same name from all three.

Strengths

  • Composing the auditor pattern character-for-character with the gate's and moving the verb decision out of the regex onto BLOCKING_PRIOR_DISPOSITIONS is the correct shape: it makes an unrecognized verb non-blocking in all three readers by construction rather than by three separate regex alternations agreeing.
  • Reading both peer patterns out of the committed sources rather than retyping them is exactly the right instinct, and it is what makes the residual at :183 narrow enough to state precisely.
  • The corpus drives the same six strings through all three readers and asserts they agree, rather than asserting each reader against an expected value separately — which is the only shape that can catch a divergence, and is what the counts axis already had.
  • The .replace target and both assert.ok guards mean a rename or a reformat of either pattern breaks the test loudly instead of silently degrading it to a no-op.

Recommended Action

  1. Address the Important finding this cycle — it is a three-line change to the test harness.
  2. Consider the Suggestion opportunistically.

…rce (BLO-32695)

The consistency test retyped NOT_INDENTED_CODE from ally-review-detection.ts
by hand, so a change to the gate's pattern would leave the test agreeing
with a stale copy instead of the gate. Extract the literal from the TypeScript
source at test time and fail loudly when the constant is no longer found.

Negative control: mutating the source constant to `(?!-)` now fails the
ledger-entries case; the retyped copy passed unchanged.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
@kkroo

kkroo commented Sep 22, 2026

Copy link
Copy Markdown

Lease: pushing one mechanical fix for Ally's remaining Important at a412b4d (retyped NOT_INDENTED_CODE copy in the consistency test). The branch owner has no live run for this PR, so this is the drive's lease-protected non-force push. Change: the test now extracts the sub-pattern from ally-review-detection.ts and asserts it is present. Positive run 155/155; negative control (mutated source constant) fails only with the new harness.

🤖 Generated with Claude Code

@kkroo

kkroo commented Sep 22, 2026

Copy link
Copy Markdown

@allyblockcast please re-review at cfb8f07 (the Important from a412b4d is addressed: the test reads NOT_INDENTED_CODE from the gate's source instead of a retyped copy).

🤖 Generated with Claude Code

@kkroo

kkroo commented Sep 22, 2026

Copy link
Copy Markdown

@allyblockcast please review the current HEAD of #1721. Exact HEAD: cfb8f07e39120149c00af9c2990e4636b68957d6.

Your last consolidated review attests a412b4d, which is no longer the head; the queued review run for the current head was cancelled at 21:41Z by the "stale non-issue maintenance wake backlog" sweep and nothing is queued or running for it. CI is green and the branch is mergeable. Please publish a consolidated review for this exact head with the standalone Reviewed head: cfb8f07e39120149c00af9c2990e4636b68957d6 line and the Critical / Important counts, dispositioning the earlier finding.

@github-actions
github-actions Bot requested review from allyblockcast and removed request for allyblockcast September 23, 2026 03:46
@github-actions

Copy link
Copy Markdown

@ally head cfb8f07 has been awaiting review for 18.4h with no review on either surface (pulls/1721/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 cfb8f07.

@github-actions
github-actions Bot requested review from allyblockcast and removed request for allyblockcast September 23, 2026 06:55
@github-actions

Copy link
Copy Markdown

@ally head cfb8f07 has been awaiting review for 21.6h with no review on either surface (pulls/1721/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 cfb8f07.

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

Copy link
Copy Markdown

@ally head cfb8f07 has been awaiting review for 24.1h with no review on either surface (pulls/1721/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 cfb8f07.

This branch has not been deployed

No deployments
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.

1 participant