Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .claude-plugin/marketplace.json
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@
"name": "swarm",
"source": "./plugins/swarm",
"description": "Local mixture-of-agents code review for Claude Code. Fans a diff across Claude lenses plus the codex and grok CLIs (grok-4.5), merges by mechanism with cross-family consensus, verifies solo findings and all design suggestions, and presents one ranked report. Optional --fix / --loop applies the findings you agreed with; --pr reviews a GitHub PR diff and posts the result. Skills: /swarm:review, /swarm:agents.",
"version": "0.5.0"
"version": "0.5.1"
}
]
}
55 changes: 47 additions & 8 deletions .claude/knowledge/features/swarm-review-pipeline.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
---
title: "Swarm Review Pipeline (/swarm:review)"
createdAt: 2026-07-08
updatedAt: 2026-07-17
updatedAt: 2026-07-18
createdFrom: "PR #24"
updatedFrom: "PR #35"
updatedFrom: "harden-swarm-design-lens-integration"
pluginVersion: 1.8.2
prime: false
reindexedAt: 2026-07-12
Expand Down Expand Up @@ -39,14 +39,26 @@ truth** (the per-cluster externals follow-up consumes it):
one broad pass → default = per-cluster → `--max` = per-lens. The **gate
stays per-lens** (a fully-pruned cluster spawns no agent); design lenses are
first-class in the gate prompt, skipped only when the diff can't pay off.
**Accepted tradeoff of the cluster default:** per-lens failure isolation is
gone — one crashed cluster finder drops that whole cluster's Claude coverage
for the round (a visible `backendError`, never silent); `--max` restores
per-lens isolation. Documented, not retried per-lens (minimal — the default
trades isolation for fewer agents).
- **`kind` is derived from the lens name** (`design` vs `defect`) — no
finding-schema change, so the 3-place schema mirror is untouched. A merged
cluster's kind comes from its TAGGED members (design only when every tagged
member is design — a design suggestion merged with a real defect must not
leave the defect ranking); untagged (`unspecified`) members don't vote, and
an **all-untagged cluster is never auto-accepted** — its "consensus" is
backed by no lens, so it is verified like a solo (the `--max` dogfooding
round found exactly this hole).
round found exactly this hole). **Untagged findings stay `kind: "defect"`**
(the safe bucket), NOT inferred as design from cluster homogeneity: 0.5.1
tried that inference (to keep a dropped-`[reuse]` design suggestion out of the
defect table) and reverted it — a design finder is invited to report defects
too, so an untagged finding may be a real off-lens BUG, and inferring `design`
routes that bug to applicability verify (wrong rubric → can drop a real defect)
and out of the `--loop` defect tally. Dropping a bug outweighs mis-filing a
suggestion (the branch's own external-only self-review caught this).
- **Verify path decision: kind-aware prompt, not bypass.** Design findings are
suggestion-shaped, but each has a falsifiable applicability core (reuse
target exists? simpler form behavior-identical? claimed waste real?) — the
Expand All @@ -60,7 +72,10 @@ truth** (the per-cluster externals follow-up consumes it):
- **Report keeps kinds apart**: defects table first, then a same-format
`Design` table (shared numbering — the workflow sorts defects first);
`balance.design` counts the subset. Lens prefixes are parsed with `[\w-]`
(hyphenated names like `removed-behavior` — plain `\w` misses them).
(hyphenated names like `removed-behavior` — plain `\w` misses them). The Design
table has no lens column; instead each design row's finding cell is prefixed
`[lens]` (0.5.1) — the SAME in the in-session table and the PR comment, so lens
attribution reads identically on both surfaces.
- Effort: design lenses run at the **same effort** as defect lenses (`xhigh`
under `--max` — user call: depth applies to design thinking too).

Expand Down Expand Up @@ -133,16 +148,32 @@ the diff out of the script, above). Claude applies edits between rounds.
❌-disagree is never touched and stays visible in the report.
- **Re-confirm claim-vs-code before every edit** — a stale finding (comment rot,
already-fixed, line drift) is reported as skipped, never fabricated into an edit.
**Kind-aware (0.5.1):** a `kind:"design"` finding has no line-local defect to
re-find, so re-confirm the *suggestion still applies* (reuse target / duplication
/ simpler form / waste still present), not "defect still present" — else an
agreed design fix is silently dropped as skipped-stale.
- **Deterministic bits live in `scripts/loop-closeout.py`, not skill prose**
(per the project's prose-drift memory — stateful skill logic drifts as
prose): `step` = the 4-part termination decision in **fixed order** (0-findings
/ nothing-agreed / no-change / cap, default 10), `box` = the OPEN-findings
prose): `step` = the termination decision in **fixed order** (0-findings /
nothing-agreed / no-change / **design-only** / cap, default 10), `box` = the OPEN-findings
close-out visualization that **shows a legitimate rise** (a fix surfaced new
findings) instead of hiding it. Stateless — Claude passes the per-round counts
in; no state file, so no cwd footgun. The determinism is **the arithmetic, not
the inputs**: `F/A/C/pending/OPEN[]` are Claude's in-session tallies, so a
the inputs**: `F/A/C/D/pending/OPEN[]` are Claude's in-session tallies, so a
miscount still feeds a wrong reason in (garbage-in) — the script can't make a
judged count reproducible, only the branch logic over it.
**`design-only` (0.5.1, via `--defects D`):** design suggestions are subjective
and self-spawning (each applied simplification surfaces a fresh one), so a
defect/design-blind tally ran the loop to the cap on them. The loop now
converges once no *defect* finding remains — design is advisory and never holds
it open; `--pending` is defect-scoped for the same reason. Omitting `--defects`
disables the reason (legacy callers see the original four). **Accepted residual
(the branch's own self-review flagged it):** like `cap`, design-only fires
BEFORE the round's re-review, so this round's design fixes are applied but not
re-reviewed — a simplification could introduce a defect the loop never catches.
Forcing a re-review would re-open the very churn design-only closes (design
findings diverge), so the close-out instead flags the residual and recommends a
fresh `/swarm:review` over the result.
- Loop mechanics mirror pr-flow `/cycle` run locally (no push / no `@claude`
poll); the `Status` column (🔧/⏭️/🔁) and stable `#` across rounds come from
the report table contract this entry defines above (P2 reserved them).
Expand Down Expand Up @@ -211,7 +242,15 @@ filled* — `gh pr diff <n>` (bare `--pr` resolves the current branch's PR via
0.5.0 moved one more rule from prose to code the same way: rows carry optional
`kind`/`lens`, and the script orders defect rows before design rows and
prefixes design finding cells with `[lens]` — the caller passes findings
through verbatim, never hand-orders or hand-prefixes.
through verbatim, never hand-orders or hand-prefixes. 0.5.1 added an
**idempotency guard**: the finder's summary may already carry its own
`[lens]` self-tag (the workflow doesn't strip it, and a merge can leave a
*different* design lens as the representative), so prefixing unconditionally
posted `[reuse] [reuse] …` / `[reuse] [simplification] …`. If the cell already
opens with a known design-lens tag (`DESIGN_LENS_TAGS`, sync-checked by
`test_lens_sync.py`) it's kept as-is. Note it is **not** a kind fallback (the
retired `DESIGN_LENSES` was): it runs only inside the already-`kind`-decided
design branch and never moves a row between tables.

## Future idea (P3+): per-cluster external prompts

Expand Down
7 changes: 7 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -193,6 +193,13 @@ entries are grouped per plugin, newest first.

## swarm

### 0.5.1 — 2026-07-18
- Make `--fix` re-confirm design-aware: a `kind: "design"` finding has no line-local defect to re-find, so an agreed (✅) design fix was silently reported skipped-stale and never applied. Step 1 now branches on kind — for design findings it re-confirms the suggestion still applies (reuse target / duplication / simpler form / waste still present), only skipping when that target is genuinely gone.
- Make `--loop` converge on design churn: the tally made no defect/design distinction, so once only design suggestions remained (each applied simplification spawning a fresh one) it ran to the cap. `loop-closeout.py step` gains `--defects D` and a new `design-only` reason (fixed order: after no-change, before cap); `--pending` is now defect-scoped (design never holds the loop open). Omitting `--defects` disables the reason (legacy callers unaffected).
- Guard `pr-post.py` against a design-row `[lens]` double-prefix (`[reuse] [reuse] …`): if the finding cell already opens with a known design-lens self-tag (spaced or not, matching the workflow tag parser), keep it and don't add a second (`DESIGN_LENS_TAGS`, sync-checked by `test_lens_sync.py`; not a kind fallback). Untagged findings deliberately stay `kind: "defect"` (the safe bucket): a design finder may report a real off-lens bug, and inferring `design` from cluster homogeneity would route that bug to applicability verify (wrong rubric) and out of the `--loop` defect tally — dropping a bug is worse than mis-filing a suggestion.
- Show the lens in the in-session Design table too: design rows now carry a `[lens]` `Befund` prefix, matching the PR-comment path (both surfaces read identically).
- Accept the cluster-default's loss of per-lens failure isolation as a documented tradeoff (one crashed cluster finder drops that cluster's Claude coverage as a visible `backendError`; `--max` restores per-lens isolation).

