feat: Significant code review suite rewrite with improved graph agent features

This commit is contained in:
Dark-Alex-17 committed 2026-09-11 15:33:21 -06:00
1 parent 838335f84d
commit 99e592565a
41 files changed
+3572 -709

No files matched your search

+30
View File
@@ -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.
-331
View File
@@ -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__}}
+493
View File
@@ -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": [],
}
}
)
)