Skip to content

fix(rollup): a reviewer that never ran must not report zero findings - #566

Merged
piekstra merged 2 commits into
mainfrom
fix/rollup-failed-reviewers
Aug 13, 2026
Merged

fix(rollup): a reviewer that never ran must not report zero findings#566
piekstra merged 2 commits into
mainfrom
fix/rollup-failed-reviewers

Conversation

@piekstra

Copy link
Copy Markdown
Contributor

The problem

Zero findings and "did not run" are the same number and opposite meanings. One says the code was examined and is clean; the other says nothing was examined. The summary table rendered both as 0.

On a real PR, this is what a run looked like where four of five reviewers failed to start:

| Reviewer                  | Findings |
| security:code-auditor     | 0        |
| rust:implementation-tests | 0        |
| architecture:solid        | 0        |
| policies:conventions      | 0        |
| documentation:docs        | 1        |

Only documentation:docs actually ran — it is the one agent that needs no repo checkout. The failures were recorded, but as incomplete_failed in a coverage section well below the headline table. The summary is the part that gets read, pasted, and acted on, and it said all-clear.

I nearly merged on that reading, twice.

Scope

The outcome logic is already correcthasIncompleteReviewerCoverage coerces approval away when coverage is incomplete, so the PR was never actually approved. This change is purely about what a human reads.

The fix

A reviewer whose coverage status did not produce a result renders as ⚠️ did not run instead of a count, reusing the existing coverageResultProduced helper.

Reviewers absent from coverage keep their number, so an unknown status cannot turn a genuine zero into a phantom failure. That case has its own test.

Tests

Two tests, both confirmed to fail with the change reverted:

  • a failed reviewer is not rendered as 0, while a genuinely clean reviewer keeps its honest 0 in the same table
  • unknown coverage leaves the count untouched

Full suite green.

Related

#565 fixes the cause of that particular fleet failure (the workbench has no refs/, so per-reviewer clones fail). This one fixes the reporting, which matters for any reviewer failure regardless of cause — the two are independent.

Zero findings and "did not run" are the same number and opposite meanings. One
says the code was examined and is clean; the other says nothing was examined.
The summary table rendered both as 0.

On a real PR that made a run where four of five reviewers failed to start read
as a clean review:

    | security:code-auditor     | 0 |
    | rust:implementation-tests | 0 |
    | architecture:solid        | 0 |
    | policies:conventions      | 0 |
    | documentation:docs        | 1 |

Only the last one had actually run. The failures were recorded, but in a
coverage section far below the headline table, so anyone reading the summary --
which is the part that gets pasted into a decision -- saw an all-clear.

The outcome logic was already correct: hasIncompleteReviewerCoverage coerces
approval away when coverage is incomplete, so the PR was never approved. This
is purely about what a human reads and acts on.

A non-producing coverage status now renders as "did not run" instead of a
count. Reviewers absent from coverage keep their number, so an unknown status
cannot turn a genuine zero into a phantom failure -- covered by its own test.
Both tests were confirmed to fail with the change reverted.

@piekstra-dev piekstra-dev left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Automated PR Review

Reviewed commit: f085ea734988
Profile: reviewer - Posting as: piekstra-dev

Summary

Reviewer Findings
go:implementation-tests 0
structure:repo-health 1
harness-engineering:repo-health 0
structure:repo-health (1 finding)

Major - internal/reviewplan/summary.go:243

Summary's doc comment states "the rendered rollup comment and dry-run JSON both consume this object, so they cannot disagree," but this fix only changes the markdown renderer. writeReviewerTable in summary.go now derives a "did not run" state from ReviewerCoverageSummary, but ReviewerSummary.Findings itself is left as a plain int (0) whether a reviewer genuinely found nothing or never ran. internal/view/review.go's newReviewSummary (dry-run/API JSON) copies reviewer.Findings straight through with no coverage awareness, so a failed reviewer still serializes as findings: 0 there — the exact ambiguity this PR sets out to fix, reintroduced in the other consumer of this same struct. Either derive the coverage-aware state once in reviewplan (e.g. expose it via ReviewerSummary or a shared helper) so both consumers stay in sync, or narrow the doc comment to acknowledge the markdown-only scope so a future reader doesn't assume JSON output already carries this fix.

Reviewer Coverage

  • go:implementation-tests — complete (broad); skipped: none; constraints: none
  • structure:repo-health — complete (broad); skipped: none; constraints: Scope limited to the three assigned files; the corroborating JSON-consumer code (internal/view/review.go) was read for context but is outside this reviewer's assignment and not inspected/skipped-listed.
  • harness-engineering:repo-health — complete (broad); skipped: none; constraints: none
Inspected files (3)
  • internal/reviewplan/reviewplan.go
  • internal/reviewplan/summary.go
  • internal/reviewplan/summary_failed_reviewer_test.go

0 PR discussion threads considered. 0 summarized; 0 resolved.


Completed in 2m 55s | $1.82 | claude-sonnet-5 | cr dev
Field Value
Model claude-sonnet-5
Reviewers go:implementation-tests, structure:repo-health, harness-engineering:repo-health
Engine claude_cli · claude-sonnet-5
Reviewed by cr · piekstra-dev
Duration 2m 55s wall · 5m 11s compute
Cost $1.82
Tokens 104 in / 19.8k out

