claude-flow 3.58.0 → 3.59.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/.claude/proven-config.json +1 -1
  2. package/README.md +2 -0
  3. package/node_modules/@claude-flow/codex/dist/cli.js +0 -0
  4. package/node_modules/@claude-flow/plugin-agent-federation/dist/bin.js +0 -0
  5. package/package.json +2 -1
  6. package/v3/@claude-flow/cli/README.md +62 -174
  7. package/v3/@claude-flow/cli/bin/cli.js +19 -1
  8. package/v3/@claude-flow/cli/catalog-manifest.json +5 -5
  9. package/v3/@claude-flow/cli/dist/src/commands/harness.d.ts +40 -0
  10. package/v3/@claude-flow/cli/dist/src/commands/harness.js +142 -0
  11. package/v3/@claude-flow/cli/dist/src/commands/index.js +1 -0
  12. package/v3/@claude-flow/cli/dist/src/commands/init.js +116 -0
  13. package/v3/@claude-flow/cli/dist/src/commands/memory.js +6 -1
  14. package/v3/@claude-flow/cli/dist/src/commands/status.js +2 -2
  15. package/v3/@claude-flow/cli/dist/src/index.d.ts +3 -0
  16. package/v3/@claude-flow/cli/dist/src/index.js +12 -2
  17. package/v3/@claude-flow/cli/dist/src/init/grok-generator.d.ts +96 -0
  18. package/v3/@claude-flow/cli/dist/src/init/grok-generator.js +371 -0
  19. package/v3/@claude-flow/cli/dist/src/init/grok-templates.d.ts +11 -0
  20. package/v3/@claude-flow/cli/dist/src/init/grok-templates.js +28 -0
  21. package/v3/@claude-flow/cli/dist/src/init/index.d.ts +2 -0
  22. package/v3/@claude-flow/cli/dist/src/init/index.js +2 -0
  23. package/v3/@claude-flow/cli/dist/src/mcp-client.js +4 -0
  24. package/v3/@claude-flow/cli/dist/src/mcp-tools/agent-execute-core.js +59 -12
  25. package/v3/@claude-flow/cli/dist/src/mcp-tools/capability-brain.d.ts +1 -1
  26. package/v3/@claude-flow/cli/dist/src/mcp-tools/capability-brain.js +5 -3
  27. package/v3/@claude-flow/cli/dist/src/mcp-tools/embeddings-tools.js +11 -4
  28. package/v3/@claude-flow/cli/dist/src/mcp-tools/hooks-tools.js +13 -3
  29. package/v3/@claude-flow/cli/dist/src/mcp-tools/index.d.ts +1 -0
  30. package/v3/@claude-flow/cli/dist/src/mcp-tools/index.js +2 -0
  31. package/v3/@claude-flow/cli/dist/src/mcp-tools/memory-tools.js +42 -4
  32. package/v3/@claude-flow/cli/dist/src/mcp-tools/task-tools.js +7 -3
  33. package/v3/@claude-flow/cli/dist/src/mcp-tools/team-tools.d.ts +15 -0
  34. package/v3/@claude-flow/cli/dist/src/mcp-tools/team-tools.js +244 -0
  35. package/v3/@claude-flow/cli/dist/src/memory/memory-bridge.d.ts +19 -0
  36. package/v3/@claude-flow/cli/dist/src/memory/memory-bridge.js +163 -3
  37. package/v3/@claude-flow/cli/dist/src/memory/memory-initializer.d.ts +4 -0
  38. package/v3/@claude-flow/cli/dist/src/memory/memory-initializer.js +70 -37
  39. package/v3/@claude-flow/cli/dist/src/memory/rabitq-index.d.ts +19 -0
  40. package/v3/@claude-flow/cli/dist/src/memory/rabitq-index.js +117 -1
  41. package/v3/@claude-flow/cli/dist/src/missions/schemas.d.ts +113 -113
  42. package/v3/@claude-flow/cli/dist/src/mods/command-registry/schema.d.ts +193 -193
  43. package/v3/@claude-flow/cli/dist/src/parser.js +3 -2
  44. package/v3/@claude-flow/cli/dist/src/services/live-harness.d.ts +111 -0
  45. package/v3/@claude-flow/cli/dist/src/services/live-harness.js +427 -0
  46. package/v3/@claude-flow/cli/package.json +4 -3
  47. package/v3/@claude-flow/cli/templates/grok/agents/ruflo-architect.md +35 -0
  48. package/v3/@claude-flow/cli/templates/grok/agents/ruflo-coder.md +34 -0
  49. package/v3/@claude-flow/cli/templates/grok/agents/ruflo-reviewer.md +30 -0
  50. package/v3/@claude-flow/cli/templates/grok/agents/ruflo-tester.md +26 -0
  51. package/v3/@claude-flow/cli/templates/grok/config.toml +63 -0
  52. package/v3/@claude-flow/cli/templates/grok/docs/README.md +186 -0
  53. package/v3/@claude-flow/cli/templates/grok/hooks/subagent-stop-team.json +15 -0
  54. package/v3/@claude-flow/cli/templates/grok/rules/ruflo-grok.md +132 -0
  55. package/v3/@claude-flow/cli/templates/grok/scripts/grok-subagent-stop-hook.mjs +90 -0
  56. package/v3/@claude-flow/cli/templates/grok/scripts/grok-team-bus.mjs +525 -0
  57. package/v3/@claude-flow/cli/templates/grok/scripts/grok-team-store.mjs +287 -0
  58. package/v3/@claude-flow/cli/templates/grok/scripts/host-statusline.mjs +281 -0
  59. package/v3/@claude-flow/cli/templates/grok/skills/agent-teams-grok/SKILL.md +98 -0
