@claude-flow/cli 3.56.9 → 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 (97) hide show
  1. package/.claude/helpers/.helpers-version +1 -1
  2. package/.claude/helpers/helpers.manifest.json +3 -3
  3. package/.claude/helpers/intelligence.cjs +51 -9
  4. package/.claude/helpers/router.cjs +0 -0
  5. package/.claude/skills/verification-quality/SKILL.md +14 -11
  6. package/README.md +1 -1
  7. package/catalog-manifest.json +2 -2
  8. package/dist/src/autopilot-state.d.ts +2 -0
  9. package/dist/src/autopilot-state.js +36 -8
  10. package/dist/src/command-resolution.d.ts +15 -0
  11. package/dist/src/command-resolution.js +25 -0
  12. package/dist/src/commands/agent.js +17 -17
  13. package/dist/src/commands/claims.js +6 -8
  14. package/dist/src/commands/daemon.js +86 -31
  15. package/dist/src/commands/memory.js +14 -2
  16. package/dist/src/commands/security-code-patterns.d.ts +2 -2
  17. package/dist/src/commands/security-code-patterns.js +61 -3
  18. package/dist/src/commands/security.d.ts +24 -0
  19. package/dist/src/commands/security.js +81 -35
  20. package/dist/src/commands/tui.d.ts +9 -0
  21. package/dist/src/commands/tui.js +29 -3
  22. package/dist/src/config/proven-config-refresh.d.ts +9 -0
  23. package/dist/src/config/proven-config-refresh.js +24 -3
  24. package/dist/src/funnel/local-signals.js +2 -1
  25. package/dist/src/index.js +6 -42
  26. package/dist/src/init/types.js +1 -1
  27. package/dist/src/mcp-tools/agent-execute-core.d.ts +2 -0
  28. package/dist/src/mcp-tools/agent-execute-core.js +15 -0
  29. package/dist/src/mcp-tools/agent-health-metrics.d.ts +22 -0
  30. package/dist/src/mcp-tools/agent-health-metrics.js +33 -0
  31. package/dist/src/mcp-tools/agent-tools.js +45 -22
  32. package/dist/src/mcp-tools/agentbbs-federation.d.ts +1 -1
  33. package/dist/src/mcp-tools/agentbbs-federation.js +7 -2
  34. package/dist/src/mcp-tools/agentdb-tools.js +25 -2
  35. package/dist/src/mcp-tools/aidefence-known-misses.d.ts +22 -0
  36. package/dist/src/mcp-tools/aidefence-known-misses.js +57 -0
  37. package/dist/src/mcp-tools/analyze-tools.js +10 -11
  38. package/dist/src/mcp-tools/autopilot-tools.js +2 -2
  39. package/dist/src/mcp-tools/browser-intent-tools.js +6 -2
  40. package/dist/src/mcp-tools/embeddings-tools.js +29 -7
  41. package/dist/src/mcp-tools/hooks-tools.js +4 -0
  42. package/dist/src/mcp-tools/memory-tools.js +70 -20
  43. package/dist/src/mcp-tools/neural-tools.js +28 -4
  44. package/dist/src/mcp-tools/ruvllm-tools.js +6 -0
  45. package/dist/src/mcp-tools/security-tools.js +55 -17
  46. package/dist/src/mcp-tools/workflow-tools.js +66 -17
  47. package/dist/src/memory/intelligence.d.ts +5 -1
  48. package/dist/src/memory/intelligence.js +105 -29
  49. package/dist/src/memory/memory-bridge.js +10 -3
  50. package/dist/src/memory/memory-initializer.js +2 -1
  51. package/dist/src/memory/search-defaults.d.ts +54 -0
  52. package/dist/src/memory/search-defaults.js +37 -0
  53. package/dist/src/memory/sona-optimizer.d.ts +28 -0
  54. package/dist/src/memory/sona-optimizer.js +59 -2
  55. package/dist/src/mods/command-registry/schema.d.ts +24 -24
  56. package/dist/src/ruvector/agent-wasm.d.ts +10 -0
  57. package/dist/src/ruvector/agent-wasm.js +27 -1
  58. package/dist/src/ruvector/diff-classifier.d.ts +2 -4
  59. package/dist/src/ruvector/diff-classifier.js +202 -69
  60. package/dist/src/ruvector/ruvllm-wasm.d.ts +4 -0
  61. package/dist/src/ruvector/ruvllm-wasm.js +12 -1
  62. package/dist/src/services/policy-runtime.d.ts +1 -1
  63. package/dist/src/services/policy-runtime.js +7 -1
  64. package/dist/src/services/worker-daemon.d.ts +11 -0
  65. package/dist/src/services/worker-daemon.js +37 -0
  66. package/dist/src/transfer/store/discovery.js +16 -17
  67. package/dist/src/transfer/store/types.d.ts +4 -0
  68. package/node_modules/@claude-flow/codex/dist/cli.js +0 -0
  69. package/node_modules/@claude-flow/plugin-agent-federation/dist/bin.js +0 -0
  70. package/node_modules/@claude-flow/security/dist/input-validator.d.ts +6 -6
  71. package/package.json +4 -4
  72. package/plugins/ruflo-console/dist/tui.mjs +162 -161
  73. package/plugins/ruflo-metaharness/.claude-flow/daemon-state.json +179 -0
  74. package/plugins/ruflo-metaharness/.claude-flow/daemon.pid +1 -0
  75. package/plugins/ruflo-metaharness/.claude-flow/logs/daemon.log +32 -0
  76. package/plugins/ruflo-metaharness/.claude-flow/metrics/backup.json +23 -0
  77. package/plugins/ruflo-metaharness/.claude-flow/metrics/codebase-map.json +11 -0
  78. package/plugins/ruflo-metaharness/.claude-flow/metrics/consolidation.json +16 -0
  79. package/plugins/ruflo-metaharness/.claude-flow/metrics/harness-loop.json +91 -0
  80. package/plugins/ruflo-metaharness/.claude-flow/metrics/performance.json +17 -0
  81. package/plugins/ruflo-metaharness/.claude-flow/metrics/security-audit.json +12 -0
  82. package/plugins/ruflo-metaharness/.claude-flow/metrics/test-gaps.json +8 -0
  83. package/plugins/ruflo-metaharness/.claude-flow/neural/patterns.json +2809 -0
  84. package/plugins/ruflo-metaharness/.claude-flow/neural/stats.json +6 -0
  85. package/plugins/ruflo-metaharness/.claude-flow/policy/state.json +11 -0
  86. package/plugins/ruflo-metaharness/.swarm/agentdb-memory.db +0 -0
  87. package/plugins/ruflo-metaharness/.swarm/agentdb-memory.db-shm +0 -0
  88. package/plugins/ruflo-metaharness/.swarm/agentdb-memory.db-wal +0 -0
  89. package/plugins/ruflo-metaharness/.swarm/backups/memory-2026-10-14T16-09-38-164Z.db +0 -0
  90. package/plugins/ruflo-metaharness/.swarm/backups/store-agentdb-memory.db-2026-10-14T16-09-38-166Z.db +0 -0
  91. package/plugins/ruflo-metaharness/.swarm/ewc-fisher.json +397 -0
  92. package/plugins/ruflo-metaharness/.swarm/memory.db +0 -0
  93. package/plugins/ruflo-metaharness/.swarm/memory.db-shm +0 -0
  94. package/plugins/ruflo-metaharness/.swarm/memory.db-wal +0 -0
  95. package/plugins/ruflo-metaharness/agentdb.rvf +0 -0
  96. package/plugins/ruflo-metaharness/agentdb.rvf.lock +0 -0
  97. package/plugins/ruflo-metaharness/ruvector.db +0 -0
