/sdlc:bug-finder
Generated from solutions/ontological/plugin/plugins/sdlc/skills/bug-finder/bug-finder.md.
Description
Section titled “Description”Sweep an entire codebase for real bugs and fix them. Partitions the tree into
module-boundary territories, then runs three chained agent workflows — scan
(three altitudes: in-function, in-module, in-library) → adversarial validate →
fix → verify-fix → land — so a fresh agent performs every step and the
orchestrating session never reads source itself. Opens one PR per module
boundary, then surveys the open PRs, computes the conflict matrix
deterministically with git merge-tree, and merges the overlapping bugfix
branches into each affected PR in a single aggregated push so CI runs once per
PR, not once per fix. Pass --bomb for maximum parallelization.
Allowed tools
Section titled “Allowed tools”BashReadGlobGrepAgentWorkflowAskUserQuestion
Source
Section titled “Source”Find and fix real bugs across a whole codebase, at a scale one context cannot hold.
Usage:
/sdlc:bug-finder— sweep the whole project./sdlc:bug-finder packages/ts solutions/ontological/lib— restrict the sweep to those paths./sdlc:bug-finder --bomb— maximum parallelization: every territory dispatched at once, deeper scan rounds, three independent refuters per candidate, two repair rounds./sdlc:bug-finder --report-only— stop after validation. No worktrees, no commits, no PRs./sdlc:bug-finder --no-reconcile— open the bugfix PRs but skip the open-PR survey and reconciliation.
The rules that govern everything
Section titled “The rules that govern everything”A bug is a defect, not a preference. Wrong result, crash, leak, corrupted
state, or a violated contract something else depends on. Style, naming, missing
tests, missing docs, and “this could be refactored” are out of scope — that is
/code-review and /simplify, not this skill.
The session orchestrates; it never reads source. Every scan, validation, fix, verification, landing, and reconciliation happens in a fresh subagent dispatched by a Workflow script. What returns to this session is a small structured summary. That is the whole defence against token decay — if you find yourself opening a source file to judge a finding, you have broken the design.
A different agent performs every step. The agent that found a bug never validates it. The agent that fixed a bug never verifies the fix. A finding that survives is one that three independently-contexted agents agreed on.
Repro beats reading, but the repo tree is read-only. An executed failure is what separates a real defect from a plausible story — the strongest verdicts this skill produces are the ones that ran the code and pasted the error. So finders and refuters are told to run repros, and to write every scratch file under the run’s scratch directory, outside the repo tree, deleting them when done. What they may never do is write inside the repo: a stray repro file there reads as someone else’s mystery diff. Step 5 is the first step allowed to touch the repo, and only inside a territory’s own worktree.
Refutation is the default. A validator’s job is to kill the finding. It
returns refuted: false only after reading the source and failing to kill it.
False findings cost far more than missed ones.
Reachability is the filter, not the vote count. Measured on a full
packages/ts run: a second refuter round killed 3 of 140 findings — 2% — while
the reachability rating cut 31%. Extra voters are a weak, expensive filter. What
separates a finding worth a PR from one worth nothing is whether a code path in
this repo reaches it today, so both the finder and the refuter must answer
reachableToday, and the fix batch is drawn from yes.
Severity belongs to the refuter. On that same run the finders’ severity was
downgraded 18 times and upgraded twice. A finder rates the defect it just
imagined; the refuter rates the one it went and checked. Rows carry both —
claimedSeverity and severity — and ranking uses the refuter’s.
One push per PR. Every push is a full CI run. Fixes are batched into one commit per territory and one push per branch; reconciliation merges every overlapping branch into an affected PR before pushing once. A PR with no textual conflict is never touched at all.
1. Resolve the scope and the mode
Section titled “1. Resolve the scope and the mode”Read the arguments. Positional paths narrow the sweep; with none, the scope is
the whole project. Record mode = bomb | normal, reportOnly, reconcile.
Pick the run’s scratch directory — this session’s scratchpad, or any path outside
the repo — and pass it to every workflow as scratchDir. That is where repro
files go.
Resolve the two paths the workflows need and keep them for later steps:
git -C . rev-parse --show-toplevelThe sdlc launcher is ${CLAUDE_PLUGIN_ROOT}/cli/sdlc.
Confirm the main checkout is clean before going further:
git status --porcelainIf anything is staged or modified, stop and report. This skill creates worktrees and commits; it must never sweep someone else’s work into them.
2. Partition the codebase into territories
Section titled “2. Partition the codebase into territories”A territory is one module boundary — and therefore one future PR. Enumerate the candidate source files:
git ls-files -- <scope> \ | grep -E '\.(ts|tsx|js|jsx|vue|rs|py|go|rb|java|kt|swift|c|h|cc|cpp)$' \ | grep -Ev '/(node_modules|dist|build|target|vendor|generated|fixtures|goldens|__snapshots__)/'Group the result into territories under these rules:
- A territory is a PR boundary. One package, or one coherent subdirectory of a package. Never span two publishable packages with one territory.
- Budget: at most ~40 source files or ~6000 lines. Split an oversized package by subdirectory until each piece fits.
- Do not scan generated output, vendored trees, lockfiles, build artifacts, or deliberately-malformed test fixtures. Agents may still read tests as evidence; they just are not scan targets.
- Drop territories with fewer than three source files — fold them into the nearest sibling instead.
When the scope has been swept before, give each territory a known array of
"file:line — title" strings for what earlier runs already found. It seeds the
scan’s exclusion list, so round 1 starts where the last run stopped instead of
re-reporting fixed bugs and burning the round budget before the deep rounds are
reached. Leave it off for a first sweep.
Give each territory a short kebab-case id (it becomes the branch name and the
PR title prefix), a human name, its paths, and an optional note naming what
the module is responsible for.
Territories are dispatched in batches of at most 20 per workflow invocation
so a run stays inside the Workflow agent caps. In normal mode, run the batches
in sequence and stop early if the user has seen enough; in --bomb mode, run
every batch.
Show the user the territory list and the batch count before dispatching.
3. Scan and validate — the first workflow
Section titled “3. Scan and validate — the first workflow”For each batch, invoke the scan workflow. It is a checked-in script; pass it by path, not inline:
Workflow({ scriptPath: "${CLAUDE_PLUGIN_ROOT}/skills/bug-finder/scan.workflow.js", args: { territories: [ … this batch … ], mode: "<bomb|normal>" }})What it does, per territory: three altitude lenses (in-function, in-module,
in-library) scan in parallel and loop until they go dry, then every surviving
candidate is handed to independent refuters — one in normal mode, three with
distinct lenses (reachability, existing guards, reproduction) in --bomb. A
candidate survives only on a non-refuted majority.
It returns
{ mode, territories: [{ id, name, paths, scanned, confirmed, unvalidated }] }.
Keep that. Do not re-derive it, and do not open the cited files yourself.
unvalidated carries candidates whose every refuter failed — a rate limit, an
API error, a skip. Those were never judged. Treat them as an outage, never as
refutations.
4. Report the confirmed findings and take one batched approval
Section titled “4. Report the confirmed findings and take one batched approval”Present the confirmed findings grouped by territory, ordered by severity —
file:line, the mechanism, the failure scenario. Say plainly how many candidates
were scanned versus how many survived, and name any territory the workflow
logged as hitting its round cap (its coverage is incomplete).
Report the unvalidated count separately and first. A run whose validators all
failed returns zero confirmed findings, and that number means nothing —
saying “no bugs found” there is a false all-clear. Say validation did not run,
and offer to re-run it; the scan results are already paid for and recoverable
from the workflow’s journal.jsonl.
If --report-only, stop here. Nothing was written.
Otherwise ask for one approval covering the whole fix batch, via AskUserQuestion: which territories to fix (default: all with confirmed findings). This is the only consent gate before code is written and PRs are opened; do not ask again per territory.
5. Create one worktree per approved territory
Section titled “5. Create one worktree per approved territory”Only for territories that survived Step 4 — an empty territory gets no worktree
and no branch. From the project root, sequentially (concurrent
git worktree add calls contend on the shared index lock):
${CLAUDE_PLUGIN_ROOT}/cli/sdlc project preflight-worktree bug-finder-<id> --branch chore/bug-finder-<id>git worktree add .sdlc/worktrees/bug-finder-<id> -b chore/bug-finder-<id> mainIf preflight reports a branch or worktree collision, an earlier run left state behind — ask the user before touching it.
Then arm each worktree with the project’s verbs.setup verbs from sdlc.yaml
(in this repo, bun install), or the quality checks will fail for missing
dependencies rather than for real defects.
6. Fix, verify, and land — the second workflow
Section titled “6. Fix, verify, and land — the second workflow”Workflow({ scriptPath: "${CLAUDE_PLUGIN_ROOT}/skills/bug-finder/fix.workflow.js", args: { territories: [ … approved, each with worktree, branch, findings … ], mode: "<bomb|normal>", repoRoot: "<project root>", sdlc: "${CLAUDE_PLUGIN_ROOT}/cli/sdlc", baseBranch: "main" }})Each territory runs its own chain, and territories run concurrently — they are
isolated by worktree, which is why parallel fixing is safe. A fix agent applies
the smallest correct change and commits nothing; a fresh verify agent reads the
diff it did not write, judges each finding independently, hunts regressions the
diff introduces, and runs the project quality checks; a repair round follows if
anything is unfixed or red (two rounds under --bomb). Only findings the
verifier confirmed get landed, in one commit and one push, as one PR per module
boundary.
It returns { landed: [{ id, branch, landed, prUrl, … }] }.
7. Survey the open PRs and compute the overlap matrix
Section titled “7. Survey the open PRs and compute the overlap matrix”Skip this step under --no-reconcile.
This step is deterministic — no agents, no CI, no pushes. Fetch, list the open PRs, and test each one against each branch this run opened:
git fetch origingh pr list --state open --base main --json number,headRefName,title --limit 200Drop this run’s own PRs from the list. For every remaining PR head and every
chore/bug-finder-* branch:
git merge-tree --write-tree --name-only origin/<headRefName> chore/bug-finder-<id>Exit 0 means the two merge cleanly — that PR is left alone, no push, no CI.
A non-zero exit means a real textual conflict, and the conflicted paths are
listed in the output. Collect them into a plan entry per PR:
{ prNumber, headRef, title, conflictsWith: [{ branch, files: [...] }] }Aggregation happens here: a PR that conflicts with three bugfix branches produces one plan entry naming all three, not three entries.
8. Reconcile the conflicting PRs — one aggregated push each
Section titled “8. Reconcile the conflicting PRs — one aggregated push each”If the plan is empty, say so and skip to Step 9 — nothing conflicts and no CI was spent.
Otherwise show the user the full plan — every PR, every branch to be merged into it, every conflicted file — and take one batched approval before touching anyone’s branch. Pushing to an open PR is outward-facing and other people may own those branches; one approval covers the whole plan, but there must be one.
On approval:
Workflow({ scriptPath: "${CLAUDE_PLUGIN_ROOT}/skills/bug-finder/reconcile.workflow.js", args: { plan: [ … ], repoRoot: "<project root>", sdlc: "${CLAUDE_PLUGIN_ROOT}/cli/sdlc" }})One agent per affected PR merges every overlapping branch into a scratch
worktree, resolves each conflict by keeping both intents — the fix applied to the
PR’s version of the code, never one side taken wholesale — runs the quality
checks, and pushes once. A conflict it cannot resolve safely aborts that PR’s
merge, pushes nothing, and comes back as needsHuman.
9. Report, then tear down what did not land
Section titled “9. Report, then tear down what did not land”Give the user, in one summary:
- Territories scanned, candidates found, findings confirmed, findings fixed.
- Every PR opened, as a clickable link, with the count of fixes it carries.
- Findings marked
skippedornot-a-bugby the fix or verify agents, and why. - PRs reconciled, PRs left alone as conflict-free, and any
needsHumanconflict. - Any territory whose coverage was capped, named explicitly. Never let a bounded sweep read as an exhaustive one.
Tear down the worktrees of territories that landed nothing:
${CLAUDE_PLUGIN_ROOT}/cli/sdlc project teardown-worktree bug-finder-<id>Leave the worktrees backing open PRs in place; they are torn down when their PR
merges, the same way /sdlc:task-close-out closes a task’s loop.
Failure modes
Section titled “Failure modes”- Dirty main checkout at Step 1 — stop. Do not stash (the stash stack is shared across worktrees) and do not commit the user’s work.
- A scan workflow returns no confirmed findings — a real and common outcome on a healthy territory. Report it as coverage, not as failure, and open nothing.
- An agent returns
null— the workflows already null-guard everyagent()result and drop that item. A territory can therefore come back with a missing stage; the report must say so rather than imply it was clean. - Mass agent failure mid-run (rate limit, outage) — the scan results already
produced are recoverable: each run’s
journal.jsonlholds oneresultline per completed agent. Scanning is the expensive half, so do not re-scan. Extract the candidates from the journal and feed them to${CLAUDE_PLUGIN_ROOT}/skills/bug-finder/validate.workflow.js, which validates an already-scanned set and takesvoters: 1for the cheap pass. Where the same batch must genuinely re-run,resumeFromRunIdreplays the completed agents from cache so only the failed ones cost anything. Never report a rate-limited run as a clean sweep. - Quality checks fail for a pre-existing reason — the verify agent reports it
in
qualityOutput. Land the fixes anyway and say in the PR body that the failure predates the branch; do not let this skill start fixing unrelated red. - A reconcile merge is genuinely incompatible — the agent aborts and returns
needsHuman. Surface it verbatim; do not retry it with a different agent. - Round cap reached before the scan went dry — coverage is incomplete for that territory. Say which one, every time.
- Three workflows, plus a recovery one. The split exists because worktrees
must be created between scanning and fixing, and the conflict matrix must be
computed after the PRs exist. Each invocation returns a small structured
object, so the session stays cheap across all of them.
validate.workflow.jsis the fourth: it re-judges an already-scanned candidate set, for the outage case in Failure modes. - Embedding a candidate set in a runner. A Workflow script has no filesystem
access, so a recovered candidate set reaches it either through
argsor by being written into a copy of the script thatscriptPaththen loads. The second avoids pushing the whole set through the session’s context. Build that copy with a replacement function — as a replacement string,$&,$`and$'are substitution patterns, and finding prose is full of$. - The scripts are orchestration definitions, not deterministic ops. They
describe how agents are fanned out, which is exactly what the Workflow tool
consumes, so they live beside the skill. The deterministic tail this skill
still runs inline — the
git merge-treeoverlap matrix in Step 7 — is a candidate for a realsdlc pr overlapverb; until that exists, keep the recipe in Step 7 rather than growing a skill-side script. - Branch naming.
chore/bug-finder-<territory-id>— bulk skill-runtime work, not a single-task implementation. See${CLAUDE_PLUGIN_ROOT}/conventions/branch-naming.md. - Committing model-generated messages. The land agent commits through
sdlc commit create, which routes the message via argv or stdin and anmktemp’d file. See${CLAUDE_PLUGIN_ROOT}/conventions/commit-messages.md. - The test runner is per package, not per repo. A mixed monorepo answers
“run the tests” differently in each package —
bun test,bun test tests,vitest, or nothing declared at all. Running a vitest package’s suite underbun testloses the DOM environment its config supplies and every test dies withdocument is not defined, which is indistinguishable at a glance from a broken fix. Both the fixer and the verifier readpackage.jsonscripts.testand use exactly that, and a red suite counts as evidence only once it names the assertion the fixer wrote. - A green suite is half the gate.
scripts.typecheckis a separate CI job fromscripts.test, and a test file that runs clean can still failtsc— a cast between types that do not overlap, a callback typed for an argument nothing passes. Landing on the suite alone ships a red build. Both are run before a fix is called done. - A test belongs in the package that owns the code it pins. A fix that reaches into a dependency puts its test there too. The failure mode is specific: mocking a bare specifier the test’s own package does not declare intercepts the module only if both packages resolve to the same copy. That is an install detail, not something the suite decides — so it mocks correctly on a dev machine and silently mocks nothing in CI, where the real module loads, throws, and gets swallowed by the component’s own error handling. Mock the relative path the code under test imports.
- Wait for the condition, not for a tick count. A fixed number of
nextTick/setTimeoutrounds races whatever the code awaits — a lazy import especially — and a loaded CI runner wins. Poll until the assertion holds, with a ceiling that throws naming what never arrived. A fixed drain is right only where the assertion is that nothing arrives. - Staging. Fix agents stage explicit paths only.
git add -Ain a shared checkout stages a parallel session’s work; the worktree isolation makes it survivable, the explicit paths make it correct. - What
--bombactually buys. Depth, through four knobs.MAX_ROUNDS4 vs 2 andDRY_ROUNDS2 vs 1 govern the scan loop;effortishighvsmedium; and every territory batch is dispatched rather than stopping early. The loop is the substance: each round re-runs all three lenses with FRESH agents handedseenTitles, the findings so far, and told to go somewhere new — so a later round is not a re-run but a forced march past the obvious into the tail.DRY_ROUNDS2 then refuses to accept a single empty round as proof the territory is clean, because one agent finding nothing is not evidence. What bomb mode does NOT buy is more voters: one refuter plus the reachability gate measured as good a filter as three, at a third of the cost. - Budget the scan, not the validation. On the
packages/tsrun the scan was the half that mattered and the half that got starved: 125 of ~1,100 scan agents survived a rate limit, so the sweep delivered roughly one round of first impressions. Validation of everything they found cost a fifth as much. When a run must be trimmed, trim voters and keep rounds. - Relationship to
/code-review. That reviews a diff; this sweeps a tree. They share the finding-then-adversarially-verify shape on purpose — a finding that cannot survive a skeptic is not a finding.