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.
- package/.claude/.proven-config-version +1 -0
- package/.claude/helpers/hook-handler.cjs +109 -23
- 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/codex/dist/dual-mode/orchestrator.d.ts +1 -0
- package/node_modules/@claude-flow/codex/dist/dual-mode/orchestrator.d.ts.map +1 -1
- package/node_modules/@claude-flow/codex/dist/dual-mode/orchestrator.js +3 -1
- package/node_modules/@claude-flow/codex/dist/dual-mode/orchestrator.js.map +1 -1
- package/node_modules/@claude-flow/codex/dist/index.d.ts +3 -3
- package/node_modules/@claude-flow/codex/dist/index.js +1 -1
- package/node_modules/@claude-flow/codex/package.json +1 -1
- package/node_modules/@claude-flow/plugin-agent-federation/dist/bin.js +0 -0
- package/node_modules/@claude-flow/security/dist/input-validator.d.ts +6 -6
- package/node_modules/@claude-flow/security/dist/policy/envelope.js +6 -6
- package/node_modules/@claude-flow/security/dist/policy/envelope.js.map +1 -1
- package/node_modules/@claude-flow/security/package.json +1 -1
- package/package.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/analyze.js +40 -29
- package/v3/@claude-flow/cli/dist/src/commands/claims.js +6 -8
- package/v3/@claude-flow/cli/dist/src/commands/daemon.js +86 -31
- package/v3/@claude-flow/cli/dist/src/commands/memory.js +14 -2
- 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 +6 -42
- package/v3/@claude-flow/cli/dist/src/init/helpers-generator.js +109 -23
- 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.js +70 -20
- package/v3/@claude-flow/cli/dist/src/mcp-tools/neural-tools.js +28 -4
- 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 +105 -29
- 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/mods/command-registry/schema.d.ts +24 -24
- package/v3/@claude-flow/cli/dist/src/parser.js +32 -22
- package/v3/@claude-flow/cli/dist/src/production/monitoring.d.ts +1 -0
- package/v3/@claude-flow/cli/dist/src/production/monitoring.js +19 -6
- package/v3/@claude-flow/cli/dist/src/production/rate-limiter.js +5 -4
- package/v3/@claude-flow/cli/dist/src/production/retry.js +3 -0
- 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/config-file-manager.js +3 -3
- package/v3/@claude-flow/cli/dist/src/services/policy-runtime.d.ts +1 -1
- package/v3/@claude-flow/cli/dist/src/services/policy-runtime.js +7 -1
- 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 +7 -7
- package/v3/@claude-flow/shared/dist/resilience/rate-limiter.js +3 -3
- package/v3/@claude-flow/shared/dist/resilience/retry.js +3 -1
- 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 (!
|
|
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 (!
|
|
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
|
-
|
|
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
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
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
|
-
|
|
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
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
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
|
-
|
|
519
|
-
|
|
520
|
-
|
|
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 || (
|
|
523
|
-
force = force || (
|
|
524
|
-
} else root = root || isRoot(
|
|
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
|
|
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');
|
|
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:
|
|
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: '
|
|
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
|
|
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
|
-
|
|
723
|
+
...metrics,
|
|
724
|
+
healthStatus: healthStatus(metrics.health),
|
|
718
725
|
status: agent.status,
|
|
719
|
-
healthy:
|
|
726
|
+
healthy: metrics.health === null ? null : metrics.health >= threshold,
|
|
720
727
|
taskCount: agent.taskCount,
|
|
721
|
-
uptime:
|
|
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
|
|
727
|
-
const
|
|
728
|
-
const
|
|
729
|
-
const
|
|
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:
|
|
733
|
-
const uptime = Date.now() - new Date(a.createdAt).getTime();
|
|
747
|
+
agents: observed.map(({ agent, metrics }) => {
|
|
734
748
|
return {
|
|
735
|
-
id:
|
|
736
|
-
type:
|
|
737
|
-
health:
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
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(
|
|
760
|
-
agentId:
|
|
761
|
-
health:
|
|
762
|
-
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 {
|
|
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 {
|
|
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 });
|