@claude-flow/cli 3.56.8 → 3.56.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. package/.claude/helpers/.helpers-version +1 -1
  2. package/.claude/helpers/helpers.manifest.json +4 -4
  3. package/.claude/helpers/hook-handler.cjs +109 -23
  4. package/.claude/helpers/intelligence.cjs +51 -9
  5. package/.claude/helpers/router.cjs +0 -0
  6. package/.claude/skills/verification-quality/SKILL.md +14 -11
  7. package/README.md +1 -1
  8. package/catalog-manifest.json +2 -2
  9. package/dist/src/autopilot-state.d.ts +2 -0
  10. package/dist/src/autopilot-state.js +36 -8
  11. package/dist/src/command-resolution.d.ts +15 -0
  12. package/dist/src/command-resolution.js +25 -0
  13. package/dist/src/commands/agent.js +17 -17
  14. package/dist/src/commands/analyze.js +40 -29
  15. package/dist/src/commands/claims.js +6 -8
  16. package/dist/src/commands/daemon.js +86 -31
  17. package/dist/src/commands/memory.js +14 -2
  18. package/dist/src/commands/security-code-patterns.d.ts +2 -2
  19. package/dist/src/commands/security-code-patterns.js +61 -3
  20. package/dist/src/commands/security.d.ts +24 -0
  21. package/dist/src/commands/security.js +81 -35
  22. package/dist/src/commands/tui.d.ts +9 -0
  23. package/dist/src/commands/tui.js +29 -3
  24. package/dist/src/config/proven-config-refresh.d.ts +9 -0
  25. package/dist/src/config/proven-config-refresh.js +24 -3
  26. package/dist/src/funnel/local-signals.js +2 -1
  27. package/dist/src/index.js +6 -42
  28. package/dist/src/init/helpers-generator.js +109 -23
  29. package/dist/src/init/types.js +1 -1
  30. package/dist/src/mcp-tools/agent-execute-core.d.ts +2 -0
  31. package/dist/src/mcp-tools/agent-execute-core.js +15 -0
  32. package/dist/src/mcp-tools/agent-health-metrics.d.ts +22 -0
  33. package/dist/src/mcp-tools/agent-health-metrics.js +33 -0
  34. package/dist/src/mcp-tools/agent-tools.js +45 -22
  35. package/dist/src/mcp-tools/agentbbs-federation.d.ts +1 -1
  36. package/dist/src/mcp-tools/agentbbs-federation.js +7 -2
  37. package/dist/src/mcp-tools/agentdb-tools.js +25 -2
  38. package/dist/src/mcp-tools/aidefence-known-misses.d.ts +22 -0
  39. package/dist/src/mcp-tools/aidefence-known-misses.js +57 -0
  40. package/dist/src/mcp-tools/analyze-tools.js +10 -11
  41. package/dist/src/mcp-tools/autopilot-tools.js +2 -2
  42. package/dist/src/mcp-tools/browser-intent-tools.js +6 -2
  43. package/dist/src/mcp-tools/embeddings-tools.js +29 -7
  44. package/dist/src/mcp-tools/hooks-tools.js +4 -0
  45. package/dist/src/mcp-tools/memory-tools.js +70 -20
  46. package/dist/src/mcp-tools/neural-tools.js +28 -4
  47. package/dist/src/mcp-tools/ruvllm-tools.js +6 -0
  48. package/dist/src/mcp-tools/security-tools.js +55 -17
  49. package/dist/src/mcp-tools/workflow-tools.js +66 -17
  50. package/dist/src/memory/intelligence.d.ts +5 -1
  51. package/dist/src/memory/intelligence.js +105 -29
  52. package/dist/src/memory/memory-bridge.js +10 -3
  53. package/dist/src/memory/memory-initializer.js +2 -1
  54. package/dist/src/memory/search-defaults.d.ts +54 -0
  55. package/dist/src/memory/search-defaults.js +37 -0
  56. package/dist/src/memory/sona-optimizer.d.ts +28 -0
  57. package/dist/src/memory/sona-optimizer.js +59 -2
  58. package/dist/src/mods/command-registry/schema.d.ts +24 -24
  59. package/dist/src/parser.js +32 -22
  60. package/dist/src/production/monitoring.d.ts +1 -0
  61. package/dist/src/production/monitoring.js +19 -6
  62. package/dist/src/production/rate-limiter.js +5 -4
  63. package/dist/src/production/retry.js +3 -0
  64. package/dist/src/ruvector/agent-wasm.d.ts +10 -0
  65. package/dist/src/ruvector/agent-wasm.js +27 -1
  66. package/dist/src/ruvector/diff-classifier.d.ts +2 -4
  67. package/dist/src/ruvector/diff-classifier.js +202 -69
  68. package/dist/src/ruvector/ruvllm-wasm.d.ts +4 -0
  69. package/dist/src/ruvector/ruvllm-wasm.js +12 -1
  70. package/dist/src/services/config-file-manager.js +3 -3
  71. package/dist/src/services/policy-runtime.d.ts +1 -1
  72. package/dist/src/services/policy-runtime.js +7 -1
  73. package/dist/src/services/worker-daemon.d.ts +11 -0
  74. package/dist/src/services/worker-daemon.js +37 -0
  75. package/dist/src/transfer/store/discovery.js +16 -17
  76. package/dist/src/transfer/store/types.d.ts +4 -0
  77. package/node_modules/@claude-flow/codex/dist/cli.js +0 -0
  78. package/node_modules/@claude-flow/codex/dist/dual-mode/orchestrator.d.ts +1 -0
  79. package/node_modules/@claude-flow/codex/dist/dual-mode/orchestrator.d.ts.map +1 -1
  80. package/node_modules/@claude-flow/codex/dist/dual-mode/orchestrator.js +3 -1
  81. package/node_modules/@claude-flow/codex/dist/dual-mode/orchestrator.js.map +1 -1
  82. package/node_modules/@claude-flow/codex/dist/index.d.ts +3 -3
  83. package/node_modules/@claude-flow/codex/dist/index.js +1 -1
  84. package/node_modules/@claude-flow/codex/package.json +1 -1
  85. package/node_modules/@claude-flow/plugin-agent-federation/dist/bin.js +0 -0
  86. package/node_modules/@claude-flow/security/dist/input-validator.d.ts +6 -6
  87. package/node_modules/@claude-flow/security/dist/policy/envelope.js +6 -6
  88. package/node_modules/@claude-flow/security/dist/policy/envelope.js.map +1 -1
  89. package/node_modules/@claude-flow/security/package.json +1 -1
  90. package/package.json +7 -7
  91. package/plugins/ruflo-console/dist/tui.mjs +162 -160
  92. package/plugins/ruflo-metaharness/.claude-flow/daemon-state.json +179 -0
  93. package/plugins/ruflo-metaharness/.claude-flow/daemon.pid +1 -0
  94. package/plugins/ruflo-metaharness/.claude-flow/logs/daemon.log +32 -0
  95. package/plugins/ruflo-metaharness/.claude-flow/metrics/backup.json +23 -0
  96. package/plugins/ruflo-metaharness/.claude-flow/metrics/codebase-map.json +11 -0
  97. package/plugins/ruflo-metaharness/.claude-flow/metrics/consolidation.json +16 -0
  98. package/plugins/ruflo-metaharness/.claude-flow/metrics/harness-loop.json +91 -0
  99. package/plugins/ruflo-metaharness/.claude-flow/metrics/performance.json +17 -0
  100. package/plugins/ruflo-metaharness/.claude-flow/metrics/security-audit.json +12 -0
  101. package/plugins/ruflo-metaharness/.claude-flow/metrics/test-gaps.json +8 -0
  102. package/plugins/ruflo-metaharness/.claude-flow/neural/patterns.json +2809 -0
  103. package/plugins/ruflo-metaharness/.claude-flow/neural/stats.json +6 -0
  104. package/plugins/ruflo-metaharness/.claude-flow/policy/state.json +11 -0
  105. package/plugins/ruflo-metaharness/.claude-plugin/plugin.json +1 -1
  106. package/plugins/ruflo-metaharness/.swarm/agentdb-memory.db +0 -0
  107. package/plugins/ruflo-metaharness/.swarm/agentdb-memory.db-shm +0 -0
  108. package/plugins/ruflo-metaharness/.swarm/agentdb-memory.db-wal +0 -0
  109. package/plugins/ruflo-metaharness/.swarm/backups/memory-2026-10-14T16-09-38-164Z.db +0 -0
  110. package/plugins/ruflo-metaharness/.swarm/backups/store-agentdb-memory.db-2026-10-14T16-09-38-166Z.db +0 -0
  111. package/plugins/ruflo-metaharness/.swarm/ewc-fisher.json +397 -0
  112. package/plugins/ruflo-metaharness/.swarm/memory.db +0 -0
  113. package/plugins/ruflo-metaharness/.swarm/memory.db-shm +0 -0
  114. package/plugins/ruflo-metaharness/.swarm/memory.db-wal +0 -0
  115. package/plugins/ruflo-metaharness/CHANGELOG.md +3 -0
  116. package/plugins/ruflo-metaharness/agentdb.rvf +0 -0
  117. package/plugins/ruflo-metaharness/agentdb.rvf.lock +0 -0
  118. package/plugins/ruflo-metaharness/hooks/screen.ts +10 -3
  119. package/plugins/ruflo-metaharness/ruvector.db +0 -0
  120. package/plugins/ruflo-metaharness/scripts/smoke.sh +4 -4
