Skip to content

/sdlc:bug-finder

Generated from solutions/ontological/plugin/plugins/sdlc/skills/bug-finder/bug-finder.md.

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.

  • Bash
  • Read
  • Glob
  • Grep
  • Agent
  • Workflow
  • AskUserQuestion

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.

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.

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-toplevel

The sdlc launcher is ${CLAUDE_PLUGIN_ROOT}/cli/sdlc.

Confirm the main checkout is clean before going further:

git status --porcelain

If 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> main

If 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 origin
gh pr list --state open --base main --json number,headRefName,title --limit 200

Drop 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 skipped or not-a-bug by the fix or verify agents, and why.
  • PRs reconciled, PRs left alone as conflict-free, and any needsHuman conflict.
  • 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.

  • 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 every agent() 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.jsonl holds one result line 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 takes voters: 1 for the cheap pass. Where the same batch must genuinely re-run, resumeFromRunId replays 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.js is 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 args or by being written into a copy of the script that scriptPath then 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-tree overlap matrix in Step 7 — is a candidate for a real sdlc pr overlap verb; 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 an mktemp’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 under bun test loses the DOM environment its config supplies and every test dies with document is not defined, which is indistinguishable at a glance from a broken fix. Both the fixer and the verifier read package.json scripts.test and 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.typecheck is a separate CI job from scripts.test, and a test file that runs clean can still fail tsc — 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/setTimeout rounds 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 -A in a shared checkout stages a parallel session’s work; the worktree isolation makes it survivable, the explicit paths make it correct.
  • What --bomb actually buys. Depth, through four knobs. MAX_ROUNDS 4 vs 2 and DRY_ROUNDS 2 vs 1 govern the scan loop; effort is high vs medium; 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 handed seenTitles, 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_ROUNDS 2 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/ts run 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.