@claude-flow/cli 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.
Files changed (59) hide show
  1. package/.claude/helpers/.helpers-version +1 -1
  2. package/.claude/helpers/helpers.manifest.json +2 -2
  3. package/README.md +2 -0
  4. package/bin/cli.js +19 -1
  5. package/catalog-manifest.json +3 -3
  6. package/dist/src/commands/harness.d.ts +40 -0
  7. package/dist/src/commands/harness.js +142 -0
  8. package/dist/src/commands/index.js +1 -0
  9. package/dist/src/commands/init.js +116 -0
  10. package/dist/src/commands/memory.js +6 -1
  11. package/dist/src/commands/status.js +2 -2
  12. package/dist/src/commands/tui.js +9 -4
  13. package/dist/src/index.d.ts +3 -0
  14. package/dist/src/index.js +12 -2
  15. package/dist/src/init/grok-generator.d.ts +96 -0
  16. package/dist/src/init/grok-generator.js +371 -0
  17. package/dist/src/init/grok-templates.d.ts +11 -0
  18. package/dist/src/init/grok-templates.js +28 -0
  19. package/dist/src/init/index.d.ts +2 -0
  20. package/dist/src/init/index.js +2 -0
  21. package/dist/src/mcp-client.js +4 -0
  22. package/dist/src/mcp-tools/agent-execute-core.js +59 -12
  23. package/dist/src/mcp-tools/capability-brain.d.ts +1 -1
  24. package/dist/src/mcp-tools/capability-brain.js +5 -3
  25. package/dist/src/mcp-tools/embeddings-tools.js +11 -4
  26. package/dist/src/mcp-tools/index.d.ts +1 -0
  27. package/dist/src/mcp-tools/index.js +2 -0
  28. package/dist/src/mcp-tools/memory-tools.js +42 -4
  29. package/dist/src/mcp-tools/task-tools.js +7 -3
  30. package/dist/src/mcp-tools/team-tools.d.ts +15 -0
  31. package/dist/src/mcp-tools/team-tools.js +244 -0
  32. package/dist/src/memory/memory-bridge.d.ts +19 -0
  33. package/dist/src/memory/memory-bridge.js +163 -3
  34. package/dist/src/memory/memory-initializer.d.ts +4 -0
  35. package/dist/src/memory/memory-initializer.js +70 -37
  36. package/dist/src/memory/rabitq-index.d.ts +19 -0
  37. package/dist/src/memory/rabitq-index.js +117 -1
  38. package/dist/src/missions/schemas.d.ts +113 -113
  39. package/dist/src/mods/command-registry/schema.d.ts +193 -193
  40. package/dist/src/services/live-harness.d.ts +111 -0
  41. package/dist/src/services/live-harness.js +427 -0
  42. package/package.json +4 -3
  43. package/plugins/ruflo-metaharness/scripts/_similarity.mjs +62 -2
  44. package/plugins/ruflo-metaharness/scripts/audit-trend.mjs +5 -5
  45. package/plugins/ruflo-metaharness/scripts/similarity.mjs +1 -0
  46. package/plugins/ruflo-metaharness/scripts/test-similarity.mjs +63 -0
  47. package/templates/grok/agents/ruflo-architect.md +35 -0
  48. package/templates/grok/agents/ruflo-coder.md +34 -0
  49. package/templates/grok/agents/ruflo-reviewer.md +30 -0
  50. package/templates/grok/agents/ruflo-tester.md +26 -0
  51. package/templates/grok/config.toml +63 -0
  52. package/templates/grok/docs/README.md +186 -0
  53. package/templates/grok/hooks/subagent-stop-team.json +15 -0
  54. package/templates/grok/rules/ruflo-grok.md +132 -0
  55. package/templates/grok/scripts/grok-subagent-stop-hook.mjs +90 -0
  56. package/templates/grok/scripts/grok-team-bus.mjs +525 -0
  57. package/templates/grok/scripts/grok-team-store.mjs +287 -0
  58. package/templates/grok/scripts/host-statusline.mjs +281 -0
  59. package/templates/grok/skills/agent-teams-grok/SKILL.md +98 -0
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Live harness probe.
3
+ *
4
+ * Reports what is already running for a working tree: the host session, a
5
+ * live daemon pid, ruflo MCP servers whose cwd is inside the tree, processes
6
+ * holding the memory databases open, and teams that are still active.
7
+ *
8
+ * The probe is observe-only. It never writes to disk, never starts anything,
9
+ * and never touches the network. It spawns at most one `ps` and two `lsof`
10
+ * calls, all under one overall deadline, and says "unavailable" instead of
11
+ * guessing when those tools are missing (Windows, minimal Linux).
12
+ *
13
+ * All OS access goes through `HarnessSys` so the probe can be tested without
14
+ * real processes.
15
+ */
16
+ export type ProbeState = 'ok' | 'unavailable' | 'timeout';
17
+ export interface RunResult {
18
+ stdout: string;
19
+ state: ProbeState;
20
+ status: number | null;
21
+ }
22
+ export interface HarnessSys {
23
+ platform: NodeJS.Platform;
24
+ env: NodeJS.ProcessEnv;
25
+ pid: number;
26
+ ppid: number;
27
+ cwd(): string;
28
+ now(): number;
29
+ run(cmd: string, args: string[], timeoutMs: number): RunResult;
30
+ signal0(pid: number): 'alive' | 'dead' | 'eperm';
31
+ exists(path: string): boolean;
32
+ readFile(path: string): string;
33
+ readDir(path: string): Array<{
34
+ name: string;
35
+ isDir: boolean;
36
+ }>;
37
+ mtimeMs(path: string): number | undefined;
38
+ realpath(path: string): string;
39
+ readlink(path: string): string | undefined;
40
+ }
41
+ export interface ProcRow {
42
+ pid: number;
43
+ ppid: number;
44
+ argv: string[];
45
+ }
46
+ export interface LiveHarness {
47
+ root: string;
48
+ host: string;
49
+ parent: {
50
+ pid: number;
51
+ name: string;
52
+ };
53
+ session: Record<string, string>;
54
+ daemon: {
55
+ state: 'live' | 'down' | 'stale';
56
+ pid?: number;
57
+ verified?: boolean;
58
+ reason?: string;
59
+ };
60
+ mcp: {
61
+ count: number | null;
62
+ servers: Array<{
63
+ pid: number;
64
+ command: string;
65
+ }>;
66
+ };
67
+ memory: {
68
+ files: string[];
69
+ openPids: number[] | null;
70
+ };
71
+ teams: Array<{
72
+ name: string;
73
+ status: string;
74
+ members: number;
75
+ lastActivity?: string;
76
+ }>;
77
+ staleTeams: string[];
78
+ probes: {
79
+ ps: ProbeState;
80
+ lsof: ProbeState;
81
+ };
82
+ status: 'live' | 'idle' | 'unknown';
83
+ elapsedMs: number;
84
+ line: string;
85
+ }
86
+ export interface CollectOptions {
87
+ root?: string;
88
+ budgetMs?: number;
89
+ teamTtlMs?: number;
90
+ }
91
+ export declare const realSys: HarnessSys;
92
+ /** The host's project dir if it says so, else the nearest `.git` above cwd, else cwd. */
93
+ export declare function findWorkspaceRoot(sys: HarnessSys): string;
94
+ /** True when `path` is one of `roots` or lies inside one on a path boundary. */
95
+ export declare function insideRoot(path: string, roots: string[]): boolean;
96
+ export declare function parsePs(stdout: string): ProcRow[];
97
+ /** Name a process by its program, looking past node/bun to the script. */
98
+ export declare function programName(argv: string[]): string;
99
+ /** A ruflo MCP server: a launcher, a ruflo identity token, then `mcp [start]`. */
100
+ export declare function isRufloMcp(argv: string[]): boolean;
101
+ /** Program name and subcommand words only; every argument is dropped and counted. */
102
+ export declare function redactCommand(argv: string[]): string;
103
+ export declare function detectHost(sys: HarnessSys, table: Map<number, ProcRow> | null): {
104
+ host: string;
105
+ parent: {
106
+ pid: number;
107
+ name: string;
108
+ };
109
+ };
110
+ export declare function collectLiveHarness(sys?: HarnessSys, opts?: CollectOptions): LiveHarness;
111
+ //# sourceMappingURL=live-harness.d.ts.map
@@ -0,0 +1,427 @@
1
+ /**
2
+ * Live harness probe.
3
+ *
4
+ * Reports what is already running for a working tree: the host session, a
5
+ * live daemon pid, ruflo MCP servers whose cwd is inside the tree, processes
6
+ * holding the memory databases open, and teams that are still active.
7
+ *
8
+ * The probe is observe-only. It never writes to disk, never starts anything,
9
+ * and never touches the network. It spawns at most one `ps` and two `lsof`
10
+ * calls, all under one overall deadline, and says "unavailable" instead of
11
+ * guessing when those tools are missing (Windows, minimal Linux).
12
+ *
13
+ * All OS access goes through `HarnessSys` so the probe can be tested without
14
+ * real processes.
15
+ */
16
+ import { spawnSync } from 'node:child_process';
17
+ import { existsSync, readdirSync, readFileSync, readlinkSync, realpathSync, statSync } from 'node:fs';
18
+ import { basename, dirname, join, sep } from 'node:path';
19
+ const DEFAULT_BUDGET_MS = 4000;
20
+ const PER_CALL_CAP_MS = 1500;
21
+ const MIN_CALL_MS = 100;
22
+ const DEFAULT_TEAM_TTL_MS = 30 * 60 * 1000;
23
+ const SHELLS = new Set(['sh', 'bash', 'zsh', 'dash', 'fish', 'ksh', 'tcsh', 'csh', 'env', 'timeout', 'npm', 'npx', 'pnpm', 'script']);
24
+ const INTERPRETERS = new Set(['node', 'nodejs', 'bun', 'deno']);
25
+ const MCP_LAUNCHERS = new Set(['node', 'nodejs', 'bun', 'deno', 'npx', 'npm', 'pnpm', 'pnpx', 'yarn', 'ruflo', 'claude-flow']);
26
+ const RUFLO_ID = /(^|\/)(ruflo|claude-flow)(@[\w.-]+)?$|(^|\/)@claude-flow\/cli(@[\w.-]+)?$|\/@claude-flow\/cli\/bin\/cli\.js$|\/ruflo\/bin\/[\w.-]+\.js$/;
27
+ const TEAM_ENDED = new Set(['shutdown', 'stopped']);
28
+ export const realSys = {
29
+ platform: process.platform,
30
+ env: process.env,
31
+ pid: process.pid,
32
+ ppid: process.ppid,
33
+ cwd: () => process.cwd(),
34
+ now: () => Date.now(),
35
+ run(cmd, args, timeoutMs) {
36
+ const r = spawnSync(cmd, args, {
37
+ encoding: 'utf8',
38
+ timeout: timeoutMs,
39
+ stdio: ['ignore', 'pipe', 'ignore'],
40
+ maxBuffer: 16 * 1024 * 1024,
41
+ windowsHide: true,
42
+ });
43
+ const code = r.error?.code;
44
+ if (code === 'ETIMEDOUT')
45
+ return { stdout: r.stdout ?? '', state: 'timeout', status: r.status };
46
+ if (r.error)
47
+ return { stdout: '', state: 'unavailable', status: r.status };
48
+ return { stdout: r.stdout ?? '', state: 'ok', status: r.status };
49
+ },
50
+ signal0(pid) {
51
+ try {
52
+ process.kill(pid, 0);
53
+ return 'alive';
54
+ }
55
+ catch (error) {
56
+ // EPERM: the pid exists but belongs to another user.
57
+ return error.code === 'EPERM' ? 'eperm' : 'dead';
58
+ }
59
+ },
60
+ exists: (path) => existsSync(path),
61
+ readFile: (path) => readFileSync(path, 'utf8'),
62
+ readDir: (path) => readdirSync(path, { withFileTypes: true }).map((d) => ({ name: d.name, isDir: d.isDirectory() })),
63
+ mtimeMs(path) {
64
+ try {
65
+ return statSync(path).mtimeMs;
66
+ }
67
+ catch {
68
+ return undefined;
69
+ }
70
+ },
71
+ realpath(path) {
72
+ try {
73
+ return realpathSync(path);
74
+ }
75
+ catch {
76
+ return path;
77
+ }
78
+ },
79
+ readlink(path) {
80
+ try {
81
+ return readlinkSync(path);
82
+ }
83
+ catch {
84
+ return undefined;
85
+ }
86
+ },
87
+ };
88
+ /** Overall deadline shared by every external call. */
89
+ class Budget {
90
+ sys;
91
+ end;
92
+ constructor(sys, totalMs) {
93
+ this.sys = sys;
94
+ this.end = sys.now() + totalMs;
95
+ }
96
+ run(cmd, args) {
97
+ const left = this.end - this.sys.now();
98
+ if (left < MIN_CALL_MS)
99
+ return { stdout: '', state: 'timeout', status: null };
100
+ return this.sys.run(cmd, args, Math.min(PER_CALL_CAP_MS, left));
101
+ }
102
+ }
103
+ /** The host's project dir if it says so, else the nearest `.git` above cwd, else cwd. */
104
+ export function findWorkspaceRoot(sys) {
105
+ const fromEnv = sys.env.GROK_WORKSPACE_ROOT || sys.env.CLAUDE_PROJECT_DIR || sys.env.CODEX_PROJECT_DIR;
106
+ if (fromEnv)
107
+ return fromEnv;
108
+ const start = sys.cwd();
109
+ let dir = start;
110
+ for (;;) {
111
+ if (sys.exists(join(dir, '.git')))
112
+ return dir;
113
+ const parent = dirname(dir);
114
+ if (parent === dir)
115
+ return start;
116
+ dir = parent;
117
+ }
118
+ }
119
+ /** True when `path` is one of `roots` or lies inside one on a path boundary. */
120
+ export function insideRoot(path, roots) {
121
+ return roots.some((root) => {
122
+ const base = root.length > 1 && root.endsWith(sep) ? root.slice(0, -1) : root;
123
+ return path === base || path.startsWith(base + sep);
124
+ });
125
+ }
126
+ export function parsePs(stdout) {
127
+ const rows = [];
128
+ for (const line of stdout.split('\n')) {
129
+ const m = /^\s*(\d+)\s+(\d+)\s+(.*\S)\s*$/.exec(line);
130
+ if (m)
131
+ rows.push({ pid: Number(m[1]), ppid: Number(m[2]), argv: m[3].split(/\s+/) });
132
+ }
133
+ return rows;
134
+ }
135
+ /** Name a process by its program, looking past node/bun to the script. */
136
+ export function programName(argv) {
137
+ const first = basename(argv[0] ?? '').replace(/^-/, '');
138
+ if (INTERPRETERS.has(first) && argv[1] && !argv[1].startsWith('-')) {
139
+ return basename(argv[1]).replace(/\.(c|m)?js$/, '');
140
+ }
141
+ return first;
142
+ }
143
+ /** A ruflo MCP server: a launcher, a ruflo identity token, then `mcp [start]`. */
144
+ export function isRufloMcp(argv) {
145
+ if (!MCP_LAUNCHERS.has(basename(argv[0] ?? '')))
146
+ return false;
147
+ const at = argv.indexOf('mcp');
148
+ if (at < 0)
149
+ return false;
150
+ const next = argv[at + 1];
151
+ if (next !== undefined && next !== 'start' && !next.startsWith('-'))
152
+ return false;
153
+ return argv.slice(0, at).some((token) => RUFLO_ID.test(token));
154
+ }
155
+ /** Program name and subcommand words only; every argument is dropped and counted. */
156
+ export function redactCommand(argv) {
157
+ const words = [programName(argv), 'mcp'];
158
+ const at = argv.indexOf('mcp');
159
+ if (argv[at + 1] === 'start')
160
+ words.push('start');
161
+ const kept = at + words.length - 1;
162
+ const dropped = Math.max(0, argv.length - kept);
163
+ return dropped > 0 ? `${words.join(' ')} (+${dropped} args)` : words.join(' ');
164
+ }
165
+ export function detectHost(sys, table) {
166
+ const env = sys.env;
167
+ let parent = { pid: sys.ppid, name: '' };
168
+ const names = [];
169
+ if (table) {
170
+ // Hooks usually run under `sh -c`; walk past shells and wrappers.
171
+ let pid = sys.ppid;
172
+ for (let depth = 0; depth < 8 && pid > 1; depth++) {
173
+ const row = table.get(pid);
174
+ if (!row)
175
+ break;
176
+ const name = programName(row.argv);
177
+ if (!SHELLS.has(name)) {
178
+ if (!parent.name)
179
+ parent = { pid, name };
180
+ names.push(name);
181
+ }
182
+ pid = row.ppid;
183
+ }
184
+ }
185
+ const seen = (re) => names.some((n) => re.test(n));
186
+ let host;
187
+ if (env.WEFTOS_SESSION || env.WEFT_SESSION || seen(/^weftos/i))
188
+ host = 'weftos';
189
+ else if (env.GROK_SESSION_ID || env.GROK_WORKSPACE_ROOT || seen(/^grok([-_.]|$)/))
190
+ host = 'grok';
191
+ else if (env.CODEX_THREAD_ID || env.CODEX_SANDBOX || env.CODEX_CI || seen(/^codex([-_.]|$)/))
192
+ host = 'codex';
193
+ else if (env.CLAUDECODE || env.CLAUDE_CODE_ENTRYPOINT || seen(/^claude([-_.]|$)/))
194
+ host = 'claude';
195
+ else
196
+ host = parent.name ? `process:${parent.name}` : 'unknown';
197
+ return { host, parent };
198
+ }
199
+ function sessionIds(env) {
200
+ const out = {};
201
+ for (const key of ['GROK_SESSION_ID', 'CODEX_THREAD_ID', 'CLAUDE_SESSION_ID', 'WEFTOS_SESSION', 'WEFT_SESSION']) {
202
+ const value = env[key];
203
+ if (value)
204
+ out[key] = value;
205
+ }
206
+ return out;
207
+ }
208
+ function daemonState(sys, root, roots, table) {
209
+ const pidFile = join(root, '.claude-flow', 'daemon.pid');
210
+ if (!sys.exists(pidFile))
211
+ return { state: 'down' };
212
+ let raw;
213
+ try {
214
+ raw = sys.readFile(pidFile).trim();
215
+ }
216
+ catch {
217
+ return { state: 'down' };
218
+ }
219
+ const pid = Number(raw);
220
+ if (!Number.isInteger(pid) || pid <= 0)
221
+ return { state: 'stale', reason: 'bad pidfile' };
222
+ // EPERM means the pid exists under another user; only ESRCH means gone.
223
+ if (sys.signal0(pid) === 'dead')
224
+ return { state: 'stale', pid, reason: 'not running' };
225
+ const row = table?.get(pid);
226
+ if (!row)
227
+ return { state: 'live', pid, verified: false };
228
+ // The pid is alive; make sure it is still our daemon and not a recycled pid.
229
+ if (!row.argv.includes('daemon'))
230
+ return { state: 'stale', pid, reason: 'pid reused' };
231
+ const ws = row.argv.indexOf('--workspace');
232
+ const workspace = ws >= 0 ? row.argv[ws + 1] : undefined;
233
+ if (workspace && !roots.includes(workspace) && !roots.includes(sys.realpath(workspace))) {
234
+ return { state: 'stale', pid, reason: 'pid belongs to another workspace' };
235
+ }
236
+ return { state: 'live', pid, verified: true };
237
+ }
238
+ /** cwd for each pid: /proc on Linux, then one batched lsof for the rest. */
239
+ function processCwds(sys, budget, pids) {
240
+ const cwds = new Map();
241
+ if (sys.platform === 'linux') {
242
+ for (const pid of pids) {
243
+ const cwd = sys.readlink(`/proc/${pid}/cwd`);
244
+ if (cwd)
245
+ cwds.set(pid, cwd);
246
+ }
247
+ }
248
+ const rest = pids.filter((pid) => !cwds.has(pid));
249
+ if (rest.length === 0)
250
+ return { cwds, lsof: null };
251
+ const r = budget.run('lsof', ['-a', '-d', 'cwd', '-p', rest.join(','), '-Fpn']);
252
+ let current = 0;
253
+ for (const line of r.stdout.split('\n')) {
254
+ if (line.startsWith('p'))
255
+ current = Number(line.slice(1));
256
+ else if (line.startsWith('n') && current)
257
+ cwds.set(current, line.slice(1));
258
+ }
259
+ return { cwds, lsof: r.state };
260
+ }
261
+ function mcpServers(sys, budget, roots, rows) {
262
+ let candidates = rows.filter((row) => row.pid !== sys.pid && isRufloMcp(row.argv));
263
+ // `npx ruflo mcp start` forks the real server; count the leaf only.
264
+ const parents = new Set(candidates.map((row) => row.ppid));
265
+ candidates = candidates.filter((row) => !parents.has(row.pid));
266
+ if (candidates.length === 0)
267
+ return { mcp: { count: 0, servers: [] }, lsof: null };
268
+ const { cwds, lsof } = processCwds(sys, budget, candidates.map((row) => row.pid));
269
+ if (lsof !== null && lsof !== 'ok' && cwds.size < candidates.length) {
270
+ return { mcp: { count: null, servers: [] }, lsof };
271
+ }
272
+ const servers = candidates
273
+ .filter((row) => {
274
+ const cwd = cwds.get(row.pid);
275
+ return cwd !== undefined && (insideRoot(cwd, roots) || insideRoot(sys.realpath(cwd), roots));
276
+ })
277
+ .map((row) => ({ pid: row.pid, command: redactCommand(row.argv) }));
278
+ return { mcp: { count: servers.length, servers }, lsof };
279
+ }
280
+ function memoryHolders(sys, budget, root) {
281
+ // memory.db is the sql.js writer; agentdb-memory.db is the native AgentDB store.
282
+ const files = [];
283
+ for (const db of ['memory.db', 'agentdb-memory.db']) {
284
+ for (const suffix of ['', '-wal', '-shm']) {
285
+ const path = join(root, '.swarm', db + suffix);
286
+ if (sys.exists(path))
287
+ files.push(path);
288
+ }
289
+ }
290
+ if (files.length === 0)
291
+ return { memory: { files, openPids: [] }, lsof: null };
292
+ if (sys.platform === 'win32')
293
+ return { memory: { files, openPids: null }, lsof: 'unavailable' };
294
+ const r = budget.run('lsof', ['-t', '--', ...files]);
295
+ if (r.state !== 'ok')
296
+ return { memory: { files, openPids: null }, lsof: r.state };
297
+ const pids = r.stdout
298
+ .split('\n')
299
+ .map((row) => Number(row.trim()))
300
+ .filter((pid) => pid > 0 && pid !== sys.pid);
301
+ return { memory: { files, openPids: [...new Set(pids)] }, lsof: 'ok' };
302
+ }
303
+ function lastActivity(sys, file, team) {
304
+ const stamps = [team.createdAt, team.updatedAt, team.plan?.updatedAt];
305
+ for (const member of Object.values(team.members ?? {})) {
306
+ stamps.push(member?.registeredAt, member?.lastStopAt, member?.lastSeenAt);
307
+ }
308
+ const times = stamps.map((s) => (s ? Date.parse(s) : NaN)).filter((t) => Number.isFinite(t));
309
+ return Math.max(sys.mtimeMs(file) ?? 0, ...times, 0);
310
+ }
311
+ /**
312
+ * Teams under .claude-flow/teams: `<name>/team.json` (the team bus layout)
313
+ * and the legacy flat `<name>.json`. A team counts as live only if it has not
314
+ * shut down AND shows activity within the TTL or has a member pid running.
315
+ */
316
+ function teamsIn(sys, root, ttlMs) {
317
+ const dir = join(root, '.claude-flow', 'teams');
318
+ const live = [];
319
+ const stale = [];
320
+ if (!sys.exists(dir))
321
+ return { live, stale };
322
+ let entries;
323
+ try {
324
+ entries = sys.readDir(dir);
325
+ }
326
+ catch {
327
+ return { live, stale };
328
+ }
329
+ for (const entry of entries) {
330
+ const file = entry.isDir
331
+ ? join(dir, entry.name, 'team.json')
332
+ : entry.name.endsWith('.json') ? join(dir, entry.name) : '';
333
+ if (!file || !sys.exists(file))
334
+ continue;
335
+ let team;
336
+ try {
337
+ team = JSON.parse(sys.readFile(file));
338
+ }
339
+ catch {
340
+ continue; // an unreadable team file is not a live team
341
+ }
342
+ if (!team || typeof team !== 'object')
343
+ continue;
344
+ if (team.shutdownAt || TEAM_ENDED.has(String(team.status)))
345
+ continue;
346
+ const name = team.name || entry.name.replace(/\.json$/, '');
347
+ const last = lastActivity(sys, file, team);
348
+ const pidAlive = Object.values(team.members ?? {}).some((m) => Number.isInteger(m?.pid) && m.pid > 0 && sys.signal0(m.pid) !== 'dead');
349
+ if (!pidAlive && sys.now() - last > ttlMs) {
350
+ stale.push(name);
351
+ continue;
352
+ }
353
+ live.push({
354
+ name,
355
+ status: team.status || 'unknown',
356
+ members: team.members ? Object.keys(team.members).length : 0,
357
+ lastActivity: last ? new Date(last).toISOString() : undefined,
358
+ });
359
+ }
360
+ return { live, stale };
361
+ }
362
+ function worst(...states) {
363
+ const rank = { ok: 0, timeout: 1, unavailable: 2 };
364
+ return states.reduce((acc, s) => (s !== null && rank[s] > rank[acc] ? s : acc), 'ok');
365
+ }
366
+ function teamTtl(sys, fallback) {
367
+ const secs = Number(sys.env.RUFLO_HARNESS_TEAM_TTL_SECS);
368
+ return Number.isFinite(secs) && secs > 0 ? secs * 1000 : fallback;
369
+ }
370
+ export function collectLiveHarness(sys = realSys, opts = {}) {
371
+ const start = sys.now();
372
+ const budget = new Budget(sys, opts.budgetMs ?? DEFAULT_BUDGET_MS);
373
+ const root = opts.root ?? findWorkspaceRoot(sys);
374
+ // Both spellings of the root, so macOS /tmp and /private/tmp compare equal.
375
+ const roots = [...new Set([root, sys.realpath(root)])];
376
+ let ps = 'unavailable';
377
+ let rows = [];
378
+ if (sys.platform !== 'win32') {
379
+ const r = budget.run('ps', ['-ax', '-o', 'pid=,ppid=,command=']);
380
+ rows = r.state === 'ok' ? parsePs(r.stdout) : [];
381
+ // busybox ps rejects -ax: "ran but listed nothing" is unavailable, not idle.
382
+ ps = r.state === 'ok' && rows.length === 0 ? 'unavailable' : r.state;
383
+ }
384
+ const table = ps === 'ok' ? new Map(rows.map((row) => [row.pid, row])) : null;
385
+ const { host, parent } = detectHost(sys, table);
386
+ const daemon = daemonState(sys, root, roots, table);
387
+ const mcpProbe = ps === 'ok'
388
+ ? mcpServers(sys, budget, roots, rows)
389
+ : { mcp: { count: null, servers: [] }, lsof: null };
390
+ const memProbe = memoryHolders(sys, budget, root);
391
+ const teams = teamsIn(sys, root, teamTtl(sys, opts.teamTtlMs ?? DEFAULT_TEAM_TTL_MS));
392
+ const probes = { ps, lsof: worst(mcpProbe.lsof, memProbe.lsof) };
393
+ const openCount = memProbe.memory.openPids === null ? null : memProbe.memory.openPids.length;
394
+ const live = daemon.state === 'live' || (mcpProbe.mcp.count ?? 0) > 0 || (openCount ?? 0) > 0 || teams.live.length > 0;
395
+ const unknown = mcpProbe.mcp.count === null || openCount === null;
396
+ const status = live ? 'live' : unknown ? 'unknown' : 'idle';
397
+ const unavailable = Object.entries(probes).filter(([, s]) => s !== 'ok').map(([k, s]) => `${k}:${s}`);
398
+ const count = (n) => (n === null ? '?' : String(n));
399
+ const parts = [
400
+ 'harness',
401
+ status,
402
+ `host=${host}`,
403
+ parent.name ? `parent=${parent.name}` : '',
404
+ `daemon=${daemon.state}${daemon.pid ? `:${daemon.pid}` : ''}`,
405
+ `mcp=${count(mcpProbe.mcp.count)}`,
406
+ `memory-open=${count(openCount)}`,
407
+ `teams=${teams.live.length}`,
408
+ teams.stale.length ? `stale-teams=${teams.stale.length}` : '',
409
+ unavailable.length ? `probe-unavailable=${unavailable.join(',')}` : '',
410
+ ].filter(Boolean);
411
+ return {
412
+ root,
413
+ host,
414
+ parent,
415
+ session: sessionIds(sys.env),
416
+ daemon,
417
+ mcp: mcpProbe.mcp,
418
+ memory: memProbe.memory,
419
+ teams: teams.live,
420
+ staleTeams: teams.stale,
421
+ probes,
422
+ status,
423
+ elapsedMs: sys.now() - start,
424
+ line: parts.join(' │ '),
425
+ };
426
+ }
427
+ //# sourceMappingURL=live-harness.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@claude-flow/cli",
3
- "version": "3.58.1",
3
+ "version": "3.59.1",
4
4
  "type": "module",