@@ -97,11 +97,19 @@ const diffCommand = {
97
97
  output.printInfo(`Analyzing diff: ${output.highlight(ref)}`);
98
98
  try {
99
99
  // Call MCP tool for diff analysis
100
+ // Shape returned by the analyze_diff MCP tool (ruvector/diff-classifier.ts).
101
+ // Typed from the classifier itself so the renderer cannot drift from it again.
100
102
  const result = await callMCPTool('analyze_diff', {
101
103
  ref,
102
- includeFileRisks: verbose,
104
+ // Always request file risks: the handler computes them anyway, and the
105
+ // high-risk file list below is derived from them.
106
+ includeFileRisks: true,
103
107
  includeReviewers: showReviewers || showAll,
104
108
  });
109
+ if (result.error) {
110
+ output.printError(`Diff analysis failed: ${result.message ?? 'unknown error'}`);
111
+ return { success: false, exitCode: 1 };
112
+ }
105
113
  // JSON output
106
114
  if (formatType === 'json') {
107
115
  output.printJson(result);
@@ -110,13 +118,18 @@ const diffCommand = {
110
118
  output.writeln();
111
119
  // Summary box
112
120
  const files = result.files || [];
113
- const risk = result.risk || { overall: 'unknown', score: 0, breakdown: { fileCount: 0, totalChanges: 0, highRiskFiles: [], securityConcerns: [], breakingChanges: [], testCoverage: 'unknown' } };
114
- const classification = result.classification || { category: 'unknown', confidence: 0, reasoning: '' };
121
+ const fileRisks = result.fileRisks || [];
122
+ const risk = result.risk || { overall: 'low', score: 0, breakdown: { low: 0, medium: 0, high: 0, critical: 0 } };
123
+ const classification = result.classification;
124
+ const category = classification?.primary ?? 'unknown';
125
+ const secondary = classification?.secondary?.length ? classification.secondary.join(', ') : '';
126
+ const totalChanges = files.reduce((sum, f) => sum + f.additions + f.deletions, 0);
127
+ const highRiskFiles = fileRisks.filter((r) => r.risk === 'high' || r.risk === 'critical').map((r) => r.file);
115
128
  output.printBox([
116
129
  `Ref: ${result.ref || 'HEAD'}`,
117
130
  `Files: ${files.length}`,
118
131
  `Risk: ${getRiskDisplay(risk.overall)} (${risk.score}/100)`,
119
- `Type: ${classification.category}${classification.subcategory ? ` (${classification.subcategory})` : ''}`,
132
+ `Type: ${category}${secondary ? ` (${secondary})` : ''}`,
120
133
  ``,
121
134
  result.summary || 'No summary available',
122
135
  ].join('\n'), 'Diff Analysis');
@@ -128,33 +141,28 @@ const diffCommand = {
128
141
  output.printTable({
129
142
  columns: [
130
143
  { key: 'metric', header: 'Metric', width: 25 },
131
- { key: 'value', header: 'Value', width: 30 },
144
+ { key: 'value', header: 'Value', width: 42 },
132
145
  ],
133
146
  data: [
134
147
  { metric: 'Overall Risk', value: getRiskDisplay(risk.overall) },
135
148
  { metric: 'Risk Score', value: `${risk.score}/100` },
136
- { metric: 'Files Changed', value: risk.breakdown.fileCount },
137
- { metric: 'Total Lines Changed', value: risk.breakdown.totalChanges },
138
- { metric: 'Test Coverage', value: risk.breakdown.testCoverage },
149
+ { metric: 'Files Changed', value: files.length },
150
+ { metric: 'Total Lines Changed', value: totalChanges },
151
+ { metric: 'Files by Risk', value: `low ${risk.breakdown.low} / medium ${risk.breakdown.medium} / high ${risk.breakdown.high} / critical ${risk.breakdown.critical}` },
139
152
  ],
140
153
  });
141
- // Security concerns
142
- if (risk.breakdown.securityConcerns.length > 0) {
143
- output.writeln();
144
- output.writeln(output.bold(output.warning('Security Concerns')));
145
- output.printList(risk.breakdown.securityConcerns.map(c => output.warning(c)));
146
- }
147
- // Breaking changes
148
- if (risk.breakdown.breakingChanges.length > 0) {
154
+ // Risk factors reported by the classifier
155
+ const riskFactors = classification?.riskFactors ?? [];
156
+ if (riskFactors.length > 0) {
149
157
  output.writeln();
150
- output.writeln(output.bold(output.error('Potential Breaking Changes')));
151
- output.printList(risk.breakdown.breakingChanges.map(c => output.error(c)));
158
+ output.writeln(output.bold(output.warning('Risk Factors')));
159
+ output.printList(riskFactors.map(c => output.warning(c)));
152
160
  }
153
161
  // High risk files
154
- if (risk.breakdown.highRiskFiles.length > 0) {
162
+ if (highRiskFiles.length > 0) {
155
163
  output.writeln();
156
164
  output.writeln(output.bold('High Risk Files'));
157
- output.printList(risk.breakdown.highRiskFiles.map(f => output.warning(f)));
165
+ output.printList(highRiskFiles.map(f => output.warning(f)));
158
166
  }
159
167
  }
160
168
  // Classification
@@ -168,13 +176,16 @@ const diffCommand = {
168
176
  { key: 'value', header: 'Value', width: 40 },
169
177
  ],
170
178
  data: [
171
- { field: 'Category', value: classification.category },
172
- { field: 'Subcategory', value: classification.subcategory || '-' },
173
- { field: 'Confidence', value: `${(classification.confidence * 100).toFixed(0)}%` },
179
+ { field: 'Category', value: category },
180
+ { field: 'Secondary', value: secondary || '-' },
181
+ { field: 'Confidence', value: `${((classification?.confidence ?? 0) * 100).toFixed(0)}%` },
182
+ { field: 'Impact', value: classification?.impactLevel ?? '-' },
174
183
  ],
175
184
  });
176
- output.writeln();
177
- output.writeln(output.dim(`Reasoning: ${classification.reasoning}`));
185
+ if (classification?.testingStrategy?.length) {
186
+ output.writeln();
187
+ output.writeln(output.dim(`Testing strategy: ${classification.testingStrategy.join(', ')}`));
188
+ }
178
189
  }
179
190
  // Reviewers
180
191
  if (showReviewers || showAll) {
@@ -190,13 +201,13 @@ const diffCommand = {
190
201
  }
191
202
  }
192
203
  // Verbose file-level details
193
- if (verbose && result.fileRisks) {
204
+ if (verbose && fileRisks.length > 0) {
194
205
  output.writeln();
195
206
  output.writeln(output.bold('File-Level Analysis'));
196
207
  output.writeln(output.dim('-'.repeat(50)));
197
208
  output.printTable({
198
209
  columns: [
199
- { key: 'path', header: 'File', width: 40 },
210
+ { key: 'file', header: 'File', width: 40 },
200
211
  { key: 'risk', header: 'Risk', width: 12, format: (v) => getRiskDisplay(String(v)) },
201
212
  { key: 'score', header: 'Score', width: 8, align: 'right' },
202
213
  { key: 'reasons', header: 'Reasons', width: 30, format: (v) => {
@@ -204,7 +215,7 @@ const diffCommand = {
204
215
  return reasons.slice(0, 2).join('; ');
205
216
  } },
206
217
  ],
207
- data: result.fileRisks,
218
+ data: fileRisks.map((r) => ({ ...r })),
208
219
  });
209
220
  }
210
221
  // Files changed table
@@ -219,7 +230,7 @@ const diffCommand = {
219
230
  { key: 'additions', header: '+', width: 8, align: 'right', format: (v) => output.success(`+${v}`) },
220
231
  { key: 'deletions', header: '-', width: 8, align: 'right', format: (v) => output.error(`-${v}`) },
221
232
  ],
222
- data: files.slice(0, 20),
233
+ data: files.slice(0, 20).map((f) => ({ ...f })),
223
234
  });
224
235
  if (files.length > 20) {
225
236
  output.writeln(output.dim(` ... and ${files.length - 20} more files`));
@@ -5,7 +5,8 @@
5
5
  * Created with ❤️ by ruv.io
6
6
  */
7
7
  import { existsSync, readFileSync, writeFileSync, mkdirSync, renameSync } from 'node:fs';
8
- import { resolve, dirname } from 'node:path';
8
+ import { resolve, dirname, join } from 'node:path';
9
+ import { homedir } from 'node:os';
9
10
  import { output } from '../output.js';
10
11
  const CLAIMS_CONFIG_PATHS = [
11
12
  '.claude-flow/claims.json',
@@ -15,7 +16,9 @@ function getClaimsConfigPaths() {
15
16
  return [
16
17
  resolve(CLAIMS_CONFIG_PATHS[0]),
17
18
  resolve(CLAIMS_CONFIG_PATHS[1]),
18
- resolve(process.env.HOME || '~', '.config/claude-flow/claims.json'),
19
+ // os.homedir(), not $HOME: HOME is normally unset on Windows, and the old
20
+ // `process.env.HOME || '~'` fallback resolved to a literal <cwd>/~ folder.
21
+ join(homedir(), '.config', 'claude-flow', 'claims.json'),
19
22
  ];
20
23
  }
21
24
  function loadClaimsConfig() {
@@ -152,18 +155,13 @@ const checkCommand = {
152
155
  const spinner = output.createSpinner({ text: 'Evaluating claim...', spinner: 'dots' });
153
156
  spinner.start();
154
157
  const fs = await import('fs');
155
- const path = await import('path');
156
158
  // Real claims evaluation from config file
157
159
  let isGranted = false;
158
160
  let reason = 'Claim not found in policy';
159
161
  let policySource = 'default';
160
162
  try {
161
163
  // Check for claims config file
162
- const claimsConfigPaths = [
163
- path.resolve('.claude-flow/claims.json'),
164
- path.resolve('claude-flow.claims.json'),
165
- path.resolve(process.env.HOME || '~', '.config/claude-flow/claims.json'),
166
- ];
164
+ const claimsConfigPaths = getClaimsConfigPaths();
167
165
  let claimsConfig = {
168
166
  // Default policy - allows basic operations
169
167
  roles: {
@@ -164,37 +164,44 @@ const startCommand = {
164
164
  fs.mkdirSync(stateDir, { recursive: true });
165
165
  }
166
166
  catch { /* exists */ }
167
- try {
168
- lockFd = fs.openSync(lockFile, fs.constants.O_CREAT | fs.constants.O_EXCL | fs.constants.O_WRONLY);
169
- fs.writeSync(lockFd, String(process.pid));
170
- }
171
- catch (e) {
172
- if (e.code === 'EEXIST') {
173
- // Another `daemon start` is mid-spawn. Wait up to 5s for it to
174
- // finish, then re-check the PID file. If the holder crashed
175
- // mid-spawn, fall through and reset; killStaleDaemons + a fresh
176
- // attempt will recover.
177
- const deadline = Date.now() + 5000;
178
- while (Date.now() < deadline) {
179
- await new Promise((r) => setTimeout(r, 100));
180
- const winnerPid = getBackgroundDaemonPid(projectRoot);
181
- if (winnerPid && isProcessRunning(winnerPid)) {
182
- if (!quiet) {
183
- output.printWarning(`Daemon already running in background (PID: ${winnerPid}). Stop it first with: daemon stop`);
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
- // Stale lockfile from a crashed prior attempt — clear it and
189
- // proceed without a held lock. Worst case we double-spawn ONCE
190
- // and the killStaleDaemons sweep below cleans up.
191
- try {
192
- fs.unlinkSync(lockFile);
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
- catch { /* ignore */ }
195
- }
196
- else {
197
- throw e;
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
  */
@@ -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.3
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 ?? 0.3;
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: [] };
@@ -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 no longer skips
10
- * execSync as `exec[^S]` did;
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 no longer skips
10
- * execSync as `exec[^S]` did;
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: re(/child_process.*exec/), type: 'Command Injection', severity: 'high', desc: 'Possible command injection' },
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