### 0.5.0 — 2026-07-17
- Grow the review lens set from 5 to 11 (all default-on): methodological `removed-behavior` + `cross-file-trace` (factual, normal verify) and design-quality `reuse` / `simplification` / `efficiency` / `altitude` (suggestion-shaped, `kind: "design"`).
- Organize the lenses into 4 clusters (`LENS_CLUSTERS` — single source of truth): breakage / threat / design / consistency. Claude fan-out runs one finder per cluster by default (≤4 agents); `--max` splits to one finder per lens (≤11). The gate still prunes per-lens.
Expand Down
2 changes: 1 addition & 1 deletion plugins/swarm/.claude-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "swarm",
"description": "Local mixture-of-agents code review for Claude Code. Fans a diff across Claude lenses plus the codex and grok CLIs (grok-4.5), merges by mechanism with cross-family consensus, verifies solo findings and all design suggestions, and presents one ranked report. Optional --fix / --loop applies the findings you agreed with; --pr reviews a GitHub PR diff and posts the result. Skills: /swarm:review, /swarm:agents.",
"version": "0.5.0",
"version": "0.5.1",
"author": {
"name": "gering"
},
Expand Down
59 changes: 44 additions & 15 deletions plugins/swarm/scripts/loop-closeout.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,17 +16,22 @@
there is no working-directory footgun.

