@claude-flow/cli 3.32.34 β 3.32.36
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.manifest.json +5 -5
- package/.claude/helpers/hook-handler.cjs +30 -0
- package/.claude/helpers/intelligence.cjs +47 -4
- package/.claude/helpers/statusline.cjs +169 -6
- package/bin/cli.js +25 -1
- package/catalog-manifest.json +4 -4
- package/dist/src/commands/agent.js +50 -2
- package/dist/src/commands/doctor.js +159 -11
- package/dist/src/commands/hooks.js +63 -4
- package/dist/src/commands/mcp.js +1 -1
- package/dist/src/commands/memory-distill.js +1 -0
- package/dist/src/commands/metaharness.js +4 -0
- package/dist/src/commands/swarm.js +89 -5
- package/dist/src/config-adapter.js +1 -0
- package/dist/src/index.js +3 -2
- package/dist/src/init/executor.js +16 -10
- package/dist/src/init/helpers-generator.js +15 -4
- package/dist/src/init/statusline-generator.js +18 -5
- package/dist/src/mcp-server.d.ts +25 -0
- package/dist/src/mcp-server.js +57 -2
- package/dist/src/mcp-tools/agent-tools.js +10 -0
- package/dist/src/mcp-tools/embeddings-tools.js +51 -10
- package/dist/src/mcp-tools/hooks-tools.js +120 -60
- package/dist/src/mcp-tools/memory-tools.js +14 -2
- package/dist/src/mcp-tools/metaharness-tools.js +7 -0
- package/dist/src/mcp-tools/swarm-tools.d.ts +16 -0
- package/dist/src/mcp-tools/swarm-tools.js +172 -7
- package/dist/src/memory/memory-bridge.d.ts +2 -0
- package/dist/src/memory/memory-bridge.js +5 -1
- package/dist/src/memory/memory-initializer.d.ts +9 -1
- package/dist/src/memory/memory-initializer.js +66 -19
- package/dist/src/services/daemon-autostart.js +7 -4
- package/dist/src/services/flywheel-receipt.d.ts +3 -0
- package/dist/src/services/flywheel-receipt.js +1 -0
- package/dist/src/services/harness-flywheel-runtime.d.ts +6 -0
- package/dist/src/services/harness-flywheel-runtime.js +19 -19
- package/dist/src/services/harness-flywheel.d.ts +2 -0
- package/dist/src/services/harness-flywheel.js +1 -0
- package/dist/src/services/harness-project-anchor.d.ts +23 -0
- package/dist/src/services/harness-project-anchor.js +129 -0
- package/dist/src/services/learned-routing.d.ts +34 -0
- package/dist/src/services/learned-routing.js +85 -0
- package/dist/src/services/memory-distillation.d.ts +1 -0
- package/dist/src/services/memory-distillation.js +81 -5
- package/dist/src/services/pheromone-adaptive.d.ts +71 -0
- package/dist/src/services/pheromone-adaptive.js +214 -0
- package/dist/src/services/worker-daemon.js +5 -1
- package/node_modules/@claude-flow/codex/.agents/skills/github-automation/SKILL.md +32 -0
- package/node_modules/@claude-flow/codex/.agents/skills/performance-analysis/SKILL.md +32 -0
- package/node_modules/@claude-flow/codex/dist/dual-mode/cli.d.ts +4 -0
- package/node_modules/@claude-flow/codex/dist/dual-mode/cli.d.ts.map +1 -1
- package/node_modules/@claude-flow/codex/dist/dual-mode/cli.js +20 -1
- package/node_modules/@claude-flow/codex/dist/dual-mode/cli.js.map +1 -1
- package/node_modules/@claude-flow/codex/dist/dual-mode/orchestrator.d.ts +4 -0
- package/node_modules/@claude-flow/codex/dist/dual-mode/orchestrator.d.ts.map +1 -1
- package/node_modules/@claude-flow/codex/dist/dual-mode/orchestrator.js +18 -2
- package/node_modules/@claude-flow/codex/dist/dual-mode/orchestrator.js.map +1 -1
- package/node_modules/@claude-flow/codex/dist/generators/skill-md.d.ts +11 -5
- package/node_modules/@claude-flow/codex/dist/generators/skill-md.d.ts.map +1 -1
- package/node_modules/@claude-flow/codex/dist/generators/skill-md.js +84 -849
- package/node_modules/@claude-flow/codex/dist/generators/skill-md.js.map +1 -1
- package/node_modules/@claude-flow/codex/dist/initializer.d.ts.map +1 -1
- package/node_modules/@claude-flow/codex/dist/initializer.js +9 -13
- package/node_modules/@claude-flow/codex/dist/initializer.js.map +1 -1
- package/package.json +1 -1
- package/plugins/ruflo-metaharness/scripts/_harness.mjs +5 -1
- package/plugins/ruflo-metaharness/scripts/_invoke.mjs +12 -13
- package/plugins/ruflo-metaharness/scripts/mcp-scan.mjs +7 -8
- package/plugins/ruflo-metaharness/scripts/smoke.sh +58 -2
- package/plugins/ruflo-metaharness/scripts/test-similarity.mjs +38 -0
- package/plugins/ruflo-metaharness/scripts/threat-model.mjs +4 -1
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"manifest": {
|
|
3
|
-
"version": "3.32.
|
|
3
|
+
"version": "3.32.36",
|
|
4
4
|
"files": {
|
|
5
5
|
"auto-memory-hook.mjs": "68be7e9a9eba7bf9c4e8a230db7bf61a243b965639f8504842799d6c6ca28762",
|
|
6
|
-
"hook-handler.cjs": "
|
|
7
|
-
"intelligence.cjs": "
|
|
8
|
-
"statusline.cjs": "
|
|
6
|
+
"hook-handler.cjs": "dae295fb9ae2626b89899c19a20cc911541af82b52d2eeb9b214d618b96e9a86",
|
|
7
|
+
"intelligence.cjs": "792c24a12704267e6b45ed0794e128131fb16e4174a43e65a5afe757bc0230af",
|
|
8
|
+
"statusline.cjs": "0457fe53f8cd2c56458ff178392536a5868efd1a573665fa43bc01d2d95ca677"
|
|
9
9
|
}
|
|
10
10
|
},
|
|
11
|
-
"signature": "
|
|
11
|
+
"signature": "qGxpst4ul6cizCQw1hjdJ5sBXj7uN7Fm1Dux2sKC7BwcPA66Nr9RzI8c8BJuKEUyPgaDtSvaY/gZTX2hWNqXBg==",
|
|
12
12
|
"algorithm": "ed25519"
|
|
13
13
|
}
|
|
@@ -286,6 +286,31 @@ async function readStdin() {
|
|
|
286
286
|
});
|
|
287
287
|
}
|
|
288
288
|
|
|
289
|
+
function claimSideEffectEvent(family, stdinData, event) {
|
|
290
|
+
if (/^(1|true|yes|on)$/i.test(process.env.RUFLO_DISABLE_HOOK_DEDUP || '')) return true;
|
|
291
|
+
try {
|
|
292
|
+
const crypto = require('crypto');
|
|
293
|
+
const eventId = event?.tool_use_id || event?.toolUseId ||
|
|
294
|
+
event?.session_id || event?.sessionId || event?.hook_event_id;
|
|
295
|
+
const payloadIdentity = eventId
|
|
296
|
+
? `event:${eventId}`
|
|
297
|
+
: `payload:${(stdinData || '').trim()}|bucket:${Math.floor(Date.now() / 2000)}`;
|
|
298
|
+
const projectRoot = process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
299
|
+
const digest = crypto.createHash('sha256')
|
|
300
|
+
.update(`ruflo-hook-dedup-v1\0${path.resolve(projectRoot)}\0${family}\0${payloadIdentity}`)
|
|
301
|
+
.digest('hex');
|
|
302
|
+
const dir = process.env.RUFLO_HOOK_DEDUP_DIR ||
|
|
303
|
+
path.join(os.tmpdir(), 'ruflo-hook-dedup-v1');
|
|
304
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
305
|
+
const fd = fs.openSync(path.join(dir, digest), 'wx', 0o600);
|
|
306
|
+
fs.writeFileSync(fd, String(Date.now()));
|
|
307
|
+
fs.closeSync(fd);
|
|
308
|
+
return true;
|
|
309
|
+
} catch (error) {
|
|
310
|
+
return error?.code === 'EEXIST' ? false : true;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
289
314
|
async function main() {
|
|
290
315
|
// Global safety timeout: hooks must NEVER hang (#1530, #1531)
|
|
291
316
|
const safetyTimer = setTimeout(() => {
|
|
@@ -302,6 +327,11 @@ async function main() {
|
|
|
302
327
|
try { hookInput = JSON.parse(stdinData); } catch (e) { /* ignore parse errors */ }
|
|
303
328
|
}
|
|
304
329
|
|
|
330
|
+
if ((command === 'post-edit' || command === 'session-end') &&
|
|
331
|
+
!claimSideEffectEvent(command, stdinData, hookInput)) {
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
|
|
305
335
|
// Normalize snake_case/camelCase: Claude Code sends tool_input/tool_name (snake_case)
|
|
306
336
|
const toolInput = hookInput.toolInput || hookInput.tool_input || {};
|
|
307
337
|
const toolName = hookInput.toolName || hookInput.tool_name || '';
|
|
@@ -17,12 +17,30 @@
|
|
|
17
17
|
const fs = require('fs');
|
|
18
18
|
const path = require('path');
|
|
19
19
|
|
|
20
|
-
|
|
20
|
+
function resolveProjectRoot(startDir) {
|
|
21
|
+
if (process.env.CLAUDE_PROJECT_DIR) {
|
|
22
|
+
return path.resolve(process.env.CLAUDE_PROJECT_DIR);
|
|
23
|
+
}
|
|
24
|
+
let dir = path.resolve(startDir || process.cwd());
|
|
25
|
+
while (true) {
|
|
26
|
+
if (fs.existsSync(path.join(dir, '.git')) ||
|
|
27
|
+
fs.existsSync(path.join(dir, '.claude-flow'))) {
|
|
28
|
+
return dir;
|
|
29
|
+
}
|
|
30
|
+
const parent = path.dirname(dir);
|
|
31
|
+
if (parent === dir) return path.resolve(startDir || process.cwd());
|
|
32
|
+
dir = parent;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
const PROJECT_ROOT = resolveProjectRoot(process.cwd());
|
|
37
|
+
const DATA_DIR = path.join(PROJECT_ROOT, '.claude-flow', 'data');
|
|
21
38
|
const STORE_PATH = path.join(DATA_DIR, 'auto-memory-store.json');
|
|
22
39
|
const GRAPH_PATH = path.join(DATA_DIR, 'graph-state.json');
|
|
23
40
|
const RANKED_PATH = path.join(DATA_DIR, 'ranked-context.json');
|
|
24
41
|
const PENDING_PATH = path.join(DATA_DIR, 'pending-insights.jsonl');
|
|
25
|
-
const
|
|
42
|
+
const LEGACY_PENDING_PATH = path.join(process.cwd(), '.claude-flow', 'data', 'pending-insights.jsonl');
|
|
43
|
+
const SESSION_DIR = path.join(PROJECT_ROOT, '.claude-flow', 'sessions');
|
|
26
44
|
const SESSION_FILE = path.join(SESSION_DIR, 'current.json');
|
|
27
45
|
|
|
28
46
|
// ββ Safety limits (fixes #1530, #1531) βββββββββββββββββββββββββββββββββββββ
|
|
@@ -47,6 +65,22 @@ const STOP_WORDS = new Set([
|
|
|
47
65
|
|
|
48
66
|
function ensureDataDir() {
|
|
49
67
|
if (!fs.existsSync(DATA_DIR)) fs.mkdirSync(DATA_DIR, { recursive: true });
|
|
68
|
+
// Recover signal written by older helpers under a subdirectory cwd. Keep
|
|
69
|
+
// the legacy file intact and append only lines not already present.
|
|
70
|
+
if (path.resolve(LEGACY_PENDING_PATH) !== path.resolve(PENDING_PATH) &&
|
|
71
|
+
fs.existsSync(LEGACY_PENDING_PATH)) {
|
|
72
|
+
try {
|
|
73
|
+
const existing = fs.existsSync(PENDING_PATH)
|
|
74
|
+
? new Set(fs.readFileSync(PENDING_PATH, 'utf-8').split('\n').filter(Boolean))
|
|
75
|
+
: new Set();
|
|
76
|
+
const recovered = fs.readFileSync(LEGACY_PENDING_PATH, 'utf-8')
|
|
77
|
+
.split('\n')
|
|
78
|
+
.filter(line => line && !existing.has(line));
|
|
79
|
+
if (recovered.length > 0) {
|
|
80
|
+
fs.appendFileSync(PENDING_PATH, recovered.join('\n') + '\n', 'utf-8');
|
|
81
|
+
}
|
|
82
|
+
} catch { /* migration is best-effort; never block hook execution */ }
|
|
83
|
+
}
|
|
50
84
|
}
|
|
51
85
|
|
|
52
86
|
function readJSON(filePath) {
|
|
@@ -300,7 +334,7 @@ function buildEdges(entries) {
|
|
|
300
334
|
*/
|
|
301
335
|
function bootstrapFromMemoryFiles() {
|
|
302
336
|
const entries = [];
|
|
303
|
-
const cwd =
|
|
337
|
+
const cwd = PROJECT_ROOT;
|
|
304
338
|
|
|
305
339
|
// Search for auto-memory directories
|
|
306
340
|
const candidates = [
|
|
@@ -1032,7 +1066,16 @@ function stats(outputJson) {
|
|
|
1032
1066
|
return report;
|
|
1033
1067
|
}
|
|
1034
1068
|
|
|
1035
|
-
module.exports = {
|
|
1069
|
+
module.exports = {
|
|
1070
|
+
init,
|
|
1071
|
+
getContext,
|
|
1072
|
+
recordEdit,
|
|
1073
|
+
feedback,
|
|
1074
|
+
consolidate,
|
|
1075
|
+
stats,
|
|
1076
|
+
resolveProjectRoot,
|
|
1077
|
+
projectRoot: PROJECT_ROOT,
|
|
1078
|
+
};
|
|
1036
1079
|
|
|
1037
1080
|
// ββ CLI entrypoint ββββββββββββββββββββββββββββββββββββββββββββββββββββββββββ
|
|
1038
1081
|
if (require.main === module) {
|
|
@@ -42,7 +42,11 @@ const CONFIG = {
|
|
|
42
42
|
hideCost: /^(1|true|yes|on)$/i.test(process.env.RUFLO_STATUSLINE_HIDE_COST || ''),
|
|
43
43
|
};
|
|
44
44
|
|
|
45
|
-
const CWD = process.cwd();
|
|
45
|
+
const CWD = process.env.CLAUDE_PROJECT_DIR || process.cwd();
|
|
46
|
+
// Replaced by statusline-generator with the package root of the CLI that
|
|
47
|
+
// installed this helper. This survives custom npm prefixes and bundled Node
|
|
48
|
+
// runtimes whose process.execPath belongs to a different tree (#2811).
|
|
49
|
+
const BAKED_INSTALL_ROOT = "";
|
|
46
50
|
|
|
47
51
|
// βββ Delegation cache βββββββββββββββββββββββββββββββββββββββββββ
|
|
48
52
|
// Cache the CLI JSON result so rapid prompt re-renders (Claude Code
|
|
@@ -404,6 +408,154 @@ function getLocalIntegration() {
|
|
|
404
408
|
return integration;
|
|
405
409
|
}
|
|
406
410
|
|
|
411
|
+
// βββ Security freshness overlay (ruvnet/ruflo#2776) ββββββββββββββ
|
|
412
|
+
// The shipped CLI producer (dist/src/funnel/local-signals.js getSecurityStatus)
|
|
413
|
+
// only ever emits PENDING / CLEAN / ISSUES β it captures `scannedAt` but never
|
|
414
|
+
// inspects it, so a year-old scan renders π‘ β forever and the renderer's
|
|
415
|
+
// STALE / IN_PROGRESS branches are unreachable. Worse, when CLI delegation
|
|
416
|
+
// fails, the stale-while-revalidate cache (readCache() below) keeps serving
|
|
417
|
+
// the pre-scan PENDING pill indefinitely, so a user who runs the advertised
|
|
418
|
+
// `ruflo security scan` sees no change β the pill freezes at "scan pending".
|
|
419
|
+
//
|
|
420
|
+
// This overlay recomputes the security block from disk on EVERY render (same
|
|
421
|
+
// pattern as adrs/agentdb/tests/hooks above), which:
|
|
422
|
+
// 1) Makes STALE reachable β when the newest scan is older than
|
|
423
|
+
// RUFLO_SCAN_STALE_HOURS (default 24h β matches the CVE feed refresh
|
|
424
|
+
// cadence), report STALE regardless of what the cached CLI JSON says.
|
|
425
|
+
// 2) Makes IN_PROGRESS reachable β when a `scan-in-progress` marker file
|
|
426
|
+
// exists and is younger than SECURITY_IN_PROGRESS_MAX_MIN (guards against
|
|
427
|
+
// a crashed scan leaving the marker behind).
|
|
428
|
+
// 3) Caps the "scan pending" display window β if PENDING has been shown for
|
|
429
|
+
// >RUFLO_SCAN_PENDING_CAP_MIN (default 30) without a completion write,
|
|
430
|
+
// switch to STALE and stop rendering the yellow indicator. The tracker
|
|
431
|
+
// lives in ~/.ruflo/statusline-scan-pending-since.json, keyed by CWD
|
|
432
|
+
// hash so multiple project checkouts don't collide.
|
|
433
|
+
// 4) Since this runs AFTER readCache() serves stale data, it bypasses the
|
|
434
|
+
// "pill freezes at PENDING" freeze in defect 2 β the overlay reads
|
|
435
|
+
// fresh disk state even when the CLI delegation is broken.
|
|
436
|
+
const SECURITY_STALE_HOURS = Math.max(1, parseInt(process.env.RUFLO_SCAN_STALE_HOURS || '24', 10) || 24);
|
|
437
|
+
const SECURITY_PENDING_CAP_MIN = Math.max(1, parseInt(process.env.RUFLO_SCAN_PENDING_CAP_MIN || '30', 10) || 30);
|
|
438
|
+
const SECURITY_IN_PROGRESS_MAX_MIN = 30; // marker older than this = crashed scan; treat as absent
|
|
439
|
+
const PENDING_TRACK_FILE = path.join(os.homedir(), '.ruflo', 'statusline-scan-pending-since.json');
|
|
440
|
+
const CWD_KEY = require('crypto').createHash('md5').update(CWD).digest('hex').slice(0, 12);
|
|
441
|
+
|
|
442
|
+
function readPendingSince() {
|
|
443
|
+
try {
|
|
444
|
+
if (!fs.existsSync(PENDING_TRACK_FILE)) return null;
|
|
445
|
+
const raw = JSON.parse(fs.readFileSync(PENDING_TRACK_FILE, 'utf-8'));
|
|
446
|
+
if (raw && typeof raw === 'object' && typeof raw[CWD_KEY] === 'number') return raw[CWD_KEY];
|
|
447
|
+
} catch { /* ignore */ }
|
|
448
|
+
return null;
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
function writePendingSince(ts) {
|
|
452
|
+
try {
|
|
453
|
+
let obj = {};
|
|
454
|
+
if (fs.existsSync(PENDING_TRACK_FILE)) {
|
|
455
|
+
try { obj = JSON.parse(fs.readFileSync(PENDING_TRACK_FILE, 'utf-8')) || {}; } catch { obj = {}; }
|
|
456
|
+
}
|
|
457
|
+
if (ts === null) { delete obj[CWD_KEY]; } else { obj[CWD_KEY] = ts; }
|
|
458
|
+
fs.mkdirSync(path.dirname(PENDING_TRACK_FILE), { recursive: true, mode: 0o700 });
|
|
459
|
+
fs.writeFileSync(PENDING_TRACK_FILE, JSON.stringify(obj), { encoding: 'utf-8', mode: 0o600 });
|
|
460
|
+
} catch { /* ignore */ }
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
function getLocalSecurity(cliSecurity) {
|
|
464
|
+
const base = (cliSecurity && typeof cliSecurity === 'object')
|
|
465
|
+
? Object.assign({}, cliSecurity)
|
|
466
|
+
: { status: 'NONE', findings: 0, cvesFixed: 0, totalCves: 0 };
|
|
467
|
+
base.findings = Math.max(0, base.findings || 0);
|
|
468
|
+
|
|
469
|
+
const scanDir = path.join(CWD, '.claude', 'security-scans');
|
|
470
|
+
|
|
471
|
+
// Detect a live in-progress marker (writer opts-in by writing this file).
|
|
472
|
+
let inProgress = false;
|
|
473
|
+
try {
|
|
474
|
+
const marker = path.join(scanDir, 'scan-in-progress');
|
|
475
|
+
if (fs.existsSync(marker)) {
|
|
476
|
+
const ageMin = (Date.now() - fs.statSync(marker).mtimeMs) / 60000;
|
|
477
|
+
if (ageMin < SECURITY_IN_PROGRESS_MAX_MIN) inProgress = true;
|
|
478
|
+
}
|
|
479
|
+
} catch { /* ignore */ }
|
|
480
|
+
|
|
481
|
+
// Find newest scan-*.json by mtime and read its findings/timestamp.
|
|
482
|
+
let newestPath = null;
|
|
483
|
+
let newestMtime = 0;
|
|
484
|
+
try {
|
|
485
|
+
if (fs.existsSync(scanDir)) {
|
|
486
|
+
for (const name of fs.readdirSync(scanDir)) {
|
|
487
|
+
if (!name.startsWith('scan-') || !name.endsWith('.json')) continue;
|
|
488
|
+
try {
|
|
489
|
+
const st = fs.statSync(path.join(scanDir, name));
|
|
490
|
+
if (st.mtimeMs > newestMtime) { newestMtime = st.mtimeMs; newestPath = path.join(scanDir, name); }
|
|
491
|
+
} catch { /* ignore */ }
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
} catch { /* ignore */ }
|
|
495
|
+
|
|
496
|
+
if (newestPath) {
|
|
497
|
+
// We have a scan on disk β the never-scanned pending tracker is no longer
|
|
498
|
+
// relevant. Clear it so a re-created directory can start a fresh window.
|
|
499
|
+
writePendingSince(null);
|
|
500
|
+
|
|
501
|
+
let scannedAtMs = newestMtime;
|
|
502
|
+
let findings = base.findings;
|
|
503
|
+
try {
|
|
504
|
+
const j = JSON.parse(fs.readFileSync(newestPath, 'utf-8'));
|
|
505
|
+
// scannedAt (CLI producer's field name) OR timestamp (writer's field name).
|
|
506
|
+
const isoStr = (j && (j.scannedAt || j.timestamp)) || null;
|
|
507
|
+
if (isoStr) {
|
|
508
|
+
const t = Date.parse(isoStr);
|
|
509
|
+
if (!isNaN(t)) scannedAtMs = t;
|
|
510
|
+
}
|
|
511
|
+
// findings may be a number, an array, or nested in summary.total.
|
|
512
|
+
if (j) {
|
|
513
|
+
if (typeof j.findings === 'number') findings = j.findings;
|
|
514
|
+
else if (Array.isArray(j.findings)) findings = j.findings.length;
|
|
515
|
+
else if (j.summary && typeof j.summary.total === 'number') findings = j.summary.total;
|
|
516
|
+
}
|
|
517
|
+
} catch { /* ignore parse β fall back to mtime + cached findings */ }
|
|
518
|
+
|
|
519
|
+
base.findings = Math.max(0, findings || 0);
|
|
520
|
+
base.scannedAt = new Date(scannedAtMs).toISOString();
|
|
521
|
+
|
|
522
|
+
const ageHours = (Date.now() - scannedAtMs) / 3600000;
|
|
523
|
+
if (ageHours >= SECURITY_STALE_HOURS) {
|
|
524
|
+
// Stale but findings still render red (a year-old ISSUES scan is still bad).
|
|
525
|
+
base.status = 'STALE';
|
|
526
|
+
} else if (inProgress) {
|
|
527
|
+
base.status = 'IN_PROGRESS';
|
|
528
|
+
} else if (base.findings > 0) {
|
|
529
|
+
base.status = 'ISSUES';
|
|
530
|
+
} else {
|
|
531
|
+
base.status = 'CLEAN';
|
|
532
|
+
}
|
|
533
|
+
return base;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
// No scan file. If a live marker exists, we're mid-scan.
|
|
537
|
+
if (inProgress) {
|
|
538
|
+
base.status = 'IN_PROGRESS';
|
|
539
|
+
// Reset the pending tracker so, if the scan crashes mid-flight, the next
|
|
540
|
+
// render starts a fresh N-minute pending window instead of an already-expired one.
|
|
541
|
+
writePendingSince(null);
|
|
542
|
+
return base;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
// Truly never-scanned: track how long we've shown PENDING. After the cap,
|
|
546
|
+
// escalate to STALE with the dim/gray glyph so the pill visibly stops
|
|
547
|
+
// shouting for attention β the user has either ignored it for 30 min or
|
|
548
|
+
// the scan is silently failing to write.
|
|
549
|
+
let pendingSince = readPendingSince();
|
|
550
|
+
if (pendingSince === null || typeof pendingSince !== 'number') {
|
|
551
|
+
pendingSince = Date.now();
|
|
552
|
+
writePendingSince(pendingSince);
|
|
553
|
+
}
|
|
554
|
+
const pendingAgeMin = (Date.now() - pendingSince) / 60000;
|
|
555
|
+
base.status = (pendingAgeMin >= SECURITY_PENDING_CAP_MIN) ? 'STALE' : 'PENDING';
|
|
556
|
+
return base;
|
|
557
|
+
}
|
|
558
|
+
|
|
407
559
|
// Overlay every locally-derived block onto the CLI data (mutates in place).
|
|
408
560
|
function applyLocalOverlays(data) {
|
|
409
561
|
data.adrs = getLocalADRCount();
|
|
@@ -411,6 +563,9 @@ function applyLocalOverlays(data) {
|
|
|
411
563
|
data.tests = getLocalTests();
|
|
412
564
|
data.hooks = getLocalHooks();
|
|
413
565
|
data.integration = getLocalIntegration();
|
|
566
|
+
// Security overlay: recompute freshness from disk on every render so cached
|
|
567
|
+
// CLI JSON can never freeze the pill at PENDING. See getLocalSecurity() above.
|
|
568
|
+
data.security = getLocalSecurity(data.security);
|
|
414
569
|
return data;
|
|
415
570
|
}
|
|
416
571
|
|
|
@@ -685,6 +840,7 @@ function getPkgVersion() {
|
|
|
685
840
|
try {
|
|
686
841
|
const home = os.homedir();
|
|
687
842
|
const pkgPaths = [
|
|
843
|
+
...(BAKED_INSTALL_ROOT ? [path.join(BAKED_INSTALL_ROOT, 'package.json')] : []),
|
|
688
844
|
path.join(home, '.claude', 'plugins', 'marketplaces', 'ruflo', 'package.json'),
|
|
689
845
|
path.join(CWD, 'node_modules', '@claude-flow', 'cli', 'package.json'),
|
|
690
846
|
path.join(CWD, 'node_modules', 'ruflo', 'package.json'),
|
|
@@ -712,7 +868,12 @@ function getPkgVersion() {
|
|
|
712
868
|
// #2221 follow-up: a custom npm prefix (e.g. ~/.npm-global) is decoupled from
|
|
713
869
|
// the node binary location, so the binDir-derived probes above all miss. Also
|
|
714
870
|
// probe the npm prefix from the environment and the common ~/.npm-global default.
|
|
715
|
-
for (const prefix of [
|
|
871
|
+
for (const prefix of [
|
|
872
|
+
process.env.npm_config_prefix,
|
|
873
|
+
process.env.PREFIX,
|
|
874
|
+
path.join(home, '.local'),
|
|
875
|
+
path.join(home, '.npm-global'),
|
|
876
|
+
]) {
|
|
716
877
|
if (prefix) globalModuleDirs.push(path.join(prefix, 'lib', 'node_modules'));
|
|
717
878
|
}
|
|
718
879
|
for (const gm of globalModuleDirs) {
|
|
@@ -731,14 +892,12 @@ function getPkgVersion() {
|
|
|
731
892
|
// right after a publish). Taking the first EXISTING candidate meant the
|
|
732
893
|
// header could show a stale version even when a newer install (e.g.
|
|
733
894
|
// node_modules/@claude-flow/cli from a plain npm install) was sitting right there.
|
|
734
|
-
let found = false;
|
|
735
895
|
for (const p of pkgPaths) {
|
|
736
896
|
if (!fs.existsSync(p)) continue;
|
|
737
897
|
try {
|
|
738
898
|
const pkg = JSON.parse(fs.readFileSync(p, 'utf-8'));
|
|
739
899
|
if (pkg && typeof pkg.version === 'string' && pkg.version.length > 0) {
|
|
740
|
-
if (
|
|
741
|
-
found = true;
|
|
900
|
+
if (compareVersions(pkg.version, ver) > 0) ver = pkg.version;
|
|
742
901
|
}
|
|
743
902
|
} catch { /* ignore */ }
|
|
744
903
|
}
|
|
@@ -851,10 +1010,14 @@ function generateStatusline() {
|
|
|
851
1010
|
if (healthAllGreen) {
|
|
852
1011
|
opsParts.push(c.brightGreen + 'π‘ β' + c.reset);
|
|
853
1012
|
} else {
|
|
1013
|
+
// #2776: STALE gets dim/gray (distinct from the actionable yellow of
|
|
1014
|
+
// PENDING/IN_PROGRESS) so a stale pill visibly stops shouting for
|
|
1015
|
+
// attention β the user can act on the "run ruflo security scan" prompt or
|
|
1016
|
+
// ignore it without a permanently-yellow indicator.
|
|
854
1017
|
if (secStatus === 'PENDING') opsParts.push(c.brightYellow + 'π‘ scan pending' + c.reset);
|
|
855
1018
|
else if (secStatus === 'IN_PROGRESS') opsParts.push(c.brightYellow + 'π‘ scanningβ¦' + c.reset);
|
|
856
1019
|
else if (secStatus === 'ISSUES') opsParts.push(c.brightRed + 'π‘ findings' + c.reset);
|
|
857
|
-
else if (secStatus === 'STALE') opsParts.push(c.
|
|
1020
|
+
else if (secStatus === 'STALE') opsParts.push(c.dim + 'π‘ scan stale' + c.reset);
|
|
858
1021
|
else if (secStatus !== 'NONE' && secStatus !== 'CLEAN') opsParts.push(c.brightRed + 'π‘ ' + secStatus.toLowerCase() + c.reset);
|
|
859
1022
|
if (findings > 0) {
|
|
860
1023
|
opsParts.push(c.brightRed + 'β ' + findings + ' finding' + (findings === 1 ? '' : 's') + c.reset);
|
package/bin/cli.js
CHANGED
|
@@ -34,6 +34,30 @@ import { dirname, join } from 'path';
|
|
|
34
34
|
const _origWarn = console.warn;
|
|
35
35
|
const _origLog = console.log;
|
|
36
36
|
const _origError = console.error;
|
|
37
|
+
|
|
38
|
+
// #2726 β Some MCP clients place every advertised schema in every model
|
|
39
|
+
// request. Keep "all" as the backwards-compatible default, but honor the
|
|
40
|
+
// same category/namespace/exact-name selector as `mcp start --tools`.
|
|
41
|
+
function _filterAdvertisedMcpTools(tools) {
|
|
42
|
+
const argv = process.argv.slice(2);
|
|
43
|
+
const toolsIndex = argv.findIndex((arg) => arg === '--tools');
|
|
44
|
+
const inlineTools = argv.find((arg) => arg.startsWith('--tools='));
|
|
45
|
+
const configured = process.env.CLAUDE_FLOW_MCP_TOOLS
|
|
46
|
+
|| (toolsIndex >= 0 ? argv[toolsIndex + 1] : undefined)
|
|
47
|
+
|| (inlineTools ? inlineTools.slice('--tools='.length) : undefined);
|
|
48
|
+
if (!configured || configured.trim().toLowerCase() === 'all') return tools;
|
|
49
|
+
const selectors = new Set(
|
|
50
|
+
configured.split(',').map((item) => item.trim().toLowerCase()).filter(Boolean),
|
|
51
|
+
);
|
|
52
|
+
if (selectors.size === 0) return tools;
|
|
53
|
+
return tools.filter((tool) => {
|
|
54
|
+
const name = tool.name.toLowerCase();
|
|
55
|
+
const category = typeof tool.category === 'string' ? tool.category.toLowerCase() : '';
|
|
56
|
+
return selectors.has(name)
|
|
57
|
+
|| selectors.has(category)
|
|
58
|
+
|| Array.from(selectors).some((selector) => name.startsWith(`${selector}_`));
|
|
59
|
+
});
|
|
60
|
+
}
|
|
37
61
|
const _isCosmeticAgentdbPatchNoise = (msg) =>
|
|
38
62
|
msg.includes('[AgentDB Patch]') && msg.includes('Controller index not found');
|
|
39
63
|
const _STDERR_REDIRECT_PREFIXES = [
|
|
@@ -237,7 +261,7 @@ if (isMCPMode) {
|
|
|
237
261
|
};
|
|
238
262
|
|
|
239
263
|
case 'tools/list': {
|
|
240
|
-
const tools = listMCPTools();
|
|
264
|
+
const tools = _filterAdvertisedMcpTools(listMCPTools());
|
|
241
265
|
return {
|
|
242
266
|
jsonrpc: '2.0',
|
|
243
267
|
id: message.id,
|
package/catalog-manifest.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"generation":
|
|
4
|
-
"generatedAt": "2026-07-
|
|
5
|
-
"gitSha": "
|
|
3
|
+
"generation": 4,
|
|
4
|
+
"generatedAt": "2026-07-29T19:08:51.000Z",
|
|
5
|
+
"gitSha": "67ff9898",
|
|
6
6
|
"catalog": {
|
|
7
7
|
"agents": 164,
|
|
8
|
-
"tools":
|
|
8
|
+
"tools": 397,
|
|
9
9
|
"skills": 34
|
|
10
10
|
},
|
|
11
11
|
"benchmark": null
|
|
@@ -230,7 +230,7 @@ const listCommand = {
|
|
|
230
230
|
}
|
|
231
231
|
// Format for display
|
|
232
232
|
const displayAgents = result.agents.map(agent => ({
|
|
233
|
-
id: agent.
|
|
233
|
+
id: agent.agentId,
|
|
234
234
|
type: agent.agentType,
|
|
235
235
|
status: agent.status,
|
|
236
236
|
created: new Date(agent.createdAt).toLocaleTimeString(),
|
|
@@ -487,6 +487,39 @@ const metricsCommand = {
|
|
|
487
487
|
const avgSuccessRate = tasksCompleted > 0
|
|
488
488
|
? `${Math.round(Object.values(typeCounts).reduce((a, d) => a + d.success, 0) / tasksCompleted * 100)}%`
|
|
489
489
|
: 'N/A';
|
|
490
|
+
// ADR-330: expose the learned scheduling signal where operators already
|
|
491
|
+
// inspect agent metrics. Old/non-APSC installs simply omit this section.
|
|
492
|
+
let pheromone;
|
|
493
|
+
const swarmStatePath = join(process.cwd(), '.claude-flow', 'swarm', 'swarm-state.json');
|
|
494
|
+
if (existsSync(swarmStatePath)) {
|
|
495
|
+
try {
|
|
496
|
+
const store = JSON.parse(readFileSync(swarmStatePath, 'utf-8'));
|
|
497
|
+
const current = Object.values(store.swarms ?? {})
|
|
498
|
+
.filter((swarm) => swarm.status === 'running' && swarm.topology === 'pheromone-adaptive')
|
|
499
|
+
.sort((a, b) => String(b.updatedAt).localeCompare(String(a.updatedAt)))[0];
|
|
500
|
+
const apsc = current?.config?.apscState;
|
|
501
|
+
if (apsc) {
|
|
502
|
+
const agents = Object.values(apsc.agents ?? {})
|
|
503
|
+
.filter((agent) => !agentId || agent.agentId === agentId)
|
|
504
|
+
.sort((a, b) => a.agentId.localeCompare(b.agentId))
|
|
505
|
+
.map((agent) => ({
|
|
506
|
+
agentId: agent.agentId,
|
|
507
|
+
role: agent.role,
|
|
508
|
+
pheromoneScore: agent.emaScore,
|
|
509
|
+
rawScore: agent.rawScore,
|
|
510
|
+
samples: agent.samples,
|
|
511
|
+
eligibility: agent.status === 'suspended' ? 'suspended' : 'eligible',
|
|
512
|
+
}));
|
|
513
|
+
pheromone = {
|
|
514
|
+
active: true,
|
|
515
|
+
dryRun: apsc.config?.dryRun !== false,
|
|
516
|
+
threshold: apsc.threshold ?? 0.5,
|
|
517
|
+
agents,
|
|
518
|
+
};
|
|
519
|
+
}
|
|
520
|
+
}
|
|
521
|
+
catch { /* malformed/legacy swarm state β preserve existing metrics */ }
|
|
522
|
+
}
|
|
490
523
|
const metrics = {
|
|
491
524
|
period,
|
|
492
525
|
summary: {
|
|
@@ -501,7 +534,8 @@ const metricsCommand = {
|
|
|
501
534
|
performance: {
|
|
502
535
|
memoryVectors: `${vectorCount} vectors`,
|
|
503
536
|
searchBackend: vectorCount > 0 ? 'HNSW-indexed' : 'none'
|
|
504
|
-
}
|
|
537
|
+
},
|
|
538
|
+
pheromone,
|
|
505
539
|
};
|
|
506
540
|
if (ctx.flags.format === 'json') {
|
|
507
541
|
output.printJson(metrics);
|
|
@@ -544,6 +578,20 @@ const metricsCommand = {
|
|
|
544
578
|
`Vectors: ${output.success(metrics.performance.memoryVectors)}`,
|
|
545
579
|
`Backend: ${output.success(metrics.performance.searchBackend)}`
|
|
546
580
|
]);
|
|
581
|
+
if (metrics.pheromone) {
|
|
582
|
+
output.writeln();
|
|
583
|
+
output.writeln(output.bold(`Pheromone Scheduling (${metrics.pheromone.dryRun ? 'dry run' : 'live'})`));
|
|
584
|
+
output.printTable({
|
|
585
|
+
columns: [
|
|
586
|
+
{ key: 'agentId', header: 'Agent', width: 20 },
|
|
587
|
+
{ key: 'role', header: 'Role', width: 18 },
|
|
588
|
+
{ key: 'pheromoneScore', header: 'EMA Score', width: 12, align: 'right' },
|
|
589
|
+
{ key: 'samples', header: 'Samples', width: 9, align: 'right' },
|
|
590
|
+
{ key: 'eligibility', header: 'Eligibility', width: 12 },
|
|
591
|
+
],
|
|
592
|
+
data: metrics.pheromone.agents,
|
|
593
|
+
});
|
|
594
|
+
}
|
|
547
595
|
return { success: true, data: metrics };
|
|
548
596
|
}
|
|
549
597
|
};
|