claude-flow 3.56.10 → 3.57.1

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 (46) hide show
  1. package/.claude/proven-config.json +1 -1
  2. package/README.md +12 -3
  3. package/node_modules/@claude-flow/codex/dist/cli.js +0 -0
  4. package/node_modules/@claude-flow/plugin-agent-federation/dist/bin.js +0 -0
  5. package/node_modules/@claude-flow/security/dist/input-validator.d.ts +6 -6
  6. package/node_modules/@claude-flow/security/dist/policy/engine.d.ts +17 -1
  7. package/node_modules/@claude-flow/security/dist/policy/engine.d.ts.map +1 -1
  8. package/node_modules/@claude-flow/security/dist/policy/engine.js +99 -24
  9. package/node_modules/@claude-flow/security/dist/policy/engine.js.map +1 -1
  10. package/node_modules/@claude-flow/security/dist/policy/types.d.ts +14 -1
  11. package/node_modules/@claude-flow/security/dist/policy/types.d.ts.map +1 -1
  12. package/node_modules/@claude-flow/security/dist/policy/types.js +3 -1
  13. package/node_modules/@claude-flow/security/dist/policy/types.js.map +1 -1
  14. package/package.json +2 -2
  15. package/v3/@claude-flow/cli/README.md +173 -62
  16. package/v3/@claude-flow/cli/catalog-manifest.json +5 -5
  17. package/v3/@claude-flow/cli/dist/src/commands/daemon.js +8 -2
  18. package/v3/@claude-flow/cli/dist/src/commands/doctor.js +95 -18
  19. package/v3/@claude-flow/cli/dist/src/commands/policy.js +6 -1
  20. package/v3/@claude-flow/cli/dist/src/index.js +5 -5
  21. package/v3/@claude-flow/cli/dist/src/init/settings-generator.js +4 -58
  22. package/v3/@claude-flow/cli/dist/src/mcp-tools/memory-tools.d.ts +1 -1
  23. package/v3/@claude-flow/cli/dist/src/mcp-tools/memory-tools.js +30 -5
  24. package/v3/@claude-flow/cli/dist/src/mcp-tools/metaharness-tools.js +1 -1
  25. package/v3/@claude-flow/cli/dist/src/mcp-tools/neural-tools.js +20 -50
  26. package/v3/@claude-flow/cli/dist/src/mcp-tools/policy-tools.js +5 -3
  27. package/v3/@claude-flow/cli/dist/src/memory/intelligence.js +80 -33
  28. package/v3/@claude-flow/cli/dist/src/memory/stats-file-lock.d.ts +7 -0
  29. package/v3/@claude-flow/cli/dist/src/memory/stats-file-lock.js +53 -0
  30. package/v3/@claude-flow/cli/dist/src/missions/schemas.d.ts +113 -113
  31. package/v3/@claude-flow/cli/dist/src/mods/command-registry/schema.d.ts +193 -193
  32. package/v3/@claude-flow/cli/dist/src/services/distill-oracle.d.ts +1 -1
  33. package/v3/@claude-flow/cli/dist/src/services/distill-oracle.js +2 -2
  34. package/v3/@claude-flow/cli/dist/src/services/policy-anchor-log.d.ts +13 -0
  35. package/v3/@claude-flow/cli/dist/src/services/policy-anchor-log.js +242 -0
  36. package/v3/@claude-flow/cli/dist/src/services/policy-archive-scan.d.ts +7 -0
  37. package/v3/@claude-flow/cli/dist/src/services/policy-archive-scan.js +50 -0
  38. package/v3/@claude-flow/cli/dist/src/services/policy-ledger-anchor.d.ts +14 -8
  39. package/v3/@claude-flow/cli/dist/src/services/policy-ledger-anchor.js +356 -40
  40. package/v3/@claude-flow/cli/dist/src/services/policy-ledger-audit.d.ts +22 -0
  41. package/v3/@claude-flow/cli/dist/src/services/policy-ledger-audit.js +260 -0
  42. package/v3/@claude-flow/cli/dist/src/services/policy-ledger-record.d.ts +3 -0
  43. package/v3/@claude-flow/cli/dist/src/services/policy-ledger-record.js +22 -0
  44. package/v3/@claude-flow/cli/dist/src/services/policy-runtime.d.ts +1 -0
  45. package/v3/@claude-flow/cli/dist/src/services/policy-runtime.js +261 -40
  46. package/v3/@claude-flow/cli/package.json +3 -3