@@ -6,14 +6,72 @@
6
6
  * - every rule is tested after comment text is blanked, so a comment naming `child_process.execFile` or `eval(` is
7
7
  * not a finding;
8
8
  * - eval needs a word start: "retrieval (" in prose is not a call; `eval(` and `window.eval(` still are;
9
- * - command injection keeps the old rule's reach (any `exec` after `child_process` on the line), and no longer skips
10
- * execSync as `exec[^S]` did;
9
+ * - command injection keeps the old rule's reach (any `exec` after `child_process` on the line), and also catches
10
+ * dynamic exec/execSync command strings when the import was on another line;
11
11
  * - a pg placeholder such as `$${paramCount + 1}` is a parameter number, not a value in the SQL text. It is exempt only
12
12
  * when its expression is a number, a loop letter, `params.length`-style, or a name containing `param` or `placeholder`, it
13
13
  * stands where SQL takes a value, and it is not inside a quoted SQL string.
14
14
  */
15
15
  /** No `g` flag, so `test` keeps no lastIndex between lines. */
16
16
  const re = (pattern) => (line) => pattern.test(line);
17
+ /** Detect a dynamic shell command at an exec/execSync call site, independent of its import. */
18
+ const interpolatedExec = (line) => {
19
+ let quote = null;
20
+ for (let i = 0; i < line.length; i++) {
21
+ const ch = line[i];
22
+ if (quote !== null) {
23
+ if (ch === '\\') {
24
+ i++;
25
+ continue;
26
+ }
27
+ if (ch === quote)
28
+ quote = null;
29
+ continue;
30
+ }
31
+ if (ch === "'" || ch === '"' || ch === '`') {
32
+ quote = ch;
33
+ continue;
34
+ }
35
+ if (i > 0 && /[\w$]/.test(line[i - 1]))
36
+ continue;
37
+ const name = line.startsWith('execSync', i) ? 'execSync' : line.startsWith('exec', i) ? 'exec' : '';
38
+ if (!name)
39
+ continue;
40
+ let arg = i + name.length;
41
+ while (/\s/.test(line[arg] ?? ''))
42
+ arg++;
43
+ if (line[arg++] !== '(')
44
+ continue;
45
+ while (/\s/.test(line[arg] ?? ''))
46
+ arg++;
47
+ const delimiter = line[arg];
48
+ if (delimiter !== '`' && delimiter !== "'" && delimiter !== '"')
49
+ continue;
50
+ for (let end = arg + 1; end < line.length; end++) {
51
+ if (line[end] === '\\') {
52
+ end++;
53
+ continue;
54
+ }
55
+ if (delimiter === '`' && line[end] === '$' && line[end + 1] === '{')
56
+ return true;
57
+ if (line[end] !== delimiter)
58
+ continue;
59
+ if (delimiter !== '`') {
60
+ let next = end + 1;
61
+ while (/\s/.test(line[next] ?? ''))
62
+ next++;
63
+ if (line[next++] === '+') {
64
+ while (/\s/.test(line[next] ?? ''))
65
+ next++;
66
+ if (/[A-Za-z_$]/.test(line[next] ?? ''))
67
+ return true;
68
+ }
69
+ }
70
+ break;
71
+ }
72
+ }
73
+ return false;
74
+ };
17
75
  // A `.length` counts only on a param-, value- or arg-named array: `req.body.length` is whatever the client sent.
