claude-flow 3.58.1 → 3.59.1
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.
- package/.claude/proven-config.json +1 -1
- package/README.md +2 -0
- package/package.json +2 -1
- package/v3/@claude-flow/cli/README.md +2 -0
- package/v3/@claude-flow/cli/bin/cli.js +19 -1
- package/v3/@claude-flow/cli/catalog-manifest.json +3 -3
- package/v3/@claude-flow/cli/dist/src/commands/harness.d.ts +40 -0
- package/v3/@claude-flow/cli/dist/src/commands/harness.js +142 -0
- package/v3/@claude-flow/cli/dist/src/commands/index.js +1 -0
- package/v3/@claude-flow/cli/dist/src/commands/init.js +116 -0
- package/v3/@claude-flow/cli/dist/src/commands/memory.js +6 -1
- package/v3/@claude-flow/cli/dist/src/commands/status.js +2 -2
- package/v3/@claude-flow/cli/dist/src/commands/tui.js +9 -4
- package/v3/@claude-flow/cli/dist/src/index.d.ts +3 -0
- package/v3/@claude-flow/cli/dist/src/index.js +12 -2
- package/v3/@claude-flow/cli/dist/src/init/grok-generator.d.ts +96 -0
- package/v3/@claude-flow/cli/dist/src/init/grok-generator.js +371 -0
- package/v3/@claude-flow/cli/dist/src/init/grok-templates.d.ts +11 -0
- package/v3/@claude-flow/cli/dist/src/init/grok-templates.js +28 -0
- package/v3/@claude-flow/cli/dist/src/init/index.d.ts +2 -0
- package/v3/@claude-flow/cli/dist/src/init/index.js +2 -0
- package/v3/@claude-flow/cli/dist/src/mcp-client.js +4 -0
- package/v3/@claude-flow/cli/dist/src/mcp-tools/agent-execute-core.js +59 -12
- package/v3/@claude-flow/cli/dist/src/mcp-tools/capability-brain.d.ts +1 -1
- package/v3/@claude-flow/cli/dist/src/mcp-tools/capability-brain.js +5 -3
- package/v3/@claude-flow/cli/dist/src/mcp-tools/embeddings-tools.js +11 -4
- package/v3/@claude-flow/cli/dist/src/mcp-tools/index.d.ts +1 -0
- package/v3/@claude-flow/cli/dist/src/mcp-tools/index.js +2 -0
- package/v3/@claude-flow/cli/dist/src/mcp-tools/memory-tools.js +42 -4
- package/v3/@claude-flow/cli/dist/src/mcp-tools/task-tools.js +7 -3
- package/v3/@claude-flow/cli/dist/src/mcp-tools/team-tools.d.ts +15 -0
- package/v3/@claude-flow/cli/dist/src/mcp-tools/team-tools.js +244 -0
- package/v3/@claude-flow/cli/dist/src/memory/memory-bridge.d.ts +19 -0
- package/v3/@claude-flow/cli/dist/src/memory/memory-bridge.js +163 -3
- package/v3/@claude-flow/cli/dist/src/memory/memory-initializer.d.ts +4 -0
- package/v3/@claude-flow/cli/dist/src/memory/memory-initializer.js +70 -37
- package/v3/@claude-flow/cli/dist/src/memory/rabitq-index.d.ts +19 -0
- package/v3/@claude-flow/cli/dist/src/memory/rabitq-index.js +117 -1
- package/v3/@claude-flow/cli/dist/src/missions/schemas.d.ts +113 -113
- package/v3/@claude-flow/cli/dist/src/mods/command-registry/schema.d.ts +193 -193
- package/v3/@claude-flow/cli/dist/src/services/live-harness.d.ts +111 -0
- package/v3/@claude-flow/cli/dist/src/services/live-harness.js +427 -0
- package/v3/@claude-flow/cli/package.json +4 -3
- package/v3/@claude-flow/cli/templates/grok/agents/ruflo-architect.md +35 -0
- package/v3/@claude-flow/cli/templates/grok/agents/ruflo-coder.md +34 -0
- package/v3/@claude-flow/cli/templates/grok/agents/ruflo-reviewer.md +30 -0
- package/v3/@claude-flow/cli/templates/grok/agents/ruflo-tester.md +26 -0
- package/v3/@claude-flow/cli/templates/grok/config.toml +63 -0
- package/v3/@claude-flow/cli/templates/grok/docs/README.md +186 -0
- package/v3/@claude-flow/cli/templates/grok/hooks/subagent-stop-team.json +15 -0
- package/v3/@claude-flow/cli/templates/grok/rules/ruflo-grok.md +132 -0
- package/v3/@claude-flow/cli/templates/grok/scripts/grok-subagent-stop-hook.mjs +90 -0
- package/v3/@claude-flow/cli/templates/grok/scripts/grok-team-bus.mjs +525 -0
- package/v3/@claude-flow/cli/templates/grok/scripts/grok-team-store.mjs +287 -0
- package/v3/@claude-flow/cli/templates/grok/scripts/host-statusline.mjs +281 -0
- 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)
|