@claude-flow/cli 3.56.7 → 3.56.9

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 (35) hide show
  1. package/.claude/helpers/helpers.manifest.json +3 -3
  2. package/.claude/helpers/hook-handler.cjs +109 -23
  3. package/README.md +9 -3
  4. package/catalog-manifest.json +2 -2
  5. package/dist/src/commands/analyze.js +40 -29
  6. package/dist/src/commands/coverage-reader.d.ts +29 -0
  7. package/dist/src/commands/coverage-reader.js +247 -0
  8. package/dist/src/commands/hooks.js +9 -180
  9. package/dist/src/commands/tui.js +2 -2
  10. package/dist/src/init/helpers-generator.js +109 -23
  11. package/dist/src/parser.js +32 -22
  12. package/dist/src/production/monitoring.d.ts +1 -0
  13. package/dist/src/production/monitoring.js +19 -6
  14. package/dist/src/production/rate-limiter.js +5 -4
  15. package/dist/src/production/retry.js +3 -0
  16. package/dist/src/services/config-file-manager.js +3 -3
  17. package/dist/src/services/policy-ledger-anchor.js +20 -2
  18. package/node_modules/@claude-flow/codex/dist/cli.js +0 -0
  19. package/node_modules/@claude-flow/codex/dist/dual-mode/orchestrator.d.ts +1 -0
  20. package/node_modules/@claude-flow/codex/dist/dual-mode/orchestrator.d.ts.map +1 -1
  21. package/node_modules/@claude-flow/codex/dist/dual-mode/orchestrator.js +3 -1
  22. package/node_modules/@claude-flow/codex/dist/dual-mode/orchestrator.js.map +1 -1
  23. package/node_modules/@claude-flow/codex/dist/index.d.ts +3 -3
  24. package/node_modules/@claude-flow/codex/dist/index.js +1 -1
  25. package/node_modules/@claude-flow/codex/package.json +1 -1
  26. package/node_modules/@claude-flow/plugin-agent-federation/dist/bin.js +0 -0
  27. package/node_modules/@claude-flow/security/dist/policy/envelope.js +6 -6
  28. package/node_modules/@claude-flow/security/dist/policy/envelope.js.map +1 -1
  29. package/node_modules/@claude-flow/security/package.json +1 -1
  30. package/package.json +5 -5
  31. package/plugins/ruflo-console/dist/tui.mjs +171 -162
  32. package/plugins/ruflo-metaharness/.claude-plugin/plugin.json +1 -1
  33. package/plugins/ruflo-metaharness/CHANGELOG.md +3 -0
  34. package/plugins/ruflo-metaharness/hooks/screen.ts +10 -3
  35. package/plugins/ruflo-metaharness/scripts/smoke.sh +4 -4