18
76
  const COUNTER = /^\s*(?:\d+|[ijkn]|(?:[A-Za-z_$][\w$]*(?:Params?|Placeholders?|Values?|Args?)|params?|placeholders?|values?|args?)\.length|[A-Za-z_$]*(?:[Pp]aram|[Pp]laceholder)[\w$]*)\s*(?:[+-]\s*\d+\s*)?$/;
19
77
  // Where a pg parameter stands in SQL: after a comparison, a list or call opener, or a clause keyword.
@@ -70,7 +128,7 @@ export const CODE_PATTERNS = [
70
128
  { match: re(/(?<![\w$])eval\s*\(/), type: 'Eval Usage', severity: 'medium', desc: 'eval() can execute arbitrary code' },
71
129
  { match: re(/innerHTML\s*=/), type: 'innerHTML', severity: 'medium', desc: 'XSS risk with innerHTML' },
72
130
  { match: re(/dangerouslySetInnerHTML/), type: 'React XSS', severity: 'medium', desc: 'React XSS risk' },
73
- { match: re(/child_process.*exec/), type: 'Command Injection', severity: 'high', desc: 'Possible command injection' },
131
+ { match: line => /child_process.*exec/.test(line) || interpolatedExec(line), type: 'Command Injection', severity: 'high', desc: 'Possible command injection' },
74
132
  { match: line => sqlInterpolation(line) && sqlInterpolation(withoutPlaceholders(line)), type: 'SQL Injection', severity: 'high', desc: 'Possible SQL injection' },
75
133
  ];
76
134
  /**
@@ -5,6 +5,30 @@
5
5
  * Created with ❤️ by ruv.io
6
6
  */
7
7
  import type { Command } from '../types.js';
8
+ /**
9
+ * #4124: `npm audit` can fail without exiting through the vulnerability path.
10
+ * In a pnpm/yarn/bun project there is no package-lock.json, so it prints
11
+ * `{"error":{"code":"ENOLOCK",...}}`. That has no `vulnerabilities` key, and
12
+ * both `scan` and `cve` used to read it as a clean tree. Only a report with a
13
+ * `vulnerabilities` object counts as a scan; anything else is "could not scan".
14
+ */
15
+ type NpmAuditVuln = {
16
+ severity: string;
17
+ via: Array<{
18
+ title?: string;
19
+ url?: string;
20
+ source?: number;
21
+ name?: string;
22
+ } | string>;
23
+ };
24
+ export type NpmAuditResult = {
25
+ ok: true;
26
+ vulnerabilities: Record<string, NpmAuditVuln>;
27
+ } | {
28
+ ok: false;
29
+ reason: string;
30
+ };
31
+ export declare function interpretNpmAudit(raw: string, dir?: string): NpmAuditResult;
8
32
  export declare const securityCommand: Command;
9
33
  export default securityCommand;
10
34
  //# sourceMappingURL=security.d.ts.map
@@ -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) {
@@ -76,7 +76,7 @@ export const DEFAULT_INIT_OPTIONS = {
76
76
  github: true,
77
77
  flowNexus: false,
78
78
  browser: true,
79
- v3: true,
79
+ v3: false, // ADR-128: opt-in like agents.v3; use --full or `init skills --v3`
80
80
  dualMode: false, // Optional: enable with --dual flag
81
81
  all: false,
82
82
  },
@@ -7,6 +7,7 @@
7
7
  * keeps the wire from agent_spawn → ProviderManager (real) in one
8
8
  * place rather than duplicated.
9
9
  */
10
+ import { type AgentExecutionSample } from './agent-health-metrics.js';
10
11
  type ClaudeModel = 'haiku' | 'sonnet' | 'opus' | 'opus-4.7' | 'inherit';
11
12
  export interface AgentRecord {
12
13
  agentId: string;
@@ -32,6 +33,7 @@ export interface AgentRecord {
32
33
  /** ADR-148 phase 2 — concrete OpenRouter slug when provider='openrouter'. */
33
34
  openrouterModel?: string;
34
35
  lastResult?: Record<string, unknown>;
36
+ executionHistory?: AgentExecutionSample[];
35
37
  }
36
38
  export declare const DEFAULT_ANTHROPIC_MODEL = "claude-sonnet-5";
37
39
  export declare function modelRejectsSamplingParams(model: string): boolean;
@@ -10,6 +10,7 @@
10
10
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs';
11
11
  import { join } from 'node:path';
12
12
  import { getProjectCwd } from './types.js';
13
+ import { appendAgentExecution, summarizeAgentExecutions } from './agent-health-metrics.js';
13
14
  import { configManager } from '../services/config-file-manager.js';
14
15
  const STORAGE_DIR = '.claude-flow';
15
16
  const AGENT_DIR = 'agents';
@@ -669,6 +670,20 @@ export async function executeAgentTask(input) {
669
670
  }
670
671
  if (out)
671
672
  current.lastResult = out;
673
+ const previousMeasuredHealth = summarizeAgentExecutions(current.executionHistory).health;
674
+ const mayUpdateStoredHealth = previousMeasuredHealth === null
675
+ ? current.health === agent.health
676
+ : current.health === previousMeasuredHealth;
677
+ current.executionHistory = appendAgentExecution(current.executionHistory, {
678
+ at: new Date().toISOString(),
679
+ success: result.success,
680
+ durationMs: result.durationMs ?? Date.now() - startedAt,
681
+ });
682
+ // An explicit agent_update during the provider call still wins. The
683
+ // measured value remains available from executionHistory in agent_health.
684
+ if (mayUpdateStoredHealth) {
685
+ current.health = summarizeAgentExecutions(current.executionHistory).health ?? current.health;
686
+ }
672
687
  saveAgentStore(latest);
673
688
  };
674
689
  if (result.success) {
@@ -0,0 +1,22 @@
1
+ /** Bounded execution evidence for agent_health. Registry records are not processes. */
2
+ export interface AgentExecutionSample {
3
+ at: string;
4
+ success: boolean;
5
+ durationMs: number;
6
+ }
7
+ export declare const AGENT_HEALTH_WINDOW = 20;
8
+ export declare function appendAgentExecution(value: unknown, sample: AgentExecutionSample): AgentExecutionSample[];
9
+ export declare function summarizeAgentExecutions(value: unknown): {
10
+ health: number | null;
11
+ errorRate: number | null;
12
+ sampleCount: number;
13
+ successes: number;
14
+ failures: number;
15
+ latency: {
16
+ avg: number | null;
17
+ p50: number | null;
18
+ p95: number | null;
19
+ p99: number | null;
20
+ };
21
+ };
22
+ //# sourceMappingURL=agent-health-metrics.d.ts.map
@@ -0,0 +1,33 @@
1
+ export const AGENT_HEALTH_WINDOW = 20;
2
+ function validSamples(value) {
3
+ if (!Array.isArray(value))
4
+ return [];
5
+ return value.filter((sample) => sample !== null && typeof sample === 'object' &&
6
+ typeof sample.at === 'string' && typeof sample.success === 'boolean' &&
7
+ typeof sample.durationMs === 'number' && Number.isFinite(sample.durationMs) && sample.durationMs >= 0).slice(-AGENT_HEALTH_WINDOW);
8
+ }
9
+ export function appendAgentExecution(value, sample) {
10
+ return [...validSamples(value), sample].slice(-AGENT_HEALTH_WINDOW);
11
+ }
12
+ export function summarizeAgentExecutions(value) {
13
+ const samples = validSamples(value);
14
+ const sampleCount = samples.length;
15
+ const successes = samples.filter(sample => sample.success).length;
16
+ const failures = sampleCount - successes;
17
+ const durations = samples.map(sample => sample.durationMs).sort((a, b) => a - b);
18
+ const percentile = (p) => sampleCount ? durations[Math.ceil(p * sampleCount) - 1] : null;
19
+ return {
20
+ health: sampleCount ? successes / sampleCount : null,
21
+ errorRate: sampleCount ? failures / sampleCount : null,
22
+ sampleCount,
23
+ successes,
24
+ failures,
25
+ latency: {
26
+ avg: sampleCount ? durations.reduce((sum, duration) => sum + duration, 0) / sampleCount : null,
27
+ p50: percentile(0.5),
28
+ p95: percentile(0.95),
29
+ p99: percentile(0.99),
30
+ },
31
+ };
32
+ }
33
+ //# sourceMappingURL=agent-health-metrics.js.map