Skip to content

fix(review-gate): honor Ally's prior-findings disposition ledger - #1568

Merged
kkroo merged 3 commits into
masterfrom
fix/ally-gate-prior-findings-ledger
Aug 31, 2026
Merged

fix(review-gate): honor Ally's prior-findings disposition ledger#1568
kkroo merged 3 commits into
masterfrom
fix/ally-gate-prior-findings-ledger

Conversation

@kkroo

@kkroo kkroo commented Aug 31, 2026

Copy link
Copy Markdown

Thinking Path

  • Paperclip is the open source app people use to manage AI agents for work
  • One of those agents, Ally, reviews pull requests, and review/ally-comment makes its findings merge-visible because GitHub ignores COMMENTED reviews for reviewDecision
  • That gate carries a finding forward from an earlier head until a later clean review of that same head clears it, but Ally never re-reviews an old head — it moves to the new one and closes the old finding in its next review, under a ### Prior Findings Dispositioned heading
  • The gate never read that ledger, so a finding Ally had already closed kept a PR red with nothing left that could ever clear it
  • This pull request teaches the gate to read the ledger, retiring prior findings by their full identity rather than by head alone
  • The benefit is that PRs stop wedging on findings their own reviewer has already dispositioned, without weakening what the gate blocks on

Linked Issues or Issue Description

No existing issue — describing inline as a bug report.

What happened. On Blockcast/libmmt#362, Ally reviewed head c9a1765 and recorded:

### Prior Findings Dispositioned (1)
- **prior:731ced5 critical 1** — fixed — the test now calls `MmtFecDecoder::new(30, false).unwrap()` …
### Critical Issues (0)
### Important Issues (0)

After one more push, review/ally-comment reported:

failure — An unresolved finding from Ally's review of 731ced5 is still
          undispositioned; no comment attests the current head.

Expected. The finding was closed by its own reviewer, so the gate should not still be blocking on it.

Actual. headsWithUndispositionedFinding keyed comments by the head each attests. A finding on head H could only be cleared by another review of H, which Ally does not do, so the PR stayed red until a human merged past it.

Regression range. Introduced with the carry behavior in #1464 (BLO-29711), which added the per-head carry without a ledger reader.

What Changed

  • ally-review-detection.ts: parse ### Prior Findings Dispositioned entries into {shortSha, severity, index}, and enumerate a review's own findings from its counted Critical/Important Issues (N) headings.
  • pr-comment-review-gate.ts: retire a head only when every finding it raised is named by a later ledger entry. Matching is per finding, not per head.
  • Disposition vocabulary is an allowlist: fixed and no-longer-applicable retire, still-present blocks, anything unrecognized does not retire.
  • A ledger entry must be at least as new as the attestation it clears and come from a review of a different head, so a review cannot disposition its own finding.
  • Abbreviated SHAs match as a prefix, never a substring.
  • Corrected the stale docstring claiming this gate "cannot wedge a PR".

Verification

pnpm exec vitest run in server/:

  • pr-comment-review-gate.test.ts — 25 passed (8 new)
  • the other six review-gate files plus github-webhook.test.ts — 304 passed across 8 files
  • pnpm exec tsc --noEmit — clean

Every new test was confirmed to discriminate by mutating the implementation and checking a different test goes red each time. This matters because five of the eight assert that dispositioning does not happen, so they pass against a no-op and prove nothing on their own:

mutation test that goes red
revert the fix entirely libmmt#362 regression
match head only, ignore severity+index partial-ledger
accept any disposition verb still-present + unknown-verb
drop no-longer-applicable from the allowlist no-longer-applicable retirement
drop the time-ordering guard ledger-predates-finding
substring instead of prefix match prefix-anchoring
treat non-enumerable findings as retired prose-only finding

Risks