Usage:
loop-closeout.py step --round R --cap N --findings F --agreed A --changed C [--pending P]
-> prints `continue` or `terminate=<reason>` (one of the 4 reasons).
--pending P = agreed findings still awaiting a user decision (default 0);
loop-closeout.py step --round R --cap N --findings F --agreed A --changed C [--defects D] [--pending P]
-> prints `continue` or `terminate=<reason>` (one of the 5 reasons).
--defects D = defect-kind findings this round (design suggestions excluded);
when given, the loop converges via `design-only` once no defects remain, so
subjective design churn (each applied simplification spawning a fresh one)
can't run the loop to the cap. Omit to disable that reason (legacy callers).
--pending P = *defect* findings still awaiting a user decision (default 0);
P > 0 keeps the loop alive past every convergence reason except the cap.
Design needs-decision does NOT count here — design never holds the loop open.
On bad input it writes to stderr and exits non-zero with NO stdout token —
the caller must treat a non-zero exit as abort, not as `continue`.

loop-closeout.py box "15 7 4 4 5 4 3 1 1 0" --reason cap
-> prints the close-out box + the termination reason line.

Reasons: 0-findings | nothing-agreed | no-change | cap
Reasons: 0-findings | nothing-agreed | no-change | design-only | cap
"""
import argparse
import sys
Expand All @@ -37,6 +42,8 @@
"0-findings": "0 findings — converged clean",
"nothing-agreed": "nothing agreed — only disagreements (❌) left open",
"no-change": "no files changed — fixed point reached",
"design-only": "no defect findings remain — design tail is advisory; its fixes "
"were applied but NOT re-reviewed (re-run to confirm they're clean)",
"cap": "cap reached",
}

Expand All @@ -48,31 +55,50 @@ def cmd_step(a: argparse.Namespace) -> int:
1. review returned zero findings -> 0-findings (clean)
2. nothing was agreed (no ✅/🟨 this round) -> nothing-agreed
3. no files changed AND nothing is pending -> no-change (fixed point)
4. this was the last allowed round -> cap
4. no DEFECT findings remain (design only) -> design-only (advisory tail)
5. this was the last allowed round -> cap
Otherwise: continue.

Any agreed finding still awaiting a user decision (`--pending` > 0) keeps the
loop alive: it suppresses ALL THREE convergence reasons (0-findings,
nothing-agreed, no-change), because none of them is a true fixed point while a
choice is still owed. Only `cap` — the safety stop — can still fire with a
decision pending. Inputs are range-checked so a mis-parsed flag (e.g.
`--cap 0`) fails loudly instead of silently collapsing the loop.
`design-only` (only when `--defects` is given) is what stops the loop from
running to the cap on subjective design churn: design suggestions are
advisory, and each applied simplification can spawn a fresh one, so once no
defect-kind finding remains the loop has converged on the part that matters —
the design tail doesn't hold it open. Omitting `--defects` disables this
reason (legacy behavior: only the other four fire). Like `cap`, it fires
BEFORE the round's re-review, so this round's design fixes were applied but
NOT re-reviewed — a simplification could have introduced a defect this round
never catches. Forcing a re-review instead would re-open the churn this reason
exists to close (design findings diverge), so the caller must flag the residual
and recommend a fresh review over the result (see the SKILL close-out).

Any DEFECT finding still awaiting a user decision (`--pending` > 0) keeps the
loop alive: it suppresses ALL FOUR convergence reasons (0-findings,
nothing-agreed, no-change, design-only), because none of them is a true fixed
point while a defect choice is still owed. Only `cap` — the safety stop — can
still fire with a decision pending. A *design* needs-decision is NOT passed as
pending (design never holds the loop). Inputs are range-checked so a mis-parsed
flag (e.g. `--cap 0`) fails loudly instead of silently collapsing the loop.
"""
for name, val, lo in (
checks = [
("--cap", a.cap, 1), ("--round", a.round, 0), ("--findings", a.findings, 0),
("--agreed", a.agreed, 0), ("--changed", a.changed, 0), ("--pending", a.pending, 0),
):
]
if a.defects is not None:
checks.append(("--defects", a.defects, 0))
for name, val, lo in checks:
if val < lo:
print(f"loop-closeout: {name} must be >= {lo} (got {val})", file=sys.stderr)
return 2