Per-workstream usage

  • orchestrator-selection — claude-sonnet-5
    • In: 6
    • Out: 2.5k
    • Cache read: 66.8k
    • Cache create: 17.8k
    • Cost: $0.17
    • Duration: 28s
  • go:implementation-tests — claude-sonnet-5
    • In: 22
    • Out: 3.5k
    • Cache read: 416.2k
    • Cache create: 27.6k
    • Cost: $0.34
    • Duration: 50s
  • structure:repo-health — claude-sonnet-5
    • In: 38
    • Out: 7.5k
    • Cache read: 804.9k
    • Cache create: 46.3k
    • Cost: $0.63
    • Duration: 1m 35s
  • harness-engineering:repo-health — claude-sonnet-5
    • In: 32
    • Out: 5.4k
    • Cache read: 619.9k
    • Cache create: 34.2k
    • Cost: $0.47
    • Duration: 1m 43s
  • orchestrator-rollup — claude-sonnet-5
    • In: 6
    • Out: 788
    • Cache read: 94.3k
    • Cache create: 26.7k
    • Cost: $0.20
    • Duration: 33s

Comment thread internal/reviewplan/summary.go
piekstra-dev
piekstra-dev previously approved these changes Aug 13, 2026

@piekstra-dev piekstra-dev left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Automated PR Review

Reviewed commit: f085ea734988
Profile: reviewer - Posting as: piekstra-dev

Summary

Reviewer Findings
go:implementation-tests 1
structure:repo-health 0
harness-engineering:repo-health 0
go:implementation-tests (1 finding)

Minor - internal/reviewplan/summary.go:243

Summary's doc comment claims "the rendered rollup comment and dry-run JSON both consume this object, so they cannot disagree," but this fix only makes the markdown renderer (writeReviewerTable) coverage-aware. ReviewerSummary.Findings itself stays a plain int, so internal/view/review.go's newReviewSummary (the dry-run/API JSON path) copies reviewer.Findings straight through with no reference to coverage status — a failed reviewer still serializes as findings: 0 there, the exact zero/did-not-run ambiguity this PR sets out to fix, just in the other consumer of this struct. Derive the coverage-aware distinction once in reviewplan (e.g. add a Ran/DidNotRun field to ReviewerSummary or expose a shared helper both writeReviewerTable and newReviewSummary call) so the two consumers cannot drift, or at minimum narrow this doc comment to say it covers table rendering only so a future reader doesn't assume the JSON output already carries this fix.

Reviewer Coverage

  • go:implementation-tests — complete (constrained); skipped: none; constraints: none
  • structure:repo-health — complete (constrained); skipped: none; constraints: Head SHA is unchanged from the prior review round; the previously raised JSON/markdown consistency concern is already tracked as a settled inline thread on internal/reviewplan/summary.go, so it is not re-raised here.
  • harness-engineering:repo-health — complete (constrained); skipped: none; constraints: The prior finding about ReviewerSummary/dry-run JSON not carrying the coverage-derived did-not-run state (internal/view/review.go's newReviewSummary) is already tracked as a settled inline thread and internal/view/review.go is outside this assignment's allowed_files, so it is not re-raised here.
Inspected files (3)
  • internal/reviewplan/reviewplan.go
  • internal/reviewplan/summary.go
  • internal/reviewplan/summary_failed_reviewer_test.go

0 PR discussion threads considered. 0 summarized; 0 resolved.


Completed in 1m 19s | $1.53 | claude-sonnet-5 | cr dev
Field Value
Model claude-sonnet-5
Reviewers go:implementation-tests, structure:repo-health, harness-engineering:repo-health
Engine claude_cli · claude-sonnet-5
Reviewed by cr · piekstra-dev
Duration 1m 19s wall · 1m 50s compute
Cost $1.53
Tokens 40 in / 7.8k out

Per-workstream usage

  • go:implementation-tests — claude-sonnet-5
    • In: 12
    • Out: 3.4k
    • Cache read: 291.0k
    • Cache create: 41.2k
    • Cost: $0.39
    • Duration: 45s
  • structure:repo-health — claude-sonnet-5
    • In: 8
    • Out: 1.4k
    • Cache read: 230.6k
    • Cache create: 55.3k
    • Cost: $0.42
    • Duration: 20s
  • harness-engineering:repo-health — claude-sonnet-5
    • In: 14
    • Out: 2.5k
    • Cache read: 415.1k
    • Cache create: 56.2k
    • Cost: $0.50
    • Duration: 32s
  • orchestrator-rollup — claude-sonnet-5
    • In: 6
    • Out: 476
    • Cache read: 102.4k
    • Cache create: 30.2k
    • Cost: $0.22
    • Duration: 12s

Comment thread internal/reviewplan/summary.go
Review caught that the previous commit fixed only the markdown renderer, while
Summary's own contract says the rollup comment and the JSON view "both consume
this object, so they cannot disagree". They disagreed: newReviewSummary copied
Findings straight through, so a reviewer that never ran still serialized as
findings: 0 -- the exact ambiguity this change exists to remove, reintroduced
in the other consumer.

The state is now derived once, by reviewplan.ReviewersProducedResults, and read
by both. The JSON gains ran, omitted when coverage says nothing either way so
an unknown status is not reported as a failure.

Test asserts all three cases in one payload -- failed, completed, unknown --
and was confirmed to fail with the mapping reverted.

@piekstra-dev piekstra-dev left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

Automated PR Review

Reviewed commit: 51086247864f
Profile: reviewer - Posting as: piekstra-dev

Summary

Reviewer Findings
go:implementation-tests 0
structure:repo-health 1
harness-engineering:repo-health 0
structure:repo-health (1 finding)

Minor - internal/reviewplan/summary.go:254

writeReviewerTable still builds its own local produced map by hand (lines 255-258) instead of calling the new exported ReviewersProducedResults helper added a few lines below it in the same file. The helper's own doc comment says "Both the rendered rollup and the JSON view derive their 'did not run' state from this, so the two cannot disagree," but that guarantee only holds for the JSON view (internal/view/review.go), which does call it -- the markdown table computes the identical coverage->produced mapping independently. Two implementations of the same rule in one file can drift silently (e.g. if coverageResultProduced's status set changes and only one call site is updated). Have writeReviewerTable call reviewplan.ReviewersProducedResults(coverage) instead of re-deriving the map inline, so there is exactly one source of truth backing the stated contract.