Moderate — this is merge-gating logic, so the failure that matters is a false green.

  • Widening what clears a finding. Mitigated by matching the full (head, severity, index) identity: one fixed entry cannot retire a sibling finding the ledger never named. Ally caught exactly this on an earlier revision of this PR.
  • Non-enumerable findings. A body whose blocking feedback came from prose or an uncounted heading yields no identities to match. extractAllyReportedFindingRefs returns null rather than [] there, because [].every(...) is true and "nothing to enumerate" would otherwise have read as "nothing left to retire".
  • Vocabulary drift. An allowlist, not a denylist. A future verb leaves a PR visibly red until someone widens the set, rather than silently clearing a live finding. The no-longer-applicable omission surfaced that way during review — as a red gate, not a merged regression.
  • No schema, config, or deployment change. Parsing only.

Known follow-up, deliberately not in this diff: when a ledger uses an unrecognized verb the gate stays red with no indication why. That is observability, not correctness — the verdict is right either way — and this is already a twice-grown merge-gate change. Happy to fold it in here if reviewers prefer.

Dedup finding worth flagging: #1471 (open, BLO-29711) also touches server/src/__tests__/pr-comment-review-gate.test.ts. No functional overlap — it moves the status context out of the review/ namespace, this changes disposition parsing — but whichever lands second will likely need a merge in that test file.

Model Used

Anthropic claude-opus-5 (1M context), via Claude Code.


  • I searched for similar open/closed PRs and confirmed this is not a duplicate

@allyblockcast

allyblockcast Bot commented Aug 31, 2026

Copy link
Copy Markdown

Hey @kkroo! Before this PR can be reviewed, a few things need attention:

Missing or incomplete:

  • Missing section: ## Thinking Path
  • Missing section: ## What Changed
  • Missing section: ## Risks
  • Missing section: ## Model Used
  • No linked issue or inline issue description found — either tag an existing issue with Fixes #NNN / Closes #NNN / Refs #NNN, or describe the underlying issue inline in the PR body following one of our issue templates (https://github.com/paperclipai/paperclip/tree/master/.github/ISSUE_TEMPLATE). See CONTRIBUTING.md → "Link Issues or Describe Them In-PR".
  • Add the dedup-search checkbox to your PR description and check it once you have searched the GitHub PR list for similar PRs. See the PR template at .github/PULL_REQUEST_TEMPLATE.md and CONTRIBUTING.md → "Before You Start: Search First".

Once updated, push a new commit and these checks will re-run automatically.

— commitperclip

@allyblockcast allyblockcast Bot left a comment

Copy link
Copy Markdown

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: 6b5b6e8

Critical Issues (0)

Important Issues (1)

  • [native-codex] server/src/services/pr-comment-review-gate.ts:187-193 — the prior-findings ledger is matched only by abbreviated head SHA, so one fixed ledger entry clears every blocking finding raised on that head. A review can report both prior:<head> critical 1 and prior:<head> important 1, then disposition only the critical item; isDispositioned still removes the entire head from headsWithUndispositionedFinding, allowing the important finding to disappear from the gate.
    • Preserve and match the complete stable finding identity (head, severity, and 1-based index), and only retire the specific finding named by the ledger. Add a regression test with two findings on one prior head where only one is marked fixed.

Suggestions (0)

Strengths

  • The parser fails closed for unrecognized disposition verbs and uses prefix matching for abbreviated SHAs.
  • The implementation preserves ordering and timestamp safeguards for later ledgers.

Recommended Action

  1. Fix the Important issue before merge.
  2. Re-run the review-gate and consistency suites.

@kkroo
kkroo force-pushed the fix/ally-gate-prior-findings-ledger branch from 6b5b6e8 to 4659ebf Compare August 31, 2026 07:52
@kkroo

kkroo commented Aug 31, 2026

Copy link
Copy Markdown
Author

Addressed the Important finding from the review of 6b5b6e8 in 4659ebf.

The finding was correct: matching the ledger on the abbreviated head SHA alone meant a single fixed entry retired every finding raised on that head, so a review reporting both prior:<head> critical 1 and prior:<head> important 1 could disposition only the critical item and the important one would disappear from the gate.

