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.
- package/.claude/proven-config.json +1 -1
- package/README.md +12 -3
- package/node_modules/@claude-flow/codex/dist/cli.js +0 -0
- package/node_modules/@claude-flow/plugin-agent-federation/dist/bin.js +0 -0
- package/node_modules/@claude-flow/security/dist/input-validator.d.ts +6 -6
- package/node_modules/@claude-flow/security/dist/policy/engine.d.ts +17 -1
- package/node_modules/@claude-flow/security/dist/policy/engine.d.ts.map +1 -1
- package/node_modules/@claude-flow/security/dist/policy/engine.js +99 -24
- package/node_modules/@claude-flow/security/dist/policy/engine.js.map +1 -1
- package/node_modules/@claude-flow/security/dist/policy/types.d.ts +14 -1
- package/node_modules/@claude-flow/security/dist/policy/types.d.ts.map +1 -1
- package/node_modules/@claude-flow/security/dist/policy/types.js +3 -1
- package/node_modules/@claude-flow/security/dist/policy/types.js.map +1 -1
- package/package.json +2 -2
- package/v3/@claude-flow/cli/README.md +173 -62
- package/v3/@claude-flow/cli/catalog-manifest.json +5 -5
- package/v3/@claude-flow/cli/dist/src/commands/daemon.js +8 -2
- package/v3/@claude-flow/cli/dist/src/commands/doctor.js +95 -18
- package/v3/@claude-flow/cli/dist/src/commands/policy.js +6 -1
- package/v3/@claude-flow/cli/dist/src/index.js +5 -5
- package/v3/@claude-flow/cli/dist/src/init/settings-generator.js +4 -58
- package/v3/@claude-flow/cli/dist/src/mcp-tools/memory-tools.d.ts +1 -1
- package/v3/@claude-flow/cli/dist/src/mcp-tools/memory-tools.js +30 -5
- package/v3/@claude-flow/cli/dist/src/mcp-tools/metaharness-tools.js +1 -1
- package/v3/@claude-flow/cli/dist/src/mcp-tools/neural-tools.js +20 -50
- package/v3/@claude-flow/cli/dist/src/mcp-tools/policy-tools.js +5 -3
- package/v3/@claude-flow/cli/dist/src/memory/intelligence.js +80 -33
- package/v3/@claude-flow/cli/dist/src/memory/stats-file-lock.d.ts +7 -0
- package/v3/@claude-flow/cli/dist/src/memory/stats-file-lock.js +53 -0
- package/v3/@claude-flow/cli/dist/src/missions/schemas.d.ts +113 -113
- package/v3/@claude-flow/cli/dist/src/mods/command-registry/schema.d.ts +193 -193
- package/v3/@claude-flow/cli/dist/src/services/distill-oracle.d.ts +1 -1
- package/v3/@claude-flow/cli/dist/src/services/distill-oracle.js +2 -2
- package/v3/@claude-flow/cli/dist/src/services/policy-anchor-log.d.ts +13 -0
- package/v3/@claude-flow/cli/dist/src/services/policy-anchor-log.js +242 -0
- package/v3/@claude-flow/cli/dist/src/services/policy-archive-scan.d.ts +7 -0
- package/v3/@claude-flow/cli/dist/src/services/policy-archive-scan.js +50 -0
- package/v3/@claude-flow/cli/dist/src/services/policy-ledger-anchor.d.ts +14 -8
- package/v3/@claude-flow/cli/dist/src/services/policy-ledger-anchor.js +356 -40
- package/v3/@claude-flow/cli/dist/src/services/policy-ledger-audit.d.ts +22 -0
- package/v3/@claude-flow/cli/dist/src/services/policy-ledger-audit.js +260 -0
- package/v3/@claude-flow/cli/dist/src/services/policy-ledger-record.d.ts +3 -0
- package/v3/@claude-flow/cli/dist/src/services/policy-ledger-record.js +22 -0
- package/v3/@claude-flow/cli/dist/src/services/policy-runtime.d.ts +1 -0
- package/v3/@claude-flow/cli/dist/src/services/policy-runtime.js +261 -40
- 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.
|
|
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
|
|
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
|
|
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 —
|
|
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
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
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:
|
|
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
|
-
|
|
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 (
|
|
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
|
-
|
|
668
|
-
|
|
669
|
-
|
|
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
|
-
|
|
706
|
-
|
|
707
|
-
|
|
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
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
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
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
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
|
-
|
|
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
|