Skip to content

Fan swarm's external voices out per lens cluster (0.7.0) - #50

Merged
gering merged 7 commits into
mainfrom
task/swarm-per-lens-externals
Jul 31, 2026
Merged

Fan swarm's external voices out per lens cluster (0.7.0)#50
gering merged 7 commits into
mainfrom
task/swarm-per-lens-externals

Conversation

@gering

@gering gering commented Jul 27, 2026

Copy link
Copy Markdown
Owner

Summary

  • codex and grok no longer run one broad multi-lens review each — every voice now fans out over the same gated lens clusters as the Claude finders (one call per cluster; per lens under --max). The gate prunes calls for everyone: a fully-gated-out cluster spawns nothing for any voice.
  • Each finding's [lens] tag becomes authoritative — the voice is its cluster, instead of a broad prompt self-tagging. LENS_BRIEF becomes genuinely single-source: the SKILL.md external-prompt HDR drops its hand-mirrored lens list.
  • Because the gate now prunes for every voice, a lens it wrongly drops would be reviewed by nobody — the old full-width external calls used to absorb that. A MANDATORY_LENSES floor (security, adversarial, correctness) is the code-level backstop, and the report's coverage line now partitions the full lens set.
  • Cost: live-backends × units external calls — ≤2×4 by default, ≤2×11 under --max — logged at fan-out, never silently capped.

Changes

Fan-out topology (workflows/swarm-review.js)

  • unitsFor() is the shared granularity ladder for both sides; externals reuse finderUnits whenever a gate ran, so the two cannot drift.
  • One unitBrief() feeds both the Claude finder prompt and the external lensInstr().
  • MANDATORY_LENSES floor; gate.run/gate.skip are rewritten so together they partition CANDIDATE_LENSES (a mismatch logs a warning rather than misreporting).
  • backendErrors carry the unit + its lenses — every backend is multi-voice now, so "codex errored" alone hid which cluster lost coverage.
  • LENS_BRIEF guard rejects apostrophes, newlines, tabs and control chars (the transport command must stay one line).

Adapter (scripts/agents.sh)

  • New run --lens-instr <s>: prepends the workflow-supplied cluster briefs to the fenced-diff prompt in deterministic shell — the same "assembly is never an LLM step" contract the diff fencing follows, and backend-agnostic, so a future voice inherits per-cluster prompts for free.
  • A present-but-empty value is refused rather than silently running a lens-free review whose findings the workflow would still label with the cluster's lenses.
  • Size messages render the limit from max_bytes instead of a hardcoded string.

Skill + tests + docs

  • --max documented as splitting every voice per lens (≤22 external calls), not just Claude; the external HDR is now lens- and kind-agnostic.
  • test_lens_sync.py: the SKILL mirror check flips to a negative assertion, the --lens-instr wiring is pinned to the actual argument construction, and the oversize headroom is checked against max_bytes with the worst-case instruction derived from the source template (a Python copy would have gone stale silently).
  • Version 0.6.0 → 0.7.0, CHANGELOG, both READMEs, blueprint, and the swarm-review-pipeline knowledge entry.

Readiness

  • ✅ README updated (root + plugin)
  • ✅ Version bumped 0.6.0 → 0.7.0, plugin.json + marketplace.json in sync
  • ✅ CHANGELOG entry added
  • ✅ Knowledge entry updated (swarm-review-pipeline.md)
  • check-structure.py — 0 errors
  • ✅ 9/9 plugin tests pass (incl. other plugins, no regressions)
  • ✅ shellcheck — no new findings
  • ➖ Build N/A (build-less repo)

Test plan

Verified by two real ensemble runs against this branch's own diff:

  • Full ensemble (12 voices): logged External fan-out: 8 call(s) — codex + grok × 4 cluster(es); 0 backend errors, so the ~1 KB shell-quoted --lens-instr round-tripped through every transport agent; no unspecified lens tags in rawPerLens; 8 cross-family consensus clusters.
  • External-only control (claude: false, 8 voices): confirmed the no-gate path falls back to full-width CANDIDATE_LENSES; 0 backend errors again (16/16 calls clean across both runs).
  • Both runs' agreed findings were applied — the control run caught a real regression in the floor's first version (gate.run not rewritten, so a floored-in lens ran but appeared in neither report column).
  • Adapter guards exercised directly: oversize-with-instruction, empty --lens-instr, missing flag value.
  • Headroom test proven to fail on real drift (a 5 KiB block added to unitBrief()6036 B > 4096 B).
  • Reviewer: sanity-check the MANDATORY_LENSES cost tradeoff — flooring correctness pins breakage + threat always-on, leaving the gate only design/consistency to prune.

🤖 Generated with Claude Code

https://claude.ai/code/session_01YWqcx14tMsuT7ptmywsMEQ

@gering

gering commented Jul 28, 2026

Copy link
Copy Markdown
Owner Author

@claude review

gering and others added 7 commits July 31, 2026 15:24
codex and grok no longer run one broad multi-lens review each — they fan
out over the SAME gated lens clusters as the Claude finders (one call per
cluster; per lens under --max). The gate now prunes calls for everyone: a
fully-gated-out cluster spawns nothing for any voice.

- New adapter flag `agents.sh run --lens-instr <s>` prepends the
  workflow-supplied cluster briefs to the fenced-diff prompt in
  deterministic shell — the same "assembly is never an LLM step" contract
  the diff fencing follows, and backend-agnostic, so a future voice
  inherits per-cluster prompts for free.
- LENS_BRIEF becomes single-source in swarm-review.js: the SKILL.md
  external-prompt HDR drops its hand-mirrored lens list. test_lens_sync.py
  flips that mirror check to a negative one and pins the --lens-instr
  wiring on both sides.
- Lens tags become authoritative (the voice IS its cluster); an untagged
  finding from a single-lens external unit now resolves to that lens
  instead of falling back to 'unspecified'.
- --max lifts every voice to per-lens granularity (previously Claude
  only). A claude:false control run keeps full-width external coverage.
- Cost is logged at fan-out (live-backends × units, <=2x4 default,
  <=2x11 under --max) and never silently capped; an all-pruned gate logs
  why no external call ran.
- Skill oversize threshold drops to 118784 B (4 KiB under the adapter cap)
  since exec now sees instruction + diff; briefs are asserted
  apostrophe-free and shell-quoted for the transport command.

Grok needs no diff-only brief variant: since 0.6.0 both externals read
project files, so both get the full cluster briefs.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YWqcx14tMsuT7ptmywsMEQ
The 0.7.0 ensemble reviewed its own diff (12 voices, 17 findings). Applying
the agreed ones:

Gate blast radius (the critical finding). With externals gated too, a lens the
low-effort haiku gate drops is now reviewed by NOBODY — the full-width external
calls used to absorb a mis-gate, and the gate's only other protection is a
sentence in its own prompt, i.e. injection-reachable via the diff it classifies.
Add MANDATORY_LENSES (security, adversarial) as a code-level floor the gate
cannot prune. Also materialize lenses the gate lists in neither `run` nor `skip`
into `skip`, so the report can't under-report what was pruned — the self-review
run silently dropped `adversarial` exactly this way.

- agents.sh: reject a present-but-EMPTY --lens-instr instead of silently running
  a lens-free review whose findings the workflow still labels with the cluster's
  lenses (mislabeled coverage is worse than a hard error). An omitted flag stays
  legal for ad-hoc calls. Self-contained usage text; size messages render the
  limit from max_bytes instead of a hardcoded "120 KiB".
- One unitBrief() now feeds both the Claude finder prompt and lensInstr(); the
  externals reuse finderUnits whenever a gate ran, so the two sides cannot drift.
- backendErrors carry the unit + its lenses: every backend is multi-voice now, so
  "codex errored" alone hid WHICH cluster lost coverage.
- LENS_BRIEF guard also rejects newlines/tabs/control chars — the transport
  command's one-line property depended on it.
- SKILL.md: --max documented as splitting EVERY voice per lens (<=22 external
  calls), not just Claude; the external HDR is now lens- AND kind-agnostic, so a
  defect-only cluster is no longer invited to report design improvements.
- test_lens_sync.py pins the skill's oversize threshold against the adapter's
  max_bytes and the largest instruction the briefs can produce.
- Stale docs corrected: the workflow's drift warning and gate comment, the
  plugin.json description (now equal to marketplace.json), the README adapter
  synopsis, and the knowledge entry (per-cluster externals shipped, with the
  assembly/floor rationale recorded).

Not applied: passing the instruction as a file payload (nobody can write that
file — the workflow sandbox has no FS and the skill's prep runs before the gate
exists; handing the write to the transport agent is the rejected LLM-assembly
option), and keeping one external voice ungated (that would undo the cost
control the per-cluster split exists for).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YWqcx14tMsuT7ptmywsMEQ
A codex+grok-only run (claude:false, 8 voices) reviewed the previous commit and
caught a real bug in its own mandatory-lens floor, plus test weaknesses:

- gate.run was still the gate's RAW pick after the floor re-added a lens, while
  gate.skip had the floored entries removed — so a lens that actually ran
  appeared in NEITHER column of the report's `Lenses: … — gated-out: …` line.
  Write the floored set back to gate.run, and assert run+skip partition
  CANDIDATE_LENSES (a mismatch logs a warning rather than misreporting silently).
- test_lens_sync.py derived its worst-case lens instruction from a Python COPY of
  unitBrief()/lensInstr(). A sentence added to the JS template would have left the
  copy stale and the headroom check green while real calls overflowed the adapter
  cap. It now derives the fixed prose from the source template literals (${...}
  stripped); verified byte-exact, and a 5 KiB block added to unitBrief() now fails
  the check instead of passing it.
- The --lens-instr wiring assertion matched the bare string, which the new
  comments satisfy on their own; it now matches the actual argument construction.
- Stale comment on the claude:false branch ("external voices review in full
  regardless" — they inherit the gated set since 0.7.0) and on the LENS_BRIEF
  loop (the SKILL mirror check is negative now). Restored the finder prompt's
  colon→bullets order that the unitBrief refactor split.

Not applied: widening MANDATORY_LENSES to correctness (would pin 2 of 4 clusters
always-on and eat the cost control the per-cluster split exists for — the
coverage line now reports every pruned lens, so the tradeoff is visible), and a
content policy for --lens-instr in the adapter (its value is trusted caller
input; anyone who can set it already controls the whole argv).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YWqcx14tMsuT7ptmywsMEQ
- MANDATORY_LENSES gains `correctness` (user call): the gate prunes for every
  voice now, so the lens class most costly to miss must not depend on a
  haiku/effort-low call that reads the untrusted diff it classifies. Accepted
  cost, stated in code and CHANGELOG: `breakage` and `threat` are pinned
  always-on, leaving the gate only `design`/`consistency` to prune — a doc-only
  diff still pays 2 clusters × live voices (9 voices worst case, 6 if the gate
  returns nothing at all).
- /swarm:review description 33 → 29 words, back inside the activation-surface
  budget check-structure.py enforces.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YWqcx14tMsuT7ptmywsMEQ
Third ensemble round (codex+grok only, 8 voices) over the full branch diff —
first external look at the mandatory floor itself. Two cross-family findings hit
the previous commit's own hardening:

- MANDATORY_LENSES is an explicit list, i.e. a MIRROR: renaming a lens in
  LENS_CLUSTERS would leave a stale entry that can never match, silently voiding
  the floor while every check stayed green. Assert the subset at startup and pin
  it in test_lens_sync.py (proven: a rename now fails CI). Kept explicit rather
  than derived from LENS_CLUSTERS.threat — which lenses are non-negotiable is a
  judgement call, not a consequence of cluster membership.
- The gate coverage rewrite copied the gate's `skip` entries verbatim, so a
  hallucinated lens name ("typo") would render as a gated-out lens that does not
  exist. Filter skip to known lenses and assert run/skip stay disjoint as well as
  complete.

- New `--lens-instr-bytes`: the empty-value check only caught a TOTAL loss, so a
  transport that shortened or paraphrased the instruction still ran and had its
  findings attributed to lenses the backend was never told to review — quietly
  hollowing out "the voice IS its cluster". The caller declares the byte count it
  built; a mismatch is a hard error. Length is computed in pure JS (the workflow
  sandbox has no Buffer/TextEncoder; verified to match Buffer.byteLength on
  multi-byte input) and memoized so command and count describe one build.
- Corrected an overstatement I wrote: the floor guarantees cluster SPAWN, not
  lens coverage — within `breakage` the gate may still prune removed-behavior /
  cross-file-trace, leaving that unit at lenses:['correctness'] for every voice.
  Disclosed via gated-out, but "breakage ran" != "deletions were reviewed".
- Stale docs: README gate bullet (Claude-only pruning, skippable security),
  knowledge isolation paragraph (per-voice now), miscounted list intro.
- Recorded the two repeatedly-refound-but-declined items (prose oversize gate,
  per-unit grok probe) as accepted residuals with reasons, so later rounds see
  them as decided rather than overlooked.

Not applied: widening the floor to the methodological lenses — that is the same
cost/coverage tradeoff the user already ruled on once, and the pruned lenses are
reported.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YWqcx14tMsuT7ptmywsMEQ
Fourth ensemble round (codex+grok, 8 voices) — first external look at the
integrity check added last round, and it took it apart correctly:

- The byte-length check could not do what its comment claimed. "Any edit changes
  the length" is false: `security` → `altitude` and `ONLY` → `ALSO` are
  same-length swaps that change the review scope while the count still matches,
  so a garbled retype would run and still be credited as a threat review.
  Replaced by `--lens-instr-sum`, an FNV-1a/32 over the instruction's UTF-8
  bytes — pure JS in the workflow (the sandbox has no Buffer/crypto/TextEncoder),
  recomputed in python3 by the adapter, verified bit-identical across ASCII,
  umlauts, em dashes and emoji. Non-cryptographic on purpose: the threat is an
  LLM mangling a retype, not a collision search.
- The check was also opt-out by omission — dropping `--lens-instr-bytes` while
  truncating the instruction skipped it entirely. The checksum is now REQUIRED
  whenever `--lens-instr` is present; only a fully lens-free ad-hoc call stays
  legal. Both holes covered end-to-end (same-length swap, missing checksum,
  malformed checksum, ad-hoc call).
- Oversize skip is now decided in the prep Bash (`EXTERNALS_OVERSIZE`) rather
  than by model arithmetic — the skill's own `--pr`/`--fix` guard already sets
  that pattern, and a compaction could otherwise let live voices through and turn
  one clean skip into N per-call backend errors. test_lens_sync.py now pins the
  threshold from that executable guard, not the prose.
- The gate prompt named only `security` as non-skippable while the code floor
  holds three lenses; it now interpolates MANDATORY_LENSES (one source).
- Fan-out error log names the failing UNIT, not just the backend.
- Stale docs: knowledge said externals are diff-only (0.6.0 gave them file-read
  + web; the residual reason design clusters are verified is correlation, not
  blindness), README adapter block missed the new flag.

Not applied: caching grok's per-unit model probe — the calls run in parallel, so
the cost is redundant network work, not wall-clock, and a cache adds staleness.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YWqcx14tMsuT7ptmywsMEQ
Fifth ensemble round (codex+grok, 8 voices) — 4 findings, down from 9, and two
were doc residue from my own last commit:

- The FNV-1a/32 checksum is implemented twice (hand-rolled JS in the workflow,
  python3 in the adapter) and nothing tested that they AGREE — the existing
  checks only grepped for the flags. A one-sided edit would make every external
  call fail its own integrity check: exit 2 per unit, the whole external half of
  the ensemble collapsing into backendErrors while Claude still runs. Both sides
  are now run against a reference implementation of the FNV-1a spec, using their
  real source (the adapter's own inlined python snippet, the workflow's extracted
  function under node) rather than copies. Mutation-tested: changed prime, dropped
  zero-pad, broken 3-byte UTF-8 path, changed offset basis and dropped overflow
  mask are all caught.
  The zero-pad case needed a deliberate vector — "f8" hashes to 0d226273, the
  only one of the set with a leading zero. Without it a dropped `padStart` passed
  while the two sides genuinely disagreed on short hashes.
- CHANGELOG kept both the superseded byte-count bullet and its replacement, so
  0.7.0 documented a `--lens-instr-bytes` flag the adapter does not implement.
  Replaced with the gate-floor/coverage-line entry it should have been.
- Knowledge heading still said "guarded by length, not trust" while its own body
  and the code describe the content checksum.

Not applied: replacing the argv transport with a file payload (precluded — the
workflow sandbox cannot write and the prep runs before the gate exists; the
verifier reached the same conclusion), and resolving grok's model probe once per
backend instead of per unit — the calls run in parallel, so it costs redundant
network round-trips, not wall-clock. Standing accepted residual.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01YWqcx14tMsuT7ptmywsMEQ
@gering
gering force-pushed the task/swarm-per-lens-externals branch from 1eceaec to a661e40 Compare July 31, 2026 13:25
@gering
gering merged commit d109303 into main Jul 31, 2026
1 check passed
@gering
gering deleted the task/swarm-per-lens-externals branch July 31, 2026 13:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant