diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index bd8a07a..e0adbbe 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -29,8 +29,8 @@ { "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 + composer models), merges by mechanism with cross-family consensus, verifies solo findings, 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.4.2" + "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 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.4.3" } ] } diff --git a/.claude/knowledge/_index.md b/.claude/knowledge/_index.md index 9074bbd..e4e9e89 100644 --- a/.claude/knowledge/_index.md +++ b/.claude/knowledge/_index.md @@ -14,7 +14,7 @@ - `features/herdr-close-automation.md` — `/close` in herdr: cwd-tab teardown, plugin SessionEnd hook, the one TUI-exit primitive, detached self-exit onto idle - `features/herdr-tab-glyphs.md` — Task-state glyphs (`○ ● ◇ ◆ ✓`) on herdr tab names: `states` mode inside the self-contained renderer (no sibling), sync-vs-`--cached` PR refresh per caller, exact-cwd rename rules, soft pr-flow shim - `features/task-archiving-on-close.md` — `/close` archives (not deletes) the task file; adaptive commit + ff-push to main -- `features/swarm-backend-adapter.md` — Verified codex/grok CLI facts (schema-enforced JSON, effort mapping, stdin hang, composer defensive-parse voice) behind `swarm`'s adapter script +- `features/swarm-backend-adapter.md` — Verified codex/grok CLI facts (schema-enforced JSON, effort mapping, stdin hang, model-aware grok readiness) behind `swarm`'s adapter script - `features/swarm-review-pipeline.md` — `/swarm:review` P2 pipeline (skill↔Workflow wiring, family-consensus, minimal security) + P5 `--fix`/`--loop` (in-session orchestration between workflow runs; deterministic termination + close-out box in `loop-closeout.py`) + `--pr` (review a PR diff, post via `gh` under the user's identity so pr-flow's `claude`-author poll ignores it; publish path is deterministic `pr-post.py`: per-cell sanitizer + stale-head gate + post) ## Deployment diff --git a/.claude/knowledge/features/swarm-backend-adapter.md b/.claude/knowledge/features/swarm-backend-adapter.md index c852176..ab3923a 100644 --- a/.claude/knowledge/features/swarm-backend-adapter.md +++ b/.claude/knowledge/features/swarm-backend-adapter.md @@ -1,9 +1,9 @@ --- title: "Swarm Backend Adapter Layer" createdAt: 2026-07-03 -updatedAt: 2026-07-15 +updatedAt: 2026-07-17 createdFrom: "PR #21" -updatedFrom: "session: 2026-07-15" +updatedFrom: "PR #37" pluginVersion: 1.8.2 prime: false reindexedAt: 2026-07-12 @@ -32,11 +32,13 @@ debugging round. `--output-last-message ` (stdout carries the agent transcript, stderr the progress log); grok prints a response **envelope** on stdout — the validated object is its `.structuredOutput` field. -- **The adapter pins `-m grok-4.5`** — the schema-capable model. grok 0.2.101 - renamed it from `grok-build` (same upstream pin-rename class as codex's - `gpt-5.6-terra`; verified drop-in: identical envelope/`structuredOutput` - shape, `--single` unchanged). `grok-composer-2.5-fast` still rejects - `--effort` ("does not support parameter reasoningEffort"). +- **The adapter pins `-m grok-4.5`** — the schema-capable model, and since + swarm 0.4.3 the *only* grok model it supports. grok 0.2.101 renamed it from + `grok-build` (same upstream pin-rename class as codex's `gpt-5.6-terra`; + verified drop-in: identical envelope/`structuredOutput` shape, `--single` + unchanged). Any other `--model` is preflight-rejected with a usage error — + only grok-4.5 enforces `--json-schema`, and an unlisted model fails late with + `structuredOutput: null` after burning a full review. - **Effort ladders**: grok is `low|medium|high` since 0.2.101 (the `max` tier is gone) → the adapter maps `xhigh`/`max`→`high`; codex has no `max` tier → map `max`→`xhigh` (`-c model_reasoning_effort=…`). Both mappings degrade a @@ -47,23 +49,90 @@ debugging round. The pipeline runs codex at `high` normally; the `--max` profile overrides both model and effort (`gpt-5.6-sol` @ `xhigh`) — see [swarm-review-pipeline](swarm-review-pipeline.md). -- **`grok-composer-2.5-fast` does not enforce `--json-schema`** — but it is - still usable as a second grok voice. Given a strict-JSON *prompt* (the adapter - appends the schema text and drops `--json-schema`/`--effort`), it emits **pure - `{"findings":[...]}` directly on stdout — no response envelope, no - `structuredOutput`** (verified P2, grok 0.2.82; simpler than grok-4.5's - envelope). The adapter routes `--model grok-composer-2.5-fast` to a separate - `run_grok_composer` path that parses the answer **defensively**: collect ALL - balanced `{...}` objects (whole string, fenced blocks, every `{` run), pick the - first **non-empty** `findings` object (a leading `{"note":…}`/`{"findings":[]}` - would otherwise mask the real one), and **validate every item against - `finding.schema.json`** (composer, unlike codex/grok-4.5, is not CLI-schema- - enforced — a malformed item must ERROR, not reach merge/verify). Both the - first-object bug and the missing per-item validation were caught by swarm - reviewing its own diff. composer is - ~2× faster than grok-4.5. It is same-family-correlated, so consumers must - count consensus by **model family**, not backend (composer + grok-4.5 - agreeing is one grok vote — see [swarm-review-pipeline](swarm-review-pipeline.md)). +- **Model-aware readiness beats an auth-only check** (swarm 0.4.3). grok drops + and renames models between releases — 0.2.101 removed + `grok-composer-2.5-fast`, which swarm had shipped as a second grok voice; the + auth-only probe kept reporting it Ready until it failed mid-review with + `Invalid params: "unknown model id"`. `ready`/`list` now also require + grok-4.5 in `grok models` (grok is the one backend with a usable model-list + command; codex has none, so its model is trusted). The gotchas, all live- + verified: + - **Parse the bullet list by SHAPE, not position — and match the id + SUBSTRING, not the field.** Lines read ` * grok-4.5 (default)`, but keying + on `$2` turns a reworded line (` * default: grok-4.5`) into garbage tokens + that read as "the model is gone" and fail closed. Matching whole + whitespace-fields starting `grok-` fixes that but still drags glued-on + punctuation along (`grok-4.5,`, `` `grok-4.5` ``, ANSI codes), which breaks + the exact-match the same way. Extract with a pattern ending on + alphanumerics (`grok-[A-Za-z0-9]+([._-][A-Za-z0-9]+)*`); a line with no + id-shaped token then contributes nothing, landing in the trust-auth degrade. + - **Match without a pipe to `grep -q`**: an early-exiting `grep -q` can + SIGPIPE the writer, and under `set -o pipefail` a *hit* would then report + failure. Newline-fence the list and use a `case` substring match. + - **An empty model list must NOT fail closed.** Offline, a timeout, or a + future CLI renaming the subcommand would otherwise silently drop grok from + every fan-out. Empty/unparseable → trust auth and let `run_grok` surface the + explicit error; a non-empty list *without* grok-4.5 → an honest "not ready" + plus an update-the-CLI hint. + - **A probe added to a local path must not make it hang — and must not lie + when it can't run.** `ready`/`list` were purely local (stat the auth file) + before this; the probe puts a network call in every `/swarm:agents` and + review start. With no coreutils `timeout`/`gtimeout` to bound it (stock + macOS), the probe is **skipped** rather than run uncapped, degrading to + trust-auth in ~25ms — but it **warns on stderr**, because a silent skip + would make the documented model-aware guarantee false on that host: the + same "promise that doesn't hold at runtime" bug the composer removal exists + to fix. + - **Route every degrade through ONE audible exit.** This one spot was fixed + across FIVE consecutive swarm rounds, each catching the previous round's + miss: (1) the no-timeout branch ran uncapped; (2) it was capped but skipped + *silently*, while the docs were tightened to "never silently"; (3) `|| true` + still swallowed a *failed* probe, so the strengthened promise was false on + two of three routes; (4) with warnings finally on every route, `rc` was only + read **when the list came back empty** — so a probe killed mid-stream with + partial output skipped the degrade entirely and its truncated list read as + "model gone" (update an already-current CLI); (5) the probe was routed + through the review jail, which dragged `_init_sandbox`'s python3 + profile-build into the local `ready`/`list` paths — where a missing python3 + then misreported as "grok models failed". The invariants that survive: + - If N routes end in the same degrade, they need **one shared exit** + (`_probe_degraded`), not N hand-written warnings. + - **Check `rc` before the output, and discard partial output.** Only a + clean exit is an answer; anything else is a degrade. + - "The probe answered honestly" (model genuinely gone → `not ready` + + update-the-CLI hint) must stay distinguishable from "the probe never + answered" (→ trust auth + warning). + The meta-lesson (the one that actually ended the loop): **a fix that keeps + coming back is a shape problem, not a patch problem.** Rounds 1–4 patched a + probe that had a security jail bolted on; round 5 deleted the jail instead, + and the findings stopped. The composer removal itself — the PR's actual + subject — drew zero findings across all five rounds. When a *feature you + added to be safe* generates every round's bugs, cutting it beats hardening + it. + - **A readiness check is not a review — don't jail it.** The probe was first + built to go through `sandboxed()` (env-secret filter + sandbox-exec + read-deny jail), by analogy to the `run` calls. Wrong analogy: `sandboxed()` + exists because a *review* feeds grok the untrusted diff; a readiness check + passes **no** untrusted input, exactly like the sibling `codex login status` + a few lines away, which is also unjailed. Routing it through the jail bought + nothing and cost a hard python3 dependency on the formerly-local `ready`/ + `list` paths (plus a shared-warning bug and a cross-backend memo bug — all + three vanished when the jail came back out). The probe runs grok directly. + - **Bound it with `timeout -k`, its own knob.** `SWARM_TIMEOUT` caps a + *review* (600s, `0` disables) — useless for a probe that `list` blocks on; + `SWARM_PROBE_TIMEOUT` (10s) is separate and a malformed/`0` value falls back + to 10, never uncapped. Plain `timeout` only SIGTERMs, so a grok that ignores + SIGTERM (or forks a stdout-inheriting child) keeps the `$(...)` substitution + blocking past the deadline — the "must never hang" hole. `-k ` sends + SIGKILL after the grace period; treat both rc 124 (SIGTERM) and 137 + (SIGKILL) as "timed out". + - **Memoize by call convention, not by wishing.** `list="$(grok_model_list)"` + runs the function in a *subshell*, so its cache-global assignments vanish + and every caller silently re-pays the network call. The cache only works if + the fetch is called directly and callers read the global + (`grok_model_fetch; local list="$_grok_models"`). + This mirrors work-system's `agent-registry.sh`, which learned the same lesson + at task-launch time. - **Headless tool execution**: both CLIs run read-only commands (e.g. `git diff`) without extra approval flags — codex inside `-s read-only` sandbox, grok headless `-p` auto-approves read-only tools. So lens prompts diff --git a/.claude/knowledge/features/swarm-review-pipeline.md b/.claude/knowledge/features/swarm-review-pipeline.md index 0debcf3..a66824f 100644 --- a/.claude/knowledge/features/swarm-review-pipeline.md +++ b/.claude/knowledge/features/swarm-review-pipeline.md @@ -1,9 +1,9 @@ --- title: "Swarm Review Pipeline (/swarm:review)" createdAt: 2026-07-08 -updatedAt: 2026-07-15 +updatedAt: 2026-07-17 createdFrom: "PR #24" -updatedFrom: "session: 2026-07-15" +updatedFrom: "PR #37" pluginVersion: 1.8.2 prime: false reindexedAt: 2026-07-12 @@ -13,9 +13,11 @@ reindexedAt: 2026-07-12 P2 turns the blueprint into a working review: a **Workflow-tool script** (`plugins/swarm/workflows/swarm-review.js`) launched by the `/swarm:review` -skill. Shape: `scope+gate → fan-out (4 voices) → merge (file,mechanism) → -verify solos → output-gated synthesis`. Four voices: Claude lenses ∥ codex ∥ -grok-4.5 ∥ composer (see [swarm-backend-adapter](swarm-backend-adapter.md)). +skill. Shape: `scope+gate → fan-out (3 voices) → merge (file,mechanism) → +verify solos → output-gated synthesis`. Three voices: Claude lenses ∥ codex ∥ +grok-4.5 (see [swarm-backend-adapter](swarm-backend-adapter.md)). A fourth, +`grok-composer-2.5-fast`, was removed in swarm 0.4.3 — the grok CLI dropped the +model. ## The skill ↔ workflow wiring (the non-obvious parts) @@ -38,10 +40,16 @@ grok-4.5 ∥ composer (see [swarm-backend-adapter](swarm-backend-adapter.md)). ## Design decisions -- **Consensus counts model *families*, not backends.** A cross-family cluster +- **Consensus counts model *families*, not voices.** A cross-family cluster (≥2 of claude / openai / grok) is CONFIRMED without extra verify; everything - else is solo and goes through the adversarial 3-state verifier. composer + - grok-4.5 agreeing is one grok vote — they cannot alone mint consensus. + else is solo and goes through the adversarial 3-state verifier. The `FAMILY` + map (`workflows/swarm-review.js`) survives the composer removal deliberately: + with composer gone the backend→family mapping is 1:1, so *for consensus + counting* it is currently a no-op — but the fan-out is many-voices-per-family + (one Claude finder per gated lens), so the rule "same vendor agreeing with + itself is one vote, not a cross-check" is still the load-bearing invariant. + Collapsing consensus onto backends/voices would silently re-introduce + correlated self-agreement the day a second same-vendor voice returns. - **Security is intentionally minimal** (user directive: no cannons-at-sparrows). The P1 adapter floor stays (sandbox, tool-less grok, secret scrub, env filter, caps); P2 adds only three cheap things — **fencing** the diff as data @@ -60,8 +68,8 @@ grok-4.5 ∥ composer (see [swarm-backend-adapter](swarm-backend-adapter.md)). the delimiter); the workflow only collision-checks it against the returned findings and extends it deterministically (`nonce-1`, `-2`…) on collision. -- **`args.claude: false`** runs an **external-only control** (codex + grok-4.5 - + composer, no Claude finder lenses, no gate; merge/verify still in-session). +- **`args.claude: false`** runs an **external-only control** (codex + grok-4.5, + no Claude finder lenses, no gate; merge/verify still in-session). Proven useful: a control run found real bugs the with-Claude run missed (an `aws_secret_access_key` scrub-list drift, `git diff` omitting untracked files) — the "different models catch different defects" premise, live. @@ -95,9 +103,9 @@ the diff out of the script, above). Claude applies edits between rounds. the report table contract this entry defines above (P2 reserved them). - **`--max` profile** (`INPUT.max` in the workflow): lifts every voice to its ceiling — codex `gpt-5.6-sol`@`xhigh` (codex has NO `max` tier, xhigh is its - top), Claude finder lenses + verifier `xhigh`; gate/merge, grok (`high` is + top), Claude finder lenses + verifier `xhigh`; gate/merge and grok (`high` is its ceiling since grok 0.2.101 dropped `max` — it runs there on both - profiles) and composer (no effort control) unchanged. Orthogonal to `--fix`/`--loop`, + profiles) unchanged. Orthogonal to `--fix`/`--loop`, composes with both. The profile's live settings are verified end-to-end (`gpt-5.6-sol`@`xhigh` at wiring time; grok re-verified at `--effort high` on 0.2.101) — the "no silent fail on a non-existent model/effort" rule. @@ -170,7 +178,9 @@ call, more thinking) over N calls. Not a default. Real background runs on this branch: a Claude-only smoke run proved the wiring (6 agents, correct return shape); the review **found a real bug in its own -composer parser** (first-object-vs-findings-object), which was then fixed. Only +grok-composer parser** (first-object-vs-findings-object), which was then fixed +— that parser has since been removed with the composer backend (0.4.3), but it +stands as the proof that swarm can catch a genuine bug in its own diff. Only `REFUTED` solos are dropped; consensus/solo/refuted counts + per-lens raw→ surviving ship in the `balance` block the skill renders. Iterating a `/swarm:review` loop over the branch caught several fix-induced regressions diff --git a/CHANGELOG.md b/CHANGELOG.md index 1e75530..41d9140 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -185,6 +185,10 @@ entries are grouped per plugin, newest first. ## swarm +### 0.4.3 — 2026-07-17 +- Remove the `grok-composer-2.5-fast` backend: grok CLI 0.2.101 dropped the model, so the composer voice (adapter path, defensive parser, workflow voice, docs) failed at runtime. `grok-4.5` is now the only grok model, and the ensemble is three voices (Claude lenses + codex + grok-4.5). +- Make grok readiness model-aware: `ready`/`list` now require `grok-4.5` to appear in `grok models`, not just auth — so a dropped or renamed model reads as "not ready" with an actionable hint instead of failing mid-review. The probe runs unjailed like the sibling `codex login status` check (a readiness check passes no untrusted diff, so it needs no sandbox and stays free of the review path's python3 profile-build), bounded by `SWARM_PROBE_TIMEOUT` (10s, `timeout -k` so a SIGTERM-ignoring CLI can't hang it; separate from the review-length `SWARM_TIMEOUT`). It falls back to the auth check — with a warning on stderr, never silently — whenever it can't produce a clean answer (no coreutils `timeout`, non-zero exit, timeout, or an empty/unparseable list), rather than dropping grok from the fan-out. + ### 0.4.2 — 2026-07-15 - Fix grok CLI 0.2.101 compat: pin `grok-4.5` (upstream renamed `grok-build`), cap grok effort at `high` (the `max` tier is gone; the adapter maps `xhigh`/`max` → `high` so stale callers degrade instead of erroring). diff --git a/CLAUDE.md b/CLAUDE.md index b7a5fec..84cd890 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -21,7 +21,7 @@ This is a **Claude Code plugin marketplace** (monorepo) containing plugins that - **knowledge-system** (v1.9.x) — Knowledge management with three layers: Rules, Knowledge, Memory. Skills: `/init`, `/query`, `/curate`, `/reindex`, `/backfill-knowledge`, `/migrate`, `/statusline` - **work-system** (v1.8.x) — Task and worktree workflow. Skills: `/define`, `/kickoff`, `/adopt`, `/continue`, `/status`, `/close`, `/list`, `/statusline` - **pr-flow** (v1.3.x) — PR review feedback loop. Skills: `/open`, `/cycle`, `/check`, `/fix`, `/rebase`, `/merge` -- **swarm** (v0.4.x) — Local mixture-of-agents code review (external `codex`/`grok` CLIs — grok-4.5 + composer — plus Claude lenses). P2: `/swarm:review` pipeline (scope→fan-out→merge→verify); P5: `--fix`/`--loop` apply the findings you agreed with. Skills: `/swarm:review`, `/swarm:agents` +- **swarm** (v0.4.x) — Local mixture-of-agents code review (external `codex`/`grok` CLIs — grok-4.5 — plus Claude lenses). P2: `/swarm:review` pipeline (scope→fan-out→merge→verify); P5: `--fix`/`--loop` apply the findings you agreed with. Skills: `/swarm:review`, `/swarm:agents` ## Plugin Anatomy diff --git a/README.md b/README.md index 8acf9d8..bcda891 100644 --- a/README.md +++ b/README.md @@ -30,7 +30,7 @@ PR review feedback loop. Create PRs with readiness checks, commit + push + trigg ### Swarm -Local mixture-of-agents code review. Fans out one review across Claude lens subagents plus the `codex` and `grok` CLIs (grok in `grok-4.5` + `composer` modes), merges and verifies their findings, and presents a single ranked report — before anything is pushed. With `--fix` / `--loop` it also applies the findings you agreed with (only Claude edits; the external agents stay review-only). `--pr []` runs the same ensemble against a GitHub PR's diff and posts the gated result as a PR comment (via your own `gh` auth, after one confirmation) — no CI or API-token setup. Complementary to PR Flow's GitHub-side loop. *(Phase 5: `/swarm:review` pipeline + fix loop + PR posting.)* +Local mixture-of-agents code review. Fans out one review across Claude lens subagents plus the `codex` and `grok` CLIs (grok-4.5), merges and verifies their findings, and presents a single ranked report — before anything is pushed. With `--fix` / `--loop` it also applies the findings you agreed with (only Claude edits; the external agents stay review-only). `--pr []` runs the same ensemble against a GitHub PR's diff and posts the gated result as a PR comment (via your own `gh` auth, after one confirmation) — no CI or API-token setup. Complementary to PR Flow's GitHub-side loop. *(Phase 5: `/swarm:review` pipeline + fix loop + PR posting.)* **Commands:** `/swarm:review [--fix | --loop[=N]] [--max]`, `/swarm:review --pr []`, `/swarm:agents` *(more to come: `/swarm:adversarial`, `/swarm:style`, `/swarm:security`)* diff --git a/plugins/swarm/.claude-plugin/plugin.json b/plugins/swarm/.claude-plugin/plugin.json index 654a710..5aaffed 100644 --- a/plugins/swarm/.claude-plugin/plugin.json +++ b/plugins/swarm/.claude-plugin/plugin.json @@ -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 + composer models), merges by mechanism with cross-family consensus, verifies solo findings, 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.4.2", + "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 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.4.3", "author": { "name": "gering" }, diff --git a/plugins/swarm/README.md b/plugins/swarm/README.md index 21d0277..2eb570b 100644 --- a/plugins/swarm/README.md +++ b/plugins/swarm/README.md @@ -14,7 +14,7 @@ Complementary to [pr-flow](../pr-flow/): pr-flow drives the GitHub-PR **Phase 5 of 6** — the pipeline can now **act** (P3/P4 lens presets still to come). `/swarm:review` fans a diff -across four voices (Claude lenses + `codex` + `grok-4.5` + `composer`), +across three voices (Claude lenses + `codex` + `grok-4.5`), merges by mechanism, verifies solo findings, presents one ranked report, and — with `--fix` / `--loop` — applies the findings you agreed with. @@ -44,22 +44,22 @@ presets). ## The pipeline (`/swarm:review`) ``` -Scope+gate → Fan-out (Claude lenses ∥ codex ∥ grok-4.5 ∥ composer) +Scope+gate → Fan-out (Claude lenses ∥ codex ∥ grok-4.5) → Merge (file, mechanism) → Verify solos → Ranked synthesis ``` 1. **Scope + gate** — a cheap agent classifies the diff and picks which Claude lenses are worth running (security is never gated out when code/args/files flow to an external process). -2. **Fan-out** — four voices in parallel: one Claude finder per gated lens plus - `codex`, `grok-4.5` and `composer` as full reviews through the adapter. +2. **Fan-out** — three voices in parallel: one Claude finder per gated lens plus + `codex` and `grok-4.5` as full reviews through the adapter. 3. **Merge** — an LLM step clusters findings by `(file, mechanism)`, not `(file, line)` (external CLIs number against the inlined diff). 4. **Verify** — solo clusters go through an adversarial 3-state verifier (`CONFIRMED`/`PLAUSIBLE`/`REFUTED`; only `REFUTED` is dropped). -**Consensus counts model *families*, not backends.** `grok-4.5` and -`composer` are both grok, so their agreement is one vote — a `CONSENSUS` tag +**Consensus counts model *families*, not voices.** Several Claude lenses +flagging the same thing is one vote, not a cross-check — a `CONSENSUS` tag requires ≥2 of *claude / openai / grok*. Everything else is a solo and earns its place through the verifier. @@ -94,7 +94,7 @@ Backends: |---------|------|-----------| | `claude` | probe-only | reviews run in-session via the Agent tool | | `codex` | external reviewer | `codex exec --output-schema` (model `gpt-5.6-terra`, effort `xhigh`) in a read-only sandbox; auth via `codex login status` | -| `grok` | external reviewer | headless `-p` with inline `--json-schema` (model `grok-4.5`); findings extracted from the response envelope. `--model grok-composer-2.5-fast` takes a separate defensive-parse path (a ~2×-faster second grok voice, no schema flag). | +| `grok` | external reviewer | headless `-p` with inline `--json-schema` (model `grok-4.5`, the only supported grok model); findings extracted from the response envelope. Readiness is model-aware: auth **and** `grok-4.5` present in `grok models`, since the CLI drops/renames models between releases. The model check falls back to auth alone — with a warning, never silently — if the probe can't produce a clean answer (no coreutils `timeout`, non-zero exit, or an unparseable list). | Unavailable backends drop from the ensemble — `claude` alone still works. `/swarm:review` reports a backend that *errored* mid-run distinctly from one diff --git a/plugins/swarm/docs/pipeline-blueprint.md b/plugins/swarm/docs/pipeline-blueprint.md index 76b40bb..f782462 100644 --- a/plugins/swarm/docs/pipeline-blueprint.md +++ b/plugins/swarm/docs/pipeline-blueprint.md @@ -263,7 +263,8 @@ credential mid-review) converged on these non-negotiable mitigations: 5. **Don't fully trust consensus.** Consensus (≥2 backends) currently skips the verifier, but agreement comes from LLM merge-clustering + correlated model bias, not independent proof. P2: still run a light verify on consensus - findings, or require cross-family agreement (not composer+grok). + findings, or require cross-family agreement (two voices from one vendor + are one vote). 6. **Prefer deterministic transport, and distinguish error from empty.** The Haiku "thin transport" wrapper can silently drop/reshape findings while staying schema-valid; where a registered workflow can shell out, pass adapter @@ -289,10 +290,5 @@ credential mid-review) converged on these non-negotiable mitigations: for the timing balance line. - **Model labels** in the balance line (`Opus-4.8`/`GPT-5.5`/`grok-4.5`), read from each backend's review output. -- **Optional composer lens-gate**: `grok-composer-2.5-fast` can't enforce - `--json-schema`/`--effort`, but a strict-JSON prompt makes it emit valid JSON - and reason on demand (tested 2/2). It's ~2× faster than grok-4.5 but its - ~20s CLI cost undercuts a Haiku gate; keep it optional, with a defensive - parser + fallback-to-all-lenses. - **Balance / footer / loop-round box:** render deterministically from the synthesis data (see task file P4/P5). diff --git a/plugins/swarm/scripts/agents.sh b/plugins/swarm/scripts/agents.sh index 3081e19..d1048ac 100755 --- a/plugins/swarm/scripts/agents.sh +++ b/plugins/swarm/scripts/agents.sh @@ -23,17 +23,18 @@ # Reasoning effort has no "max" tier -> max maps to xhigh. # grok — headless `-p` with inline --json-schema; the validated object is # the `.structuredOutput` field of a response envelope. Needs an -# explicit model (-m): grok-4.5 is the schema-capable default and +# explicit model (-m): grok-4.5 is the sole schema-capable model and # accepts --effort (ladder is low|medium|high — no max tier, so the # adapter maps xhigh/max down to high, mirroring codex's missing -# max). The one other accepted model is -# grok-composer-2.5-fast (`--model grok-composer-2.5-fast`): it -# ignores --json-schema/--effort, so the adapter drives it with a -# strict-JSON prompt and parses the findings defensively (verified: -# emits pure {"findings":[...]} on stdout, no envelope). It is a -# second, ~2x-faster grok voice — same family as grok-4.5, so a -# caller must treat their agreement as one family, not consensus. -# Auth heuristic: non-empty ~/.grok/auth.json (no status command). +# max). Readiness is model-aware: auth (non-empty ~/.grok/auth.json — +# there is no status command) AND grok-4.5 listed by `grok models`. +# The CLI rejects an unlisted -m id at launch ("unknown model id") +# and drops/renames models between releases (0.2.101 removed +# grok-composer-2.5-fast), so an auth-only check would advertise a +# model the CLI no longer offers. The probe degrades to auth-only — +# with a warning, never silently — when it cannot run: no coreutils +# timeout to bound it, or an empty/unparseable list. Its bound is +# SWARM_PROBE_TIMEOUT (10s), not SWARM_TIMEOUT (a review-length cap). # # Exit codes: 0 ok · 1 unavailable / not ready / run failed · 2 usage error @@ -43,7 +44,6 @@ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" DEFAULT_SCHEMA="$SCRIPT_DIR/schema/finding.schema.json" CODEX_DEFAULT_MODEL="gpt-5.6-terra" GROK_DEFAULT_MODEL="grok-4.5" -GROK_COMPOSER_MODEL="grok-composer-2.5-fast" # Default HOME so `$HOME` expansions below (auth file, sandbox deny paths) don't # abort the whole script under `set -u` when HOME is unset. HOME="${HOME:-$(cd ~ 2>/dev/null && pwd || echo /nonexistent)}" @@ -144,13 +144,22 @@ _sandbox_deny_paths() { SANDBOX_CMD=() _sandbox_warned="" -_sandbox_ready="" +# Sentinel no backend name can equal: the memo below compares against the +# backend, so an empty initial value would collide with an empty/unset argument +# and skip jail construction entirely — failing OPEN, the one direction a +# sandbox must never fail. +_sandbox_ready="" _init_sandbox() { - # Lazy, per-backend (needs python3 for realpath). One backend per process, so - # building once is fine. - [[ -n "$_sandbox_ready" ]] && return - _sandbox_ready=1 + # Lazy, per-backend (needs python3 for realpath). Memoized ON THE BACKEND, not + # a bare "already built" flag: the profile encodes which cred dir stays + # readable, so reusing another backend's jail would deny a backend its OWN + # token and leave a sibling's readable — the exact cross-backend theft the + # denylist prevents. Only sandboxed() (a `run` call) reaches here today, but + # keying on the backend keeps it correct if a second entry point returns. local backend="${1:-}" + [[ "$_sandbox_ready" == "$backend" ]] && return + _sandbox_ready="$backend" + SANDBOX_CMD=() if command -v sandbox-exec >/dev/null; then # Build the deny profile via python: realpath each path (defeats symlinks # like /tmp→/private/tmp, /etc→/private/etc — sandbox-exec matches the @@ -201,6 +210,10 @@ _env_filter_args() { sandboxed() { # OS jail + env filter around an external call. $1 = backend (its own cred dir # stays readable; siblings' are denied). Warn once if no jail is available. + # Wraps a review, which processes the untrusted diff — that is why it carries + # the full jail; the readiness probe deliberately does NOT go through here + # (it processes no diff, and _init_sandbox's python3 profile-build must not + # become a dependency of the local `ready`/`list` paths — see grok_model_fetch). local backend="$1"; shift _init_sandbox "$backend" if ((${#SANDBOX_CMD[@]} == 0)) && [[ -z "$_sandbox_warned" ]]; then @@ -278,12 +291,123 @@ available_version() { return 0 } +# Wall-clock bound for the readiness probe. Deliberately NOT SWARM_TIMEOUT: that +# caps a full review (600s default) and may be disabled with 0, neither of which +# suits a probe that `list`/`ready` block on. Override with SWARM_PROBE_TIMEOUT; +# a malformed or 0 value falls back to 10 (never uncapped, never a `timeout` +# usage error that would read as "model missing"). +PROBE_TIMEOUT="${SWARM_PROBE_TIMEOUT:-10}" +[[ "$PROBE_TIMEOUT" =~ ^[1-9][0-9]*$ ]] || PROBE_TIMEOUT=10 + +# grok's model list, memoized for the process (`grok models` is a network call — +# fetch at most once). +_grok_models_done="" +_grok_models="" +_probe_degraded() { + # The ONE exit for every "the model check did not happen" route (no timeout + # binary, probe failed, probe timed out, unparseable list). Each ends in the + # same trust-auth degrade, so each must be equally audible: the docs promise + # the check falls back "never silently", and a promise that holds on only some + # routes is the runtime-lie this branch removes. + echo "warning: grok model probe unavailable ($1) — readiness falls back to auth alone; the ${GROK_DEFAULT_MODEL} check did not run" >&2 +} +grok_model_fetch() { + # Populates the cache globals. Call it DIRECTLY — never as `$(grok_model_fetch)`: + # a command substitution runs it in a subshell, the assignments die with that + # subshell, and every caller silently re-pays the network call. + [[ -n "$_grok_models_done" ]] && return 0 + _grok_models_done=1 + local to="" + if command -v timeout >/dev/null; then to="timeout" + elif command -v gtimeout >/dev/null; then to="gtimeout" + fi + if [[ -z "$to" ]]; then + # `ready`/`list` were purely local before this probe, so it must never be the + # thing that hangs them: with no way to bound the call, skip it rather than + # run it uncapped. + _probe_degraded "no timeout/gtimeout on PATH — install coreutils to restore it" + return 0 + fi + # Run grok DIRECTLY, not through sandboxed(): this is a readiness check, not a + # review — it passes no untrusted diff to grok, so it needs no read-deny jail, + # exactly like the sibling `codex login status` check a few lines down. Going + # through sandboxed() would also drag _init_sandbox's python3 profile-build + # into the `ready`/`list` paths (a hard dep they never had), whose failure the + # degrade branch would then misreport as "grok models failed". + # `-k` (SIGKILL after a grace period) is what actually enforces the bound: a + # grok that ignores SIGTERM, or forks a stdout-inheriting child, would keep the + # `$(...)` substitution blocking past the timeout — the "must never hang" hole. + local raw rc=0 + raw="$("$to" -k 3 "$PROBE_TIMEOUT" grok models /dev/null)" || rc=$? + # Check rc BEFORE looking at the output, and discard whatever arrived: a probe + # killed mid-stream (timeout) or erroring late can still have flushed a PARTIAL + # list. Reading that as authoritative is worse than not probing — a truncated + # list missing grok-4.5 reports "model gone" and tells the user to update an + # already-current CLI. Only a clean exit produces an answer; everything else is + # a degrade. + if (( rc != 0 )); then + # GNU timeout: 124 = SIGTERM ended the job at the deadline (a definite + # timeout). 137 (128+9) = the job was SIGKILLed — almost always our own `-k` + # firing on a SIGTERM-ignoring grok, but an OS OOM-kill or an external + # SIGKILL yields the same code, so don't assert a timeout that may not have + # happened; word it for both. + if (( rc == 124 )); then _probe_degraded "\`grok models\` timed out after ${PROBE_TIMEOUT}s" + elif (( rc == 137 )); then _probe_degraded "\`grok models\` was killed (SIGKILL — likely the ${PROBE_TIMEOUT}s \`-k\` bound)" + else _probe_degraded "\`grok models\` failed (rc=$rc)" + fi + return 0 + fi + # One model id PER BULLET LINE: the id is the FIRST grok-shaped token after the + # `*` marker (documented form " * grok-4.5 (default)"). Take only the first — + # scanning the whole line would also pick up a grok-4.5 mentioned in trailing + # PROSE on another model's line ("* grok-5 (successor to grok-4.5)"), reporting + # a retired model as still offered. Match the id SUBSTRING, not the raw field, + # so glued-on punctuation ("grok-4.5," / "grok-4.5." / backticks) doesn't ride + # along and break the exact-match below; the pattern ends on alphanumerics, so + # a trailing separator is never captured. No id-shaped token → empty → degrade. + _grok_models="$(printf '%s\n' "$raw" | awk ' + /^[[:space:]]*\*/ { + for (i = 1; i <= NF; i++) + if (match($i, /grok-[A-Za-z0-9]+([._-][A-Za-z0-9]+)*/)) { + print substr($i, RSTART, RLENGTH) + break + } + }')" + if [[ -z "$_grok_models" ]]; then + _probe_degraded "\`grok models\` returned no model ids — output format may have changed" + fi +} + +grok_model_offered() { + # Three-state, collapsed to an exit code: 0 = the CLI lists grok-4.5, 1 = it + # lists models but NOT grok-4.5 (an honest "gone"), 0 = the list is empty / + # unparseable / not probed (probe unusable — offline, no timeout binary, or a + # future CLI renaming the subcommand). The empty case deliberately trusts auth + # instead of failing closed: silently dropping grok from every fan-out is + # worse than letting run_grok surface its explicit "unknown model id" error. + grok_model_fetch + local list="$_grok_models" + # Substring match on newline-fenced text, NOT `grep -qxF`: an early-exiting + # `grep -q` can SIGPIPE the writer, and pipefail would then report failure + # even on a hit. + case "$list" in + "") return 0 ;; + *) case $'\n'"$list"$'\n' in + *$'\n'"$GROK_DEFAULT_MODEL"$'\n'*) return 0 ;; + *) return 1 ;; + esac ;; + esac +} + ready_check() { local backend="$1" case "$backend" in claude) return 0 ;; # in-session, no separate auth codex) codex login status >/dev/null 2>&1 ;; - grok) [[ -s "$GROK_AUTH_FILE" ]] ;; + # Model-aware: auth alone would advertise grok even when the CLI no longer + # offers the one model the adapter can drive (grok drops/renames models + # between releases — 0.2.101 removed grok-composer-2.5-fast). + grok) [[ -s "$GROK_AUTH_FILE" ]] && grok_model_offered ;; esac } @@ -291,7 +415,13 @@ ready_hint() { # claude needs no hint: it is always available + ready in-session. case "$1" in codex) echo "run: codex login" ;; - grok) echo "run: grok login" ;; + grok) + if [[ ! -s "$GROK_AUTH_FILE" ]]; then + echo "run: grok login" + else + echo "this grok CLI does not offer $GROK_DEFAULT_MODEL (see: grok models) — update the grok CLI" + fi + ;; esac } @@ -380,12 +510,12 @@ subcmd_run() { exit 2 fi - local prompt_file="" effort="xhigh" effort_set="" model="" schema="$DEFAULT_SCHEMA" + local prompt_file="" effort="xhigh" model="" schema="$DEFAULT_SCHEMA" while [[ $# -gt 0 ]]; do [[ $# -ge 2 ]] || { echo "Missing value for $1" >&2; exit 2; } case "$1" in --prompt-file) prompt_file="$2"; shift 2 ;; - --effort) effort="$2"; effort_set=1; shift 2 ;; + --effort) effort="$2"; shift 2 ;; --model) model="$2"; shift 2 ;; --schema) schema="$2"; shift 2 ;; *) echo "Unknown flag: $1" >&2; exit 2 ;; @@ -396,11 +526,6 @@ subcmd_run() { *) echo "Invalid effort: $effort (low|medium|high|xhigh|max)" >&2; exit 2 ;; esac [[ -f "$schema" ]] || { echo "Schema not found: $schema" >&2; exit 2; } - # composer has no reasoning-effort control; say so if the caller set --effort - # explicitly, so review depth isn't misjudged against grok-4.5. - if [[ "$backend" == "grok" && "$model" == "$GROK_COMPOSER_MODEL" && -n "$effort_set" ]]; then - echo "note: --effort is ignored for $GROK_COMPOSER_MODEL (it has no reasoning-effort control)" >&2 - fi # The prompt travels as ONE argv word, so the binding limit is the per-argument # cap, not total ARG_MAX: Linux MAX_ARG_STRLEN is 128 KiB (macOS has no @@ -492,19 +617,12 @@ run_grok() { case "$effort" in xhigh|max) effort="high" ;; esac local grok_model="${model:-$GROK_DEFAULT_MODEL}" - # grok-composer-2.5-fast is the one non-default model the adapter supports; it - # ignores --json-schema/--effort, so it takes a separate defensive-parse path. - if [[ "$grok_model" == "$GROK_COMPOSER_MODEL" ]]; then - run_grok_composer "$prompt" "$schema" - return - fi - - # Preflight-reject any OTHER non-default model: only grok-4.5 enforces - # --json-schema (and accepts --effort). An unlisted model would silently - # return structuredOutput:null and fail late with no schema output — so reject - # up front with a usage error rather than burn a review on it. + # Preflight-reject any non-default model: only grok-4.5 enforces --json-schema + # (and accepts --effort). Another model would silently return + # structuredOutput:null and fail late with no schema output — so reject up + # front with a usage error rather than burn a review on it. if [[ "$grok_model" != "$GROK_DEFAULT_MODEL" ]]; then - echo "grok model '$grok_model' does not enforce --json-schema — the adapter requires schema output; use $GROK_DEFAULT_MODEL (default) or $GROK_COMPOSER_MODEL" >&2 + echo "grok model '$grok_model' does not enforce --json-schema — the adapter requires schema output; use $GROK_DEFAULT_MODEL (the only supported grok model)" >&2 exit 2 fi @@ -557,141 +675,6 @@ print() ' | scrub_secrets } -run_grok_composer() { - # grok-composer-2.5-fast: ~2x faster than grok-4.5 but ignores - # --json-schema/--effort (would return plain text with structuredOutput:null). - # So we DON'T pass those flags — instead we append a strict-JSON directive - # (the schema itself) to the prompt and parse the answer defensively. Verified: - # composer then emits pure {"findings":[...]} on stdout (no envelope). Same - # sandbox + env filter + scrub as grok-4.5; only the schema mechanism differs. - local prompt="$1" schema="$2" - local full - full="$prompt - ---- -Respond with ONLY a single JSON object conforming to this JSON Schema — no prose, -no explanation, no markdown code fences: -$(cat "$schema")" - - # The assembled prompt travels as ONE argv word (--single=), so re-check it - # against Linux MAX_ARG_STRLEN (128 KiB): subcmd_run caps the *prompt* at 120 - # KiB, but this path appends the schema+directive into the SAME word, eating - # that headroom. Only a large custom --schema realizes the gap (the bundled - # schema is ~2.4 KiB); guard at 126 KiB so a genuine overflow errors cleanly. - local full_bytes - full_bytes=$(printf '%s' "$full" | wc -c) - if (( full_bytes > 129024 )); then - echo "grok(composer) prompt+schema is $(( full_bytes / 1024 )) KiB — over the 126 KiB single-argument limit; narrow the diff or use a smaller --schema" >&2 - exit 1 - fi - - local raw rc=0 - raw="$(sandboxed grok grok -m "$GROK_COMPOSER_MODEL" \ - --tools "" --disable-web-search \ - --single="$full" /dev/null)" || rc=$? - if (( rc != 0 )); then - (( rc == 124 )) && echo "grok(composer) timed out after ${ADAPTER_TIMEOUT}s" >&2 \ - || echo "grok(composer) failed" >&2 - exit 1 - fi - # Cap the output before parsing: the defensive brace-scan below is O(n^2) in - # the number of '{' and runs OUTSIDE with_timeout (which wraps only the grok - # subprocess), so a multi-MB brace-dense blob could spin for minutes. A valid - # findings payload (<=100 items, capped fields) stays well under 512 KiB. - local raw_bytes - raw_bytes=$(printf '%s' "$raw" | wc -c) - if (( raw_bytes > 524288 )); then - echo "grok(composer) output too large ($(( raw_bytes / 1024 )) KiB) — refusing to parse" >&2 - exit 1 - fi - # Defensive parse: composer is not schema-enforced, so the answer may arrive as - # bare JSON (observed), wrapped in ```json fences, or as a grok envelope with a - # .structuredOutput field. Extract a {"findings":[...]} object from any of those, - # or fail non-zero (so the caller records an ERROR, never a false empty review). - printf '%s' "$raw" | python3 -c ' -import json, re, sys - -def findings_from(o): - # Return the findings-bearing object: the object itself, or a nested - # structuredOutput, else None. - if isinstance(o, dict): - if isinstance(o.get("findings"), list): - return o - inner = o.get("structuredOutput") - if isinstance(inner, dict) and isinstance(inner.get("findings"), list): - return inner - return None - -def all_objects(s): - # Every JSON object we can pull out, in priority order: whole string, each - # fenced block, then every balanced {...} run. We collect ALL of them (not - # the first decodable one) so a leading non-findings object cannot mask the - # real findings object later in the stream. - objs = [] - for cand in (s.strip(), *[m.strip() for m in re.findall(r"```(?:json)?\s*(.*?)```", s, re.S)]): - try: - objs.append(json.loads(cand)) - except Exception: - pass - start = s.find("{") - while start != -1: - try: - obj, _ = json.JSONDecoder().raw_decode(s[start:]) - objs.append(obj) - except Exception: - pass - start = s.find("{", start + 1) - return objs - -SEV = {"critical", "warning", "minor"} -CONF = {"high", "medium", "low"} -# DRIFT WARNING: these caps/keys hand-mirror finding.schema.json (and -# FINDING_ITEM in swarm-review.js). The caps double as injection limits, so the -# three copies must stay in sync — edit them together. MAXITEMS mirrors the -# schema findings maxItems. -MAXLEN = {"file": 500, "summary": 400, "failure_scenario": 1200, "recommendation": 800} -MAXITEMS = 100 -KEYS = {"file", "line", "severity", "summary", "failure_scenario", "confidence", "recommendation"} - -def valid_item(f): - # Mirror finding.schema.json EXACTLY. composer is not CLI-schema-enforced - # (unlike codex/grok-4.5), so a malformed item must ERROR here rather than - # reach merge/verify, which rely on the uniform-findings contract. Check the - # exact key set (additionalProperties:false + all required), maxLength, and - # enums — a hand-rolled type-only subset would drop valid rows and pass - # oversized/extra-key ones. - if not isinstance(f, dict) or set(f) != KEYS: - return False - for k, m in MAXLEN.items(): - v = f.get(k) - if not isinstance(v, str) or len(v) > m: - return False - ln = f.get("line") - if not isinstance(ln, int) or isinstance(ln, bool) or ln < 0: - return False - return f.get("severity") in SEV and f.get("confidence") in CONF - -objs = all_objects(sys.stdin.read()) -cands = [f for f in (findings_from(o) for o in objs) if f is not None] -if not cands: - sys.stderr.write("grok(composer) returned no {findings:[...]} object (content withheld)\n") - sys.exit(1) -# Prefer a NON-EMPTY findings object over an empty preamble object that would -# otherwise mask the real answer (a leading {"findings":[]} or {"note":...}); -# fall back to the first object if every candidate is empty. -d = next((c for c in cands if c["findings"]), cands[0]) -if len(d["findings"]) > MAXITEMS: - sys.stderr.write("grok(composer) emitted %d findings (> %d maxItems)\n" % (len(d["findings"]), MAXITEMS)) - sys.exit(1) -bad = [i for i, f in enumerate(d["findings"]) if not valid_item(f)] -if bad: - sys.stderr.write("grok(composer) emitted %d finding(s) violating finding.schema.json\n" % len(bad)) - sys.exit(1) -json.dump({"findings": d["findings"]}, sys.stdout) -print() -' | scrub_secrets -} - main() { local cmd="${1:-}" shift || true diff --git a/plugins/swarm/scripts/schema/finding.schema.json b/plugins/swarm/scripts/schema/finding.schema.json index beb6577..4e38a94 100644 --- a/plugins/swarm/scripts/schema/finding.schema.json +++ b/plugins/swarm/scripts/schema/finding.schema.json @@ -1,6 +1,6 @@ { "$schema": "http://json-schema.org/draft-07/schema#", - "$comment": "DRIFT WARNING: this schema is hand-mirrored in workflows/swarm-review.js (FINDING_ITEM) and scripts/agents.sh (composer KEYS/MAXLEN/MAXITEMS). The field caps double as injection limits — edit all three together.", + "$comment": "DRIFT WARNING: this schema is hand-mirrored in workflows/swarm-review.js (FINDING_ITEM). The field caps double as injection limits — edit both together.", "title": "Swarm review findings", "description": "Shared findings shape every swarm backend normalizes into. Enforced on codex via `codex exec --output-schema` and on grok via `--json-schema`, so the ensemble merge receives uniform JSON. All item fields are required (strict structured-output modes reject optional properties); use line=0 and confidence as honest defaults rather than omitting.", "type": "object", diff --git a/plugins/swarm/skills/agents/SKILL.md b/plugins/swarm/skills/agents/SKILL.md index 37a0bd3..ab6a17c 100644 --- a/plugins/swarm/skills/agents/SKILL.md +++ b/plugins/swarm/skills/agents/SKILL.md @@ -35,7 +35,12 @@ user_invocable: true - `claude` is always ready when Claude Code runs (reviews happen in-session via the Agent tool; the external CLIs are called through the adapter). - **`grok` Ready is a heuristic** — it means a non-empty `~/.grok/auth.json` - exists, NOT that the token is valid/unexpired (codex, by contrast, runs a - real `codex login status`). So grok can show Ready yet fail at review time on - a stale token; treat it as "credentials present" and let the run surface a - real auth error. + exists **and** that `grok models` still lists the adapter's model + (`grok-4.5`), NOT that the token is valid/unexpired (codex, by contrast, runs + a real `codex login status`). So grok can show Ready yet fail at review time + on a stale token; treat it as "credentials present" and let the run surface a + real auth error. Not-ready with a "does not offer grok-4.5" hint means the + grok CLI dropped/renamed the model — update the CLI, it is not an auth + problem. The model check degrades to auth-only (with a warning on stderr) when + the probe can't run — no coreutils `timeout` to bound it, or an unreadable + model list. diff --git a/plugins/swarm/skills/review/SKILL.md b/plugins/swarm/skills/review/SKILL.md index 8df82b2..acbdf5e 100644 --- a/plugins/swarm/skills/review/SKILL.md +++ b/plugins/swarm/skills/review/SKILL.md @@ -1,17 +1,17 @@ --- name: review description: | - Local mixture-of-agents review: Claude lenses plus codex and grok (grok-4.5 + - composer), one ranked report. --fix/--loop applies agreed findings; --pr reviews - a GitHub PR diff and posts the result. + Local mixture-of-agents review: Claude lenses plus codex and grok, one ranked + report. --fix/--loop applies agreed findings; --pr reviews and posts on a + GitHub PR. Trigger: "swarm review", "review my changes", "review this PR". user_invocable: true --- # Swarm Review -> Fan one code review across Claude lenses + codex + grok-4.5 + composer, -> merge by mechanism, verify solos, and present one ranked report. +> Fan one code review across Claude lenses + codex + grok-4.5, merge by +> mechanism, verify solos, and present one ranked report. ## Arguments @@ -48,9 +48,8 @@ branch delta). `--fix`/`--loop` — composes with both (`--max --loop` = max-depth fix loop). Set `max: true` in the workflow args (step 2). It bumps: codex → `gpt-5.6-sol` at `xhigh` (codex has no `max` tier), Claude finder lenses + - the adversarial verifier → `xhigh`. gate/merge, the grok voice (`high` is - grok's ceiling — it runs there on both profiles) and the composer voice (no - effort control) are unchanged. + the adversarial verifier → `xhigh`. gate/merge and the grok voice (`high` is + grok's ceiling — it runs there on both profiles) are unchanged. - Anything left after removing the flags → the scope argument for step 1. Without either flag the review is **read-only**: present the report and offer to @@ -254,9 +253,8 @@ echo "LIVE_JSON=$(bash "${CLAUDE_PLUGIN_ROOT}/scripts/agents.sh" list --json | t - `SWARM_WARN=…` → surface that line: the scope narrowed to uncommitted changes because no default-branch ancestor was found. Then continue. - From `LIVE_JSON` build `externalVoices`: include `"codex"` iff codex is - `available && ready`; include `"grok"` and `"composer"` iff grok is - `available && ready` (both share the grok CLI + auth). If none are live, the - review runs with the Claude lenses alone — say so. + `available && ready`; include `"grok"` iff grok is `available && ready`. If + none are live, the review runs with the Claude lenses alone — say so. - **Oversize** — if `PROMPT_BYTES` > 122880 the diff exceeds the adapter's 120 KiB (122880-byte) per-call cap, so the external CLIs cannot run: set `externalVoices` to `[]` (Claude-lens-only review), tell the user the external backends were skipped, @@ -283,8 +281,8 @@ Workflow({ Fill ``/``/`` from the echoed values. Add `max: true` to `args` when `--max` was given (step 1 stripped it) — the deepest-effort profile. Add `claude: false` to `args` -for an **external-only control run** (codex + grok-4.5 + composer, no Claude -finder lenses — merge/verify still run in-session); default is the full ensemble. +for an **external-only control run** (codex + grok-4.5, no Claude finder +lenses — merge/verify still run in-session); default is the full ensemble. The workflow runs in the background for several minutes — **tell the user they can watch live progress with `/workflows`** while it runs. It returns `{ findings, refuted, backendErrors, balance, gate }`. @@ -316,11 +314,10 @@ here in German. Do not translate finding content.) - **Ort** — `` `file:line` `` in backticks. - **Befund** — one short clause, **≤ ~40 chars** (hard budget); no emoji here. - **Quelle** — who raised it + ensemble confidence, folded into one cell: the - concrete models (`claude→opus`, `codex→gpt`, `grok→grok`, `composer→composer`, - dot-joined, e.g. `opus·grok`) then a confidence glyph — **`✓` = CONFIRMED · - `~` = PLAUSIBLE** (e.g. `opus·grok ✓`, `gpt ~`). Never the backend names / single - letters. grok+composer = one family (no cross-family consensus). A single-source - review (no ensemble) omits this column. + concrete models (`claude→opus`, `codex→gpt`, `grok→grok`, dot-joined, e.g. + `opus·grok`) then a confidence glyph — **`✓` = CONFIRMED · `~` = PLAUSIBLE** + (e.g. `opus·grok ✓`, `gpt ~`). Never the backend names / single letters. A + single-source review (no ensemble) omits this column. - **V** — YOUR main-session Verdict, icon only, the action gate: ✅ agree · 🟨 partial · ❌ disagree. Distinct from the `✓`/`~` confidence in Quelle. - **Notiz** — the *why*, **≤ ~55 chars** (hard budget — let the renderer wrap it @@ -589,9 +586,9 @@ post. Do **not** re-implement the sanitize/gate/post logic inline. ## Notes -- **Consensus = cross-family agreement** (≥2 of claude / openai / grok). Two - grok voices (grok-4.5 + composer) agreeing count as one family, so they - cannot alone mint a CONSENSUS — solos go through the adversarial verifier. +- **Consensus = cross-family agreement** (≥2 of claude / openai / grok). Voices + from one vendor count once — Claude's lens voices agreeing with each other is + one family, not a quorum — so solos go through the adversarial verifier. - **Security floor** (inherited from the adapter, plus this pipeline): the diff is fenced as data, external CLIs run sandboxed + tool-less (grok) with a secret scrub at the adapter boundary, and a final **output gate** re-scrubs diff --git a/plugins/swarm/workflows/swarm-review.js b/plugins/swarm/workflows/swarm-review.js index 9e06b0f..fa05d88 100644 --- a/plugins/swarm/workflows/swarm-review.js +++ b/plugins/swarm/workflows/swarm-review.js @@ -1,9 +1,9 @@ export const meta = { name: 'swarm-review', - description: 'Local mixture-of-agents review: scope+gate → fan-out (Claude lenses + codex + grok + composer) → (file,mechanism) merge with family-aware consensus → verify solos → output-gated ranked synthesis.', + description: 'Local mixture-of-agents review: scope+gate → fan-out (Claude lenses + codex + grok) → (file,mechanism) merge with family-aware consensus → verify solos → output-gated ranked synthesis.', phases: [ { title: 'Scope', detail: 'classify diff + gate lenses' }, - { title: 'Fan-out', detail: 'Claude lenses + codex + grok + composer in parallel' }, + { title: 'Fan-out', detail: 'Claude lenses + codex + grok in parallel' }, { title: 'Merge', detail: 'cluster by (file, mechanism), consensus by family' }, { title: 'Verify', detail: '3-state verify of solo clusters' }, ], @@ -13,7 +13,7 @@ export const meta = { // args.adapter absolute path to scripts/agents.sh // args.diffFile file the Claude finders read (raw unified diff) // args.externalPromptFile file the external CLIs get (review instr + fenced diff) -// args.externalVoices which external backends are live (subset of the three) +// args.externalVoices which external backends are live (subset of codex, grok) // args.claude false → external-only control run (no Claude lenses) // args.max true (strict boolean) → deepest-effort profile below // Normalize: the runtime may deliver `args` as an object OR a JSON string. @@ -88,11 +88,10 @@ const LENS_BRIEF = { conventions: 'repo conventions: naming, doc/README sync, version-sync, sibling-script idioms', } -// One finding. DRIFT WARNING: this schema is hand-mirrored in THREE places — -// scripts/schema/finding.schema.json (canonical, CLI-enforced on codex/grok), -// this FINDING_ITEM, and agents.sh's composer validator (KEYS/MAXLEN/SEV/CONF). -// The caps double as injection limits, so all three must stay in sync — edit -// them together. +// One finding. DRIFT WARNING: this schema is hand-mirrored in TWO places — +// scripts/schema/finding.schema.json (canonical, CLI-enforced on codex/grok) +// and this FINDING_ITEM. The caps double as injection limits, so both must stay +// in sync — edit them together. const FINDING_ITEM = { type: 'object', additionalProperties: false, required: ['file', 'line', 'severity', 'summary', 'failure_scenario', 'confidence', 'recommendation'], @@ -118,9 +117,10 @@ const EXTERNAL_SCHEMA = { }, } -// backend label -> model family (consensus counts distinct FAMILIES, so two -// grok voices agreeing is one vote, not independent cross-check). -const FAMILY = { claude: 'claude', codex: 'openai', grok: 'grok', composer: 'grok' } +// backend label -> model family. Consensus counts distinct FAMILIES, not +// backends: if two voices ever share a vendor again (as grok-4.5 + composer +// did), their agreement must count once, not as an independent cross-check. +const FAMILY = { claude: 'claude', codex: 'openai', grok: 'grok' } // ---- output gate: last-line secret scrub over surviving findings ------------ // Runs on EVERY finding (incl. Claude finders, which never pass the adapter) @@ -259,7 +259,7 @@ log(runClaude ? `Gate: ${gate?.change_kind || 'unknown'} — running lenses [${r : `Gate: skipped — external-only run (no Claude lenses)`) // ============================================================================ -// Phase 2 — Ensemble fan-out (4 voices in parallel) +// Phase 2 — Ensemble fan-out (Claude lenses + the external voices, in parallel) // ============================================================================ phase('Fan-out') const claudeThunks = runLensesSafe.map((lens) => () => @@ -279,11 +279,10 @@ const claudeThunks = runLensesSafe.map((lens) => () => const EXTERNAL_VOICES = [ { backend: 'codex', label: 'codex:full', cmd: `bash "${ADAPTER}" run codex ${MAX ? `--model ${MAX_CODEX_MODEL} --effort xhigh` : '--effort high'} --prompt-file "${EXTERNAL_PROMPT}"` }, { backend: 'grok', label: 'grok:full', cmd: `bash "${ADAPTER}" run grok --effort high --prompt-file "${EXTERNAL_PROMPT}"` }, - { backend: 'composer', label: 'composer:full', cmd: `bash "${ADAPTER}" run grok --model grok-composer-2.5-fast --prompt-file "${EXTERNAL_PROMPT}"` }, ] // Only spawn transports for backends the skill reported live (probed via the // adapter); absent CLIs would otherwise show up as noisy "errors". -const wantVoices = Array.isArray(INPUT.externalVoices) ? INPUT.externalVoices : ['codex', 'grok', 'composer'] +const wantVoices = Array.isArray(INPUT.externalVoices) ? INPUT.externalVoices : ['codex', 'grok'] const externalThunks = EXTERNAL_VOICES.filter((v) => wantVoices.includes(v.backend)).map((v) => () => agent( `You are a thin transport wrapper — do NOT review the code yourself, do NOT modify the command. Run EXACTLY this with the Bash tool (timeout 600000) and wait for it to finish:\n\n` + @@ -354,7 +353,8 @@ if (pool.length > 0) { members.forEach((i) => assigned.add(i)) const backends = Array.from(new Set(members.map((i) => pool[i].backend))).sort() const families = Array.from(new Set(members.map((i) => pool[i].family))).sort() - // Consensus requires >=2 distinct FAMILIES (composer+grok-4.5 = one family). + // Consensus requires >=2 distinct FAMILIES (see FAMILY: same-vendor voices + // count once; Claude's many lens voices are one family, not a quorum). return { ...c, member_indices: members, backends, families, consensus: families.length >= 2 ? 'CONFIRMED' : 'solo' } }).filter((c) => c.backends.length > 0) // drop clusters whose member_indices all filtered out — no backing voice @@ -436,7 +436,7 @@ const findings = gatedFindings.sort((a, b) => (sevOf(a) - sevOf(b)) || (conRank( // Per-backend rollup for the balance "Agents" line: concrete short model label // + voice/finding counts + whether it ran clean. Wall-time (per-agent durationMs) // needs a registered workflow to surface — tracked as P4 wiring. -const MODEL_LABEL = { claude: 'opus', codex: 'gpt', grok: 'grok-4.5', composer: 'composer' } +const MODEL_LABEL = { claude: 'opus', codex: 'gpt', grok: 'grok-4.5' } const agents = {} for (const v of voices) { const a = agents[v.backend] || (agents[v.backend] = { backend: v.backend, model: MODEL_LABEL[v.backend] || v.backend, voices: 0, failedVoices: 0, findings: 0, ok: true })