Skip to content

feat(bedrock-pr-review): FinalWalk review lifecycle, adjudication, on-demand triggers - #23

Closed
aprilb wants to merge 2 commits into
mainfrom
feat/reviewer-finalwalk-alignment
Closed

feat(bedrock-pr-review): FinalWalk review lifecycle, adjudication, on-demand triggers#23
aprilb wants to merge 2 commits into
mainfrom
feat/reviewer-finalwalk-alignment

Conversation

@aprilb

@aprilb aprilb commented Sep 2, 2026

Copy link
Copy Markdown
Member

Refs modsy/digitalops-platform#176

Brings the Bedrock reviewer in line with the FinalWalk standard
(digital-analytics-len/FinalWalk, ADR-004).

FinalWalk itself is not callable from modsy: it is an internal repo in the
lennar-emu enterprise and modsy is a Team-plan org outside it, so a modsy job
token cannot resolve it and the finalwalk-ai App cannot install on modsy repos.
This ports the behaviors instead.

Why

Positional comment identity is what makes an AI reviewer noisy. Identity was
path:line, so any edit earlier in a file shifted every finding below it: the
same finding failed to match its old comment, got deleted, and got re-posted.
Because the new comment is a different object, a reviewer's acknowledgement or
resolution did not survive the next push. There was also no notion of a resolved
thread, so a dismissed finding came straight back.

Review lifecycle

  • Content-based identity. sha256(path + literal source text of the anchored line + occurrence index), read from the checked-out merge tree. Line numbers
    are excluded and the model's own prose is never part of identity, so a reworded
    finding keeps its comment and an unrelated edit above it does not move it. The
    occurrence index disambiguates files that repeat a line (common in JSON/YAML/SQL).
  • Resolution is permanent dismissal. Thread state comes from one GraphQL
    reviewThreads query. The exact finding is suppressed on every later run, and so
    is a nearby reworded restatement (same path, within 10 lines, claim-token Jaccard

    = 0.35). Every suppression prints a ::notice::; nothing is dropped silently.

  • Sticky threads. A finding the model does not re-emit is deleted as fixed only
    when the code it anchors on changed since the last reviewed commit. On unchanged
    code, non-reemission is sampling noise: the code cannot have been fixed by not
    changing. With no incremental data at all (first review, force-push, unreachable
    prior SHA) the comment is kept, never deleted on a guess.
  • Live-variant reconciliation. A restatement of a claim already open on a nearby
    thread (within 40 lines) reconciles with that thread instead of duplicating it.
  • A human reply is never destroyed. Not on a move, not on a rescan.
  • One persistent summary comment, updated in place, stamped with the commit it
    reviewed. That stamp replaces parsing per-run review bodies for idempotency.

Precision and cost

  • Adjudication pass. Every candidate goes back to a second model with the
    surrounding source, which drops what that source refutes (a "missing check" whose
    excerpt contains the check), plus duplicates, speculation with no named mechanism,
    and anything contradicting the repo's review guide. Defaults to Sonnet 4.6
    regardless of the finding model: precision is model-insensitive once verification
    is on, but the judgment's recall is not, and a weak judge silently drops real
    hedged findings. Fail-open, so an adjudicator error keeps the full candidate set
    rather than losing findings. Disable with adjudicate: false.
  • min_confidence (default medium) drops low-confidence findings. An absent
    confidence field counts as medium, so an omitted field never silently drops one.
  • max_files (default 50) skips oversized PRs rather than reviewing them badly.
  • review_guide_path feeds .github/review-guide.md to both passes.
  • Dependabot PRs no-op: they run without repo or org secrets, so the OIDC role
    assumption cannot succeed and a red check is the only possible outcome.

On-demand triggers

Comment Effect
@bedrock-review Re-review the current HEAD, overriding the already-reviewed and draft skips
@bedrock-review dismiss Delete every finding this reviewer posted; resolved threads stay as the permanent record

Comment bodies are passed through env, never interpolated into the shell.

These need a caller change to fire. The triggers live in the caller, not the
reusable workflow. examples/pr-review-bedrock.yml is updated; the 7 Spec Tool
callers need the two extra events added, tracked as a sub-task on
modsy/digitalops-platform#176.

Prompt loading (bug fix in passing)

Dropped the cross-repo actions/checkout of this repo for the shared prompt and
embedded it as PROMPT_B64, matching codex-pr-review.yml. A consumer's job token
can resolve a reusable workflow in an internal repo but cannot git fetch its
contents, so this was a latent break here (this repo is public today) and an active
one for product-org-len/ci-workflows, which is internal and still carries the
checkout. It is the same failure https://github.com/product-org-len/ci-workflows/pull/8
fixed for the Codex reviewer. check-prompt-embed.yml now guards both embedded
copies, so the two cannot silently diverge from the prompt file.

Removing that step also removes the only localized line between the two
ci-workflows repos (repository: modsy/ci-workflows), so the file can now be
byte-identical in both.

Self-review findings, fixed in this PR

/code-review on my own diff surfaced three defects, all fixed here with tests:

  1. Thread deleted alongside the candidate it reconciled with. The live-variant
    reconcile dropped the new candidate to avoid a duplicate but did not mark the
    thread it matched as still asserted, so the same run's orphan sweep could delete
    that thread. A real finding vanished on a run where nothing was fixed.
  2. Marker regex missed the summary's own marker. bedrock-review(?::...) does
    not match <!-- bedrock-reviewer -->. Dismiss removed the summary only by
    accident, via the separate reviewed-sha stamp that sits beside it, so an empty
    head_sha or a marker rename would have silently broken it.
  3. Severity flap deleted a live thread. A finding re-emitted one grade lower
    went to "Minor notes" and never reached the reconcile set, orphaning its thread.

Fixed at the class level rather than per case: any finding still asserted this run
keeps its thread, whichever presentation bucket it lands in (inline, minor,
non-diff, over the cap, or dropped by a floor). Only a claim the model did not
restate at all is a deletion candidate, and even then only if its code changed.

A fourth, found while testing the changed-ranges pipeline: +++ /dev/null (a
deleted file) did not match the b/ prefix, so path kept the previous file's
value and the deleted file's hunk was attributed to it. Verified against a real
diff, pre-fix vs post-fix:

OLD: added.txt 1 3 / added.txt 0 0 / keep.txt 3 3 / puredel.txt 1 1
NEW: added.txt 1 3 / keep.txt 3 3 / puredel.txt 1 1

Testing

node --test tests/                      # 38 pass
python3 scripts/validate-workflows.py   # 7 workflows valid

tests/reconcile.test.mjs extracts the workflow's PURE LOGIC region from the
YAML at test time and runs it, so the workflow file is the only copy and there is
nothing to drift. Coverage: identity stability across line shifts and rewording,
anchor fallbacks, occurrence indexing, claim tokenization and stopwords, Jaccard,
variant matching inside and outside both windows, the confidence and severity
floors including the fail-open on a missing grade, sticky-thread windows, marker
ownership (including that it does not claim the Codex reviewer's comments,
which post under the same github-actions[bot] login), and thread preservation.

Mutation-checked, so the assertions are not vacuous:

Mutation Result
Put the line number back into identity 2 tests fail
Revert the marker regex 2 tests fail
Ignore variant matches in thread preservation 1 test fails

scripts/validate-workflows.py loads every workflow here as YAML and parses each
embedded bash, Python and JavaScript block. A reusable workflow only fails when a
consumer dispatches it, so a syntax error in a block scalar is otherwise invisible
until it breaks somebody else's PR. It earned its place immediately: a raw NUL byte
in the embedded JS parsed fine as JavaScript and made the whole file invalid YAML.
Both checks run in CI via the new tests.yml.

Not covered by local testing

The end-to-end review has not run against a live PR: it needs the OIDC role, and
a reusable workflow only executes when a consumer dispatches it. Everything above
is static validation plus unit tests on the reconcile logic. The reviewer is
advisory and fail-soft throughout (continue-on-error on both model calls, and
every API mutation individually guarded), so a runtime failure degrades to no
review rather than a red check, but the first live run is the real proof and is
worth watching.

Known transition gap

Inline comments posted by the previous version of this workflow carry no
fingerprint marker, so this version will neither update nor delete them: they stay
as orphaned threads on PRs that are already open. Claiming unmarked bot comments is
not an option, because the Codex reviewer posts under the same
github-actions[bot] login and we would delete its threads. Self-limiting to PRs
open at merge time.

UI / visual proof

N/A, no user interface. The user-visible surface is PR comments, which cannot be
captured before the first live run for the reason above.

…-demand triggers

Brings the Bedrock reviewer in line with the FinalWalk standard
(digital-analytics-len/FinalWalk, ADR-004). Positional comment identity is
what makes an AI reviewer noisy: any edit earlier in a file shifts every
finding below it, so on most pushes the same finding fails to match its old
comment and gets deleted and re-posted, taking any acknowledgement or
resolution with it.

Review lifecycle:

- Comment identity is now content-based: sha256 of the path plus the literal
  source text of the anchored line, read from the checked-out merge tree, plus
  an occurrence index for files that repeat a line. Line numbers are excluded,
  and the model's own prose is never part of identity, so a reworded finding
  keeps its comment.