Ally already emits a stable per-finding identity — severity plus a 1-based index within that bucket — and the counted Critical/Important Issues (N) headings enumerate what a head raised. A head is now retired only once every finding it reported is named by a later ledger entry.

One case worth calling out beyond the report: a body whose blocking feedback came from prose or an uncounted heading yields no identities to match against. extractAllyReportedFindingRefs returns null there rather than an empty list, and the head stays carried — otherwise "no enumerable findings" would have read as "nothing left to retire" and cleared it.

Three tests added, including the two-findings/one-fixed regression requested. Each was confirmed to discriminate by mutation: reverting to head-only matching reds the partial-ledger test, and treating non-enumerable findings as retired reds the prose test.

@allyblockcast

allyblockcast Bot commented Aug 31, 2026

Copy link
Copy Markdown

Hey @kkroo! Before this PR can be reviewed, a few things need attention:

Missing or incomplete:

  • Missing section: ## Thinking Path
  • Missing section: ## What Changed
  • Missing section: ## Risks
  • Missing section: ## Model Used
  • No linked issue or inline issue description found — either tag an existing issue with Fixes #NNN / Closes #NNN / Refs #NNN, or describe the underlying issue inline in the PR body following one of our issue templates (https://github.com/paperclipai/paperclip/tree/master/.github/ISSUE_TEMPLATE). See CONTRIBUTING.md → "Link Issues or Describe Them In-PR".
  • Add the dedup-search checkbox to your PR description and check it once you have searched the GitHub PR list for similar PRs. See the PR template at .github/PULL_REQUEST_TEMPLATE.md and CONTRIBUTING.md → "Before You Start: Search First".

Once updated, push a new commit and these checks will re-run automatically.

— commitperclip

@allyblockcast allyblockcast Bot left a comment

Copy link
Copy Markdown

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. Toolkit and gstack skill bodies were unavailable in this runtime; their required review categories were applied directly to the exact PR diff.
Reviewed head: 4659ebf

Prior Findings Dispositioned (1)

  • prior:6b5b6e8 important 1 — fixed — server/src/services/pr-comment-review-gate.ts:196-203 now matches the complete prior-finding identity by head prefix, severity, and index, so one ledger entry cannot retire unrelated findings from the same head.

Critical Issues (0)

Important Issues (1)

  • [native-codex] server/src/services/ally-review-detection.ts:83-88 — the ledger only treats fixed as a resolving disposition. The review contract also defines no-longer-applicable as retiring a prior finding, and the regex accepts that verb, so such a valid ledger entry is silently ignored and the finding remains blocking indefinitely.
    • Include no-longer-applicable in RESOLVED_PRIOR_DISPOSITIONS and add a regression test proving that disposition retires the named prior finding without allowing unrelated findings to clear.

Suggestions (0)

Strengths

  • The ledger now preserves severity and 1-based finding index, preventing partial-ledger entries from clearing an entire prior head.
  • Prefix-anchored SHA matching and timestamp ordering provide conservative identity and chronology checks.
  • The added tests cover partial retirement, unknown verbs, stale ordering, prefix matching, and unenumerated findings.

Recommended Action

  1. Fix the Important issue before merge.
  2. Re-run the review-gate and consistency suites.

kkroo added 3 commits August 31, 2026 08:39
The comment-review gate carried a finding forward from an earlier head
until a later clean review of that *same* head cleared it. Ally never
does that — it moves to the new head and closes the old finding in its
next review, under a "Prior Findings Dispositioned" heading naming the
head the finding was raised against. The gate never read that block, so
a finding Ally had already closed kept the PR red once the head moved on
again.

Seen on Blockcast/libmmt#362: Ally's review of c9a1765 recorded
`prior:731ced5 critical 1 — fixed`, and review/ally-comment still
reported "An unresolved finding from Ally's review of 731ced5 is still
undispositioned" against the following head.

