Skip to content

Commit 06d716a

Browse files
will-lamertonclaude
andcommitted
fix(nc-review): three severity levels, verdict derived not trusted
Run 5 produced a good review and then mislabelled it. Five real findings, all marked advisory, verdict "clean" — so a PR with five things to fix got agent:clean, which is the label a maintainer skims. Copilot read the same PR as needing amends and was right. Two causes, both in the rubric I wrote. 1. Confidence and severity were conflated. The old text put "anything you are less than confident about" in the advisory bucket. Those are orthogonal: a finding can be high-impact and uncertain. Severity is now rated by impact alone, with uncertainty expressed in the detail text instead — and an explicit instruction not to file anything it cannot assert at all. 2. Binary severity forced the bad verdict. Findings that individually do not block a merge collapsed into "clean". There is now a middle level, `important`: a human reviewer would ask for a change before approving. The test-that-passes-without-exercising-anything case is given as the worked example, since a green suite makes that more dangerous rather than less. The verdict is now derived from the findings in the workflow rather than taken from the model: any blocking -> needs-work, any findings -> comments, none -> clean. A mismatch is logged as a notice. Three labels, mutually exclusive, with the other two removed unconditionally so a re-review cannot leave a stale pair. The heading now carries the tally ("comments — 2 important, 3 nits") so it cannot disagree with the table beneath it. Replaying run 5's findings through the new logic yields "comments — 2 important, 3 nits" and agent:comments. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01PZY52ePXLjwG9TaQgq2cHT
1 parent 85f1309 commit 06d716a

2 files changed

Lines changed: 122 additions & 32 deletions

File tree

.github/nc-review/rubric.md

Lines changed: 72 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -113,21 +113,75 @@ Do not demand tests for docs-only, comment-only or config-only changes.
113113
- **Do not speculate.** If you did not read the code, do not assert a bug in it.
114114
Where you are unsure, say so and mark it `advisory` — an honest "worth
115115
checking" is useful; a confident wrong claim is not.
116-
- **Do not pad.** A clean PR gets a short summary and an empty findings list.
117-
That is a good outcome, not a failure to find something.
116+
- **Do not pad.** If a pull request genuinely has nothing worth raising, say so
117+
in a sentence and return an empty findings list. Do not invent a `nit` to look
118+
thorough. But do not use this as an excuse to skip real findings either — see
119+
the calibration note under Severity.
118120
- **Do not moralise.** Many contributors here are new. Findings are about the
119121
code, never about the person.
120122

121-
## Severity
123+
## Severity — rate by impact, not by your confidence
122124

123-
- `blocking` — a maintainer should not merge until this is resolved. Correctness
124-
bugs, security problems, broken contracts, duplicates, a new feature with no
125-
meaningful test.
126-
- `advisory` — worth raising; a maintainer may reasonably merge anyway. Style of
127-
approach, minor edge cases, suggestions, anything you are less than confident
128-
about.
125+
These are two different things and must not be mixed:
129126

130-
If nothing is `blocking`, the verdict is `clean`.
127+
- **Severity** is how much the finding matters if it is true.
128+
- **Confidence** is how sure you are that it is true.
129+
130+
Rate severity by **impact alone**. If you are unsure whether something is real,
131+
say so in the `detail` ("I could not verify whether X handles Y") — do not
132+
downgrade the severity to hedge. And if you are not confident enough to assert a
133+
finding at all, do not file it. A quiet omission is better than a confident
134+
error, but a hedged real finding is better than a silent one.
135+
136+
Three levels:
137+
138+
**`blocking`** — do not merge until this is resolved.
139+
140+
- A correctness bug that will misbehave for real inputs
141+
- Any security problem
142+
- A broken public contract: CLI flags, config schema, tool interfaces, session
143+
or `RunRecord` formats
144+
- A duplicate of another open PR
145+
- A new feature with no test at all
146+
147+
**`important`** — a human reviewer would ask for a change before approving. Not
148+
catastrophic, but it should not merge as-is without a reason.
149+
150+
- A test that does not actually exercise what it claims to — e.g. it asserts a
151+
failure path that silently succeeds under some environments, so it passes
152+
while proving nothing. The suite going green makes this *more* dangerous, not
153+
less.
154+
- Removing existing coverage without replacing it
155+
- An unhandled edge case that a plausible user will hit
156+
- Logic that is correct today but fragile against a likely near-term change
157+
- Duplicated logic that must now be kept in sync in two places, where drift
158+
would cause a real bug
159+
160+
**`nit`** — genuinely optional. The author may ignore it.
161+
162+
- Naming, comment wording, a documentation inconsistency with no behavioural
163+
effect
164+
- A self-healing race with no security or correctness impact
165+
- Preference about structure where the current approach is defensible
166+
167+
**Calibration.** If you find yourself marking everything `nit`, you are
168+
under-calling. Ask of each finding: *would a careful human reviewer ask for a
169+
change before approving?* If yes, it is at least `important`. "The maintainer
170+
could merge this anyway" is true of almost everything and is not the test.
171+
172+
## Verdict
173+
174+
Derived mechanically from the findings — do not set it by feel:
175+
176+
| Verdict | When |
177+
|---|---|
178+
| `clean` | **no findings at all** |
179+
| `comments` | at least one finding, none `blocking` |
180+
| `needs-work` | at least one `blocking` finding |
181+
182+
`clean` means you have nothing to say. A pull request with five things worth
183+
fixing is **not** clean, even if none of them block the merge — labelling it
184+
clean tells a maintainer to skim past findings you spent the run producing.
131185

