name: 01-review description: Opus reads the PR diff against the NeoHaskell quality rubric, writes findings to .pipeline/findings-17.json, and surfaces a digest in conversation. kind: leaf executor: opus model: claude-opus-4-7
PR Review (Opus)
Opus reads the open PR diff and produces a structured findings file with proposed fixes. The maintainer reads the digest in-conversation, edits .pipeline/findings-17.json to accept/reject/defer each finding, then runs pipeline.py approve 17 to release the apply step.
Inputs
pipeline.py get pr_number— the PR to reviewgh pr diff <pr_number>— the diff under reviewgit diff main..HEAD— full branch delta as cross-checkdocs/architecture/<adr_number>-<slug>.md— architecture doc; check all named files exist in the treedocs/decisions/<adr_number>-<slug>.md— ADR for contextcore/CLAUDE.md— style table (cite when raising style findings)
Plan
- Read the diff and cross-reference it against the architecture doc.
- Apply the rubric below to every file in the diff.
- Emit
.pipeline/findings-17.json— an array of findings with proposed fixes (schema below). - Surface a markdown digest of the findings in the orchestrator's conversation.
- Call
pipeline.py complete 17. STOP (PAUSE — maintainer reviews next).
Findings schema
{
"rule": "<short-kebab-case-id>",
"severity": "high" | "medium" | "low",
"location": "<file:line[-line]>",
"explanation": "<one paragraph: what's wrong + why it matters>",
"proposed_fix": {
"kind": "edit" | "delete" | "rename" | "add",
"file": "<path>",
"summary": "<one sentence: what to change>"
},
"maintainer_decision": "pending"
}
The maintainer sets maintainer_decision to "accept", "reject", or "defer" per entry before running approve.
Rubric
Category 1: NeoHaskell style violations
Cite core/CLAUDE.md for the exact rule. Common offenders:
$used anywhere — should be|>whereclauses — should bedo letlet..in— should bedo let<>/++on strings — should be[fmt|...|]Either— should beResultIOin user code — should beTask err valpure/return— should beTask.yield val- Unqualified imports of nhcore modules — should be
import Foo (Foo); import Foo qualified !strictness annotations — nhcore hasStrictglobally;!is redundant- Single-letter type params — should be
forall element result.etc.
Category 2: Hackage imports without Ghc prefix
Any import of Control.Concurrent.Async, Data.Either, Data.IORef, Data.Map, Data.Map.Strict, Data.Vector, etc. must be aliased with a Ghc prefix (e.g. import qualified Data.Map.Strict as GhcMap). Bare or differently-aliased Hackage imports are a finding.
Category 3: Task.fromIO (… Task.runResult …) round-trip smell
Any code that does Task.fromIO (someIoLib (Task.runResult task)) is escaping Task to use an IO library, then reconstructing Task. This means a needed nhcore primitive is missing. Propose extending nhcore (e.g. add AsyncTask.race) rather than perpetuating the escape pattern.
Category 4: Dead code / unnecessary plumbing
- Aliases that wrap a single existing function without adding meaning (e.g.
nilUuid = Uuid.nil) - Redundant
Task.yield unitafter a step that already yields unit (ConcurrentVar.modify,Task.forEach, etc.) passfollowed byTask.yield unit- Public functions with no callers in the diff or elsewhere in the tree
case True/FalseoverBool— should beif/then/else
Category 5: Test anti-patterns
- Setup-error swallowing:
case … of Err (ConnectionFailed _) -> passor anyErr _ -> passon a fixture call — finding ruletest-swallows-infra-failure - Mismatched name vs body: test name says
emits/reads in chunks/logs/deletes/writes/updates/replays/resumes, body doesn't assert the named primitive — ruletest-name-body-mismatch - Trivial-fixture error-path:
it "fails with <X>"whose setup isSubscriber.new <InMemory>.new Registry.empty(or equivalent empty fixture) and body assertsErr (<X> _) -> pass— ruletest-trivial-fixture-error-path - Panicky-let non-behavioral: body is just
let _x = builder ... ; pass— ruletest-panicky-let-non-behavioral
Category 6: Architecture-doc round-trip
For every file path named in the architecture doc's "Module map" or similar section, verify the file exists in the tree. Each missing file → finding rule arch-doc-artifact-missing with severity high.
Category 7: Schema-migration smells
DROP TABLEin any file undercore/service/not matching*Spec.hs,/testlib/,/test-service/→ ruleschema-drop-table-production, severityhighCREATE TABLEin any application-startup code path (not in a file matching*/Migrations/*.sql) → ruleschema-migration-in-app-code, severitymedium
Output format
After writing .pipeline/findings-17.json, surface a markdown digest like:
## Opus PR review — N findings
| # | Severity | Rule | Location | Proposed fix |
|---|----------|------|----------|--------------|
| 1 | high | ghc-async-in-task | core/service/.../Subscriber.hs:300-321 | Replace GhcAsync.race with AsyncTask.race |
| 2 | medium | redundant-yield-unit | core/service/.../Subscriber.hs:390 | Drop trailing Task.yield unit |
| ... | ... | ... | ... | ... |
Maintainer: edit `.pipeline/findings-17.json` to set `maintainer_decision` for each (accept / reject / defer), then run `pipeline.py approve 17`.
Then call python3 .claude/skills/feature-pipeline-preview/scripts/pipeline.py complete 17 and STOP.
Refusals
- PR number not set in pipeline state → "phase 16 didn't record a PR; cannot review"
gh pr diffreturns non-zero → surface and refuse- Architecture doc missing → "phase 7 architecture doc missing; cannot do round-trip check"