The existing docstring names exactly what was missing — a clean review of
another head can't disposition H because nothing establishes that the
other head contains the fix. The ledger is that evidence: it's a direct
assertion about a named prior tree, not an inference from comment order.

Only `fixed` clears. `still-present` asserts the opposite, and the
sibling consistency guard already treats it as blocking (I2c). An
unrecognized verb doesn't clear either, so a new word in Ally's
vocabulary can't silently unblock a merge before someone decides it
should. A ledger entry also has to be at least as new as the attestation
it clears and come from a review of a different head, so a review can't
disposition its own finding.

Parsing lives in ally-review-detection.ts alongside the other shared
predicates, matching the entry shape check-ally-review-consistency.mjs
already parses.
Ally's review of 6b5b6e8 caught this: matching the ledger on the
abbreviated head SHA alone meant one `fixed` entry cleared every finding
raised on that head. A review reporting both `prior:<head> critical 1`
and `prior:<head> important 1` could disposition only the critical item
and the important one would vanish from the gate — the exact class of
silent drop this gate exists to prevent.

Ally already emits a stable identity per finding (severity plus its
1-based index within that bucket), and its counted `Critical/Important
Issues (N)` headings enumerate what a head raised. So a head is retired
only once every finding it reported is named by a later ledger.

A body whose blocking feedback came from prose or an uncounted heading
yields no identities to match, so it can never be fully retired and stays
carried rather than being cleared by an unrelated entry.
Ally's review of 4659ebf caught a third disposition verb this allowlist
was missing. The vocabulary is three words, not two: `fixed` and
`no-longer-applicable` retire a prior finding, `still-present` asserts it
stands. A ledger entry using the missing verb parsed fine and was then
dropped, so the finding stayed blocking with nothing left that could
retire it.

`no-longer-applicable` means the finding does not apply to this code —
often that it was incorrect as filed — so it retires without implying
anything changed. Blockcast/onprem-k8s#2881 states the contract; it is
also in use on #1126 and Blockcast/go-amt#93. Worth
recording that the verb appears nowhere in this repo and nowhere in `gh
search code`; it is only observable in real review bodies, which is why
the first pass concluded there were two verbs.

The allowlist stays an allowlist. The failure modes are asymmetric: an
unrecognized verb leaves a PR visibly red until someone widens this set,
whereas a denylist would silently clear a live finding. This very
omission surfaced as a red gate rather than as a merged regression.

Retiring stays per-finding, so `no-longer-applicable` gets the same
severity+index identity matching as `fixed` and cannot clear a sibling
finding the ledger never named.
@kkroo
kkroo force-pushed the fix/ally-gate-prior-findings-ledger branch from 4659ebf to 0a09fe4 Compare August 31, 2026 08:40
@kkroo

kkroo commented Aug 31, 2026

Copy link
Copy Markdown
Author

Addressed in 0a09fe4. The finding was correct and I checked it rather than taking it on assertion, since widening this allowlist is a merge-gate policy change.

no-longer-applicable appears nowhere in this repo and returns nothing from gh search code — which is why the first pass concluded the vocabulary was two words. It is only observable in real review bodies: gh api '/search/issues?q=org%3ABlockcast+"no-longer-applicable"' returns 116 hits, and Blockcast/onprem-k8s#2881 states the contract outright — "the gate accepts fixed / still-present / no-longer-applicable" — with live use also on #1126 and Blockcast/go-amt#93. Two verbs retire, still-present blocks, which lines up with check-ally-review-consistency.mjs treating still-present alone as blocking (I2c).

Kept it an allowlist rather than flipping to "retire unless still-present". The failure modes aren't symmetric: an unrecognized verb leaves a PR visibly red until someone widens the set, whereas a denylist would silently clear a live finding — the outcome this gate exists to prevent. This omission surfaced as a red gate, not as a merged regression, which is the behaviour working.

