claude-flow 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 (92) hide show
  1. package/.claude/.proven-config-version +1 -0
  2. package/.claude/helpers/hook-handler.cjs +109 -23
  3. package/.claude/helpers/intelligence.cjs +51 -9
  4. package/.claude/proven-config.json +42 -0
  5. package/.claude/skills/verification-quality/SKILL.md +14 -11
  6. package/README.md +1 -1
  7. package/node_modules/@claude-flow/codex/dist/cli.js +0 -0
  8. package/node_modules/@claude-flow/codex/dist/dual-mode/orchestrator.d.ts +1 -0
  9. package/node_modules/@claude-flow/codex/dist/dual-mode/orchestrator.d.ts.map +1 -1
  10. package/node_modules/@claude-flow/codex/dist/dual-mode/orchestrator.js +3 -1
  11. package/node_modules/@claude-flow/codex/dist/dual-mode/orchestrator.js.map +1 -1
  12. package/node_modules/@claude-flow/codex/dist/index.d.ts +3 -3
  13. package/node_modules/@claude-flow/codex/dist/index.js +1 -1
  14. package/node_modules/@claude-flow/codex/package.json +1 -1
  15. package/node_modules/@claude-flow/plugin-agent-federation/dist/bin.js +0 -0
  16. package/node_modules/@claude-flow/security/dist/input-validator.d.ts +6 -6
  17. package/node_modules/@claude-flow/security/dist/policy/envelope.js +6 -6
  18. package/node_modules/@claude-flow/security/dist/policy/envelope.js.map +1 -1
  19. package/node_modules/@claude-flow/security/package.json +1 -1
  20. package/package.json +5 -5
  21. package/v3/@claude-flow/cli/dist/src/autopilot-state.d.ts +2 -0
  22. package/v3/@claude-flow/cli/dist/src/autopilot-state.js +36 -8
  23. package/v3/@claude-flow/cli/dist/src/command-resolution.d.ts +15 -0
  24. package/v3/@claude-flow/cli/dist/src/command-resolution.js +25 -0
  25. package/v3/@claude-flow/cli/dist/src/commands/agent.js +17 -17
  26. package/v3/@claude-flow/cli/dist/src/commands/analyze.js +40 -29
  27. package/v3/@claude-flow/cli/dist/src/commands/claims.js +6 -8
  28. package/v3/@claude-flow/cli/dist/src/commands/daemon.js +86 -31
  29. package/v3/@claude-flow/cli/dist/src/commands/memory.js +14 -2
  30. package/v3/@claude-flow/cli/dist/src/commands/security-code-patterns.d.ts +2 -2
  31. package/v3/@claude-flow/cli/dist/src/commands/security-code-patterns.js +61 -3
  32. package/v3/@claude-flow/cli/dist/src/commands/security.d.ts +24 -0
  33. package/v3/@claude-flow/cli/dist/src/commands/security.js +81 -35
  34. package/v3/@claude-flow/cli/dist/src/commands/tui.d.ts +9 -0
  35. package/v3/@claude-flow/cli/dist/src/commands/tui.js +29 -3
  36. package/v3/@claude-flow/cli/dist/src/config/proven-config-refresh.d.ts +9 -0
  37. package/v3/@claude-flow/cli/dist/src/config/proven-config-refresh.js +24 -3
  38. package/v3/@claude-flow/cli/dist/src/funnel/local-signals.js +2 -1
  39. package/v3/@claude-flow/cli/dist/src/index.js +6 -42
  40. package/v3/@claude-flow/cli/dist/src/init/helpers-generator.js +109 -23
  41. package/v3/@claude-flow/cli/dist/src/init/types.js +1 -1
  42. package/v3/@claude-flow/cli/dist/src/mcp-tools/agent-execute-core.d.ts +2 -0
  43. package/v3/@claude-flow/cli/dist/src/mcp-tools/agent-execute-core.js +15 -0
  44. package/v3/@claude-flow/cli/dist/src/mcp-tools/agent-health-metrics.d.ts +22 -0
  45. package/v3/@claude-flow/cli/dist/src/mcp-tools/agent-health-metrics.js +33 -0
  46. package/v3/@claude-flow/cli/dist/src/mcp-tools/agent-tools.js +45 -22
  47. package/v3/@claude-flow/cli/dist/src/mcp-tools/agentbbs-federation.d.ts +1 -1
  48. package/v3/@claude-flow/cli/dist/src/mcp-tools/agentbbs-federation.js +7 -2
  49. package/v3/@claude-flow/cli/dist/src/mcp-tools/agentdb-tools.js +25 -2
  50. package/v3/@claude-flow/cli/dist/src/mcp-tools/aidefence-known-misses.d.ts +22 -0
  51. package/v3/@claude-flow/cli/dist/src/mcp-tools/aidefence-known-misses.js +57 -0
  52. package/v3/@claude-flow/cli/dist/src/mcp-tools/analyze-tools.js +10 -11
  53. package/v3/@claude-flow/cli/dist/src/mcp-tools/autopilot-tools.js +2 -2
  54. package/v3/@claude-flow/cli/dist/src/mcp-tools/browser-intent-tools.js +6 -2
  55. package/v3/@claude-flow/cli/dist/src/mcp-tools/embeddings-tools.js +29 -7
  56. package/v3/@claude-flow/cli/dist/src/mcp-tools/hooks-tools.js +4 -0
  57. package/v3/@claude-flow/cli/dist/src/mcp-tools/memory-tools.js +70 -20
  58. package/v3/@claude-flow/cli/dist/src/mcp-tools/neural-tools.js +28 -4
  59. package/v3/@claude-flow/cli/dist/src/mcp-tools/ruvllm-tools.js +6 -0
  60. package/v3/@claude-flow/cli/dist/src/mcp-tools/security-tools.js +55 -17
  61. package/v3/@claude-flow/cli/dist/src/mcp-tools/workflow-tools.js +66 -17
  62. package/v3/@claude-flow/cli/dist/src/memory/intelligence.d.ts +5 -1
  63. package/v3/@claude-flow/cli/dist/src/memory/intelligence.js +105 -29
  64. package/v3/@claude-flow/cli/dist/src/memory/memory-bridge.js +10 -3
  65. package/v3/@claude-flow/cli/dist/src/memory/memory-initializer.js +2 -1
  66. package/v3/@claude-flow/cli/dist/src/memory/search-defaults.d.ts +54 -0
  67. package/v3/@claude-flow/cli/dist/src/memory/search-defaults.js +37 -0
  68. package/v3/@claude-flow/cli/dist/src/memory/sona-optimizer.d.ts +28 -0
  69. package/v3/@claude-flow/cli/dist/src/memory/sona-optimizer.js +59 -2
  70. package/v3/@claude-flow/cli/dist/src/mods/command-registry/schema.d.ts +24 -24
  71. package/v3/@claude-flow/cli/dist/src/parser.js +32 -22
  72. package/v3/@claude-flow/cli/dist/src/production/monitoring.d.ts +1 -0
  73. package/v3/@claude-flow/cli/dist/src/production/monitoring.js +19 -6
  74. package/v3/@claude-flow/cli/dist/src/production/rate-limiter.js +5 -4
  75. package/v3/@claude-flow/cli/dist/src/production/retry.js +3 -0
  76. package/v3/@claude-flow/cli/dist/src/ruvector/agent-wasm.d.ts +10 -0
  77. package/v3/@claude-flow/cli/dist/src/ruvector/agent-wasm.js +27 -1
  78. package/v3/@claude-flow/cli/dist/src/ruvector/diff-classifier.d.ts +2 -4
  79. package/v3/@claude-flow/cli/dist/src/ruvector/diff-classifier.js +202 -69
  80. package/v3/@claude-flow/cli/dist/src/ruvector/ruvllm-wasm.d.ts +4 -0
  81. package/v3/@claude-flow/cli/dist/src/ruvector/ruvllm-wasm.js +12 -1
  82. package/v3/@claude-flow/cli/dist/src/services/config-file-manager.js +3 -3
  83. package/v3/@claude-flow/cli/dist/src/services/policy-runtime.d.ts +1 -1
  84. package/v3/@claude-flow/cli/dist/src/services/policy-runtime.js +7 -1
  85. package/v3/@claude-flow/cli/dist/src/services/worker-daemon.d.ts +11 -0
  86. package/v3/@claude-flow/cli/dist/src/services/worker-daemon.js +37 -0
  87. package/v3/@claude-flow/cli/dist/src/transfer/store/discovery.js +16 -17
  88. package/v3/@claude-flow/cli/dist/src/transfer/store/types.d.ts +4 -0
  89. package/v3/@claude-flow/cli/package.json +7 -7
  90. package/v3/@claude-flow/shared/dist/resilience/rate-limiter.js +3 -3
  91. package/v3/@claude-flow/shared/dist/resilience/retry.js +3 -1
  92. package/v3/@claude-flow/shared/package.json +1 -1
