claude-flow 3.56.9 → 3.57.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/.proven-config-version +1 -0
- package/.claude/helpers/intelligence.cjs +51 -9
- package/.claude/proven-config.json +42 -0
- package/.claude/skills/verification-quality/SKILL.md +14 -11
- package/README.md +1 -1
- package/node_modules/@claude-flow/codex/dist/cli.js +0 -0
- package/node_modules/@claude-flow/plugin-agent-federation/dist/bin.js +0 -0
- package/node_modules/@claude-flow/security/dist/policy/engine.d.ts +17 -1
- package/node_modules/@claude-flow/security/dist/policy/engine.d.ts.map +1 -1
- package/node_modules/@claude-flow/security/dist/policy/engine.js +99 -24
- package/node_modules/@claude-flow/security/dist/policy/engine.js.map +1 -1
- package/node_modules/@claude-flow/security/dist/policy/types.d.ts +14 -1
- package/node_modules/@claude-flow/security/dist/policy/types.d.ts.map +1 -1
- package/node_modules/@claude-flow/security/dist/policy/types.js +3 -1
- package/node_modules/@claude-flow/security/dist/policy/types.js.map +1 -1
- package/package.json +2 -2
- package/v3/@claude-flow/cli/README.md +164 -62
- package/v3/@claude-flow/cli/catalog-manifest.json +5 -5
- package/v3/@claude-flow/cli/dist/src/autopilot-state.d.ts +2 -0
- package/v3/@claude-flow/cli/dist/src/autopilot-state.js +36 -8
- package/v3/@claude-flow/cli/dist/src/command-resolution.d.ts +15 -0
- package/v3/@claude-flow/cli/dist/src/command-resolution.js +25 -0
- package/v3/@claude-flow/cli/dist/src/commands/agent.js +17 -17
- package/v3/@claude-flow/cli/dist/src/commands/claims.js +6 -8
- package/v3/@claude-flow/cli/dist/src/commands/daemon.js +94 -33
- package/v3/@claude-flow/cli/dist/src/commands/doctor.js +95 -18
- package/v3/@claude-flow/cli/dist/src/commands/memory.js +14 -2
- package/v3/@claude-flow/cli/dist/src/commands/policy.js +6 -1
- package/v3/@claude-flow/cli/dist/src/commands/security-code-patterns.d.ts +2 -2
- package/v3/@claude-flow/cli/dist/src/commands/security-code-patterns.js +61 -3
- package/v3/@claude-flow/cli/dist/src/commands/security.d.ts +24 -0
- package/v3/@claude-flow/cli/dist/src/commands/security.js +81 -35
- package/v3/@claude-flow/cli/dist/src/commands/tui.d.ts +9 -0
- package/v3/@claude-flow/cli/dist/src/commands/tui.js +29 -3
- package/v3/@claude-flow/cli/dist/src/config/proven-config-refresh.d.ts +9 -0
- package/v3/@claude-flow/cli/dist/src/config/proven-config-refresh.js +24 -3
- package/v3/@claude-flow/cli/dist/src/funnel/local-signals.js +2 -1
- package/v3/@claude-flow/cli/dist/src/index.js +11 -47
- package/v3/@claude-flow/cli/dist/src/init/settings-generator.js +4 -58
- package/v3/@claude-flow/cli/dist/src/init/types.js +1 -1
- package/v3/@claude-flow/cli/dist/src/mcp-tools/agent-execute-core.d.ts +2 -0
- package/v3/@claude-flow/cli/dist/src/mcp-tools/agent-execute-core.js +15 -0
- package/v3/@claude-flow/cli/dist/src/mcp-tools/agent-health-metrics.d.ts +22 -0
- package/v3/@claude-flow/cli/dist/src/mcp-tools/agent-health-metrics.js +33 -0
- package/v3/@claude-flow/cli/dist/src/mcp-tools/agent-tools.js +45 -22
- package/v3/@claude-flow/cli/dist/src/mcp-tools/agentbbs-federation.d.ts +1 -1
- package/v3/@claude-flow/cli/dist/src/mcp-tools/agentbbs-federation.js +7 -2
- package/v3/@claude-flow/cli/dist/src/mcp-tools/agentdb-tools.js +25 -2
- package/v3/@claude-flow/cli/dist/src/mcp-tools/aidefence-known-misses.d.ts +22 -0
- package/v3/@claude-flow/cli/dist/src/mcp-tools/aidefence-known-misses.js +57 -0
- package/v3/@claude-flow/cli/dist/src/mcp-tools/analyze-tools.js +10 -11
- package/v3/@claude-flow/cli/dist/src/mcp-tools/autopilot-tools.js +2 -2
- package/v3/@claude-flow/cli/dist/src/mcp-tools/browser-intent-tools.js +6 -2
- package/v3/@claude-flow/cli/dist/src/mcp-tools/embeddings-tools.js +29 -7
- package/v3/@claude-flow/cli/dist/src/mcp-tools/hooks-tools.js +4 -0
- package/v3/@claude-flow/cli/dist/src/mcp-tools/memory-tools.d.ts +1 -1
- package/v3/@claude-flow/cli/dist/src/mcp-tools/memory-tools.js +100 -25
- package/v3/@claude-flow/cli/dist/src/mcp-tools/metaharness-tools.js +1 -1
- package/v3/@claude-flow/cli/dist/src/mcp-tools/neural-tools.js +48 -54
- package/v3/@claude-flow/cli/dist/src/mcp-tools/policy-tools.js +5 -3
- package/v3/@claude-flow/cli/dist/src/mcp-tools/ruvllm-tools.js +6 -0
- package/v3/@claude-flow/cli/dist/src/mcp-tools/security-tools.js +55 -17
- package/v3/@claude-flow/cli/dist/src/mcp-tools/workflow-tools.js +66 -17
- package/v3/@claude-flow/cli/dist/src/memory/intelligence.d.ts +5 -1
- package/v3/@claude-flow/cli/dist/src/memory/intelligence.js +158 -35
- package/v3/@claude-flow/cli/dist/src/memory/memory-bridge.js +10 -3
- package/v3/@claude-flow/cli/dist/src/memory/memory-initializer.js +2 -1
- package/v3/@claude-flow/cli/dist/src/memory/search-defaults.d.ts +54 -0
- package/v3/@claude-flow/cli/dist/src/memory/search-defaults.js +37 -0
- package/v3/@claude-flow/cli/dist/src/memory/sona-optimizer.d.ts +28 -0
- package/v3/@claude-flow/cli/dist/src/memory/sona-optimizer.js +59 -2
- package/v3/@claude-flow/cli/dist/src/memory/stats-file-lock.d.ts +7 -0
- package/v3/@claude-flow/cli/dist/src/memory/stats-file-lock.js +53 -0
- package/v3/@claude-flow/cli/dist/src/mods/command-registry/schema.d.ts +24 -24
- package/v3/@claude-flow/cli/dist/src/ruvector/agent-wasm.d.ts +10 -0
- package/v3/@claude-flow/cli/dist/src/ruvector/agent-wasm.js +27 -1
- package/v3/@claude-flow/cli/dist/src/ruvector/diff-classifier.d.ts +2 -4
- package/v3/@claude-flow/cli/dist/src/ruvector/diff-classifier.js +202 -69
- package/v3/@claude-flow/cli/dist/src/ruvector/ruvllm-wasm.d.ts +4 -0
- package/v3/@claude-flow/cli/dist/src/ruvector/ruvllm-wasm.js +12 -1
- package/v3/@claude-flow/cli/dist/src/services/distill-oracle.d.ts +1 -1
- package/v3/@claude-flow/cli/dist/src/services/distill-oracle.js +2 -2
- package/v3/@claude-flow/cli/dist/src/services/policy-anchor-log.d.ts +13 -0
- package/v3/@claude-flow/cli/dist/src/services/policy-anchor-log.js +242 -0
- package/v3/@claude-flow/cli/dist/src/services/policy-archive-scan.d.ts +7 -0
- package/v3/@claude-flow/cli/dist/src/services/policy-archive-scan.js +50 -0
- package/v3/@claude-flow/cli/dist/src/services/policy-ledger-anchor.d.ts +14 -8
- package/v3/@claude-flow/cli/dist/src/services/policy-ledger-anchor.js +356 -40
- package/v3/@claude-flow/cli/dist/src/services/policy-ledger-audit.d.ts +22 -0
- package/v3/@claude-flow/cli/dist/src/services/policy-ledger-audit.js +260 -0
- package/v3/@claude-flow/cli/dist/src/services/policy-ledger-record.d.ts +3 -0
- package/v3/@claude-flow/cli/dist/src/services/policy-ledger-record.js +22 -0
- package/v3/@claude-flow/cli/dist/src/services/policy-runtime.d.ts +2 -1
- package/v3/@claude-flow/cli/dist/src/services/policy-runtime.js +267 -40
- package/v3/@claude-flow/cli/dist/src/services/worker-daemon.d.ts +11 -0
- package/v3/@claude-flow/cli/dist/src/services/worker-daemon.js +37 -0
- package/v3/@claude-flow/cli/dist/src/transfer/store/discovery.js +16 -17
- package/v3/@claude-flow/cli/dist/src/transfer/store/types.d.ts +4 -0
- package/v3/@claude-flow/cli/package.json +5 -5
|
@@ -164,37 +164,44 @@ const startCommand = {
|
|
|
164
164
|
fs.mkdirSync(stateDir, { recursive: true });
|
|
165
165
|
}
|
|
166
166
|
catch { /* exists */ }
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
}
|
|
185
|
-
return { success: true };
|
|
167
|
+
lockFd = tryAcquireDaemonLock(lockFile);
|
|
168
|
+
if (lockFd === null) {
|
|
169
|
+
// Another `daemon start` is mid-spawn. Wait for it while its holder
|
|
170
|
+
// is alive; only a lock whose holder is DEAD is stale and may be
|
|
171
|
+
// retaken — atomically, via O_EXCL again, never by proceeding
|
|
172
|
+
// lockless. The old fixed 5s timeout + unlink let every waiter
|
|
173
|
+
// through on Windows, where the holder's killStaleDaemons
|
|
174
|
+
// (`tasklist /v`) routinely takes longer than that, and each waiter
|
|
175
|
+
// then forked its own daemon (3 per workspace observed).
|
|
176
|
+
const waitMs = Number(process.env.RUFLO_DAEMON_LOCK_WAIT_MS) > 0
|
|
177
|
+
? Number(process.env.RUFLO_DAEMON_LOCK_WAIT_MS)
|
|
178
|
+
: 60_000;
|
|
179
|
+
const deadline = Date.now() + waitMs;
|
|
180
|
+
while (lockFd === null) {
|
|
181
|
+
const winnerPid = getBackgroundDaemonPid(projectRoot);
|
|
182
|
+
if (winnerPid && isProcessRunning(winnerPid)) {
|
|
183
|
+
if (!quiet) {
|
|
184
|
+
output.printWarning(`Daemon already running in background (PID: ${winnerPid}). Stop it first with: daemon stop`);
|
|
186
185
|
}
|
|
186
|
+
return { success: true };
|
|
187
187
|
}
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
188
|
+
if (daemonLockIsStale(lockFile)) {
|
|
189
|
+
try {
|
|
190
|
+
fs.unlinkSync(lockFile);
|
|
191
|
+
}
|
|
192
|
+
catch { /* raced with another taker */ }
|
|
193
|
+
lockFd = tryAcquireDaemonLock(lockFile);
|
|
194
|
+
continue;
|
|
193
195
|
}
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
196
|
+
if (Date.now() >= deadline) {
|
|
197
|
+
// A live process is still starting the daemon. Spawning another
|
|
198
|
+
// would be the duplicate this lock exists to prevent.
|
|
199
|
+
if (!quiet) {
|
|
200
|
+
output.printWarning('Another `daemon start` is still in progress for this workspace; not starting a second daemon.');
|
|
201
|
+
}
|
|
202
|
+
return { success: true };
|
|
203
|
+
}
|
|
204
|
+
await new Promise((r) => setTimeout(r, 100));
|
|
198
205
|
}
|
|
199
206
|
}
|
|
200
207
|
// Dedup check while holding the lock.
|
|
@@ -298,15 +305,22 @@ const startCommand = {
|
|
|
298
305
|
// NOTE: Do NOT write PID file here — startDaemon() writes it internally.
|
|
299
306
|
// Writing it before startDaemon() causes checkExistingDaemon() to detect
|
|
300
307
|
// our own PID and return early, leaving no workers scheduled (#1478 Bug 1).
|
|
301
|
-
// Clean up PID file on exit
|
|
308
|
+
// Clean up PID file on exit — only if it is OURS. A process that lost
|
|
309
|
+
// the singleton race must never delete the winner's PID file, or the
|
|
310
|
+
// next auto-start sees "no daemon" and forks yet another one.
|
|
302
311
|
const cleanup = () => {
|
|
303
312
|
try {
|
|
304
|
-
if (fs.existsSync(pidFile)) {
|
|
313
|
+
if (fs.existsSync(pidFile) && fs.readFileSync(pidFile, 'utf-8').trim() === String(process.pid)) {
|
|
305
314
|
fs.unlinkSync(pidFile);
|
|
306
315
|
}
|
|
307
316
|
}
|
|
308
317
|
catch { /* ignore */ }
|
|
309
318
|
};
|
|
319
|
+
// WorkerDaemon.start() declines (returns without running) when another
|
|
320
|
+
// live daemon already owns daemon.pid. Such a process must exit rather
|
|
321
|
+
// than park on the keep-alive below: it has no workers and no TTL, so
|
|
322
|
+
// it would linger as an idle duplicate forever.
|
|
323
|
+
const declined = (d) => !d.getStatus().running;
|
|
310
324
|
process.on('exit', cleanup);
|
|
311
325
|
process.on('SIGINT', () => { cleanup(); process.exit(0); });
|
|
312
326
|
process.on('SIGTERM', () => { cleanup(); process.exit(0); });
|
|
@@ -319,6 +333,11 @@ const startCommand = {
|
|
|
319
333
|
spinner.start();
|
|
320
334
|
const daemon = await startDaemon(projectRoot, config);
|
|
321
335
|
const status = daemon.getStatus();
|
|
336
|
+
if (declined(daemon)) {
|
|
337
|
+
spinner.stop();
|
|
338
|
+
output.printWarning(`Daemon already running (PID: ${getBackgroundDaemonPid(projectRoot) ?? 'unknown'}); not starting a second one.`);
|
|
339
|
+
return { success: true };
|
|
340
|
+
}
|
|
322
341
|
spinner.succeed('Worker daemon started (foreground mode)');
|
|
323
342
|
output.writeln();
|
|
324
343
|
output.printBox([
|
|
@@ -371,7 +390,9 @@ const startCommand = {
|
|
|
371
390
|
await new Promise(() => { }); // Never resolves - daemon runs until killed
|
|
372
391
|
}
|
|
373
392
|
else {
|
|
374
|
-
await startDaemon(projectRoot, config);
|
|
393
|
+
const daemon = await startDaemon(projectRoot, config);
|
|
394
|
+
if (declined(daemon))
|
|
395
|
+
return { success: true };
|
|
375
396
|
setInterval(() => { }, 60_000); // Keep alive with ref'd handle (#1478)
|
|
376
397
|
await new Promise(() => { }); // Keep alive
|
|
377
398
|
}
|
|
@@ -910,6 +931,40 @@ function getBackgroundDaemonPid(projectRoot) {
|
|
|
910
931
|
return null;
|
|
911
932
|
}
|
|
912
933
|
}
|
|
934
|
+
/**
|
|
935
|
+
* Atomically create the launcher lock and stamp it with our PID.
|
|
936
|
+
* Returns the fd, or null when another process holds the lock.
|
|
937
|
+
*/
|
|
938
|
+
function tryAcquireDaemonLock(lockFile) {
|
|
939
|
+
try {
|
|
940
|
+
const fd = fs.openSync(lockFile, fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_WRONLY);
|
|
941
|
+
fs.writeSync(fd, String(process.pid));
|
|
942
|
+
return fd;
|
|
943
|
+
}
|
|
944
|
+
catch (e) {
|
|
945
|
+
if (e.code === 'EEXIST')
|
|
946
|
+
return null;
|
|
947
|
+
throw e;
|
|
948
|
+
}
|
|
949
|
+
}
|
|
950
|
+
/**
|
|
951
|
+
* A lock is stale only when its holder is gone. An empty lock is the brief
|
|
952
|
+
* window between O_EXCL create and the PID write, so it counts as held
|
|
953
|
+
* unless it has been empty for a while (holder crashed in that window).
|
|
954
|
+
*/
|
|
955
|
+
function daemonLockIsStale(lockFile) {
|
|
956
|
+
try {
|
|
957
|
+
const raw = fs.readFileSync(lockFile, 'utf-8').trim();
|
|
958
|
+
const pid = parseInt(raw, 10);
|
|
959
|
+
if (!raw || Number.isNaN(pid)) {
|
|
960
|
+
return Date.now() - fs.statSync(lockFile).mtimeMs > 10_000;
|
|
961
|
+
}
|
|
962
|
+
return pid !== process.pid && !isProcessRunning(pid);
|
|
963
|
+
}
|
|
964
|
+
catch {
|
|
965
|
+
return false; // vanished: the next O_EXCL attempt decides
|
|
966
|
+
}
|
|
967
|
+
}
|
|
913
968
|
/**
|
|
914
969
|
* Check if a process is running
|
|
915
970
|
*/
|
|
@@ -1192,12 +1247,18 @@ const statusCommand = {
|
|
|
1192
1247
|
// its real consent state into daemon-state.json — prefer that when it
|
|
1193
1248
|
// is the one running.
|
|
1194
1249
|
let aiWorkersEnabled = status.config.aiWorkersEnabled;
|
|
1250
|
+
// #4238: same for the TTL — a daemon started with `--ttl 0` must not be
|
|
1251
|
+
// reported with this process's 12h default.
|
|
1252
|
+
let ttlMs = status.config.ttlMs;
|
|
1195
1253
|
if (bgRunning) {
|
|
1196
1254
|
try {
|
|
1197
1255
|
const st = JSON.parse(fs.readFileSync(join(projectRoot, '.claude-flow', 'daemon-state.json'), 'utf-8'));
|
|
1198
1256
|
if (typeof st?.config?.aiWorkersEnabled === 'boolean') {
|
|
1199
1257
|
aiWorkersEnabled = st.config.aiWorkersEnabled;
|
|
1200
1258
|
}
|
|
1259
|
+
if (typeof st?.config?.ttlMs === 'number' && Number.isFinite(st.config.ttlMs)) {
|
|
1260
|
+
ttlMs = st.config.ttlMs;
|
|
1261
|
+
}
|
|
1201
1262
|
}
|
|
1202
1263
|
catch { /* no/partial state — fall back to in-process config */ }
|
|
1203
1264
|
}
|
|
@@ -1210,8 +1271,8 @@ const statusCommand = {
|
|
|
1210
1271
|
`Status: ${statusIcon} ${statusText}${mode}`,
|
|
1211
1272
|
`PID: ${displayPid}`,
|
|
1212
1273
|
status.startedAt ? `Started: ${status.startedAt.toISOString()}` : '',
|
|
1213
|
-
|
|
1214
|
-
? `TTL: ${Math.round(
|
|
1274
|
+
ttlMs > 0
|
|
1275
|
+
? `TTL: ${Math.round(ttlMs / 3600000)}h (self-shutdown)`
|
|
1215
1276
|
: `TTL: ${output.dim('off (runs until stopped)')}`,
|
|
1216
1277
|
// #2661: surface the AI-consent gate so "why is audit local-only?"
|
|
1217
1278
|
// is answerable from `daemon status` alone.
|
|
@@ -2515,6 +2515,12 @@ export const doctorCommand = {
|
|
|
2515
2515
|
name: 'doctor',
|
|
2516
2516
|
description: 'System diagnostics and health checks',
|
|
2517
2517
|
options: [
|
|
2518
|
+
{
|
|
2519
|
+
name: 'json',
|
|
2520
|
+
description: 'Emit versioned JSON health checks (cannot be combined with repair flags)',
|
|
2521
|
+
type: 'boolean',
|
|
2522
|
+
default: false
|
|
2523
|
+
},
|
|
2518
2524
|
{
|
|
2519
2525
|
name: 'fix',
|
|
2520
2526
|
short: 'f',
|
|
@@ -2561,6 +2567,7 @@ export const doctorCommand = {
|
|
|
2561
2567
|
}
|
|
2562
2568
|
],
|
|
2563
2569
|
examples: [
|
|
2570
|
+
{ command: 'claude-flow doctor --json -c node', description: 'Emit machine-readable health checks' },
|
|
2564
2571
|
{ command: 'claude-flow doctor', description: 'Run full health check' },
|
|
2565
2572
|
{ command: 'claude-flow doctor --fix', description: 'Print suggested fix commands (does not auto-apply)' },
|
|
2566
2573
|
{ command: 'claude-flow doctor --install', description: 'Auto-install missing dependencies' },
|
|
@@ -2575,6 +2582,21 @@ export const doctorCommand = {
|
|
|
2575
2582
|
const verbose = ctx.flags.verbose;
|
|
2576
2583
|
// Parser camelCases kebab-case flag names — read via `fixHandles`, not `['fix-handles']`.
|
|
2577
2584
|
const fixHandles = ctx.flags.fixHandles;
|
|
2585
|
+
const json = ctx.flags.json;
|
|
2586
|
+
const reportError = (message) => {
|
|
2587
|
+
if (json)
|
|
2588
|
+
output.writeln(JSON.stringify({ schemaVersion: 1, checks: [], error: message }));
|
|
2589
|
+
else
|
|
2590
|
+
output.printError(message);
|
|
2591
|
+
return { success: false, exitCode: 1, message };
|
|
2592
|
+
};
|
|
2593
|
+
if (json && (showFix || autoInstall || fixHandles)) {
|
|
2594
|
+
return reportError('--json cannot be combined with --fix, --install, or --fix-handles');
|
|
2595
|
+
}
|
|
2596
|
+
// Reuse the same callable for anonymous checks so their IDs do not depend
|
|
2597
|
+
// on display names or on whether the full or component suite was selected.
|
|
2598
|
+
const helperCheck = () => checkHelperIntegrity();
|
|
2599
|
+
const nativeAddonCheck = () => checkNativeAddonHeaders();
|
|
2578
2600
|
// #2677: `--component memory` now runs the whole memory-health suite,
|
|
2579
2601
|
// not just the existence check. Values can be a single check or an
|
|
2580
2602
|
// array — expanded at execution time. Stuinfla's report showed the
|
|
@@ -2592,7 +2614,7 @@ export const doctorCommand = {
|
|
|
2592
2614
|
'browser': checkAgentBrowserVersion,
|
|
2593
2615
|
'config': checkConfigFile,
|
|
2594
2616
|
'stale-settings': checkStaleSettingsNpx, // #2448
|
|
2595
|
-
'helpers':
|
|
2617
|
+
'helpers': helperCheck, // #3565
|
|
2596
2618
|
'daemon': checkDaemonStatus,
|
|
2597
2619
|
get memory() {
|
|
2598
2620
|
return [
|
|
@@ -2608,7 +2630,7 @@ export const doctorCommand = {
|
|
|
2608
2630
|
];
|
|
2609
2631
|
},
|
|
2610
2632
|
'memory-package': checkMemoryPackageVersion, // #3392
|
|
2611
|
-
'native-addon':
|
|
2633
|
+
'native-addon': nativeAddonCheck, // #4055
|
|
2612
2634
|
'learning': checkLearningBridge, // #2545
|
|
2613
2635
|
'learning-bridge': checkLearningBridge, // #2545
|
|
2614
2636
|
'api': checkApiKeys,
|
|
@@ -2634,8 +2656,7 @@ export const doctorCommand = {
|
|
|
2634
2656
|
};
|
|
2635
2657
|
if (component !== undefined && !Object.hasOwn(componentMap, component)) {
|
|
2636
2658
|
const message = `Unknown doctor component: ${JSON.stringify(component)}. Valid components: ${Object.keys(componentMap).join(', ')}`;
|
|
2637
|
-
|
|
2638
|
-
return { success: false, exitCode: 1, message };
|
|
2659
|
+
return reportError(message);
|
|
2639
2660
|
}
|
|
2640
2661
|
// Early-return short-circuit: `--fix-handles` is a targeted mitigation, not
|
|
2641
2662
|
// part of the health-check flow. Runs, reports, exits.
|
|
@@ -2699,12 +2720,15 @@ export const doctorCommand = {
|
|
|
2699
2720
|
output.writeln(output.dim('Upstream tracking: https://github.com/anthropics/claude-code/issues/67888'));
|
|
2700
2721
|
return { success: true };
|
|
2701
2722
|
}
|
|
2702
|
-
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2723
|
+
if (!json) {
|
|
2724
|
+
output.writeln();
|
|
2725
|
+
output.writeln(output.bold('RuFlo Doctor'));
|
|
2726
|
+
output.writeln(output.dim('System diagnostics and health check'));
|
|
2727
|
+
output.writeln(output.dim('─'.repeat(50)));
|
|
2728
|
+
output.writeln();
|
|
2729
|
+
}
|
|
2730
|
+
const nativeAgentDbPaths = await existingNativeAgentDbPaths();
|
|
2731
|
+
const nativeAgentDbChecks = nativeAgentDbPaths
|
|
2708
2732
|
.map((dbPath) => () => checkNativeAgentDbStructuralIntegrity(dbPath));
|
|
2709
2733
|
const allChecks = [
|
|
2710
2734
|
checkVersionFreshness,
|
|
@@ -2716,7 +2740,7 @@ export const doctorCommand = {
|
|
|
2716
2740
|
checkGitRepo,
|
|
2717
2741
|
checkConfigFile,
|
|
2718
2742
|
checkStaleSettingsNpx, // #2448/#2677 — runaway `npx @latest` in settings
|
|
2719
|
-
|
|
2743
|
+
helperCheck, // #3565 — installed signed helpers vs manifest
|
|
2720
2744
|
checkDaemonStatus,
|
|
2721
2745
|
checkMemoryDatabase,
|
|
2722
2746
|
checkMemoryStructuralIntegrity, // #2737 — bounded, native quick_check on every default run
|
|
@@ -2724,7 +2748,7 @@ export const doctorCommand = {
|
|
|
2724
2748
|
checkMemoryPersistenceDriver, // #2968/#3321 — read-only native capability probe
|
|
2725
2749
|
checkLearningBridge, // #2545 — can the auto-memory hook actually load @claude-flow/memory?
|
|
2726
2750
|
checkMemoryPackageVersion, // #3392 — loaded @claude-flow/memory must satisfy the CLI's declared range
|
|
2727
|
-
|
|
2751
|
+
nativeAddonCheck, // #4055 — better-sqlite3 built against Node 24.19+ headers on 24.19-24.21
|
|
2728
2752
|
checkApiKeys,
|
|
2729
2753
|
checkMcpServers,
|
|
2730
2754
|
checkMcpSchemaOverhead, // #2726 — fixed tools/list prompt cost
|
|
@@ -2747,21 +2771,59 @@ export const doctorCommand = {
|
|
|
2747
2771
|
const entry = componentMap[component];
|
|
2748
2772
|
checksToRun = Array.isArray(entry) ? entry : [entry];
|
|
2749
2773
|
}
|
|
2774
|
+
// These are API identifiers: keep them stable even when user-facing names
|
|
2775
|
+
// change. Grouped components have a distinct ID for each individual check.
|
|
2776
|
+
const checkIds = new Map([
|
|
2777
|
+
[checkVersionFreshness, 'version'], [checkNodeVersion, 'node'],
|
|
2778
|
+
[checkNpmVersion, 'npm'], [checkClaudeCode, 'claude'],
|
|
2779
|
+
[checkAgentBrowserVersion, 'browser'], [checkGit, 'git'],
|
|
2780
|
+
[checkGitRepo, 'git-repo'], [checkConfigFile, 'config'],
|
|
2781
|
+
[checkStaleSettingsNpx, 'stale-settings'], [helperCheck, 'helpers'],
|
|
2782
|
+
[checkDaemonStatus, 'daemon'], [checkMemoryDatabase, 'memory-presence'],
|
|
2783
|
+
[checkMemoryStructuralIntegrity, 'memory-structural'],
|
|
2784
|
+
[checkMemoryIntegrity, 'memory-integrity'],
|
|
2785
|
+
[checkMemoryPersistenceDriver, 'memory-driver'],
|
|
2786
|
+
[checkMemoryPackageVersion, 'memory-package'],
|
|
2787
|
+
[checkMemoryContent, 'memory-content'],
|
|
2788
|
+
[checkMemoryEmbeddingCoverage, 'memory-embeddings'],
|
|
2789
|
+
[checkMemoryReflexionCoverage, 'memory-reflexion'],
|
|
2790
|
+
[checkMemoryCritiqueCoverage, 'memory-critique'],
|
|
2791
|
+
[checkLearningBridge, 'learning'], [nativeAddonCheck, 'native-addon'],
|
|
2792
|
+
[checkApiKeys, 'api'], [checkMcpServers, 'mcp'],
|
|
2793
|
+
[checkMcpSchemaOverhead, 'mcp-overhead'], [checkAIDefence, 'aidefence'],
|
|
2794
|
+
[checkDiskSpace, 'disk'], [checkBuildTools, 'typescript'],
|
|
2795
|
+
[checkAgenticFlow, 'agentic-flow'], [checkEncryptionAtRest, 'encryption'],
|
|
2796
|
+
[checkFederationBreaker, 'federation'], [checkMetaharness, 'metaharness'],
|
|
2797
|
+
[checkMetaharnessDeclaredPackages, 'metaharness-packages'],
|
|
2798
|
+
[checkMetaharnessIntegration, 'metaharness-integration'],
|
|
2799
|
+
[checkFunnel, 'funnel'], [checkProxySponsoredConsent, 'proxy-consent'],
|
|
2800
|
+
[checkProxyBinary, 'proxy-binary'], [checkProxyProcess, 'proxy-process'],
|
|
2801
|
+
[checkProxyBindAddress, 'proxy-bind'], [checkAuth, 'auth'],
|
|
2802
|
+
[checkTypesafeRouter, 'typesafe'], [checkMods, 'mods'],
|
|
2803
|
+
]);
|
|
2804
|
+
nativeAgentDbChecks.forEach((check, index) => {
|
|
2805
|
+
// The path distinguishes simultaneous authority files; normalize separators.
|
|
2806
|
+
checkIds.set(check, `memory-native:${nativeAgentDbPaths[index].replace(/\\/g, '/')}`);
|
|
2807
|
+
});
|
|
2808
|
+
if (json && checksToRun.some(check => !checkIds.has(check))) {
|
|
2809
|
+
return reportError('Doctor check is missing a stable JSON ID');
|
|
2810
|
+
}
|
|
2750
2811
|
const results = [];
|
|
2751
2812
|
const fixes = [];
|
|
2752
2813
|
// OPTIMIZATION: Run all checks in parallel for 3-5x faster execution
|
|
2753
|
-
const spinner = output.createSpinner({ text: 'Running health checks in parallel...', spinner: 'dots' });
|
|
2754
|
-
spinner
|
|
2814
|
+
const spinner = json ? undefined : output.createSpinner({ text: 'Running health checks in parallel...', spinner: 'dots' });
|
|
2815
|
+
spinner?.start();
|
|
2755
2816
|
try {
|
|
2756
2817
|
// Execute all checks concurrently
|
|
2757
2818
|
const checkResults = await Promise.allSettled(checksToRun.map(check => check()));
|
|
2758
|
-
spinner
|
|
2819
|
+
spinner?.stop();
|
|
2759
2820
|
// Process results in order
|
|
2760
2821
|
for (const settledResult of checkResults) {
|
|
2761
2822
|
if (settledResult.status === 'fulfilled') {
|
|
2762
2823
|
const result = settledResult.value;
|
|
2763
2824
|
results.push(result);
|
|
2764
|
-
|
|
2825
|
+
if (!json)
|
|
2826
|
+
output.writeln(formatCheck(result));
|
|
2765
2827
|
if (result.fix && (result.status === 'fail' || result.status === 'warn')) {
|
|
2766
2828
|
fixes.push(`${result.name}: ${result.fix}`);
|
|
2767
2829
|
}
|
|
@@ -2773,12 +2835,15 @@ export const doctorCommand = {
|
|
|
2773
2835
|
message: settledResult.reason?.message || 'Unknown error'
|
|
2774
2836
|
};
|
|
2775
2837
|
results.push(errorResult);
|
|
2776
|
-
|
|
2838
|
+
if (!json)
|
|
2839
|
+
output.writeln(formatCheck(errorResult));
|
|
2777
2840
|
}
|
|
2778
2841
|
}
|
|
2779
2842
|
}
|
|
2780
2843
|
catch (error) {
|
|
2781
|
-
spinner
|
|
2844
|
+
spinner?.stop();
|
|
2845
|
+
if (json)
|
|
2846
|
+
return reportError(`Failed to run health checks: ${error instanceof Error ? error.message : String(error)}`);
|
|
2782
2847
|
output.writeln(output.error('Failed to run health checks'));
|
|
2783
2848
|
}
|
|
2784
2849
|
// #2545: --fix / --install can actually repair the Learning Bridge by
|
|
@@ -2827,6 +2892,18 @@ export const doctorCommand = {
|
|
|
2827
2892
|
const passed = results.filter(r => r.status === 'pass').length;
|
|
2828
2893
|
const warnings = results.filter(r => r.status === 'warn').length;
|
|
2829
2894
|
const failed = results.filter(r => r.status === 'fail').length;
|
|
2895
|
+
if (json) {
|
|
2896
|
+
const checks = results.map((result, index) => ({
|
|
2897
|
+
id: checkIds.get(checksToRun[index]),
|
|
2898
|
+
status: result.status,
|
|
2899
|
+
name: result.name,
|
|
2900
|
+
detail: result.message,
|
|
2901
|
+
...(result.fix ? { fix: result.fix } : {}),
|
|
2902
|
+
}));
|
|
2903
|
+
const data = { schemaVersion: 1, checks };
|
|
2904
|
+
output.writeln(JSON.stringify(data));
|
|
2905
|
+
return { success: failed === 0, exitCode: failed > 0 ? 1 : 0, data };
|
|
2906
|
+
}
|
|
2830
2907
|
output.writeln();
|
|
2831
2908
|
output.writeln(output.dim('─'.repeat(50)));
|
|
2832
2909
|
output.writeln();
|
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
* Memory operations for AgentDB integration
|
|
4
4
|
*/
|
|
5
5
|
import { validateAppendConditions } from '../memory/append-conditions.js';
|
|
6
|
+
import { DEFAULT_SEARCH_THRESHOLD, nearMisses } from '../memory/search-defaults.js';
|
|
6
7
|
import { output } from '../output.js';
|
|
7
8
|
import { select, confirm, input } from '../prompt.js';
|
|
8
9
|
import { callMCPTool, MCPClientError } from '../mcp-client.js';
|
|
@@ -430,7 +431,7 @@ const searchCommand = {
|
|
|
430
431
|
// default to 0.7, which silently re-broke #2558: `memory search` matched
|
|
431
432
|
// content word-for-word and still returned nothing. Regression guard:
|
|
432
433
|
// __tests__/memory-search-recall-2558.test.ts.
|
|
433
|
-
default: 0.
|
|
434
|
+
default: 0.2
|
|
434
435
|
},
|
|
435
436
|
{
|
|
436
437
|
name: 'type',
|
|
@@ -499,7 +500,9 @@ const searchCommand = {
|
|
|
499
500
|
// coalescing preserves an explicit zero. Fallback aligned with the
|
|
500
501
|
// option's declared `default: 0.7` (was `0.3` — the two disagreed
|
|
501
502
|
// and --help advertised a default the code did not honor).
|
|
502
|
-
const threshold = ctx.flags.threshold ??
|
|
503
|
+
const threshold = ctx.flags.threshold ?? DEFAULT_SEARCH_THRESHOLD;
|
|
504
|
+
// The parser fills the option's declared default, so an untouched flag arrives as the default value.
|
|
505
|
+
const thresholdWasDefault = ctx.flags.threshold === undefined || ctx.flags.threshold === DEFAULT_SEARCH_THRESHOLD;
|
|
503
506
|
const searchType = ctx.flags.type || 'semantic';
|
|
504
507
|
const buildHnsw = (ctx.flags['build-hnsw'] || ctx.flags.buildHnsw);
|
|
505
508
|
const requestedIntent = ctx.flags.intent || 'mixed';
|
|
@@ -738,6 +741,15 @@ const searchCommand = {
|
|
|
738
741
|
output.writeln();
|
|
739
742
|
if (results.length === 0) {
|
|
740
743
|
output.printWarning('No results found');
|
|
744
|
+
if (thresholdWasDefault) {
|
|
745
|
+
const near = await nearMisses(searchEntries, { query, namespace, provenanceFilter, dbPath: dbPathSearch });
|
|
746
|
+
if (near) {
|
|
747
|
+
output.writeln(output.dim(` ${near.hint.replace(' in nearMisses', ' below').replace('Pass threshold: 0', 'Pass --threshold 0')}`));
|
|
748
|
+
for (const n of near.nearMisses) {
|
|
749
|
+
output.writeln(output.dim(` ${n.similarity.toFixed(2)} ${n.namespace}/${n.key}`));
|
|
750
|
+
}
|
|
751
|
+
}
|
|
752
|
+
}
|
|
741
753
|
await warnIfSiblingHasRows(ctx.flags.path);
|
|
742
754
|
output.writeln(output.dim('Try: claude-flow memory store -k "key" --value "data"'));
|
|
743
755
|
return { success: true, data: [] };
|
|
@@ -30,6 +30,7 @@ export const policyCommand = {
|
|
|
30
30
|
{ name: 'mode', type: 'string', description: 'Policy mode: legacy | observe | enforce' },
|
|
31
31
|
{ name: 'project-root', type: 'string', description: 'Project root containing .claude-flow/policy' },
|
|
32
32
|
{ name: 'establish-anchor', type: 'boolean', description: 'verify: explicitly anchor a ledger that has receipts but no anchor (interactive, logged with user and time)' },
|
|
33
|
+
{ name: 'full', type: 'boolean', description: 'verify: additionally replay the archived receipts (receipts.ledger.jsonl) to verify the complete historical chain, not just the retained hot tail' },
|
|
33
34
|
],
|
|
34
35
|
async action(context) {
|
|
35
36
|
const args = context.args ?? [];
|
|
@@ -93,9 +94,13 @@ export const policyCommand = {
|
|
|
93
94
|
}
|
|
94
95
|
if (operation === 'verify') {
|
|
95
96
|
const establishAnchor = flags['establish-anchor'] === true || flags.establishAnchor === true;
|
|
97
|
+
const full = flags.full === true;
|
|
96
98
|
if (establishAnchor)
|
|
97
99
|
requireInteractiveAdministrator();
|
|
98
|
-
const ledger = await verifyPolicyLedger(root, { establishAnchor });
|
|
100
|
+
const ledger = await verifyPolicyLedger(root, { establishAnchor, full });
|
|
101
|
+
if (ledger.error === 'receipt-key-id-mismatch') {
|
|
102
|
+
output.printError('Restore the original CLAUDE_FLOW_POLICY_KEY_ID for this ledger; do not rewrite or re-sign historical receipts.');
|
|
103
|
+
}
|
|
99
104
|
const result = print({ ...ledger, anchors: anchorPosture(root), scope: LEDGER_TRUST_SCOPE });
|
|
100
105
|
return ledger.valid ? result : { ...result, success: false, exitCode: 1 };
|
|
101
106
|
}
|
|
@@ -6,8 +6,8 @@
|
|
|
6
6
|
* - every rule is tested after comment text is blanked, so a comment naming `child_process.execFile` or `eval(` is
|
|
7
7
|
* not a finding;
|
|
8
8
|
* - eval needs a word start: "retrieval (" in prose is not a call; `eval(` and `window.eval(` still are;
|
|
9
|
-
* - command injection keeps the old rule's reach (any `exec` after `child_process` on the line), and
|
|
10
|
-
* execSync
|
|
9
|
+
* - command injection keeps the old rule's reach (any `exec` after `child_process` on the line), and also catches
|
|
10
|
+
* dynamic exec/execSync command strings when the import was on another line;
|
|
11
11
|
* - a pg placeholder such as `$${paramCount + 1}` is a parameter number, not a value in the SQL text. It is exempt only
|
|
12
12
|
* when its expression is a number, a loop letter, `params.length`-style, or a name containing `param` or `placeholder`, it
|
|
13
13
|
* stands where SQL takes a value, and it is not inside a quoted SQL string.
|
|
@@ -6,14 +6,72 @@
|
|
|
6
6
|
* - every rule is tested after comment text is blanked, so a comment naming `child_process.execFile` or `eval(` is
|
|
7
7
|
* not a finding;
|
|
8
8
|
* - eval needs a word start: "retrieval (" in prose is not a call; `eval(` and `window.eval(` still are;
|
|
9
|
-
* - command injection keeps the old rule's reach (any `exec` after `child_process` on the line), and
|
|
10
|
-
* execSync
|
|
9
|
+
* - command injection keeps the old rule's reach (any `exec` after `child_process` on the line), and also catches
|
|
10
|
+
* dynamic exec/execSync command strings when the import was on another line;
|
|
11
11
|
* - a pg placeholder such as `$${paramCount + 1}` is a parameter number, not a value in the SQL text. It is exempt only
|
|
12
12
|
* when its expression is a number, a loop letter, `params.length`-style, or a name containing `param` or `placeholder`, it
|
|
13
13
|
* stands where SQL takes a value, and it is not inside a quoted SQL string.
|
|
14
14
|
*/
|
|
15
15
|
/** No `g` flag, so `test` keeps no lastIndex between lines. */
|
|
16
16
|
const re = (pattern) => (line) => pattern.test(line);
|
|
17
|
+
/** Detect a dynamic shell command at an exec/execSync call site, independent of its import. */
|
|
18
|
+
const interpolatedExec = (line) => {
|
|
19
|
+
let quote = null;
|
|
20
|
+
for (let i = 0; i < line.length; i++) {
|
|
21
|
+
const ch = line[i];
|
|
22
|
+
if (quote !== null) {
|
|
23
|
+
if (ch === '\\') {
|
|
24
|
+
i++;
|
|
25
|
+
continue;
|
|
26
|
+
}
|
|
27
|
+
if (ch === quote)
|
|
28
|
+
quote = null;
|
|
29
|
+
continue;
|
|
30
|
+
}
|
|
31
|
+
if (ch === "'" || ch === '"' || ch === '`') {
|
|
32
|
+
quote = ch;
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
if (i > 0 && /[\w$]/.test(line[i - 1]))
|
|
36
|
+
continue;
|
|
37
|
+
const name = line.startsWith('execSync', i) ? 'execSync' : line.startsWith('exec', i) ? 'exec' : '';
|
|
38
|
+
if (!name)
|
|
39
|
+
continue;
|
|
40
|
+
let arg = i + name.length;
|
|
41
|
+
while (/\s/.test(line[arg] ?? ''))
|
|
42
|
+
arg++;
|
|
43
|
+
if (line[arg++] !== '(')
|
|
44
|
+
continue;
|
|
45
|
+
while (/\s/.test(line[arg] ?? ''))
|
|
46
|
+
arg++;
|
|
47
|
+
const delimiter = line[arg];
|
|
48
|
+
if (delimiter !== '`' && delimiter !== "'" && delimiter !== '"')
|
|
49
|
+
continue;
|
|
50
|
+
for (let end = arg + 1; end < line.length; end++) {
|
|
51
|
+
if (line[end] === '\\') {
|
|
52
|
+
end++;
|
|
53
|
+
continue;
|
|
54
|
+
}
|
|
55
|
+
if (delimiter === '`' && line[end] === '$' && line[end + 1] === '{')
|
|
56
|
+
return true;
|
|
57
|
+
if (line[end] !== delimiter)
|
|
58
|
+
continue;
|
|
59
|
+
if (delimiter !== '`') {
|
|
60
|
+
let next = end + 1;
|
|
61
|
+
while (/\s/.test(line[next] ?? ''))
|
|
62
|
+
next++;
|
|
63
|
+
if (line[next++] === '+') {
|
|
64
|
+
while (/\s/.test(line[next] ?? ''))
|
|
65
|
+
next++;
|
|
66
|
+
if (/[A-Za-z_$]/.test(line[next] ?? ''))
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
break;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
return false;
|
|
74
|
+
};
|
|
17
75
|
// A `.length` counts only on a param-, value- or arg-named array: `req.body.length` is whatever the client sent.
|
|
18
76
|
const COUNTER = /^\s*(?:\d+|[ijkn]|(?:[A-Za-z_$][\w$]*(?:Params?|Placeholders?|Values?|Args?)|params?|placeholders?|values?|args?)\.length|[A-Za-z_$]*(?:[Pp]aram|[Pp]laceholder)[\w$]*)\s*(?:[+-]\s*\d+\s*)?$/;
|
|
19
77
|
// Where a pg parameter stands in SQL: after a comparison, a list or call opener, or a clause keyword.
|
|
@@ -70,7 +128,7 @@ export const CODE_PATTERNS = [
|
|
|
70
128
|
{ match: re(/(?<![\w$])eval\s*\(/), type: 'Eval Usage', severity: 'medium', desc: 'eval() can execute arbitrary code' },
|
|
71
129
|
{ match: re(/innerHTML\s*=/), type: 'innerHTML', severity: 'medium', desc: 'XSS risk with innerHTML' },
|
|
72
130
|
{ match: re(/dangerouslySetInnerHTML/), type: 'React XSS', severity: 'medium', desc: 'React XSS risk' },
|
|
73
|
-
{ match:
|
|
131
|
+
{ match: line => /child_process.*exec/.test(line) || interpolatedExec(line), type: 'Command Injection', severity: 'high', desc: 'Possible command injection' },
|
|
74
132
|
{ match: line => sqlInterpolation(line) && sqlInterpolation(withoutPlaceholders(line)), type: 'SQL Injection', severity: 'high', desc: 'Possible SQL injection' },
|
|
75
133
|
];
|
|
76
134
|
/**
|
|
@@ -5,6 +5,30 @@
|
|
|
5
5
|
* Created with ❤️ by ruv.io
|
|
6
6
|
*/
|
|
7
7
|
import type { Command } from '../types.js';
|
|
8
|
+
/**
|
|
9
|
+
* #4124: `npm audit` can fail without exiting through the vulnerability path.
|
|
10
|
+
* In a pnpm/yarn/bun project there is no package-lock.json, so it prints
|
|
11
|
+
* `{"error":{"code":"ENOLOCK",...}}`. That has no `vulnerabilities` key, and
|
|
12
|
+
* both `scan` and `cve` used to read it as a clean tree. Only a report with a
|
|
13
|
+
* `vulnerabilities` object counts as a scan; anything else is "could not scan".
|
|
14
|
+
*/
|
|
15
|
+
type NpmAuditVuln = {
|
|
16
|
+
severity: string;
|
|
17
|
+
via: Array<{
|
|
18
|
+
title?: string;
|
|
19
|
+
url?: string;
|
|
20
|
+
source?: number;
|
|
21
|
+
name?: string;
|
|
22
|
+
} | string>;
|
|
23
|
+
};
|
|
24
|
+
export type NpmAuditResult = {
|
|
25
|
+
ok: true;
|
|
26
|
+
vulnerabilities: Record<string, NpmAuditVuln>;
|
|
27
|
+
} | {
|
|
28
|
+
ok: false;
|
|
29
|
+
reason: string;
|
|
30
|
+
};
|
|
31
|
+
export declare function interpretNpmAudit(raw: string, dir?: string): NpmAuditResult;
|
|
8
32
|
export declare const securityCommand: Command;
|
|
9
33
|
export default securityCommand;
|
|
10
34
|
//# sourceMappingURL=security.d.ts.map
|