skills/council-review/SKILL.md
--- name: council-review description: Run a "Council of LLMs" code review — dispatch the whole review-agent fleet (general, architecture, simplicity, SOLID/DRY, security, way-of-working, accessibility) in parallel against a diff, branch, or PR, each through its own lens, then merge their findings into one severity-tagged report. Inspired by Shopify's multi-model review ensemble (Farhan Thawar). Use when the user wants a deep multi-lens review, a "council review", a "full review", a pre-merge gat
npx skillsauth add RonanCodes/ronan-skills skills/council-reviewInstall this skill globally with one command. Works with Claude Code, Cursor, and Windsurf.
3 of 9 scanners reported clean
Some scanners were skipped, did not run, or reported a non-clean status. Review each row below.
Review a change the way Shopify reviews code now: not with one reviewer, but with a panel of agents, each carrying one lens, run in parallel, then merged into a single verdict. The pattern (a "Council of LLMs") comes from Farhan Thawar's Compile 26 talk. The point of the council is that a security lens, an architecture lens, and a simplicity lens each see a different slice of the same diff and catch failure modes a single general reviewer misses.
The differentiator is the lenses, not the model count. Each council member is a focused subagent in ~/Dev/ronan-skills/agents/ that stays in its lane.
Spec and rationale: [[council-of-llms]] in llm-wiki-ai-research/wiki/concepts/. Severity taxonomy and report templates: read ../code-review/reference.md.
| Agent | Lens | Owns |
|-------|------|------|
| code-reviewer | general / correctness | logic bugs, the overall gate (the "main reviewer / general actor") |
| architecture-reviewer | structure | boundaries, coupling, dependency direction, deep modules |
| simplicity-reviewer | YAGNI / KISS / rule-of-3 | over-engineering, premature abstraction, dead code |
| solid-dry-reviewer | SOLID + DRY | SRP/OCP/LSP/ISP/DIP, real-vs-coincidental duplication |
| security-reviewer | security | authz/secrets/injection, the auth-guards rule |
| way-of-working-reviewer | process | tests-that-cover, commit hygiene, scope discipline, definition-of-done |
| accessibility-reviewer | a11y / mobile | semantics, keyboard, contrast, 390px touch |
| prevention-reviewer | systemic / recurrence | runs LAST, not in parallel: reads the merged findings and proposes the cheapest durable guardrail per root cause (lint rule / hook / CI / test > CLAUDE.md / ADR / doc > skill) so the class cannot recur |
The roster is extensible: add an agent file with a new lens and a row in the config lenses list. See agents/AGENTS.md for the fleet doc.
A repo declares how strict its council is with a committed level key in .ro-code-review.json (the same resolver the gate uses, see /ro:code-review-gate). --level on the CLI overrides it for one run. A level controls three things at once: which lenses run, the block-on threshold, and which finding classes are reported.
| Level | Lenses | Block on | Classes reported |
|-------|--------|----------|------------------|
| critical | code-reviewer (correctness) + security-reviewer | critical | showstoppers only: real security vulnerabilities, data loss, correctness bugs |
| standard | adds architecture, solid-dry, performance, api-contract, observability, way-of-working | critical | showstoppers + structural ("how we do things"); style and vibes classes suppressed |
| strict | everything, including simplicity, accessibility, docs | per blockOn (default critical) | all classes, including nitpicks (typography like em-dashes, comment style, naming vibes) |
strict is the DEFAULT when level is unset. An unconfigured repo gets the full council; relaxing is an explicit, committed decision the team can see in the config file. An explicit lenses list (config or --lenses) still wins over the level's lens set, and --block-on still wins over the level's threshold.
The critical use case that motivated levels: irregular develop-into-main merges of code that was already reviewed PR-by-PR. Re-litigating naming and typography on a merge diff like that is noise; you only want showstoppers.
Every finding carries a class slug alongside its lens tag (e.g. em-dash, comment-style, naming-vibes, dead-code, missing-test, n-plus-one, secret-in-code, injection, contract-drift). Lenses are who looked; classes are what they found. Levels, ignore, scopes, and the recurrence memory all operate on classes. Three tiers map classes to levels:
standard and up.strict.There is no closed class registry; lenses mint slugs as needed. But a lens MUST reuse the slug the repo's config or .code-review/recurrence.json already uses for the same class of issue; stable slugs are what make ignore lists, scopes, and recurrence tallies work.
ignore: classes this repo never reports"ignore": ["comment-style", "naming-vibes"]
Findings of an ignored class are dropped at merge time, at every level. This is for classes (especially nitpick classes) the team has decided it genuinely does not care about in this repo. Suppression must be visible, not silent: the merged report ends with one line, N findings suppressed by repo config (classes: comment-style, naming-vibes). A team member reading the report can always see what the config hid and reopen the decision.
scopes: a class limited to certain surfaces"scopes": { "em-dash": "user-facing" }
A scoped class is not fully on or off; it is enforced only on the named surface and ignored elsewhere. user-facing is the canonical scope: enforced on release notes, website and landing copy, docs content, UI microcopy; ignored in code comments, commit messages, PR bodies, internal notes. Out-of-scope findings count toward the suppression line like ignored ones.
The concrete case this was built for: dataforce bans em-dashes in user-facing surfaces only. A build-failing lint rule across the whole codebase was too nitpicky for the team, so the class was re-scoped rather than dropped: customers never see an em-dash, and nobody gets blocked over one in a commit body.
checklist: repo-specific checks every run verifies"checklist": [
"Every new MCP tool ships with an HTTP shim, a docs-registry section, and a Bruno entry in the same PR"
]
Free-text checks the council verifies on every run, on top of the lenses' own judgment. At dispatch, each lens receives the checklist lines relevant to its lane (the MCP three-artefact rule above goes to the way-of-working and api-contract lenses; a line matching no lane goes to code-reviewer). A missed checklist item is reported as class checklist-miss at major severity, and is never suppressed by level (a repo that wrote the check down wants it verified at every level).
/ro:council-review # review the working tree / current branch vs main
/ro:council-review 142 # review PR #142
/ro:council-review feat/x # review branch feat/x vs main
/ro:council-review main...HEAD # review an explicit range
/ro:council-review --lenses security,architecture # only these lenses
/ro:council-review --level critical # showstoppers only (e.g. a develop-into-main merge already reviewed PR-by-PR)
/ro:council-review --level standard # correctness + structure; style and vibes classes suppressed
/ro:council-review --fix # after review, dispatch implementer agents to fix findings, then re-review
/ro:council-review --fix --report .code-review/council-20260629-110101.md # skip review, implement an existing report
/ro:council-review --fix --ship # fix loop, then merge + deploy via /ro:ship once the verdict clears
/ro:council-review --block-on critical+major # stricter gate verdict
/ro:council-review 142 --post # publish the merged report to PR #142 (GitHub or Bitbucket)
/ro:council-review --no-pdf # skip the auto-rendered PDF (default: render + open it)
Picking the report up in a fresh session. Every report ends with a copy-paste command that starts a new Claude session, reads the report, and dispatches the implementer agents (the review is read-only, so fixing is always a separate, explicit step). The command is just:
claude "/ro:council-review --fix --report <report-path>"
This is the handoff seam: review now, implement later (or elsewhere), with the report as the contract.
When invoked via "implement the code review feedback" / "implement feedback" / "fix the council findings" with no --report, auto-detect the most recent .code-review/council-*.md (ls -t), confirm it with the user (show its date + verdict), and run the --fix flow on it. If there is no report, say so and offer to run a fresh review first. This is the in-repo equivalent of the printed pickup command.
Pick one (mirror the code-reviewer agent):
gh pr view <N> + gh pr diff <N>, base = the PR base.git diff main...<branch>.... or ..) → use as-is.git diff + git diff --cached), and if empty, git diff main...HEAD.Compute a short scope label and the changed-files list (git diff --name-only <range>). If the changed set is empty, stop and say so.
Read config via the resolver in /ro:code-review-gate (later wins): built-in default (level strict, all lenses) → ~/.claude/code-review-policy.json → .ro-code-review.json (committed) → .ro-code-review.local.json (gitignored). CLI flags win over all files: --level sets the level, --lenses names lenses explicitly.
The resolved level picks the lens set (see the Levels table); an explicit lenses list overrides it. Skip accessibility-reviewer automatically when the changed set has no frontend/UI files, unless explicitly named. Resolve ignore, scopes, and checklist here too: checklist feeds dispatch (step 3), the class filters apply at merge (step 4). Levels save cost by not running lenses at all; ignore and scopes filter at merge so the suppression stays countable and visible.
Farhan Thawar's ensemble mixed model families (GPT, Claude, Gemini) for cost and latency. This council is Claude-only, so the equivalent is mixing model tiers. Each agent carries a model: default in its own frontmatter, so the right tier is used even when an agent is called directly:
| Tier | Agents | Why | |------|--------|-----| | Opus | code-reviewer, architecture-reviewer, security-reviewer, refactorer | deep reasoning, high cost of a miss | | Sonnet | simplicity, solid-dry, way-of-working, performance, api-contract, observability, dev, test-writer | judgment, but more pattern-based | | Haiku | accessibility, docs | mechanical checklist work, cheapest, fastest |
A repo can override any of these via the models map in its .ro-code-review.json (see /ro:code-review-gate). When a config override is present, pass it as opts.model; otherwise let the agent's frontmatter default stand. This is the "human time is worth more than clanker time" rule turned into config: spend the expensive model only where a miss is expensive.
Spawn ALL enabled lens agents in ONE message (multiple Agent tool calls in a single block so they run concurrently), each with:
subagent_type = the agent name (e.g. architecture-reviewer).checklist lines relevant to this lens's lane, when the repo config has any: the lens verifies each line against the diff and reports misses as class checklist-miss (major).opts.model ONLY to override the agent's frontmatter default (i.e. when the repo's config models map names a different tier for this lens); otherwise omit it and let the agent's own model: default apply.Each agent reads the surrounding code itself; do not paste diffs into the prompt beyond the scope pointer.
Collect every agent's output. Then:
file:line flagged by two lenses → keep both lens tags on one entry, highest severity wins.[lens] prefix and its class slug so the reader sees which council member raised it and what class it belongs to.standard, everything but showstoppers at critical), whose class is in ignore, or that fall outside the class's scopes surface (e.g. an em-dash in a code comment when the class is scoped user-facing). checklist-miss is never dropped. Count everything dropped.N findings suppressed by repo config (classes: em-dash, comment-style). Never suppress silently.REQUEST-CHANGES if any finding at or above the block-on threshold (default critical; --level sets it too), else APPROVE-WITH-NITS if any minor/major, else APPROVE. Suppressed findings never block.The lenses find what is wrong in this diff. They do not ask why it was possible or stop it recurring, that is a different shape (it reasons over the whole finding set, not the diff). After merging findings, if there is at least one finding above praise, dispatch the prevention-reviewer agent once, handing it the merged findings (or the report path) AND .code-review/recurrence.json (the cross-run tally, see Recurrence memory below). It returns, per root-cause cluster, the cheapest durable guardrail at the strongest feasible rung of its enforcement ladder (build-failing lint/type/hook/CI/test > advisory > CLAUDE.md/ADR/doc > process/skill), each marked apply: auto or human-judgement.
The recurrence tally gives it a second signal beyond this run's findings, with two escalation paths per class:
seen high, fixed tracking it): the team agrees these are real, so the class has earned a mechanical guardrail. Propose the highest feasible rung of the enforcement ladder, exactly the existing behaviour, now backed by cross-run evidence instead of one report.seen high, rejected dominating): the finding is the problem, not the code. Propose adding the class to the repo's ignore list or giving it a scopes entry instead of a guardrail, routed through the feedback loop below so the human decides.The load-bearing rule it enforces: a finding that violated a rule ALREADY written in CLAUDE.md is proof the documented rule is insufficient, escalate it to mechanical enforcement, do not just re-document it. (Canonical example: em-dashes are banned in CLAUDE.md yet keep reaching review, the prevention is an eslint rule that fails the build, not a louder doc.)
Skip this pass on a clean APPROVE (nothing to harden) and in --pre-push mode (keep it terse). Append its output to the report as a ## Prevention section.
Write .code-review/council-<YYYYMMDD-HHMMSS>.md (create .code-review/ if missing; it is gitignored by convention). Structure: header (scope, verdict, lenses run, files reviewed), a one-line-per-finding list grouped as above, then a per-lens appendix (each agent's raw verdict), then the ## Prevention section from step 4b (the guardrails that stop these findings recurring).
End with a Pickup block (this is the handoff seam, every report is resumable):
## Pickup
Implement these fixes in a fresh session:
claude --dangerously-skip-permissions "/ro:council-review --fix --report <this-file-path>"
Or in this repo just say: "implement the code review feedback".
The pickup command includes --dangerously-skip-permissions so a pasted-into-terminal resume runs unattended without permission prompts (the fix flow spawns implementer agents that edit files and run the gate). The in-repo natural-language shortcut stays prompt-gated as normal.
If the verdict is clean, the Pickup block instead says "ready to push/merge, nothing to implement".
Also write the verdict to .code-review/verdict as a single bare token (APPROVE, APPROVE-WITH-NITS, or REQUEST-CHANGES). This is the machine-readable signal the pre-push gate reads (a claude -p exit code cannot carry the verdict, so the file is the contract). Overwrite it each run.
And update .code-review/recurrence.json (see Recurrence memory): seen +1 for every class that produced at least one reported finding this run.
The council keeps .code-review/recurrence.json (the dir is already gitignored): a per-class tally across runs in this repo. Create it on first write.
{
"em-dash": { "seen": 4, "fixed": 3, "rejected": 1, "lastSeen": "2026-07-01" },
"missing-test": { "seen": 2, "fixed": 2, "rejected": 0, "lastSeen": "2026-06-28" }
}
Three counters per class, three moments to bump them:
seen +1 per run in which the class produced at least one reported finding (step 5).fixed +1 when a --fix round implements findings of that class (step 6).rejected +1 when the user dismisses the class's findings or the feedback loop ignores, scopes, or level-downgrades it.The tally exists to feed the prevention pass (step 4b): fix-recurrence escalates to a mechanical guardrail, reject-recurrence de-escalates to ignore or a scope. The worked example is the em-dash story in dataforce: the class recurred 4 times, the enforcement ladder escalated it to a build-failing lint rule, and the team then found full-codebase enforcement too nitpicky, so it was re-scoped to user-facing surfaces only. Both directions of the loop are correct: recurrence that keeps getting fixed hardens the repo, recurrence that keeps getting rejected relaxes the config.
When the user pushes back on a review's nitpickiness ("too nitpicky", "we don't care about em-dashes here", "stop flagging comment style"), do NOT just apologise and move on. The complaint is a config signal. The council MUST offer (AskUserQuestion) to update the repo's committed config, with the concrete options:
strict to standard) when the complaint covers a whole tier of findings.ignore when this repo genuinely never cares about it, anywhere."em-dash": "user-facing") when it matters on some surfaces but not others.On any option except leave-as-is: write the change to .ro-code-review.json and commit it on the current branch (🔧 config: ...), so the decision is durable and team-visible, not a one-session mood. Record the pushback in .code-review/recurrence.json as a rejected +1 for the class either way.
Right after the report is written, render it to a styled, self-contained PDF and open it on the Mac, so a glanceable artefact is waiting when the user comes back to the computer:
"${CLAUDE_PLUGIN_ROOT:-$HOME/Dev/ronan-skills}/skills/council-review/scripts/report-to-pdf.sh" \
.code-review/council-<YYYYMMDD-HHMMSS>.md
The script (pandoc → styled HTML → Chrome headless --print-to-pdf) writes a sibling .pdf next to the report and opens it. It is self-contained (no LaTeX). Print the PDF path to the user. Skip with --no-pdf, and in --pre-push mode (terse, no GUI). If pandoc or Chrome is missing the script exits non-zero with a one-line reason — note it and carry on; the markdown report is still the source of truth. The PDF is shareable as-is (e.g. WhatsApp), so this doubles as the "send it to the author" artefact.
--fix (close the loop)--fix bridges review to implementation. Two entry paths:
/ro:council-review --fix): run steps 1-5, then fix if REQUEST-CHANGES./ro:council-review --fix --report <path>): skip steps 1-5, read that report's findings as the work list. This is the pickup command printed in every report, so a new session resumes exactly where the review left off.Then:
main, master, develop, or release/*, do NOT fix in place. Create fix/council-<YYYYMMDD-HHMMSS> off the current HEAD and switch to it before any implementer runs, then tell the user the new branch name. Fixing only ever lands on a working branch, never a protected one.dev): code/logic fixes → dev; missing-test findings (from the way-of-working lens) → test-writer; simplicity / SOLID-DRY / architecture refactors → refactorer. If an author agent from this session is still live, prefer routing to it (most context); otherwise spawn the implementer fresh with the report path + that finding set.Council-review: trailer naming the lens(es) that flagged it and the report path, so git log --grep "Council-review" recovers the provenance while the commits are live (before a squash collapses them). Shape:
<emoji> <type>: <subject>
<body>
Council-review: [<lens>[,<lens>]] <report-path>
e.g. Council-review: [simplicity,solid-dry] .code-review/council-20260629-191918.md. When a finding maps to a real tracker issue, add Closes #N as well (the trailer is for review provenance, the issue ref for the tracker). Tell implementer agents to include this trailer. Do NOT rewrite already-pushed commits to add it; instead fold the lens-level summary into the squash-merge message at ship time and into the PR body. Keep the no-em-dash rule in the trailer too (lens names are slugs, the report path is a path, so this is automatic).## Prevention section: route every apply: auto guardrail (the lint rule, the hook, the test, the type, the CLAUDE.md line) to dev as part of the same fix round, so the build-failing check that stops recurrence ships WITH the fix. For each human-judgement proposal (an ADR, an architecture change), do not silently apply it, file it as a GitHub issue (or open the ADR stub) and surface it to the user. Carry the Council-review: trailer on these commits too.fixed +1 in .code-review/recurrence.json for every class whose findings this round implemented. If the user instead rejected a class's findings during the fix round, that is rejected +1 and a feedback-loop prompt (see above).--ship (below) is the one exception, and only because the human typed it: the flag IS the explicit authorization to merge, given up front instead of at the end.--ship (merge and deploy once the verdict clears)--ship extends --fix with the last mile: when the fix loop clears, hand the branch to the /ro:ship flow so review, fix, gate, merge, and deploy are one gesture. The one-word entry point is the alias /ro:council-ship.
--ship implies --fix. A bare /ro:council-review --ship runs the full review-then-fix loop first.APPROVE or APPROVE-WITH-NITS under the active --block-on threshold. Only then does shipping start.--ship, the council never merges. Passing --ship is the human granting that authorization explicitly for this run; their name still goes on the PR./ro:ship. Do not re-implement the merge logic here. Read ../ship/SKILL.md and follow its flow: local gate (the repo's quality-checks or pre-push equivalent), push the branch, open the PR, squash-merge per the ro ci policy (gh pr merge --squash --admin --delete-branch when remoteCI is skip), then deploy via the repo's deploy path. All of /ro:ship's guards apply, including the refuse-on-main rule and the drizzle DB gate.--fix run would. The pickup pattern stays intact: a stopped --ship run is resumable from its report like any other, and the human decides whether to fix further or ship manually.Council-review: trailers into the squash-merge message and the PR body, as step 6 already requires at ship time.Print the verdict, the counts per severity, the report path, the PDF path (from step 5b), the one most important finding, and (when not clean) the Pickup command verbatim so the user can resume in a fresh session:
claude --dangerously-skip-permissions "/ro:council-review --fix --report .code-review/council-<ts>.md"
Keep it short; the detail is in the file.
--post (publish to a PR, GitHub or Bitbucket)When --post is set and the scope is a PR, publish the merged council verdict as inline review comments. A council post is branded and attributed as the council, not as a single RoBot reviewer — this is what distinguishes it from a /ro:code-review --post:
**🏛️ Council review** (NOT 🤖 RoBot review). Idempotency markers are <!-- council-review:start sha=… --> / <!-- council-review:end --> / <!-- council-review:inline -->. Do NOT delegate the post to /ro:code-review --post — that emits RoBot branding and merges the lenses into one voice, which is exactly what a council post must not do. Reuse code-review's gh api JSON shape, line-validation, and dedupe logic (see ../code-review/reference.md), but post under the council identity here.**🏛️ Council review** — _⚠️ Critical (blocking)_ · [correctness]. When two lenses flagged the same finding, list both: · [architecture] · [way-of-working] — these convergence cases are the highest-confidence findings and the reader must see them. Every finding bullet in the walkthrough body keeps its [lens] prefix too.Detect the host from the remote (git remote get-url origin):
github.com): post one atomic council review via gh api repos/<owner>/<repo>/pulls/<N>/reviews --method POST --input <json>. event: REQUEST_CHANGES if any finding is at/above the block-on threshold, APPROVE when clean, else COMMENT.
side: RIGHT; removed line → side: LEFT). Validate every path:line against the parsed PR diff BEFORE posting — GitHub returns 422 for an out-of-hunk line and rejects the whole atomic review. A finding on a line the PR did not change (e.g. a pre-existing .parse() the PR merely made newly reachable, or an unchanged system-prompt string) CANNOT be inlined: move it into the walkthrough body or the Out of scope section with its file:line and [lens] tag. (Lesson from dataforce #386: roughly half the findings landed on unchanged lines and had to be summarised in the body.)<!-- council-review:start marker and update the body with PUT .../pulls/<N>/reviews/<id> (-f body=…), and PATCH each existing inline comment via .../pulls/comments/<id>. This keeps the threads alive and avoids leaving a dismissed review hanging on the PR. Only dismiss-and-repost if the head SHA moved enough that the old line anchors are stale.bitbucket.org / Bitbucket Server): post via the REST API with curl. Inline comments: POST /2.0/repositories/{workspace}/{repo}/pullrequests/{id}/comments with body {"content":{"raw":"..."},"inline":{"path":"<file>","to":<line>}}. Auth with an app password / token from ~/.claude/.env (BITBUCKET_TOKEN, BITBUCKET_USER); if absent, tell the user which env keys to add and fall back to the local report. Tag each comment **🏛️ Council review** and carry the [lens] prefix to match the GitHub convention.Every posted comment embeds a copy-paste fix prompt so a reader can act on it immediately, for example:
**🏛️ Council review** — _⚠️ Critical (blocking)_ · [security]
Hardcoded secret. Move to env and rotate.
▸ To fix with an agent, paste in this repo:
/ro:council-review --fix --report .code-review/council-<ts>.md
(or, for just this finding) "use the dev agent to fix <file>:<line>: <one-line summary>"
Local-report-first stays the default; --post is opt-in. The report's Pickup block and the per-comment fix prompts use the same command, so review-to-fix is one consistent gesture whether you read the report locally or on the PR.
--pre-push makes the output terse and the exit semantics machine-readable for the git hook (see /ro:code-review-gate).testing
--- name: linear-pipeline description: The Fable orchestrator for a single dispatched Linear ticket. Holds almost no context itself; it receives `--issue <ID> --detached`, decides the stage sequence, and fans out a sub-agent per stage, passing forward only each stage's artifact (never re-derived, never inlined into its own context). Step zero, before any planning or stage routing, is a boundary triage against `canon/security-boundary.md` (#199): a match tags Ronan Connolly and stops the run, no
development
--- name: in-your-face description: Capture a chat-only answer into a durable artifact (markdown + HTML, PDF when cheap) and launch it automatically so the user cannot miss it. Use when user says "in your face", "don't let me lose this", "save that answer", "make that durable", or right after answering a substantive side question (a recipe, comparison, how-to, or generated prompt) that would otherwise die with the context. category: workflow argument-hint: [--no-open] [--vault <short>] [hint of
tools
One-shot headless OpenAI Codex CLI calls for background/admin AI tasks — summaries, classification, extraction, admin glue. The default engine for anything that runs AI constantly in the background (daemon-driven, per-event), because it bills the flat ChatGPT subscription instead of Claude usage or per-token API spend, and it keeps working while Claude is rate-limited. NEVER for coding — coding stays Claude. Use when a skill or daemon needs a cheap always-on AI call, when the user says "use codex", "ask codex", "codex as backup", or when building a background summarizer/classifier into a listener or loop. Reads auth from ~/.codex/auth.json (ChatGPT account, no API key).
research
Turn a warranty rejection, repair quote, or RMA email into a cited decision brief — legal read (NL/EU consumer law), is the part user-serviceable, live part and new-unit prices, repair-vs-DIY-vs-new economics, before-you-send-it checklist, deadlines. Use when the user pastes or screenshots a repair quote, warranty rejection, "not covered" email, onderzoekskosten fee, or asks "should I repair or replace this".