- Resolving a thread dismisses that finding permanently, including a nearby
  reworded restatement (same path, within 10 lines, claim-token Jaccard >=
  0.35). Thread state comes from one GraphQL reviewThreads query.
- A finding the model does not re-emit is deleted as fixed only when the code
  it anchors on changed since the last reviewed commit. On unchanged code,
  non-reemission is sampling noise. Without incremental data at all the comment
  is kept, never deleted on a guess.
- A restatement of a claim already open on a nearby thread (within 40 lines)
  reconciles with that thread instead of duplicating it.
- A thread a human replied to is never deleted, on a move or a rescan.
- The summary is one persistent comment updated in place, stamped with the
  commit it reviewed. That stamp replaces parsing per-run review bodies for
  idempotency.

Precision and cost:

- An adjudication pass sends every candidate back with the surrounding source
  and drops what that source refutes, plus duplicates, unmechanised
  speculation, and anything contradicting the repo's review guide. Defaults to
  Sonnet 4.6 regardless of the finding model: precision is model-insensitive
  once verification is on, but the judgment's recall is not. Fail-open, so an
  adjudicator error keeps the full candidate set.
- min_confidence drops low-confidence findings; an absent confidence field
  counts as medium so an omitted field never silently drops a finding.
- max_files skips oversized PRs rather than reviewing them badly.
- review_guide_path feeds .github/review-guide.md to both passes.
- Dependabot PRs no-op: they run without secrets, so the role assumption cannot
  succeed and a red check is the only possible outcome.

On-demand triggers: @bedrock-review re-reviews the current HEAD (overriding the
already-reviewed and draft skips), @bedrock-review dismiss deletes every
finding this reviewer posted. Comment bodies are passed through env, never
interpolated into the shell.

Also drops the cross-repo checkout of this repo for the shared prompt and
embeds it as PROMPT_B64, matching codex-pr-review.yml. A consumer's job token
can resolve a reusable workflow in an internal repo but cannot fetch its
contents, so the checkout was a latent break for every consumer and an active
one for the product-org-len mirror. check-prompt-embed.yml now guards both
copies.

Tests: tests/reconcile.test.mjs extracts the workflow's PURE LOGIC region from
the YAML at test time, so the workflow file is the only copy. 38 tests covering
identity stability, dismissal and variant matching, the confidence and severity
floors, sticky threads, marker ownership, and thread preservation.
scripts/validate-workflows.py loads every workflow as YAML and parses each
embedded bash, Python and JavaScript block, since a reusable workflow only
fails when a consumer dispatches it.
Self-review of the previous commit surfaced six defects. All are fixed here,
each with a test that fails when the fix is reverted.

1. Nothing before the model call may red the advisory check. The OIDC role
   assumption, Python setup, SDK install, caller write and the post step had no
   continue-on-error, so a repo missing from the reviewer role's trust policy
   red-Xed every PR in it, contradicting the fail-soft guarantee this workflow
   states in its own header. All are fail-soft now, a Check setup step names
   what failed, and the run summary reports it instead of letting it surface as
   a confusing model error.

2. On-demand and dismiss now require write access. Both are privileged: dismiss
   deletes findings, and on-demand spends against the caller's Bedrock role
   while deliberately bypassing the debounce and already-reviewed gates. Anyone
   can comment on a PR, so a drive-by commenter could previously delete every
   finding or drive unbounded spend. Gated on the comment's author_association
   being OWNER, MEMBER or COLLABORATOR, with a ::notice:: naming a denied
   association so it is visible rather than silent.

3. The adjudicator can re-grade what it rescues. The confidence floor runs after
   adjudication, and a verdict could not change confidence, so a hedged finding
   the judge confirmed and rewrote into an assertion was then discarded by the
   default medium floor: precisely the candidate the adjudication pass exists to
   rescue. A verdict may now carry a confidence, and a rewritten candidate is
   floored at medium on the grounds that the rewrite IS the assertion.

4. max_files uses the exact changed-file count. It was derived from
   `gh pr view --json files`, which resolves through GraphQL `files(first: 100)`
   and saturates at 100, so any cap above that never fired: a 600-file PR
   reported 100 and sailed through a cap of 150. Read `.changed_files` from the
   REST pulls resource instead, and fail the guard open on a malformed value.

5. Comment events dedupe by PR. They were isolated by run_id, so two
   `@bedrock-review` comments ran at once, neither saw the other's posts, both
   opened a thread per finding, and the loser was unreachable forever after
   because the reconcile pass indexes one comment per fingerprint. Now keyed on
   the PR plus an event kind, so command runs dedupe with each other while still
   never cancelling a push review.