@@ -0,0 +1,287 @@
1
+ /**
2
+ * Storage layer for the host-agnostic Agent Teams bus (ADR-402).
3
+ * Imported by grok-team-bus.mjs; not a CLI.
4
+ *
5
+ * Layout (all under <project>/.claude-flow/teams/<team>/):
6
+ * team.json roster, plan, status (lock + atomic rename)
7
+ * team.lock cross-process lock (O_EXCL file + token)
8
+ * mailbox/<agent>/*.json pending messages, named <priority>_<id>.json
9
+ * mailbox/<agent>/archive/ drained messages
10
+ */
11
+
12
+ import fs from 'node:fs';
13
+ import path from 'node:path';
14
+
15
+ export const NAME_RE = /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,63}$/;
16
+ // A waiter must never give up before the lock it's waiting on can even be
17
+ // considered stale — otherwise every spawn attempted in the gap between
18
+ // "waiter gives up" and "lock is stale" fails outright even though nothing
19
+ // is actually stuck (ADR-402 round-2 review, N3). So LOCK_WAIT_MS >=
20
+ // LOCK_STALE_MS, always.
21
+ //
22
+ // Both are also sized to fit inside the SubagentStop hook's own 5s command
23
+ // timeout (templates/grok/hooks/subagent-stop-team.json): worst case, the
24
+ // hook waits out the full stale window, breaks the lock, and runs its (tiny,
25
+ // sub-millisecond) critical section — that must land comfortably under 5s
26
+ // including node startup. 4s stale / 4.5s wait leaves ~0.5s of margin for
27
+ // process startup and the break-then-acquire dance, while still being far
28
+ // longer than any legitimate holder ever needs (team.json read-modify-write
29
+ // is the only thing done under this lock).
30
+ const LOCK_WAIT_MS = 4_500;
31
+ const LOCK_STALE_MS = 4_000;
32
+
33
+ // ---------------------------------------------------------------------------
34
+ // Names, paths, and filesystem safety
35
+ // ---------------------------------------------------------------------------
36
+
37
+ export function safeName(name, field = 'name') {
38
+ if (typeof name !== 'string' || !NAME_RE.test(name)) {
39
+ throw new Error(`Invalid ${field} "${name}" — use 1-64 alphanumeric, dash, underscore (must start alphanumeric)`);
40
+ }
41
+ return name;
42
+ }
43
+
44
+ export function teamsRoot(projectRoot) {
45
+ return path.join(projectRoot, '.claude-flow', 'teams');
46
+ }
47
+
48
+ export function teamDir(projectRoot, team) {
49
+ return path.join(teamsRoot(projectRoot), safeName(team, 'team'));
50
+ }
51
+
52
+ export function teamFile(projectRoot, team) {
53
+ return path.join(teamDir(projectRoot, team), 'team.json');
54
+ }
55
+
56
+ export function mailboxDir(projectRoot, team, agent) {
57
+ return path.join(teamDir(projectRoot, team), 'mailbox', safeName(agent, 'agent'));
58
+ }
59
+
60
+ /**
61
+ * Create `dir` and every missing parent below `base`, refusing to pass through
62
+ * a symlink. A planted symlink would otherwise redirect writes outside the
63
+ * project.
64
+ */
65
+ export function ensureRealDir(base, dir) {
66
+ const rel = path.relative(base, dir);
67
+ if (rel.startsWith('..') || path.isAbsolute(rel)) {
68
+ throw new Error(`Refusing path outside ${base}: ${dir}`);
69
+ }
70
+ let cur = base;
71
+ fs.mkdirSync(base, { recursive: true, mode: 0o700 });
72
+ assertRealDir(cur);
73
+ for (const part of rel.split(path.sep).filter(Boolean)) {
74
+ cur = path.join(cur, part);
75
+ try {
76
+ fs.mkdirSync(cur, { mode: 0o700 });
77
+ } catch (err) {
78
+ if (err.code !== 'EEXIST') throw err;
79
+ }
80
+ assertRealDir(cur);
81
+ }
82
+ }
83
+
84
+ export function assertRealDir(dir) {
85
+ const st = fs.lstatSync(dir);
86
+ if (st.isSymbolicLink() || !st.isDirectory()) {
87
+ throw new Error(`Refusing to use ${dir}: not a real directory (symlink?)`);
88
+ }
89
+ }
90
+
91
+ /** Existing real directory → true; missing → false; symlink/file → throws. */
92
+ export function realDirExists(dir) {
93
+ try {
94
+ assertRealDir(dir);
95
+ return true;
96
+ } catch (err) {
97
+ if (err.code === 'ENOENT') return false;
98
+ throw err;
99
+ }
100
+ }
101
+
102
+ export function readJson(file) {
103
+ try {
104
+ return JSON.parse(fs.readFileSync(file, 'utf8'));
105
+ } catch {
106
+ return null;
107
+ }
108
+ }
109
+
110
+ /** Write via a unique temp file + rename so readers never see a torn file. */
111
+ export function writeJsonAtomic(file, data) {
112
+ const tmp = `${file}.${process.pid}.${Math.random().toString(36).slice(2, 10)}.tmp`;
113
+ fs.writeFileSync(tmp, JSON.stringify(data, null, 2) + '\n', { encoding: 'utf8', flag: 'wx', mode: 0o600 });
114
+ for (let attempt = 0; ; attempt++) {
115
+ try {
116
+ fs.renameSync(tmp, file);
117
+ return;
118
+ } catch (err) {
119
+ // Windows can refuse a rename over a file another process has open.
120
+ if (attempt >= 20 || (err.code !== 'EPERM' && err.code !== 'EBUSY' && err.code !== 'EACCES')) {
121
+ try { fs.rmSync(tmp, { force: true }); } catch { /* ignore */ }
122
+ throw err;
123
+ }
124
+ sleepMs(10);
125
+ }
126
+ }
127
+ }
128
+
129
+ /**
130
+ * Synchronous sleep via Atomics.wait — blocks this thread (and, in an MCP
131
+ * server, every other in-flight request on it) for `ms`. Kept only for the
132
+ * tiny (<=200ms worst case, ~20 attempts x 10ms), bounded Windows
133
+ * rename-retry in writeJsonAtomic, which is a rare edge case and not worth
134
+ * threading a Promise through every atomic write for. Anything that can
135
+ * block for the length of a lock wait — potentially seconds — must use
136
+ * sleepMsAsync instead (ADR-402 round-2 review, N3).
137
+ */
138
+ export function sleepMs(ms) {
139
+ Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, ms);
140
+ }
141
+
142
+ /** Non-blocking sleep: yields to the event loop instead of the whole thread. */
143
+ export function sleepMsAsync(ms) {
144
+ return new Promise((resolve) => setTimeout(resolve, ms));
145
+ }
146
+
147
+ export function nowIso() {
148
+ return new Date().toISOString();
149
+ }
150
+
151
+ // ---------------------------------------------------------------------------
152
+ // Lock: one team.json writer at a time, across processes
153
+ // ---------------------------------------------------------------------------
154
+
155
+ /**
156
+ * Run `fn` holding the team lock: a `team.lock` file created with O_EXCL and
157
+ * holding a random token. A lock older than LOCK_STALE_MS (a crashed holder)
158
+ * is broken by renaming it aside and checking the token: if a live holder
159
+ * re-took the lock in between, the rename caught theirs, so it is linked back
160
+ * (link fails rather than overwrite) and this waiter keeps waiting. Release
161
+ * removes the file only while it still holds our token.
162
+ *
163
+ * Async: the wait loop yields via sleepMsAsync (setTimeout), not a blocking
164
+ * Atomics.wait, so an MCP server (or any other caller) keeps serving other
165
+ * requests while this call waits for the lock (ADR-402 round-2 review, N3).
166
+ * `fn` itself still runs synchronously once the lock is held — team.json
167
+ * read-modify-write is fast enough that yielding mid-critical-section isn't
168
+ * worth the added complexity.
169
+ */
170
+ export async function withTeamLock(projectRoot, team, fn) {
171
+ const dir = teamDir(projectRoot, team);
172
+ if (!realDirExists(dir)) throw new Error(`Team "${team}" not found — run create first`);
173
+ const lock = path.join(dir, 'team.lock');
174
+ const token = `${process.pid}.${Date.now()}.${Math.random().toString(36).slice(2, 10)}`;
175
+ const deadline = Date.now() + LOCK_WAIT_MS;
176
+ for (let wait = 2; ; wait = Math.min(wait * 2, 50)) {
177
+ try {
178
+ fs.writeFileSync(lock, token, { flag: 'wx', mode: 0o600 });
179
+ break;
180
+ } catch (err) {
181
+ if (err.code !== 'EEXIST') throw err;
182
+ }
183
+ breakStaleLock(lock, token);
184
+ if (Date.now() > deadline) throw new Error(`Timed out waiting for the lock on team "${team}"`);
185
+ await sleepMsAsync(wait + Math.floor(Math.random() * wait));
186
+ }
187
+ try {
188
+ return fn();
189
+ } finally {
190
+ try {
191
+ if (fs.readFileSync(lock, 'utf8') === token) fs.rmSync(lock, { force: true });
192
+ } catch {
193
+ /* already gone */
194
+ }
195
+ }
196
+ }
197
+
198
+ /** True when the lock token names a pid that still exists on this host. */
199
+ function lockHolderAlive(token) {
200
+ const pid = Number(String(token).split('.')[0]);
201
+ if (!Number.isInteger(pid) || pid <= 0) return false;
202
+ try {
203
+ process.kill(pid, 0);
204
+ return true;
205
+ } catch (err) {
206
+ // EPERM: the process exists and we cannot signal it. ESRCH: it is gone.
207
+ return err.code === 'EPERM';
208
+ }
209
+ }
210
+
211
+ /**
212
+ * Break `lock` if it's still stale. Re-verifies the token and mtime we
213
+ * observed as stale immediately before renaming anything: without this, a
214
+ * waiter that read a stale token, then got preempted while another waiter
215
+ * broke the same lock and a fresh holder re-acquired it, would rename that
216
+ * LIVE lock out from under its holder — and the window that opens between
217
+ * the rename and the "put it back" recovery lets a fourth party grab the
218
+ * vacated slot, leaving two holders at once (ADR-402 round-2 review, N2).
219
+ * If anything has changed since our first observation, someone else already
220
+ * handled it: back off without touching the file and let the caller's own
221
+ * wait loop retry. A token whose pid is still alive is left alone even when
222
+ * the file is older than LOCK_STALE_MS (round-3 review).
223
+ */
224
+ export function breakStaleLock(lock, token) {
225
+ let seen, mtime;
226
+ try {
227
+ mtime = fs.statSync(lock).mtimeMs;
228
+ if (Date.now() - mtime <= LOCK_STALE_MS) return;
229
+ seen = fs.readFileSync(lock, 'utf8');
230
+ } catch {
231
+ return; // released meanwhile
232
+ }
233
+ let mtime2, seen2;
234
+ try {
235
+ mtime2 = fs.statSync(lock).mtimeMs;
236
+ seen2 = fs.readFileSync(lock, 'utf8');
237
+ } catch {
238
+ return; // released meanwhile
239
+ }
240
+ if (mtime2 !== mtime || seen2 !== seen) return; // changed underneath us — back off
241
+ // A live holder is not stale, however old the file is. The lock is not
242
+ // refreshed while held, so a stall longer than LOCK_STALE_MS (SIGSTOP,
243
+ // sleep, a hook killed mid-update) used to let the next writer break it
244
+ // and drop one of the two updates. The token starts with the holder's
245
+ // pid; if that pid is still alive on this host, leave the lock alone
246
+ // (ADR-402 round-3 review). A dead or unparsable pid keeps the mtime rule.
247
+ if (lockHolderAlive(seen2)) return;
248
+
249
+ const aside = `${lock}.stale.${token}`;
250
+ try {
251
+ fs.renameSync(lock, aside);
252
+ } catch {
253
+ return; // another waiter broke it first
254
+ }
255
+ try {
256
+ if (fs.readFileSync(aside, 'utf8') !== seen) {
257
+ // Same-millisecond collision the re-verify above couldn't rule out:
258
+ // put the live lock back if the slot is still free.
259
+ try { fs.linkSync(aside, lock); } catch { /* slot taken; that holder's release is a no-op */ }
260
+ }
261
+ } finally {
262
+ fs.rmSync(aside, { force: true });
263
+ }
264
+ }
265
+
266
+ export function loadTeam(projectRoot, team) {
267
+ const t = readJson(teamFile(projectRoot, team));
268
+ if (!t) throw new Error(`Team "${team}" not found — run create first`);
269
+ return t;
270
+ }
271
+
272
+ /** Read-modify-write team.json under the lock. `mutate` returns the result. */
273
+ export function updateTeam(projectRoot, team, mutate) {
274
+ return withTeamLock(projectRoot, team, () => {
275
+ const t = loadTeam(projectRoot, team);
276
+ const result = mutate(t);
277
+ writeJsonAtomic(teamFile(projectRoot, team), t);
278
+ return result;
279
+ });
280
+ }
281
+
282
+ export function assertActive(t) {
283
+ if (t.status !== 'active') {
284
+ throw new Error(`Team "${t.name}" is ${t.status}; create it again (force) to reuse the name`);
285
+ }
286
+ }
287
+
@@ -0,0 +1,281 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * One "what is loaded" line for every host.
4
+ *
5
+ * grok ~/.grok/config.toml [ui.status_line] command. Project config
6
+ * cannot set this (Grok 1.0.41 reads [ui] from the user file only).
7
+ * SessionStart stdout is ignored, so a hook cannot print the line.
8
+ * `ruflo init --grok --grok-statusline` installs a copy of this
9
+ * script at ~/.grok/ruflo/host-statusline.mjs and points the row at
10
+ * that absolute path; it reports on whichever repo Grok is in.
11
+ * claude .claude/settings.json statusLine → .claude/helpers/statusline.cjs
12
+ * (the rich row). This script is the same facts, not a replacement.
13
+ * codex no status row. `node scripts/host-statusline.mjs --host codex`
14
+ * prints the line; nothing in Codex displays it for you.
15
+ *
16
+ * Grok pipes a JSON payload on stdin. A tty (a manual run) is left alone.
17
+ * node scripts/host-statusline.mjs
18
+ * node scripts/host-statusline.mjs --json
19
+ * node scripts/host-statusline.mjs --host claude
20
+ */
21
+
22
+ import { existsSync, readFileSync } from 'node:fs';
23
+ import { homedir } from 'node:os';
24
+ import { join, resolve } from 'node:path';
25
+
26
+ const args = process.argv.slice(2);
27
+ const JSON_OUT = args.includes('--json');
28
+ const hostFlag = args.indexOf('--host');
29
+ const HOST = (hostFlag >= 0 ? args[hostFlag + 1] : 'grok').toLowerCase();
30
+ const ROOT = resolve(process.env.RUFLO_HOST_ROOT || process.cwd());
31
+
32
+ export const HOST_STATUS = {
33
+ grok: {
34
+ surface: 'ui.status_line',
35
+ scope: 'user',
36
+ command: 'node "<home>/.grok/ruflo/host-statusline.mjs"',
37
+ },
38
+ claude: {
39
+ surface: 'statusLine',
40
+ scope: 'project',
41
+ command: 'node .claude/helpers/statusline.cjs',
42
+ },
43
+ codex: {
44
+ surface: null,
45
+ scope: null,
46
+ command: 'node scripts/host-statusline.mjs --host codex',
47
+ },
48
+ };
49
+
50
+ function has(rel) {
51
+ return existsSync(join(ROOT, rel));
52
+ }
53
+
54
+ // ---------------------------------------------------------------------------
55
+ // Minimal, comment-aware TOML table reader.
56
+ //
57
+ // This script is a zero-dependency copy: `init --grok --grok-statusline`
58
+ // writes it to ~/.grok/ruflo/host-statusline.mjs, and `init --grok` also
59
+ // copies it to <project>/scripts/host-statusline.mjs — both live outside any
60
+ // node_modules tree, so (unlike grok-generator.ts, which is part of the
61
+ // installed @claude-flow/cli package) it can't `import '@iarna/toml'`
62
+ // without risking "Cannot find package" at the exact moment Grok tries to
63
+ // render the status line. The previous version matched `trusted = true` /
64
+ // `[mcp_servers.name]` via substring/regex on the raw file text, which
65
+ // happily matched inside a `#` comment and never noticed a sibling
66
+ // `enabled = false` key. This walks real `[section]` boundaries and strips
67
+ // comments outside of string literals first, instead (ADR-402 round-2
68
+ // review, minor item 3). It is not a general TOML parser — only enough of
69
+ // one for the two shapes this file actually reads: table headers (with
70
+ // quoted or bare dotted keys) and simple `key = true|false` booleans.
71
+ // ---------------------------------------------------------------------------
72
+
73
+ function stripComment(line) {
74
+ let quote = null;
75
+ for (let i = 0; i < line.length; i++) {
76
+ const c = line[i];
77
+ if (quote) {
78
+ if (c === '\\' && quote === '"') { i++; continue; }
79
+ if (c === quote) quote = null;
80
+ continue;
81
+ }
82
+ if (c === '"' || c === "'") { quote = c; continue; }
83
+ if (c === '#') return line.slice(0, i);
84
+ }
85
+ return line;
86
+ }
87
+
88
+ /** Splits a dotted TOML key on top-level dots only (not dots inside quotes), then dequotes each part. */
89
+ function splitDottedKey(raw) {
90
+ const parts = [];
91
+ let cur = '';
92
+ let quote = null;
93
+ for (const c of raw) {
94
+ if (quote) {
95
+ cur += c;
96
+ if (c === quote) quote = null;
97
+ continue;
98
+ }
99
+ if (c === '"' || c === "'") { quote = c; cur += c; continue; }
100
+ if (c === '.') { parts.push(cur.trim()); cur = ''; continue; }
101
+ cur += c;
102
+ }
103
+ parts.push(cur.trim());
104
+ return parts.map((p) => p.replace(/^["']|["']$/g, ''));
105
+ }
106
+
107
+ /**
108
+ * '[a."b.c"]' or '[a.b]' → normalized key 'a.b.c' (dequoted parts joined by '.'),
109
+ * or null if not a table header. A `]` inside a quoted key is part of the key
110
+ * (`[folders."/x/a]b"]`), not the end of the header.
111
+ */
112
+ function sectionHeaderKey(line) {
113
+ const trimmed = line.trim();
114
+ if (!trimmed.startsWith('[') || trimmed.startsWith('[[')) return null; // array-of-tables: not modeled
115
+ let quote = null;
116
+ let end = -1;
117
+ for (let i = 1; i < trimmed.length; i++) {
118
+ const c = trimmed[i];
119
+ if (quote) {
120
+ if (c === '\\' && quote === '"') { i++; continue; }
121
+ if (c === quote) quote = null;
122
+ continue;
123
+ }
124
+ if (c === '"' || c === "'") { quote = c; continue; }
125
+ if (c === ']') { end = i; break; }
126
+ }
127
+ if (end < 0 || trimmed.slice(end + 1).trim() !== '') return null;
128
+ const inner = trimmed.slice(1, end).trim();
129
+ if (!inner) return null;
130
+ return splitDottedKey(inner).join('.');
131
+ }
132
+
133
+ /** { normalizedSectionPath -> { key -> rawValueText } } for every `[table]` in the file. */
134
+ function parseTomlTables(text) {
135
+ const tables = new Map([['', {}]]);
136
+ let current = '';
137
+ for (const rawLine of text.split('\n')) {
138
+ const line = stripComment(rawLine).trim();
139
+ if (!line) continue;
140
+ const header = sectionHeaderKey(line);
141
+ if (header !== null) {
142
+ current = header;
143
+ if (!tables.has(current)) tables.set(current, {});
144
+ continue;
145
+ }
146
+ const kv = /^([^=]+?)\s*=\s*(.+)$/.exec(line);
147
+ if (!kv) continue;
148
+ tables.get(current)[kv[1].trim().replace(/^["']|["']$/g, '')] = kv[2].trim();
149
+ }
150
+ return tables;
151
+ }
152
+
153
+ function tomlBool(raw) {
154
+ if (raw === 'true') return true;
155
+ if (raw === 'false') return false;
156
+ return undefined;
157
+ }
158
+
159
+ function sectionTrusted(text, folder) {
160
+ const tables = parseTomlTables(text);
161
+ const table = tables.get(sectionHeaderKey(`[folders."${folder}"]`));
162
+ return table ? tomlBool(table.trusted) === true : false;
163
+ }
164
+
165
+ function grokTrusted() {
166
+ const file = join(homedir(), '.grok', 'trusted_folders.toml');
167
+ if (!existsSync(file)) return false;
168
+ try {
169
+ return sectionTrusted(readFileSync(file, 'utf8'), ROOT);
170
+ } catch {
171
+ return false;
172
+ }
173
+ }
174
+
175
+ /** A `[mcp_servers.<name>]` table exists and is not explicitly `enabled = false`. */
176
+ function configHasServer(file, name) {
177
+ if (!existsSync(file)) return false;
178
+ let tables;
179
+ try {
180
+ tables = parseTomlTables(readFileSync(file, 'utf8'));
181
+ } catch {
182
+ return false;
183
+ }
184
+ const table = tables.get(sectionHeaderKey(`[mcp_servers.${name}]`));
185
+ if (!table) return false;
186
+ return tomlBool(table.enabled) !== false;
187
+ }
188
+
189
+ function readStdinPayload() {
190
+ if (process.stdin.isTTY) return null;
191
+ try {
192
+ const raw = readFileSync(0, 'utf8');
193
+ if (!raw.trim().startsWith('{')) return null;
194
+ return JSON.parse(raw);
195
+ } catch {
196
+ return null;
197
+ }
198
+ }
199
+
200
+ function snapshot(host) {
201
+ if (host === 'claude') {
202
+ const settings = join(ROOT, '.claude', 'settings.json');
203
+ let wired = false;
204
+ if (existsSync(settings)) {
205
+ try {
206
+ wired = Boolean(JSON.parse(readFileSync(settings, 'utf8')).statusLine?.command);
207
+ } catch {
208
+ wired = false;
209
+ }
210
+ }
211
+ return {
212
+ host,
213
+ surface: HOST_STATUS.claude.surface,
214
+ loaded: {
215
+ statusLine: wired,
216
+ helper: has('.claude/helpers/statusline.cjs'),
217
+ },
218
+ };
219
+ }
220
+ if (host === 'codex') {
221
+ const homeCfg = join(homedir(), '.codex', 'config.toml');
222
+ return {
223
+ host,
224
+ surface: null,
225
+ loaded: {
226
+ mcp: configHasServer(homeCfg, 'ruflo') || configHasServer(homeCfg, 'claude-flow'),
227
+ },
228
+ };
229
+ }
230
+ const agents = ['ruflo-architect', 'ruflo-coder', 'ruflo-tester', 'ruflo-reviewer'].filter((name) =>
231
+ has(join('.grok', 'agents', `${name}.md`)),
232
+ );
233
+ const skills = ['agent-teams-grok'].filter((name) =>
234
+ has(join('.grok', 'skills', name, 'SKILL.md')),
235
+ );
236
+ const projectCfg = join(ROOT, '.grok', 'config.toml');
237
+ const userCfg = join(homedir(), '.grok', 'config.toml');
238
+ return {
239
+ host: 'grok',
240
+ surface: HOST_STATUS.grok.surface,
241
+ loaded: {
242
+ mcp: configHasServer(projectCfg, 'ruflo') || configHasServer(projectCfg, 'claude-flow') || configHasServer(userCfg, 'ruflo'),
243
+ rules: has('.grok/rules/ruflo-grok.md'),
244
+ agents: agents.length,
245
+ skills: skills.length,
246
+ hook: has('.grok/hooks/subagent-stop-team.json'),
247
+ trusted: grokTrusted(),
248
+ },
249
+ };
250
+ }
251
+
252
+ function lineFor(snap, stdin) {
253
+ if (snap.host === 'claude') {
254
+ const ok = snap.loaded.statusLine && snap.loaded.helper;
255
+ return ok ? 'RuFlo loaded │ claude statusLine' : 'RuFlo │ claude statusLine missing';
256
+ }
257
+ if (snap.host === 'codex') {
258
+ return snap.loaded.mcp ? 'RuFlo │ codex mcp configured (no status row)' : 'RuFlo │ codex mcp not configured';
259
+ }
260
+ const L = snap.loaded;
261
+ const gaps = [];
262
+ if (!L.mcp) gaps.push('mcp');
263
+ if (!L.rules) gaps.push('rules');
264
+ if (L.agents < 4) gaps.push(`agents ${L.agents}/4`);
265
+ if (L.skills < 1) gaps.push('skill agent-teams-grok');
266
+ if (!L.hook) gaps.push('hook');
267
+ if (!L.trusted) gaps.push('untrusted');
268
+ const head = gaps.length ? `RuFlo │ missing ${gaps.join(', ')}` : 'RuFlo loaded │ ruflo │ rules │ 4 agents │ skill │ hook │ trusted';
269
+ const model = stdin?.model?.display_name;
270
+ const pct = stdin?.context_window?.used_percentage;
271
+ const tail = [model, pct == null ? '' : `${pct}% ctx`].filter(Boolean).join(' │ ');
272
+ return tail ? `${head} │ ${tail}` : head;
273
+ }
274
+
275
+ const snap = snapshot(HOST);
276
+ const text = lineFor(snap, readStdinPayload());
277
+ if (JSON_OUT) {
278
+ process.stdout.write(JSON.stringify({ ...snap, line: text, map: HOST_STATUS }, null, 2) + '\n');
279
+ } else if (text) {
280
+ process.stdout.write(`${text}\n`);
281
+ }
@@ -0,0 +1,98 @@
1
+ ---
2
+ name: agent-teams-grok
3
+ description: >
4
+ Run Ruflo-style named Agent Teams on Grok Build using the host-agnostic team bus
5
+ (scripts/grok-team-bus.mjs). Use when coordinating multi-agent pipelines, feature
6
+ development with architect→coder→tester→reviewer, or replacing Claude SendMessage.
7
+ ---
8
+
9
+ # Agent Teams on Grok (ADR-402)
10
+
11
+ Claude `SendMessage` / TeammateTool are **not** available. Prefer **Ruflo MCP** `team_*` tools; CLI bus is the fallback.
12
+
13
+ ## Preferred: MCP tools
14
+
15
+ | Tool | Purpose |
16
+ |------|---------|
17
+ | `team_create` | Create team + topology |
18
+ | `team_plan` | Ordered pipeline steps |
19
+ | `team_spawn` | Register agent; returns **spawn plan** for `spawn_subagent` |
20
+ | `team_send` / `team_broadcast` | Handoff messages |
21
+ | `team_inbox` | Drain/peek mailbox |
22
+ | `team_status` / `team_on_stop` / `team_shutdown` | Lifecycle |
23
+
24
+ Discover: `search_tool` query `team_create`. Call via `use_tool` as `ruflo__team_create` etc.
25
+
26
+ ## Quick start (CLI fallback)
27
+
28
+ ```bash
29
+ # 1. Create team
30
+ node scripts/grok-team-bus.mjs create --name feature-x --topology hierarchical
31
+
32
+ # 2. Register plan
33
+ node scripts/grok-team-bus.mjs plan --team feature-x \
34
+ --steps '["architect","developer","tester","reviewer"]'
35
+
36
+ # 3. Register agents (prints spawnPlan JSON for spawn_subagent)
37
+ node scripts/grok-team-bus.mjs spawn --team feature-x --agent architect --role architect \
38
+ --prompt "Design <feature>. Handoff to developer." --next developer
39
+ node scripts/grok-team-bus.mjs spawn --team feature-x --agent developer --role developer \
40
+ --prompt "Implement from architect design." --next tester
41
+ node scripts/grok-team-bus.mjs spawn --team feature-x --agent tester --role tester \
42
+ --prompt "Test implementation." --next reviewer
43
+ node scripts/grok-team-bus.mjs spawn --team feature-x --agent reviewer --role reviewer \
44
+ --prompt "Review code and tests."
45
+ ```
46
+
47
+ ## Lead (you) then
48
+
49
+ Checked against **Grok Build 1.0.41**. Pass only the live spawn arguments:
50
+
51
+ ```
52
+ spawn_subagent({
53
+ prompt: spawnPlan.prompt,
54
+ description: spawnPlan.host.grok.spawn.description,
55
+ background: spawnPlan.host.grok.spawn.background,
56
+ isolation: spawnPlan.host.grok.spawn.isolation,
57
+ })
58
+ ```
59
+
60
+ 1. Parse `spawnPlan.host.grok.spawn` (and `prompt`). Call **`spawn_subagent`** in **one message** for every agent (`background: true`).
61
+ 2. Leave `host.grok.advisory` on the plan. `capability_mode` and `subagent_type` are not spawn arguments on this Grok. The prompt carries the read-only or worktree constraint. `isolation` is the knob Grok enforces.
62
+ 3. `.grok/agents/ruflo-*` are session profiles (`grok --agent-profile ruflo-coder` or `/agents`). `spawn_subagent` does not select them. An omitted type is `general-purpose`.
63
+ 4. Children must not call `spawn_subagent`. Nesting depth is 1. The lead spawns the whole pipeline.
64
+ 5. Pass `description` unchanged (`<role>:<agent>@<team>`): the `SubagentStop` hook reads it to run `on-stop` for that team. Then spawn or resume the next step if needed.
65
+ 6. Synthesize results; `shutdown` the team. A shut-down team refuses spawn and send; `team_create --force` reopens the name.
66
+
67
+ Every call names its team: mail is per team, and several teams can run at once. `team_spawn` stops at the team's `maxAgents`.
68
+
69
+ If `grok --version` is newer than 1.0.41, re-read `~/.grok/docs/user-guide/16-subagents.md` (Spawning Subagents) before forwarding extra keys. A key the schema dropped fails the spawn.
70
+
71
+ ## Defaults
72
+
73
+ | Role | Prompt constraint (`advisory.capability_mode`) | `isolation` (passed through) |
74
+ |------|------------------------------------------------|------------------------------|
75
+ | architect / reviewer | read-only | `none` |
76
+ | developer / tester | full tools | **`worktree`** |
77
+
78
+ ## Messaging
79
+
80
+ ```bash
81
+ node scripts/grok-team-bus.mjs send --team feature-x --to developer \
82
+ --from architect --summary "design" --message "..."
83
+ node scripts/grok-team-bus.mjs inbox --team feature-x --agent developer
84
+ node scripts/grok-team-bus.mjs status --team feature-x
85
+ ```
86
+
87
+ ## Also use Ruflo MCP when live
88
+
89
+ - `memory_search` / `memory_store` with namespace `team:feature-x`
90
+ - `swarm_init` for coordination records (then **you** still execute)
91
+
92
+ ## Anti-patterns
93
+
94
+ - Waiting for `swarm start` to write code
95
+ - Inventing SendMessage
96
+ - Passing `capability_mode` or `subagent_type` into `spawn_subagent` (they are advisory on Grok Build 1.0.41)
97
+ - Parallel coders on the same tree without worktrees
98
+ - A child calling `spawn_subagent` (depth limit is 1)