@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
@@ -2,6 +2,7 @@
2
2
  * Diff Classifier for Change Analysis
3
3
  */
4
4
  import { execFileSync, execFile } from 'node:child_process';
5
+ import { createHash } from 'node:crypto';
5
6
  import { promisify } from 'node:util';
6
7
  const DEFAULT_CONFIG = {
7
8
  maxDiffSize: 10000,
@@ -9,7 +10,7 @@ const DEFAULT_CONFIG = {
9
10
  detectRefactoring: true,
10
11
  minConfidence: 0.5,
11
12
  };
12
- const CLASSIFICATION_PATTERNS = {
13
+ const COMMIT_CLASSIFICATION_PATTERNS = {
13
14
  feature: [/^feat/, /add.*feature/, /implement/, /new.*functionality/i],
14
15
  bugfix: [/^fix/, /bug/, /patch/, /resolve.*issue/i, /hotfix/i],
15
16
  refactor: [/^refactor/, /restructure/, /reorganize/, /cleanup/i, /rename/i],
@@ -18,10 +19,45 @@ const CLASSIFICATION_PATTERNS = {
18
19
  config: [/^config/, /\.config\./, /package\.json/, /tsconfig/, /\.env/],
19
20
  style: [/^style/, /format/, /lint/, /prettier/, /eslint/],
20
21
  };
22
+ // File paths and commit subjects are different signals. Prefixes such as
23
+ // "fix" and words such as "bug" are useful in subjects, but match unrelated
24
+ // paths like src/debug/logger.ts and src/dispatcher.ts.
25
+ const PATH_CLASSIFICATION_PATTERNS = {
26
+ docs: [/(^|\/)docs?\//i, /(^|\/)readme(?:\.[^/]*)?$/i, /\.mdx?$/i],
27
+ test: [/(^|\/)(?:__tests__|tests?|specs?)\//i, /\.(?:test|spec)\.[^/]+$/i],
28
+ config: [/(^|\/)(?:configs?\/|config(?:\.[^/]*)?$)/i, /\.config\.[^/]+$/i,
29
+ /(^|\/)(?:package|tsconfig)(?:\.[^/]*)?\.json$/i, /(^|\/)\.env(?:\.[^/]*)?$/i],
30
+ style: [/\.(?:css|scss|sass|less)$/i],
31
+ };
21
32
  const IMPACT_KEYWORDS = {
22
- security: 3, auth: 3, payment: 3, database: 2, api: 2, core: 2,
33
+ payment: 3, database: 2, api: 2, core: 2,
23
34
  util: 1, helper: 1, test: 0, mock: 0, fixture: 0,
24
35
  };
36
+ function pathHasTerm(path, term) {
37
+ const tokens = path.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase().split(/[^a-z0-9]+/);
38
+ const aliases = {
39
+ auth: ['auth', 'authentication', 'authorization', 'authenticator', 'oauth'],
40
+ util: ['util', 'utils', 'utility', 'utilities'],
41
+ test: ['test', 'tests'],
42
+ mock: ['mock', 'mocks'],
43
+ fixture: ['fixture', 'fixtures'],
44
+ config: ['config', 'configuration'],
45
+ };
46
+ return (aliases[term] ?? [term]).some(value => tokens.includes(value));
47
+ }
48
+ // Match path terms rather than substrings: `encryption` must trigger review,
49
+ // while unrelated names such as `author` and `tokenizer` must not.
50
+ const SECURITY_PATH_TERMS = new Set([
51
+ 'security', 'secure', 'auth', 'oauth', 'authenticate', 'authentication',
52
+ 'authorization', 'authorized', 'authenticator', 'crypto', 'cryptography',
53
+ 'encrypt', 'encryption', 'encrypted', 'decrypt', 'decryption', 'decrypted',
54
+ 'cipher', 'secret', 'secrets', 'password', 'passwords', 'token', 'tokens',
55
+ 'vault', 'kms',
56
+ ]);
57
+ function isSecuritySensitivePath(path) {
58
+ const terms = path.replace(/([a-z])([A-Z])/g, '$1-$2').toLowerCase().split(/[^a-z0-9]+/);
59
+ return terms.some(term => SECURITY_PATH_TERMS.has(term));
60
+ }
25
61
  export class DiffClassifier {
26
62
  config;
27
63
  ruvectorEngine = null;
@@ -47,10 +83,12 @@ export class DiffClassifier {
47
83
  const files = [];
48
84
  const fileBlocks = diffContent.split(/^diff --git/m).filter(Boolean);
49
85
  for (const block of fileBlocks) {
50
- const pathMatch = block.match(/a\/(.+?)\s+b\/(.+)/);
51
- if (!pathMatch)
86
+ const headerEnd = block.indexOf('\n');
87
+ const header = block.slice(0, headerEnd < 0 ? undefined : headerEnd);
88
+ const destination = header.lastIndexOf(' b/');
89
+ if (!header.startsWith(' a/') || destination < 0)
52
90
  continue;
53
- const path = pathMatch[2];
91
+ const path = header.slice(destination + 3);
54
92
  const hunks = this.parseHunks(block);
55
93
  const additions = hunks.reduce((sum, h) => sum + h.changes.filter(c => c.type === 'add').length, 0);
56
94
  const deletions = hunks.reduce((sum, h) => sum + h.changes.filter(c => c.type === 'remove').length, 0);
@@ -71,7 +109,15 @@ export class DiffClassifier {
71
109
  }
72
110
  classifyCommitMessage(message) {
73
111
  const lowerMessage = message.toLowerCase();
74
- for (const [type, patterns] of Object.entries(CLASSIFICATION_PATTERNS)) {
112
+ const conventional = lowerMessage.match(/^([a-z]+)(?:\([^)]*\))?!?:/);
113
+ const conventionalTypes = {
114
+ feat: 'feature', feature: 'feature', fix: 'bugfix', refactor: 'refactor',
115
+ docs: 'docs', doc: 'docs', test: 'test', config: 'config', style: 'style',
116
+ };
117
+ if (conventional && conventionalTypes[conventional[1]]) {
118
+ return conventionalTypes[conventional[1]];
119
+ }
120
+ for (const [type, patterns] of Object.entries(COMMIT_CLASSIFICATION_PATTERNS)) {
75
121
  for (const pattern of patterns) {
76
122
  if (pattern.test(lowerMessage))
77
123
  return type;
@@ -85,16 +131,43 @@ export class DiffClassifier {
85
131
  clearCache() { this.classificationCache.clear(); }
86
132
  parseHunks(block) {
87
133
  const hunks = [];
88
- const hunkMatches = block.matchAll(/@@ -(\d+),?(\d*) \+(\d+),?(\d*) @@([^\n]*)\n([\s\S]*?)(?=@@|$)/g);
89
- for (const match of hunkMatches) {
90
- const oldStart = parseInt(match[1], 10);
91
- const oldLines = parseInt(match[2] || '1', 10);
92
- const newStart = parseInt(match[3], 10);
93
- const newLines = parseInt(match[4] || '1', 10);
94
- const content = match[6] || '';
95
- const changes = this.parseChanges(content, newStart);
96
- hunks.push({ oldStart, oldLines, newStart, newLines, content, changes });
134
+ const readRange = (value, prefix) => {
135
+ if (!value.startsWith(prefix))
136
+ return null;
137
+ const parts = value.slice(1).split(',');
138
+ if (parts.length > 2 || parts.some(part => !/^\d+$/.test(part)))
139
+ return null;
140
+ const start = Number(parts[0]);
141
+ const count = parts.length === 2 ? Number(parts[1]) : 1;
142
+ return Number.isSafeInteger(start) && Number.isSafeInteger(count) ? [start, count] : null;
143
+ };
144
+ let current = null;
145
+ const finish = () => {
146
+ if (!current)
147
+ return;
148
+ const content = current.lines.join('\n');
149
+ hunks.push({
150
+ oldStart: current.oldStart, oldLines: current.oldLines,
151
+ newStart: current.newStart, newLines: current.newLines,
152
+ content, changes: this.parseChanges(content, current.newStart),
153
+ });
154
+ };
155
+ for (const line of block.split('\n')) {
156
+ if (line.startsWith('@@ -')) {
157
+ const end = line.indexOf(' @@', 3);
158
+ const coordinates = end < 0 ? [] : line.slice(3, end).split(' ');
159
+ const oldRange = coordinates.length === 2 ? readRange(coordinates[0], '-') : null;
160
+ const newRange = coordinates.length === 2 ? readRange(coordinates[1], '+') : null;
161
+ if (oldRange && newRange) {
162
+ finish();
163
+ current = { oldStart: oldRange[0], oldLines: oldRange[1], newStart: newRange[0], newLines: newRange[1], lines: [] };
164
+ continue;
165
+ }
166
+ }
167
+ if (current)
168
+ current.lines.push(line);
97
169
  }
170
+ finish();
98
171
  return hunks;
99
172
  }
100
173
  parseChanges(content, startLine) {
@@ -133,20 +206,26 @@ export class DiffClassifier {
133
206
  return classification;
134
207
  }
135
208
  getCacheKey(path, hunks) {
136
- const hunkSummary = hunks.map(h => h.oldStart + ':' + h.newStart).join(',');
137
- return path + ':' + hunkSummary;
209
+ const digest = createHash('sha256');
210
+ digest.update(path);
211
+ for (const hunk of hunks) {
212
+ digest.update(`${hunk.oldStart}:${hunk.newStart}:`);
213
+ for (const change of hunk.changes)
214
+ digest.update(`${change.type}:${change.content}\n`);
215
+ }
216
+ return digest.digest('hex');
138
217
  }
139
218
  determinePrimaryClassification(path, hunks) {
140
- for (const [type, patterns] of Object.entries(CLASSIFICATION_PATTERNS)) {
219
+ for (const [type, patterns] of Object.entries(PATH_CLASSIFICATION_PATTERNS)) {
141
220
  for (const pattern of patterns) {
142
221
  if (pattern.test(path))
143
222
  return type;
144
223
  }
145
224
  }
146
- const allContent = hunks.flatMap(h => h.changes.map(c => c.content)).join('\n').toLowerCase();
225
+ const allContent = hunks.flatMap(h => h.changes.filter(c => c.type !== 'context').map(c => c.content)).join('\n').toLowerCase();
147
226
  if (/function|class|interface|type\s+\w+/.test(allContent) && hunks.some(h => h.changes.filter(c => c.type === 'add').length > 10))
148
227
  return 'feature';
149
- if (/fix|bug|issue|error|exception/.test(allContent))
228
+ if (/\b(?:fix(?:ed|es)?|bug|issue|exception)\b/.test(allContent))
150
229
  return 'bugfix';
151
230
  if (this.config.detectRefactoring && this.isRefactoring(hunks))
152
231
  return 'refactor';
@@ -167,7 +246,7 @@ export class DiffClassifier {
167
246
  }
168
247
  determineSecondaryClassifications(path, hunks, primary) {
169
248
  const secondary = [];
170
- for (const [type, patterns] of Object.entries(CLASSIFICATION_PATTERNS)) {
249
+ for (const [type, patterns] of Object.entries(PATH_CLASSIFICATION_PATTERNS)) {
171
250
  if (type === primary)
172
251
  continue;
173
252
  for (const pattern of patterns) {
@@ -181,7 +260,7 @@ export class DiffClassifier {
181
260
  }
182
261
  calculateConfidence(path, hunks, primary) {
183
262
  let confidence = 0.5;
184
- for (const patterns of Object.values(CLASSIFICATION_PATTERNS)) {
263
+ for (const patterns of Object.values(PATH_CLASSIFICATION_PATTERNS)) {
185
264
  for (const pattern of patterns) {
186
265
  if (pattern.test(path)) {
187
266
  confidence += 0.2;
@@ -200,11 +279,12 @@ export class DiffClassifier {
200
279
  }
201
280
  determineImpactLevel(path, hunks) {
202
281
  let score = 0;
203
- const lowerPath = path.toLowerCase();
204
282
  for (const [keyword, weight] of Object.entries(IMPACT_KEYWORDS)) {
205
- if (lowerPath.includes(keyword))
283
+ if (pathHasTerm(path, keyword))
206
284
  score = Math.max(score, weight);
207
285
  }
286
+ if (isSecuritySensitivePath(path))
287
+ score = Math.max(score, 3);
208
288
  const totalChanges = hunks.reduce((sum, h) => sum + h.changes.filter(c => c.type !== 'context').length, 0);
209
289
  if (totalChanges > 100)
210
290
  score = Math.max(score, 2);
@@ -224,9 +304,9 @@ export class DiffClassifier {
224
304
  reviewers.push(...(typeReviewers[primary] || typeReviewers.unknown));
225
305
  if (impact === 'critical' || impact === 'high')
226
306
  reviewers.push('security-reviewer');
227
- if (/security|auth/.test(path))
307
+ if (isSecuritySensitivePath(path))
228
308
  reviewers.push('security-team');
229
- if (/database|migration/.test(path))
309
+ if (pathHasTerm(path, 'database') || pathHasTerm(path, 'migration'))
230
310
  reviewers.push('dba');
231
311
  return [...new Set(reviewers)].slice(0, 4);
232
312
  }
@@ -240,9 +320,9 @@ export class DiffClassifier {
240
320
  strategies.push('regression-tests');
241
321
  strategies.push('e2e-tests');
242
322
  }
243
- if (/api|endpoint|route|handler/.test(path))
323
+ if (['api', 'endpoint', 'route', 'handler'].some(term => pathHasTerm(path, term)))
244
324
  strategies.push('api-contract-tests');
245
- if (/security|auth|crypto/.test(path))
325
+ if (isSecuritySensitivePath(path))
246
326
  strategies.push('security-audit');
247
327
  return strategies.slice(0, 5);
248
328
  }
@@ -255,11 +335,11 @@ export class DiffClassifier {
255
335
  risks.push('Critical system component - requires careful review');
256
336
  if (impact === 'high')
257
337
  risks.push('High-impact area - monitor after deployment');
258
- if (/security|auth/.test(path))
338
+ if (isSecuritySensitivePath(path))
259
339
  risks.push('Security-sensitive code');
260
- if (/database|migration/.test(path))
340
+ if (pathHasTerm(path, 'database') || pathHasTerm(path, 'migration'))
261
341
  risks.push('Database changes - ensure backup strategy');
262
- if (/config|env/.test(path))
342
+ if (pathHasTerm(path, 'config') || pathHasTerm(path, 'env'))
263
343
  risks.push('Configuration changes - verify all environments');
264
344
  const allContent = hunks.flatMap(h => h.changes.map(c => c.content)).join('\n');
265
345
  if (/TODO|FIXME|HACK/.test(allContent))
@@ -318,20 +398,24 @@ const CACHE_TTL_MS = 5000; // 5 seconds - short TTL since diffs change frequentl
318
398
  * Only allows safe characters: alphanumeric, -, _, /, ., ~, ^
319
399
  */
320
400
  function validateGitRef(ref) {
321
- // Block shell metacharacters and dangerous patterns
322
- if (!/^[a-zA-Z0-9_\-./~^@]+$/.test(ref)) {
401
+ if (ref.length > 256) {
402
+ throw new Error(`Invalid git ref: too long`);
403
+ }
404
+ // A ref is passed as one argv element, but still must not be an option or
405
+ // contain shell metacharacters. A range may omit its right endpoint (HEAD).
406
+ if (!/^[a-zA-Z0-9_][a-zA-Z0-9_\-./~^@]*$/.test(ref)) {
323
407
  throw new Error(`Invalid git ref: contains unsafe characters`);
324
408
  }
325
- // Block multiple dots (path traversal)
326
- if (ref.includes('..') && !ref.match(/^[a-zA-Z0-9_\-]+\.\.\.?[a-zA-Z0-9_\-]+$/)) {
327
- if (!/^\w+\.\.[.\w]+$/.test(ref)) {
409
+ if (ref.includes('..')) {
410
+ const range = ref.match(/^(.+?)(\.\.\.?)(.*)$/);
411
+ // Only one range separator is allowed. In particular, reject traversal
412
+ // inside either endpoint, while accepting branch paths and ancestry syntax.
413
+ if (!range || range[1].includes('..') || range[3].includes('..') ||
414
+ !/^[a-zA-Z0-9_][a-zA-Z0-9_\-./~^@]*$/.test(range[1]) ||
415
+ (range[3] !== '' && !/^[a-zA-Z0-9_][a-zA-Z0-9_\-./~^@]*$/.test(range[3]))) {
328
416
  throw new Error(`Invalid git ref: suspicious pattern`);
329
417
  }
330
418
  }
331
- // Max length check
332
- if (ref.length > 256) {
333
- throw new Error(`Invalid git ref: too long`);
334
- }
335
419
  }
336
420
  /**
337
421
  * Get git diff statistics using SINGLE combined command (optimized)
@@ -500,24 +584,23 @@ export function assessFileRisk(file) {
500
584
  reasons.push('Medium change size (>100 lines)');
501
585
  }
502
586
  // Path-based risk
503
- const lowerPath = file.path.toLowerCase();
504
- if (/security|auth|crypto|password/.test(lowerPath)) {
587
+ if (isSecuritySensitivePath(file.path)) {
505
588
  score += 40;
506
589
  reasons.push('Security-sensitive file');
507
590
  }
508
- if (/payment|billing|transaction/.test(lowerPath)) {
591
+ if (['payment', 'billing', 'transaction'].some(term => pathHasTerm(file.path, term))) {
509
592
  score += 35;
510
593
  reasons.push('Payment-related file');
511
594
  }
512
- if (/database|migration|schema/.test(lowerPath)) {
595
+ if (['database', 'migration', 'schema'].some(term => pathHasTerm(file.path, term))) {
513
596
  score += 25;
514
597
  reasons.push('Database-related file');
515
598
  }
516
- if (/core|main|index/.test(lowerPath)) {
599
+ if (['core', 'main', 'index'].some(term => pathHasTerm(file.path, term))) {
517
600
  score += 15;
518
601
  reasons.push('Core module');
519
602
  }
520
- if (/config|env|settings/.test(lowerPath)) {
603
+ if (['config', 'env', 'settings'].some(term => pathHasTerm(file.path, term))) {
521
604
  score += 20;
522
605
  reasons.push('Configuration file');
523
606
  }
@@ -570,19 +653,68 @@ function getClassifier() {
570
653
  }
571
654
  return classifierInstance;
572
655
  }
573
- /**
574
- * Classify a diff based on files (uses singleton classifier)
575
- */
576
- export function classifyDiff(files) {
656
+ /** Classify file metadata with the actual patch and, for one-commit ranges, its subject. */
657
+ export function classifyDiff(files, patch = '', commitSubject = '') {
577
658
  const classifier = getClassifier();
578
- const fileDiffs = files.map(f => ({
579
- path: f.path,
580
- hunks: [],
581
- additions: f.additions,
582
- deletions: f.deletions,
583
- classification: classifier['classifyFile'](f.path, []),
584
- }));
585
- return classifier['computeOverallClassification'](fileDiffs);
659
+ const parsed = new Map(classifier.parseDiff(patch).map(file => [file.path, file]));
660
+ const fileDiffs = files.map(f => {
661
+ const patchFile = parsed.get(f.path);
662
+ const hunks = patchFile?.hunks ?? [];
663
+ return {
664
+ path: f.path,
665
+ hunks,
666
+ additions: f.additions,
667
+ deletions: f.deletions,
668
+ classification: patchFile?.classification ?? classifier['classifyFile'](f.path, hunks),
669
+ };
670
+ });
671
+ const overall = classifier['computeOverallClassification'](fileDiffs);
672
+ if (fileDiffs.length > 0 && commitSubject) {
673
+ const subjectType = classifier.classifyCommitMessage(commitSubject);
674
+ if (subjectType !== 'unknown') {
675
+ if (overall.primary !== subjectType && overall.primary !== 'unknown') {
676
+ overall.secondary = [overall.primary, ...overall.secondary].filter((type, index, all) => type !== subjectType && all.indexOf(type) === index).slice(0, 3);
677
+ }
678
+ overall.primary = subjectType;
679
+ overall.confidence = Math.max(overall.confidence, 0.85);
680
+ }
681
+ }
682
+ return overall;
683
+ }
684
+ function getSingleCommitSubjectSync(ref) {
685
+ if (!ref.includes('..'))
686
+ return '';
687
+ try {
688
+ const output = execFileSync('git', ['log', '-2', '--format=%s', ref], { encoding: 'utf-8', maxBuffer: 64 * 1024 });
689
+ const subjects = output.trim().split('\n').filter(Boolean);
690
+ return subjects.length === 1 ? subjects[0] : '';
691
+ }
692
+ catch {
693
+ return '';
694
+ }
695
+ }
696
+ function getDiffContextSync(ref) {
697
+ validateGitRef(ref);
698
+ let patch = '';
699
+ try {
700
+ patch = execFileSync('git', ['diff', '--no-ext-diff', '--no-textconv', '--unified=0', ref], { encoding: 'utf-8', maxBuffer: 10 * 1024 * 1024 });
701
+ }
702
+ catch { /* A large or unavailable patch falls back to path-only classification. */ }
703
+ return { patch, subject: getSingleCommitSubjectSync(ref) };
704
+ }
705
+ async function getDiffContextAsync(ref) {
706
+ validateGitRef(ref);
707
+ const execFileAsync = promisify(execFile);
708
+ const patchPromise = execFileAsync('git', ['diff', '--no-ext-diff', '--no-textconv', '--unified=0', ref], { maxBuffer: 10 * 1024 * 1024 }).then(({ stdout }) => stdout).catch(() => '');
709
+ const subjectPromise = ref.includes('..')
710
+ ? execFileAsync('git', ['log', '-2', '--format=%s', ref], { maxBuffer: 64 * 1024 })
711
+ .then(({ stdout }) => {
712
+ const subjects = stdout.trim().split('\n').filter(Boolean);
713
+ return subjects.length === 1 ? subjects[0] : '';
714
+ }).catch(() => '')
715
+ : Promise.resolve('');
716
+ const [patch, subject] = await Promise.all([patchPromise, subjectPromise]);
717
+ return { patch, subject };
586
718
  }
587
719
  /**
588
720
  * Suggest reviewers based on files and risks
@@ -590,20 +722,20 @@ export function classifyDiff(files) {
590
722
  export function suggestReviewers(files, fileRisks) {
591
723
  const reviewers = new Set();
592
724
  for (const file of files) {
593
- const lowerPath = file.path.toLowerCase();
594
- if (/security|auth|crypto/.test(lowerPath))
725
+ const hasAny = (...terms) => terms.some(term => pathHasTerm(file.path, term));
726
+ if (isSecuritySensitivePath(file.path))
595
727
  reviewers.add('security-team');
596
- if (/database|migration/.test(lowerPath))
728
+ if (hasAny('database', 'migration'))
597
729
  reviewers.add('dba');
598
- if (/api|endpoint|route/.test(lowerPath))
730
+ if (hasAny('api', 'endpoint', 'route'))
599
731
  reviewers.add('api-owner');
600
- if (/test|spec/.test(lowerPath))
732
+ if (hasAny('test', 'spec'))
601
733
  reviewers.add('qa-engineer');
602
- if (/config|deploy|ci/.test(lowerPath))
734
+ if (hasAny('config', 'deploy', 'ci'))
603
735
  reviewers.add('devops');
604
- if (/ui|component|style/.test(lowerPath))
736
+ if (hasAny('ui', 'component', 'style'))
605
737
  reviewers.add('frontend-lead');
606
- if (/model|service|repository/.test(lowerPath))
738
+ if (hasAny('model', 'service', 'repository'))
607
739
  reviewers.add('backend-lead');
608
740
  }
609
741
  // Add based on risk
@@ -634,13 +766,13 @@ export async function analyzeDiff(options) {
634
766
  }
635
767
  }
636
768
  // Use async git diff for non-blocking operation
637
- const files = await getGitDiffNumstatAsync(ref);
769
+ const [files, context] = await Promise.all([getGitDiffNumstatAsync(ref), getDiffContextAsync(ref)]);
638
770
  // Parallel file risk assessment for large diffs
639
771
  const fileRisks = files.length > 20
640
772
  ? await Promise.all(files.map(f => Promise.resolve(assessFileRisk(f))))
641
773
  : files.map(assessFileRisk);
642
774
  const risk = assessOverallRisk(files, fileRisks);
643
- const classification = classifyDiff(files);
775
+ const classification = classifyDiff(files, context.patch, context.subject);
644
776
  const recommendedReviewers = suggestReviewers(files, fileRisks);
645
777
  const totalAdditions = files.reduce((sum, f) => sum + f.additions, 0);
646
778
  const totalDeletions = files.reduce((sum, f) => sum + f.deletions, 0);
@@ -669,9 +801,10 @@ export function analyzeDiffSync(options) {
669
801
  return cached.result;
670
802
  }
671
803
  const files = getGitDiffNumstat(ref);
804
+ const context = getDiffContextSync(ref);
672
805
  const fileRisks = files.map(assessFileRisk);
673
806
  const risk = assessOverallRisk(files, fileRisks);
674
- const classification = classifyDiff(files);
807
+ const classification = classifyDiff(files, context.patch, context.subject);
675
808
  const recommendedReviewers = suggestReviewers(files, fileRisks);
676
809
  const totalAdditions = files.reduce((sum, f) => sum + f.additions, 0);
677
810
  const totalDeletions = files.reduce((sum, f) => sum + f.deletions, 0);
@@ -65,7 +65,11 @@ export interface RuvllmStatus {
65
65
  available: boolean;
66
66
  initialized: boolean;
67
67
  version: string | null;
68
+ /** Present when unavailable: how to install the missing package. */
69
+ hint?: string;
68
70
  }
71
+ /** #4149: the wasm package is an optional add-on, not bundled with the CLI. */
72
+ export declare const RUVLLM_WASM_INSTALL_HINT: string;
69
73
  /**
70
74
  * Check if @ruvector/ruvllm-wasm is installed and loadable.
71
75
  */
@@ -24,6 +24,14 @@ import { readFileSync } from 'node:fs';
24
24
  import { createRequire } from 'node:module';
25
25
  // WASM binary requires at least 768-dim input for MicroLoRA adapt()
26
26
  const MICROLORA_WASM_MIN_DIM = 768;
27
+ /** #4149: the wasm package is an optional add-on, not bundled with the CLI. */
28
+ export const RUVLLM_WASM_INSTALL_HINT = '@ruvector/ruvllm-wasm is not installed. Install it next to the CLI: ' +
29
+ '"npm i -g @ruvector/ruvllm-wasm" for a global install, or ' +
30
+ '"npm i @ruvector/ruvllm-wasm" in your project.';
31
+ function isModuleNotFound(err) {
32
+ const code = err?.code;
33
+ return code === 'ERR_MODULE_NOT_FOUND' || code === 'MODULE_NOT_FOUND';
34
+ }
27
35
  // ── WASM Module Detection & Init ─────────────────────────────
28
36
  let _wasmReady = false;
29
37
  /**
@@ -55,6 +63,9 @@ export async function initRuvllmWasm() {
55
63
  _wasmReady = true;
56
64
  }
57
65
  catch (err) {
66
+ if (isModuleNotFound(err)) {
67
+ throw new Error(`Failed to initialize @ruvector/ruvllm-wasm: ${RUVLLM_WASM_INSTALL_HINT}`);
68
+ }
58
69
  throw new Error(`Failed to initialize @ruvector/ruvllm-wasm: ${err}`);
59
70
  }
60
71
  }
@@ -64,7 +75,7 @@ export async function initRuvllmWasm() {
64
75
  export async function getRuvllmStatus() {
65
76
  const available = await isRuvllmWasmAvailable();
66
77
  if (!available) {
67
- return { available: false, initialized: false, version: null };
78
+ return { available: false, initialized: false, version: null, hint: RUVLLM_WASM_INSTALL_HINT };
68
79
  }
69
80
  try {
70
81
  const mod = await import('@ruvector/ruvllm-wasm');
@@ -1,4 +1,4 @@
1
- import { AgenticPolicyEngine, type BudgetLimit, type CapabilityEnvelope, type PolicyApproval, type PolicyDecision, type PolicyRequest, type PolicyRule, type PolicyState } from '@claude-flow/security';
1
+ import { AgenticPolicyEngine, type BudgetLimit, type CapabilityEnvelope, type PolicyApproval, type PolicyDecision, type PolicyRequest, type PolicyRule, type PolicyState } from '@claude-flow/security/policy/index';
2
2
  /**
3
3
  * Exported for ADR-406 mission storage, which takes the same per-file lock
4
4
  * (pid, pid namespace and boot id identify a dead owner) rather than a copy.
@@ -1,4 +1,10 @@
1
- import { AgenticPolicyEngine, createLegacyCompatibleState, isMcpCallerAuthEnabled, decodeTokenEnvelope, publicKeyFromHex, verifyInvocationToken, } from '@claude-flow/security';
1
+ // Narrow subpath imports (not the '@claude-flow/security' barrel): this module
2
+ // is loaded eagerly by the MCP server, and the barrel also evaluates the OAuth
3
+ // callback server (an ESM `node:http` import that materialises http's lazy
4
+ // undici getters), bcryptjs, keychain and credential helpers that the policy
5
+ // runtime never uses.
6
+ import { AgenticPolicyEngine, createLegacyCompatibleState, } from '@claude-flow/security/policy/index';
7
+ import { isMcpCallerAuthEnabled, decodeTokenEnvelope, publicKeyFromHex, verifyInvocationToken, } from '@claude-flow/security/mcp-caller-identity';
2
8
  import { createHash, createHmac, randomBytes, randomUUID, timingSafeEqual } from 'node:crypto';
3
9
  import { execFileSync } from 'node:child_process';
4
10
  import { closeSync, constants as fsConstants, existsSync, fstatSync, lstatSync, mkdirSync, openSync, readFileSync, readSync, readlinkSync, realpathSync, renameSync, statSync, unlinkSync, writeFileSync, } from 'node:fs';
@@ -316,6 +316,17 @@ export declare class WorkerDaemon extends EventEmitter {
316
316
  * Execute a worker with timeout protection
317
317
  */
318
318
  private executeWorker;
319
+ /**
320
+ * Feed a finished worker job into the self-learning pipeline as a one-step
321
+ * trajectory (SONA + ReasoningBank + @ruvector/ruvllm forwarding, persisted
322
+ * to `.claude-flow/neural/stats.json`). Before this, the daemon could run
323
+ * hundreds of jobs at "100% success" while `trajectoriesRecorded` stayed 0:
324
+ * no daemon code path ever called the intelligence module.
325
+ *
326
+ * Best-effort and bounded: never throws, never changes the WorkerResult.
327
+ * Opt-out: RUFLO_DAEMON_NO_LEARN=1.
328
+ */
329
+ private recordWorkerTrajectory;
319
330
  /**
320
331
  * Run a function with timeout (P1 fix)
321
332
  * @param fn - The async function to execute
@@ -75,6 +75,9 @@ const CONSOLIDATE_DEDUP_DISTANCE = 0.2;
75
75
  // `--no-distill` flag on `daemon start` (see commands/daemon.ts), which sets
76
76
  // this env var on the forked/foreground daemon process.
77
77
  const NO_DISTILL_ENV = 'RUFLO_DAEMON_NO_DISTILL';
78
+ // Opt-out for recording each finished worker job as a learning trajectory
79
+ // (see WorkerDaemon.recordWorkerTrajectory).
80
+ const NO_LEARN_ENV = 'RUFLO_DAEMON_NO_LEARN';
78
81
  // #2356 — Self-terminating lifecycle defaults. A background daemon with no
79
82
  // upper bound on its lifetime runs until the box reboots; in the field this
80
83
  // leaked tens of thousands of headless `claude --print` sweeps over many days
@@ -1234,6 +1237,7 @@ export class WorkerDaemon extends EventEmitter {
1234
1237
  this.emit('worker:complete', result);
1235
1238
  this.log('info', `Worker ${workerConfig.type} completed in ${durationMs}ms`);
1236
1239
  this.saveState();
1240
+ await this.recordWorkerTrajectory(result);
1237
1241
  return result;
1238
1242
  }
1239
1243
  catch (error) {
@@ -1253,6 +1257,7 @@ export class WorkerDaemon extends EventEmitter {
1253
1257
  this.emit('worker:error', result);
1254
1258
  this.log('error', `Worker ${workerConfig.type} failed: ${result.error}`);
1255
1259
  this.saveState();
1260
+ await this.recordWorkerTrajectory(result);
1256
1261
  return result;
1257
1262
  }
1258
1263
  finally {
@@ -1261,6 +1266,38 @@ export class WorkerDaemon extends EventEmitter {
1261
1266
  this.processPendingWorkers();
1262
1267
  }
1263
1268
  }
1269
+ /**
1270
+ * Feed a finished worker job into the self-learning pipeline as a one-step
1271
+ * trajectory (SONA + ReasoningBank + @ruvector/ruvllm forwarding, persisted
1272
+ * to `.claude-flow/neural/stats.json`). Before this, the daemon could run
1273
+ * hundreds of jobs at "100% success" while `trajectoriesRecorded` stayed 0:
1274
+ * no daemon code path ever called the intelligence module.
1275
+ *
1276
+ * Best-effort and bounded: never throws, never changes the WorkerResult.
1277
+ * Opt-out: RUFLO_DAEMON_NO_LEARN=1.
1278
+ */
1279
+ async recordWorkerTrajectory(result) {
1280
+ if (process.env[NO_LEARN_ENV] === '1')
1281
+ return;
1282
+ try {
1283
+ const intel = await import('../memory/intelligence.js');
1284
+ await intel.recordTrajectory([{
1285
+ type: 'result',
1286
+ // A worker error is free text (it can quote command output): keep only its first line, cut, so it cannot fill the learning store.
1287
+ content: `daemon worker ${result.type} ${result.success ? 'succeeded' : `failed: ${String(result.error ?? 'unknown error').split('\n')[0].slice(0, 200)}`}`,
1288
+ metadata: {
1289
+ source: 'worker-daemon',
1290
+ worker: result.type,
1291
+ success: result.success,
1292
+ durationMs: result.durationMs,
1293
+ },
1294
+ timestamp: Date.now(),
1295
+ }], result.success ? 'success' : 'failure');
1296
+ }
1297
+ catch (error) {
1298
+ this.log('warn', `Worker ${result.type}: trajectory recording skipped: ${error instanceof Error ? error.message : String(error)}`);
1299
+ }
1300
+ }
1264
1301
  /**
1265
1302
  * Run a function with timeout (P1 fix)
1266
1303
  * @param fn - The async function to execute