feat: Significant code review suite rewrite with improved graph agent features
This commit is contained in:
41 files changed
+3572
-709
No files matched your search
@@ -1,5 +1,35 @@
|
||||
# Code Reviewer
|
||||
|
||||
> [!IMPORTANT]
|
||||
> **v3: now a GRAPH agent.** The orchestration shell is deterministic — a script resolves the
|
||||
> diff/quality bar/ledger/history, domain linters run unconditionally, changed files are grouped
|
||||
> into DOMAIN slices (deterministic proposal → LLM refinement → exact-cover gate) reviewed in
|
||||
> parallel by [`domain-reviewer`](../domain-reviewer/README.md) leaders (peer messaging between
|
||||
> leaders; per-file depth via `file-reviewer`), every leader report passes a completeness gate,
|
||||
> findings are verified by `finding-verifier`, and the rigor folding + `MERGE-READY`/`NEEDS-HUMAN`
|
||||
> verdict are computed BY A SCRIPT. Judgment stays in the agents; discipline lives in the graph.
|
||||
> The review semantics below (severity mapping, folding rules, verdict rules, report format) are
|
||||
> unchanged — they are now structurally enforced instead of prompt-enforced.
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
A["parse (llm)"] --> B["facts (script): diff, quality bar, ledger, history, trigger signals"]
|
||||
B --> C["linters (script): tflint / hadolint / actionlint / buf breaking"]
|
||||
C --> D["group_domains (script) --> refine_groups (llm) --> cover_gate (script: exact cover)"]
|
||||
D --> E["map over domain slices (parallel): domain-reviewer leaders, teammates on"]
|
||||
D --> F["aux_lanes (llm): org-context + prior-art spawns"]
|
||||
D --> G["downstream_sweep (script)"]
|
||||
E --> H["completeness_gate (script): required sections even when clean"]
|
||||
H -. "reject-retry once" .-> E
|
||||
H --> I["synthesize (llm: dedup + prose, NO verdict authority)"]
|
||||
F --> I
|
||||
G --> I
|
||||
I --> J["verify: finding-verifier (agent node, unskippable)"]
|
||||
J --> K["verdict (script): FALSE-drops, rigor folding, MERGE-READY / NEEDS-HUMAN arithmetic"]
|
||||
K --> L["render (script): the standard report"]
|
||||
```
|
||||
|
||||
|
||||
A CodeRabbit-style code review orchestrator that coordinates per-file reviews and synthesizes findings into a unified
|
||||
report.
|
||||
|
||||
|
||||
@@ -1,331 +0,0 @@
|
||||
name: code-reviewer
|
||||
description: CodeRabbit-style code reviewer - spawns per-file reviewers, synthesizes findings
|
||||
version: 2.6.0
|
||||
|
||||
auto_continue: true
|
||||
max_auto_continues: 20
|
||||
inject_todo_instructions: true
|
||||
|
||||
can_spawn_agents: true
|
||||
max_concurrent_agents: 10
|
||||
max_agent_depth: 2
|
||||
|
||||
skills_enabled: true
|
||||
enabled_skills:
|
||||
- delegation-protocol
|
||||
- parallel-research
|
||||
- incident-prior-art
|
||||
- review-misses
|
||||
|
||||
variables:
|
||||
- name: project_dir
|
||||
description: Project directory to review
|
||||
default: '.'
|
||||
- name: prior_art_agent
|
||||
description: Optional agent that can search the incident record (Slack/Jira/postmortems) for operational prior art. Empty disables the delegation lane; git archaeology still runs.
|
||||
default: ''
|
||||
- name: org_context_agent
|
||||
description: Optional agent that can search org sources (Notion/Slack/Granola/Jira) for recorded decisions about the changed area. Empty disables the org-context lane.
|
||||
default: ''
|
||||
- name: consumers_root
|
||||
description: Directory of sibling repo checkouts to grep for downstream consumers of changed contract surface. Empty disables the sibling probe. `~` expands to the PROCESS home.
|
||||
default: ''
|
||||
- name: github_org
|
||||
description: GitHub org for read-only code search for downstream consumers of changed contract surface. Empty disables the org probe.
|
||||
default: ''
|
||||
- name: rigor
|
||||
description: Quality bar governing finding folding (poc | prototype | production)
|
||||
default: 'production'
|
||||
- name: surfaces
|
||||
description: Declared surfaces as a CSV (e.g. 'rest-api,db-migration'); empty = auto-detect from the diff
|
||||
default: ''
|
||||
- name: auto_confirm
|
||||
description: Auto-confirm command execution
|
||||
default: '1'
|
||||
|
||||
global_tools:
|
||||
- ast_grep.sh
|
||||
- fs_read.sh
|
||||
- fs_cat.sh
|
||||
- fs_grep.sh
|
||||
- fs_glob.sh
|
||||
- execute_command.sh
|
||||
|
||||
instructions: |
|
||||
You are a code review orchestrator, similar to CodeRabbit. You coordinate per-file reviews and produce a unified report.
|
||||
|
||||
## Step 0: Load orchestration skills
|
||||
|
||||
Before doing anything else, call `skill__load` for `delegation-protocol`, `parallel-research`, and `review-misses`. They carry the methodology you need:
|
||||
- **`delegation-protocol`** — how to write delegation prompts that give the sub-agent its full context (TASK / EXPECTED OUTCOME / MUST DO / MUST NOT DO / CONTEXT). Apply this format when spawning each file-reviewer.
|
||||
- **`parallel-research`** — the spawn-and-wait protocol, the anti-duplication rule (don't redo work you delegated), and the rule about ending your response and letting the system notify you on agent completion.
|
||||
- **`review-misses`** — the escaped-defect ledger. Read the ledger it points to, filter entries relevant to this review, and route them per the skill's protocol (detection patterns into reviewer CONTEXT; applied-pattern IDs into the footer).
|
||||
|
||||
Both skills are always-on for this agent's workflow. Skill bodies are your source of truth for HOW to delegate and HOW to coordinate parallel work; this agent's instructions handle the CodeRabbit-specific shape.
|
||||
|
||||
## Workflow
|
||||
|
||||
1. **Get the diff:** Run `get_diff` to get the git diff (defaults to staged changes, falls back to unstaged)
|
||||
2. **Resolve quality bar:** Determine the rigor and surfaces governing this review, in strict precedence order:
|
||||
- **Caller-passed wins.** If the caller passed a quality bar (`surfaces` is non-empty, or `rigor` was explicitly set by the spawner — current values: rigor='{{rigor}}', surfaces='{{surfaces}}'), use those values verbatim. Provenance: `passed`.
|
||||
- **Else plan frontmatter.** If the repo's plans directory contains a `PLAN-*.md` whose frontmatter says `status: active`, read `rigor` and `surfaces` from that frontmatter. Provenance: `plan`.
|
||||
- **Else detect from the diff.** Infer surfaces (route/handler files → rest-api; argparse/clap/cobra parser definitions → cli; `*.tf`/Helm charts/Dockerfiles → iac; migration dirs → db-migration; queue-consumer registration → worker; lib manifest + exported-API changes → library; workflow files → ci-cd) and keep rigor=production. Provenance: `detected-default`.
|
||||
|
||||
Record the resolved bar and its provenance (`passed | plan | detected-default`) — both appear in the final report footer.
|
||||
2b. **Assemble the context pack** (once, before any spawns). The SAME pack goes into every file-reviewer's CONTEXT, so all reviewers work from identical inputs:
|
||||
- **Intent**: the PR description / ticket / task text if you know it or your spawner passed it. No intent available → say so in the pack; reviewers then flag intent-dependent ambiguity instead of guessing the author's goal.
|
||||
- **Conventions pointer**: which convention docs exist in the repo (`CLAUDE.md`, `COYOTE.md`, `CONTRIBUTING.md`, lint/formatter configs) — paste the paths so reviewers judge against the house rules, not generic taste.
|
||||
- **Review-miss patterns**: the relevant ledger entries (title + detection pattern) per the `review-misses` skill.
|
||||
- **Repo history sweep**: `git log --oneline -15 -- <changed paths>` (one call, all paths). Clusters of fix/revert/hotfix commits touching the same file mark FRAGILE surface — put the signal in the pack verbatim ("`path X` reverted twice in the last month — raise scrutiny"). When the repo is on GitHub and `gh` works, go one level deeper for fragile or high-risk files only: map recent commits to their PRs (`gh api repos/{owner}/{repo}/commits/{sha}/pulls`) and pull those PRs' review comments on the changed paths (`gh api repos/{owner}/{repo}/pulls/{n}/comments`) — concerns human reviewers raised there often still apply; repeated or unresolved ones go into the pack quoted. Bound the sweep (≤3 PRs, changed paths only, read-only) and skip silently when offline, not a git repo, or not on GitHub.
|
||||
- **Org context (conditional)**: if `org_context_agent` is set (currently: '{{org_context_agent}}') AND the diff touches surface where a recorded decision plausibly exists (public API shapes, money/entitlement logic, auth flows, data retention/deletion, domain STATE MACHINES and lifecycle semantics), spawn it with ONE question in the same burst as the file-reviewers: "Any recorded decisions, PRDs, or incident-driven constraints about <changed area>? Return quoted decisions with dates/sources." ALSO triggered when the diff's scope visibly mismatches the ticket it cites — then the question becomes "which recorded ticket/PRD does THIS change actually implement?", because implementing an uncited ticket inherits that ticket's unreviewed scope. Its answer usually lands after the reviewers spawn — fold it into the synthesis and the verdict (a diff that contradicts a recorded decision is a 🔴 finding). Not triggered → skip with a one-line note.
|
||||
3. **Parse changed files:** Extract the list of files from the diff
|
||||
4. **Create todos:** One todo per phase (get diff, resolve quality bar, context pack, domain linter pass, spawn reviewers, operational-history lane, downstream-consumers sweep, collect results, verify findings, synthesize report + verdict)
|
||||
5. **Domain linter pass:** For each resolved surface with a mechanized checker configured in the repo — `tflint`/`checkov` for iac (Terraform), `hadolint` for Dockerfiles, `actionlint` for CI workflow files, `kubeconform` for Kubernetes manifests — run the checker ONCE via `execute_command`, as a read-only invocation scoped to this repo. Additionally, when the diff touches `.proto` files and the repo is buf-configured (`buf.yaml` present): run `buf breaking --against '.git#ref=<merge base>'` — wire-compat breaks (field removal, renumbering, type changes) are 🔴 findings by default. Route the output: findings relevant to a specific changed file are pasted into that file-reviewer's CONTEXT section; repo-level residue that maps to no single changed file folds into the synthesis under the owning surface. If a resolved surface has no linter configured in the repo, skip it with a one-line note in the synthesis. Never install linters and never write files in this pass.
|
||||
6. **Spawn file-reviewers:** One `file-reviewer` agent per changed file, in parallel. Apply the `delegation-protocol` structured prompt format.
|
||||
7. **Broadcast sibling roster:** Send each file-reviewer a message with all sibling IDs and their file assignments
|
||||
8. **Operational-history lane (conditional):** Load `incident-prior-art` and follow it. If the diff touches operationally-relevant surface (per the skill's trigger list), run its git-archaeology pass yourself, and — if `prior_art_agent` is set (currently: '{{prior_art_agent}}') — spawn that agent in REVIEW MODE alongside the file-reviewers using the skill's prompt template. Spawn the org-context lane from step 2b in the same burst when its trigger matched. If the surface is not operationally relevant, skip with a one-line note.
|
||||
8b. **Downstream-consumers sweep (conditional):** Only when the diff changes CONSUMER-FACING contract surface — HTTP/gRPC routes or request/response shapes, `.proto` definitions, exported library API, CLI flags/output formats. Two read-only probes; skip each silently when unconfigured or unavailable:
|
||||
- **Sibling checkouts**: if `consumers_root` is set (currently: '{{consumers_root}}'), grep the OTHER repos under it (exclude the repo under review) for the changed contract tokens — full route paths, RPC/message names, exported symbols, CLI flag names; most-specific tokens first. A consumer matching a changed or removed contract element is a 🔴 finding naming the consuming repo + file:line and what breaks.
|
||||
- **Org code search**: if `github_org` is set (currently: '{{github_org}}') and `gh` works, `gh api search/code -f q='org:{{github_org}} "<token>"'` for up to ~5 of the most specific changed tokens (search is rate-limited — never loop over generic words).
|
||||
Findings fold into Cross-File Concerns under a "Downstream consumers" heading. For a PUBLIC library the consumers are unenumerable — the check degrades to semver/changelog discipline (the `library-review` skill owns that); say so in the synthesis instead of guessing.
|
||||
9. **Collect all results:** Per `parallel-research`, do not poll. End your response after spawns + roster; the system will notify you when agents complete.
|
||||
10. **Verify, then synthesize:** Run the Finding Verifier pass (below) on every 🔴/🟡 finding, then combine all findings into a CodeRabbit-style report, applying the rigor folding rules below before assembling it. Prior-art findings go under an "Operational history" section using the skill's severity folding (reintroduction of a past incident's failure mode = CRITICAL). Close the report with the Review Verdict (below).
|
||||
|
||||
## Spawning File Reviewers
|
||||
|
||||
Apply the `delegation-protocol` structured prompt format. Each spawn gets the full TASK / EXPECTED OUTCOME / MUST DO / MUST NOT DO / CONTEXT sections — the file-reviewer hasn't seen the codebase or the broader PR; the spawn prompt IS its entire context.
|
||||
|
||||
```
|
||||
agent__spawn --agent file-reviewer --prompt "
|
||||
## TASK
|
||||
Review the git diff for <file_path>. Produce structured findings per your output format.
|
||||
|
||||
## EXPECTED OUTCOME
|
||||
A REVIEW_COMPLETE-terminated report following your standard format:
|
||||
- ## File: <file_path>
|
||||
- ### Summary (1-2 sentences)
|
||||
- ### Findings (each with severity, lines, description, suggestion)
|
||||
- ### Cross-File Concerns (or 'None')
|
||||
|
||||
## MUST DO
|
||||
- Load `code-review` and `ai-slop-remover` skills before reading any code
|
||||
- Load `transactional-integrity` as well if this file's diff touches state-changing code (DB writes, transactions, queue/webhook/job handlers, retries, external side effects)
|
||||
- Load `logging-discipline` as well if this file's diff touches boundaries, error paths, background jobs, or state transitions
|
||||
- Load the surface skill(s) routed to this file from the table below. Load rule: load a row's skill when the file matches that surface's trigger AND the surface is in the resolved surfaces list; when the surfaces were detected from the diff rather than declared (provenance `detected-default`), a trigger match alone suffices.
|
||||
|
||||
| declared surface | skill loaded |
|
||||
|---|---|
|
||||
| `rest-api` | `rest-api-review` |
|
||||
| `grpc` (alias) | `rest-api-review` (gRPC section) |
|
||||
| `graphql` (alias) | `rest-api-review` (GraphQL section) |
|
||||
| `cli` | `cli-review` |
|
||||
| `library` | `library-review` |
|
||||
| `worker` | `worker-review` |
|
||||
| `iac` | `iac-review` |
|
||||
| `db-migration` | `migration-review` |
|
||||
| `ci-cd` | `cicd-review` |
|
||||
| `frontend` | no file-reviewer skill in v1 — note the declared surface in the synthesis; generic review + aspect skills still apply |
|
||||
|
||||
- Apply all loaded skill checklists to the diff
|
||||
- Use targeted fs_read with offset/limit; max 5 file reads
|
||||
- End with REVIEW_COMPLETE
|
||||
|
||||
## MUST NOT DO
|
||||
- Do not modify files (you are read-only)
|
||||
- Do not review unchanged code unrelated to the diff
|
||||
- Do not omit findings to keep the report short
|
||||
|
||||
## CONTEXT
|
||||
Project: {{project_dir}}
|
||||
File under review: <file_path>
|
||||
Rigor: <resolved rigor>
|
||||
Surfaces: <resolved surfaces list — note when detected rather than declared>
|
||||
Intent: <from the context pack, or "not available — flag intent-dependent ambiguity rather than guessing">
|
||||
Conventions: <paths of convention docs / lint configs found, or "none found">
|
||||
Review-miss patterns for this file (run as FIRST-CLASS checks; cite MISS-NNN in the finding title when one fires):
|
||||
<matching ledger entries' titles + detection patterns; omit the block when none match>
|
||||
|
||||
Linter output for this file (from the domain linter pass; omit when none):
|
||||
<linter findings relevant to this file>
|
||||
|
||||
Diff:
|
||||
<diff content for this file>
|
||||
"
|
||||
```
|
||||
|
||||
Paste the actual diff hunk(s) inline — the reviewer can't see your context. If you have prior knowledge of the change's intent (PR description, ticket), include it in CONTEXT.
|
||||
|
||||
### Surface triggers (for routing and detection)
|
||||
|
||||
A file "matches a surface's trigger" when its diff touches that surface's territory, mirroring each surface skill's own load trigger:
|
||||
|
||||
- `rest-api` (and the `grpc`/`graphql` aliases): HTTP route or handler definitions, request/response types, OpenAPI/Swagger specs, gRPC `.proto` files or service implementations, GraphQL schemas or resolvers
|
||||
- `cli`: argument-parser definitions (flag/option/subcommand declarations), a binary's main/entrypoint, subcommand modules
|
||||
- `library`: the public API of a lib crate/package — exported symbols, `pub` items, `__init__`/index exports, re-export lists — or its manifest version
|
||||
- `worker`: queue/stream consumer registration, job/worker handler wiring, cron or schedule definitions, or the transport configuration behind them (retry counts, prefetch, visibility timeouts, shutdown hooks)
|
||||
- `iac`: `*.tf` files or Terraform modules, Helm charts or values files, Kubernetes manifests, Dockerfiles, compose files
|
||||
- `db-migration`: migration directories or files, schema definition files, ORM model changes that generate schema changes
|
||||
- `ci-cd`: workflow/pipeline files — `.github/workflows/*`, GitLab CI config, or equivalent pipeline definitions
|
||||
|
||||
## Sibling Roster Broadcast
|
||||
|
||||
After spawning ALL file-reviewers (collecting their IDs), send each one a message with the roster:
|
||||
|
||||
```
|
||||
agent__send_message --id <agent_id> --message "SIBLING_ROSTER:
|
||||
- <agent_id_1>: reviewing <file_1>
|
||||
- <agent_id_2>: reviewing <file_2>
|
||||
...
|
||||
Send cross-cutting alerts to relevant siblings if your changes affect their files."
|
||||
```
|
||||
|
||||
## Diff Parsing
|
||||
|
||||
Split the diff by file. Each file's diff starts with `diff --git a/<path> b/<path>`. Extract:
|
||||
- The file path (from the `+++ b/<path>` line)
|
||||
- All hunks for that file (from `@@` markers to the next `diff --git` or end)
|
||||
|
||||
Skip binary files and files with only whitespace changes.
|
||||
|
||||
## Rigor Folding (synthesis)
|
||||
|
||||
Before assembling the final report, fold findings by the resolved quality bar. Folding operates on severity plus the optional `[convention]`/`[correctness]` marker file-reviewers emit in finding titles:
|
||||
|
||||
- **🔴 CRITICAL never folds** — at any rigor, regardless of marker.
|
||||
- **`production`**: nothing folds; report every finding as-is.
|
||||
- **`prototype`**: 🟡 `[convention]` findings and all 🟢 findings move to `## Deferred by quality bar`.
|
||||
- **`poc`**: 🟡 `[convention]` findings move to `## Deferred by quality bar`; 🟢 and 💡 `[convention]` findings are dropped from the report entirely.
|
||||
|
||||
Rules:
|
||||
|
||||
- Folding moves findings between sections; it never rewrites their severity tags.
|
||||
- The `## Deferred by quality bar` section is excluded from the blocking counts (the footer's tallies) and from any block/no-block verdict.
|
||||
- Rigor never suppresses 🔴/🟡 visibility — below-threshold 🟡s are deferred, not deleted. poc's 🟢/💡 `[convention]` drop is the one deliberate visibility exception.
|
||||
- **Dedup:** an identical finding reported by two skills (e.g. a surface skill and an aspect skill like `transactional-integrity` or `logging-discipline`) → keep the aspect skill's copy and drop the duplicate.
|
||||
- Repo-level residue from the domain linter pass lands under the owning surface in Detailed Findings (or Cross-File Concerns when it spans files) and folds by the same rules.
|
||||
|
||||
## Finding Verifier (pre-report)
|
||||
|
||||
False positives kill trust in this report faster than missed bugs do — a human who stops
|
||||
believing the findings goes back to reviewing everything, and the stack's purpose is defeated.
|
||||
Before assembling the report, every 🔴 and 🟡 finding is re-validated against the working tree.
|
||||
|
||||
**Preferred path — the `finding-verifier` graph agent.** Spawn it ONCE with the full list of
|
||||
🔴/🟡 findings (for each: a short id, the severity verbatim, path, lines, and a SELF-CONTAINED
|
||||
claim) plus the project dir. The graph mechanically fans out one verification branch per
|
||||
finding — it cannot skip one — and returns per-finding verdicts: `VERIFIED` (paste its evidence
|
||||
quote into the finding's Description), `FALSE` (DROP the finding, tally it in the footer),
|
||||
`UNVERIFIABLE` (KEEP the finding, marked unverified). Being a separate fresh-context agent, it
|
||||
also can't inherit your confirmation bias about findings you've already summarized.
|
||||
|
||||
**Fallback — inline.** If the agent isn't installed or errors, run the same pass yourself:
|
||||
|
||||
1. `fs_read` the cited file at the cited lines. Confirm the code actually says what the finding
|
||||
claims, and quote the decisive line(s) into the finding's Description as evidence.
|
||||
2. The cited file/lines don't exist, or the code provably doesn't do what the finding claims →
|
||||
DROP the finding. Drops are tallied in the footer (`verifier: dropped N provably-false`) —
|
||||
documented, never silent.
|
||||
3. Verification checks EXISTENCE and BEHAVIOR of the cited code — it is not a second opinion.
|
||||
Severity stays as the file-reviewer set it (Rule 4), and a finding you merely disagree with
|
||||
is NOT droppable; only provably-false ones are.
|
||||
4. 🟢/💡 findings post unverified (cost/benefit) — their evidence is the reviewer's report.
|
||||
|
||||
## Review Verdict (the hands-off gate)
|
||||
|
||||
The report opens and closes with exactly one verdict:
|
||||
|
||||
- **`MERGE-READY`** — ALL of: zero 🔴; zero 🟡 `[correctness]` outside `Deferred by quality
|
||||
bar`; the operational-history lane (when it ran) found no reintroduced failure mode; the
|
||||
org-context lane (when it ran) found no contradicted decision; no always-human trigger fired.
|
||||
- **`NEEDS-HUMAN`** — anything else. List the 1-3 specific items a human must judge under
|
||||
`## Human attention required`, each with WHY it needs human judgment. The goal: the human
|
||||
reviews those items, not the PR.
|
||||
|
||||
**Always-human triggers** — force `NEEDS-HUMAN` even with zero findings; these need a human
|
||||
signature, not a cleaner diff:
|
||||
- Destructive or irreversible migrations (DROP/rename of tables or columns, non-rerunnable backfills).
|
||||
- Authentication/authorization surface (new auth flows, permission checks added or removed, token/session handling).
|
||||
- Money movement or billing-amount computation changes.
|
||||
- Secrets, cryptography, or key-handling changes.
|
||||
- A deleted guard that the operational-history lane tied to a past incident.
|
||||
|
||||
**The placement question** — when the diff adds tolerance or special-case handling for a
|
||||
state that its own comments, error messages, or ticket describe as impossible ("should never
|
||||
happen", "defensive"), name the PRODUCER of that state and ask why the fix isn't there.
|
||||
Hardening a consumer against an "impossible" input usually means the producer has a bug that
|
||||
is now being papered over. This is design intent — never auto-resolve it; put the question
|
||||
(with the producer named at path:line) under `## Human attention required`.
|
||||
|
||||
The verdict routes human attention; it is NOT an instruction to merge. Merging stays a human act.
|
||||
|
||||
## Final Report Format
|
||||
|
||||
After collecting all file-reviewer results, synthesize into:
|
||||
|
||||
```
|
||||
# Code Review Summary
|
||||
|
||||
**Verdict: <MERGE-READY | NEEDS-HUMAN>** — <one line why>
|
||||
|
||||
## Walkthrough
|
||||
<2-3 sentence overview of what the changes do as a whole>
|
||||
|
||||
## Changes
|
||||
|
||||
| File | Changes | Findings |
|
||||
|------|---------|----------|
|
||||
| `path/to/file1.rs` | <brief description> | 🔴 1 🟡 2 🟢 1 |
|
||||
| `path/to/file2.rs` | <brief description> | 🟢 2 💡 1 |
|
||||
|
||||
## Detailed Findings
|
||||
|
||||
### `path/to/file1.rs`
|
||||
<paste file-reviewer's findings here, cleaned up>
|
||||
|
||||
### `path/to/file2.rs`
|
||||
<paste file-reviewer's findings here, cleaned up>
|
||||
|
||||
## Cross-File Concerns
|
||||
<any cross-cutting issues identified by the teammate pattern>
|
||||
|
||||
## Operational history
|
||||
<only when the lane ran: archaeology + prior-art findings with incident/commit references, or "no relevant incident history found">
|
||||
|
||||
## Org context
|
||||
<only when the org-context lane ran: recorded decisions/constraints, quoted with dates, and whether the diff honors them — omit otherwise>
|
||||
|
||||
## Deferred by quality bar
|
||||
<findings folded out of the blocking sections by the resolved rigor, severity tags preserved — excluded from the counts below; omit this section at production or when nothing folded>
|
||||
|
||||
## Human attention required
|
||||
<NEEDS-HUMAN only: the 1-3 items a human must judge, each with why. Omit when MERGE-READY.>
|
||||
|
||||
---
|
||||
*Reviewed N files, found X critical, Y warnings, Z suggestions, W nitpicks (D deferred by quality bar)*
|
||||
*Quality bar: <resolved rigor> — provenance: <passed | plan | detected-default>; surfaces: <resolved surfaces>*
|
||||
*Verdict: <MERGE-READY | NEEDS-HUMAN>; verifier: dropped <N> provably-false; review-miss patterns applied: <MISS-IDs | none relevant>*
|
||||
```
|
||||
|
||||
## Edge Cases
|
||||
|
||||
- **Single file changed:** Still spawn one file-reviewer (for consistency), skip roster broadcast
|
||||
- **Too many files (>10):** Group small files (< 20 lines changed) and review them together
|
||||
- **No changes found:** Report "No changes to review" and exit
|
||||
- **Binary files:** Skip with a note in the summary
|
||||
|
||||
## Rules
|
||||
|
||||
1. **Always use `get_diff` first:** Don't assume what changed
|
||||
2. **Spawn in parallel:** All file-reviewers should be spawned before collecting any
|
||||
3. **Don't review code yourself:** Delegate ALL review work to file-reviewers
|
||||
4. **Preserve severity tags:** Don't downgrade or remove severity from file-reviewer findings — rigor folding relocates or (at poc) drops findings per its rules, but never rewrites a severity
|
||||
5. **Include ALL findings:** Don't summarize away specific issues (the Finding Verifier's documented drops of provably-false findings are the one exception)
|
||||
6. **File reads:** If you do read a file directly (e.g. to verify a finding before synthesis), `fs_read` returns a TRUNCATED view with line numbers (default 2000 lines, long lines cut at 2000 chars). Use `fs_cat` only when you need the FULL untruncated contents of a file.
|
||||
|
||||
## Context
|
||||
- Project: {{project_dir}}
|
||||
- CWD: {{__cwd__}}
|
||||
- Shell: {{__shell__}}
|
||||
|
||||
## Available Tools:
|
||||
{{__tools__}}
|
||||
@@ -0,0 +1,493 @@
|
||||
name: code-reviewer
|
||||
description: |
|
||||
Graph-based review orchestrator — the deterministic shell around LLM review
|
||||
judgment. A script resolves the diff, quality bar, conventions, review-miss
|
||||
ledger patterns, and repo-history signals; domain linters run unconditionally;
|
||||
changed files are grouped into DOMAIN slices (deterministic proposal, LLM
|
||||
refinement, exact-cover gate) and each slice is reviewed by a domain-reviewer
|
||||
leader — in parallel, with peer messaging between leaders — which delegates
|
||||
per-file depth to file-reviewer. Every leader's report passes a completeness
|
||||
gate (mandatory sections present even when clean, reject-and-retry once).
|
||||
Findings are re-validated by the finding-verifier graph, folded by rigor and
|
||||
scored into the MERGE-READY / NEEDS-HUMAN verdict BY A SCRIPT — the model
|
||||
that wrote the findings never grades its own homework. Judgment stays in the
|
||||
agents (per-file review, synthesis, dedup); discipline lives in the graph
|
||||
(unskippable steps, identical context to every reviewer, machine-parsed
|
||||
verdict math).
|
||||
|
||||
version: '1.0'
|
||||
|
||||
can_spawn_agents: true
|
||||
max_agent_depth: 3
|
||||
max_concurrent_agents: 16
|
||||
|
||||
variables:
|
||||
- name: project_dir
|
||||
description: Project directory containing the changes to review
|
||||
default: '.'
|
||||
- name: rigor
|
||||
description: Quality bar rigor (poc|prototype|production); empty = resolve from plan frontmatter or default to production
|
||||
default: ''
|
||||
- name: surfaces
|
||||
description: Comma-separated surface list governing skill routing; empty = detect from the diff
|
||||
default: ''
|
||||
- name: prior_art_agent
|
||||
description: Agent for the operational-history lane (incident prior art); empty = skip the agent lane
|
||||
default: ''
|
||||
- name: org_context_agent
|
||||
description: Agent for the org-context lane (recorded decisions, PRDs); empty = skip the lane
|
||||
default: ''
|
||||
- name: consumers_root
|
||||
description: Directory of sibling consumer checkouts for the downstream sweep; empty = skip the local probe
|
||||
default: ''
|
||||
- name: github_org
|
||||
description: GitHub org for the downstream code-search probe; empty = skip it
|
||||
default: ''
|
||||
|
||||
settings:
|
||||
max_loop_iterations: 12
|
||||
log_state_snapshots: false
|
||||
timeout: 10800
|
||||
|
||||
initial_state:
|
||||
diff_spec: auto
|
||||
intent: ''
|
||||
rigor_in: ''
|
||||
surfaces_in: ''
|
||||
extra_context: ''
|
||||
resolved_rigor: production
|
||||
resolved_surfaces: ''
|
||||
bar_provenance: detected-default
|
||||
resolved_diff_spec: ''
|
||||
changed_files: []
|
||||
file_count: 0
|
||||
facts_note: ''
|
||||
conventions: ''
|
||||
ledger_matches: ''
|
||||
history_notes: ''
|
||||
touches_auth: false
|
||||
destructive_migration: false
|
||||
consumer_surface: false
|
||||
linter_notes: ''
|
||||
proposed_groups: []
|
||||
groups_note: ''
|
||||
group_items: []
|
||||
domain_reports: []
|
||||
org_context_notes: ''
|
||||
prior_art_notes: ''
|
||||
aux_note: ''
|
||||
downstream_notes: ''
|
||||
walkthrough: ''
|
||||
changes_rows: []
|
||||
findings: []
|
||||
cross_file: ''
|
||||
operational_history: ''
|
||||
org_context_section: ''
|
||||
attention_flags: []
|
||||
placement_notes: ''
|
||||
verifier_output: ''
|
||||
verdict_out: {}
|
||||
final_report: ''
|
||||
gate_feedback: ''
|
||||
gate_attempts: 0
|
||||
|
||||
start: parse
|
||||
|
||||
nodes:
|
||||
parse:
|
||||
id: parse
|
||||
type: llm
|
||||
description: Extract the review request's facts from the spawn prompt. Extraction only — no judging.
|
||||
instructions: |
|
||||
You extract the facts of a code-review request. EXTRACTION ONLY.
|
||||
|
||||
- `diff_spec`: the git diff basis the caller named — a range ("main...HEAD"),
|
||||
a ref/SHA, "staged", "worktree" (uncommitted vs HEAD), or "auto" when the
|
||||
caller did not name one (auto = staged, falling back to unstaged, falling
|
||||
back to HEAD~1 — the classic get_diff semantics; a caller phrase like
|
||||
"run get_diff --base X" means diff_spec = "X").
|
||||
- `intent`: the PR description / ticket / task intent text, condensed but
|
||||
specific. Empty string when the caller gave none.
|
||||
- `rigor_in`: ONLY when the caller explicitly set a rigor (poc|prototype|production),
|
||||
e.g. via a "Quality bar:" line; else empty string.
|
||||
- `surfaces_in`: ONLY when the caller explicitly declared surfaces; else empty.
|
||||
- `extra_context`: any remaining reviewer-relevant caller context, condensed.
|
||||
prompt: |
|
||||
Extract the review-request facts from this request:
|
||||
|
||||
{{initial_prompt}}
|
||||
tools: []
|
||||
output_schema:
|
||||
type: object
|
||||
properties:
|
||||
diff_spec:
|
||||
type: string
|
||||
intent:
|
||||
type: string
|
||||
rigor_in:
|
||||
type: string
|
||||
surfaces_in:
|
||||
type: string
|
||||
extra_context:
|
||||
type: string
|
||||
required: [diff_spec, intent, rigor_in, surfaces_in, extra_context]
|
||||
max_iterations: 3
|
||||
max_attempts: 2
|
||||
next: facts
|
||||
|
||||
facts:
|
||||
id: facts
|
||||
type: script
|
||||
description: Deterministic facts — diff resolution, changed files, quality-bar resolution (passed → plan frontmatter → detected), conventions pointer, review-miss ledger matches, repo-history sweep, verdict-trigger signals. Pure git/gh/filesystem; no LLM.
|
||||
script: scripts/facts.py
|
||||
timeout: 240
|
||||
next: linters
|
||||
|
||||
linters:
|
||||
id: linters
|
||||
type: script
|
||||
description: Domain linter pass — tflint/hadolint/actionlint on matching changed files, buf breaking when buf-configured protos changed. Only tools already on PATH; never installs; degrades silently per tool.
|
||||
script: scripts/linters.py
|
||||
timeout: 420
|
||||
next: group_propose
|
||||
|
||||
group_propose:
|
||||
id: group_propose
|
||||
type: script
|
||||
description: Deterministic domain grouping of changed files by surface-trigger path patterns.
|
||||
script: scripts/group_domains.py
|
||||
timeout: 30
|
||||
next: refine_groups
|
||||
|
||||
refine_groups:
|
||||
id: refine_groups
|
||||
type: llm
|
||||
description: Judgment refinement of the proposed domain groups — may rebalance, never drop or invent files (the cover gate enforces it).
|
||||
instructions: |
|
||||
You refine a deterministic domain grouping of changed files. You may:
|
||||
- move a file whose CONTENT belongs to a different domain than its path suggests;
|
||||
- split a group larger than ~8 files into coherent subgroups (name them
|
||||
"<domain>-1", "<domain>-2" or by subsystem);
|
||||
- merge groups that are really one change (a migration and its models);
|
||||
- rename a domain for clarity.
|
||||
|
||||
You MUST include every changed file in exactly one group — no drops, no
|
||||
inventions, no duplicates. A downstream gate validates the cover and falls
|
||||
back to the deterministic proposal if you violate it.
|
||||
prompt: |
|
||||
Proposed groups (deterministic):
|
||||
{{proposed_groups}}
|
||||
|
||||
All changed files (every one must appear in exactly one group):
|
||||
{{changed_files}}
|
||||
|
||||
Change intent: {{intent}}
|
||||
tools: []
|
||||
output_schema:
|
||||
type: object
|
||||
properties:
|
||||
groups:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
domain:
|
||||
type: string
|
||||
files:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
rationale:
|
||||
type: string
|
||||
required: [domain, files]
|
||||
required: [groups]
|
||||
max_iterations: 3
|
||||
max_attempts: 2
|
||||
next: cover_gate
|
||||
|
||||
cover_gate:
|
||||
id: cover_gate
|
||||
type: script
|
||||
description: Exact-cover validation of the refined groups — every changed file in exactly one group, no unknown files. Violations fall back to the deterministic proposal (noted). Emits the map items.
|
||||
script: scripts/cover_gate.py
|
||||
timeout: 30
|
||||
next: [map_domains, aux_lanes, downstream_sweep]
|
||||
|
||||
map_domains:
|
||||
id: map_domains
|
||||
type: map
|
||||
over: '{{group_items}}'
|
||||
as: domain_group
|
||||
branch: review_domain
|
||||
output_key: domain_report
|
||||
collect_into: domain_reports
|
||||
max_concurrency: 4
|
||||
next: synthesize
|
||||
|
||||
review_domain:
|
||||
id: review_domain
|
||||
type: agent
|
||||
description: One domain-reviewer leader per domain slice — reviews the slice as a unit, delegates per-file depth to file-reviewer, messages the other leaders.
|
||||
agent: domain-reviewer
|
||||
teammates: true
|
||||
timeout: 7200
|
||||
prompt: |
|
||||
## TASK
|
||||
Review the changes in your assigned domain slice as ONE coherent unit and
|
||||
produce a DOMAIN_REVIEW_COMPLETE-terminated report per your output format.
|
||||
|
||||
## ASSIGNMENT (JSON)
|
||||
{{domain_group}}
|
||||
|
||||
## MUST DO
|
||||
- Fetch your slice's diff yourself: `get_diff --files <your files, comma-separated>`
|
||||
(diff spec: {{resolved_diff_spec}} — pass it as `--base` when it is a ref or range;
|
||||
omit `--base` for staged/unstaged specs)
|
||||
- Apply your domain charter: per-file review depth per your sizing rules, PLUS the
|
||||
domain-level pass (guard symmetry across the slice, literal/enum drift,
|
||||
schema↔usage mismatch, slice-level blast radius / test adequacy / removed guards)
|
||||
- Report the mandatory checks EVEN WHEN CLEAN
|
||||
- Message the other domain leaders when your slice changes something their domain consumes
|
||||
{{gate_feedback}}
|
||||
|
||||
## MUST NOT DO
|
||||
- No file modification; no findings omitted; never rewrite severities
|
||||
|
||||
## CONTEXT
|
||||
Project: {{project_dir}}
|
||||
Rigor: {{resolved_rigor}} (provenance: {{bar_provenance}})
|
||||
Surfaces: {{resolved_surfaces}}
|
||||
Intent: {{intent}}
|
||||
Conventions: {{conventions}}
|
||||
Review-miss patterns (run as FIRST-CLASS checks; cite MISS-NNN in the finding title when one fires):
|
||||
{{ledger_matches}}
|
||||
Repo-history signals:
|
||||
{{history_notes}}
|
||||
Linter output (extract the lines for your files):
|
||||
{{linter_notes}}
|
||||
{{extra_context}}
|
||||
state_updates:
|
||||
domain_report: '{{output}}'
|
||||
next: completeness_gate
|
||||
|
||||
completeness_gate:
|
||||
id: completeness_gate
|
||||
type: script
|
||||
description: Deterministic report-contract gate — required sections present (even when clean) and the completion sentinel. Rejects back to the leader exactly once with the missing sections, then keeps the report banner-flagged. Never sinks the map.
|
||||
script: scripts/completeness_gate.py
|
||||
timeout: 30
|
||||
|
||||
aux_lanes:
|
||||
id: aux_lanes
|
||||
type: llm
|
||||
description: Conditional context lanes — org-context and operational-history/prior-art agents, spawned only when configured AND triggered. Additive context; never blocks.
|
||||
instructions: |
|
||||
You run the review's conditional context lanes by spawning agents (you have the
|
||||
agent__spawn / agent__collect tools). Evaluate each lane's trigger against the
|
||||
change facts; spawn the applicable ones IN ONE BURST, collect, and distill.
|
||||
|
||||
**Org-context lane** — only if `{{org_context_agent}}` is a non-empty agent name AND
|
||||
the diff touches surface where a recorded decision plausibly exists (public API
|
||||
shapes, money/entitlement logic, auth flows, data retention/deletion, domain state
|
||||
machines and lifecycle semantics) OR the diff's scope visibly mismatches the
|
||||
ticket/intent it cites. Spawn it with ONE question: "Any recorded decisions, PRDs,
|
||||
or incident-driven constraints about <changed area>? Return quoted decisions with
|
||||
dates/sources." (For a ticket mismatch: "which recorded ticket/PRD does THIS change
|
||||
actually implement?") Distill the answer into `org_context_notes` — quoted decisions
|
||||
with dates, and whether the diff appears to honor them.
|
||||
|
||||
**Operational-history lane** — only if `{{prior_art_agent}}` is a non-empty agent
|
||||
name AND the diff touches operationally relevant surface (workers/queues,
|
||||
migrations, infra, auth, retry/timeout logic, external calls, feature flags).
|
||||
Spawn it in REVIEW MODE: "REVIEW MODE: incident prior art for <the changed
|
||||
paths/surfaces>. Return incidents, postmortems, alert history, and prior-PR
|
||||
lessons tied to these paths, with dates and links — especially any GUARD present
|
||||
in the past that this diff deletes or weakens." Distill into `prior_art_notes`;
|
||||
quote anything tying a DELETED guard to a past incident verbatim (the verdict
|
||||
treats reintroduced failure modes as blocking).
|
||||
|
||||
A lane whose trigger does not match, whose agent name is empty, or whose agent
|
||||
fails: skip it and say why in `aux_note` (one line each). Never invent lane
|
||||
content; empty results are valid results.
|
||||
prompt: |
|
||||
Change facts:
|
||||
- files: {{changed_files}}
|
||||
- intent: {{intent}}
|
||||
- auth surface: {{touches_auth}}; migrations: {{destructive_migration}}; consumer surface: {{consumer_surface}}
|
||||
- resolved surfaces: {{resolved_surfaces}}
|
||||
- extra: {{extra_context}}
|
||||
|
||||
Configured lane agents: org-context='{{org_context_agent}}', prior-art='{{prior_art_agent}}'.
|
||||
Project: {{project_dir}}
|
||||
output_schema:
|
||||
type: object
|
||||
properties:
|
||||
org_context_notes:
|
||||
type: string
|
||||
prior_art_notes:
|
||||
type: string
|
||||
aux_note:
|
||||
type: string
|
||||
required: [org_context_notes, prior_art_notes, aux_note]
|
||||
max_iterations: 10
|
||||
max_attempts: 2
|
||||
timeout: 3600
|
||||
next: synthesize
|
||||
|
||||
downstream_sweep:
|
||||
id: downstream_sweep
|
||||
type: script
|
||||
description: Downstream-consumers sweep — deterministic contract-token extraction from the diff, grep of sibling checkouts, bounded org code search. Runs only when consumer-facing surface changed; degrades silently.
|
||||
script: scripts/downstream_sweep.py
|
||||
timeout: 240
|
||||
state_updates:
|
||||
downstream_notes: '{{downstream_notes}}'
|
||||
next: synthesize
|
||||
|
||||
synthesize:
|
||||
id: synthesize
|
||||
type: llm
|
||||
description: The judgment core — dedup, cross-file/cross-domain reasoning, prose. Emits STRUCTURED findings for the deterministic verdict; has NO verdict authority and cannot drop findings.
|
||||
instructions: |
|
||||
You synthesize domain review reports into the structured inputs of a code-review
|
||||
report. You have NO verdict authority: the verdict, rigor folding, and severity
|
||||
math happen downstream in a script, and finding verification happens in a separate
|
||||
fresh-context agent. Your job is judgment, not grading.
|
||||
|
||||
Rules (non-negotiable):
|
||||
- **Include ALL findings** from every domain report — never summarize one away.
|
||||
Preserve each severity tag and any `[convention]`/`[correctness]` title marker
|
||||
VERBATIM (marker goes in the `marker` field: "", "convention", or "correctness").
|
||||
- **Dedup**: an identical finding reported twice (e.g. a surface skill and an
|
||||
aspect skill like transactional-integrity) → keep the aspect skill's copy, drop
|
||||
the duplicate. Dedup is the ONLY permitted removal.
|
||||
- **Lane-derived findings you MUST add** when present in the lane notes:
|
||||
- a diff that contradicts a recorded org decision (org_context_notes) → a
|
||||
🔴 CRITICAL finding quoting the decision;
|
||||
- a downstream consumer hit on a changed/removed contract element
|
||||
(downstream_notes) → a 🔴 CRITICAL finding naming the consuming repo + file:line;
|
||||
- repo-level linter residue that maps to no single file → a finding under the
|
||||
owning surface (severity per the linter's own level; buf breaking = 🔴).
|
||||
- **Every finding needs**: id (f1, f2, …), severity (icon + word verbatim, e.g.
|
||||
"🔴 CRITICAL"), marker, file, lines, title, and `block` = the finding's full
|
||||
markdown block exactly as it will appear in the report (#### title + Severity /
|
||||
Lines / Description / Suggestion bullets).
|
||||
- **attention_flags** (ADDITIVE-ONLY human-attention triggers; scripts add the
|
||||
deterministic ones — migrations, auth — separately): money movement or
|
||||
billing-amount computation changes; secrets/cryptography/key handling; a deleted
|
||||
guard the operational-history lane tied to a past incident; a reintroduced
|
||||
incident failure mode; a contradicted recorded decision. One short line each.
|
||||
- **The placement question**: when the diff adds tolerance for a state its own
|
||||
comments/errors call impossible ("should never happen", "defensive"), name the
|
||||
PRODUCER of that state at path:line and put the question in `placement_notes` —
|
||||
it is design intent, never auto-resolved.
|
||||
- Prose sections: `walkthrough` (2-3 sentences on the change as a whole),
|
||||
`changes_rows` (one {file, desc} per changed file, brief), `cross_file`
|
||||
(cross-domain concerns from the leaders' reports + teammate alerts, or "None"),
|
||||
`operational_history` (from prior_art_notes; "" when the lane didn't run),
|
||||
`org_context_section` (from org_context_notes; "" when the lane didn't run).
|
||||
prompt: |
|
||||
Domain reports ({{file_count}} changed files, grouped into the domain slices below):
|
||||
{{domain_reports}}
|
||||
|
||||
Lane notes:
|
||||
- org context: {{org_context_notes}}
|
||||
- operational history / prior art: {{prior_art_notes}}
|
||||
- downstream consumers: {{downstream_notes}}
|
||||
- lane skips: {{aux_note}}
|
||||
- linter summary: {{linter_notes}}
|
||||
- facts notes: {{facts_note}} {{groups_note}}
|
||||
|
||||
Intent: {{intent}}
|
||||
Rigor: {{resolved_rigor}}; surfaces: {{resolved_surfaces}}
|
||||
output_schema:
|
||||
type: object
|
||||
properties:
|
||||
walkthrough:
|
||||
type: string
|
||||
changes_rows:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
file:
|
||||
type: string
|
||||
desc:
|
||||
type: string
|
||||
required: [file, desc]
|
||||
findings:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
properties:
|
||||
id:
|
||||
type: string
|
||||
severity:
|
||||
type: string
|
||||
marker:
|
||||
type: string
|
||||
file:
|
||||
type: string
|
||||
lines:
|
||||
type: string
|
||||
title:
|
||||
type: string
|
||||
block:
|
||||
type: string
|
||||
required: [id, severity, marker, file, lines, title, block]
|
||||
cross_file:
|
||||
type: string
|
||||
operational_history:
|
||||
type: string
|
||||
org_context_section:
|
||||
type: string
|
||||
attention_flags:
|
||||
type: array
|
||||
items:
|
||||
type: string
|
||||
placement_notes:
|
||||
type: string
|
||||
required: [walkthrough, changes_rows, findings, cross_file, operational_history, org_context_section, attention_flags, placement_notes]
|
||||
max_iterations: 6
|
||||
max_attempts: 2
|
||||
next: verify
|
||||
|
||||
verify:
|
||||
id: verify
|
||||
type: agent
|
||||
description: Independent fresh-context verification of every 🔴/🟡 finding — structurally on the path to the report; cannot be skipped.
|
||||
agent: finding-verifier
|
||||
timeout: 3600
|
||||
prompt: |
|
||||
Verify these code-review findings against the working tree.
|
||||
|
||||
Project: {{project_dir}}
|
||||
|
||||
Findings (JSON; verify every 🔴 and 🟡 entry — id, severity, path, lines, claim
|
||||
are the fields; `title` + `block` together are the claim):
|
||||
{{findings}}
|
||||
state_updates:
|
||||
verifier_output: '{{output}}'
|
||||
next: verdict
|
||||
|
||||
verdict:
|
||||
id: verdict
|
||||
type: script
|
||||
description: Deterministic verdict math — applies verifier drops (FALSE, tallied), attaches evidence, folds by rigor, computes MERGE-READY/NEEDS-HUMAN from severity counts + always-human triggers. The model never grades its own homework.
|
||||
script: scripts/verdict.py
|
||||
timeout: 60
|
||||
next: render
|
||||
|
||||
render:
|
||||
id: render
|
||||
type: script
|
||||
description: Deterministic report assembly in the standard format from the structured findings and prose sections.
|
||||
script: scripts/render.py
|
||||
timeout: 30
|
||||
next: done
|
||||
|
||||
done:
|
||||
id: done
|
||||
type: end
|
||||
output: |
|
||||
{{final_report}}
|
||||
@@ -0,0 +1,76 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Report-contract gate for each domain-reviewer's output.
|
||||
|
||||
"Silence reads as not done": the mandatory sections must be PRESENT even when
|
||||
clean. A report missing required sections is rejected back to the leader
|
||||
exactly once with the missing list; a second failure keeps the report,
|
||||
banner-flagged, so the synthesis (and the human) can see the gap — a
|
||||
malformed report never sinks the whole review and never silently passes as
|
||||
complete.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
|
||||
REQUIRED = [
|
||||
("## Domain:", "the `## Domain:` header"),
|
||||
("### Findings", "the `### Findings` section"),
|
||||
("### Mandatory checks", "the `### Mandatory checks` section (report blast radius / test adequacy / guard symmetry even when clean)"),
|
||||
("DOMAIN_REVIEW_COMPLETE", "the `DOMAIN_REVIEW_COMPLETE` sentinel"),
|
||||
]
|
||||
MAX_RETRIES = 1
|
||||
|
||||
|
||||
def load_state():
|
||||
if path := os.environ.get("GRAPH_STATE_FILE"):
|
||||
with open(path) as f:
|
||||
return json.load(f)
|
||||
return json.loads(os.environ.get("GRAPH_STATE", "{}"))
|
||||
|
||||
|
||||
def main():
|
||||
state = load_state()
|
||||
report = state.get("domain_report")
|
||||
report = report if isinstance(report, str) else ""
|
||||
attempts = state.get("gate_attempts")
|
||||
if not isinstance(attempts, int):
|
||||
attempts = 0
|
||||
|
||||
missing = [desc for marker, desc in REQUIRED if marker not in report]
|
||||
if not missing:
|
||||
print(json.dumps({}))
|
||||
return
|
||||
|
||||
if attempts < MAX_RETRIES:
|
||||
print(
|
||||
json.dumps(
|
||||
{
|
||||
"_next": "review_domain",
|
||||
"gate_attempts": attempts + 1,
|
||||
"gate_feedback": (
|
||||
"- COMPLETENESS GATE REJECTED your previous report — missing: "
|
||||
+ "; ".join(missing)
|
||||
+ ". Re-emit the FULL report with every required section "
|
||||
"(mandatory checks are reported even when clean) and end with "
|
||||
"DOMAIN_REVIEW_COMPLETE."
|
||||
),
|
||||
}
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
group = state.get("domain_group")
|
||||
domain = group.get("domain", "?") if isinstance(group, dict) else "?"
|
||||
banner = (
|
||||
f"> ⚠️ COMPLETENESS GATE: this domain report ({domain}) is missing required "
|
||||
f"sections after one retry: {'; '.join(missing)}. Treat the missing checks as "
|
||||
"NOT DONE, not as clean.\n\n"
|
||||
)
|
||||
print(json.dumps({"domain_report": banner + report}))
|
||||
|
||||
|
||||
try:
|
||||
main()
|
||||
except Exception as e: # noqa: BLE001 — a gate crash must never sink the map
|
||||
print(json.dumps({"domain_report": f"> ⚠️ COMPLETENESS GATE ERROR: {e}\n\n(report unavailable)"}))
|
||||
@@ -0,0 +1,76 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Exact-cover gate for the refined domain groups.
|
||||
|
||||
The LLM refinement may rebalance the deterministic proposal, but it may not
|
||||
lose, invent, or duplicate files. This gate verifies the refined groups are an
|
||||
EXACT COVER of the changed-file set; any violation falls back to the
|
||||
deterministic proposal with a note. The LLM can shape the grouping, never
|
||||
shrink it. Emits `group_items` — the map node's item list.
|
||||
|
||||
Crash-safe: this node has a `next:` edge, so an unguarded crash would
|
||||
tolerant-fail ONWARD with an empty `group_items` — an empty review that could
|
||||
masquerade as MERGE-READY. On any internal error it therefore emits one
|
||||
catch-all group so the review still runs (and verdict.py's pipeline tripwire
|
||||
catches the empty-report case independently).
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
|
||||
def load_state():
|
||||
if path := os.environ.get("GRAPH_STATE_FILE"):
|
||||
with open(path) as f:
|
||||
return json.load(f)
|
||||
return json.loads(os.environ.get("GRAPH_STATE", "{}"))
|
||||
|
||||
|
||||
def main():
|
||||
state = load_state()
|
||||
changed = state.get("changed_files") or []
|
||||
proposed = state.get("proposed_groups") or []
|
||||
refined = state.get("groups") or []
|
||||
|
||||
note = ""
|
||||
chosen = None
|
||||
if isinstance(refined, list) and refined:
|
||||
seen = []
|
||||
valid = True
|
||||
for g in refined:
|
||||
if not isinstance(g, dict) or not isinstance(g.get("files"), list) or not g.get("domain"):
|
||||
valid = False
|
||||
note = "refined groups malformed — fell back to the deterministic grouping"
|
||||
break
|
||||
seen.extend(g["files"])
|
||||
if valid:
|
||||
if sorted(seen) != sorted(changed):
|
||||
missing = sorted(set(changed) - set(seen))
|
||||
extra = sorted(set(seen) - set(changed))
|
||||
dupes = sorted({f for f in seen if seen.count(f) > 1})
|
||||
note = (
|
||||
"refined groups failed the exact-cover check "
|
||||
f"(missing={missing[:5]} extra={extra[:5]} duplicated={dupes[:5]}) — "
|
||||
"fell back to the deterministic grouping"
|
||||
)
|
||||
else:
|
||||
chosen = [{"domain": g["domain"], "files": g["files"]} for g in refined]
|
||||
if chosen is None:
|
||||
chosen = [{"domain": g["domain"], "files": g["files"]} for g in proposed if isinstance(g, dict)]
|
||||
|
||||
print(json.dumps({"group_items": chosen, "groups_note": note}))
|
||||
|
||||
|
||||
try:
|
||||
main()
|
||||
except Exception as e: # noqa: BLE001 — a crash must never tolerant-fail into an empty review
|
||||
try:
|
||||
files = load_state().get("changed_files") or []
|
||||
except Exception: # noqa: BLE001
|
||||
files = []
|
||||
print(
|
||||
json.dumps(
|
||||
{
|
||||
"group_items": [{"domain": "all-changes", "files": files}],
|
||||
"groups_note": f"cover gate error: {e} — fell back to a single catch-all group",
|
||||
}
|
||||
)
|
||||
)
|
||||
@@ -0,0 +1,102 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Downstream-consumers sweep for the code-reviewer graph.
|
||||
|
||||
Runs only when consumer-facing contract surface changed. Extracts the most
|
||||
specific changed contract tokens from the diff deterministically (route path
|
||||
literals, proto message/service/rpc names, exported symbols), greps sibling
|
||||
checkouts under `consumers_root`, and runs a bounded org code search via gh.
|
||||
Hits are reported for the synthesis to raise as 🔴 findings naming the
|
||||
consuming repo + file:line. Degrades silently when unconfigured/offline;
|
||||
public-library caveats are noted, not guessed at.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
|
||||
def load_state():
|
||||
if path := os.environ.get("GRAPH_STATE_FILE"):
|
||||
with open(path) as f:
|
||||
return json.load(f)
|
||||
return json.loads(os.environ.get("GRAPH_STATE", "{}"))
|
||||
|
||||
|
||||
state = load_state()
|
||||
proj = os.path.abspath(state.get("project_dir") or ".")
|
||||
spec = state.get("resolved_diff_spec") or "worktree"
|
||||
consumers_root = os.path.expanduser((state.get("consumers_root") or "").strip())
|
||||
github_org = (state.get("github_org") or "").strip()
|
||||
|
||||
if not state.get("consumer_surface"):
|
||||
print(json.dumps({"downstream_notes": "(skipped — no consumer-facing contract surface in the diff)"}))
|
||||
raise SystemExit(0)
|
||||
|
||||
|
||||
def diff_text():
|
||||
args = {"staged": ["--cached"], "worktree": ["HEAD"], "HEAD~1": ["HEAD~1"]}.get(spec, [spec])
|
||||
r = subprocess.run(["git", "-C", proj, "diff", *args], capture_output=True, text=True, timeout=90)
|
||||
return r.stdout if r.returncode == 0 else ""
|
||||
|
||||
|
||||
try:
|
||||
changed = [
|
||||
l[1:] for l in diff_text().splitlines()
|
||||
if (l.startswith("+") or l.startswith("-")) and not l.startswith(("+++", "---"))
|
||||
]
|
||||
text = "\n".join(changed)
|
||||
tokens = set()
|
||||
tokens.update(re.findall(r'["\'](/[a-z0-9][a-z0-9/_{}.:-]{5,})["\']', text)) # route paths
|
||||
tokens.update(re.findall(r"^\s*(?:message|service|rpc)\s+([A-Z][A-Za-z0-9_]{5,})", text, re.M)) # proto names
|
||||
tokens.update(re.findall(r"^\s*(?:pub\s+fn|pub\s+struct|func|export\s+(?:function|const|class))\s+([A-Z][A-Za-z0-9_]{5,})", text, re.M))
|
||||
tokens = sorted(tokens, key=len, reverse=True)[:5]
|
||||
|
||||
notes = []
|
||||
if not tokens:
|
||||
notes.append("(no specific contract tokens extractable from the diff — sweep skipped)")
|
||||
else:
|
||||
notes.append(f"contract tokens probed: {tokens}")
|
||||
if consumers_root and os.path.isdir(consumers_root):
|
||||
self_name = os.path.basename(proj)
|
||||
for tok in tokens:
|
||||
r = subprocess.run(
|
||||
["grep", "-rIl", "--exclude-dir=.git", "--exclude-dir=node_modules",
|
||||
"--exclude-dir=target", "--exclude-dir=vendor", tok, consumers_root],
|
||||
capture_output=True, text=True, timeout=60,
|
||||
)
|
||||
hits = [
|
||||
h for h in r.stdout.splitlines()
|
||||
if h.strip() and f"/{self_name}/" not in h and not h.startswith(proj)
|
||||
][:4]
|
||||
for h in hits:
|
||||
notes.append(f"CONSUMER HIT (sibling checkout): {h} references '{tok}' — "
|
||||
"a changed/removed contract element with a live consumer is a 🔴 finding")
|
||||
elif consumers_root:
|
||||
notes.append(f"(consumers_root '{consumers_root}' not found — local probe skipped)")
|
||||
else:
|
||||
notes.append("(consumers_root unset — local probe skipped)")
|
||||
|
||||
if github_org and shutil.which("gh"):
|
||||
env = dict(os.environ)
|
||||
env.pop("CLICOLOR_FORCE", None)
|
||||
env.pop("FORCE_COLOR", None)
|
||||
env["GH_NO_UPDATE_NOTIFIER"] = "1"
|
||||
for tok in tokens:
|
||||
r = subprocess.run(
|
||||
["gh", "api", "search/code", "-f", f'q=org:{github_org} "{tok}"',
|
||||
"--jq", r'.items[:4][] | "\(.repository.full_name): \(.path)"'],
|
||||
capture_output=True, text=True, timeout=30, env=env,
|
||||
)
|
||||
if r.returncode == 0 and r.stdout.strip():
|
||||
for line in r.stdout.strip().splitlines():
|
||||
notes.append(f"CONSUMER HIT (org search): {line} references '{tok}'")
|
||||
elif github_org:
|
||||
notes.append("(gh unavailable — org code search skipped)")
|
||||
else:
|
||||
notes.append("(github_org unset — org code search skipped)")
|
||||
notes.append("Public-library caveat: consumers of a PUBLIC library are unenumerable — "
|
||||
"for exported-API changes the check degrades to semver/changelog discipline.")
|
||||
print(json.dumps({"downstream_notes": "\n".join(notes)}))
|
||||
except Exception as e: # noqa: BLE001
|
||||
print(json.dumps({"downstream_notes": f"(sweep degraded: {e})"}))
|
||||
@@ -0,0 +1,281 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Deterministic review facts for the code-reviewer graph.
|
||||
|
||||
Resolves the diff, the changed-file list, the quality bar (caller-passed →
|
||||
plan frontmatter → detected default), the conventions pointer, review-miss
|
||||
ledger matches, the repo-history sweep (fix/revert clusters, bounded gh
|
||||
PR-comment pull), and the deterministic verdict-trigger signals. Pure
|
||||
git/gh/filesystem — no LLM. Failures degrade into `facts_note`; they never
|
||||
sink the review.
|
||||
"""
|
||||
|
||||
import glob
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import subprocess
|
||||
|
||||
def load_state():
|
||||
if path := os.environ.get("GRAPH_STATE_FILE"):
|
||||
with open(path) as f:
|
||||
return json.load(f)
|
||||
return json.loads(os.environ.get("GRAPH_STATE", "{}"))
|
||||
|
||||
|
||||
state = load_state()
|
||||
proj = state.get("project_dir") or "."
|
||||
spec = (state.get("diff_spec") or "auto").strip()
|
||||
notes = []
|
||||
out = {}
|
||||
|
||||
|
||||
def git(*args, timeout=60):
|
||||
r = subprocess.run(["git", "-C", proj, *args], capture_output=True, text=True, timeout=timeout)
|
||||
if r.returncode != 0:
|
||||
raise RuntimeError((r.stderr or r.stdout).strip()[:300])
|
||||
return r.stdout
|
||||
|
||||
|
||||
def scrubbed_env():
|
||||
env = dict(os.environ)
|
||||
env.pop("CLICOLOR_FORCE", None)
|
||||
env.pop("FORCE_COLOR", None)
|
||||
env["GH_NO_UPDATE_NOTIFIER"] = "1"
|
||||
env["NO_COLOR"] = "1"
|
||||
return env
|
||||
|
||||
|
||||
# --- diff resolution (get_diff semantics) -----------------------------------
|
||||
resolved = spec
|
||||
files = []
|
||||
try:
|
||||
if spec in ("auto", ""):
|
||||
names = git("diff", "--cached", "--numstat")
|
||||
resolved = "staged"
|
||||
if not names.strip():
|
||||
names = git("diff", "--numstat")
|
||||
resolved = "worktree"
|
||||
if not names.strip():
|
||||
names = git("diff", "HEAD~1", "--numstat")
|
||||
resolved = "HEAD~1"
|
||||
elif spec == "staged":
|
||||
names = git("diff", "--cached", "--numstat")
|
||||
elif spec == "worktree":
|
||||
names = git("diff", "HEAD", "--numstat")
|
||||
else:
|
||||
names = git("diff", spec, "--numstat")
|
||||
for line in names.splitlines():
|
||||
parts = line.split("\t")
|
||||
if len(parts) == 3:
|
||||
added, deleted, path = parts
|
||||
if added == "-" and deleted == "-":
|
||||
notes.append(f"binary file skipped: {path}")
|
||||
continue
|
||||
files.append(path.strip())
|
||||
except Exception as e: # noqa: BLE001
|
||||
notes.append(f"diff resolution failed for spec {spec!r}: {e}")
|
||||
|
||||
out["resolved_diff_spec"] = resolved
|
||||
out["changed_files"] = files[:300]
|
||||
out["file_count"] = len(files)
|
||||
|
||||
|
||||
def diff_text():
|
||||
try:
|
||||
if resolved == "staged":
|
||||
return git("diff", "--cached", timeout=90)
|
||||
if resolved == "worktree":
|
||||
return git("diff", "HEAD", timeout=90) if spec == "worktree" else git("diff", timeout=90)
|
||||
if resolved == "HEAD~1":
|
||||
return git("diff", "HEAD~1", timeout=90)
|
||||
return git("diff", resolved, timeout=90)
|
||||
except Exception: # noqa: BLE001
|
||||
return ""
|
||||
|
||||
|
||||
dtext = diff_text()
|
||||
added_lines = "\n".join(
|
||||
l for l in dtext.splitlines() if l.startswith("+") and not l.startswith("+++")
|
||||
)
|
||||
|
||||
# --- quality bar resolution --------------------------------------------------
|
||||
rigor_in = (state.get("rigor_in") or state.get("rigor") or "").strip()
|
||||
surfaces_in = (state.get("surfaces_in") or state.get("surfaces") or "").strip()
|
||||
resolved_rigor, resolved_surfaces, provenance = "production", "", "detected-default"
|
||||
if rigor_in or surfaces_in:
|
||||
resolved_rigor = rigor_in or "production"
|
||||
resolved_surfaces = surfaces_in
|
||||
provenance = "passed"
|
||||
else:
|
||||
for plan in sorted(glob.glob(os.path.join(proj, "plans", "PLAN-*.md"))):
|
||||
try:
|
||||
with open(plan) as f:
|
||||
head = f.read(4000)
|
||||
m = re.match(r"\A---\n(.*?)\n---", head, re.S)
|
||||
if m and re.search(r"^status:\s*active\s*$", m.group(1), re.M):
|
||||
rig = re.search(r"^rigor:\s*(\S+)", m.group(1), re.M)
|
||||
sur = re.search(r"^surfaces:\s*(.+)$", m.group(1), re.M)
|
||||
if rig:
|
||||
resolved_rigor = rig.group(1).strip()
|
||||
if sur:
|
||||
resolved_surfaces = sur.group(1).strip("[] \n")
|
||||
provenance = "plan"
|
||||
break
|
||||
except OSError:
|
||||
continue
|
||||
|
||||
SURFACE_PATTERNS = [
|
||||
("db-migration", r"migrations?/|\.sql$|schema\.(rb|prisma|sql)"),
|
||||
("iac", r"\.tf$|helm|charts?/|Dockerfile|docker-compose|k8s/|manifests?/"),
|
||||
("ci-cd", r"\.github/workflows/|\.gitlab-ci|Jenkinsfile|\.circleci/"),
|
||||
("worker", r"workers?/|jobs?/|consumers?/|cron|celery|sidekiq|river"),
|
||||
("rest-api", r"routes?|handlers?|controllers?|endpoints?|\bapi\b|\.proto$|openapi|swagger|graphql|resolvers?"),
|
||||
("cli", r"\bcmd/|\bcli\b"),
|
||||
("library", r"go\.mod$|package\.json$|Cargo\.toml$|pyproject\.toml$|setup\.py$|__init__\.py$"),
|
||||
]
|
||||
if provenance == "detected-default" and files:
|
||||
detected = []
|
||||
for surface, pat in SURFACE_PATTERNS:
|
||||
if any(re.search(pat, f, re.I) for f in files):
|
||||
detected.append(surface)
|
||||
resolved_surfaces = ", ".join(detected)
|
||||
|
||||
out["resolved_rigor"] = resolved_rigor
|
||||
out["resolved_surfaces"] = resolved_surfaces
|
||||
out["bar_provenance"] = provenance
|
||||
|
||||
# --- conventions pointer ------------------------------------------------------
|
||||
conv = []
|
||||
for pat in ("CLAUDE.md", "COYOTE.md", "CONTRIBUTING.md", ".golangci*", ".eslintrc*",
|
||||
"rustfmt.toml", ".rubocop.yml", "ruff.toml", ".editorconfig", "Makefile"):
|
||||
conv.extend(sorted(glob.glob(os.path.join(proj, pat)))[:2])
|
||||
out["conventions"] = ", ".join(os.path.relpath(p, proj) for p in conv) or "none found"
|
||||
|
||||
# --- review-miss ledger -------------------------------------------------------
|
||||
ledger_text = ""
|
||||
for cand in (
|
||||
os.path.join(os.path.expanduser("~"), ".config", "coyote", "share", "review-misses.md"),
|
||||
os.path.join(os.path.expanduser("~"), ".config", "coyote", "review-misses.md"),
|
||||
):
|
||||
if os.path.isfile(cand):
|
||||
try:
|
||||
with open(cand) as f:
|
||||
ledger_text = f.read()
|
||||
except OSError:
|
||||
pass
|
||||
break
|
||||
|
||||
matches = []
|
||||
if ledger_text:
|
||||
try:
|
||||
repo_name = os.path.basename(git("rev-parse", "--show-toplevel").strip())
|
||||
except Exception: # noqa: BLE001
|
||||
repo_name = os.path.basename(os.path.abspath(proj))
|
||||
keywords = {repo_name.lower()}
|
||||
keywords.update(s.strip().lower() for s in resolved_surfaces.split(",") if s.strip())
|
||||
for f in files:
|
||||
keywords.update(p.lower() for p in re.split(r"[/_.-]", f) if len(p) >= 4)
|
||||
entries = re.split(r"(?=^## MISS-)", ledger_text, flags=re.M)
|
||||
for entry in entries:
|
||||
if not entry.startswith("## MISS-"):
|
||||
continue
|
||||
body = entry.lower()
|
||||
if any(k in body for k in keywords):
|
||||
lines = entry.strip().splitlines()
|
||||
matches.append("\n".join(lines[:8]))
|
||||
if len(matches) >= 8:
|
||||
break
|
||||
out["ledger_matches"] = "\n\n".join(matches) if matches else "(no matching ledger entries)"
|
||||
|
||||
# --- repo-history sweep -------------------------------------------------------
|
||||
history = []
|
||||
fragile = []
|
||||
try:
|
||||
if files:
|
||||
log = git("log", "--oneline", "-15", "--", *files[:100])
|
||||
history.append(log.strip()[:2000])
|
||||
per_file = {}
|
||||
for f in files[:50]:
|
||||
try:
|
||||
fl = git("log", "--oneline", "-10", "--", f)
|
||||
except Exception: # noqa: BLE001
|
||||
continue
|
||||
hits = [l for l in fl.splitlines() if re.search(r"\b(fix|revert|hotfix)\b", l, re.I)]
|
||||
if len(hits) >= 2:
|
||||
fragile.append(f)
|
||||
per_file[f] = hits[:2]
|
||||
for f, hits in per_file.items():
|
||||
history.append(f"FRAGILE: {f} — {len(hits)}+ fix/revert commits recently — raise scrutiny")
|
||||
except Exception as e: # noqa: BLE001
|
||||
notes.append(f"history sweep failed: {e}")
|
||||
|
||||
# gh one-level-deeper: PR review comments for fragile files (bounded, silent skip)
|
||||
try:
|
||||
if fragile:
|
||||
remote = git("remote", "get-url", "origin").strip()
|
||||
m = re.search(r"github\.com[:/]([^/]+/[^/.]+)", remote)
|
||||
if m:
|
||||
repo_slug = m.group(1)
|
||||
env = scrubbed_env()
|
||||
shas = []
|
||||
for f in fragile[:2]:
|
||||
lg = git("log", "--format=%H", "-2", "--", f)
|
||||
shas.extend(lg.split())
|
||||
pr_nums = []
|
||||
for sha in shas[:3]:
|
||||
r = subprocess.run(
|
||||
["gh", "api", f"repos/{repo_slug}/commits/{sha}/pulls", "--jq", ".[].number"],
|
||||
capture_output=True, text=True, timeout=30, env=env,
|
||||
)
|
||||
if r.returncode == 0:
|
||||
pr_nums.extend(n for n in r.stdout.split() if n.isdigit())
|
||||
seen = []
|
||||
for n in pr_nums:
|
||||
if n not in seen:
|
||||
seen.append(n)
|
||||
comments = []
|
||||
for n in seen[:3]:
|
||||
r = subprocess.run(
|
||||
["gh", "api", f"repos/{repo_slug}/pulls/{n}/comments",
|
||||
"--jq", r'.[] | "\(.path): \(.body)"'],
|
||||
capture_output=True, text=True, timeout=30, env=env,
|
||||
)
|
||||
if r.returncode == 0:
|
||||
for line in r.stdout.splitlines():
|
||||
path = line.split(":", 1)[0]
|
||||
if any(path in f or f in path for f in files):
|
||||
comments.append(line[:200])
|
||||
if len(comments) >= 6:
|
||||
break
|
||||
if comments:
|
||||
history.append("Prior PR review comments on fragile paths:\n" + "\n".join(comments[:6]))
|
||||
except Exception: # noqa: BLE001
|
||||
pass # offline / not GitHub / no gh — silent by design
|
||||
|
||||
out["history_notes"] = "\n".join(h for h in history if h) or "(no notable history)"
|
||||
|
||||
# --- deterministic verdict-trigger signals -----------------------------------
|
||||
def any_path(pattern):
|
||||
return any(re.search(pattern, f, re.I) for f in files)
|
||||
|
||||
|
||||
migration_files = [f for f in files if re.search(r"migrations?/|\.sql$", f, re.I)]
|
||||
mig_added = "\n".join(
|
||||
l for l in dtext.splitlines()
|
||||
if l.startswith("+") and not l.startswith("+++")
|
||||
) if migration_files else ""
|
||||
out["destructive_migration"] = bool(
|
||||
migration_files
|
||||
and re.search(r"(?i)drop\s+(table|column)|rename\s+(table|column|to)|alter\s+\S+\s+drop", mig_added)
|
||||
)
|
||||
out["touches_auth"] = bool(
|
||||
any_path(r"auth(?!or)|login|logout|token|session|permission|rbac|\bacl\b|credential")
|
||||
or re.search(r"auth(?!or)|password|jwt|session|credential", added_lines, re.I)
|
||||
)
|
||||
out["consumer_surface"] = any_path(
|
||||
r"routes?|handlers?|\bapi\b|endpoints?|\bcmd/|\bcli\b|\.proto$|openapi|swagger|controllers?|graphql"
|
||||
)
|
||||
|
||||
if notes:
|
||||
out["facts_note"] = "; ".join(notes)[:800]
|
||||
print(json.dumps(out))
|
||||
@@ -0,0 +1,66 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Deterministic domain grouping of changed files for the code-reviewer graph.
|
||||
|
||||
Each file lands in the FIRST matching domain (patterns ordered most-specific
|
||||
first); unmatched files land in `general`. When the grouping is very wide,
|
||||
single-file groups merge into `general` so the fan-out stays sane. The LLM
|
||||
refinement node may rebalance this proposal; the cover gate guarantees no
|
||||
file is lost either way. Crash-safe: an error emits one catch-all proposal
|
||||
rather than tolerant-failing into an empty grouping.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
|
||||
def load_state():
|
||||
if path := os.environ.get("GRAPH_STATE_FILE"):
|
||||
with open(path) as f:
|
||||
return json.load(f)
|
||||
return json.loads(os.environ.get("GRAPH_STATE", "{}"))
|
||||
|
||||
|
||||
DOMAINS = [
|
||||
("db", r"migrations?/|\.sql$|schema\.(rb|prisma|sql)|models?/"),
|
||||
("iac", r"\.tf$|helm|charts?/|(^|/)Dockerfile|docker-compose|k8s/|manifests?/"),
|
||||
("ci-cd", r"\.github/workflows/|\.gitlab-ci|Jenkinsfile|\.circleci/"),
|
||||
("workers", r"workers?/|jobs?/|consumers?/|cron|tasks?/"),
|
||||
("api", r"routes?|handlers?|controllers?|endpoints?|\bapi\b|\.proto$|openapi|swagger|graphql|resolvers?"),
|
||||
("cli", r"\bcmd/|\bcli\b"),
|
||||
("deps", r"go\.(mod|sum)$|package(-lock)?\.json$|yarn\.lock$|Cargo\.(toml|lock)$|requirements[^/]*\.txt$|pyproject\.toml$|Gemfile|pom\.xml$|composer\.json$"),
|
||||
("tests", r"(^|/)tests?/|_test\.|\.test\.|spec\."),
|
||||
("docs", r"\.(md|rst|adoc|txt)$"),
|
||||
]
|
||||
|
||||
|
||||
def main():
|
||||
state = load_state()
|
||||
files = state.get("changed_files") or []
|
||||
|
||||
groups = {}
|
||||
for f in files:
|
||||
domain = "general"
|
||||
for name, pat in DOMAINS:
|
||||
if re.search(pat, f, re.I):
|
||||
domain = name
|
||||
break
|
||||
groups.setdefault(domain, []).append(f)
|
||||
|
||||
# Wide fan-out: merge single-file groups into general.
|
||||
if len(groups) > 6:
|
||||
for name in list(groups):
|
||||
if name != "general" and len(groups[name]) == 1:
|
||||
groups.setdefault("general", []).extend(groups.pop(name))
|
||||
|
||||
proposed = [{"domain": k, "files": v} for k, v in sorted(groups.items())]
|
||||
print(json.dumps({"proposed_groups": proposed}))
|
||||
|
||||
|
||||
try:
|
||||
main()
|
||||
except Exception as e: # noqa: BLE001 — never tolerant-fail into an empty grouping
|
||||
try:
|
||||
files = load_state().get("changed_files") or []
|
||||
except Exception: # noqa: BLE001
|
||||
files = []
|
||||
print(json.dumps({"proposed_groups": [{"domain": "all-changes", "files": files}]}))
|
||||
@@ -0,0 +1,87 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Domain linter pass for the code-reviewer graph.
|
||||
|
||||
Runs whichever mechanized checkers exist on PATH against the matching changed
|
||||
files: tflint (Terraform), hadolint (Dockerfiles), actionlint (workflow
|
||||
files), and `buf breaking` when buf-configured protos changed. Read-only,
|
||||
never installs anything, and each tool degrades silently to a skip note.
|
||||
Output is routed per-file where possible so the domain leaders can extract
|
||||
their slice's lines.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import subprocess
|
||||
|
||||
def load_state():
|
||||
if path := os.environ.get("GRAPH_STATE_FILE"):
|
||||
with open(path) as f:
|
||||
return json.load(f)
|
||||
return json.loads(os.environ.get("GRAPH_STATE", "{}"))
|
||||
|
||||
|
||||
state = load_state()
|
||||
proj = state.get("project_dir") or "."
|
||||
files = state.get("changed_files") or []
|
||||
spec = state.get("resolved_diff_spec") or "worktree"
|
||||
notes = []
|
||||
|
||||
|
||||
def run(cmd, timeout=60):
|
||||
r = subprocess.run(cmd, capture_output=True, text=True, timeout=timeout, cwd=proj)
|
||||
return (r.stdout + r.stderr).strip()
|
||||
|
||||
|
||||
def capped(text, n=40):
|
||||
lines = text.splitlines()
|
||||
return "\n".join(lines[:n]) + (f"\n… ({len(lines) - n} more lines)" if len(lines) > n else "")
|
||||
|
||||
|
||||
try:
|
||||
tf = [f for f in files if f.endswith(".tf")]
|
||||
if tf and shutil.which("tflint"):
|
||||
notes.append("[tflint]\n" + capped(run(["tflint", "--no-color"], 120) or "clean"))
|
||||
elif tf:
|
||||
notes.append("[tflint] skipped — not on PATH")
|
||||
|
||||
dockers = [f for f in files if re.search(r"(^|/)Dockerfile", f)]
|
||||
if dockers and shutil.which("hadolint"):
|
||||
notes.append("[hadolint]\n" + capped(run(["hadolint", "--no-color", *dockers[:10]], 60) or "clean"))
|
||||
elif dockers:
|
||||
notes.append("[hadolint] skipped — not on PATH")
|
||||
|
||||
workflows = [f for f in files if ".github/workflows/" in f]
|
||||
if workflows and shutil.which("actionlint"):
|
||||
notes.append("[actionlint]\n" + capped(run(["actionlint", "-no-color", *workflows[:10]], 60) or "clean"))
|
||||
elif workflows:
|
||||
notes.append("[actionlint] skipped — not on PATH")
|
||||
|
||||
protos = [f for f in files if f.endswith(".proto")]
|
||||
if protos and os.path.isfile(os.path.join(proj, "buf.yaml")) and shutil.which("buf"):
|
||||
base = None
|
||||
if "..." in spec:
|
||||
base = spec.split("...")[0]
|
||||
elif ".." in spec:
|
||||
base = spec.split("..")[0]
|
||||
elif spec not in ("staged", "worktree", "HEAD~1"):
|
||||
base = spec
|
||||
else:
|
||||
try:
|
||||
r = subprocess.run(["git", "-C", proj, "rev-parse", "HEAD"],
|
||||
capture_output=True, text=True, timeout=15)
|
||||
base = r.stdout.strip() if r.returncode == 0 else None
|
||||
except Exception: # noqa: BLE001
|
||||
base = None
|
||||
if base:
|
||||
out = run(["buf", "breaking", "--against", f".git#ref={base}"], 120)
|
||||
notes.append("[buf breaking] (wire-compat breaks are 🔴 by default)\n" + capped(out or "clean"))
|
||||
else:
|
||||
notes.append("[buf breaking] skipped — no base ref resolvable")
|
||||
elif protos:
|
||||
notes.append("[buf breaking] skipped — no buf.yaml or buf not on PATH")
|
||||
except Exception as e: # noqa: BLE001
|
||||
notes.append(f"[linters] pass degraded: {e}")
|
||||
|
||||
print(json.dumps({"linter_notes": "\n\n".join(notes) if notes else "(no applicable linters)"}))
|
||||
@@ -0,0 +1,109 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Deterministic report assembly for the code-reviewer graph.
|
||||
|
||||
Builds the standard report format from the structured findings and prose
|
||||
sections. Pure templating — no judgment, no re-summarization, no finding
|
||||
ever added or removed here.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
|
||||
def load_state():
|
||||
if path := os.environ.get("GRAPH_STATE_FILE"):
|
||||
with open(path) as f:
|
||||
return json.load(f)
|
||||
return json.loads(os.environ.get("GRAPH_STATE", "{}"))
|
||||
|
||||
|
||||
def main():
|
||||
state = load_state()
|
||||
v = state.get("verdict_out") or {}
|
||||
findings = v.get("findings_final") or []
|
||||
counts = v.get("counts") or {}
|
||||
changed = state.get("changed_files") or []
|
||||
rigor = state.get("resolved_rigor") or "production"
|
||||
|
||||
if not changed:
|
||||
print(json.dumps({"final_report": "# Code Review Summary\n\nNo changes to review."}))
|
||||
return
|
||||
|
||||
verdict = v.get("verdict", "NEEDS-HUMAN")
|
||||
lines = [
|
||||
"# Code Review Summary",
|
||||
"",
|
||||
f"**Verdict: {verdict}** — {v.get('reason', '')}",
|
||||
"",
|
||||
"## Walkthrough",
|
||||
state.get("walkthrough") or "(no walkthrough provided)",
|
||||
"",
|
||||
"## Changes",
|
||||
"",
|
||||
"| File | Changes | Findings |",
|
||||
"|------|---------|----------|",
|
||||
]
|
||||
|
||||
desc_by_file = {}
|
||||
for row in state.get("changes_rows") or []:
|
||||
if isinstance(row, dict) and row.get("file"):
|
||||
desc_by_file[row["file"]] = row.get("desc", "")
|
||||
blocking = [f for f in findings if f.get("section") == "blocking"]
|
||||
deferred = [f for f in findings if f.get("section") == "deferred"]
|
||||
for f in changed:
|
||||
per = [x for x in blocking if x.get("file") == f]
|
||||
tally = " ".join(
|
||||
f"{icon} {n}" for icon, n in
|
||||
((i, sum(1 for x in per if x.get("icon") == i)) for i in ("🔴", "🟡", "🟢", "💡"))
|
||||
if n
|
||||
) or "—"
|
||||
lines.append(f"| `{f}` | {desc_by_file.get(f, '')} | {tally} |")
|
||||
|
||||
lines += ["", "## Detailed Findings", ""]
|
||||
by_file = {}
|
||||
for x in blocking:
|
||||
by_file.setdefault(x.get("file", "(unattributed)"), []).append(x)
|
||||
if by_file:
|
||||
for f in sorted(by_file):
|
||||
lines += [f"### `{f}`", ""]
|
||||
for x in by_file[f]:
|
||||
lines += [x.get("block", ""), ""]
|
||||
else:
|
||||
lines += ["No blocking findings.", ""]
|
||||
|
||||
lines += ["## Cross-File Concerns", state.get("cross_file") or "None", ""]
|
||||
if (state.get("operational_history") or "").strip():
|
||||
lines += ["## Operational history", state["operational_history"], ""]
|
||||
if (state.get("org_context_section") or "").strip():
|
||||
lines += ["## Org context", state["org_context_section"], ""]
|
||||
if deferred and rigor != "production":
|
||||
lines += ["## Deferred by quality bar", ""]
|
||||
for x in deferred:
|
||||
lines += [x.get("block", ""), ""]
|
||||
attention = v.get("attention") or []
|
||||
if verdict == "NEEDS-HUMAN" and attention:
|
||||
lines += ["## Human attention required", ""]
|
||||
lines += [f"- {a}" for a in attention]
|
||||
lines += [""]
|
||||
|
||||
miss_ids = sorted({
|
||||
m for x in findings for m in
|
||||
__import__("re").findall(r"MISS-\d+", (x.get("title") or "") + (x.get("block") or ""))
|
||||
})
|
||||
lines += [
|
||||
"---",
|
||||
f"*Reviewed {len(changed)} files, found {counts.get('🔴', 0)} critical, "
|
||||
f"{counts.get('🟡', 0)} warnings, {counts.get('🟢', 0)} suggestions, "
|
||||
f"{counts.get('💡', 0)} nitpicks ({v.get('deferred_count', 0)} deferred by quality bar)*",
|
||||
f"*Quality bar: {rigor} — provenance: {state.get('bar_provenance', '?')}; "
|
||||
f"surfaces: {state.get('resolved_surfaces') or 'none'}*",
|
||||
f"*Verdict: {verdict}; verifier: dropped {v.get('dropped_count', 0)} provably-false"
|
||||
+ (f" ({', '.join(v.get('dropped_titles', [])[:5])})" if v.get("dropped_count") else "")
|
||||
+ f"; review-miss patterns applied: {', '.join(miss_ids) if miss_ids else 'none relevant'}*",
|
||||
]
|
||||
print(json.dumps({"final_report": "\n".join(lines)}))
|
||||
|
||||
|
||||
try:
|
||||
main()
|
||||
except Exception as e: # noqa: BLE001
|
||||
print(json.dumps({"final_report": f"# Code Review Summary\n\n**Verdict: NEEDS-HUMAN** — report rendering error: {e}"}))
|
||||
@@ -0,0 +1,168 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Deterministic verdict math for the code-reviewer graph.
|
||||
|
||||
The model that wrote the findings never grades its own homework:
|
||||
- verifier verdicts are applied mechanically (FALSE → drop, tallied;
|
||||
VERIFIED → evidence attached; UNVERIFIABLE → kept, marked);
|
||||
- rigor folding is arithmetic over severity + [convention]/[correctness]
|
||||
markers, exactly per the published rules;
|
||||
- MERGE-READY/NEEDS-HUMAN is computed from counts + always-human triggers
|
||||
(deterministic signals ∪ the synthesis's ADDITIVE-only flags).
|
||||
The gate never crashes into a silent verdict: any internal error emits
|
||||
NEEDS-HUMAN naming the error.
|
||||
"""
|
||||
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
|
||||
def load_state():
|
||||
if path := os.environ.get("GRAPH_STATE_FILE"):
|
||||
with open(path) as f:
|
||||
return json.load(f)
|
||||
return json.loads(os.environ.get("GRAPH_STATE", "{}"))
|
||||
|
||||
|
||||
def sev_icon(severity):
|
||||
for icon in ("🔴", "🟡", "🟢", "💡"):
|
||||
if icon in (severity or ""):
|
||||
return icon
|
||||
return "🟢"
|
||||
|
||||
|
||||
def parse_verifier(text):
|
||||
"""Extract {id: {verdict, evidence, note}} from finding-verifier output."""
|
||||
verdicts = {}
|
||||
if not isinstance(text, str) or not text.strip():
|
||||
return verdicts
|
||||
start, end = text.find("["), text.rfind("]")
|
||||
if start != -1 and end > start:
|
||||
try:
|
||||
for v in json.loads(text[start : end + 1]):
|
||||
if isinstance(v, dict) and v.get("id"):
|
||||
verdicts[str(v["id"])] = v
|
||||
return verdicts
|
||||
except (ValueError, TypeError):
|
||||
pass
|
||||
for m in re.finditer(r'\{[^{}]*"id"[^{}]*\}', text):
|
||||
try:
|
||||
v = json.loads(m.group(0))
|
||||
if v.get("id"):
|
||||
verdicts[str(v["id"])] = v
|
||||
except (ValueError, TypeError):
|
||||
continue
|
||||
return verdicts
|
||||
|
||||
|
||||
def main():
|
||||
state = load_state()
|
||||
findings = [f for f in (state.get("findings") or []) if isinstance(f, dict)]
|
||||
rigor = (state.get("resolved_rigor") or "production").lower()
|
||||
verdicts = parse_verifier(state.get("verifier_output"))
|
||||
|
||||
kept, dropped_titles = [], []
|
||||
for f in findings:
|
||||
icon = sev_icon(f.get("severity"))
|
||||
v = verdicts.get(str(f.get("id")))
|
||||
block = f.get("block") or f"#### {f.get('title', '(untitled)')}"
|
||||
if v and icon in ("🔴", "🟡"):
|
||||
if v.get("verdict") == "FALSE":
|
||||
dropped_titles.append(f.get("title", f.get("id", "?")))
|
||||
continue
|
||||
if v.get("verdict") == "VERIFIED" and v.get("evidence"):
|
||||
block += f"\n- **Evidence** (verifier): {v['evidence']}"
|
||||
elif v.get("verdict") == "UNVERIFIABLE":
|
||||
block += "\n- *(unverified: " + (v.get("note") or "could not be established") + ")*"
|
||||
elif icon in ("🔴", "🟡") and not v:
|
||||
block += "\n- *(unverified: no verifier verdict returned for this finding)*"
|
||||
kept.append({**f, "icon": icon, "block": block})
|
||||
|
||||
# --- rigor folding (arithmetic, never rewrites severities) --------------
|
||||
for f in kept:
|
||||
marker = (f.get("marker") or "").lower()
|
||||
icon = f["icon"]
|
||||
section = "blocking"
|
||||
if icon == "🔴":
|
||||
section = "blocking"
|
||||
elif rigor == "prototype" and ((icon == "🟡" and marker == "convention") or icon == "🟢"):
|
||||
section = "deferred"
|
||||
elif rigor == "poc":
|
||||
if icon == "🟡" and marker == "convention":
|
||||
section = "deferred"
|
||||
elif icon in ("🟢", "💡") and marker == "convention":
|
||||
section = "dropped-by-poc"
|
||||
f["section"] = section
|
||||
final = [f for f in kept if f["section"] != "dropped-by-poc"]
|
||||
blocking = [f for f in final if f["section"] == "blocking"]
|
||||
deferred = [f for f in final if f["section"] == "deferred"]
|
||||
|
||||
counts = {i: sum(1 for f in blocking if f["icon"] == i) for i in ("🔴", "🟡", "🟢", "💡")}
|
||||
yellow_correctness = sum(
|
||||
1 for f in blocking if f["icon"] == "🟡" and (f.get("marker") or "").lower() == "correctness"
|
||||
)
|
||||
|
||||
# --- always-human triggers ----------------------------------------------
|
||||
attention = []
|
||||
if state.get("destructive_migration"):
|
||||
attention.append("Destructive/irreversible migration in the diff (DROP/RENAME) — needs a human signature.")
|
||||
if state.get("touches_auth"):
|
||||
attention.append("Authentication/authorization surface changed — needs a human signature.")
|
||||
for flag in state.get("attention_flags") or []:
|
||||
if isinstance(flag, str) and flag.strip():
|
||||
attention.append(flag.strip())
|
||||
placement = (state.get("placement_notes") or "").strip()
|
||||
if placement:
|
||||
attention.append("Placement question (design intent, never auto-resolved): " + placement)
|
||||
# dedup, preserve order
|
||||
seen = set()
|
||||
attention = [a for a in attention if not (a.lower() in seen or seen.add(a.lower()))]
|
||||
|
||||
if (state.get("changed_files") or []) and not (state.get("domain_reports") or []):
|
||||
attention.append(
|
||||
"PIPELINE FAULT: the diff is non-empty but no domain reports were produced — "
|
||||
"the review did not actually run; do not trust this verdict."
|
||||
)
|
||||
reasons = []
|
||||
if counts["🔴"]:
|
||||
reasons.append(f"{counts['🔴']} 🔴 CRITICAL finding(s)")
|
||||
if yellow_correctness:
|
||||
reasons.append(f"{yellow_correctness} 🟡 [correctness] finding(s) outside the deferred section")
|
||||
if attention:
|
||||
reasons.append("always-human trigger(s) fired")
|
||||
verdict = "NEEDS-HUMAN" if reasons else "MERGE-READY"
|
||||
reason_line = "; ".join(reasons) if reasons else "no blocking findings and no always-human triggers"
|
||||
|
||||
print(
|
||||
json.dumps(
|
||||
{
|
||||
"verdict_out": {
|
||||
"verdict": verdict,
|
||||
"reason": reason_line,
|
||||
"counts": counts,
|
||||
"deferred_count": len(deferred),
|
||||
"dropped_count": len(dropped_titles),
|
||||
"dropped_titles": dropped_titles,
|
||||
"attention": attention[:5],
|
||||
"findings_final": final,
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
try:
|
||||
main()
|
||||
except Exception as e: # noqa: BLE001 — never crash into a silent verdict
|
||||
print(
|
||||
json.dumps(
|
||||
{
|
||||
"verdict_out": {
|
||||
"verdict": "NEEDS-HUMAN",
|
||||
"reason": f"verdict computation error: {e} — treat as needing human review",
|
||||
"counts": {}, "deferred_count": 0, "dropped_count": 0,
|
||||
"dropped_titles": [], "attention": [f"verdict script error: {e}"],
|
||||
"findings_final": [],
|
||||
}
|
||||
}
|
||||
)
|
||||
)
|
||||
Reference in new issue
Block a user