@@ -84,6 +84,27 @@ export function currentInstallEnv(cwd = process.cwd()) {
84
84
  catch { /* none */ }
85
85
  return env;
86
86
  }
87
+ /**
88
+ * #4176: is `cwd` a ruflo project? A bare `.claude/` is not enough: Claude Code
89
+ * itself creates `.claude/.cc-writes/` in whatever directory a Bash command ran
90
+ * in, so every subdirectory a session cd-ed into used to qualify and received a
91
+ * proven-config.json. A ruflo project carries `ruflo init`'s own markers: a
92
+ * `.claude/settings.json`, the `.claude/helpers/` directory, or an earlier
93
+ * adoption stamp.
94
+ */
95
+ export function isRufloProject(cwd) {
96
+ try {
97
+ const claudeDir = path.join(cwd, '.claude');
98
+ if (!fs.statSync(claudeDir).isDirectory())
99
+ return false;
100
+ return fs.existsSync(path.join(claudeDir, 'settings.json'))
101
+ || fs.existsSync(path.join(claudeDir, 'helpers'))
102
+ || fs.existsSync(path.join(claudeDir, PROVEN_CONFIG_STAMP));
103
+ }
104
+ catch {
105
+ return false;
106
+ }
107
+ }
87
108
  /**
88
109
  * The testable adoption core: given a signed champion and the local env, adopt
89
110
  * it into `cwd/.claude` iff it is newer than the stamp AND passes both gates.
@@ -92,8 +113,8 @@ export function currentInstallEnv(cwd = process.cwd()) {
92
113
  export function adoptSignedConfig(cwd, signed, env, opts = {}) {
93
114
  try {
94
115
  const claudeDir = path.join(cwd, '.claude');
95
- if (!fs.existsSync(claudeDir))
96
- return { adopted: false }; // not a ruflo project
116
+ if (!isRufloProject(cwd))
117
+ return { adopted: false }; // not a ruflo project (#4176)
97
118
  const championId = signed.manifest?.policy?.ref;
98
119
  if (!championId)
99
120
  return { adopted: false, skipped: 'manifest missing policy.ref' };
@@ -137,7 +158,7 @@ export function adoptSignedConfig(cwd, signed, env, opts = {}) {
137
158
  */
