megamaid: a review → plan → fix pipeline for code review with configurable agents and authoritative project directives.

A Claude Code plugin for code review. /megamaid:review finds issues, /megamaid:plan turns each one into a fix instruction, /megamaid:fix applies them, and /megamaid:vacu-suck runs all three in one go.
Reviewers follow eight shipped directives plus your own, and a layered config lets each project override the defaults.
Forked from comb 0.15.3. megamaid moves comb's rules into a hooks module and requires a Claude Code build with hooks modules, with the module loaded; comb stays for builds without them.
With comb also installed, both plugins' model-invocable skills trigger on the same phrases; install one, or use explicit /megamaid:* commands.
/megamaid:review runs 1–5 reviewer agents over a PR, branch, file list, or spec. A code change gets a severity-ranked report with a verdict. A prose change (spec, design doc) gets a shorter report with labelled findings./megamaid:plan writes one self-contained fix instruction per finding./megamaid:fix applies the instructions. Each item gets an implementer and a reviewer, and items that touch different files run in parallel. Before each commit, the reviewer runs the plan's Check: line and the project's tests run. A failed attempt is reverted and recorded before the retry. Each passing item is committed as <code>: <title>. Only you can start it; see "Let Claude start fixes" below to change that./megamaid:vacu-suck runs review, plan, and fix without pauses. On a prose change it stops after review./megamaid:patterns scans the codebase and writes a PATTERNS manifest: the project's actual conventions, with file:line references. Review, plan, and fix give agents the relevant excerpt as a baseline. --shave shrinks an existing manifest without changing what it says./megamaid:configure changes the config from a plain-language request./megamaid:help prints an overview. /megamaid:help <command> shows one command in detail.Every command takes a free-form focus brief that steers which agents run and what they prioritize:
/megamaid:review look for spec/plan misalignment
/megamaid:vacu-suck ensure simplicity, scope discipline, and TDD coverage
vendor/claude-code-types/), with megamaid's hooks module loaded. A command that replies MEGAMAID-MODULE-NOT-LOADED means it did not load; /megamaid:help lists the causes.python3 3.8 or later. Every command starts by running a bundled stdlib script and stops if it can't.git. gh is also needed to review a PR by number.effort value.claude --plugin-dir /path/to/megamaid-claude-plugin
Coming from comb? Before your first megamaid run, and only while docs/megamaid/ does not exist yet, move comb's output folder with git mv docs/combs docs/megamaid (plain mv if it is untracked); this also puts the PATTERNS manifest where megamaid reads it. Then run /megamaid:configure to migrate comb's config, and point any paths.* it copied from docs/combs/ at docs/megamaid/. If you don't move the folder, /megamaid:configure offers to move or copy the manifest instead. An empty {} megamaid.config.json silences the config notice.
In a git repo with changes on a branch:
/megamaid:review
The report lands in docs/megamaid/reviews/. Run /megamaid:plan on it, then /megamaid:fix. Or run /megamaid:vacu-suck to do all three.
Three layers are deep-merged, with later layers winning:
config/defaults.json in the plugin~/.claude/megamaid.config.json (global)<project-root>/.claude/megamaid.config.json (project)/megamaid:configure makes changes for you, for example "disable test-auditor" or "use sonnet for the fix implementer". It shows a diff, then writes the change to the global or project file.
config/defaults.json lists every key with its default. Three kinds of setting are absent by default: effort.<lane>, and the per-role agents.<role>.model and agents.<role>.effort.
{
"models": { "fix": { "implementer_standard": "sonnet" } },
"effort": { "review": "medium" },
"agents": { "simplifier": { "model": "haiku" } }
}
sonnet, opus, haiku, or fable. The agent dispatch rejects a full model ID.models.fix.implementer_standard or models.fix.implementer_trivial and keep models.plan strong.low, medium, high, xhigh, or max. When it is unset, agents use your session's effort.In one measured review run, medium found the same bugs as high for about 8% less.
{ "fix": { "commit_per_item": false } }
{ "fix": { "model_invocation": true } }
/megamaid:fix is user-only by default because it edits code and commits. With this flag, asking in your own words ("apply the fixes") runs the same workflow, with the same questions and commits. Without the flag, such a request gets a short reply naming /megamaid:fix and the flag.
{ "agents": { "implementer": { "subagent_type": "your-org:implementer" },
"planner": { "subagent_type": "your-org:planner" } } }
The default writer, megamaid:implementer, edits only the files the instruction names, never commits, and reports every file it touched. A writer from outside this plugin gets an explicit instruction that the directives are authoritative. It also gets the same boundaries, and the same file and git checks after it returns.
The default planner, megamaid:planner, writes one instruction file per finding and never edits source. A planner from outside this plugin gets the same authority instruction and its lens; a code-shaped plan run counts the output folder after it returns, and that is the only check (a plan run has no git checks).
{
"agents": {
"react-expert": {
"subagent_type": "react-expert",
"when_to_use": "Diff includes React components, hooks, or JSX."
},
"simplifier": { "subagent_type": "pr-review-toolkit:code-simplifier" },
"test-auditor": null
}
}
A new role needs subagent_type and when_to_use. Overriding a shipped role can set just the fields you change. null removes a role.
{ "directives": { "user_path": "docs/our-rules" } }
The plugin ships eight directives in directives/:
simplicity.md: no overengineering or speculative fixesmodularity.md: single responsibility, clear interfacesreusability.md: no copy-paste, no premature abstractionmaintainability.md: naming, comments, no dead codequality.md: correctness, error handlingconsistency.md: follow existing patterns and the specscope-discipline.md: stay inside the requested changetesting.md: real tests that cover changed behaviorYour directives in directives.user_path (default docs/directives/) apply on top. Both sets are authoritative, and findings cite them as <file>.md §N.N. Agents get the files by path, not pasted into their prompt. To use only your own directives, set "directives": { "include_plugin_defaults": false }.
Five read-only reviewers:
megamaid:code-reviewer: bugs, logic, contracts, securitymegamaid:simplifier: overengineering, dead code, duplicationmegamaid:silent-failure-hunter: swallowed errors, hidden fallbacksmegamaid:test-auditor: coverage and test qualitymegamaid:consistency-auditor: fit with existing patterns and with the spec or planRead-only is enforced by instruction. The edit tools are blocked, but Bash stays available for git.
Each finding must name a real cost, such as a failure mode, a coverage gap, or a maintenance burden. A clean diff should come back with few or no findings. simplifier, silent-failure-hunter, and consistency-auditor re-check their findings in a final step. code-reviewer and test-auditor dropped that step after an experiment showed it changed nothing.
Before a finding enters the report, /megamaid:review opens every cited line and recomputes every count. Each finding carries a Verified: line stating what was checked and what was not. /megamaid:plan re-checks anything marked unchecked.
Four more agents:
megamaid:planner is the planner for /megamaid:plan and for the issues /megamaid:fix discovers.megamaid:implementer is the writer for /megamaid:fix.megamaid:pattern-scanner is used only by /megamaid:patterns.megamaid:pattern-shaver is used only by /megamaid:patterns --shave.scripts/megamaid.py merges config, resolves each agent's model and effort, matches directives to the focus brief, names reports, and batches fix items. Each dispatch prompt is a few lines plus the path to a per-run brief that the script writes. Planner and fix agents also get their role's brief in shared/. Working files live in .megamaid/, which ignores itself in git and is cleaned at the end of each run.scripts/manifest.py cuts the PATTERNS manifest down to the sections whose scope matches the change, and lists the omitted sections by line range. The rules are in shared/manifest-slicing.md. Add a one-line Home: to a section if it is included too often.skills/*/SKILL.md and shared/*.md define the behavior, and CHANGELOG.md records every change to it. Skills run in your session's model; agents get the model from config on each dispatch.PLAYBOOK.md describes the workflow and design rules.
Release gate, in order:
claude plugin validate .scripts/check-contract.sh runs the free checks:config/defaults.json.hooks/stop-line.ts.tsc) and its tests pass (claude plugin test).claude plugin eval . --scaffold --ablation none --runs 1 --allow-tools Bash Write Edit runs the eight behavioral cases in evals/, at about 5 USD per pass. On macOS it needs a standalone git, such as Homebrew's. evals/README.md explains the flags.scripts/smoke.sh runs the free gates plus three short claude -p runs (about 0.25 USD): help with the module on, with proof it loaded, and review with the module off, which must stop. Run it once before a PR.
The hooks module type-checks against a pinned copy of the engine's types in vendor/claude-code-types/. To refresh it for a new Claude Code: run claude --plugin-dir . once, then scripts/sync-types.sh, and commit the result on its own as chore(types): Claude Code X. scripts/replay-graders.py re-scores a kept eval run for free. experiments/ holds two-arm runs that test whether a prompt passage is still needed: run them with --eval-dir experiments, and see experiments/README.md for results.
CI runs gates 1 and 2 on every push. The eval suite runs on manual dispatch.
v0.3.1. See CHANGELOG.md; earlier history is linked from its 0.1.0 entry.
MIT
hooks/register.ts 13 lines1import type { Register } from 'claude-code'
2import { registerSkillPrompt } from './skill-prompt'
3import { registerReadNote } from './read-note'
4import { registerAgents } from './agents'
5import { registerSession } from './session'
6
7export const register: Register = on => {
8 registerSkillPrompt(on)
9 registerReadNote(on)
10 registerAgents(on)
11 registerSession(on)
12}
13hooks/skill-prompt.ts 26 lines1import { update } from 'claude-code'
2import type { On } from 'claude-code'
3import { stripStopLine } from './stop-line'
4import { isRunSkill, startRun } from './run'
5
6const RUN = { plugin: 'megamaid', key: 'run' } as const
7
8// One registration on purpose: if a second skill.prompt hook of this plugin threw, the strip would
9// be lost and a loaded module would read as "not loaded". Run start sits in try; the .catch
10// replays next (nothing beneath runs twice) and strips, so an overrun cannot cost the strip either.
11export function registerSkillPrompt(on: On): void {
12 on('skill.prompt', { skill: /^megamaid:/ }, async ($, e, next) => {
13 const text = stripStopLine((await next(e)).text)
14 try {
15 if (isRunSkill(e.skill)) {
16 const run = startRun(crypto.randomUUID(), e.skill, await $.clock.now())
17 await update($, RUN, () => run)
18 $.ui.log(`run ${e.skill} started (${run.id})`, { to: 'debug' })
19 }
20 } catch (err) {
21 $.ui.log(`run detection failed: ${err instanceof Error ? err.message : String(err)}`, { to: 'debug' })
22 }
23 return { text }
24 }).catch(async ($, e, next) => ({ text: stripStopLine((await next(e)).text) }))
25}
26hooks/read-note.ts 29 lines1import type { On } from 'claude-code'
2import { SKIP_NOTE, STOP_SKILLS } from './stop-line'
3
4// vacu-suck and fix-auto Read sub-skill files that open with the stop line. While the module is
5// loaded, their main-loop Read gets a note that the line does not apply; with no module there is
6// no note and the run stops. The hook refuses nothing: a fault means no note, which stops the run.
7export function registerReadNote(on: On): void {
8 on('tool.call', { tool: 'Read' }, async ($, e, next) => {
9 const r = await next(e)
10 if (e.tool !== 'Read' || e.agentId !== undefined) return r
11 if (r.deny !== undefined || r.isError === true) return r
12 const path = String(e.file_path)
13 if (!/[\\/]SKILL\.md$/.test(path)) return r
14 const root = (await $.fs.stat($.plugin.root, { resolve: true })).realPath
15 const real = (await $.fs.stat(path, { resolve: true })).realPath
16 if (root === undefined || real === undefined) return r
17 const name = skillName(root, real)
18 if (name === null || !STOP_SKILLS.includes(name)) return r
19 return { ...r, context: [...(r.context ?? []), SKIP_NOTE] }
20 }).catch(($, e, next) => next(e))
21}
22
23// The skill a resolved path names when it is <root>/skills/<name>/SKILL.md, else null.
24function skillName(root: string, real: string): string | null {
25 if (!real.startsWith(root)) return null
26 const m = /^[\\/]skills[\\/]([^\\/]+)[\\/]SKILL\.md$/.exec(real.slice(root.length))
27 return m?.[1] ?? null
28}
29hooks/agents.ts 34 lines1import { update } from 'claude-code'
2import type { On } from 'claude-code'
3import { addAgent, parseBrief, startRun } from './run'
4
5const RUN = { plugin: 'megamaid', key: 'run' } as const
6
7// Records megamaid's own main-loop dispatches: a prompt that names a run brief, or (the briefless
8// scanner and shaver) a megamaid-provided agent while the stored run is patterns. The stored run is
9// the last run, not an active one, so nothing else is attributed to it. The hook refuses nothing.
10export function registerAgents(on: On): void {
11 on('agent.spawn', async ($, e, next) => {
12 const r = await next(e)
13 if (e.parentAgentId !== undefined || r.agentId === undefined) return r
14 const agentId = r.agentId
15 const brief = parseBrief(e.prompt)
16 if (brief === null) {
17 if (e.provider.plugin.split('@')[0] !== 'megamaid') return r
18 const { value: stored } = await $.state.get(RUN)
19 if (stored?.skill !== 'megamaid:patterns') return r
20 }
21 const now = await $.clock.now()
22 const fresh = crypto.randomUUID()
23 await update($, RUN, run => addAgent(run ?? startRun(fresh, 'none', now), agentId, {
24 subagentType: e.subagentType,
25 provider: e.provider.plugin,
26 role: brief?.role ?? 'unknown',
27 lane: brief?.lane ?? null,
28 runStem: brief?.runStem ?? null,
29 }))
30 $.ui.log(`${brief?.lane ?? '-'}/${brief?.role ?? 'unknown'} spawned as ${e.subagentType} (${agentId})`, { to: 'debug' })
31 return r
32 }).catch(($, e, next) => next(e))
33}
34hooks/session.ts 21 lines1import { update } from 'claude-code'
2import type { On } from 'claude-code'
3
4const RUN = { plugin: 'megamaid', key: 'run' } as const
5
6export function registerSession(on: On): void {
7 // smoke.sh reads this line to know the module loaded.
8 on('session.start', async ($, e, next) => {
9 const { version } = await $.session.version()
10 $.ui.log(`module loaded (Claude Code ${version})`, { to: 'debug' })
11 return next(e)
12 })
13 // /clear ends the conversation too; the next skill starts a fresh run. next first: the chain
14 // shares one time limit, so the write spends what is left after the other hooks.
15 on('session.end', async ($, e, next) => {
16 const r = await next(e)
17 await update($, RUN, () => null)
18 return r
19 }).catch(($, e, next) => next(e))
20}
21hooks/stop-line.ts 12 lines1// The stop line is copied into every skill but help (check-contract §11 keeps the copies equal to
2// STOP_LINE). The module strips it; with no module the model reads it and stops.
3export const STOP_LINE = "STOP. megamaid's hooks module is not loaded in this session, so everything below this line is disabled: do not follow it, run no tools, and reply only with `MEGAMAID-MODULE-NOT-LOADED: run /megamaid:help`."
4export const STOP_TOKEN = 'MEGAMAID-MODULE-NOT-LOADED'
5export const TOKEN_LINE = 'MEGAMAID-MODULE-NOT-LOADED: run /megamaid:help'
6export const SKIP_NOTE = "megamaid's hooks module is loaded; the STOP line at the top of this file does not apply."
7export const STOP_SKILLS: readonly string[] = ['configure', 'fix', 'fix-auto', 'patterns', 'plan', 'review', 'vacu-suck']
8
9export function stripStopLine(text: string): string {
10 return text.split('\n').filter(line => line.replace(/\r$/, '') !== STOP_LINE).join('\n')
11}
12hooks/run.ts 32 lines1import type { MegamaidAgent, MegamaidRun } from '../types'
2
3// Pure helpers for run detection. No $ here, and no state ref: each file that writes state
4// declares its own RUN const (validate reads a ref only as a literal const of that file).
5
6export function isRunSkill(name: string): boolean {
7 return name.startsWith('megamaid:') && name !== 'megamaid:help'
8}
9
10export function startRun(id: string, skill: string, startedAt: number): MegamaidRun {
11 return { id, skill, startedAt, agents: {} }
12}
13
14// A run brief: .megamaid/excerpts/<stem>.<lane>.<role>.brief.md, written by megamaid.py brief.
15// <stem> is <skill>-<digits>-<hex4>; <lane> is the brief's skill (review, plan, fix); <role> is a
16// config key, unrestricted, so it is whatever stands between <lane>. and .brief.md.
17const BRIEF = /\.megamaid[\\/]excerpts[\\/]([a-z][a-z-]*-[0-9]+-[0-9a-f]{4})\.([a-z][a-z-]*)\.([^\s\\/]+?)\.brief\.md/
18
19export type Brief = { runStem: string; lane: string; role: string }
20
21export function parseBrief(prompt: string): Brief | null {
22 const m = BRIEF.exec(prompt)
23 if (m === null) return null
24 const [, runStem, lane, role] = m
25 if (runStem === undefined || lane === undefined || role === undefined) return null
26 return { runStem, lane, role }
27}
28
29export function addAgent(run: MegamaidRun, agentId: string, agent: MegamaidAgent): MegamaidRun {
30 return { ...run, agents: { ...run.agents, [agentId]: agent } }
31}
32types/index.d.ts 8 lines1export type MegamaidAgent = { subagentType: string; provider: string; role: string; lane: string | null; runStem: string | null }
2export type MegamaidRun = { id: string; skill: string; startedAt: number; agents: Record<string, MegamaidAgent> }
3declare module 'claude-code' {
4 interface PluginState {
5 megamaid: { run: MegamaidRun | null }
6 }
7}
8