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