@@ -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);',
@@ -134,7 +134,6 @@ export class CommandParser {
134
134
  // command, because that's what caused `daemon start` to resolve as `start`
135
135
  // with `daemon` left as a positional.
136
136
  let resolvedCmd;
137
- let resolvedSub;
138
137
  const resolvedCommands = [];
139
138
  let sawFirstPositional = false;
140
139
  for (let index = 0; index < args.length; index++) {
@@ -173,16 +172,16 @@ export class CommandParser {
173
172
  const child = parent.subcommands.find(sc => sc.name === arg || sc.aliases?.includes(arg));
174
173
  if (child) {
175
174
  resolvedCommands.push(child);
176
- resolvedSub ??= child;
177
175
  continue;
178
176
  }
179
177
  }
180
178
  // A positional that is not the next subcommand ends the command path.
181
179
  break;
182
180
  }
183
- const { aliases, booleanFlags, stringFlags } = this.getFlagScope(resolvedCommands);
181
+ const { aliases, booleanFlags, stringFlags, arrayFlags } = this.getFlagScope(resolvedCommands);
184
182
  let i = 0;
185
183
  let parsingFlags = true;
184
+ let commandSlotAvailable = true;
186
185
  while (i < args.length) {
187
186
  const arg = args[i];
188
187
  // Check for end of flags marker
@@ -194,8 +193,17 @@ export class CommandParser {
194
193
  // Handle flags
195
194
  if (parsingFlags && arg.startsWith('-')) {
196
195
  const parseResult = this.parseFlag(args, i, aliases, booleanFlags, stringFlags);
196
+ // Array declarations retain text and accumulate repeated occurrences.
197
+ for (const key of arrayFlags) {
198
+ const value = parseResult.flags[key];
199
+ if (value !== undefined && value !== true && value !== false) {
200
+ const previous = result.flags[key];
201
+ parseResult.flags[key] = [...(Array.isArray(previous) ? previous : []), String(value)];
202
+ }
203
+ }
197
204
  // Apply to result flags
198
- Object.assign(result.flags, parseResult.flags);
205
+ const { _: ignoredPositionals, ...parsedFlags } = parseResult.flags;
206
+ Object.assign(result.flags, parsedFlags);
199
207
  i = parseResult.nextIndex;
200
208
  continue;
201
209
  }
@@ -203,7 +211,9 @@ export class CommandParser {
203
211
  // Fix for #1596: treat lazy command names as commands here too so that
204
212
  // downstream dispatch sees `commandPath = ['daemon', 'start']` instead of
205
213
  // `commandPath = ['start'], positional = ['daemon']`.
206
- if (result.command.length === 0 && this.isKnownCommandName(arg)) {
214
+ const isCommandSlot = commandSlotAvailable && parsingFlags;
215
+ commandSlotAvailable = false;
216
+ if (isCommandSlot && this.isKnownCommandName(arg)) {
207
217
  // This is a command
208
218
  result.command.push(arg);
209
219
  // Check for subcommand (level 1) — only possible for sync commands
@@ -248,7 +258,7 @@ export class CommandParser {
248
258
  // `default: true` silently dropped and reached the action handler as
249
259
  // `undefined`. That trapped `memory store --upsert` and every other
250
260
  // subcommand that leaned on a per-flag default.
251
- this.applyDefaults(result.flags, resolvedCmd, resolvedSub);
261
+ this.applyDefaults(result.flags, resolvedCommands);
252
262
  return result;
253
263
  }
254
264
  parseFlag(args, index, aliases, booleanFlags, stringFlags) {
@@ -472,11 +482,18 @@ export class CommandParser {
472
482
  aliases[option.short] = option.name;
473
483
  }
474
484
  }
485
+ const arrayFlags = new Set((this.options.arrayFlags ?? []).map(flag => this.normalizeKey(flag)));
486
+ for (const flag of arrayFlags)
487
+ stringFlags.add(flag);
475
488
  const layers = [this.globalOptions, ...commands.map(command => command.options ?? [])];
476
489
  for (const options of layers) {
477
490
  for (const option of options) {
478
491
  const key = this.normalizeKey(option.name);
479
- if (option.type === 'string')
492
+ if (option.type === 'array')
493
+ arrayFlags.add(key);
494
+ else if (option.type)
495
+ arrayFlags.delete(key);
496
+ if (option.type === 'string' || option.type === 'array')
480
497
  stringFlags.add(key);
481
498
  else if (option.type)
482
499
  stringFlags.delete(key);
@@ -488,7 +505,7 @@ export class CommandParser {
488
505
  }
489
506
  for (const flag of stringFlags)
490
507
  booleanFlags.delete(flag);
491
- return { aliases, booleanFlags, stringFlags };
508
+ return { aliases, booleanFlags, stringFlags, arrayFlags };
492
509
  }
493
510
  getBooleanFlags() {
494
511
  const flags = new Set();
@@ -526,23 +543,16 @@ export class CommandParser {
526
543
  }
527
544
  return flags;
528
545
  }
529
- applyDefaults(flags, command, subcommand) {
530
- // #2775: apply defaults from globals AND the resolved command/subcommand.
531
- // Subcommand > command > global (later writes lose to earlier — because
532
- // we only set when `undefined`, so the FIRST option definition that
533
- // supplies a default wins; walk narrow-to-broad so subcommand options
534
- // apply before broader ones do).
535
- const layers = [];
536
- if (subcommand?.options)
537
- layers.push(subcommand.options);
538
- if (command?.options)
539
- layers.push(command.options);
546
+ applyDefaults(flags, commands) {
547
+ // Walk every resolved command from narrow to broad so nested defaults
548
+ // have the same precedence as their declared flag types.
549
+ const layers = [...commands].reverse().map(command => command.options ?? []);
540
550
  layers.push(this.globalOptions);
541
551
  for (const layer of layers) {
542
552
  for (const opt of layer) {
543
553
  const key = this.normalizeKey(opt.name);
544
554
  if (flags[key] === undefined && opt.default !== undefined) {
545
- flags[key] = opt.default;
555
+ flags[key] = (Array.isArray(opt.default) ? [...opt.default] : opt.default);
546
556
  }
547
557
  }
548
558
  }
@@ -551,7 +561,7 @@ export class CommandParser {
551
561
  for (const [key, value] of Object.entries(this.options.defaults)) {
552
562
  const normalizedKey = this.normalizeKey(key);
553
563
  if (flags[normalizedKey] === undefined) {
554
- flags[normalizedKey] = value;
564
+ flags[normalizedKey] = (Array.isArray(value) ? [...value] : value);
555
565
  }
556
566
  }
557
567
  }
@@ -572,7 +582,7 @@ export class CommandParser {
572
582
  // followed by another flag) is parsed as boolean `true`. Commands then treat `true` as
573
583
  // the value: `hooks route --task` crashed with "task.trim is not a function" and
574
584
  // `memory retrieve --key` looked up the key "true". Report it instead.
575
- if ((opt.type === 'string' || opt.type === 'number') && flags[key] === true) {
585
+ if ((opt.type === 'string' || opt.type === 'number' || opt.type === 'array') && flags[key] === true) {
576
586
  errors.push(`Option --${opt.name} needs a value`);
577
587
  }
578
588
  // Check choices
@@ -71,6 +71,7 @@ export declare class MonitoringHooks {
71
71
  private healthStatus;
72
72
  private startTime;
73
73
  private activeRequests;
74
+ private requestGeneration;
74
75
  private requestCount;
75
76
  private errorCount;
76
77
  private healthChecks;
@@ -37,6 +37,7 @@ export class MonitoringHooks {
37
37
  healthStatus;
38
38
  startTime = Date.now();
39
39
  activeRequests = 0;
40
+ requestGeneration = 0;
40
41
  requestCount = 0;
41
42
  errorCount = 0;
42
43
  healthChecks = new Map();
@@ -101,9 +102,14 @@ export class MonitoringHooks {
101
102
  this.activeRequests++;
102
103
  this.requestCount++;
103
104
  const startTime = Date.now();
105
+ const generation = this.requestGeneration;
106
+ let completed = false;
104
107
  this.counter('request_started', 1, { requestId });
105
108
  // Return end function
106
109
  return () => {
110
+ if (completed || generation !== this.requestGeneration)
111
+ return;
112
+ completed = true;
107
113
  this.activeRequests--;
108
114
  const duration = Date.now() - startTime;
109
115
  this.responseTimes.push(duration);
@@ -156,7 +162,7 @@ export class MonitoringHooks {
156
162
  catch (error) {
157
163
  checks[name] = {
158
164
  status: 'unhealthy',
159
- message: error.message,
165
+ message: error instanceof Error ? error.message : String(error),
160
166
  lastCheck: Date.now(),
161
167
  responseTimeMs: Date.now() - startTime,
162
168
  };
@@ -168,13 +174,13 @@ export class MonitoringHooks {
168
174
  checks,
169
175
  timestamp: Date.now(),
170
176
  };
171
- return this.healthStatus;
177
+ return structuredClone(this.healthStatus);
172
178
  }
173
179
  /**
174
180
  * Get current health status
175
181
  */
176
182
  getHealthStatus() {
177
- return this.healthStatus;
183
+ return structuredClone(this.healthStatus);
178
184
  }
179
185
  // ============================================================================
180
186
  // Performance Metrics
@@ -217,7 +223,7 @@ export class MonitoringHooks {
217
223
  if (level) {
218
224
  filtered = filtered.filter(a => a.level === level);
219
225
  }
220
- return filtered;
226
+ return filtered.map(alert => ({ ...alert }));
221
227
  }
222
228
  /**
223
229
  * Acknowledge an alert
@@ -248,7 +254,7 @@ export class MonitoringHooks {
248
254
  if (since) {
249
255
  filtered = filtered.filter(m => m.timestamp >= since);
250
256
  }
251
- return filtered;
257
+ return filtered.map(metric => ({ ...metric, labels: { ...metric.labels } }));
252
258
  }
253
259
  /**
254
260
  * Get all metrics summary
@@ -278,6 +284,7 @@ export class MonitoringHooks {
278
284
  * Reset all metrics
279
285
  */
280
286
  reset() {
287
+ this.requestGeneration++;
281
288
  this.metrics = [];
282
289
  this.responseTimes = [];
283
290
  this.alerts = [];
@@ -308,7 +315,13 @@ export class MonitoringHooks {
308
315
  const recentAlert = this.alerts.find(a => a.metric === name &&
309
316
  !a.acknowledged &&
310
317
  Date.now() - a.timestamp < 60000);
311
- if (!recentAlert) {
318
+ if (recentAlert && level === 'critical' && recentAlert.level !== 'critical') {
319
+ Object.assign(recentAlert, {
320
+ level, value, threshold, timestamp: Date.now(),
321
+ message: `${name} exceeded ${level} threshold: ${value} >= ${threshold}`,
322
+ });
323
+ }
324
+ else if (!recentAlert) {
312
325
  this.alerts.push({
313
326
  id: `alert_${Date.now()}_${Math.random().toString(36).substring(7)}`,
314
327
  level,
@@ -90,6 +90,7 @@ export class RateLimiter {
90
90
  */
91
91
  getStatus(operation, userId) {
92
92
  const limits = this.getLimits(operation);
93
+ const capacity = Math.floor(limits.maxRequests * this.config.burstMultiplier);
93
94
  const bucketKey = userId && this.config.perUserLimits
94
95
  ? `${operation}:${userId}`
95
96
  : `global:${operation}`;
@@ -98,8 +99,8 @@ export class RateLimiter {
98
99
  if (!bucket) {
99
100
  return {
100
101
  current: 0,
101
- limit: limits.maxRequests,
102
- remaining: limits.maxRequests,
102
+ limit: capacity,
103
+ remaining: capacity,
103
104
  resetAt: now + limits.windowMs,
104
105
  };
105
106
  }
@@ -107,8 +108,8 @@ export class RateLimiter {
107
108
  const validRequests = bucket.requests.filter(t => t > now - limits.windowMs);
108
109
  return {
109
110
  current: validRequests.length,
110
- limit: limits.maxRequests,
111
- remaining: Math.max(0, limits.maxRequests - validRequests.length),
111
+ limit: capacity,
112
+ remaining: Math.max(0, capacity - validRequests.length),
112
113
  resetAt: validRequests.length > 0
113
114
  ? validRequests[0] + limits.windowMs
114
115
  : now + limits.windowMs,
@@ -74,6 +74,9 @@ function fibonacci(n) {
74
74
  * Check if an error should be retried
75
75
  */
76
76
  function shouldRetryError(error, attempt, config) {
77
+ // Exhaustion always takes precedence over custom retry policies.
78
+ if (attempt >= config.maxAttempts)
79
+ return false;
77
80
  // Check custom retry function
78
81
  if (config.shouldRetry) {
79
82
  return config.shouldRetry(error, attempt);
@@ -123,16 +123,16 @@ export class ConfigFileManager {
123
123
  this.selectProject(cwd);
124
124
  // A targeted update must not persist unrelated defaults (notably the
125
125
  // default memory path, which would relocate an existing memory store).
126
- const config = this.config ?? this.load(cwd) ?? {};
126
+ const config = structuredClone(this.config ?? this.load(cwd) ?? {});
127
127
  if (Object.hasOwn(config, key)) {
128
128
  config[key] = value;
129
129
  }
130
130
  else {
131
131
  setNestedValue(config, key, value);
132
132
  }
133
- this.config = config;
134
133
  const targetPath = this.configPath ?? path.resolve(cwd, CONFIG_FILENAMES[0]);
135
134
  this.writeAtomic(targetPath, config);
135
+ this.config = config;
136
136
  this.configPath = targetPath;
137
137
  }
138
138
  /** Create a new config file with defaults */
@@ -271,7 +271,7 @@ export function parseConfigValue(value) {
271
271
  return parseFloat(value);
272
272
  try {
273
273
  const parsed = JSON.parse(value);
274
- if (typeof parsed === 'object')
274
+ if (typeof parsed === 'object' || (typeof parsed === 'number' && Number.isFinite(parsed)))
275
275
  return parsed;
276
276
  }
277
277
  catch { /* not JSON, use as string */ }
@@ -14,10 +14,28 @@
14
14
  import { createHash } from 'node:crypto';
15
15
  import { existsSync, mkdirSync, readFileSync, realpathSync } from 'node:fs';
16
16
  import { dirname, isAbsolute, join, resolve } from 'node:path';
17
- import { homedir } from 'node:os';
17
+ import { homedir, userInfo } from 'node:os';
18
18
  const LOG_FILE = 'ledger-anchors.json';
19
19
  const MIRROR_FILE = 'ledger-anchor-head.json';
20
20
  export const anchorLogPath = (projectRoot) => join(resolve(projectRoot), '.claude-flow', 'policy', LOG_FILE);
21
+ /**
22
+ * The home directory trust material lives under: $HOME when it is an absolute path, else the account's home. An empty or relative
23
+ * $HOME (an unset variable in a wrapper script, `HOME=. cmd`) would make join() yield a path relative to the working directory and
24
+ * drop the HMAC key and anchor mirror into the project; that is never wanted, so it falls back to the account home or refuses.
25
+ */
26
+ function trustHome() {
27
+ const home = homedir();
28
+ if (home && isAbsolute(home))
29
+ return home;
30
+ let account = '';
31
+ try {
32
+ account = userInfo().homedir;
33
+ }
34
+ catch { /* no account entry for this uid */ }
35
+ if (account && isAbsolute(account))
36
+ return account;
37
+ throw new Error('policy-trust-home-unresolved: $HOME is empty or relative and this account has no home directory; set HOME to an absolute path');
38
+ }
21
39
  /**
22
40
  * Directory holding per-project trust material (anchor mirror, HMAC key).
23
41
  * Resolved from the environment at call time (#3919): $XDG_CONFIG_HOME when it
@@ -26,7 +44,7 @@ export const anchorLogPath = (projectRoot) => join(resolve(projectRoot), '.claud
26
44
  * user who already has trust state under the legacy ~/.config keeps using it.
27
45
  */
28
46
  export function policyTrustRoot() {
29
- const legacy = join(homedir(), '.config', 'ruflo', 'policy-trust');
47
+ const legacy = join(trustHome(), '.config', 'ruflo', 'policy-trust');
30
48
  const xdg = process.env.XDG_CONFIG_HOME;
31
49
  if (!xdg || !isAbsolute(xdg))
32
50
  return legacy;
File without changes
@@ -7,6 +7,7 @@ export interface WorkerCapabilityEnvelope {
7
7
  actions?: string[];
8
8
  resources?: string[];
9
9
  tools?: string[];
10
+ /** Opt-in per-tool ceiling: requires a trusted concurrency meter at dispatch. */
10
11
  maxConcurrency?: number;
11
12
  network?: boolean;
12
13
  destructive?: boolean;
@@ -1 +1 @@
1
- {"version":3,"file":"orchestrator.d.ts","sourceRoot":"","sources":["../../src/dual-mode/orchestrator.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"orchestrator.d.ts","sourceRoot":"","sources":["../../src/dual-mode/orchestrator.ts"],"names":[],"mappings":"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"}
@@ -434,7 +434,9 @@ Remember: Other agents depend on your results in shared memory. Be concise and s
434
434
  actions: ['*'],
435
435
  resources: ['*'],
436
436
  tools: ['*'],
437
- maxConcurrency: 1,
437
+ // MCP dispatch does not yet supply a worker-wide concurrency meter.
438
+ // Leave this dimension unconstrained unless the caller opts in; the
439
+ // scheduler still bounds worker processes with config.maxConcurrent.
438
440
  network: false,
439
441
  destructive: false,
440
442
  delegationDepth: 0,