6. The model's line number is coerced once at ingest. A string "42" failed the
   commentable-line Set lookup and demoted the finding to a summary bullet even
   when its line was in the diff, and would have failed the strict equality that
   decides whether a finding moved, delete-and-reposting an unmoved comment on
   every run. A finding with an unusable line is now dropped with a warning
   rather than silently mishandled.

Tests: 89, up from 38. Two new suites, both extracting the code under test from
the workflow YAML so there is still only one copy: tests/trigger.test.mjs covers
the author-association gate and that a hostile comment body cannot reach the
shell, and tests/adjudication.test.mjs covers the verdict applier's keep, drop,
rewrite, re-grade and fail-open paths. reconcile.test.mjs gains workflow-shape
guards for the concurrency group, the fail-soft chain and the exact file count.

Mutation-checked: reverting any one of the six fixes fails at least one test.
@aprilb

aprilb commented Sep 2, 2026

Copy link
Copy Markdown
Member Author

Second review round. /code-review on the diff surfaced six more defects, all
fixed in the follow-up commit, each with a test that fails when the fix is
reverted.

# Defect Fix
1 The OIDC role assumption, Python setup, SDK install, caller write and the post step had no continue-on-error, so a repo missing from the reviewer role's trust policy red-Xed every PR in it, contradicting the fail-soft guarantee in this workflow's own header All fail-soft; a Check setup step names what failed and the run summary reports it
2 @bedrock-review and dismiss accepted any commenter, so a drive-by could delete every finding or drive unbounded Bedrock spend Gated on author_association in OWNER/MEMBER/COLLABORATOR, with a ::notice:: naming a denied association
3 The confidence floor ran after adjudication and a verdict could not change confidence, so a hedged finding the judge confirmed and rewrote into an assertion was then discarded by the default medium floor A verdict may carry confidence; a rewritten candidate is floored at medium, because the rewrite is the assertion
4 max_files came from gh pr view --json files, which saturates at 100, so any cap above that never fired (a 600-file PR reported 100) Exact .changed_files from the REST pulls resource
5 Comment events were isolated by run_id, so two @bedrock-review comments raced, both posted, and the loser became permanently unreachable (the reconcile pass indexes one comment per fingerprint) Keyed on the PR plus an event kind, so command runs dedupe with each other but still never cancel a push review
6 The model's line was used verbatim from JSON, so a string "42" failed the commentable-line Set lookup and demoted the finding off the diff Coerced once at ingest; an unusable line is dropped with a warning

Findings 1 and 3 are the ones I would not have wanted to ship. 1 is the same
class as the cross-repo checkout this PR already fixes, a hard-fail step sitting
before the fail-soft boundary, and it would have hit the first adopting repo
before its trust-policy entry existed. 3 was self-inflicted by this PR: the
adjudication pass and min_confidence were both added here and they fought each
other, so verification silently discarded exactly the findings it had just
rescued.

Tests are 89, up from 38. Two new suites, both extracting the code under test
from the workflow YAML so there is still only one copy:

  • tests/trigger.test.mjs covers the author-association gate across six
    non-writer associations and both commands, plus that a hostile comment body
    cannot reach the shell.
  • tests/adjudication.test.mjs covers the verdict applier's keep, drop,
    rewrite, re-grade and fail-open paths.
  • tests/reconcile.test.mjs gains workflow-shape guards for the concurrency
    group, the fail-soft chain and the exact file count.

Mutation-checked, reverting each fix in turn:

Reverted fix Tests that fail
Author-association gate 13
parseLine coercion 3
Rescued-hedge re-grade 1
Concurrency dedupe 1
Fail-soft on the AWS step 1
Exact file count 1

@aprilb

aprilb commented Sep 3, 2026

Copy link
Copy Markdown
Member Author

Closing unmerged. The Spec Tool repos are moving to GitHub Copilot code review,
so the reviewer this PR hardens is being retired on the modsy side rather than
improved. Work continues on the product-org-len copy only:
https://github.com/product-org-len/ci-workflows/pull/14

Nothing here is lost. The two commits are recoverable at d1dd281
(d1dd281e891251bb0174f80f7f371186635a3158), parent d2e1618, and the same
content is on the product-org-len branch. Reopen or cherry-pick from there if
the modsy reviewer ever comes back.

@aprilb aprilb closed this Sep 3, 2026
@aprilb
aprilb deleted the feat/reviewer-finalwalk-alignment branch September 3, 2026 03:38
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