5
5
  "description": "Ruflo CLI - Enterprise AI agent orchestration with 60+ specialized agents, swarm coordination, MCP server, self-learning hooks, and vector memory for Claude Code",
6
6
  "main": "dist/src/index.js",
@@ -76,6 +76,7 @@
76
76
  "!dist/**/*.tsbuildinfo",
77
77
  "bin",
78
78
  "scripts/postinstall.cjs",
79
+ "templates",
79
80
  ".claude",
80
81
  "plugins",
81
82
  "README.md",
@@ -126,11 +127,11 @@
126
127
  "ws": "^8.21.0",
127
128
  "yaml": "^2.8.0",
128
129
  "zod": "^3.22.0",
129
- "@claude-flow/memory": "3.0.6"
130
+ "@claude-flow/memory": "3.0.8"
130
131
  },
131
132
  "optionalDependencies": {
132
133
  "@agntcy/slim-bindings": "2.0.0-alpha.5",
133
- "@claude-flow/memory": "3.0.6",
134
+ "@claude-flow/memory": "3.0.8",
134
135
  "@metaharness/darwin": "~0.12.0",
135
136
  "@metaharness/flywheel": "~0.1.10",
136
137
  "@metaharness/radio": "~0.1.0",
@@ -13,10 +13,13 @@
13
13
  //
14
14
  // CONTRACT (PUBLIC)
15
15
  // projectToVec(input) → 9-dim numerical feature vector
16
+ // normalizeCost(usd) → [0,1] log-band normalization (vector index 8)
16
17
  // cosine(a, b) → [0,1]
17
18
  // categoricalAgreement(a, b) → [0,1] over 4 enum fields
18
19
  // jaccard(a, b) → [0,1] over agent_topology[]
19
- // similarity(a, b, opts?) → { overall, components, perDimension? }
20
+ // verdictFor(overall) → 'near-identical' | 'minor-drift' | 'moderate-drift' | 'major-drift'
21
+ // VERDICT_THRESHOLDS → the bands verdictFor() walks, highest first
22
+ // similarity(a, b, opts?) → { overall, verdict, components, perDimension? }
20
23
  //
21
24
  // WEIGHT DEFAULTS (from ADR-152 §Decision)
22
25
  // overall = 0.60·cosine + 0.25·categorical + 0.15·jaccard
@@ -29,6 +32,59 @@ const DEFAULT_WEIGHTS = Object.freeze({ cosine: 0.6, categorical: 0.25, jaccard:
29
32
 
30
33
  const CATEGORICAL_FIELDS = Object.freeze(['repo_type', 'archetype', 'template', 'recommendedMode']);
31
34
 
35
+ // ─────────────────────────────────────────────────────────────────────
36
+ // Cost normalization — vector index 8.
37
+ //
38
+ // ADR-152 Table 1 specified `log10(usd + 0.001) / log10(10)` clamped to
39
+ // [0,1]. `Math.log10(10)` is 1, so the division was a no-op, and the raw
40
+ // log is negative for every cost below $0.999 — the outer clamp then
41
+ // flattened that whole range to 0. Every realistic per-run cost collapsed
42
+ // to the same value, so the documented 9-dim vector was effectively
43
+ // 8-dim and `estCostPerRunUsd` could not influence any score.
44
+ //
45
+ // The replacement is a real log band: costs at or below `min` map to 0,
46
+ // at or above `max` map to 1, log-interpolated in between. It is
47
+ // monotonic and live across the whole realistic range ($0.001–$10), and
48
+ // non-finite or missing input degrades to the band floor rather than NaN.
49
+ // ─────────────────────────────────────────────────────────────────────
50
+
51
+ export const COST_BAND_USD = Object.freeze({ min: 0.001, max: 10 });
52
+
53
+ export function normalizeCost(usd) {
54
+ const lo = Math.log10(COST_BAND_USD.min);
55
+ const hi = Math.log10(COST_BAND_USD.max);
56
+ const finite = typeof usd === 'number' && Number.isFinite(usd) ? usd : COST_BAND_USD.min;
57
+ const clamped = Math.min(COST_BAND_USD.max, Math.max(COST_BAND_USD.min, finite));
58
+ return (Math.log10(clamped) - lo) / (hi - lo);
59
+ }
60
+
61
+ // ─────────────────────────────────────────────────────────────────────
62
+ // Verdict bands — single source of truth.
63
+ //
64
+ // These bands previously lived inline in `audit-trend.mjs` with a
65
+ // `minor-drift` floor of 0.80, while `docs/metaharness-user-guide.md`
66
+ // documented 0.85 for the same band. Two encodings of one policy, and
67
+ // they disagreed. Consumers now import this table instead of re-encoding
68
+ // the bands, so the primitive and its documentation cannot drift apart.
69
+ //
70
+ // Ordered highest-first; `verdictFor` returns the first band whose `min`
71
+ // the score reaches.
72
+ // ─────────────────────────────────────────────────────────────────────
73
+
74
+ export const VERDICT_THRESHOLDS = Object.freeze([
75
+ Object.freeze({ min: 0.95, verdict: 'near-identical' }),
76
+ Object.freeze({ min: 0.85, verdict: 'minor-drift' }),
77
+ Object.freeze({ min: 0.5, verdict: 'moderate-drift' }),
78
+ Object.freeze({ min: Number.NEGATIVE_INFINITY, verdict: 'major-drift' }),
79
+ ]);
80
+
81
+ export function verdictFor(overall) {
82
+ for (const band of VERDICT_THRESHOLDS) {
83
+ if (overall >= band.min) return band.verdict;
84
+ }
85
+ return 'major-drift';
86
+ }
87
+
32
88
  // ─────────────────────────────────────────────────────────────────────
33
89
  // 9-dim feature vector. The mapping mirrors ADR-152 §Decision Table 1.
34
90
  // Missing fields default to 0 — that's the graceful-degradation path.
@@ -46,7 +102,7 @@ export function projectToVec(input) {
46
102
  g.risk_score ?? 0,
47
103
  g.test_confidence ?? 0,
48
104
  g.publish_readiness ?? 0,
49
- Math.max(0, Math.min(1, Math.log10((s.estCostPerRunUsd ?? 0) + 0.001) / Math.log10(10))),
105
+ normalizeCost(s.estCostPerRunUsd),
50
106
  ];
51
107
  }
52
108
 
@@ -99,6 +155,10 @@ export function similarity(a, b, opts = {}) {
99
155
 
100
156
  const result = {
101
157
  overall: round4(overall),
158
+ // The verdict is derived here, next to the score it classifies, so
159
+ // every consumer (similarity.mjs, audit-trend.mjs) reports the same
160
+ // band for the same number.
161
+ verdict: verdictFor(round4(overall)),
102
162
  components: {
103
163
  cosine: round4(cos),
104
164
  categorical: round4(cat),
@@ -31,7 +31,10 @@ import { runRufloCli } from './_invoke.mjs';
31
31
  // iter 38 — structural-distance drift via ADR-152 §3.1 production module.
32
32
  // Falls back to null if either record predates iter-38 oia-audit (no
33
33
  // fingerprint field) — graceful degradation, never throws.
34
- import { similarity } from './_similarity.mjs';
34
+ // The verdict bands come from the module, not a local literal: this file
35
+ // previously re-encoded them with a 0.80 minor-drift floor while
36
+ // docs/metaharness-user-guide.md documented 0.85.
37
+ import { similarity, verdictFor } from './_similarity.mjs';
35
38
  // iter 63 — shared SEVERITY_RANK from _harness.mjs (was a local literal
36
39
  // missing info/warn/error/critical, which caused NaN-compare hazards).
37
40
  import { SEVERITY_RANK, rankSeverity } from './_harness.mjs';
@@ -168,10 +171,7 @@ function main() {
168
171
  // Distance is the complement of similarity in [0,1]
169
172
  distance: Number((1 - sim.overall).toFixed(4)),
170
173
  components: sim.components,
171
- verdict: sim.overall >= 0.95 ? 'near-identical'
172
- : sim.overall >= 0.80 ? 'minor-drift'
173
- : sim.overall >= 0.50 ? 'moderate-drift'
174
- : 'major-drift',
174
+ verdict: verdictFor(sim.overall),
175
175
  };
176
176
  } else {
177
177
  structuralDistance = {