Retiring stays per-finding, so the new verb gets the same severity+index matching as fixed and can't clear a sibling finding the ledger never named. Two tests added, both mutation-checked: dropping the verb from the allowlist reds the first, and accepting any verb reds the still-present and unknown-verb guards.

One thing I did not fold in, to keep this diff reviewable: when a ledger entry uses an unrecognized verb the gate stays red with no indication why, which is the "silently ignored" part of the report. That's an observability gap rather than a correctness one — the verdict is right either way — so it seems better as a follow-up than as more surface on a merge-gate change that has already grown twice. Happy to do it here instead if you'd rather.

Suites: 304 across eight review-gate/webhook files. tsc clean.

@allyblockcast

allyblockcast Bot commented Aug 31, 2026

Copy link
Copy Markdown

Hey @kkroo! Before this PR can be reviewed, a few things need attention:

Missing or incomplete:

  • Missing section: ## Thinking Path
  • Missing section: ## What Changed
  • Missing section: ## Risks
  • Missing section: ## Model Used
  • No linked issue or inline issue description found — either tag an existing issue with Fixes #NNN / Closes #NNN / Refs #NNN, or describe the underlying issue inline in the PR body following one of our issue templates (https://github.com/paperclipai/paperclip/tree/master/.github/ISSUE_TEMPLATE). See CONTRIBUTING.md → "Link Issues or Describe Them In-PR".
  • Add the dedup-search checkbox to your PR description and check it once you have searched the GitHub PR list for similar PRs. See the PR template at .github/PULL_REQUEST_TEMPLATE.md and CONTRIBUTING.md → "Before You Start: Search First".

Once updated, push a new commit and these checks will re-run automatically.

— commitperclip

@allyblockcast allyblockcast Bot left a comment

Copy link
Copy Markdown

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: 0a09fe4

Prior Findings Dispositioned (1)

  • prior:4659ebf important 1 — fixed — server/src/services/ally-review-detection.ts:96 now includes no-longer-applicable in the resolved disposition vocabulary, and the added test at server/src/__tests__/pr-comment-review-gate.test.ts:379-396 verifies that it retires the named prior finding.

Critical Issues (0)

Important Issues (0)

Suggestions (0)

Strengths

  • Prior-finding retirement is now matched per head prefix, severity, and 1-based finding index, so partial ledgers cannot clear unrelated findings.
  • The parser fails closed for unrecognized disposition verbs and safely supports both resolving dispositions defined by the review contract.
  • The regression coverage exercises chronology, prefix matching, partial retirement, unenumerated findings, and no-longer-applicable handling.

Recommended Action

  1. Merge when the remaining repository checks are green.

@allyblockcast

allyblockcast Bot commented Aug 31, 2026

Copy link
Copy Markdown

Hey @kkroo! Before this PR can be reviewed, a few things need attention:

Missing or incomplete:

  • Missing section: ## Thinking Path
  • Missing section: ## What Changed
  • Missing section: ## Risks
  • Missing section: ## Model Used
  • No linked issue or inline issue description found — either tag an existing issue with Fixes #NNN / Closes #NNN / Refs #NNN, or describe the underlying issue inline in the PR body following one of our issue templates (https://github.com/paperclipai/paperclip/tree/master/.github/ISSUE_TEMPLATE). See CONTRIBUTING.md → "Link Issues or Describe Them In-PR".
  • Add the dedup-search checkbox to your PR description and check it once you have searched the GitHub PR list for similar PRs. See the PR template at .github/PULL_REQUEST_TEMPLATE.md and CONTRIBUTING.md → "Before You Start: Search First".

Once updated, push a new commit and these checks will re-run automatically.

— commitperclip

@kkroo
kkroo added this pull request to the merge queue Aug 31, 2026
Merged via the queue into master with commit bd35f20 Aug 31, 2026
21 of 23 checks passed
@kkroo
kkroo deleted the fix/ally-gate-prior-findings-ledger branch August 31, 2026 10:08
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