Skip to content

feat: Grok Build host surface + host-agnostic team bus (ADR-402) - #3512

Merged
ruvnet merged 5 commits into
ruvnet:mainfrom
weave-logic-ai:feat/grok-host-adr320
Oct 20, 2026
Merged

ruvnet merged 5 commits into
ruvnet:mainfrom
weave-logic-ai:feat/grok-host-adr320

Conversation

@aepod

@aepod aepod commented Sep 28, 2026 •

Copy link
Copy Markdown
Contributor

What this adds

ADR-402 makes Ruflo's Agent Teams work as a host-agnostic MCP surface instead of a Claude Code-only feature built on SendMessage/Task. The team bus (create/spawn/send/inbox/broadcast/plan/status/on_stop/shutdown) now lives in Ruflo as team_* MCP tools backed by a filesystem mailbox under .claude-flow/teams/{teamId}/, with team_spawn returning a spawn plan keyed by host (host.grok.spawn, host.claude.taskType, ...) so any host adapter can drive it. Claude Code's teammate-plugin stays as one adapter rather than the only one.

The first non-Claude adapter is Grok Build. npx ruflo init --grok scaffolds .grok/config.toml, .grok/rules/, .grok/agents/*.md, .grok/skills/, and a SubagentStop hook wired to team_on_stop, mirroring the existing init --codex pattern. On Grok Build 1.0.41 the lead calls spawn_subagent directly with the plan Ruflo hands back; it doesn't need Grok to have its own SendMessage-equivalent.

Also included:

  • docs/grok/README.md and README-GROK.md: the operator guide and the monorepo/pre-publish guide for using Ruflo from Grok Build, including running it against an external project rather than the ruflo monorepo itself.
  • scripts/bench-grok-host-conformance.mjs: a no-LLM conformance bench that checks the MCP/CLI surface Grok is supposed to call (host surface, tool inventory, team lifecycle, swarm, hive-mind, learning loop, neural, CLI parity, init product) — this is what backs the ADR-402 Phase 4 claim, not a live Grok run.
  • scripts/probe-host-live.mjs / scripts/probe-grok-host-live.mjs: live, single-machine probes of an actually-installed Grok/Claude/Codex binary and its MCP config. These aren't part of the conformance claim and their sample output isn't included in this PR (see "left out" below).
  • scripts/grok-team-bus.mjs, scripts/grok-subagent-stop-hook.mjs: the hook-side implementation the templates install.
  • v3/@claude-flow/cli/src/mcp-tools/team-tools.ts and the memory-tools/memory-initializer changes needed to support it (tag-preserving memory export/import round-trips, team_* MCP tool registration).

Two merge conflicts came up bringing this current with upstream/main (v3/@claude-flow/cli/src/mcp-tools/memory-tools.ts and .../src/memory/memory-initializer.ts), both in code this branch didn't originally touch that upstream had since changed independently. Both were resolved to keep upstream's newer behavior (the dbPath-aware storeEntry call with a real success check, and the TTL-aware liveMemoryRowSql() helper) plus this branch's tag-preservation on import — not by picking one side wholesale.

The design record is ADR-402 (v3/docs/adr/ADR-402-host-agnostic-agent-teams.md). It was drafted as ADR-320 on the fork; 320, 400 and 401 are already taken upstream, so it is renumbered here.

Relationship to #3373

#3373 (open, fixes #3372) makes Grok Build a detected host: a row in defaultHostRegistry(), maybeAutoDetectGrok, and the trust instruction. This PR is complementary: it adds the team bus as team_* MCP tools and an explicit init --grok scaffold for Grok's agents, rules, skills and SubagentStop hook. It follows the same line on folder trust. init --grok prints the /hooks-trust / grok --trust step and never writes ~/.grok/trusted_folders.toml. Both PRs edit v3/@claude-flow/cli/src/commands/init.ts, so whichever lands second needs a rebase there; I'm happy to do that one.

How this was tested

All of this was run locally against the merged tree (upstream/main + feat/grok-host, after the fork-local cleanup below), node 22.23.1 / pnpm 11.13.0:

  • pnpm install --frozen-lockfile in v3/ — clean, no lockfile changes needed.
  • pnpm build (pnpm -r build, same as the CI build job) — all 25 workspace packages built with no errors.
  • pnpm typecheck (pnpm -r typecheck, same as CI) — exit 0, no errors.
  • pnpm test (vitest run, same as CI's test job) — full v3 suite. Failures seen: two memory-command tests in @claude-flow/cli/__tests__/commands.test.ts ("should search memory" / "should list memory entries") that need a real memory DB the test file never seeds (memory store is it.skip'd there as "requires live memory service"), plus a handful of graph-analyzer.test.ts and provider-integration.test.ts (Ollama-not-running) failures that vary between the pnpm-hoisted duplicate copies of the same spec file. I reproduced the same two commands.test.ts failures, deterministically, on a clean upstream/main checkout with no grok-host changes at all — they're pre-existing and unrelated to this branch, not something this PR introduces.
  • The three extra scripts the CI test job runs from v3/@claude-flow/cli: node scripts/test-mcp-policy-stdio.mjs (10/10 pass), node scripts/smoke-flywheel-proof.mjs (PASSED), node scripts/replay-generation.mjs (PASSED, offline clean-room replay).
  • node scripts/bench-grok-host-conformance.mjs (the feature's own bench) — 91/92 checks pass, 0 critical failures, 1 warning. The report ADR-402 points to (docs/benchmarks/grok-host-conformance-latest.md) is this run.
  • The CI witness-verify job's check: node plugins/ruflo-core/scripts/witness/verify.mjs --manifest verification/macos/manifest.md.json --json against the built tree returns ok: true, regressed: 0 (89 pass, 28 drift, 0 missing) with upstream's manifest unchanged. This PR doesn't touch verification/.

What was deliberately left out of this branch, and why

The fork branch (weave-logic-ai/ruflo feat/grok-host) carried some things that only make sense there:

  • data/clone-data.ledger.json/.proof.json/.rvf: the fork's own clone-tracker bot state, unrelated to this feature. Restored to upstream's version.
  • docs/handoff.md: a personal session handoff note. Removed; doesn't exist upstream.
  • A handoff Grok skill (.grok/skills/handoff/ and its templates/grok/skills/handoff/ copy). It wrote session notes to docs/handoff.md, a personal workflow rather than default Ruflo behavior, and init --grok would have installed it into every project. Removed along with its references in the bench file list, the init --grok output, and both Grok READMEs.
  • docs/benchmarks/{grok-host,host}-live-latest.{json,md}: one-machine output from the live probe scripts (installed binary paths, local MCP config detection). Not referenced by any doc or script, so removed rather than scrubbed.
  • docs/benchmarks/grok-host-conformance-latest.{json,md}: this one is referenced (ADR-402, README-GROK.md), so it's kept but regenerated fresh and with the CLI/repo path made repo-relative instead of this machine's absolute worktree path.
  • verification/macos/: the fork had a regenerated witness manifest and a local witness run in history.jsonl, both keyed to the fork branch. Upstream's versions are kept; they verify clean against this tree.
  • agentdb.rvf / agentdb.rvf.lock (root and v3/@claude-flow/cli/) and the related .gitignore edits: the fork untracked these as a side cleanup. Out of scope here, so they're left exactly as upstream has them.
  • Fork issue-tracker ids in the memory-tools / memory-initializer / memory-bridge comments: stripped; the comments themselves stay.
  • .grok/config.toml (the ruflo repo's own dev config, not the shipped template under v3/@claude-flow/cli/templates/grok/, which was already portable): switched the local-CLI MCP override to a path relative to the repo root and commented out the ruvnet-brain block instead of hardcoding this machine's home directory, matching the template's {{HOME}}-placeholder convention.

None of this touches the feature's actual surface — the MCP tools, the CLI, the templates, the ADR, or the operator docs are unchanged by the cleanup.

Follow-ups

  • The conformance bench and the live-probe scripts overlap in what they check host discovery/config for; worth folding probe-host-live.mjs's installed-binary detection into the conformance bench (or clarifying the split in the scripts' own doc comments) rather than keeping them as three separate mjs entry points.
  • verification/macos/manifest.md.json currently carries 28 "drift" entries (content changed since it was last signed, marker still present, not a regression) — worth a regen pass with real release versions once this lands, rather than as part of this PR.
  • ADR-402 still needs the "live Claude Task/SendMessage side-by-side" comparison it calls out as unaddressed in the Phase 4 note.

🤖 Generated with RuFlo

@aepod
aepod requested a review from ruvnet as a code owner September 28, 2026 03:49
@aepod
aepod force-pushed the feat/grok-host-adr320 branch 4 times, most recently from fef3d8d to 9f0f8f4 Compare September 28, 2026 04:55
@aepod aepod changed the title feat: Grok Build host surface + host-agnostic team bus (ADR-320) feat: Grok Build host surface + host-agnostic team bus (ADR-402) Sep 28, 2026
@aepod
aepod force-pushed the feat/grok-host-adr320 branch from 9f0f8f4 to a0f8cd7 Compare September 28, 2026 05:16
};

function readPrompt(path) {
return spawnSync('/bin/cat', [path], { encoding: 'utf8' }).stdout || '';
aepod added a commit to weave-logic-ai/ruflo that referenced this pull request Sep 28, 2026
Extend the host-agnostic team bus from ruvnet#3512 beyond Grok.

- Host-adapter seam (mcp-tools/team-hosts/): one TeamHostAdapter per
  host (grok, claude, codex, command) supplies the spawn plan, the
  protocol lines and stop-identity parsing. Grok's plan output is
  unchanged.
- Codex host: team_spawn returns a `codex exec` plan (prompt on stdin,
  --json events, -o last message, no bypass flags). `ruflo team run`
  executes it; turn.completed with exit 0 is done, turn.failed, a
  non-zero exit or a timeout is failed. No hook trust is needed on this
  path.
- Generic command host: any agent CLI declared in
  .claude-flow/team-hosts.json (argv only, never a shell, strictly
  validated) can be a teammate.
- `ruflo team <verb> --params`, `team run` and `team hook-stop`. The
  runner drains the member's inbox into a "Messages for you" block in
  the prompt, so a child without Ruflo MCP still gets queued work, and
  re-queues them if the run fails. A failed run reports to the lead
  only and does not advance the plan.
- team_on_stop takes an outcome, runId (duplicates are no-ops) and
  reason, and normalizes `role:agent` names.
- team.json gets schemaVersion 1: v0 data is read and upgraded, newer
  versions are refused, writes use an exclusive lock plus tmp+rename.
  The team tools are the only writer; the Grok bus script becomes a
  shim over `ruflo team`.
- init --codex adds an Agent Teams section to AGENTS.md, an
  agent-teams skill, and a SubagentStop hook in .codex/hooks.json
  (on by default, --no-team-hooks to skip). Hooks call the local CLI
  (`npx --no-install ruflo`), never ruflo@latest.
- @claude-flow/codex: extract runHeadlessProcess and
  buildWorkerEnvironment; version 3.0.4.
- Host conformance bench covers codex and command hosts; the live
  probe spends model turns only with --execute / --live.
- ADR-402 rewritten as one ADR: bus, format, adapter seam, command
  host, runner and stop signal, then Codex, Grok and Claude.

Co-Authored-By: RuFlo <ruv@ruv.net>
@aepod
aepod force-pushed the feat/grok-host-adr320 branch from 0dee8bb to a0f8cd7 Compare September 28, 2026 10:58
@aepod

aepod commented Sep 28, 2026

Copy link
Copy Markdown
Contributor Author

Added 0dee8bb81 — feat(grok): show a loaded-status row from the user config.

init --grok appends [ui.status_line] to ~/.grok/config.toml when that block is missing. Grok Build 1.0.41 reads the row from the user file. The script prints whether the Ruflo MCP server, rules, four agents, two skills, the team hook, and folder trust are present.

@proffesor-for-testing proffesor-for-testing left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks Mat — the team-bus direction is right, and the core is in good shape: pnpm install --frozen-lockfile, the cli build and tsc --noEmit are clean (0 errors), the new team-tools / ollama-local-default-model tests pass (9/9), and 250 related existing tests pass on this head and on main (b14c79e) alike. The spawn plan does carry only prompt, description, background, isolation, and path traversal in team names is rejected.

Requesting changes for the items below. Each (R) was reproduced on head 0dee8bb81; (C) is from reading the code, cross-checked by a second reviewer (Codex).

Blocking

  1. init --grok can corrupt a valid ~/.grok/config.toml (R). grok-generator.ts:136-149 detects an existing status line only by the literal header line [ui.status_line], then appends a new block. Existing valid configs using an inline table ([ui] + status_line = {…}), dotted keys (status_line.type = …), [ ui.status_line ] or [ui."status_line"] all become invalid TOML (tomllib: Cannot declare ('ui','status_line') twice). Grok would then presumably fail to read the user config.
  2. Silent write to the user's home (R). grok-generator.ts:245-254 writes (or creates) ~/.grok/config.toml on every init --grok, with no opt-out, no dry-run, and the path is not listed in the init summary. Suggest: make it opt-in (e.g. --grok-statusline) or print the snippet for the user to paste; if writing, parse and merge with a TOML library.
  3. Mailboxes are per agent name, not per team (R). team-tools.ts:148,253,450-458 — team_inbox validates team and then ignores it. A message sent to coder on team A was returned by team_inbox for coder on team B. team_status also counts other teams' mail.
  4. Lost updates on team.json (R). team-tools.ts:169-180,341-366 and scripts/grok-team-bus.mjs:94-96 do read-modify-write with no lock and no atomic rename. 30 parallel team_spawn calls left 13/30 members registered. Parallel SubagentStop hooks can drop plan advances the same way.
  5. The SubagentStop hook never advances the plan (R). The spawn plan sets description: "role:agent" (team-tools.ts:183-188); the hook falls back to description as the agent name (grok-subagent-stop-hook.mjs:51-72), and on-stop rejects coder:alice (: is not allowed). Stdio is ignored, so it fails silently — the plan stayed at ['ready','pending'].

Major

  1. The advertised status row can't be reached (R). host-statusline.mjs:120 expects two skills (agent-teams-grok, handoff) but only agent-teams-grok ships, so after init the best output is RuFlo │ missing skills 1/2.
  2. User-level status command runs a repo-relative script in every repo (C). grok-generator.ts:123 installs sh -c 'test -f scripts/host-statusline.mjs && node scripts/host-statusline.mjs' into the user config, so any repo opened in Grok that ships scripts/host-statusline.mjs gets it executed — unless Grok gates status-line commands on folder trust. Please pin it to an absolute path or a ruflo subcommand.
  3. team_shutdown doesn't close the team, and maxAgents isn't enforced (C). team_shutdown only sets status (:650-661), but spawn and send check only that the team exists (:341, :390). maxAgents is stored (:305) and never checked at spawn.
  4. Fresh install can't use what init advertises (C). The generated MCP config launches ruflo@latest (templates/grok/config.toml:12-28, which itself notes a local build is needed until a release ships team_*), while init.ts:449 prints team_create / team_spawn as next steps.
  5. Windows (C). The status command requires sh, and a Windows home path inserted into a TOML basic string keeps backslashes (\U…), which is an invalid escape (grok-generator.ts:115-135).

Minor

  • Broadcasts sent before any member exists go to _broadcast/ (:413-426), which team_inbox can never read (names must start alphanumeric) (R).
  • priority is unvalidated: "abc" produced NaN_msg_….json; filenames sort lexically, so 10_ sorts before 2_ (R).
  • A mailbox directory that is a symlink is followed on write (:417-431) (C).
  • Two concurrent inbox drains throw ENOENT on renameSync; activeTeam() picks the first active team when several are active (C).

Scope

  • The memory tag-filter / export v1.1 changes (memory-bridge.ts, memory-initializer.ts, memory-tools.ts) and the provider-precedence / Ollama default-model changes (agent-execute-core.ts:190,201,508-523) aren't Grok-related and change behaviour for existing users: with an explicit provider set, a stray OPENROUTER_API_KEY no longer wins, and claude-* model IDs now map to the Ollama default. Could those go in their own PR? (The tag filter also loads up to 100k rows into memory.)
  • scripts/* are byte-identical copies of templates/grok/scripts/*, and the repo-root .grok/ and docs/grok/README.md have already drifted from the templates. The 1,785-line benchmark JSON, 1,101-line bench script and 464-line README-GROK.md would be easier to review separately.

CI / merge order

  • No checks appear in the PR UI because this is a fork PR: 28 workflow runs on 0dee8bb8 are action_required, waiting for maintainer approval.
  • #3513 is stacked on a0f8cd71e without 0dee8bb81; the only rebase conflict is scripts/probe-host-live.mjs (see the review there).

Review method: three agent reviewers built each PR in isolated worktrees and ran the new and related suites plus live CLI/MCP probes, compared against a main baseline; Codex CLI then cross-checked the findings and added the (C) items. — Dragan (QE)

@aepod

aepod commented Sep 28, 2026

Copy link
Copy Markdown
Contributor Author

Thanks, Dragan. That was a thorough review, and the reproductions made the fixes easy to test. I force-updated the branch with a single commit on main (fa61df2). Point by point:

Blocking

  1. ~/.grok/config.toml corruption. init now parses the user file with @iarna/toml, which the cli already depends on. If ui.status_line exists in any form (header, [ ui.status_line ], [ui."status_line"], dotted keys, inline table), init leaves it alone. Otherwise it appends the block, re-parses the result, and writes only if the new file parses, holds exactly our row, and leaves every other value unchanged. It writes through a temp file and a rename. A file it can't parse, or can't merge safely (for example ui = {…} as an inline table), is not touched, and init prints the snippet instead. grok-init.test.ts covers all of your cases. It also checks the merged output with Python tomllib when that's available.
  2. Silent home write. Off by default now. Plain init --grok writes only inside the project and prints the snippet to paste. --grok-statusline opts in, --dry-run writes nothing, and the summary lists every path written, including anything under ~/.grok.
  3. Mailboxes per team. Mail now lives in .claude-flow/teams/<team>/mailbox/<agent>/. team_inbox requires team, and team_status counts only that team. The A/B case you reproduced is a test.
  4. Lost updates. Every team.json change takes a cross-process lock (mkdir team.lock, with stale-lock recovery) and is written through a temp file and a rename. There is now one implementation, used by the MCP tools, the CLI and the hook (see Scope below). New test: 15 CLI processes plus 15 MCP calls spawn at the same time, and all 30 register. I also removed the lock temporarily to check the test catches the bug: it failed with 16/30.
  5. SubagentStop never advanced. The spawn description is now <role>:<agent>@<team>, and the hook parses it. The hook calls the bus directly instead of a child process whose output was discarded. If it can't record a team subagent (unknown team, shut-down team, agent on several teams), it prints the reason to stderr and exits 1. Subagents outside any team still exit 0 with no output. Tests: the plan goes to ['done','ready'], the ambiguous and missing-team cases fail with a message, and 12 hooks running in parallel lose no updates.

Major

  1. Status row needed handoff. Removed. That was a personal skill that doesn't ship. After init, the row reports everything as loaded except folder trust, which is set in Grok (there's a test for this).
  2. Repo-relative command. --grok-statusline installs a copy at ~/.grok/ruflo/host-statusline.mjs, and the command is node "<that absolute path>". It needs no sh, and no repo can supply its own script. If the user already has their own status row, init doesn't install anything.
  3. Shutdown and maxAgents. team_shutdown closes the team: spawn, send, broadcast, plan and on-stop refuse afterwards. Inbox and status still work, so leftover mail can be drained. team_spawn enforces maxAgents; re-spawning an existing member doesn't take a new seat.
  4. ruflo@latest. The generated project config pins the MCP server to the version that ran init (ruflo@<version>). That release is the one with team_*, since init and the tools ship together. Run from a source checkout, init points at that checkout's bin/cli.js and prints a note saying so.
  5. Windows. The status command no longer needs sh. Every value inserted into TOML (home directory, CLI path) is escaped as a TOML string, and there's a test with C:\Users\… paths. On Windows the npx launch uses cmd /c npx, the same as .mcp.json.

Minor

  • A broadcast with no members is now refused with a clear error. Mail is never parked in an unreadable _broadcast/.
  • priority must be an integer from 0 to 999, and the filename prefix is zero-padded (002_…), so priority 10 sorts after 2. "abc" is rejected.
  • Mailbox paths are created one directory at a time and a symlink anywhere in the path is refused. Message files are opened with wx.
  • A drain claims each message by renaming it into archive/ before reading it. Two drains running at once each get a separate set of messages and neither throws (test: 40 messages, two processes).
  • Several teams can be active at once. Every call names its team, and the hook resolves the team from the description as described in item 5. I removed activeTeam().

Scope

  • The memory tag-filter / export v1.1 changes and the provider-precedence / Ollama default-model changes are out of this PR. They'll come as their own PR. So will the conformance bench, its JSON report, and README-GROK.md.
  • The drift: the repo-root scripts/grok-*, scripts/host-statusline.mjs, .grok/ and docs/grok/README.md copies are deleted. templates/grok/ is the only source. The MCP tools now import templates/grok/scripts/grok-team-bus.mjs instead of carrying a TS copy, so the CLI and MCP paths can't drift either. scripts/probe-host-live.mjs stays, and it tells you to run init --grok in the checkout first.
  • I also classified team_* in the capability brain. Without it, guidance-brain.test.ts failed on this branch.

Verification (fork head vs. main fa61df2): pnpm install --frozen-lockfile, the cli build, and tsc --noEmit all pass with 0 errors. The new suites pass: team-tools 12 tests and grok-init 21. On the full cli vitest run the branch has 7 failing tests and main has 7. Six fail on both, all in memory or dual-init tests (#2226, commands memory search/list, #2545 ×2, #2636). The seventh on main is #3286 auto-memory, which passes here. The seventh on the branch is guidance-brain, fixed by the capability-brain change above; it now passes alone and in the related suites.

One thing I couldn't verify: whether Grok runs hook commands through a shell on Windows. The SubagentStop hook still uses node "$CLAUDE_PROJECT_DIR/…", like the Claude hooks do.

@aepod
aepod force-pushed the feat/grok-host-adr320 branch from 6f5badc to 19c1afe Compare September 28, 2026 16:49
@aepod

aepod commented Sep 28, 2026

Copy link
Copy Markdown
Contributor Author

One follow-up to the reply above: I force-updated to 19c1afe17, still a single commit, now on current main (3dce4664d).

  • Stale-lock race. Your feat(teams): Codex and generic command hosts for the team bus (ADR-402) #3513 review flagged a race in breaking a stale lock, and the new team-store lock here had the same bug: a waiter that found an old lock deleted it outright, so it could remove a lock another process had just taken. The lock is now a team.lock file holding an owner token. A stale lock is renamed aside and its token re-checked, and it's put back if a live holder re-took it in between. Release deletes the lock only if it still holds the releaser's own token. New test: "breaks a stale lock left by a crashed holder". The team suite now has 13 tests and passed five runs in a row; team, init and guidance-brain together pass 42/42, and tsc shows 0 errors.
  • Rebase. The only new commit on main (fix(gateway): align ChatGPT tool annotations with behavior #3525) touches plugins/ruflo-x-gateway only; the patch applies byte-identically.

aepod added a commit to weave-logic-ai/ruflo that referenced this pull request Sep 28, 2026
Extend the host-agnostic team bus from ruvnet#3512 beyond Grok and Claude,
built on its single store (templates/grok/scripts/grok-team-bus.mjs +
grok-team-store.mjs). No second store: the CLI loads those files for
every read and write.

- Exec hosts (mcp-tools/team-hosts/): a Codex adapter and a generic
  command host declared in .claude-flow/team-hosts.json (argv only,
  never a shell, strictly validated). team_spawn takes `hosts` and
  `model` and adds a host.<label> entry per exec host next to the
  store's grok and claude entries (spawnMember gains `hostPlans` and
  `model`). Unknown team_spawn keys are rejected (`task` suggests
  `prompt`).
- `ruflo team run` runs one headless turn and records the stop:
  - The command is rebuilt from the built-in adapter or the validated
    team-hosts.json entry at run time; the plan stored in team.json is
    never executed.
  - A command host runs only after `ruflo team trust-host <label>`,
    recorded in ~/.claude-flow/ and tied to a digest of the entry.
    Shells, command wrappers and path-like commands also need
    --allow-unsafe-command. passEnv can never re-add secret-named or
    CLAUDE_FLOW_* variables.
  - Refuses a shut-down team, a plan step that is not ready, and a
    second concurrent run of the same agent (claimed under the store
    lock; a dead runner's claim is replaced).
  - Drains only this team's mailbox for the agent into the prompt and
    re-queues to the same team on failure.
  - Result file reads are capped and symlinks ignored; --dry-run shows
    argv, passed env names and trust state.
- onStop takes outcome, runId (repeats are no-ops) and reason; a
  failed stop marks the step failed and does not advance.
- `ruflo team <verb> --params` and `team hook-stop`. hook-stop reads
  stdin to EOF, reports an unparseable payload or ambiguous team on
  stderr instead of recording `{}`, and resolves the team from the
  payload, a `role:agent@team` description, TEAM_NAME, or the only
  active team listing the agent.
- @claude-flow/codex: runHeadlessProcess, killProcessTree and
  buildWorkerEnvironment in dual-mode/process.ts. The child runs in its
  own process group; a timeout or interrupt sends SIGTERM to the group,
  then SIGKILL after a grace period (taskkill /T on Windows), and the
  call returns only after exit. Captured output keeps the head and the
  tail (so turn.completed survives) and decodes UTF-8 once. The
  orchestrator's stopAll kills process trees. No version bump.
- init --codex adds an Agent Teams section to AGENTS.md, an
  agent-teams skill, and a SubagentStop hook in .codex/hooks.json
  (--no-team-hooks to skip).
- init --grok never writes through a symlink inside the project and
  never follows a symlinked template entry.
- probe-host-live: codex/command team round trips; model turns only
  with --execute / --live; no /bin/cat.
- ADR-402 amendment: exec hosts, the trust model (including that
  team_send `from` and hook-stop are unauthenticated on a file bus),
  the runner, and the Codex host.

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

@proffesor-for-testing proffesor-for-testing left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Round 2 on 19c1afe17. Thanks Mat, this is a big improvement: all ten round-1 items are fixed, and we reproduced each fix. One new packaging issue blocks, and it's a one-line fix; once that's in, I'm happy to approve with nits.

Round-1 item Result
1. TOML corruption Fixed. Run from a CLI installed out of an npm pack tarball, with a temp HOME: inline table, dotted keys, [ ui.status_line ] and [ui."status_line"] stay byte-identical. Inline ui = {…}, [[ui]] and unparsable files fall back to the snippet. Merged files parse with python tomllib.
2. Home write Fixed. Plain init --grok and --dry-run write nothing under ~/.grok.
3. Per-team mailbox Fixed. Mail sent to coder on A doesn't show in B's team_inbox.
4. Lost updates Fixed. 15 CLI + 15 MCP parallel spawns register 30/30, and a stale lock is broken. See N2/N3 for what's left.
5. SubagentStop Fixed. coder:alice@H1 advances the plan; ambiguous, missing and shut-down teams print to stderr and exit 1; non-team descriptions exit 0.
6–9 Fixed. The status row is reachable once the folder is trusted. The status script lives at an absolute path under ~/.grok/ruflo/ and is refreshed on re-run. Shutdown closes spawn/send/broadcast/plan/on-stop. maxAgents is enforced. The MCP pin is ruflo@<version>.
10. Windows Read only, not run: values are escaped as TOML strings and sh is gone.
Minors Fixed. Empty broadcasts are refused. Priority abc/-1/1000/1.5 is refused and filenames are zero-padded. Whitespace-only messages are refused.

Blocking

N1: the claude-flow package doesn't ship the templates the team tools now load at runtime (R). The root package.json files list includes v3/@claude-flow/cli/dist/** but not v3/@claude-flow/cli/templates/**, and main has no templates directory at all today, so this PR is the first to depend on it. From an npm pack of the root package:

  • mcp exec -t team_create fails with Cannot find module …/templates/grok/scripts/grok-team-bus.mjs;
  • init --grok fails with "Grok templates not found".

The @claude-flow/cli tarball (which ruflo depends on) does include templates and works end to end, so this only hits npx claude-flow users, a published entry point (3.48.0). Fix: add v3/@claude-flow/cli/templates/** to the root files list.

Strongly suggested

  • N2: stale-lock break can still admit two holders (R, narrow). grok-team-store.mjs:163-185: waiter W1 reads the stale token; meanwhile W2 breaks the lock and C takes a fresh one; W1 then renames C's live lock aside, D takes the free slot with wx, and restoring C's lock fails. A scripted replay of that ordering left C and D both holding the lock. It needs a crashed holder plus three waiters. Suggest: if the token changed, back off and retry instead of renaming.
  • N3: lock wait (10 s) is shorter than the stale window (30 s) (R). After a kill -9 of a holder, spawns in the next ~20 s time out at 10 s, and the SubagentStop hook's own timeout is 5 s (subagent-stop-team.json:9). The wait also uses a synchronous Atomics.wait inside the MCP server, which blocks it (store:16-17,115).

Minor / nits

  • --grok-statusline replaces a symlinked ~/.grok/config.toml (dotfiles setups) with a regular file at mode 0600 (grok-generator.ts:216-221) (R).
  • An integer above 2^53 in the user config crashes init with "Do not know how to serialize a BigInt" after the project files are written. The user config is untouched (grok-generator.ts:162,250) (R).
  • The status row matches text instead of parsing: trusted = true in a comment reads as trusted, and an MCP server with enabled = false still reads as ruflo (host-statusline.mjs:54-76) (R).
  • @iarna/toml is TOML 0.5, so some valid TOML 1.0 (mixed-type arrays) falls back to the snippet. That's safe.
  • Re-creating a team with force keeps its old mailbox (grok-team-bus.mjs:152-158) (C).
  • Priority accepts 1e2 / 0x10.
  • Stray changes: .claude/helpers/.helpers-version bump and router.js mode 644→755.

Build: pnpm install --frozen-lockfile passes, the cli build passes, and tsc --noEmit shows 0 errors. The swarm/shared/memory/neural/cli-core deps need to be built first. team-tools, grok-init and guidance-brain pass 42/42. Merges cleanly onto current main.

Round-2 method: an agent reviewer rebuilt the PR, re-ran the round-1 reproductions (including from an npm packed install), and Codex CLI cross-checked; new Codex claims were verified before inclusion. (R) = reproduced, (C) = read from code. — Dragan (QE)

aepod added a commit to weave-logic-ai/ruflo that referenced this pull request Sep 29, 2026
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>
aepod added a commit to weave-logic-ai/ruflo that referenced this pull request Sep 29, 2026
…n claims (ruvnet#3513)

MAJOR A — secrets reaching command hosts through the inherited environment.
buildWorkerEnvironment's base-environment strip (codex/dual-mode/process.ts)
kept a narrower, separately maintained regex than command.ts's passEnv
validation, and didn't generalize CLAUDE_FLOW_*. A trusted host's base env
still leaked PGPASSWORD, DATABASE_URL, SSH_AUTH_SOCK, GITHUB_PAT, MYSQL_PWD,
SESSION_SECRET_X, MY_AUTH, SLACK_WEBHOOK_URL and every CLAUDE_FLOW_*.
New shared source of truth: codex/src/dual-mode/env-policy.ts's
isProtectedEnvName, imported by both process.ts (buildWorkerEnvironment) and
cli's team-hosts/command.ts (passEnv validation). Regression tests: unit-level
strip check plus an end-to-end spawn of a real `printenv` host with every
poisoned name from the repro.

MAJOR E — SIGKILL escalation cancelled when the leader exits first.
runHeadlessProcess's settle() cleared every pending timer, including a
just-armed SIGKILL escalation, as soon as the group leader's `close` event
fired — which can happen before a descendant that trapped SIGTERM and
detached its own stdio (`exec >/dev/null 2>&1`) actually dies. The escalation
is no longer tracked in the timers settle() clears; it now fires unconditionally
after the grace period. Regression test reproduces the exact
trap/detach scenario and asserts no descendant survives past the grace period.

MAJOR F — re-spawning a member mid-run drops its concurrency claim.
spawnMember (grok-team-bus.mjs) overwrote a member's whole record, dropping
the status/runId/runPid/runHost a live `ruflo team run` had set, letting a
second run start concurrently. Re-spawn now carries a live claim forward.
onStop now validates the runId it's given against the member's *current*
runId before releasing the claim, so a stop report with no runId or a stale
one can't clobber a live one. Tests: respawn-mid-run still blocks a second
run; onStop with missing/stale runId leaves a live claim untouched.

MEDIUM B — unsafe-command list missed interpreters; trust recorded before shown.
trust.ts's UNSAFE_COMMANDS now includes node, python/python3, perl, ruby, npx,
git, find, and other general-purpose interpreters that run code via a flag.
`ruflo team trust-host` now prints the resolved host entry before persisting
the trust record, not after. Tests: interpreter classification, an end-to-end
`node -e` host refused then allowed, and a spy on stdout/fs ordering proving
the preview print precedes the write.

MEDIUM D — SIGHUP not forwarded; drained messages lost on runner death.
team-runner.ts forwards SIGHUP the same way it already forwards SIGINT/SIGTERM
(a closed terminal/SSH session no longer orphans the child untracked). Because
an aborted run already takes the failed-outcome requeue path, this also
requeues drained-but-undelivered inbox messages for the SIGHUP case. SIGKILL
of the runner itself can't be caught by definition; documented as a known
limitation in docs/adr/ADR-402-host-agnostic-agent-teams.md.

MINOR fixes:
1. trust.ts's recordTrust takes a cross-process lock (same O_EXCL/stale-break
   pattern as team.json's withTeamLock), fixing lost records and a
   racing --revoke getting clobbered. Regression test spawns 12 real `node`
   processes against the built dist (a single process can't reproduce this —
   recordTrust is synchronous) and asserts all 11 trusts and 1 revoke land
   correctly.
2. codex/initializer.ts's mergeTeamStopHook refuses a symlinked .codex
   directory, mirroring the Grok-side init path's existing check.
3. team-run-helpers.ts's readResultFile opens with O_NOFOLLOW and fstats the
   descriptor instead of lstat-then-open, closing a TOCTOU window.
4. team-runner.ts wraps run-directory/result-file/run-file handling in a
   try/catch: any exception after the inbox has been drained now requeues
   the drained messages (unless the normal failed-outcome path already did)
   and releases the run claim, instead of stranding the member "running".

Also: root package.json's "files" array was missing
v3/@claude-flow/cli/templates/** even though the cli package's own files
array already includes templates — added (shared one-liner with ruvnet#3512).

Build + typecheck + tests green for both packages (codex: 26/26 new-file
tests, cli: 107/107 team-related tests, all existing tests still pass).
Pre-existing, unrelated failures in this large monorepo (codex worktrees.test.ts
macOS /tmp symlink resolution; assorted cli memory/native-binding tests) are
untouched by this change and were failing before it.

UNRESOLVED: @claude-flow/codex's own package.json declares 3.0.3, but npm's
published `@claude-flow/codex` latest is still 3.0.2 — a @claude-flow/cli
release right now would resolve the older, published 3.0.2 and miss
runHeadlessProcess/env-policy and everything else added in this branch and
ruvnet#3512. This needs an actual `npm publish` of @claude-flow/codex before (or
as part of) shipping this PR; out of scope for a worktree change.

Co-Authored-By: RuFlo <ruv@ruv.net>
@aepod

aepod commented Sep 29, 2026 •

Copy link
Copy Markdown
Contributor Author

This addresses the blocker plus everything under "strongly suggested" and "minor."

Blocking

  1. N1 (templates not shipped by the root package). Added v3/@claude-flow/cli/templates/** to the root package.json files array. Verified by npm pack at repo root, extracting the tarball, and confirming v3/@claude-flow/cli/templates/grok/scripts/grok-team-bus.mjs is actually in it this time.

Strongly suggested

  1. N2 (stale-lock break can admit two holders). The bug was W1 acting on a token it read before re-checking it was still stale, then renaming a live lock aside instead of failing safe. It now re-reads the token immediately before breaking/renaming; if the token changed since the stale read, it backs off and retries the wait loop instead of touching the lock. Covered by a monkeypatched unit test that forces the race window, plus a real concurrent-child-process test (crashed holder, three waiters) asserting exactly one holder at a time.

  2. N3 (wait window shorter than stale window, plus a sync Atomics.wait blocking the MCP server). Reconciled the timings to 4s wait / 4.5s stale threshold, comfortably inside the SubagentStop hook's 5s budget. Replaced the synchronous Atomics.wait poll with an async backoff loop threaded through the whole call chain (store, team-bus, the hook script, team-tools.ts), so a caller waiting on a lock no longer blocks the MCP server from serving anyone else.

Minor

  1. --grok-statusline now writes through a symlinked ~/.grok/config.toml instead of replacing it with a plain file.
  2. Config values above 2^53 are validated before any project files are written, so a BigInt-serialization failure can't leave a half-written init anymore. It either fails with a clear error naming the offending key, or it doesn't touch disk at all.
  3. host-statusline.mjs parses the TOML with @iarna/toml (already a dependency here) instead of text-matching, so a trusted = true in a comment or a enabled = false server no longer confuses it.
  4. Force-recreating a team now clears the old mailbox instead of carrying it over.
  5. Priority parsing rejects 1e2 and 0x10 now too; plain base-10 integers only, same as it already rejected abc/-1/1000/1.5.
  6. Reverted the stray .helpers-version bump and the router.js mode change (644→755). Neither belonged to this PR; both look like helper auto-refresh noise from a concurrent session.

Not touched, both intentionally: the @iarna/toml 0.5-vs-1.0 mixed-array fallback you flagged as safe, and round-1 item 10 (Windows), which was already verified fixed.

Build: npm run build and tsc --noEmit both clean. grok-init, team-tools, and guidance-brain suites: 41/41. Full @claude-flow/cli suite: 3816 passed, 20 failed. All 20 are pre-existing, in files that need @claude-flow/memory/@claude-flow/mcp/@claude-flow/codex built first in a fresh checkout; none touch grok/team/statusline code (checked by name and by grep).

@proffesor-for-testing proffesor-for-testing left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Round 3 on 37147fa99: approving, with nits. Thanks Mat.

Verified by reproduction

  • N1 (packaging): fixed. We ran npm pack --ignore-scripts at the repo root (the "Exit handler" error didn't occur here), and the tarball includes all four templates/grok/scripts/*.mjs. We installed it into a temp project with a sandboxed HOME: init --grok --grok-statusline, mcp exec -t team_create and team_spawn all succeed.
  • Minor items: a symlinked ~/.grok/config.toml is written through and the link is kept. An integer above 2^53 is reported (naming the key) before any project file is written. The status line now parses TOML: # trusted = true doesn't count and enabled = false shows as missing. create --force clears the mailbox (3 files → 0). 1e2 and 0x10 are rejected through both the CLI and MCP.
  • N3 wiring: the lock wait is async, and every caller awaits it (team-tools.ts, the stop hook, the CLI).
  • Build and tests: tsc --noEmit passes, grok-init / team-tools / guidance-brain pass 49/49, and the branch merges cleanly onto current main.
  • Stray files: nothing under .claude/ differs from main.

Follow-up suggestions (not blocking)

  1. The lock is never refreshed while held, and it counts as stale after 4 s (grok-team-store.mjs:30-31). A holder that stalls for more than 4 s (SIGSTOP, laptop sleep, or the hook's 5 s timeout killing it mid-update) gets its lock broken by the next writer, and one of the two updates is lost. We reproduced this with a 6 s holder inside updateTeam: both writers read counter=0, and the final value was 1. The N2 race is also still reachable, but only with pauses injected between the token re-check and the rename (:219-229). 100 natural runs with 3 and 6 waiters showed no overlap. Checking that the pid in the lock token is still alive (process.kill(pid, 0)) before breaking the lock would close both on a single host.
  2. The >2^53 check at grok-generator.ts:435 also rejects the case where the user already has their own [ui.status_line], where nothing would be serialized. Falling back to printing the snippet would keep that case working as before.
  3. Nit: the header regex at host-statusline.mjs:111 rejects ] inside a quoted key, so a trusted folder like /x/a]b reads as untrusted.

Round-3 method: an agent reviewer rebuilt the head, re-ran the round-2 reproductions (including a root npm pack install), and ran timing-injected lock races; Codex CLI cross-checked, and we verified its claims before including them. — Dragan (QE)

aepod added a commit to weave-logic-ai/ruflo that referenced this pull request Oct 2, 2026
Extend the host-agnostic team bus from ruvnet#3512 beyond Grok and Claude,
built on its single store (templates/grok/scripts/grok-team-bus.mjs +
grok-team-store.mjs). No second store: the CLI loads those files for
every read and write.

- Exec hosts (mcp-tools/team-hosts/): a Codex adapter and a generic
  command host declared in .claude-flow/team-hosts.json (argv only,
  never a shell, strictly validated). team_spawn takes `hosts` and
  `model` and adds a host.<label> entry per exec host next to the
  store's grok and claude entries (spawnMember gains `hostPlans` and
  `model`). Unknown team_spawn keys are rejected (`task` suggests
  `prompt`).
- `ruflo team run` runs one headless turn and records the stop:
  - The command is rebuilt from the built-in adapter or the validated
    team-hosts.json entry at run time; the plan stored in team.json is
    never executed.
  - A command host runs only after `ruflo team trust-host <label>`,
    recorded in ~/.claude-flow/ and tied to a digest of the entry.
    Shells, command wrappers and path-like commands also need
    --allow-unsafe-command. passEnv can never re-add secret-named or
    CLAUDE_FLOW_* variables.
  - Refuses a shut-down team, a plan step that is not ready, and a
    second concurrent run of the same agent (claimed under the store
    lock; a dead runner's claim is replaced).
  - Drains only this team's mailbox for the agent into the prompt and
    re-queues to the same team on failure.
  - Result file reads are capped and symlinks ignored; --dry-run shows
    argv, passed env names and trust state.
- onStop takes outcome, runId (repeats are no-ops) and reason; a
  failed stop marks the step failed and does not advance.
- `ruflo team <verb> --params` and `team hook-stop`. hook-stop reads
  stdin to EOF, reports an unparseable payload or ambiguous team on
  stderr instead of recording `{}`, and resolves the team from the
  payload, a `role:agent@team` description, TEAM_NAME, or the only
  active team listing the agent.
- @claude-flow/codex: runHeadlessProcess, killProcessTree and
  buildWorkerEnvironment in dual-mode/process.ts. The child runs in its
  own process group; a timeout or interrupt sends SIGTERM to the group,
  then SIGKILL after a grace period (taskkill /T on Windows), and the
  call returns only after exit. Captured output keeps the head and the
  tail (so turn.completed survives) and decodes UTF-8 once. The
  orchestrator's stopAll kills process trees. No version bump.
- init --codex adds an Agent Teams section to AGENTS.md, an
  agent-teams skill, and a SubagentStop hook in .codex/hooks.json
  (--no-team-hooks to skip).
- init --grok never writes through a symlink inside the project and
  never follows a symlinked template entry.
- probe-host-live: codex/command team round trips; model turns only
  with --execute / --live; no /bin/cat.
- ADR-402 amendment: exec hosts, the trust model (including that
  team_send `from` and hook-stop are unauthenticated on a file bus),
  the runner, and the Codex host.

Co-Authored-By: RuFlo <ruv@ruv.net>
aepod added a commit to weave-logic-ai/ruflo that referenced this pull request Oct 2, 2026
…n claims (ruvnet#3513)

MAJOR A — secrets reaching command hosts through the inherited environment.
buildWorkerEnvironment's base-environment strip (codex/dual-mode/process.ts)
kept a narrower, separately maintained regex than command.ts's passEnv
validation, and didn't generalize CLAUDE_FLOW_*. A trusted host's base env
still leaked PGPASSWORD, DATABASE_URL, SSH_AUTH_SOCK, GITHUB_PAT, MYSQL_PWD,
SESSION_SECRET_X, MY_AUTH, SLACK_WEBHOOK_URL and every CLAUDE_FLOW_*.
New shared source of truth: codex/src/dual-mode/env-policy.ts's
isProtectedEnvName, imported by both process.ts (buildWorkerEnvironment) and
cli's team-hosts/command.ts (passEnv validation). Regression tests: unit-level
strip check plus an end-to-end spawn of a real `printenv` host with every
poisoned name from the repro.

MAJOR E — SIGKILL escalation cancelled when the leader exits first.
runHeadlessProcess's settle() cleared every pending timer, including a
just-armed SIGKILL escalation, as soon as the group leader's `close` event
fired — which can happen before a descendant that trapped SIGTERM and
detached its own stdio (`exec >/dev/null 2>&1`) actually dies. The escalation
is no longer tracked in the timers settle() clears; it now fires unconditionally
after the grace period. Regression test reproduces the exact
trap/detach scenario and asserts no descendant survives past the grace period.

MAJOR F — re-spawning a member mid-run drops its concurrency claim.
spawnMember (grok-team-bus.mjs) overwrote a member's whole record, dropping
the status/runId/runPid/runHost a live `ruflo team run` had set, letting a
second run start concurrently. Re-spawn now carries a live claim forward.
onStop now validates the runId it's given against the member's *current*
runId before releasing the claim, so a stop report with no runId or a stale
one can't clobber a live one. Tests: respawn-mid-run still blocks a second
run; onStop with missing/stale runId leaves a live claim untouched.

MEDIUM B — unsafe-command list missed interpreters; trust recorded before shown.
trust.ts's UNSAFE_COMMANDS now includes node, python/python3, perl, ruby, npx,
git, find, and other general-purpose interpreters that run code via a flag.
`ruflo team trust-host` now prints the resolved host entry before persisting
the trust record, not after. Tests: interpreter classification, an end-to-end
`node -e` host refused then allowed, and a spy on stdout/fs ordering proving
the preview print precedes the write.

MEDIUM D — SIGHUP not forwarded; drained messages lost on runner death.
team-runner.ts forwards SIGHUP the same way it already forwards SIGINT/SIGTERM
(a closed terminal/SSH session no longer orphans the child untracked). Because
an aborted run already takes the failed-outcome requeue path, this also
requeues drained-but-undelivered inbox messages for the SIGHUP case. SIGKILL
of the runner itself can't be caught by definition; documented as a known
limitation in docs/adr/ADR-402-host-agnostic-agent-teams.md.

MINOR fixes:
1. trust.ts's recordTrust takes a cross-process lock (same O_EXCL/stale-break
   pattern as team.json's withTeamLock), fixing lost records and a
   racing --revoke getting clobbered. Regression test spawns 12 real `node`
   processes against the built dist (a single process can't reproduce this —
   recordTrust is synchronous) and asserts all 11 trusts and 1 revoke land
   correctly.
2. codex/initializer.ts's mergeTeamStopHook refuses a symlinked .codex
   directory, mirroring the Grok-side init path's existing check.
3. team-run-helpers.ts's readResultFile opens with O_NOFOLLOW and fstats the
   descriptor instead of lstat-then-open, closing a TOCTOU window.
4. team-runner.ts wraps run-directory/result-file/run-file handling in a
   try/catch: any exception after the inbox has been drained now requeues
   the drained messages (unless the normal failed-outcome path already did)
   and releases the run claim, instead of stranding the member "running".

Also: root package.json's "files" array was missing
v3/@claude-flow/cli/templates/** even though the cli package's own files
array already includes templates — added (shared one-liner with ruvnet#3512).

Build + typecheck + tests green for both packages (codex: 26/26 new-file
tests, cli: 107/107 team-related tests, all existing tests still pass).
Pre-existing, unrelated failures in this large monorepo (codex worktrees.test.ts
macOS /tmp symlink resolution; assorted cli memory/native-binding tests) are
untouched by this change and were failing before it.

UNRESOLVED: @claude-flow/codex's own package.json declares 3.0.3, but npm's
published `@claude-flow/codex` latest is still 3.0.2 — a @claude-flow/cli
release right now would resolve the older, published 3.0.2 and miss
runHeadlessProcess/env-policy and everything else added in this branch and
as part of) shipping this PR; out of scope for a worktree change.

Co-Authored-By: RuFlo <ruv@ruv.net>
@aepod
aepod force-pushed the feat/grok-host-adr320 branch from 37147fa to a1d30ab Compare October 2, 2026 07:29
aepod added a commit to weave-logic-ai/ruflo that referenced this pull request Oct 14, 2026
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>
aepod added a commit to weave-logic-ai/ruflo that referenced this pull request Oct 14, 2026
…us line (ruvnet#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>
aepod added a commit to weave-logic-ai/ruflo that referenced this pull request Oct 14, 2026
…uvnet#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>
@aepod
aepod force-pushed the feat/grok-host-adr320 branch from a1d30ab to c6472d0 Compare October 14, 2026 08:17
aepod added a commit to weave-logic-ai/ruflo that referenced this pull request Oct 14, 2026
Extend the host-agnostic team bus from ruvnet#3512 beyond Grok and Claude,
built on its single store (templates/grok/scripts/grok-team-bus.mjs +
grok-team-store.mjs). No second store: the CLI loads those files for
every read and write.

- Exec hosts (mcp-tools/team-hosts/): a Codex adapter and a generic
  command host declared in .claude-flow/team-hosts.json (argv only,
  never a shell, strictly validated). team_spawn takes `hosts` and
  `model` and adds a host.<label> entry per exec host next to the
  store's grok and claude entries (spawnMember gains `hostPlans` and
  `model`). Unknown team_spawn keys are rejected (`task` suggests
  `prompt`).
- `ruflo team run` runs one headless turn and records the stop:
  - The command is rebuilt from the built-in adapter or the validated
    team-hosts.json entry at run time; the plan stored in team.json is
    never executed.
  - A command host runs only after `ruflo team trust-host <label>`,
    recorded in ~/.claude-flow/ and tied to a digest of the entry.
    Shells, command wrappers and path-like commands also need
    --allow-unsafe-command. passEnv can never re-add secret-named or
    CLAUDE_FLOW_* variables.
  - Refuses a shut-down team, a plan step that is not ready, and a
    second concurrent run of the same agent (claimed under the store
    lock; a dead runner's claim is replaced).
  - Drains only this team's mailbox for the agent into the prompt and
    re-queues to the same team on failure.
  - Result file reads are capped and symlinks ignored; --dry-run shows
    argv, passed env names and trust state.
- onStop takes outcome, runId (repeats are no-ops) and reason; a
  failed stop marks the step failed and does not advance.
- `ruflo team <verb> --params` and `team hook-stop`. hook-stop reads
  stdin to EOF, reports an unparseable payload or ambiguous team on
  stderr instead of recording `{}`, and resolves the team from the
  payload, a `role:agent@team` description, TEAM_NAME, or the only
  active team listing the agent.
- @claude-flow/codex: runHeadlessProcess, killProcessTree and
  buildWorkerEnvironment in dual-mode/process.ts. The child runs in its
  own process group; a timeout or interrupt sends SIGTERM to the group,
  then SIGKILL after a grace period (taskkill /T on Windows), and the
  call returns only after exit. Captured output keeps the head and the
  tail (so turn.completed survives) and decodes UTF-8 once. The
  orchestrator's stopAll kills process trees. No version bump.
- init --codex adds an Agent Teams section to AGENTS.md, an
  agent-teams skill, and a SubagentStop hook in .codex/hooks.json
  (--no-team-hooks to skip).
- init --grok never writes through a symlink inside the project and
  never follows a symlinked template entry.
- probe-host-live: codex/command team round trips; model turns only
  with --execute / --live; no /bin/cat.
- ADR-402 amendment: exec hosts, the trust model (including that
  team_send `from` and hook-stop are unauthenticated on a file bus),
  the runner, and the Codex host.

Co-Authored-By: RuFlo <ruv@ruv.net>
aepod added a commit to weave-logic-ai/ruflo that referenced this pull request Oct 14, 2026
…n claims (ruvnet#3513)

MAJOR A — secrets reaching command hosts through the inherited environment.
buildWorkerEnvironment's base-environment strip (codex/dual-mode/process.ts)
kept a narrower, separately maintained regex than command.ts's passEnv
validation, and didn't generalize CLAUDE_FLOW_*. A trusted host's base env
still leaked PGPASSWORD, DATABASE_URL, SSH_AUTH_SOCK, GITHUB_PAT, MYSQL_PWD,
SESSION_SECRET_X, MY_AUTH, SLACK_WEBHOOK_URL and every CLAUDE_FLOW_*.
New shared source of truth: codex/src/dual-mode/env-policy.ts's
isProtectedEnvName, imported by both process.ts (buildWorkerEnvironment) and
cli's team-hosts/command.ts (passEnv validation). Regression tests: unit-level
strip check plus an end-to-end spawn of a real `printenv` host with every
poisoned name from the repro.

MAJOR E — SIGKILL escalation cancelled when the leader exits first.
runHeadlessProcess's settle() cleared every pending timer, including a
just-armed SIGKILL escalation, as soon as the group leader's `close` event
fired — which can happen before a descendant that trapped SIGTERM and
detached its own stdio (`exec >/dev/null 2>&1`) actually dies. The escalation
is no longer tracked in the timers settle() clears; it now fires unconditionally
after the grace period. Regression test reproduces the exact
trap/detach scenario and asserts no descendant survives past the grace period.

MAJOR F — re-spawning a member mid-run drops its concurrency claim.
spawnMember (grok-team-bus.mjs) overwrote a member's whole record, dropping
the status/runId/runPid/runHost a live `ruflo team run` had set, letting a
second run start concurrently. Re-spawn now carries a live claim forward.
onStop now validates the runId it's given against the member's *current*
runId before releasing the claim, so a stop report with no runId or a stale
one can't clobber a live one. Tests: respawn-mid-run still blocks a second
run; onStop with missing/stale runId leaves a live claim untouched.

MEDIUM B — unsafe-command list missed interpreters; trust recorded before shown.
trust.ts's UNSAFE_COMMANDS now includes node, python/python3, perl, ruby, npx,
git, find, and other general-purpose interpreters that run code via a flag.
`ruflo team trust-host` now prints the resolved host entry before persisting
the trust record, not after. Tests: interpreter classification, an end-to-end
`node -e` host refused then allowed, and a spy on stdout/fs ordering proving
the preview print precedes the write.

MEDIUM D — SIGHUP not forwarded; drained messages lost on runner death.
team-runner.ts forwards SIGHUP the same way it already forwards SIGINT/SIGTERM
(a closed terminal/SSH session no longer orphans the child untracked). Because
an aborted run already takes the failed-outcome requeue path, this also
requeues drained-but-undelivered inbox messages for the SIGHUP case. SIGKILL
of the runner itself can't be caught by definition; documented as a known
limitation in docs/adr/ADR-402-host-agnostic-agent-teams.md.

MINOR fixes:
1. trust.ts's recordTrust takes a cross-process lock (same O_EXCL/stale-break
   pattern as team.json's withTeamLock), fixing lost records and a
   racing --revoke getting clobbered. Regression test spawns 12 real `node`
   processes against the built dist (a single process can't reproduce this —
   recordTrust is synchronous) and asserts all 11 trusts and 1 revoke land
   correctly.
2. codex/initializer.ts's mergeTeamStopHook refuses a symlinked .codex
   directory, mirroring the Grok-side init path's existing check.
3. team-run-helpers.ts's readResultFile opens with O_NOFOLLOW and fstats the
   descriptor instead of lstat-then-open, closing a TOCTOU window.
4. team-runner.ts wraps run-directory/result-file/run-file handling in a
   try/catch: any exception after the inbox has been drained now requeues
   the drained messages (unless the normal failed-outcome path already did)
   and releases the run claim, instead of stranding the member "running".

Also: root package.json's "files" array was missing
v3/@claude-flow/cli/templates/** even though the cli package's own files
array already includes templates — added (shared one-liner with ruvnet#3512).

Build + typecheck + tests green for both packages (codex: 26/26 new-file
tests, cli: 107/107 team-related tests, all existing tests still pass).
Pre-existing, unrelated failures in this large monorepo (codex worktrees.test.ts
macOS /tmp symlink resolution; assorted cli memory/native-binding tests) are
untouched by this change and were failing before it.

UNRESOLVED: @claude-flow/codex's own package.json declares 3.0.3, but npm's
published `@claude-flow/codex` latest is still 3.0.2 — a @claude-flow/cli
release right now would resolve the older, published 3.0.2 and miss
runHeadlessProcess/env-policy and everything else added in this branch and
as part of) shipping this PR; out of scope for a worktree change.

Co-Authored-By: RuFlo <ruv@ruv.net>
@aepod

aepod commented Oct 14, 2026

Copy link
Copy Markdown
Contributor Author

Rebased onto 3.56.9 (55e2c90). The three reviewed commits applied cleanly. One new commit, c6472d0: analyze-json-output.test.ts (new on main) failed because loading the MCP tool table pulled in @iarna/toml via grokTemplatesRoot; that now lives in init/grok-templates.ts. Build, typecheck, full suite (no test fails that passes on main) and team suites 72/72 pass.

aepod and others added 4 commits October 18, 2026 20:41
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>
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>
…us line (ruvnet#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>
…uvnet#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>
@aepod
aepod force-pushed the feat/grok-host-adr320 branch from c6472d0 to 6aaaf82 Compare October 19, 2026 01:44
aepod added a commit to weave-logic-ai/ruflo that referenced this pull request Oct 19, 2026
Extend the host-agnostic team bus from ruvnet#3512 beyond Grok and Claude,
built on its single store (templates/grok/scripts/grok-team-bus.mjs +
grok-team-store.mjs). No second store: the CLI loads those files for
every read and write.

- Exec hosts (mcp-tools/team-hosts/): a Codex adapter and a generic
  command host declared in .claude-flow/team-hosts.json (argv only,
  never a shell, strictly validated). team_spawn takes `hosts` and
  `model` and adds a host.<label> entry per exec host next to the
  store's grok and claude entries (spawnMember gains `hostPlans` and
  `model`). Unknown team_spawn keys are rejected (`task` suggests
  `prompt`).
- `ruflo team run` runs one headless turn and records the stop:
  - The command is rebuilt from the built-in adapter or the validated
    team-hosts.json entry at run time; the plan stored in team.json is
    never executed.
  - A command host runs only after `ruflo team trust-host <label>`,
    recorded in ~/.claude-flow/ and tied to a digest of the entry.
    Shells, command wrappers and path-like commands also need
    --allow-unsafe-command. passEnv can never re-add secret-named or
    CLAUDE_FLOW_* variables.
  - Refuses a shut-down team, a plan step that is not ready, and a
    second concurrent run of the same agent (claimed under the store
    lock; a dead runner's claim is replaced).
  - Drains only this team's mailbox for the agent into the prompt and
    re-queues to the same team on failure.
  - Result file reads are capped and symlinks ignored; --dry-run shows
    argv, passed env names and trust state.
- onStop takes outcome, runId (repeats are no-ops) and reason; a
  failed stop marks the step failed and does not advance.
- `ruflo team <verb> --params` and `team hook-stop`. hook-stop reads
  stdin to EOF, reports an unparseable payload or ambiguous team on
  stderr instead of recording `{}`, and resolves the team from the
  payload, a `role:agent@team` description, TEAM_NAME, or the only
  active team listing the agent.
- @claude-flow/codex: runHeadlessProcess, killProcessTree and
  buildWorkerEnvironment in dual-mode/process.ts. The child runs in its
  own process group; a timeout or interrupt sends SIGTERM to the group,
  then SIGKILL after a grace period (taskkill /T on Windows), and the
  call returns only after exit. Captured output keeps the head and the
  tail (so turn.completed survives) and decodes UTF-8 once. The
  orchestrator's stopAll kills process trees. No version bump.
- init --codex adds an Agent Teams section to AGENTS.md, an
  agent-teams skill, and a SubagentStop hook in .codex/hooks.json
  (--no-team-hooks to skip).
- init --grok never writes through a symlink inside the project and
  never follows a symlinked template entry.
- probe-host-live: codex/command team round trips; model turns only
  with --execute / --live; no /bin/cat.
- ADR-402 amendment: exec hosts, the trust model (including that
  team_send `from` and hook-stop are unauthenticated on a file bus),
  the runner, and the Codex host.

Co-Authored-By: RuFlo <ruv@ruv.net>
aepod added a commit to weave-logic-ai/ruflo that referenced this pull request Oct 19, 2026
…n claims (ruvnet#3513)

MAJOR A — secrets reaching command hosts through the inherited environment.
buildWorkerEnvironment's base-environment strip (codex/dual-mode/process.ts)
kept a narrower, separately maintained regex than command.ts's passEnv
validation, and didn't generalize CLAUDE_FLOW_*. A trusted host's base env
still leaked PGPASSWORD, DATABASE_URL, SSH_AUTH_SOCK, GITHUB_PAT, MYSQL_PWD,
SESSION_SECRET_X, MY_AUTH, SLACK_WEBHOOK_URL and every CLAUDE_FLOW_*.
New shared source of truth: codex/src/dual-mode/env-policy.ts's
isProtectedEnvName, imported by both process.ts (buildWorkerEnvironment) and
cli's team-hosts/command.ts (passEnv validation). Regression tests: unit-level
strip check plus an end-to-end spawn of a real `printenv` host with every
poisoned name from the repro.

MAJOR E — SIGKILL escalation cancelled when the leader exits first.
runHeadlessProcess's settle() cleared every pending timer, including a
just-armed SIGKILL escalation, as soon as the group leader's `close` event
fired — which can happen before a descendant that trapped SIGTERM and
detached its own stdio (`exec >/dev/null 2>&1`) actually dies. The escalation
is no longer tracked in the timers settle() clears; it now fires unconditionally
after the grace period. Regression test reproduces the exact
trap/detach scenario and asserts no descendant survives past the grace period.

MAJOR F — re-spawning a member mid-run drops its concurrency claim.
spawnMember (grok-team-bus.mjs) overwrote a member's whole record, dropping
the status/runId/runPid/runHost a live `ruflo team run` had set, letting a
second run start concurrently. Re-spawn now carries a live claim forward.
onStop now validates the runId it's given against the member's *current*
runId before releasing the claim, so a stop report with no runId or a stale
one can't clobber a live one. Tests: respawn-mid-run still blocks a second
run; onStop with missing/stale runId leaves a live claim untouched.

MEDIUM B — unsafe-command list missed interpreters; trust recorded before shown.
trust.ts's UNSAFE_COMMANDS now includes node, python/python3, perl, ruby, npx,
git, find, and other general-purpose interpreters that run code via a flag.
`ruflo team trust-host` now prints the resolved host entry before persisting
the trust record, not after. Tests: interpreter classification, an end-to-end
`node -e` host refused then allowed, and a spy on stdout/fs ordering proving
the preview print precedes the write.

MEDIUM D — SIGHUP not forwarded; drained messages lost on runner death.
team-runner.ts forwards SIGHUP the same way it already forwards SIGINT/SIGTERM
(a closed terminal/SSH session no longer orphans the child untracked). Because
an aborted run already takes the failed-outcome requeue path, this also
requeues drained-but-undelivered inbox messages for the SIGHUP case. SIGKILL
of the runner itself can't be caught by definition; documented as a known
limitation in docs/adr/ADR-402-host-agnostic-agent-teams.md.

MINOR fixes:
1. trust.ts's recordTrust takes a cross-process lock (same O_EXCL/stale-break
   pattern as team.json's withTeamLock), fixing lost records and a
   racing --revoke getting clobbered. Regression test spawns 12 real `node`
   processes against the built dist (a single process can't reproduce this —
   recordTrust is synchronous) and asserts all 11 trusts and 1 revoke land
   correctly.
2. codex/initializer.ts's mergeTeamStopHook refuses a symlinked .codex
   directory, mirroring the Grok-side init path's existing check.
3. team-run-helpers.ts's readResultFile opens with O_NOFOLLOW and fstats the
   descriptor instead of lstat-then-open, closing a TOCTOU window.
4. team-runner.ts wraps run-directory/result-file/run-file handling in a
   try/catch: any exception after the inbox has been drained now requeues
   the drained messages (unless the normal failed-outcome path already did)
   and releases the run claim, instead of stranding the member "running".

Also: root package.json's "files" array was missing
v3/@claude-flow/cli/templates/** even though the cli package's own files
array already includes templates — added (shared one-liner with ruvnet#3512).

Build + typecheck + tests green for both packages (codex: 26/26 new-file
tests, cli: 107/107 team-related tests, all existing tests still pass).
Pre-existing, unrelated failures in this large monorepo (codex worktrees.test.ts
macOS /tmp symlink resolution; assorted cli memory/native-binding tests) are
untouched by this change and were failing before it.

UNRESOLVED: @claude-flow/codex's own package.json declares 3.0.3, but npm's
published `@claude-flow/codex` latest is still 3.0.2 — a @claude-flow/cli
release right now would resolve the older, published 3.0.2 and miss
runHeadlessProcess/env-policy and everything else added in this branch and
as part of) shipping this PR; out of scope for a worktree change.

Co-Authored-By: RuFlo <ruv@ruv.net>
@aepod

aepod commented Oct 19, 2026

Copy link
Copy Markdown
Contributor Author

Rebased onto main b93144803. New head 6aaaf82f8.

  • Conflict: README.md only. Main added the "An origin you can inspect" section in the same place as the Grok Build line. I kept both, with the Grok line after main's section.
  • Code: no changes. Range-diff shows commits 2-4 identical.
  • ADR-402: still free on main (main has 400, 401, 404-409).
  • Test V3 Packages: 9165 passed / 4 failed / 228 skipped of 9397. Every failing file also fails on main in the same environment (main: 9118 / 7 / 228 of 9353).
  • Root Test Suite: no failing file that main doesn't also fail.
  • Also: pnpm -r build clean, grok-init.test.ts 26/26, team-tools.test.ts 18/18.

Gates ran in clean Linux containers (node 22 for V3 Packages, node 20 for the root suite), compared against the same job on main in the same environment. Main shows some failing files there that pass in real CI, so the claim is only that this branch adds none.

#3513 is stacked on this one, so this can merge first.

@ruvnet
ruvnet merged commit 953d8d0 into ruvnet:main Oct 20, 2026
126 checks passed
meefs pushed a commit to meefs/claude-code-flow that referenced this pull request Oct 20, 2026
Minor release (new backward-compatible flags: Grok Build CLI host
surface ruvnet#3512, memory list/search tag filter ruvnet#3526) bundled with a
round of correctness fixes merged since 3.58.1: memory delete-sync
(ruvnet#4138), real AgentDB HNSWIndex replacing the dead getController('hnsw')
path with safe exact-search fallback (ruvnet#4249/ruvnet#4322), harness live-process
reporting (ruvnet#3521), RaBitQ on-demand index build (ruvnet#4318/ruvnet#4319),
metaharness ADR-152 cost-dimension and verdict-band fix (ruvnet#3520), swarm
idle-agent heartbeat false-positive (ruvnet#4320), ephemeral-agent termination
idempotency (ruvnet#4323), status/task_list field-mapping and total-count
fixes (ruvnet#4321/ruvnet#4324).

Also fixes the actual cause of broken images/links on
npmjs.com/package/ruflo: `ruflo/package.json`'s `repository.directory:
"ruflo"` field added an extra path layer on top of the README content
(the root README.md, copied in verbatim by `prepublishOnly` at publish
time, which is already written with repo-root-relative paths) — every
relative image/link resolved to e.g. `raw/main/ruflo/ruflo/assets/...`
instead of `raw/main/ruflo/assets/...`. Removed the `directory` field to
match claude-flow's own (directory-less) repository entry. A prior PR
(ruvnet#4325) fixed `ruflo/README.md`'s own relative paths for anyone browsing
that file directly on GitHub, but that file is overwritten by the root
README at publish time and was never the actual npm-rendered content.

Published to npm: @claude-flow/cli@3.59.0, claude-flow@3.59.0,
ruflo@3.59.0, and standalone @claude-flow/memory@3.0.8 (the ruvnet#4249 fix —
not part of the 3-package train, published separately per its own
leaf's npm presence). All three umbrella packages' latest/alpha/v3alpha
dist-tags verified pointing at 3.59.0.

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

4 participants