132186
## Output
133187

@@ -136,23 +190,24 @@ before or after, no markdown fences. Schema:
136190

137191
```json
138192
{
139-
"verdict": "clean",
193+
"verdict": "comments",
140194
"summary": "Two or three sentences. What the change does, whether it is correct, and whether it is ready.",
141195
"findings": [
142196
{
143-
"area": "correctness",
144-
"severity": "blocking",
145-
"file": "source/tools/execute-bash.ts",
146-
"line": 142,
197+
"area": "tests",
198+
"severity": "important",
199+
"file": "source/vscode/discovery.spec.ts",
200+
"line": 509,
147201
"detail": "Specific and actionable. What is wrong, why it matters, and what would fix it."
148202
}
149203
],
150204
"duplicate_of": null
151205
}
152206
```
153207

154-
- `verdict``"clean"` or `"needs-work"`. `needs-work` if and only if at least
155-
one finding is `blocking`.
208+
- `verdict``"clean"`, `"comments"` or `"needs-work"`, derived from the
209+
findings per the table above.
210+
- `severity``"blocking"`, `"important"` or `"nit"`.
156211
- `area` — one of `correctness`, `security`, `design`, `tests`, `duplicate`,
157212
`scope`, `changeset`, `contributing`.
158213
- `file` / `line` — where the finding is. Omit both if it is not tied to a

.github/workflows/nc-review.yml

