@claude-flow/cli 3.56.8 → 3.56.10

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. package/.claude/helpers/.helpers-version +1 -1
  2. package/.claude/helpers/helpers.manifest.json +4 -4
  3. package/.claude/helpers/hook-handler.cjs +109 -23
  4. package/.claude/helpers/intelligence.cjs +51 -9
  5. package/.claude/helpers/router.cjs +0 -0
  6. package/.claude/skills/verification-quality/SKILL.md +14 -11
  7. package/README.md +1 -1
  8. package/catalog-manifest.json +2 -2
  9. package/dist/src/autopilot-state.d.ts +2 -0
  10. package/dist/src/autopilot-state.js +36 -8
  11. package/dist/src/command-resolution.d.ts +15 -0
  12. package/dist/src/command-resolution.js +25 -0
  13. package/dist/src/commands/agent.js +17 -17
  14. package/dist/src/commands/analyze.js +40 -29
  15. package/dist/src/commands/claims.js +6 -8
  16. package/dist/src/commands/daemon.js +86 -31
  17. package/dist/src/commands/memory.js +14 -2
  18. package/dist/src/commands/security-code-patterns.d.ts +2 -2
  19. package/dist/src/commands/security-code-patterns.js +61 -3
  20. package/dist/src/commands/security.d.ts +24 -0
  21. package/dist/src/commands/security.js +81 -35
  22. package/dist/src/commands/tui.d.ts +9 -0
  23. package/dist/src/commands/tui.js +29 -3
  24. package/dist/src/config/proven-config-refresh.d.ts +9 -0
  25. package/dist/src/config/proven-config-refresh.js +24 -3
  26. package/dist/src/funnel/local-signals.js +2 -1
  27. package/dist/src/index.js +6 -42
  28. package/dist/src/init/helpers-generator.js +109 -23
  29. package/dist/src/init/types.js +1 -1
  30. package/dist/src/mcp-tools/agent-execute-core.d.ts +2 -0
  31. package/dist/src/mcp-tools/agent-execute-core.js +15 -0
  32. package/dist/src/mcp-tools/agent-health-metrics.d.ts +22 -0
  33. package/dist/src/mcp-tools/agent-health-metrics.js +33 -0
  34. package/dist/src/mcp-tools/agent-tools.js +45 -22
  35. package/dist/src/mcp-tools/agentbbs-federation.d.ts +1 -1
  36. package/dist/src/mcp-tools/agentbbs-federation.js +7 -2
  37. package/dist/src/mcp-tools/agentdb-tools.js +25 -2
  38. package/dist/src/mcp-tools/aidefence-known-misses.d.ts +22 -0
  39. package/dist/src/mcp-tools/aidefence-known-misses.js +57 -0
  40. package/dist/src/mcp-tools/analyze-tools.js +10 -11
  41. package/dist/src/mcp-tools/autopilot-tools.js +2 -2
  42. package/dist/src/mcp-tools/browser-intent-tools.js +6 -2
  43. package/dist/src/mcp-tools/embeddings-tools.js +29 -7
  44. package/dist/src/mcp-tools/hooks-tools.js +4 -0
  45. package/dist/src/mcp-tools/memory-tools.js +70 -20
  46. package/dist/src/mcp-tools/neural-tools.js +28 -4
  47. package/dist/src/mcp-tools/ruvllm-tools.js +6 -0
  48. package/dist/src/mcp-tools/security-tools.js +55 -17
  49. package/dist/src/mcp-tools/workflow-tools.js +66 -17
  50. package/dist/src/memory/intelligence.d.ts +5 -1
  51. package/dist/src/memory/intelligence.js +105 -29
  52. package/dist/src/memory/memory-bridge.js +10 -3
  53. package/dist/src/memory/memory-initializer.js +2 -1
  54. package/dist/src/memory/search-defaults.d.ts +54 -0
  55. package/dist/src/memory/search-defaults.js +37 -0
  56. package/dist/src/memory/sona-optimizer.d.ts +28 -0
  57. package/dist/src/memory/sona-optimizer.js +59 -2
  58. package/dist/src/mods/command-registry/schema.d.ts +24 -24
  59. package/dist/src/parser.js +32 -22
  60. package/dist/src/production/monitoring.d.ts +1 -0
  61. package/dist/src/production/monitoring.js +19 -6
  62. package/dist/src/production/rate-limiter.js +5 -4
  63. package/dist/src/production/retry.js +3 -0
  64. package/dist/src/ruvector/agent-wasm.d.ts +10 -0
  65. package/dist/src/ruvector/agent-wasm.js +27 -1
  66. package/dist/src/ruvector/diff-classifier.d.ts +2 -4
  67. package/dist/src/ruvector/diff-classifier.js +202 -69
  68. package/dist/src/ruvector/ruvllm-wasm.d.ts +4 -0
  69. package/dist/src/ruvector/ruvllm-wasm.js +12 -1
  70. package/dist/src/services/config-file-manager.js +3 -3
  71. package/dist/src/services/policy-runtime.d.ts +1 -1
  72. package/dist/src/services/policy-runtime.js +7 -1
  73. package/dist/src/services/worker-daemon.d.ts +11 -0
  74. package/dist/src/services/worker-daemon.js +37 -0
  75. package/dist/src/transfer/store/discovery.js +16 -17
  76. package/dist/src/transfer/store/types.d.ts +4 -0
  77. package/node_modules/@claude-flow/codex/dist/cli.js +0 -0
  78. package/node_modules/@claude-flow/codex/dist/dual-mode/orchestrator.d.ts +1 -0
  79. package/node_modules/@claude-flow/codex/dist/dual-mode/orchestrator.d.ts.map +1 -1
  80. package/node_modules/@claude-flow/codex/dist/dual-mode/orchestrator.js +3 -1
  81. package/node_modules/@claude-flow/codex/dist/dual-mode/orchestrator.js.map +1 -1
  82. package/node_modules/@claude-flow/codex/dist/index.d.ts +3 -3
  83. package/node_modules/@claude-flow/codex/dist/index.js +1 -1
  84. package/node_modules/@claude-flow/codex/package.json +1 -1
  85. package/node_modules/@claude-flow/plugin-agent-federation/dist/bin.js +0 -0
  86. package/node_modules/@claude-flow/security/dist/input-validator.d.ts +6 -6
  87. package/node_modules/@claude-flow/security/dist/policy/envelope.js +6 -6
  88. package/node_modules/@claude-flow/security/dist/policy/envelope.js.map +1 -1
  89. package/node_modules/@claude-flow/security/package.json +1 -1
  90. package/package.json +7 -7
  91. package/plugins/ruflo-console/dist/tui.mjs +162 -160
  92. package/plugins/ruflo-metaharness/.claude-flow/daemon-state.json +179 -0
  93. package/plugins/ruflo-metaharness/.claude-flow/daemon.pid +1 -0
  94. package/plugins/ruflo-metaharness/.claude-flow/logs/daemon.log +32 -0
  95. package/plugins/ruflo-metaharness/.claude-flow/metrics/backup.json +23 -0
  96. package/plugins/ruflo-metaharness/.claude-flow/metrics/codebase-map.json +11 -0
  97. package/plugins/ruflo-metaharness/.claude-flow/metrics/consolidation.json +16 -0
  98. package/plugins/ruflo-metaharness/.claude-flow/metrics/harness-loop.json +91 -0
  99. package/plugins/ruflo-metaharness/.claude-flow/metrics/performance.json +17 -0
  100. package/plugins/ruflo-metaharness/.claude-flow/metrics/security-audit.json +12 -0
  101. package/plugins/ruflo-metaharness/.claude-flow/metrics/test-gaps.json +8 -0
  102. package/plugins/ruflo-metaharness/.claude-flow/neural/patterns.json +2809 -0
  103. package/plugins/ruflo-metaharness/.claude-flow/neural/stats.json +6 -0
  104. package/plugins/ruflo-metaharness/.claude-flow/policy/state.json +11 -0
  105. package/plugins/ruflo-metaharness/.claude-plugin/plugin.json +1 -1
  106. package/plugins/ruflo-metaharness/.swarm/agentdb-memory.db +0 -0
  107. package/plugins/ruflo-metaharness/.swarm/agentdb-memory.db-shm +0 -0
  108. package/plugins/ruflo-metaharness/.swarm/agentdb-memory.db-wal +0 -0
  109. package/plugins/ruflo-metaharness/.swarm/backups/memory-2026-10-14T16-09-38-164Z.db +0 -0
  110. package/plugins/ruflo-metaharness/.swarm/backups/store-agentdb-memory.db-2026-10-14T16-09-38-166Z.db +0 -0
  111. package/plugins/ruflo-metaharness/.swarm/ewc-fisher.json +397 -0
  112. package/plugins/ruflo-metaharness/.swarm/memory.db +0 -0
  113. package/plugins/ruflo-metaharness/.swarm/memory.db-shm +0 -0
  114. package/plugins/ruflo-metaharness/.swarm/memory.db-wal +0 -0
  115. package/plugins/ruflo-metaharness/CHANGELOG.md +3 -0
  116. package/plugins/ruflo-metaharness/agentdb.rvf +0 -0
  117. package/plugins/ruflo-metaharness/agentdb.rvf.lock +0 -0
  118. package/plugins/ruflo-metaharness/hooks/screen.ts +10 -3
  119. package/plugins/ruflo-metaharness/ruvector.db +0 -0
  120. package/plugins/ruflo-metaharness/scripts/smoke.sh +4 -4