converged = a.pending <= 0 # a pending decision is never a fixed point
converged = a.pending <= 0 # a pending defect decision is never a fixed point
if converged and a.findings <= 0:
print("terminate=0-findings")
elif converged and a.agreed <= 0:
print("terminate=nothing-agreed")
elif converged and a.changed <= 0:
print("terminate=no-change")
elif converged and a.defects is not None and a.defects <= 0:
print("terminate=design-only")
elif a.round + 1 >= a.cap:
print("terminate=cap")
else:
Expand Down Expand Up @@ -133,8 +159,11 @@ def main() -> int:
s.add_argument("--findings", type=int, required=True, help="findings this round")
s.add_argument("--agreed", type=int, required=True, help="✅+🟨 findings this round")
s.add_argument("--changed", type=int, required=True, help="files changed this round")
s.add_argument("--defects", type=int, default=None,
help="defect-kind findings this round (design excluded); enables the "
"design-only reason. Omit to disable it (legacy).")
s.add_argument("--pending", type=int, default=0,
help="agreed findings still awaiting a user decision (default 0)")
help="DEFECT findings still awaiting a user decision (default 0)")
s.set_defaults(func=cmd_step)

b = sub.add_parser("box", help="render the OPEN-findings close-out box")
Expand Down
32 changes: 30 additions & 2 deletions plugins/swarm/scripts/pr-post.py
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,23 @@ def _safe_pr_num(pr_num):
return sanitize_prose(pr_num)


# Design-cluster lens names, mirrored from swarm-review.js LENS_CLUSTERS.design
# (kept in sync by test_lens_sync.py). NOT a kind fallback (the retired
# DESIGN_LENSES was — bucketing keys on `kind` ALONE, see _row_kind): this runs
# only INSIDE the already-decided design branch, purely to recognize a "[lens] "
# self-tag the finder may already have baked into the summary, so we don't add a
# second prefix. It can never move a row between the defect/design tables.
DESIGN_LENS_TAGS = ("reuse", "simplification", "efficiency", "altitude")
# Match a leading "[lens]" tag the SAME way the workflow's own parser does
# (/^\s*\[([\w-]+)\]/) — no trailing-whitespace requirement. Requiring a space
# after "]" would miss a valid but unspaced self-tag ("[reuse]ExtractHelper" or a
# bare "[reuse]"), which the workflow accepts as tagged, and re-prefix it to
# "[reuse] [reuse]…" — the exact double-prefix this guard exists to prevent.
_DESIGN_PREFIX_RE = re.compile(
r"\s*\[(" + "|".join(DESIGN_LENS_TAGS) + r")\]", re.IGNORECASE
)


def _row_kind(r: dict) -> str:
"""Row kind. 'design' ONLY when explicitly tagged `kind: "design"`; anything
else — including a missing/junk kind — is a defect, the safe bucket. The lens
Expand Down Expand Up @@ -258,10 +275,21 @@ def render_body(data: dict) -> str:
# sanitizing: the brackets come out entity-encoded like any other
# cell content and render back as literal [lens] — an untrusted
# lens value gets the full sanitizer, same as the finding text.
# Idempotency guard: the workflow doesn't strip a finder's own
# "[lens] " self-tag from the summary, and a merge can leave the
# representative summary tagged with a DIFFERENT design lens than the
# cluster's resolved `lens`. Prefixing unconditionally would post
# "[reuse] [reuse] …" (or "[reuse] [simplification] …"). If the cell
# already opens with a known design-lens tag, keep it and don't add a
# second one.
befund = r.get("befund")
if _row_kind(r) == "design":
lens = str(r.get("lens") or "").strip() or "design"
befund = f"[{lens}] {'' if befund is None else befund}".rstrip()
body = "" if befund is None else str(befund)
if _DESIGN_PREFIX_RE.match(body):
befund = body # already self-tagged — leave it
else:
lens = str(r.get("lens") or "").strip() or "design"
befund = f"[{lens}] {body}".rstrip()
cells = [
sanitize_prose(r.get("num", "")),
sanitize_prose(r.get("sev", "")),
Expand Down
Loading
Loading