Lines changed: 50 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -230,8 +230,9 @@ jobs:
230230
env:
231231
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
232232
run: |
233-
gh label create "agent:clean" --color "0e8a16" --description "nc-review found no blocking issues" --force
234-
gh label create "agent:needs-work" --color "d93f0b" --description "nc-review found blocking issues" --force
233+
gh label create "agent:clean" --color "0e8a16" --description "nc-review had nothing to raise" --force
234+
gh label create "agent:comments" --color "fbca04" --description "nc-review left non-blocking findings" --force
235+
gh label create "agent:needs-work" --color "d93f0b" --description "nc-review found blocking findings" --force
235236
236237
# Nanocoder resolves providers from `agents.config.json` in the working
237238
# directory first. This is written at run time rather than committed: a
@@ -357,16 +358,42 @@ jobs:
357358
exit 0
358359
fi
359360
360-
VERDICT=$(jq -r '.verdict // "needs-work"' "$V")
361361
SUMMARY=$(jq -r '.summary // "No summary provided."' "$V")
362362
DUP=$(jq -r '.duplicate_of // empty' "$V")
363363
364+
N_BLOCK=$(jq '[.findings[]? | select(.severity == "blocking")] | length' "$V")
365+
N_IMP=$(jq '[.findings[]? | select(.severity == "important")] | length' "$V")
366+
N_NIT=$(jq '[.findings[]? | select(.severity == "nit")] | length' "$V")
367+
N_ALL=$(jq '.findings | length' "$V")
368+
369+
# Derive the verdict from the findings rather than trusting the model's
370+
# own label. Run 5 returned verdict "clean" alongside five findings,
371+
# which put an agent:clean label on a PR that had five things to fix —
372+
# and the label is what a maintainer skims.
373+
if [ "$N_BLOCK" -gt 0 ]; then
374+
VERDICT="needs-work"
375+
elif [ "$N_ALL" -gt 0 ]; then
376+
VERDICT="comments"
377+
else
378+
VERDICT="clean"
379+
fi
380+
381+
CLAIMED=$(jq -r '.verdict // "(none)"' "$V")
382+
[ "$CLAIMED" != "$VERDICT" ] && \
383+
echo "::notice::model said '$CLAIMED', findings imply '$VERDICT' — using '$VERDICT'"
384+
385+
# Human-readable tally, e.g. "1 blocking, 2 important, 2 nits".
386+
COUNTS=""
387+
[ "$N_BLOCK" -gt 0 ] && COUNTS="${N_BLOCK} blocking"
388+
[ "$N_IMP" -gt 0 ] && COUNTS="${COUNTS:+$COUNTS, }${N_IMP} important"
389+
[ "$N_NIT" -gt 0 ] && COUNTS="${COUNTS:+$COUNTS, }${N_NIT} nit$([ "$N_NIT" -gt 1 ] && echo s)"
390+
364391
{
365-
if [ "$VERDICT" = "clean" ]; then
366-
echo "### nc-review: no blocking issues"
367-
else
368-
echo "### nc-review: needs work"
369-
fi
392+
case "$VERDICT" in
393+
needs-work) echo "### nc-review: needs work — ${COUNTS}" ;;
394+
comments) echo "### nc-review: comments — ${COUNTS}" ;;
395+
clean) echo "### nc-review: nothing to raise" ;;
396+
esac
370397
echo
371398
echo "$SUMMARY"
372399
echo
@@ -378,21 +405,29 @@ jobs:
378405
echo "| | Area | Where | Finding |"
379406
echo "|---|---|---|---|"
380407
jq -r '.findings[] |
381-
"| \(if .severity == "blocking" then "🔴" else "🟡" end)"
408+
"| \(if .severity == "blocking" then "🔴" elif .severity == "important" then "🟠" else "" end)"
382409
+ " | `\(.area)`"
383410
+ " | \(if .file then "`\(.file)\(if .line then ":\(.line)" else "" end)`" else "—" end)"
384411
+ " | \(.detail | gsub("\n"; " ")) |"' "$V"
385412
echo
386413
fi
387414
echo "---"
388415
echo
389-
echo "<sub>Automated code review — correctness, security, design, tests, plus duplicates and scope. Advisory: a human still decides. Not a substitute for review, and not exhaustive. The required status checks separately cover lint, formatting, types, unused dependencies, the test suite and the build. This bot never merges. Maintainers can rerun with \`/re-review\`.</sub>"
416+
echo "<sub>🔴 blocking · 🟠 a reviewer would ask for a change · ⚪ optional</sub>"
417+
echo
418+
echo "<sub>Automated code review — correctness, security, design, tests, plus duplicates and scope. A human still decides; this is not a substitute for review and is not exhaustive. The required status checks separately cover lint, formatting, types, unused dependencies, the test suite and the build. This bot never merges. Maintainers can rerun with \`/re-review\`.</sub>"
390419
} > /tmp/nc-review/comment.md
391420
392421
gh pr comment "$PR" --repo "$REPO" --body-file /tmp/nc-review/comment.md
393422
394-
if [ "$VERDICT" = "clean" ]; then
395-
gh pr edit "$PR" --repo "$REPO" --add-label "agent:clean" --remove-label "agent:needs-work" || true
396-
else
397-
gh pr edit "$PR" --repo "$REPO" --add-label "agent:needs-work" --remove-label "agent:clean" || true
398-
fi
423+
# Exactly one agent:* label should ever be present. Remove the other
424+
# two unconditionally so a re-review cannot leave a stale pair.
425+
case "$VERDICT" in
426+
clean) KEEP="agent:clean"; DROP="agent:comments agent:needs-work" ;;
427+
comments) KEEP="agent:comments"; DROP="agent:clean agent:needs-work" ;;
428+
needs-work) KEEP="agent:needs-work"; DROP="agent:clean agent:comments" ;;
429+
esac
430+
ARGS="--add-label $KEEP"
431+
for d in $DROP; do ARGS="$ARGS --remove-label $d"; done
432+
# shellcheck disable=SC2086
433+
gh pr edit "$PR" --repo "$REPO" $ARGS || true

0 commit comments

Comments
 (0)