@@ -155,6 +155,7 @@ const scanCommand = {
155
155
  spinner.start();
156
156
  const findings = [];
157
157
  let criticalCount = 0, highCount = 0, mediumCount = 0, lowCount = 0;
158
+ let depsSkipped;
158
159
  try {
159
160
  const fs = await import('fs');
160
161
  const path = await import('path');
@@ -178,35 +179,41 @@ const scanCommand = {
178
179
  // npm audit exits non-zero when vulnerabilities found — stdout still has JSON
179
180
  auditResult = (auditErr instanceof Error && 'stdout' in auditErr ? auditErr.stdout : undefined) || '{}';
180
181
  }
181
- try {
182
- const audit = JSON.parse(auditResult);
183
- if (audit.vulnerabilities) {
184
- for (const [pkg, vuln] of Object.entries(audit.vulnerabilities)) {
185
- const sev = vuln.severity || 'low';
186
- const title = Array.isArray(vuln.via) && vuln.via[0]?.title ? vuln.via[0].title : 'Vulnerability';
187
- if (sev === 'critical')
188
- criticalCount++;
189
- else if (sev === 'high')
190
- highCount++;
191
- else if (sev === 'moderate' || sev === 'medium')
192
- mediumCount++;
193
- else
194
- lowCount++;
195
- findings.push({
196
- severity: sev === 'critical' ? 'critical' :
197
- sev === 'high' ? 'high' :
198
- sev === 'moderate' || sev === 'medium' ? 'medium' : 'low',
199
- type: 'Dependency CVE',
200
- location: `package.json:${pkg}`,
201
- description: title.substring(0, 35),
202
- });
203
- }
182
+ const audit = interpretNpmAudit(auditResult, path.resolve(target));
183
+ if (!audit.ok) {
184
+ depsSkipped = audit.reason;
185
+ }
186
+ else {
187
+ for (const [pkg, vuln] of Object.entries(audit.vulnerabilities)) {
188
+ const sev = vuln.severity || 'low';
189
+ const first = Array.isArray(vuln.via) ? vuln.via[0] : undefined;
190
+ const title = typeof first === 'object' && first?.title ? first.title : 'Vulnerability';
191
+ if (sev === 'critical')
192
+ criticalCount++;
193
+ else if (sev === 'high')
194
+ highCount++;
195
+ else if (sev === 'moderate' || sev === 'medium')
196
+ mediumCount++;
197
+ else
198
+ lowCount++;
199
+ findings.push({
200
+ severity: sev === 'critical' ? 'critical' :
201
+ sev === 'high' ? 'high' :
202
+ sev === 'moderate' || sev === 'medium' ? 'medium' : 'low',
203
+ type: 'Dependency CVE',
204
+ location: `package.json:${pkg}`,
205
+ description: title.substring(0, 35),
206
+ });
204
207
  }
205
208
  }
206
- catch { /* JSON parse failed, no vulns */ }
207
209
  }
210
+ else {
211
+ depsSkipped = 'No package.json found in the scan target; dependency scan could not run.';
212
+ }
213
+ }
214
+ catch (err) {
215
+ depsSkipped = `npm audit could not run: ${err instanceof Error ? err.message : String(err)}`;
208
216
  }
209
- catch { /* npm audit failed */ }
210
217
  }
211
218
  // Phase 2: Scan for hardcoded secrets
212
219
  if (scanType === 'all' || scanType === 'code') {
@@ -342,9 +349,14 @@ const scanCommand = {
342
349
  output.writeln(output.dim(`... and ${findings.length - 20} more issues`));
343
350
  }
344
351
  }
352
+ else if (depsSkipped) {
353
+ output.writeln(output.warning('No issues found by the checks that ran, but the dependency scan did not run.'));
354
+ }
345
355
  else {
346
356
  output.writeln(output.success('No security issues found!'));
347
357
  }
358
+ if (depsSkipped)
359
+ output.writeln(output.warning(`Dependency scan skipped: ${depsSkipped}`));
348
360
  output.writeln();
349
361
  output.printBox([
350
362
  `Target: ${target}`,
@@ -367,6 +379,7 @@ const scanCommand = {
367
379
  low: lowCount,
368
380
  total: findings.length,
369
381
  },
382
+ ...(depsSkipped ? { incomplete: true, skipped: { deps: depsSkipped } } : {}),
370
383
  findings,
371
384
  };
372
385
  // Persist the scan result so downstream consumers (the statusline's
@@ -408,6 +421,11 @@ const scanCommand = {
408
421
  output.printJson(record);
409
422
  else if (outputFormat === 'sarif')
410
423
  output.printJson(scanSarif(findings));
424
+ if (depsSkipped) {
425
+ if (outputFormat !== 'text')
426
+ output.printError(`Dependency scan skipped: ${depsSkipped}`);
427
+ return { success: false, exitCode: 2 };
428
+ }
411
429
  return { success: findings.length === 0 || (criticalCount === 0 && highCount === 0) };
412
430
  }
413
431
  catch (error) {
@@ -418,6 +436,36 @@ const scanCommand = {
418
436
  }
419
437
  },
420
438
  };
439
+ export function interpretNpmAudit(raw, dir) {
440
+ let audit;
441
+ try {
442
+ audit = JSON.parse(raw);
443
+ }
444
+ catch {
445
+ return { ok: false, reason: 'npm audit did not return a JSON report.' };
446
+ }
447
+ if (audit && typeof audit === 'object' && audit.error) {
448
+ const code = typeof audit.error.code === 'string' ? audit.error.code : 'error';
449
+ const summary = typeof audit.error.summary === 'string' ? audit.error.summary : 'npm audit failed.';
450
+ return { ok: false, reason: `npm audit failed (${code}): ${summary}${code === 'ENOLOCK' ? auditHint(dir) : ''}` };
451
+ }
452
+ if (!audit || typeof audit !== 'object' || !audit.vulnerabilities || typeof audit.vulnerabilities !== 'object' || Array.isArray(audit.vulnerabilities)) {
453
+ return { ok: false, reason: 'npm audit returned no vulnerability report.' };
454
+ }
455
+ return { ok: true, vulnerabilities: audit.vulnerabilities };
456
+ }
457
+ function auditHint(dir) {
458
+ if (!dir)
459
+ return '';
460
+ const has = (f) => existsSync(resolvePath(dir, f));
461
+ if (has('pnpm-lock.yaml'))
462
+ return ' This is a pnpm project: run `pnpm audit` instead.';
463
+ if (has('yarn.lock'))
464
+ return ' This is a yarn project: run `yarn npm audit` (Berry) or `yarn audit` (v1) instead.';
465
+ if (has('bun.lock') || has('bun.lockb'))
466
+ return ' This is a bun project: run `bun audit` instead.';
467
+ return ' Create one with `npm i --package-lock-only`, or use your package manager\'s audit.';
468
+ }
421
469
  // CVE subcommand
422
470
  const cveCommand = {
423
471
  name: 'cve',
@@ -451,22 +499,20 @@ const cveCommand = {
451
499
  // npm audit exits non-zero when vulnerabilities found — stdout still has JSON
452
500
  auditJson = (e instanceof Error && 'stdout' in e ? e.stdout : '') || '{}';
453
501
  }
454
- let audit;
455
- try {
456
- audit = JSON.parse(auditJson);
457
- }
458
- catch {
459
- output.writeln(output.warning('⚠ Could not parse `npm audit --json` output.'));
460
- output.writeln(output.dim('Make sure you are inside a project with a package.json.'));
502
+ const audit = interpretNpmAudit(auditJson, process.cwd());
503
+ if (!audit.ok) {
504
+ output.writeln(output.warning(`⚠ Could not check dependencies: ${audit.reason}`));
505
+ output.writeln(output.dim('No result is reported rather than a false all-clear.'));
461
506
  return { success: false, exitCode: 2 };
462
507
  }
463
- const vulns = audit.vulnerabilities || {};
508
+ const vulns = audit.vulnerabilities;
464
509
  const cveRows = [];
465
510
  const CVE_RE = /CVE-\d{4}-\d{4,7}/g;
466
511
  for (const [pkg, v] of Object.entries(vulns)) {
467
512
  const sev = v.severity || 'low';
468
- const titles = (v.via || []).filter((x) => typeof x === 'object' && x?.title).map((x) => x.title);
469
- const urls = (v.via || []).filter((x) => typeof x === 'object' && x?.url).map((x) => x.url);
513
+ const via = (v.via || []).filter((x) => typeof x === 'object' && x !== null);
514
+ const titles = via.filter((x) => x.title).map((x) => x.title);
515
+ const urls = via.filter((x) => x.url).map((x) => x.url);
470
516
  const allText = `${titles.join(' ')} ${urls.join(' ')}`;
471
517
  const cveIds = Array.from(new Set(allText.match(CVE_RE) || []));
472
518
  cveRows.push({ pkg, severity: sev, cveIds, title: titles[0] || 'Vulnerability', url: urls[0] });
@@ -1,6 +1,15 @@
1
1
  import type { Command } from '../types.js';
2
2
  /** `<own install>/plugins/ruflo-console/dist/tui.mjs`, the first that exists. */
3
3
  export declare function locateTuiBundle(startDir?: string): string | null;
4
+ /** The look and motion flags: `--look bbs|orbital|plain` (default bbs, as in the console), `--fps N` (0-30, default 8; 0 draws pictures still), `--no-anim`, `--no-pictures`. A bad value is ignored, never passed on. */
5
+ export declare function lookFlags(flags: Record<string, unknown> | undefined): {
6
+ look?: string;
7
+ fps?: number;
8
+ anim?: boolean;
9
+ pictures?: boolean;
10
+ boot?: boolean;
11
+ mouse?: boolean;
12
+ };
4
13
  /** The AI page's settings from the flags (and one environment variable for the endpoint list). Absent flags leave the defaults; a bad number is ignored, never passed on. */
5
14
  export declare function aiPage(flags: Record<string, unknown> | undefined): {
6
15
  model?: string;
@@ -34,6 +34,20 @@ function cliEntry() {
34
34
  const entry = join(__dirname, '..', '..', '..', 'bin', 'cli.js');
35
35
  return existsSync(entry) ? entry : undefined;
36
36
  }
37
+ /** The look and motion flags: `--look bbs|orbital|plain` (default bbs, as in the console), `--fps N` (0-30, default 8; 0 draws pictures still), `--no-anim`, `--no-pictures`. A bad value is ignored, never passed on. */
38
+ export function lookFlags(flags) {
39
+ const look = flags?.look;
40
+ const fps = flags?.fps;
41
+ const number = typeof fps === 'number' ? fps : typeof fps === 'string' && /^\d{1,2}$/.test(fps) ? Number(fps) : undefined;
42
+ return {
43
+ ...(look === 'bbs' || look === 'orbital' || look === 'plain' ? { look } : {}),
44
+ ...(number !== undefined && Number.isFinite(number) && number >= 0 && number <= 30 ? { fps: Math.floor(number) } : {}),
45
+ ...(flags?.anim === false ? { anim: false } : {}),
46
+ ...(flags?.pictures === false ? { pictures: false } : {}),
47
+ ...(flags?.boot === true ? { boot: true } : {}),
48
+ ...(flags?.mouse === true ? { mouse: true } : {}),
49
+ };
50
+ }
37
51
  /** The AI page's settings from the flags (and one environment variable for the endpoint list). Absent flags leave the defaults; a bad number is ignored, never passed on. */
38
52
  export function aiPage(flags) {
39
53
  const model = typeof flags?.model === 'string' && flags.model !== '' ? flags.model : undefined;
@@ -46,11 +60,17 @@ export const tuiCommand = {
46
60
  name: 'tui',
47
61
  description: 'The ruflo console in your terminal: Overview, Cost, Swarm, Events, Missions (read-only) and an AI page that asks Codex or OpenRouter, each question after a card.',
48
62
  options: [
49
- { name: 'view', description: `Page to open: ${VIEWS.join(' | ')}`, type: 'string', default: 'overview' },
63
+ { name: 'view', description: `Page to open: ${VIEWS.join(' | ')}, or any console view (menu, federation, settings, ...): 0 opens the menu and : goes to a page by name`, type: 'string', default: 'overview' },
50
64
  { name: 'ai', description: 'The AI page (ask Codex read-only or OpenRouter, each question after a confirm card). --no-ai turns it off', type: 'boolean', default: true },
51
65
  { name: 'model', description: 'OpenRouter model for this session (default openrouter/auto)', type: 'string' },
52
66
  { name: 'max-spend-usd', description: 'Stop sending to OpenRouter once the provider-reported spend reaches this (default 0.5)', type: 'number' },
53
67
  { name: 'convo-targets', description: 'Extra OpenAI-compatible endpoints, the console convoTargets syntax: endpoint:<name>=<https-base-url>|<KEY_ENV>|<model>;... (a key is named by its environment variable). Also RUFLO_TUI_CONVO_TARGETS', type: 'string' },
68
+ { name: 'look', description: 'The console look: bbs (default, as in Claude Code) | orbital | plain. Key t cycles them', type: 'string' },
69
+ { name: 'fps', description: 'Frames per second for moving pictures, 0-30 (default 8; 0 draws every picture still)', type: 'number' },
70
+ { name: 'anim', description: 'Animate the pictures. --no-anim draws them still', type: 'boolean', default: true },
71
+ { name: 'pictures', description: 'Draw the pictures of the console (title art, swarm graph, gauges). --no-pictures shows their words only', type: 'boolean', default: true },
72
+ { name: 'mouse', description: 'Read mouse reports: click a page in the bar, scroll with the wheel (every action also has a key)', type: 'boolean', default: false },
73
+ { name: 'boot', description: 'Play the look\'s start-up screen first (as the console\'s boot setting)', type: 'boolean', default: false },
54
74
  { name: 'plain', description: 'Print one frame as plain text and exit 0 (scripts and CI). Without it, a non-terminal gets a hint and exit 2', type: 'boolean', default: false },
55
75
  ],
56
76
  examples: [
@@ -62,7 +82,9 @@ export const tuiCommand = {
62
82
  async action(context) {
63
83
  const ctx = context;
64
84
  const wanted = ctx.flags?.view ?? ctx.args?.find(arg => !arg.startsWith('-')) ?? 'overview';
65
- if (!VIEWS.includes(wanted)) {
85
+ // The six pages are known here; any other console view (menu, federation, settings, …) is checked against the bundle's own list once it is loaded.
86
+ const isKnown = VIEWS.includes(wanted);
87
+ if (!isKnown && !/^[a-z]{2,16}$/.test(wanted)) {
66
88
  output.writeln(output.error(`tui: unknown view "${String(wanted).slice(0, 40)}". Use one of: ${VIEWS.join(', ')}`));
67
89
  return { success: false, exitCode: 2, data: { error: 'unknown-view' } };
68
90
  }
@@ -80,12 +102,16 @@ export const tuiCommand = {
80
102
  output.writeln(output.error(`tui: could not load the console bundle: ${error instanceof Error ? error.message.slice(0, 160) : 'unknown error'}`));
81
103
  return { success: false, exitCode: 1, data: { error: 'bundle-load-failed' } };
82
104
  }
105
+ if (!isKnown && mod.isPage?.(wanted) !== true) {
106
+ output.writeln(output.error(`tui: unknown view "${String(wanted).slice(0, 40)}". Use one of: ${VIEWS.join(', ')}, or any console view such as menu, federation, settings`));
107
+ return { success: false, exitCode: 2, data: { error: 'unknown-view' } };
108
+ }
83
109
  const stop = new AbortController();
84
110
  const onSignal = () => stop.abort();
85
111
  process.once('SIGTERM', onSignal);
86
112
  process.once('SIGHUP', onSignal);
87
113
  try {
88
- const code = await mod.runTui({ cwd: ctx.cwd ?? process.cwd(), view: wanted, ...(ctx.flags?.plain === true && { plain: true }), ...(ctx.flags?.ai !== false && { aiPage: aiPage(ctx.flags) }), ...(cliEntry() !== undefined && { cliEntry: cliEntry() }), ...(process.env.CLAUDE_CONFIG_DIR !== undefined && { configDir: process.env.CLAUDE_CONFIG_DIR }) }, { stdin: process.stdin, stdout: process.stdout, stderr: process.stderr, env: process.env, signal: stop.signal });
114
+ const code = await mod.runTui({ cwd: ctx.cwd ?? process.cwd(), view: wanted, ...lookFlags(ctx.flags), ...(ctx.flags?.plain === true && { plain: true }), ...(ctx.flags?.ai !== false && { aiPage: aiPage(ctx.flags) }), ...(cliEntry() !== undefined && { cliEntry: cliEntry() }), ...(process.env.CLAUDE_CONFIG_DIR !== undefined && { configDir: process.env.CLAUDE_CONFIG_DIR }) }, { stdin: process.stdin, stdout: process.stdout, stderr: process.stderr, env: process.env, signal: stop.signal });
89
115
  return { success: code === 0, exitCode: code };
90
116
  }
91
117
  finally {
@@ -15,6 +15,15 @@ export declare const ADOPTED_CONFIG_FILE = "proven-config.json";
15
15
  export declare function loadShippedChampion(srcPath: string): SignedProvenConfig | null;
16
16
  /** Build the local environment the champion's constraints are checked against. */
17
17
  export declare function currentInstallEnv(cwd?: string): InstallEnv;
18
+ /**
19
+ * #4176: is `cwd` a ruflo project? A bare `.claude/` is not enough: Claude Code
20
+ * itself creates `.claude/.cc-writes/` in whatever directory a Bash command ran
21
+ * in, so every subdirectory a session cd-ed into used to qualify and received a
22
+ * proven-config.json. A ruflo project carries `ruflo init`'s own markers: a
23
+ * `.claude/settings.json`, the `.claude/helpers/` directory, or an earlier
24
+ * adoption stamp.
25
+ */
26
+ export declare function isRufloProject(cwd: string): boolean;
18
27
  export interface AdoptResult {
19
28
  adopted: boolean;
20
29
  from?: string;
@@ -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,
package/dist/src/index.js CHANGED
@@ -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;