Reviewer Coverage

  • go:implementation-tests — complete (constrained); skipped: none; constraints: none
  • structure:repo-health — complete (constrained); inspected 3 assigned files (5 inspected across reviewers): internal/reviewplan/reviewplan.go, internal/reviewplan/summary.go, internal/reviewplan/summary_failed_reviewer_test.go; skipped: none; constraints: internal/view/review.go and internal/view/review_reviewer_ran_test.go, which also changed in this revision, are outside this reviewer's assigned/allowed files and were read only for cross-file context, not inspected as part of this review.
  • harness-engineering:repo-health — complete (constrained); inspected 3 assigned files (5 inspected across reviewers): internal/reviewplan/reviewplan.go, internal/reviewplan/summary.go, internal/reviewplan/summary_failed_reviewer_test.go; skipped: none; constraints: none
Inspected files (5)
  • internal/reviewplan/reviewplan.go
  • internal/reviewplan/summary.go
  • internal/reviewplan/summary_failed_reviewer_test.go
  • internal/view/review.go
  • internal/view/review_reviewer_ran_test.go

0 PR discussion threads considered. 0 summarized; 0 resolved.


Completed in 1m 51s | $2.51 | claude-sonnet-5 | cr dev
Field Value
Model claude-sonnet-5
Reviewers go:implementation-tests, structure:repo-health, harness-engineering:repo-health
Engine claude_cli · claude-sonnet-5
Reviewed by cr · piekstra-dev
Duration 1m 51s wall · 3m 10s compute
Cost $2.51
Tokens 70 in / 13.5k out

Per-workstream usage

  • go:implementation-tests — claude-sonnet-5
    • In: 26
    • Out: 4.7k
    • Cache read: 944.8k
    • Cache create: 69.3k
    • Cost: $0.77
    • Duration: 1m 04s
  • structure:repo-health — claude-sonnet-5
    • In: 22
    • Out: 4.2k
    • Cache read: 881.2k
    • Cache create: 74.5k
    • Cost: $0.77
    • Duration: 58s
  • harness-engineering:repo-health — claude-sonnet-5
    • In: 16
    • Out: 3.8k
    • Cache read: 640.9k
    • Cache create: 77.3k
    • Cost: $0.71
    • Duration: 51s
  • orchestrator-rollup — claude-sonnet-5
    • In: 6
    • Out: 803
    • Cache read: 112.1k
    • Cache create: 34.8k
    • Cost: $0.25
    • Duration: 15s

// both as 0 let a run where four of five reviewers failed to start read as a
// clean review, with the failure visible only further down in the coverage
// section that a reader skimming the summary never reaches.
func writeReviewerTable(out *strings.Builder, reviewers []ReviewerSummary, coverage []ReviewerCoverageSummary) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

writeReviewerTable still builds its own local produced map by hand (lines 255-258) instead of calling the new exported ReviewersProducedResults helper added a few lines below it in the same file. The helper's own doc comment says "Both the rendered rollup and the JSON view derive their 'did not run' state from this, so the two cannot disagree," but that guarantee only holds for the JSON view (internal/view/review.go), which does call it -- the markdown table computes the identical coverage->produced mapping independently. Two implementations of the same rule in one file can drift silently (e.g. if coverageResultProduced's status set changes and only one call site is updated). Have writeReviewerTable call reviewplan.ReviewersProducedResults(coverage) instead of re-deriving the map inline, so there is exactly one source of truth backing the stated contract.

Reply inline to this comment.

@piekstra
piekstra merged commit 2f9fa05 into main Aug 13, 2026
10 checks passed
@piekstra
piekstra deleted the fix/rollup-failed-reviewers branch August 13, 2026 19:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants