feat(ruflo-swarm): live swarm pane mod for Claude Code (function hooks) by ruvnet · Pull Request #3607 · ruvnet/ruflo · GitHub
Skip to content

feat(ruflo-swarm): live swarm pane mod for Claude Code (function hooks) - #3607

Merged
ruvnet merged 9 commits into
mainfrom
feat/claude-code-mods-swarm-ui
Oct 1, 2026
Merged

ruvnet merged 9 commits into
mainfrom
feat/claude-code-mods-swarm-ui

Conversation

@ruvnet

@ruvnet ruvnet commented Oct 1, 2026 •

Copy link
Copy Markdown
Owner

What this adds

A Claude Code mod (function-hooks module) in plugins/ruflo-swarm/hooks/. It draws a live swarm pane inside Claude Code and acts on the swarm through the real ruflo CLI. Without function hooks, nothing in it loads, and the plugin's existing commands, skills and agents behave exactly as before (the hooks.json holds modules only).

Only plugins/ruflo-swarm/** changes. Nothing outside the plugin is touched: no CLI source, no marketplace.json, no lockfile.

Feature checklist

Status
Pane: a tile per agent, coloured by state with a word beside it (ruflo agents, the hive queen, Claude Code subagents and teammates, the main loop) done
Tiles turn blue while their loop reads files and yellow while it writes, then fade done (seen live, see below)
Topology (hierarchical / mesh / ring / star), leader starred, leader source stated done
Task board joined with claims (pending / claimed / done / failed) done
Cost and context from $.session.usage(); a missing figure shows as n/a done
The router's last pick seen this session, with its score; under routeThreshold it reads "below threshold" done
Hive-mind proposals, vote counts and recent decisions done
Narrow fallback under 44 columns; sections dropped from the bottom when rows run out; theme colours only done
Buttons: logs/activity, claim, steal, hand off, pause/resume a claim, stop, offer for stealing, re-route, vote yes/no, put the next command in the prompt done
Confirm step on stop / steal / hand off (lapses after 30 s); every action re-reads the disk and reports "seen on disk" or "exited 0 but the disk does not show it" done
agent.spawn: Claude Code subagents become members; opt-in injectSpawnContext appends a one-line swarm note (facts from disk only) done
turn.* / tool.call: live activity per loop; tool.call only observes and passes the result through unchanged done
Opt-in on('*') audit: event and tool names plus agent ids, buffered (max 200) and flushed every 30 s via memory store into the ruflo-swarm-audit namespace. Never content. done
Commands: /ruflo-swarm-pane, /ruflo-swarm-status [json], /ruflo-swarm-topology, /ruflo-swarm-claims, /ruflo-swarm-consensus; the markdown /ruflo-swarm:watch also opens the pane, then falls through done
Real "pause an agent" not offered: ruflo has no agent pause. The pane pauses and resumes the agent's claim (claims_status) and says so.
An agent's full transcript not offered: there is no transcript API for a mod. For ruflo agents the pane shows agent logs; for Claude Code loops it shows the tool calls the hooks saw, labelled "activity".
Consuming a core $.ruflo noun deferred (see the contract below)

Contract with mods-planner (core mod)

Proposed in writing at the start; no reply had arrived when this PR opened, so I proceeded as the proposal allowed. mods-planner has since agreed to (a), (b) and (c) as written. Their notes:

  • Their mod hooks agent.spawn only when the user turns on costHardStop. It then denies spawns at 100% of budget and never rewrites the prompt. The two compose: a deny ends the dispatch, and this mod's opt-in note only appends text.
  • They update the marketplace ruflo-swarm entry to 0.3.0 in feat(mods): ruflo as a Claude Code mod — function hooks, opt-in (ADR-404) #3608.
  • They ship $.ruflo (segment, lastRoute, snapshot, typed in plugins/ruflo-mods/types/index.d.ts). Reading the router pick from it is a possible follow-up; this PR does not depend on it.

The terms as proposed:

  • (a) No dependency on a core $.ruflo noun. This mod reads swarm state from disk itself, so it can be removed or used without the core mod. If the core mod later ships $.ruflo with a types/index.d.ts, the router pick can come from it in a follow-up.
  • (b) UI surfaces. The core mod owns AbovePrompt, the statusline, PromptHint drawing, prompt.submit, tool.check, learning signals and the trust gate. This mod owns the Pane (requestId: ruflo-swarm) and the ruflo-swarm-* commands. It registers no prompt.submit and no tool.check hook. It observes PromptHint renders only to learn the viewport, always calls next(e) and draws nothing there.
  • (c) Manifests. Shared manifests and marketplace.json belong to mods-planner. This PR edits only plugins/ruflo-swarm/.claude-plugin/plugin.json (version 0.3.0, a userConfig block, keywords) and plugins/ruflo-swarm/hooks/hooks.json.

Update: 2.1.287 live, and ruOS-hosted agents

Live on 2.1.287 with no env var. After the account switch the rollout flag serves on.

  • ~/.local/bin/claude --plugin-dir plugins/ruflo-swarm loads the module: hooks module ruflo-swarm@inline loaded (worker, environment 1, tier user).
  • The pane opens unasked (ui.open ruflo-swarm ruflo-swarm (unasked, 210 columns): placed).
  • claude -p "/ruflo-swarm-status" prints the real mesh swarm.
  • claude plugin test runs and passes there (38/38).

The 2.1.287 "did not load" finding below was account-specific: the rollout flag was off for the earlier account.

ruOS remote hosts (contract agreed in writing with the ruflo-ruos lane, PR #3605). The pane reads three things:

  • agents[id].config.host where kind === 'ruos';
  • .claude-flow/ruos/hosts.json;
  • the last 64 KB of .claude-flow/ruos/events.jsonl.

What it shows:

  • A remote agent's tile reads coder @ruos-desktop-1. Its state comes from the newest run event (running, completed, failed (exit N), stopped).
  • A "ruOS hosts" section lists each desktop with its state, heartbeat age and agent count.
  • If agents name a host but there is no host file, the section reads "not on disk".

How it treats the files:

  • Ids must match ruflo's id shape; names are cleaned and cut to 40 characters.
  • Free text (error, lastResult) is never read.
  • An event log over 2 MB is not read, and the pane says so. There is no ranged read, so the cap is on the whole file.

No button mutates ruOS. The pane never depends on a $.ruos noun. It never uses the /ruos commands or the ruos-view pane id. It only observes mcp__ruos__* tool calls.

Seen live on 2.1.287 against a real ruflo swarm, with contract files I wrote by hand: ruflo-ruos was not running here. The tiles showed coder @ruos-desktop-1 (working) and tester @ruos-desktop-2 (done), and the hosts section showed the heartbeat age.

Fix found live: $.store belongs to the plugin, not the folder, so a router pick from another project appeared in a new folder's pane. Persisted pane state is now keyed by working directory, and the router line shows the pick's age. A test covers this.

Evidence

Engine-kit tests (claude plugin test plugins/ruflo-swarm): 38 pass, 0 fail on 2.1.287 (32/32 earlier on 2.1.282). The fixtures are the files ruflo 3.49.0 really wrote in a run; the hive token is replaced by a sentinel that is asserted never to render. Coverage:

  • pane rendering, including $.ui.mount accepted at 24 to 160 columns;
  • button presses through confirm, cancel and lapse;
  • refusal tolerance: every $ call the mod makes is denied, and it still starts, answers and draws;
  • the narrow fallback and the empty no-swarm state;
  • the tool call passed on untouched;
  • options tested through register with a recording on, because inline test plugins can't import the module;
  • hostile text: 100k-character shapes, the 4 MB size cap, terminal escapes, flag-like task text.

vitest: npx vitest run --root plugins/ruflo-swarm tests/parse.spec.ts gives 6/6.

Other checks:

  • claude plugin validate --strict plugins/ruflo-swarm: clean on 2.1.282 and 2.1.287.
  • tsc -p plugins/ruflo-swarm: clean, against upstream mods/types/claude-code.d.ts (written by 2.1.277), fetched by scripts/fetch-mod-types.sh into a gitignored .claude/types/.
  • smoke.sh: 11/11. audit-plugin-manifest, audit-plugin-hooks-cross-platform and audit-plugin-mcp-prefix: ok. smoke-all-plugins: 35/36. The one failure is in ruflo-metaharness step 17z6, which was already failing and is untouched here.

Benchmark (engine kit, 60 iterations each, through $.ui.render):

agents render median / p95 narrow median / p95 refresh (re-read every file, mtime-cached) median / p95
8 1.07 / 1.77 ms 0.49 / 0.86 ms 1.45 / 2.31 ms
32 0.95 / 1.52 ms 0.51 / 1.11 ms 1.38 / 2.03 ms
100 1.00 / 1.62 ms 0.54 / 0.84 ms 1.78 / 2.70 ms

Render time stays flat because the tile grid is capped (at most 6 rows, about 36 tiles, then "+N more agents"). 100 agents does not draw 100 tiles.

Throttling, so no hook waits on slow work:

  • disk re-reads are debounced to 400 ms, plus a 2 s tick while a turn runs and 10 s while idle, and only while the pane is open;
  • the frame timer runs only while a tile is lit;
  • CLI calls run only on a button press;
  • audit flushes run on a timer.

Live run (Claude Code 2.1.282, CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1 --plugin-dir plugins/ruflo-swarm, in a real swarm made with npx @claude-flow/cli@3.49.0 swarm init + agent spawn ×3–4 + task create ×2 + claims + hive-mind init + a consensus proposal)

  • Debug log: hooks module ruflo-swarm@inline loaded (worker, environment 1, tier user); events: session.start,ui.render,ui.close,command.run,turn.start,turn.complete,agent.spawn,tool.call, then $.command.register (ruflo-swarm): /ruflo-swarm-status listed.
  • claude -p "/ruflo-swarm-status" printed the real swarm: the queen, coder busy, tester and reviewer idle, tasks: 1 pending, 1 claimed.
  • In the interactive TUI (tmux) the pane drew the real hierarchical swarm and, in a second directory, a mesh swarm (★queen ⇄ coder ⇄ tester ⇄ reviewer ⇄ architect ⇄ claude (main), all-to-all: 6 members, 15 links). At session start it opened by itself (ui.open … (unasked, 210 columns): placed).
  • /ruflo-swarm:watch opened the pane (ui.open … (asked)), and command.run settled … next() included: the markdown command still ran.
  • Buttons pressed in the TUI with ctrl+x tab, Tab and Enter:
    • ◀ ▶ moved the selection;
    • logs ran the real agent logs and showed its lines;
    • pause claim printed ✓ seen on disk, and claims.json really read "status": "paused";
    • stop… showed the confirm row; confirm printed ✓ seen on disk: stop agent …, and agents/store.json really read coder terminated.
  • A Bash hooks route --format json in a turn set the pane's router line to router: tester 72% (testing-task) · alt reviewer 62%.
  • An unnamed Explore subagent appeared as a tile (via agent.spawn) and turned ✓ done.
  • The main loop's tile went from claude colour (working) to the blue suggestion colour while Read ran, then back to idle. This was checked from ANSI captures (38;5;174 → 38;5;153 → 38;5;246).

What the live run found (engine and ruflo behaviour, not fixed here)

  1. 2.1.287 did not load the module, with or without CLAUDE_CODE_ENABLE_FUNCTION_HOOKS=1. The brief expected it on by default there. Debug log: installed plugins' hooks modules not loaded: rollout flag (tengu_plugin_hooks_modules) is off, from GrowthBook (the disk cache of an earlier session); built-in plugins load regardless. The kit tests and validate still pass on 2.1.287.
  2. Named Agent calls raise no agent.spawn. A named Agent call ran as an in-process teammate (spawnInProcessTeammate), and $.agent.list() listed it as type teammate. Teammates are now labelled by their name.
  3. A loop's ids don't match. One loop's tool.call agentId matched neither the spawn result nor agent.list(). The pane names such a loop subagent …<id suffix> rather than guessing which agent it is.
  4. ruflo defect, a candidate issue: hive-mind consensus -a vote … --voter-id <agent> prints [OK] Vote recorded (For: undefined, Against: undefined) and records no vote in hive-mind/state.json. The pane reports it as "exited 0 but the disk does not show it".
  5. hooks route reports matched: true at 0.41 for gibberish (zzqx florp). This is adjacent to hooks_route reports its highest confidence (70%) exactly when nothing matched, including on blank and non-English tasks #3567 and is noted in hive-mind consensus vote prints [OK] Vote recorded but writes no vote #3609. The pane shows the score and "below threshold", never a bare match.

Not verified

  • Behaviour under sec-default (no managed settings on this machine). Refusal of every $ call is covered in the kit instead.
  • mcp.call transport: mutations go through npx --offline -y @claude-flow/cli@latest (mcp exec for the claims tools), not the MCP server.
  • Steal, hand off, claim, vote and offer were exercised in the kit, plus the vote CLI by hand; stop and pause were pressed live.
  • The audit flush argv was checked against the real CLI by hand (memory store / memory retrieve), not from a live session with audit: true.

Security

  • Every CLI call is $.process.run with fixed argv, through one of four fixed prefixes. The default, npx --offline, never touches the network.
  • Ids must match ruflo's id shape. The one JSON argument is built with JSON.stringify and passed as a single argv element.
  • Task text reaches the router as one argv element, with control characters stripped. Text that starts with - is refused.
  • Destructive actions need a confirm step. The hive token is never read into state.
  • There is no tool.check hook and no prompt.submit hook. No secrets are involved.

CI

Root vitest run collects *.test.ts. The seven engine-kit test files import claude-code/testing, which exists only inside claude plugin test, so they are listed in scripts/ci-test-baseline.txt with a comment, following the ruflo-x-gateway precedent. The lead authorized this for these files only. The pure readers still run under root vitest through tests/parse.spec.ts (6/6).

Packages

None need a version bump or publish: this is plugin content only, and its version is now 0.3.0. The marketplace entry (if a description or version sync is wanted) belongs to mods-planner.

🤖 Generated with RuFlo

ruvnet added 8 commits October 1, 2026 17:26
…ctions, commands

Function-hooks module for the ruflo-swarm plugin: reads ruflo's swarm,
agent, task, claims and hive-mind files via $.fs, merges Claude Code's own
subagents, draws a live Pane, and acts through fixed-argv ruflo CLI calls
with confirm-on-destructive and verify-on-disk.

Co-Authored-By: RuFlo <ruv@ruv.net>
… ruflo run

Co-Authored-By: RuFlo <ruv@ruv.net>
…and audit options

Co-Authored-By: RuFlo <ruv@ruv.net>
…efresh benchmark at 8/32/100 agents

Co-Authored-By: RuFlo <ruv@ruv.net>
…; document the mod

Live run on 2.1.282 showed a named Agent call runs as an in-process teammate
listed as type 'teammate', and a loop whose tool calls reach the hooks with no
spawn or listing. Name the first, call the second a subagent by id suffix.

Co-Authored-By: RuFlo <ruv@ruv.net>
… argv value

Co-Authored-By: RuFlo <ruv@ruv.net>
…ract); keep pane state per folder

Reads agents[id].config.host (kind ruos), .claude-flow/ruos/hosts.json and the
tail of .claude-flow/ruos/events.jsonl as agreed with the ruflo-ruos lane:
ids checked, names cleaned and bounded, free text (error, lastResult) never
read, an event log past 2 MB not read. The router pick and selection held in
$.store are now keyed by working directory, and the router line says the
pick's age, after a live 2.1.287 run showed a pick from another folder.

Co-Authored-By: RuFlo <ruv@ruv.net>
They import 'claude-code/testing', which exists only under
`claude plugin test plugins/ruflo-swarm`; root vitest cannot resolve it
(same pattern as ruflo-x-gateway). The pure readers still run under root
vitest via tests/parse.spec.ts.

Co-Authored-By: RuFlo <ruv@ruv.net>
ruvnet added a commit that referenced this pull request Oct 1, 2026
…artbeat

- every remote command gets a fresh 64-bit nonce; markers print as
  RUOS<nonce>_NAME via printf 'RUOS%s_NAME' '<nonce>', so the token never
  appears in the command or its desktop_exec echo; parsers accept only
  whole lines with that command's nonce (test replays raw echoes that
  contain every marker name)
- test that poll command + 2 KiB slice fit the ~4 KiB cap, echo counted
- pid file holds digits only; audit hash masks the nonce (stable)
- scripts/ci-test-baseline.txt: the 6 node:test files (run by smoke.sh)
  that root vitest reports as 'No test suite found' (lead-authorized)
- hosts.json heartbeatAt is ISO for the swarm pane (#3607)

Co-Authored-By: RuFlo <ruv@ruv.net>
ruvnet added a commit that referenced this pull request Oct 1, 2026
…lerant $ (ADR-404)

- engine.create adds a flat $.ruflo noun (types/index.d.ts, plugin.json
  "types"): segment({id,text}), lastRoute(), snapshot(). Other mods (ruOS,
  ruflo-ruos) contribute status segments instead of drawing a second bar.
  Segments are untrusted: id token, control/bidi stripped, 48 chars, max 8,
  sorted by id. One drawer for the status line, from the $ beneath.
- plugin.register trust gate: names what a later-registering user-tier mod
  can do (process.run, http.fetch, env.set; tool.check, tool.call, *,
  classic.*, plugin.register, prompt.compose), from the host scan.
  userConfig modTrust: observe (default) | refuse-risky | off, plus
  modTrustAllow. Under refuse-risky a gate failure refuses.
- every ui.status/log/toast call tolerates refusal (admins can withhold $)
- tsconfig extends the types Claude Code >= 2.1.287 writes on load
- kit tests: 15/15 on Claude Code 2.1.287, incl. two tighten-only tool.check
  mods composing, trust refusal at load, budget ladder with options
- vitest: 95/95 (noun/segment and trust suites added)
- CI ratchet baseline: the four engine-kit files (claude-code/testing is not
  resolvable by root vitest), as #3605/#3607 do

Co-Authored-By: RuFlo <ruv@ruv.net>
…ust gate

Co-Authored-By: RuFlo <ruv@ruv.net>
@ruvnet
ruvnet merged commit fbaeca0 into main Oct 1, 2026
113 checks passed
ruvnet added a commit that referenced this pull request Oct 1, 2026
Resolves .claude-plugin/marketplace.json and scripts/ci-test-baseline.txt
after #3608/#3607 merged: both sides appended entries, so the resolution
keeps both (ruflo-mods + ruflo-ruos marketplace entries; all baseline blocks).

Co-Authored-By: RuFlo <ruv@ruv.net>
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