* feat: Grok Build host surface + host-agnostic team bus (ADR-402) Adds `ruflo init --grok` and nine team_* MCP tools. Together they let a lead agent on Grok Build (or Codex, or Claude) run a named agent team without Claude's SendMessage. The team state lives in the project under .claude-flow/teams/. Team bus (team_create, team_spawn, team_send, team_broadcast, team_inbox, team_plan, team_status, team_on_stop, team_shutdown) - One implementation: templates/grok/scripts/grok-team-bus.mjs (ops, CLI) plus grok-team-store.mjs (storage). The MCP tools import these files, the SubagentStop hook imports them, and init --grok copies them into a project as a CLI with no dependencies. The repo root has no second copy. - Mail is scoped per team: teams/<team>/mailbox/<agent>/. team_inbox requires the team, and team_status counts only that team's mail. - team.json updates take a cross-process lock (team.lock, an O_EXCL file holding an owner token) and are written through a temp file and a rename. A stale lock is broken by renaming it aside and checking the token, never by a blind delete, and release removes only the holder's own token. 30 concurrent spawns from separate processes register all 30. - team_shutdown closes the team. After it, spawn, send, plan and on-stop refuse. team_spawn enforces maxAgents. - A broadcast with no members is refused. priority is an integer from 0 to 999, stored as a zero-padded filename prefix. Symlinked mailbox directories are refused. Concurrent drains claim each message by rename, so no message is delivered twice and no drain throws ENOENT. - The spawn description is <role>:<agent>@<team>. The SubagentStop hook parses it to advance that team's plan. When the hook cannot record a team subagent, or the team is ambiguous, it writes the reason to stderr and exits 1. init --grok - Writes .grok/ (config, rules, four agent profiles, a skill, the SubagentStop hook), scripts/ and docs/grok/README.md in the project, and lists every path it wrote. --dry-run writes nothing. - Pins the project MCP server to the running release (ruflo@<version>, which ships team_*) instead of @latest. From a source checkout it points at that checkout's bin/cli.js. Inserted values are escaped as TOML strings, so Windows paths are valid. - Writes nothing outside the project unless --grok-statusline is passed. With the flag, it installs ~/.grok/ruflo/host-statusline.mjs and merges [ui.status_line] into ~/.grok/config.toml using @iarna/toml. The merge leaves an existing status row alone in any TOML form. It writes only when the re-parsed result keeps every other value, and otherwise prints the snippet to paste. The command is `node "<absolute path>"`, which needs no shell and runs no script from the repo. - The status row expects only the agent-teams-grok skill. Also classifies team_* in the capability brain (swarm-coordination) and adds ADR-402. Tests: __tests__/team-tools.test.ts, __tests__/grok-init.test.ts. Co-Authored-By: RuFlo <ruv@ruv.net> * fix(grok-host): address round-2 review of ADR-402 team bus (#3512) N1 (blocking): root package.json "files" listed cli/dist but not cli/templates, so npm pack (and npx claude-flow) shipped a build that team_create / init --grok couldn't find grok-team-bus.mjs / grok-team-store.mjs from. Added v3/@claude-flow/cli/templates/** and verified with a real npm pack + tar extract (files present with the fix, absent without it). N2: breakStaleLock renamed a stale lock without re-checking it was still the lock it first observed. A waiter that read a stale token could have that exact lock broken by someone else and re-acquired fresh in between, and the rename would steal that live lock out from under its holder, opening a window for a fourth party to grab the vacated slot — two holders at once. Now re-verifies token + mtime immediately before renaming and backs off (no rename at all) if either changed. Added a direct regression test (monkeypatched fs to force the exact interleaving) plus a concurrent-CLI-process test with a pre-seeded stale lock. N3: LOCK_WAIT_MS (10s) was shorter than LOCK_STALE_MS (30s), so any acquire attempted in the ~20s gap between "waiter gives up" and "lock considered stale" failed even with nothing actually stuck — and the SubagentStop hook's own 5s command timeout couldn't survive either value. Lowered stale/wait to 4s/4.5s (wait >= stale, both comfortably under the hook's 5s budget with real critical sections being sub-millisecond JSON r-m-w). Also replaced the synchronous Atomics.wait lock-wait loop with an async setTimeout-based poll (sleepMsAsync), so an MCP server keeps serving other requests while a caller waits for the lock; threaded async/await through withTeamLock/createTeam/spawnMember/runCli/main and the SubagentStop hook and team-tools.ts's dispatcher. sleepMs (sync) stays only for writeJsonAtomic's bounded Windows rename retry. Minor 1: --grok-statusline replaced a symlinked ~/.grok/config.toml with a plain file, destroying the symlink. writeFileAtomic now resolves an existing symlink and writes through it instead. Minor 2: a TOML integer above 2^53 in the user's config parses to a BigInt (@iarna/toml) and crashed JSON.stringify inside the status-line merge, which ran as the LAST step of init — leaving project files already written. Now validated up front, before any file is touched, naming the offending key; init fails clean with nothing written instead of half-done. Minor 3: host-statusline.mjs matched `trusted = true` / `[mcp_servers.x]` via substring/regex, so a commented-out trusted=true read as trusted and an `enabled = false` server still counted as loaded. Can't add a runtime dependency on @iarna/toml here (this script ships as a zero-dependency copy outside any node_modules tree — project scripts/ and ~/.grok/ruflo/host-statusline.mjs). Added a small comment-aware table/key parser instead and used it for grok, and for codex's mcp_servers check too (same bug class). Minor 4: team_create --force reopened a team but left the old incarnation's mailboxes on disk. Now clears mailbox/ when recreating. Minor 5: priority accepted "1e2" and "0x10" (Number() coerces both). Now requires a plain base-10 integer literal string. Minor (stray changes): reverted an incidental .helpers-version bump and a router.js mode change (644->755) picked up from a concurrent session's helper auto-refresh — not part of this PR. Verified: cli build + tsc --noEmit clean; grok-init.test.ts and team-tools.test.ts (41 tests, including 8 new ones) and guidance-brain.test.ts pass; full `npm test` for @claude-flow/cli is green except 20 pre-existing failures across 16 files, all from @claude-flow/memory / @claude-flow/mcp / @claude-flow/codex not being built in this environment (none touch grok/team/statusline code). Co-Authored-By: RuFlo <ruv@ruv.net> * fix(grok-host): address round-3 review nits on the team lock and status line (#3512) A lock whose holder pid is still alive is no longer broken after 4s, so a stalled writer does not lose an update. An existing [ui.status_line] no longer fails init when another value is a BigInt. Quoted trusted-folder keys may contain ]. Co-Authored-By: RuFlo <ruv@ruv.net> * fix(grok-host): keep the TOML parser out of the MCP tool import path (#3512) grokTemplatesRoot moves to init/grok-templates.ts, which has no dependencies. The team_* tools import it from there, so loading the MCP tool table no longer pulls in grok-generator and @iarna/toml. On current main, analyze-json-output.test.ts runs the analyze command from a copied source tree with only cli-core and security beside it, and failed 20 of 20 with "Cannot find package '@iarna/toml'" before this change. grok-generator re-exports grokTemplatesRoot, so its callers are unchanged. Co-Authored-By: RuFlo <ruv@ruv.net> --------- Co-authored-by: RuFlo <ruv@ruv.net>
11 KiB
Ruflo on Grok Build
How to run Ruflo inside Grok Build after npx ruflo init --grok.
Host contract checked against Grok Build 1.0.41 (grok 1.0.41, macOS, 2026-09-27) and the user guide shipped with that binary (~/.grok/docs/user-guide/). Newer Grok builds change spawn arguments and which keys a project config may set. Run the check in When Grok updates before treating this page as current.
Ruflo is the ledger: memory, swarm records, the team bus, hooks. Grok is the executor: it edits files, runs the shell, and calls spawn_subagent. After team_create or swarm_init, keep working. Those calls record coordination. They do not write the feature.
What init --grok writes
| Path | Purpose |
|---|---|
.grok/config.toml |
Project MCP server ruflo and permission allows |
.grok/rules/ruflo-grok.md |
Host doctrine loaded as a project rule |
.grok/agents/ |
Session profiles (ruflo-architect, ruflo-coder, ruflo-tester, ruflo-reviewer) |
.grok/skills/agent-teams-grok/ |
Skill the lead follows to spawn a pipeline |
.grok/hooks/subagent-stop-team.json |
SubagentStop → team bus idle-assign |
scripts/grok-team-bus.mjs, scripts/grok-team-store.mjs |
Team bus CLI (same store as the MCP team_* tools) |
scripts/grok-subagent-stop-hook.mjs |
Hook script: advances the plan when a teammate stops |
scripts/host-statusline.mjs |
The "what is loaded" line (see Loaded status) |
docs/grok/README.md |
This guide |
Setup
From the app you want Grok to work in (any repo, not only the Ruflo monorepo):
npx -y ruflo@latest init --grok
# see what it would write, write nothing:
# npx -y ruflo@latest init --grok --dry-run
# overwrite an earlier scaffold:
# npx -y ruflo@latest init --grok --force
init --grok writes only inside this project, and prints every path it wrote. It pins [mcp_servers.ruflo] to the Ruflo version that ran it (for example ruflo@3.48.0), because the team_* tools ship in that same release and an older @latest from a stale npx cache would lack them. Run from a Ruflo source checkout, it points the server at that checkout's bin/cli.js instead; re-run it from a published release before you commit .grok/config.toml.
Then:
- Trust the folder.
/hooks-trust, or launch withgrok --trust. On 1.0.41 one grant covers project MCP, hooks, skills, and project rules together. The decision is stored in~/.grok/trusted_folders.toml. - Restart Grok with this folder as the workspace so
.grok/config.tomlloads. - Confirm the server:
grok mcp list # ruflo (project)
grok mcp doctor ruflo # handshake ok; team_create present once this build is the server
init writes the MCP block itself. To register by hand, scope it to the project. The default scope is your user config (~/.grok/config.toml), which would install Ruflo for every repo:
grok mcp add ruflo --scope project -- npx -y ruflo@<version> mcp start
Use a version that ships team_* (the one init --grok pinned). -s project is the same flag. Cold npx often exceeds the 30s default startup timeout; the scaffold sets startup_timeout_sec = 120.
Use it in a session
Grok does not put each Ruflo tool on the model as its own function. The model has search_tool and use_tool.
search_toolwith a keyword (team_create,memory_search,swarm_init).use_toolwith the catalog key:ruflo__team_create,ruflo__memory_search.- Stay on a small subset. A full Ruflo server exposes hundreds of tools; keyword search is how the model reaches them.
MCP parameter names on the team tools are team and agent.
Agent teams
team_create → team_plan → team_spawn → spawn_subagent → team_send / team_inbox → team_on_stop → team_shutdown
team_spawn returns a plan. The lead calls spawn_subagent with the top-level prompt and only host.grok.spawn:
spawn_subagent({
prompt: spawnPlan.prompt,
description: spawnPlan.host.grok.spawn.description,
background: spawnPlan.host.grok.spawn.background,
isolation: spawnPlan.host.grok.spawn.isolation
})
host.grok.advisory.capability_mode and subagent_type describe the role. They are not arguments of spawn_subagent on 1.0.41. Passing them fails the call. The child is general-purpose. The prompt states the read-only constraint. isolation: "worktree" is the setting Grok enforces, and it is how writers avoid editing the lead's tree.
Only the lead spawns. A child that calls spawn_subagent hits the depth limit (1).
Pass description through unchanged. It is <role>:<agent>@<team>, and the SubagentStop hook parses it to mark that agent idle and advance that team's plan. If a description has no @team, the hook uses the one active team that lists the agent; when several do, it advances nothing and says so on stderr.
How the bus behaves:
- State and mail live in
.claude-flow/teams/<team>/(team.json,mailbox/<agent>/). Mail is per team:team_inboxneedsteamandagent, andteam_statuscounts only that team's mail. team.jsonchanges take a lock and are written to a temp file and renamed, so parallel spawns and hooks do not drop updates.team_spawnrefuses once the team hasmaxAgentsmembers (re-spawning an existing member is fine).team_shutdowncloses the team: spawn, send, broadcast, plan, and on-stop refuse afterwards.team_inboxandteam_statusstill work so remaining mail can be drained.team_create --forcereopens the name.- A broadcast (
to: "*") goes to current members and is refused while the team has none. priorityis an integer 0-999; lower is read first.- Several teams can be active at once; every call names its team.
.grok/agents/ruflo-coder.md and the other profiles apply when you start Grok as that agent (grok --agent-profile ruflo-coder or /agents). Spawn does not select them.
CLI bus, same plan shape, no MCP required:
node scripts/grok-team-bus.mjs create --name feature-x --topology hierarchical
node scripts/grok-team-bus.mjs plan --team feature-x \
--steps '["architect","developer","tester","reviewer"]'
node scripts/grok-team-bus.mjs spawn --team feature-x --agent architect --role architect \
--prompt "Design the feature." --next developer
Map the printed spawnPlan.host.grok.spawn the same way. Skill: agent-teams-grok.
What the project config loads
On 1.0.41, .grok/config.toml contributes:
[mcp_servers][plugins][permission][mcp].max_output_bytes
Other tables, including [subagents], are read from ~/.grok/config.toml only. The scaffold used to write [subagents] enabled = true into the project file. That key does nothing there. Subagents are on by default.
Permission allows in the scaffold use Grok's native forms, Bash(...) and MCPTool(ruflo__*). Deny still beats allow.
Hooks
.grok/hooks/subagent-stop-team.json runs on SubagentStop and calls scripts/grok-subagent-stop-hook.mjs. It runs only after the folder is trusted. $CLAUDE_PROJECT_DIR is set (alias of GROK_WORKSPACE_ROOT).
UserPromptSubmit hooks that allow the prompt do not inject stdout into the model on 1.0.41. If a route hook seems to vanish, read .swarm/route-latest.md or call the route tool. That is a host limit, not a missing Ruflo file.
Loaded status
scripts/host-statusline.mjs is the shared "what loaded" line: RuFlo loaded │ ruflo │ rules │ 4 agents │ skill │ hook │ trusted, or the names of what is missing. Claude shows its own richer statusLine. Codex has no status row; node scripts/host-statusline.mjs --host codex prints the same facts.
Grok reads [ui.status_line] only from your user file, ~/.grok/config.toml; a project config cannot set it. init --grok does not touch that file unless you ask. It prints the snippet to add, or with --grok-statusline it:
- copies the script to
~/.grok/ruflo/host-statusline.mjs, and - merges this into
~/.grok/config.toml(parsed with a TOML parser; an existingui.status_linein any form is left alone, and a file it cannot merge safely is not changed — you get the snippet instead):
[ui.status_line]
type = "command"
command = 'node "/home/you/.grok/ruflo/host-statusline.mjs"'
The command is an absolute path to your own copy, so no repo can substitute a script of its own. It reports on whichever folder Grok is in. Restart Grok after the user file changes.
Prove it
From a Ruflo checkout (this script is not copied by init --grok; run init --grok in the checkout first):
node scripts/probe-host-live.mjs --host grok
node scripts/probe-host-live.mjs --host all --no-execute
node scripts/probe-host-live.mjs --host grok --live
One contract (discover, connect, SessionStart receipt, optional memory round trip) with a Grok, Claude, or Codex adapter. A check that CLI cannot answer is a skip, not a pass.
When Grok updates
The July 2026 host work assumed Grok 1.0.34 and a spawn schema that accepted subagent_type and capability_mode. 1.0.41 dropped both from the model-facing tool and stopped applying [subagents] from project config. The same class of drift will happen again.
After grok --version changes:
- Open
~/.grok/docs/user-guide/16-subagents.mdand read Spawning Subagents. The parameter table is the contract. Forward a key only if it is listed. - Open
26-config-reference.mdand confirm which sections a project.grok/config.tomlmay set (1.0.41: mcp servers, plugins, permission, mcp output cap). - Open
07-mcp-servers.mdand confirmgrok mcp add --scopestill defaults touser, and that tools are stillsearch_tool/use_toolwithserver__toolnames. - Open
10-hooks.mdand confirmSubagentStop, folder trust, and$CLAUDE_PROJECT_DIR. - Open
22-permissions-and-safety.mdand confirmMCPTool(server__tool)andBash(...)still match the allows in.grok/config.toml. - Spawn once with
host.grok.spawnonly. If the tool rejectsbackground, the user guide's name for that flag isrun_in_background— use the name the live schema accepts. - Update the
contractstring inteam_spawn/scripts/grok-team-bus.mjsand this page to the version you just checked.
grok inspect shows which rules, skills, and MCP servers the trusted folder actually loaded.
Optional
npx -y ruflo@latest daemon start
npx -y ruflo@latest doctor
RuvNet Brain is off in the scaffold. Uncomment [mcp_servers.ruvnet-brain] in .grok/config.toml after npx ruvnet-brain@latest, and set env.KB_DIR to the absolute ~/.cache/ruvnet-brain/kb path. init --grok fills in your home directory for those paths when it creates the file.
Architecture: ADR-402 (Host-Agnostic Agent Teams), in the Ruflo repo at v3/docs/adr/ADR-402-host-agnostic-agent-teams.md.