138
159
  export async function autoAdoptProvenConfigIfStale(cwd = process.cwd()) {
139
160
  try {
140
- if (!fs.existsSync(path.join(cwd, '.claude')))
161
+ if (!isRufloProject(cwd))
141
162
  return { adopted: false };
142
163
  const src = findPackageProvenConfig();
143
164
  if (!src)
@@ -34,7 +34,8 @@ export function getSecurityStatus(cwd = process.cwd()) {
34
34
  const rawFindings = scan.summary?.total ?? scan.totalFindings ?? scan.findings?.length ?? 0;
35
35
  const findings = Math.max(0, Number.isFinite(rawFindings) ? rawFindings : 0);
36
36
  return {
37
- status: findings > 0 ? 'ISSUES' : 'CLEAN',
37
+ // #4124: a scan whose dependency phase could not run is not a clean scan.
38
+ status: findings > 0 ? 'ISSUES' : scan.incomplete === true ? 'PENDING' : 'CLEAN',
38
39
  findings,
39
40
  scannedAt: typeof scan.timestamp === 'string' ? scan.timestamp : undefined,
40
41
  cvesFixed: 0,
@@ -16,6 +16,7 @@ import { fileURLToPath } from 'url';
16
16
  import { dirname, join } from 'path';
17
17
  import { commandParser } from './parser.js';
18
18
  import { output } from './output.js';
19
+ import { resolveTargetCommand } from './command-resolution.js';
19
20
  import { commands, getCommandsByCategory, getCommand, getCommandAsync, getCommandNames, getLazyCommandNames, hasCommand } from './commands/index.js';
20
21
  import { suggestCommand } from './suggest.js';
21
22
  import { runStartupUpdateCheck } from './update/index.js';
@@ -264,48 +265,11 @@ export class CLI {
264
265
  this.output.writeln(this.output.dim(` ${message}`));
265
266
  process.exit(1);
266
267
  }
267
- // Handle subcommand (supports nested subcommands)
268
- let targetCommand = command;
269
- let subcommandArgs = positional;
270
- // Process command path (e.g., ['hooks', 'worker', 'list'])
271
- // Note: When parser includes subcommand in commandPath, positional already excludes it
272
- if (commandPath.length > 1 && command.subcommands) {
273
- const subcommandName = commandPath[1];
274
- const subcommand = command.subcommands.find(sc => sc.name === subcommandName || sc.aliases?.includes(subcommandName));
275
- if (subcommand) {
276
- targetCommand = subcommand;
277
- // Parser already extracted subcommand from positional, so use as-is
278
- subcommandArgs = positional;
279
- // Check for nested subcommand (level 2)
280
- if (commandPath.length > 2 && subcommand.subcommands) {
281
- const nestedName = commandPath[2];
282
- const nestedSubcommand = subcommand.subcommands.find(sc => sc.name === nestedName || sc.aliases?.includes(nestedName));
283
- if (nestedSubcommand) {
284
- targetCommand = nestedSubcommand;
285
- // Parser already extracted nested subcommand too
286
- subcommandArgs = positional;
287
- }
288
- }
289
- }
290
- }
291
- else if (positional.length > 0 && command.subcommands) {
292
- // Check if first positional is a subcommand
293
- const subcommandName = positional[0];
294
- const subcommand = command.subcommands.find(sc => sc.name === subcommandName || sc.aliases?.includes(subcommandName));
295
- if (subcommand) {
296
- targetCommand = subcommand;
297
- subcommandArgs = positional.slice(1);
298
- // Check for nested subcommand (level 2 from positional)
299
- if (subcommandArgs.length > 0 && subcommand.subcommands) {
300
- const nestedName = subcommandArgs[0];
301
- const nestedSubcommand = subcommand.subcommands.find(sc => sc.name === nestedName || sc.aliases?.includes(nestedName));
302
- if (nestedSubcommand) {
303
- targetCommand = nestedSubcommand;
304
- subcommandArgs = subcommandArgs.slice(1);
305
- }
306
- }
307
- }
308
- }
268
+ // Handle subcommands at any depth (#4157): walk the parsed command path, or the
269
+ // positional arguments when the path only names the top-level command.
270
+ const resolved = resolveTargetCommand(command, commandPath, positional);
271
+ const targetCommand = resolved.target;
272
+ const subcommandArgs = resolved.args;
309
273
  // Validate flags
310
274
  const validationErrors = this.parser.validateFlags(flags, targetCommand);
311
275
  if (validationErrors.length > 0) {
@@ -478,7 +478,8 @@ module.exports = commands;
478
478
  }
479
479
  // Literal-word root guard mirrored in the mod and both shipped classic helpers.
480
480
  const ROOT_DELETE_CHECK_SOURCE = String.raw `function hasRootDelete(command, depth = 0) {
481
- let word = '', quote = '', started = false, redirect = false
481
+ // mask has one letter per character of word: 'u' unquoted, 'q' quoted or escaped (only unquoted braces expand).
482
+ let word = '', mask = '', quote = '', started = false, redirect = false
482
483
  let ansiNul = false // a NUL inside $'...' ends that string, as in bash
483
484
  let inRm = false, optionsEnded = false, recursive = false, force = false, root = false
484
485
  const isRoot = (operand) => {
@@ -508,23 +509,92 @@ const ROOT_DELETE_CHECK_SOURCE = String.raw `function hasRootDelete(command, dep
508
509
  if (c === 'c' && at + 1 < s.length) return [String.fromCharCode(s.charCodeAt(at + 1) & 31), at + 1]
509
510
  return ['\\' + c, at]
510
511
  }
511
- const finishWord = () => {
512
- if (!started) return false
513
- // Literal shell strings (e.g. sh -c 'rm -rf /') also carried the old guard.
514
- // Bound rescanning to four levels; beyond that retain its conservative check.
515
- if (/[\s;&|()\x60]/.test(word)) {
516
- if (depth < 4 ? hasRootDelete(word, depth + 1) : word.includes('rm -rf /')) return true
512
+ // Past the rescan bound (or the brace budget), a string is judged by its tokens: an rm and a possible root operand
513
+ // anywhere in it deny. Quote characters are dropped first, so r"m" is still rm; a path token with braces may expand to /.
514
+ const looseRootDelete = (text) => {
515
+ const tokens = text.replace(/["'\\]/g, '').split(/[\s;&|()\x60$]+/)
516
+ return tokens.some(t => t === 'rm' || t.endsWith('/rm')) && tokens.some(t => isRoot(t) || (t.includes('/') && /[{}]/.test(t)))
517
+ }
518
+ // Bash brace expansion of one word ({a,b} lists, nested or glued to text), and nothing else: no {1..3} sequences, and
519
+ // nothing inside parameter braces after a dollar sign. Only unquoted braces and commas count. Iterative and bounded:
520
+ // past BRACE_WORDS words or BRACE_CHARS characters of work in this call it returns undefined.
521
+ const BRACE_WORDS = 4096
522
+ let braceChars = 1_000_000
523
+ // The leftmost expandable group: an unquoted {...} with an unquoted comma at its own level, outside any dollar-brace parameter. One pass.
524
+ const firstGroup = (text, marks) => {
525
+ const stack = []
526
+ let dollars = 0
527
+ let best
528
+ for (let k = 0; k < text.length; k++) {
529
+ if (marks[k] !== 'u') continue
530
+ const c = text[k]
531
+ if (c === '{') {
532
+ const dollar = k > 0 && text[k - 1] === '$' && marks[k - 1] === 'u'
533
+ if (dollar) dollars++
534
+ stack.push({ open: k, commas: [], dollar: dollar || dollars > 0 })
535
+ } else if (c === ',' && stack.length > 0) stack[stack.length - 1].commas.push(k)
536
+ else if (c === '}' && stack.length > 0) {
537
+ const top = stack.pop()
538
+ if (top.dollar && text[top.open - 1] === '$' && marks[top.open - 1] === 'u') dollars--
539
+ if (!top.dollar && top.commas.length > 0 && (best === undefined || top.open < best[0])) best = [top.open, top.commas, k]
540
+ }
541
+ }
542
+ return best
543
+ }
544
+ const braceExpand = (text, marks) => {
545
+ const out = []
546
+ const todo = [[text, marks]]
547
+ while (todo.length > 0) {
548
+ const [t, m] = todo.pop()
549
+ if ((braceChars -= t.length) < 0) return undefined
550
+ const group = firstGroup(t, m)
551
+ if (group === undefined) {
552
+ out.push(t)
553
+ if (out.length > BRACE_WORDS) return undefined
554
+ continue
555
+ }
556
+ const [open, commas, close] = group
557
+ const cuts = [open, ...commas, close]
558
+ // Pushed in reverse, so the words come out in bash's left-to-right order.
559
+ for (let c = cuts.length - 2; c >= 0; c--) {
560
+ const from = cuts[c] + 1, to = cuts[c + 1]
561
+ todo.push([t.slice(0, open) + t.slice(from, to) + t.slice(close + 1), m.slice(0, open) + m.slice(from, to) + m.slice(close + 1)])
562
+ }
563
+ if (todo.length > BRACE_WORDS) return undefined
517
564
  }
518
- if (!inRm) inRm = word === 'rm' || word.endsWith('/rm')
519
- else if (!optionsEnded && word === '--') optionsEnded = true
520
- else if (!optionsEnded && word.startsWith('-')) {
565
+ return out
566
+ }
567
+ const takeWord = (w) => {
568
+ if (!inRm) inRm = w === 'rm' || w.endsWith('/rm')
569
+ else if (!optionsEnded && w === '--') optionsEnded = true
570
+ else if (!optionsEnded && w.startsWith('-')) {
521
571
  // GNU getopt accepts any unambiguous long-option prefix: --r, --recur, --forc.
522
- recursive = recursive || (word.length > 2 && '--recursive'.startsWith(word)) || /^-[a-z]*r[a-z]*$/.test(word)
523
- force = force || (word.length > 2 && '--force'.startsWith(word)) || /^-[a-z]*f[a-z]*$/.test(word)
524
- } else root = root || isRoot(word)
525
- word = ''; started = false
572
+ recursive = recursive || (w.length > 2 && '--recursive'.startsWith(w)) || (/^-[a-z]*$/.test(w) && w.includes('r'))
573
+ force = force || (w.length > 2 && '--force'.startsWith(w)) || (/^-[a-z]*$/.test(w) && w.includes('f'))
574
+ } else root = root || isRoot(w)
526
575
  return inRm && recursive && force && root
527
576
  }
577
+ const finishWord = () => {
578
+ if (!started) return false
579
+ const text = word, marks = mask
580
+ word = ''; mask = ''; started = false
581
+ // Literal shell strings (e.g. sh -c 'rm -rf /', or sh -c '{rm,-rf,/}') also carried the old guard.
582
+ // Bound rescanning to four levels; beyond that judge the string by its tokens.
583
+ if (/[\s;&|()\x60{]/.test(text)) {
584
+ if (depth < 4 ? hasRootDelete(text, depth + 1) : looseRootDelete(text)) return true
585
+ }
586
+ // An unquoted {a,b} becomes separate words, as bash expands it: rm -rf {/*,} is rm -rf /*.
587
+ const words = text.includes('{') ? braceExpand(text, marks) : [text]
588
+ if (words === undefined) {
589
+ // Too many alternatives to check: one of them may be the root (an rm in progress fails closed), or the
590
+ // list itself may spell the rm: judge its tokens.
591
+ if (inRm) root = true
592
+ if (looseRootDelete(text.replace(/[{},]/g, ' '))) return true
593
+ return takeWord(text)
594
+ }
595
+ for (const w of words) if (w !== '' && takeWord(w)) return true
596
+ return false
597
+ }
528
598
  // A command substitution's body, from just after its opening backtick. Bash removes a
529
599
  // backslash only before $, a backtick or a backslash (and " inside double quotes) and
530
600
  // drops backslash-newline; any other backslash stays for the nested scan.
@@ -538,7 +608,7 @@ const ROOT_DELETE_CHECK_SOURCE = String.raw `function hasRootDelete(command, dep
538
608
  }
539
609
  body += command[j]
540
610
  }
541
- return [j, depth < 4 ? hasRootDelete(body, depth + 1) : body.includes('rm -rf /')]
611
+ return [j, depth < 4 ? hasRootDelete(body, depth + 1) : looseRootDelete(body)]
542
612
  }
543
613
  const finishCommand = () => {
544
614
  const denied = inRm && recursive && force && root
@@ -555,8 +625,8 @@ const ROOT_DELETE_CHECK_SOURCE = String.raw `function hasRootDelete(command, dep
555
625
  else if (char === '\\' && i + 1 < command.length) {
556
626
  const [text, last] = ansiEscape(command, i + 1)
557
627
  i = last
558
- if (!ansiNul) { const nul = text.indexOf('\0'); if (nul < 0) word += text; else { word += text.slice(0, nul); ansiNul = true } }
559
- } else if (!ansiNul) word += char
628
+ if (!ansiNul) { const nul = text.indexOf('\0'); const kept = nul < 0 ? text : text.slice(0, nul); word += kept; mask += 'q'.repeat(kept.length); if (nul >= 0) ansiNul = true }
629
+ } else if (!ansiNul) { word += char; mask += 'q' }
560
630
  continue
561
631
  }
562
632
  if (char === quote) quote = ''
@@ -566,20 +636,20 @@ const ROOT_DELETE_CHECK_SOURCE = String.raw `function hasRootDelete(command, dep
566
636
  // The substitution is gone from the word, as bash's empty output is (a quoted empty substitution glued to -rf leaves -rf).
567
637
  // A # right after it is still inside the word: a placeholder keeps a rescan (sh -c "...")
568
638
  // from reading it as the start of a comment.
569
- if (command[end + 1] === '#') word += '\u0001'
639
+ if (command[end + 1] === '#') { word += '\u0001'; mask += 'q' }
570
640
  i = end
571
641
  }
572
642
  else if (quote === '"' && char === '\\' && i + 1 < command.length &&
573
643
  (command[i + 1] === '"' || command[i + 1] === '\\' || command[i + 1] === '$' ||
574
644
  command.charCodeAt(i + 1) === 96 || command[i + 1] === '\n')) {
575
645
  const next = command[++i]
576
- if (next !== '\n') word += next
577
- } else word += char
646
+ if (next !== '\n') { word += next; mask += 'q' }
647
+ } else { word += char; mask += 'q' }
578
648
  continue
579
649
  }
580
650
  if (char === '\\' && i + 1 < command.length) {
581
651
  const next = command[++i]
582
- if (next !== '\n') { word += next; started = true }
652
+ if (next !== '\n') { word += next; mask += 'q'; started = true }
583
653
  } else if (char === '\x60' && command.indexOf('\x60', i + 1) > i) {
584
654
  // Command substitution: scan the body as its own command; the substitution
585
655
  // stays inside the enclosing word, so the rm being parsed keeps its state,
@@ -602,10 +672,26 @@ const ROOT_DELETE_CHECK_SOURCE = String.raw `function hasRootDelete(command, dep
602
672
  redirect = char === '<' || char === '>'
603
673
  if (!redirectionAmpersand && (char === '\n' || ';|&()'.includes(char)) && finishCommand()) return true
604
674
  } else {
605
- word += char; started = true
675
+ word += char; mask += 'u'; started = true
606
676
  }
607
677
  }
608
678
  return finishWord() || finishCommand()
679
+ }
680
+
681
+ // A fork bomb's definition as bash reads it, whatever the spacing or the name: f(){ f|f& }, function f { f|f& },
682
+ // function f(){ f|f& }. Defining one is the danger: the call may come anywhere later.
683
+ function hasForkBomb(command) {
684
+ const s = command.replace(/\s+/g, '')
685
+ for (let brace = s.indexOf('{'); brace >= 0; brace = s.indexOf('{', brace + 1)) {
686
+ const end = s.startsWith('()', brace - 2) ? brace - 2 : brace
687
+ let start = end
688
+ while (start > 0 && end - start < 72 && /[a-z0-9_:.-]/.test(s[start - 1])) start--
689
+ const run = s.slice(start, end)
690
+ for (const name of run.startsWith('function') && run.length > 8 ? [run, run.slice(8)] : [run]) {
691
+ if (name && s.startsWith(name + '|' + name + '&}', brace + 1)) return true
692
+ }
693
+ }
694
+ return false
609
695
  }`;
610
696
  /**
611
697
  * Generate hook-handler.cjs (cross-platform hook dispatcher)
@@ -816,7 +902,7 @@ export function generateHookHandler() {
816
902
  " var dangerous = ['rm -rf /', 'format c:', 'del /s /q c:\\\\', ':(){:|:&};:'];",
817
903
  ROOT_DELETE_CHECK_SOURCE.split('\n').map((line) => ' ' + line).join('\n'),
818
904
  ' for (var i = 0; i < dangerous.length; i++) {',
819
- " if (dangerous[i] === 'rm -rf /' ? hasRootDelete(cmd) : cmd.includes(dangerous[i])) {",
905
+ " if (dangerous[i] === 'rm -rf /' ? hasRootDelete(cmd) : dangerous[i] === ':(){:|:&};:' ? hasForkBomb(cmd) : cmd.includes(dangerous[i])) {",
820
906
  " console.error('[BLOCKED] Dangerous command detected: ' + dangerous[i]);",
821
907
  ' // Claude Code PreToolUse: exit 2 blocks execution; exit 1 is non-blocking.',
822
908
  ' process.exit(2);',
@@ -76,7 +76,7 @@ export const DEFAULT_INIT_OPTIONS = {
76
76
  github: true,
77
77
  flowNexus: false,
78
78
  browser: true,
79
- v3: true,
79
+ v3: false, // ADR-128: opt-in like agents.v3; use --full or `init skills --v3`
80
80
  dualMode: false, // Optional: enable with --dual flag
81
81
  all: false,
82
82
  },
@@ -7,6 +7,7 @@
7
7
  * keeps the wire from agent_spawn → ProviderManager (real) in one
8
8
  * place rather than duplicated.
9
9
  */
10
+ import { type AgentExecutionSample } from './agent-health-metrics.js';
10
11
  type ClaudeModel = 'haiku' | 'sonnet' | 'opus' | 'opus-4.7' | 'inherit';
11
12
  export interface AgentRecord {
12
13
  agentId: string;
@@ -32,6 +33,7 @@ export interface AgentRecord {
32
33
  /** ADR-148 phase 2 — concrete OpenRouter slug when provider='openrouter'. */
33
34
  openrouterModel?: string;
34
35
  lastResult?: Record<string, unknown>;
36
+ executionHistory?: AgentExecutionSample[];
35
37
  }
36
38
  export declare const DEFAULT_ANTHROPIC_MODEL = "claude-sonnet-5";
37
39
  export declare function modelRejectsSamplingParams(model: string): boolean;
@@ -10,6 +10,7 @@
10
10
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
11
11
  import { join } from 'node:path';
12
12
  import { getProjectCwd } from './types.js';
13
+ import { appendAgentExecution, summarizeAgentExecutions } from './agent-health-metrics.js';
13
14
  import { configManager } from '../services/config-file-manager.js';
14
15
  const STORAGE_DIR = '.claude-flow';
15
16
  const AGENT_DIR = 'agents';
@@ -669,6 +670,20 @@ export async function executeAgentTask(input) {
669
670
  }
670
671
  if (out)
671
672
  current.lastResult = out;
673
+ const previousMeasuredHealth = summarizeAgentExecutions(current.executionHistory).health;
674
+ const mayUpdateStoredHealth = previousMeasuredHealth === null
675
+ ? current.health === agent.health
676
+ : current.health === previousMeasuredHealth;
677
+ current.executionHistory = appendAgentExecution(current.executionHistory, {
678
+ at: new Date().toISOString(),
679
+ success: result.success,
680
+ durationMs: result.durationMs ?? Date.now() - startedAt,
681
+ });
682
+ // An explicit agent_update during the provider call still wins. The
683
+ // measured value remains available from executionHistory in agent_health.
684
+ if (mayUpdateStoredHealth) {
685
+ current.health = summarizeAgentExecutions(current.executionHistory).health ?? current.health;
686
+ }
672
687
  saveAgentStore(latest);
673
688
  };
674
689
  if (result.success) {
@@ -0,0 +1,22 @@
1
+ /** Bounded execution evidence for agent_health. Registry records are not processes. */
2
+ export interface AgentExecutionSample {
3
+ at: string;
4
+ success: boolean;
5
+ durationMs: number;
6
+ }
7
+ export declare const AGENT_HEALTH_WINDOW = 20;
8
+ export declare function appendAgentExecution(value: unknown, sample: AgentExecutionSample): AgentExecutionSample[];
9
+ export declare function summarizeAgentExecutions(value: unknown): {
10
+ health: number | null;
11
+ errorRate: number | null;
12
+ sampleCount: number;
13
+ successes: number;
14
+ failures: number;
15
+ latency: {
16
+ avg: number | null;
17
+ p50: number | null;
18
+ p95: number | null;
19
+ p99: number | null;
20
+ };
21
+ };
22
+ //# sourceMappingURL=agent-health-metrics.d.ts.map
@@ -0,0 +1,33 @@
1
+ export const AGENT_HEALTH_WINDOW = 20;
2
+ function validSamples(value) {
3
+ if (!Array.isArray(value))
4
+ return [];
5
+ return value.filter((sample) => sample !== null && typeof sample === 'object' &&
6
+ typeof sample.at === 'string' && typeof sample.success === 'boolean' &&
7
+ typeof sample.durationMs === 'number' && Number.isFinite(sample.durationMs) && sample.durationMs >= 0).slice(-AGENT_HEALTH_WINDOW);
8
+ }
9
+ export function appendAgentExecution(value, sample) {
10
+ return [...validSamples(value), sample].slice(-AGENT_HEALTH_WINDOW);
11
+ }
12
+ export function summarizeAgentExecutions(value) {
13
+ const samples = validSamples(value);
14
+ const sampleCount = samples.length;
15
+ const successes = samples.filter(sample => sample.success).length;
16
+ const failures = sampleCount - successes;
17
+ const durations = samples.map(sample => sample.durationMs).sort((a, b) => a - b);
18
+ const percentile = (p) => sampleCount ? durations[Math.ceil(p * sampleCount) - 1] : null;
19
+ return {
20
+ health: sampleCount ? successes / sampleCount : null,
21
+ errorRate: sampleCount ? failures / sampleCount : null,
22
+ sampleCount,
23
+ successes,
24
+ failures,
25
+ latency: {
26
+ avg: sampleCount ? durations.reduce((sum, duration) => sum + duration, 0) / sampleCount : null,
27
+ p50: percentile(0.5),
28
+ p95: percentile(0.95),
29
+ p99: percentile(0.99),
30
+ },
31
+ };
32
+ }
33
+ //# sourceMappingURL=agent-health-metrics.js.map
@@ -10,6 +10,7 @@ import { readRecordStore } from './record-store.js';
10
10
  import { getProjectCwd } from './types.js';
11
11
  import { validateIdentifier, validateText, validateAgentSpawn } from './validate-input.js';
12
12
  import { executeAgentTask } from './agent-execute-core.js';
13
+ import { AGENT_HEALTH_WINDOW, summarizeAgentExecutions } from './agent-health-metrics.js';
13
14
  import { pheromoneAgentEligibility } from './swarm-tools.js';
14
15
  // Storage paths
15
16
  const STORAGE_DIR = '.claude-flow';
@@ -691,7 +692,7 @@ export const agentTools = [
691
692
  },
692
693
  {
693
694
  name: 'agent_health',
694
- description: 'Compute an agent\'s rolling health score (0-1) from recent task success ratio + response-latency p50/p95 + error rate. Use when native Task tool is wrong because you\'re running a long-lived agent (autonomous loop / hive-mind worker / federation peer) and need to detect degradation before the breaker trips it. For one-shot Task invocations there is no history to score. Pair with hooks_post-task so the scores stay current.',
695
+ description: 'Report measured agent_execute provider-call outcomes over the last 20 calls: API success-ratio health (0-1), error rate, and response-latency p50/p95. This does not measure task correctness. Agents with no observed calls have unknown health; registration is not proof of a running process. Native Task and hooks_post-task outcomes are not recorded here. Use when you need measured reliability for agents driven through agent_execute; native Task results are invisible to this tool, so inspect those directly.',
695
696
  category: 'agent',
696
697
  inputSchema: {
697
698
  type: 'object',
@@ -708,58 +709,80 @@ export const agentTools = [
708
709
  }
709
710
  const store = loadAgentStore();
710
711
  const agents = Object.values(store.agents).filter(a => a.status !== 'terminated');
711
- const threshold = input.threshold || 0.5;
712
+ const threshold = input.threshold === undefined ? 0.5 : input.threshold;
713
+ if (!Number.isFinite(threshold) || threshold < 0 || threshold > 1) {
714
+ return { error: 'threshold must be a finite number between 0 and 1' };
715
+ }
716
+ const healthStatus = (health) => health === null ? 'unknown' : health >= threshold ? 'healthy' : health >= 0.3 ? 'degraded' : 'unhealthy';
712
717
  if (input.agentId) {
713
718
  const agent = store.agents[input.agentId];
714
719
  if (agent) {
720
+ const metrics = summarizeAgentExecutions(agent.executionHistory);
715
721
  return {
716
722
  agentId: agent.agentId,
717
- health: agent.health,
723
+ ...metrics,
724
+ healthStatus: healthStatus(metrics.health),
718
725
  status: agent.status,
719
- healthy: agent.health >= threshold,
726
+ healthy: metrics.health === null ? null : metrics.health >= threshold,
720
727
  taskCount: agent.taskCount,
721
- uptime: Date.now() - new Date(agent.createdAt).getTime(),
728
+ uptime: null,
729
+ registryAgeMs: Date.now() - new Date(agent.createdAt).getTime(),
730
+ windowSize: AGENT_HEALTH_WINDOW,
731
+ source: 'agent_execute',
722
732
  };
723
733
  }
724
734
  return { agentId: input.agentId, error: 'Agent not found' };
725
735
  }
726
- const healthyAgents = agents.filter(a => a.health >= threshold);
727
- const degradedAgents = agents.filter(a => a.health >= 0.3 && a.health < threshold);
728
- const unhealthyAgents = agents.filter(a => a.health < 0.3);
729
- const avgHealth = agents.length > 0 ? agents.reduce((sum, a) => sum + a.health, 0) / agents.length : 1;
736
+ const observed = agents.map(agent => ({ agent, metrics: summarizeAgentExecutions(agent.executionHistory) }));
737
+ const healthyAgents = observed.filter(({ metrics }) => healthStatus(metrics.health) === 'healthy');
738
+ const degradedAgents = observed.filter(({ metrics }) => healthStatus(metrics.health) === 'degraded');
739
+ const unhealthyAgents = observed.filter(({ metrics }) => healthStatus(metrics.health) === 'unhealthy');
740
+ const measuredAgents = observed.filter(({ metrics }) => metrics.health !== null);
741
+ const unknownCount = agents.length - measuredAgents.length;
742
+ const avgHealth = measuredAgents.length > 0
743
+ ? measuredAgents.reduce((sum, { metrics }) => sum + metrics.health, 0) / measuredAgents.length
744
+ : null;
730
745
  return {
731
746
  // CLI expected fields
732
- agents: agents.map(a => {
733
- const uptime = Date.now() - new Date(a.createdAt).getTime();
747
+ agents: observed.map(({ agent, metrics }) => {
734
748
  return {
735
- id: a.agentId,
736
- type: a.agentType,
737
- health: a.health >= threshold ? 'healthy' : (a.health >= 0.3 ? 'degraded' : 'unhealthy'),
738
- uptime,
739
- tasks: { active: a.taskCount > 0 ? 1 : 0, queued: 0, completed: a.taskCount, failed: 0 },
740
- _note: 'Per-agent OS metrics not available — use system_metrics for real CPU/memory',
749
+ id: agent.agentId,
750
+ type: agent.agentType,
751
+ health: healthStatus(metrics.health),
752
+ score: metrics.health,
753
+ sampleCount: metrics.sampleCount,
754
+ errorRate: metrics.errorRate,
755
+ latency: metrics.latency,
756
+ errors: { count: metrics.failures },
757
+ uptime: null,
758
+ registryAgeMs: Date.now() - new Date(agent.createdAt).getTime(),
759
+ tasks: { active: agent.status === 'busy' ? 1 : 0, queued: null, completed: metrics.successes, failed: metrics.failures },
760
+ _note: `Provider-call outcomes and latency cover only the last ${AGENT_HEALTH_WINDOW} agent_execute calls; task correctness, process liveness and CPU/memory are not observed`,
741
761
  };
742
762
  }),
743
763
  overall: {
744
764
  healthy: healthyAgents.length,
745
765
  degraded: degradedAgents.length,
746
766
  unhealthy: unhealthyAgents.length,
767
+ unknown: unknownCount,
747
768
  cpu: null,
748
769
  memory: null,
749
770
  _note: 'Per-agent CPU/memory not available — use system_metrics for real OS-level stats',
750
- score: Math.round(avgHealth * 100),
771
+ score: avgHealth === null ? null : Math.round(avgHealth * 100),
751
772
  issues: unhealthyAgents.length,
752
773
  },
753
774
  // Additional fields
754
775
  total: agents.length,
776
+ observedCount: measuredAgents.length,
777
+ unknownCount,
755
778
  healthyCount: healthyAgents.length,
756
779
  unhealthyCount: unhealthyAgents.length,
757
780
  threshold,
758
781
  avgHealth,
759
- unhealthyAgents: unhealthyAgents.map(a => ({
760
- agentId: a.agentId,
761
- health: a.health,
762
- status: a.status,
782
+ unhealthyAgents: unhealthyAgents.map(({ agent, metrics }) => ({
783
+ agentId: agent.agentId,
784
+ health: metrics.health,
785
+ status: agent.status,
763
786
  })),
764
787
  };
765
788
  },
@@ -50,7 +50,7 @@
50
50
  *
51
51
  * @module @claude-flow/cli/mcp-tools/agentbbs-federation
52
52
  */
53
- import { type Server } from 'node:http';
53
+ import type { Server } from 'node:http';
54
54
  /** Max envelopes accepted from one peer in one sync. */
55
55
  export declare const MAX_ENVELOPES_PER_SYNC = 5000;
56
56
  /** Max serialized bytes for a single envelope. */
@@ -53,7 +53,12 @@
53
53
  import { existsSync, mkdirSync, readFileSync, writeFileSync, appendFileSync, chmodSync } from 'node:fs';
54
54
  import { join } from 'node:path';
55
55
  import { createHash, randomBytes, timingSafeEqual } from 'node:crypto';
56
- import { createServer } from 'node:http';
56
+ import { createRequire } from 'node:module';
57
+ // Lazy, CommonJS-style load of node:http: an ESM `import … from 'node:http'`
58
+ // materialises every export of the builtin, which trips http's lazy undici
59
+ // getters (~2.5 MB heap / ~6 MB RSS) at MCP startup even when no server is
60
+ // ever started. require() of the builtin does not touch those getters.
61
+ const loadHttp = () => createRequire(import.meta.url)('node:http');
57
62
  /** Max envelopes accepted from one peer in one sync. */
58
63
  export const MAX_ENVELOPES_PER_SYNC = 5_000;
59
64
  /** Max serialized bytes for a single envelope. */
@@ -389,7 +394,7 @@ export async function syncRoomFromPeer(basePath, peer, roomId, fetchImpl = fetch
389
394
  */
390
395
  export function serveFederation(basePath, opts = {}) {
391
396
  const host = opts.bindHost ?? '127.0.0.1';
392
- const server = createServer((req, res) => {
397
+ const server = loadHttp().createServer((req, res) => {
393
398
  const send = (code, obj) => {
394
399
  const buf = Buffer.from(JSON.stringify(obj));
395
400
  res.writeHead(code, { 'content-type': 'application/json', 'content-length': buf.length });