@@ -92,13 +92,13 @@ export class CLI {
92
92
  const parseResult = this.parser.parse(args);
93
93
  const { command: commandPath, flags, positional } = parseResult;
94
94
  // Handle global flags
95
+ if (flags.noColor || flags.color === false) {
96
+ this.output.setColorEnabled(false);
97
+ }
95
98
  if (flags.version || flags.V) {
96
99
  this.showVersion();
97
100
  return;
98
101
  }
99
- if (flags.noColor) {
100
- this.output.setColorEnabled(false);
101
- }
102
102
  // Set verbosity level based on flags
103
103
  if (flags.quiet) {
104
104
  this.output.setVerbosity('quiet');
@@ -114,7 +114,7 @@ export class CLI {
114
114
  this.output.printDebug(`CWD: ${process.cwd()}`);
115
115
  }
116
116
  // Run startup update check (non-blocking, silent on skip)
117
- if (!flags.noUpdate && commandPath[0] !== 'update') {
117
+ if (!flags.noUpdate && flags.update !== false && commandPath[0] !== 'update' && !(commandPath[0] === 'doctor' && flags.json)) {
118
118
  this.checkForUpdatesOnStartup().catch(() => { });
119
119
  }
120
120
  // Version-stamped helper auto-refresh — propagate hook fixes to an already
@@ -536,7 +536,7 @@ export class CLI {
536
536
  catch (error) {
537
537
  // Config loading is optional - don't fail if it doesn't exist
538
538
  if (process.env.DEBUG) {
539
- this.output.writeln(this.output.dim(`Config loading failed: ${error.message}`));
539
+ this.output.writeErrorln(this.output.dim(`Config loading failed: ${error.message}`));
540
540
  }
541
541
  return undefined;
542
542
  }
@@ -27,7 +27,6 @@ export function generateSettings(options) {
27
27
  allow: [
28
28
  'Bash(npx @claude-flow*)',
29
29
  'Bash(npx claude-flow*)',
30
- 'Bash(node .claude/*)',
31
30
  'mcp__claude-flow__*',
32
31
  ],
33
32
  deny: [
@@ -201,7 +200,7 @@ function generateHooksConfig(config, scope) {
201
200
  const hooks = {};
202
201
  // Node.js scripts handle errors internally via try/catch.
203
202
  // No shell-level error suppression needed (2>/dev/null || true breaks Windows).
204
- // PreToolUse — validate commands and edits before execution
203
+ // PreToolUse — validate shell commands before execution
205
204
  if (config.preToolUse) {
206
205
  hooks.PreToolUse = [
207
206
  {
@@ -214,19 +213,9 @@ function generateHooksConfig(config, scope) {
214
213
  },
215
214
  ],
216
215
  },
217
- {
218
- matcher: 'Write|Edit|MultiEdit',
219
- hooks: [
220
- {
221
- type: 'command',
222
- command: hookHandlerCmd('pre-edit', scope),
223
- timeout: config.timeout,
224
- },
225
- ],
226
- },
227
216
  ];
228
217
  }
229
- // PostToolUse — record edits and commands for session metrics / learning
218
+ // PostToolUse — record edits for session metrics / learning
230
219
  if (config.postToolUse) {
231
220
  hooks.PostToolUse = [
232
221
  {
@@ -239,16 +228,6 @@ function generateHooksConfig(config, scope) {
239
228
  },
240
229
  ],
241
230
  },
242
- {
243
- matcher: 'Bash',
244
- hooks: [
245
- {
246
- type: 'command',
247
- command: hookHandlerCmd('post-bash', scope),
248
- timeout: config.timeout,
249
- },
250
- ],
251
- },
252
231
  ];
253
232
  }
254
233
  // UserPromptSubmit — intelligent task routing
@@ -312,16 +291,13 @@ function generateHooksConfig(config, scope) {
312
291
  },
313
292
  ];
314
293
  }
315
- // PreCompact — preserve context before compaction
294
+ // PreCompact — end the current session before compaction. The handler has no
295
+ // manual/automatic compaction-specific command, so don't wire no-op hooks.
316
296
  if (config.preCompact) {
317
297
  hooks.PreCompact = [
318
298
  {
319
299
  matcher: 'manual',
320
300
  hooks: [
321
- {
322
- type: 'command',
323
- command: hookHandlerCmd('compact-manual', scope),
324
- },
325
301
  {
326
302
  type: 'command',
327
303
  command: hookHandlerCmd('session-end', scope),
@@ -332,10 +308,6 @@ function generateHooksConfig(config, scope) {
332
308
  {
333
309
  matcher: 'auto',
334
310
  hooks: [
335
- {
336
- type: 'command',
337
- command: hookHandlerCmd('compact-auto', scope),
338
- },
339
311
  {
340
312
  type: 'command',
341
313
  command: hookHandlerCmd('session-end', scope),
@@ -345,18 +317,6 @@ function generateHooksConfig(config, scope) {
345
317
  },
346
318
  ];
347
319
  }
348
- // SubagentStart — status update when a sub-agent is spawned
349
- hooks.SubagentStart = [
350
- {
351
- hooks: [
352
- {
353
- type: 'command',
354
- command: hookHandlerCmd('status', scope),
355
- timeout: 3000,
356
- },
357
- ],
358
- },
359
- ];
360
320
  // SubagentStop — track agent completion for metrics
361
321
  // NOTE: The valid event is "SubagentStop" (not "SubagentEnd")
362
322
  hooks.SubagentStop = [
@@ -370,20 +330,6 @@ function generateHooksConfig(config, scope) {
370
330
  ],
371
331
  },
372
332
  ];
373
- // Notification — capture Claude Code notifications for logging
374
- if (config.notification) {
375
- hooks.Notification = [
376
- {
377
- hooks: [
378
- {
379
- type: 'command',
380
- command: hookHandlerCmd('notify', scope),
381
- timeout: 3000,
382
- },
383
- ],
384
- },
385
- ];
386
- }
387
333
  // NOTE: TeammateIdle, TaskCompleted, and PostCompact are NOT accepted by
388
334
  // Claude Code's settings.json validator (rejected as "Invalid key in record").
389
335
  // Agent Teams coordination lives in claudeFlow.agentTeams.hooks instead.
@@ -9,7 +9,7 @@
9
9
  *
10
10
  * @module v3/cli/mcp-tools/memory-tools
11
11
  */
12
- import type { MCPTool } from './types.js';
12
+ import { type MCPTool } from './types.js';
13
13
  export declare const DANGEROUS_KEY_PATTERN: RegExp;
14
14
  /**
15
15
  * #3570: the one key rule for every memory write path (MCP store, CLI store,
@@ -11,8 +11,10 @@
11
11
  */
12
12
  import { existsSync, mkdirSync, readdirSync, readFileSync, statSync, unlinkSync, writeFileSync } from 'fs';
13
13
  import { homedir } from 'os';
14
- import { join, resolve } from 'path';
14
+ import { basename, dirname, join, resolve } from 'path';
15
+ import { execFileSync } from 'node:child_process';
15
16
  import { createHash } from 'crypto';
17
+ import { getProjectCwd } from './types.js';
16
18
  import { validateIdentifier } from './validate-input.js';
17
19
  import { DEFAULT_SEARCH_THRESHOLD, nearMisses } from '../memory/search-defaults.js';
18
20
  // #1604: Align with memory-initializer.ts — single source of truth is .swarm/memory.db
@@ -146,7 +148,24 @@ function resolveProjectMemoryDir(claudeProjectsDir, projectPathOverride) {
146
148
  sources.push(projectPathOverride);
147
149
  }
148
150
  else {
149
- sources.push(process.cwd());
151
+ const project = getProjectCwd();
152
+ sources.push(project);
153
+ // Claude Code shares auto-memory across linked Git worktrees under the
154
+ // main checkout's project slug. Preserve the direct-path match first,
155
+ // then try the common Git directory's parent. Explicit overrides above
156
+ // remain authoritative, and non-Git projects need no Git dependency.
157
+ try {
158
+ const commonDir = execFileSync('git', ['rev-parse', '--path-format=absolute', '--git-common-dir'], {
159
+ cwd: project,
160
+ encoding: 'utf-8',
161
+ timeout: 1000,
162
+ windowsHide: true,
163
+ stdio: ['ignore', 'pipe', 'ignore'],
164
+ }).trim();
165
+ if (basename(commonDir) === '.git')
166
+ sources.push(dirname(resolve(project, commonDir)));
167
+ }
168
+ catch { /* Git unavailable or not a repository: use the project path */ }
150
169
  }
151
170
  for (const source of sources) {
152
171
  // Candidate 1: legacy POSIX hash — what shipped before #1883
@@ -1066,6 +1085,7 @@ export const memoryTools = [
1066
1085
  }
1067
1086
  let imported = 0;
1068
1087
  let skipped = 0;
1088
+ const importErrors = [];
1069
1089
  // #1791.8 — Claude Code's `~/.claude/projects/` accumulates historical
1070
1090
  // project_id directories (truncated forms, sandbox cwds, renamed
1071
1091
  // workspaces) that all contain copies of the same memory files. The
@@ -1105,7 +1125,7 @@ export const memoryTools = [
1105
1125
  // #1884 — sanitize key so memory_delete can later remove it. Without
1106
1126
  // this, dangerous chars from frontmatter `name` strand the key.
1107
1127
  const key = sanitizeMemoryKey(`claude:${memFile.project}:${name}`);
1108
- const stored = await storeEntry({ key, value: body, namespace: ns, generateEmbeddingFlag: true });
1128
+ const stored = await storeEntry({ key, value: body, namespace: ns, generateEmbeddingFlag: true, upsert: true });
1109
1129
  if (!stored.success)
1110
1130
  throw new Error(stored.error || `Failed to store ${key}`);
1111
1131
  imported++;
@@ -1120,15 +1140,19 @@ export const memoryTools = [
1120
1140
  // #1884 — sanitize so any dangerous chars in the heading don't
1121
1141
  // produce keys memory_delete will reject.
1122
1142
  const key = sanitizeMemoryKey(`claude:${memFile.project}:${name}:${sectionTitle.slice(0, 50)}`);
1123
- const stored = await storeEntry({ key, value: sectionBody, namespace: ns, generateEmbeddingFlag: true });
1143
+ const stored = await storeEntry({ key, value: sectionBody, namespace: ns, generateEmbeddingFlag: true, upsert: true });
1124
1144
  if (!stored.success)
1125
1145
  throw new Error(stored.error || `Failed to store ${key}`);
1126
1146
  imported++;
1127
1147
  }
1128
1148
  }
1129
1149
  }
1130
- catch {
1150
+ catch (err) {
1131
1151
  skipped++;
1152
+ // Say why a file was skipped instead of returning a bare failure (#4197).
1153
+ if (importErrors.length < 5) {
1154
+ importErrors.push(`${memFile.path}: ${err instanceof Error ? err.message : String(err)}`);
1155
+ }
1132
1156
  }
1133
1157
  }
1134
1158
  // AUDIT #3: report the embedding backend truthfully — a hash-fallback
@@ -1147,6 +1171,7 @@ export const memoryTools = [
1147
1171
  skipped,
1148
1172
  duplicatesSkipped,
1149
1173
  excludedByPattern,
1174
+ ...(importErrors.length > 0 ? { errors: importErrors } : {}),
1150
1175
  files: memoryFiles.length,
1151
1176
  projects: projects.size,
1152
1177
  namespace: ns,
@@ -399,7 +399,7 @@ export const metaharnessTools = [
399
399
  },
400
400
  // ───────────────────────────────────────────────────────────────────────
401
401
  // ADR-153 — @metaharness/darwin integration (3 tools).
402
- // Backed by the separate `@metaharness/darwin@~0.10.2` npm package, NOT
402
+ // Backed by the separate `@metaharness/darwin@~0.12.0` npm package, NOT
403
403
  // the umbrella `metaharness`. Plugin scripts shell out via _darwin.mjs.
404
404
  // Same {success, data, degraded, exitCode} contract.
405
405
  // ───────────────────────────────────────────────────────────────────────
@@ -908,9 +908,6 @@ export const neuralTools = [
908
908
  if (!v.valid)
909
909
  return { success: false, error: v.error };
910
910
  }
911
- // Resolve provider so embeddingProvider in the result matches the old
912
- // eager-init reporting.
913
- await ensureEmbeddings();
914
911
  const store = loadNeuralStore();
915
912
  const method = input.method || 'quantize';
916
913
  const targetReduction = input.targetSize || 0.5;
@@ -919,38 +916,25 @@ export const neuralTools = [
919
916
  return { success: false, error: 'No patterns to compress. Train patterns first with neural_train.' };
920
917
  }
921
918
  const beforeCount = patterns.length;
922
- const beforeSize = patterns.reduce((s, p) => s + (p.embedding?.length || 0) * 4, 0); // Float32 = 4 bytes
923
919
  if (method === 'quantize') {
924
- try {
925
- const { quantizeInt8, getQuantizationStats } = await import('../memory/memory-initializer.js');
926
- let totalCompressed = 0;
927
- for (const pattern of patterns) {
928
- if (pattern.embedding && pattern.embedding.length > 0) {
929
- const stats = getQuantizationStats(pattern.embedding);
930
- const quantized = quantizeInt8(pattern.embedding);
931
- // Store quantized metadata (keep original embedding for search)
932
- pattern._quantized = {
933
- scale: quantized.scale,
934
- zeroPoint: quantized.zeroPoint,
935
- compressionRatio: stats.compressionRatio,
936
- };
937
- totalCompressed++;
938
- }
939
- }
940
- saveNeuralStore(store);
941
- return {
942
- success: true, _real: true, method,
943
- embeddingProvider: embeddingServiceName,
944
- patternsCompressed: totalCompressed,
945
- compressionRatio: '3.92x (Int8)',
946
- beforeBytes: beforeSize,
947
- afterBytes: Math.round(beforeSize / 3.92),
948
- };
949
- }
950
- catch {
951
- return { success: false, error: 'Quantization requires memory-initializer. Run `memory init` first.' };
952
- }
920
+ // Neural-store readers consume `embedding` as a full-precision number[]
921
+ // (search, prediction, deduplication, and optimization). The former
922
+ // implementation discarded the Int8 vector, persisted only metadata,
923
+ // and reported a calculated 3.92x reduction. Refuse until the store has
924
+ // a persisted representation with a reader-side decode contract.
925
+ const storedBytes = readFileSync(getNeuralPath()).byteLength;
926
+ return {
927
+ success: false,
928
+ error: 'Quantized embeddings are not persisted: neural-store readers require full-precision embedding arrays.',
929
+ method,
930
+ patternsCompressed: 0,
931
+ beforeBytes: storedBytes,
932
+ afterBytes: storedBytes,
933
+ compressionRatio: '1.00x (unchanged JSON storage)',
934
+ };
953
935
  }
936
+ // Resolve provider only for operations that actually use embeddings.
937
+ await ensureEmbeddings();
954
938
  if (method === 'prune') {
955
939
  // Prune patterns with low usage count below threshold (targetReduction as min usage)
956
940
  const threshold = targetReduction;
@@ -1136,24 +1120,10 @@ export const neuralTools = [
1136
1120
  if (duplicatesRemoved > 0)
1137
1121
  actions.push(`Removed ${duplicatesRemoved} near-duplicate patterns`);
1138
1122
  }
1139
- // memory / balanced: quantize large embeddings
1123
+ // memory / balanced: report the unsupported persisted representation
1124
+ // instead of attaching metadata for a discarded Int8 vector.
1140
1125
  if (target === 'memory' || target === 'balanced') {
1141
- try {
1142
- const { quantizeInt8, getQuantizationStats } = await import('../memory/memory-initializer.js');
1143
- for (const p of Object.values(store.patterns)) {
1144
- if (p.embedding && p.embedding.length > 0 && !p._quantized) {
1145
- const stats = getQuantizationStats(p.embedding);
1146
- const q = quantizeInt8(p.embedding);
1147
- p._quantized = { scale: q.scale, zeroPoint: q.zeroPoint, compressionRatio: stats.compressionRatio };
1148
- patternsQuantized++;
1149
- }
1150
- }
1151
- if (patternsQuantized > 0)
1152
- actions.push(`Quantized ${patternsQuantized} pattern embeddings (Int8, ~3.92x)`);
1153
- }
1154
- catch {
1155
- actions.push('Quantization skipped (memory-initializer not available)');
1156
- }
1126
+ actions.push('Quantization skipped: neural-store readers require persisted full-precision embedding arrays.');
1157
1127
  }
1158
1128
  // accuracy / balanced: prune low-usage, zero-embedding patterns
1159
1129
  if (target === 'accuracy' || target === 'balanced') {
@@ -24,9 +24,11 @@ export const policyTools = [
24
24
  category: 'security',
25
25
  inputSchema: {
26
26
  type: 'object',
27
- properties: {},
27
+ properties: {
28
+ full: { type: 'boolean', description: 'Also replay the archived receipts to verify the complete historical ledger, not just the retained hot tail (#3164)' },
29
+ },
28
30
  },
29
- handler: async (_input, context) => {
31
+ handler: async (input, context) => {
30
32
  const root = typeof context?.projectRoot === 'string' ? context.projectRoot : process.cwd();
31
33
  const state = loadPolicyState(root);
32
34
  return {
@@ -39,7 +41,7 @@ export const policyTools = [
39
41
  approvals: state.approvals.length,
40
42
  receipts: state.receipts.length,
41
43
  },
42
- ledger: await verifyPolicyLedger(root),
44
+ ledger: await verifyPolicyLedger(root, { full: input.full === true }),
43
45
  };
44
46
  },
45
47
  },
@@ -16,6 +16,7 @@ import { createRequire } from 'node:module';
16
16
  import { join } from 'node:path';
17
17
  import { resolveTrainingBackend } from '../ruvector/lora-adapter.js';
18
18
  import { getProjectCwd } from '../mcp-tools/types.js';
19
+ import { withStatsFileLock } from './stats-file-lock.js';
19
20
  // ============================================================================
20
21
  // Persistence Configuration
21
22
  // ============================================================================
@@ -664,11 +665,9 @@ function bumpStat(key, n = 1) {
664
665
  }
665
666
  function readStatsFile() {
666
667
  try {
667
- const path = getStatsPath();
668
- if (!existsSync(path))
669
- return null;
670
- const data = JSON.parse(readFileSync(path, 'utf-8'));
671
- return data && typeof data === 'object' ? data : null;
668
+ // Read-only callers keep their unavailable-on-error contract, while invalid
669
+ // disk values cannot poison local state and a later repaired-file flush.
670
+ return readStatsForWrite(getStatsPath());
672
671
  }
673
672
  catch {
674
673
  return null;
@@ -701,33 +700,79 @@ function maxTimestamp(a, b) {
701
700
  return a;
702
701
  return Math.max(a, b);
703
702
  }
704
- /**
705
- * Save stats to disk: merge this process's unsaved deltas into whatever is
706
- * currently on disk (read-merge-write, atomic rename), then adopt the merged
707
- * totals locally so this process also sees other writers' progress.
708
- */
703
+ /** Missing files are first use; unreadable or invalid files must not be replaced. */
704
+ function readStatsForWrite(path) {
705
+ let text;
706
+ try {
707
+ text = readFileSync(path, 'utf-8');
708
+ }
709
+ catch (error) {
710
+ if (error.code === 'ENOENT')
711
+ return null;
712
+ throw error;
713
+ }
714
+ let data;
715
+ try {
716
+ data = JSON.parse(text);
717
+ }
718
+ catch {
719
+ throw Object.assign(new Error('Invalid learning stats JSON'), { code: 'EINVALIDSTATS' });
720
+ }
721
+ if (!data || typeof data !== 'object' || Array.isArray(data)) {
722
+ throw Object.assign(new Error('Invalid learning stats object'), { code: 'EINVALIDSTATS' });
723
+ }
724
+ for (const key of ['trajectoriesRecorded', 'patternsLearned', 'signalsProcessed']) {
725
+ const value = data[key];
726
+ // Older files may omit a counter, but present counters must be exact counts.
727
+ if (value === undefined)
728
+ continue;
729
+ if (typeof value !== 'number' || !Number.isSafeInteger(value) || value < 0) {
730
+ throw Object.assign(new Error(`Invalid learning stats field: ${key}`), { code: 'EINVALIDSTATS' });
731
+ }
732
+ }
733
+ const timestamp = data.lastAdaptation;
734
+ if (timestamp != null && (typeof timestamp !== 'number' || !Number.isFinite(timestamp) || timestamp < 0)) {
735
+ throw Object.assign(new Error('Invalid learning stats timestamp'), { code: 'EINVALIDSTATS' });
736
+ }
737
+ return data;
738
+ }
739
+ /** Merge pending deltas under the cooperating-writer lock, then adopt committed totals. */
709
740
  function savePersistedStats() {
741
+ let statsPath;
710
742
  try {
711
- ensureDataDir();
712
- const path = getStatsPath();
713
- const disk = readStatsFile();
714
- const merged = disk
715
- ? {
716
- trajectoriesRecorded: (disk.trajectoriesRecorded ?? 0) + pendingDelta.trajectoriesRecorded,
717
- patternsLearned: (disk.patternsLearned ?? 0) + pendingDelta.patternsLearned,
718
- signalsProcessed: (disk.signalsProcessed ?? 0) + pendingDelta.signalsProcessed,
719
- lastAdaptation: maxTimestamp(disk.lastAdaptation ?? null, globalStats.lastAdaptation),
743
+ statsPath = join(ensureDataDir(), 'stats.json');
744
+ withStatsFileLock(statsPath, (path) => {
745
+ const disk = readStatsForWrite(path);
746
+ const merged = disk
747
+ ? {
748
+ trajectoriesRecorded: (disk.trajectoriesRecorded ?? 0) + pendingDelta.trajectoriesRecorded,
749
+ patternsLearned: (disk.patternsLearned ?? 0) + pendingDelta.patternsLearned,
750
+ signalsProcessed: (disk.signalsProcessed ?? 0) + pendingDelta.signalsProcessed,
751
+ lastAdaptation: maxTimestamp(disk.lastAdaptation ?? null, globalStats.lastAdaptation),
752
+ }
753
+ : { ...globalStats };
754
+ for (const key of ['trajectoriesRecorded', 'patternsLearned', 'signalsProcessed']) {
755
+ if (!Number.isSafeInteger(merged[key]) || merged[key] < 0) {
756
+ throw Object.assign(new Error('Learning stats counter overflow'), { code: 'EINVALIDSTATS' });
757
+ }
758
+ }
759
+ if (merged.lastAdaptation != null &&
760
+ (!Number.isFinite(merged.lastAdaptation) || merged.lastAdaptation < 0)) {
761
+ throw Object.assign(new Error('Invalid merged learning stats timestamp'), { code: 'EINVALIDSTATS' });
720
762
  }
721
- : { ...globalStats };
722
- const tmp = `${path}.${process.pid}.tmp`;
723
- writeFileSync(tmp, JSON.stringify(merged, null, 2), 'utf-8');
724
- renameSync(tmp, path);
725
- globalStats = merged;
726
- pendingDelta = { trajectoriesRecorded: 0, patternsLearned: 0, signalsProcessed: 0 };
727
- persistedStatsLoaded = true;
763
+ const tmp = `${path}.${process.pid}.tmp`;
764
+ writeFileSync(tmp, JSON.stringify(merged, null, 2), 'utf-8');
765
+ renameSync(tmp, path);
766
+ // Commit local state before release: a failed release must not replay it.
767
+ globalStats = merged;
768
+ pendingDelta = { trajectoriesRecorded: 0, patternsLearned: 0, signalsProcessed: 0 };
769
+ persistedStatsLoaded = true;
770
+ });
728
771
  }
729
- catch {
730
- // Ignore save errors (deltas are retained for the next save)
772
+ catch (error) {
773
+ const { code = 'UNKNOWN', path = statsPath || 'neural stats directory' } = error;
774
+ // Do not print file contents, which may be malformed or user-controlled.
775
+ console.warn(`[intelligence] Could not persist learning stats at ${path} (${code}); pending increments retained`);
731
776
  }
732
777
  }
733
778
  /**
@@ -1015,13 +1060,15 @@ export async function recordTrajectory(steps, verdict) {
1015
1060
  const avgQuality = verdict === 'success' ? 1.0 : verdict === 'partial' ? 0.5 : 0.0;
1016
1061
  ruvllmCoord.recordTrajectory({
1017
1062
  steps: enrichedSteps.map(s => ({
1018
- state: s.content,
1019
- action: s.type,
1020
- reward: avgQuality,
1021
- embedding: s.embedding || []
1063
+ type: ['query', 'route', 'generate', 'memory', 'feedback'].includes(s.type)
1064
+ ? s.type : 'generate',
1065
+ input: s.content,
1066
+ output: '',
1067
+ durationMs: 0,
1068
+ confidence: avgQuality
1022
1069
  })),
1023
1070
  totalReward: avgQuality,
1024
- success: verdict === 'success'
1071
+ outcome: verdict
1025
1072
  });
1026
1073
  }
1027
1074
  catch {
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Serialize cooperating local writers without stealing a paused writer's lock.
3
+ * Abnormal termination can leave an orphan: recovery requires confirming that
4
+ * no writer still owns the directory before removing it. Never age-reap it.
5
+ */
6
+ export declare function withStatsFileLock<T>(file: string, write: (canonicalFile: string) => T): T;
7
+ //# sourceMappingURL=stats-file-lock.d.ts.map
@@ -0,0 +1,53 @@
1
+ import { mkdirSync, realpathSync, rmdirSync } from 'node:fs';
2
+ import { basename, dirname, join } from 'node:path';
3
+ /**
4
+ * Serialize cooperating local writers without stealing a paused writer's lock.
5
+ * Abnormal termination can leave an orphan: recovery requires confirming that
6
+ * no writer still owns the directory before removing it. Never age-reap it.
7
+ */
8
+ export function withStatsFileLock(file, write) {
9
+ const canonicalFile = join(realpathSync(dirname(file)), basename(file));
10
+ const lock = `${canonicalFile}.lock`;
11
+ const deadline = performance.now() + 1000;
12
+ const sleeper = new Int32Array(new SharedArrayBuffer(4));
13
+ for (;;) {
14
+ try {
15
+ mkdirSync(lock, { mode: 0o700 });
16
+ break;
17
+ }
18
+ catch (error) {
19
+ if (error.code !== 'EEXIST')
20
+ throw error;
21
+ const remaining = deadline - performance.now();
22
+ if (remaining <= 0) {
23
+ throw Object.assign(new Error('Learning stats lock is busy'), { code: 'ELOCKED', path: lock });
24
+ }
25
+ Atomics.wait(sleeper, 0, 0, Math.min(10, remaining));
26
+ }
27
+ }
28
+ const cleanup = () => {
29
+ // Only remove our empty directory. Never recursively remove foreign data.
30
+ rmdirSync(lock);
31
+ };
32
+ const onExit = () => {
33
+ try {
34
+ cleanup();
35
+ }
36
+ catch { /* abnormal exits may require deliberate recovery */ }
37
+ };
38
+ process.once('exit', onExit);
39
+ try {
40
+ return write(canonicalFile);
41
+ }
42
+ finally {
43
+ process.removeListener('exit', onExit);
44
+ try {
45
+ cleanup();
46
+ }
47
+ catch (error) {
48
+ const code = error.code || 'UNKNOWN';
49
+ console.warn(`[intelligence] Could not release learning stats lock at ${lock} (${code}); verify ownership before recovery`);
50
+ }
51
+ }
52
+ }
53
+ //# sourceMappingURL=stats-file-lock.js.map