@claude-flow/cli 3.56.10 → 3.57.0

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 (68) hide show
  1. package/.claude/helpers/.helpers-version +1 -1
  2. package/.claude/helpers/helpers.manifest.json +2 -2
  3. package/catalog-manifest.json +2 -2
  4. package/dist/src/commands/daemon.js +8 -2
  5. package/dist/src/commands/doctor.js +95 -18
  6. package/dist/src/commands/policy.js +6 -1
  7. package/dist/src/index.js +5 -5
  8. package/dist/src/init/settings-generator.js +4 -58
  9. package/dist/src/mcp-tools/memory-tools.d.ts +1 -1
  10. package/dist/src/mcp-tools/memory-tools.js +30 -5
  11. package/dist/src/mcp-tools/metaharness-tools.js +1 -1
  12. package/dist/src/mcp-tools/neural-tools.js +20 -50
  13. package/dist/src/mcp-tools/policy-tools.js +5 -3
  14. package/dist/src/memory/intelligence.js +80 -33
  15. package/dist/src/memory/stats-file-lock.d.ts +7 -0
  16. package/dist/src/memory/stats-file-lock.js +53 -0
  17. package/dist/src/services/distill-oracle.d.ts +1 -1
  18. package/dist/src/services/distill-oracle.js +2 -2
  19. package/dist/src/services/policy-anchor-log.d.ts +13 -0
  20. package/dist/src/services/policy-anchor-log.js +242 -0
  21. package/dist/src/services/policy-archive-scan.d.ts +7 -0
  22. package/dist/src/services/policy-archive-scan.js +50 -0
  23. package/dist/src/services/policy-ledger-anchor.d.ts +14 -8
  24. package/dist/src/services/policy-ledger-anchor.js +356 -40
  25. package/dist/src/services/policy-ledger-audit.d.ts +22 -0
  26. package/dist/src/services/policy-ledger-audit.js +260 -0
  27. package/dist/src/services/policy-ledger-record.d.ts +3 -0
  28. package/dist/src/services/policy-ledger-record.js +22 -0
  29. package/dist/src/services/policy-runtime.d.ts +1 -0
  30. package/dist/src/services/policy-runtime.js +261 -40
  31. package/node_modules/@claude-flow/security/dist/input-validator.d.ts +6 -6
  32. package/node_modules/@claude-flow/security/dist/policy/engine.d.ts +17 -1
  33. package/node_modules/@claude-flow/security/dist/policy/engine.d.ts.map +1 -1
  34. package/node_modules/@claude-flow/security/dist/policy/engine.js +99 -24
  35. package/node_modules/@claude-flow/security/dist/policy/engine.js.map +1 -1
  36. package/node_modules/@claude-flow/security/dist/policy/types.d.ts +14 -1
  37. package/node_modules/@claude-flow/security/dist/policy/types.d.ts.map +1 -1
  38. package/node_modules/@claude-flow/security/dist/policy/types.js +3 -1
  39. package/node_modules/@claude-flow/security/dist/policy/types.js.map +1 -1
  40. package/package.json +2 -2
  41. package/plugins/ruflo-console/dist/tui.mjs +153 -153
  42. package/plugins/ruflo-metaharness/hooks/screen.ts +7 -1
  43. package/plugins/ruflo-metaharness/scripts/_darwin.mjs +4 -4
  44. package/plugins/ruflo-metaharness/.claude-flow/daemon-state.json +0 -179
  45. package/plugins/ruflo-metaharness/.claude-flow/daemon.pid +0 -1
  46. package/plugins/ruflo-metaharness/.claude-flow/logs/daemon.log +0 -32
  47. package/plugins/ruflo-metaharness/.claude-flow/metrics/backup.json +0 -23
  48. package/plugins/ruflo-metaharness/.claude-flow/metrics/codebase-map.json +0 -11
  49. package/plugins/ruflo-metaharness/.claude-flow/metrics/consolidation.json +0 -16
  50. package/plugins/ruflo-metaharness/.claude-flow/metrics/harness-loop.json +0 -91
  51. package/plugins/ruflo-metaharness/.claude-flow/metrics/performance.json +0 -17
  52. package/plugins/ruflo-metaharness/.claude-flow/metrics/security-audit.json +0 -12
  53. package/plugins/ruflo-metaharness/.claude-flow/metrics/test-gaps.json +0 -8
  54. package/plugins/ruflo-metaharness/.claude-flow/neural/patterns.json +0 -2809
  55. package/plugins/ruflo-metaharness/.claude-flow/neural/stats.json +0 -6
  56. package/plugins/ruflo-metaharness/.claude-flow/policy/state.json +0 -11
  57. package/plugins/ruflo-metaharness/.swarm/agentdb-memory.db +0 -0
  58. package/plugins/ruflo-metaharness/.swarm/agentdb-memory.db-shm +0 -0
  59. package/plugins/ruflo-metaharness/.swarm/agentdb-memory.db-wal +0 -0
  60. package/plugins/ruflo-metaharness/.swarm/backups/memory-2026-10-14T16-09-38-164Z.db +0 -0
  61. package/plugins/ruflo-metaharness/.swarm/backups/store-agentdb-memory.db-2026-10-14T16-09-38-166Z.db +0 -0
  62. package/plugins/ruflo-metaharness/.swarm/ewc-fisher.json +0 -397
  63. package/plugins/ruflo-metaharness/.swarm/memory.db +0 -0
  64. package/plugins/ruflo-metaharness/.swarm/memory.db-shm +0 -0
  65. package/plugins/ruflo-metaharness/.swarm/memory.db-wal +0 -0
  66. package/plugins/ruflo-metaharness/agentdb.rvf +0 -0
  67. package/plugins/ruflo-metaharness/agentdb.rvf.lock +0 -0
  68. package/plugins/ruflo-metaharness/ruvector.db +0 -0
@@ -3,20 +3,34 @@
3
3
  // callback server (an ESM `node:http` import that materialises http's lazy
4
4
  // undici getters), bcryptjs, keychain and credential helpers that the policy
5
5
  // runtime never uses.
6
- import { AgenticPolicyEngine, createLegacyCompatibleState, } from '@claude-flow/security/policy/index';
6
+ import { AgenticPolicyEngine, POLICY_STATE_VERSION, createLegacyCompatibleState, } from '@claude-flow/security/policy/index';
7
7
  import { isMcpCallerAuthEnabled, decodeTokenEnvelope, publicKeyFromHex, verifyInvocationToken, } from '@claude-flow/security/mcp-caller-identity';
8
8
  import { createHash, createHmac, randomBytes, randomUUID, timingSafeEqual } from 'node:crypto';
9
9
  import { execFileSync } from 'node:child_process';
10
- import { closeSync, constants as fsConstants, existsSync, fstatSync, lstatSync, mkdirSync, openSync, readFileSync, readSync, readlinkSync, realpathSync, renameSync, statSync, unlinkSync, writeFileSync, } from 'node:fs';
10
+ import { closeSync, constants as fsConstants, existsSync, fstatSync, fsyncSync, ftruncateSync, writeSync, lstatSync, mkdirSync, openSync, readFileSync, readSync, readlinkSync, realpathSync, renameSync, unlinkSync, writeFileSync, } from 'node:fs';
11
11
  import { dirname, join, resolve } from 'node:path';
12
12
  import { hostname, userInfo } from 'node:os';
13
13
  import { syncPolicyProjection } from '../mods/policy-projection.js';
14
+ import { ATOMIC_JSON_WRITER } from './policy-anchor-log.js';
15
+ import { captureArchiveSnapshot, verifyFullPolicyLedger } from './policy-ledger-audit.js';
14
16
  import { assessAnchors, policyTrustRoot, recordAnchor } from './policy-ledger-anchor.js';
15
17
  const POLICY_DIR = join('.claude-flow', 'policy');
16
18
  const POLICY_FILE = 'state.json';
17
19
  const LOCK_FILE = 'state.lock';
18
- const LOCK_STALE_MS = 30_000;
20
+ const RECEIPT_ARCHIVE_FILE = 'receipts.ledger.jsonl';
19
21
  const LOCK_WAIT_MS = 5_000;
22
+ // #3164: without a bound, `state.receipts` grows forever and every
23
+ // transaction's verify/clone/serialize cost is O(all receipts ever issued) —
24
+ // eventually exceeding LOCK_WAIT_MS and timing out every MCP tool call.
25
+ // Pruned receipts are archived to RECEIPT_ARCHIVE_FILE, never discarded.
26
+ const DEFAULT_LEDGER_RETENTION = 2_000;
27
+ function ledgerRetention() {
28
+ const raw = process.env.CLAUDE_FLOW_POLICY_LEDGER_RETENTION;
29
+ if (!raw)
30
+ return DEFAULT_LEDGER_RETENTION;
31
+ const parsed = Number(raw);
32
+ return Number.isInteger(parsed) && parsed >= 0 ? parsed : DEFAULT_LEDGER_RETENTION;
33
+ }
20
34
  // A lock is ~170 bytes plus the hostname (at most 255 bytes).
21
35
  const LOCK_MAX_BYTES = 1_024;
22
36
  // Linux PID namespace of this process, e.g. `pid:[4026531836]`; undefined on
@@ -45,7 +59,7 @@ const BOOT_ID = (() => {
45
59
  function paths(projectRoot) {
46
60
  const root = resolve(projectRoot);
47
61
  const dir = join(root, POLICY_DIR);
48
- return { dir, state: join(dir, POLICY_FILE), lock: join(dir, LOCK_FILE) };
62
+ return { dir, state: join(dir, POLICY_FILE), lock: join(dir, LOCK_FILE), archive: join(dir, RECEIPT_ARCHIVE_FILE) };
49
63
  }
50
64
  function sleep(ms) {
51
65
  return new Promise((resolveSleep) => setTimeout(resolveSleep, ms));
@@ -68,7 +82,7 @@ function isPidAlive(pid) {
68
82
  /**
69
83
  * Reads a lock only if it is a small regular file: never follows a symlink,
70
84
  * never blocks on a FIFO or device, never reads more than LOCK_MAX_BYTES.
71
- * Anything else returns undefined and the caller keeps the mtime rule.
85
+ * Anything else returns undefined; unknown owners are preserved.
72
86
  */
73
87
  function readLockFile(lockPath) {
74
88
  if (!lstatSync(lockPath).isFile())
@@ -94,8 +108,8 @@ function readLockFile(lockPath) {
94
108
  * boot and in this PID namespace: anywhere else (a bwrap --unshare-pid
95
109
  * sandbox, a container, another machine on a shared filesystem) a live owner
96
110
  * also answers ESRCH. Locks without that identity (older ruflo, a crash
97
- * between create and write, anything unparsable) return false and keep the
98
- * mtime rule.
111
+ * between create and write, anything unparsable) return false and require
112
+ * explicit operator inspection rather than age-only takeover.
99
113
  */
100
114
  function lockOwnerIsDead(lockPath) {
101
115
  try {
@@ -127,6 +141,36 @@ function lockOwnerIsDead(lockPath) {
127
141
  return false;
128
142
  }
129
143
  }
144
+ /** Serialize dead-owner reclamation across processes. A normal acquirer can
145
+ * create state.lock after deletion, but the next reclaimer must re-probe that
146
+ * new owner under this guard. Guard orphans fail closed, requiring explicit
147
+ * operator recovery after validating their exact recorded owner and nonce.
148
+ * Unknown/foreign owners are never stolen merely because their mtime aged. */
149
+ function reclaimDeadLock(lockPath) {
150
+ const guard = `${lockPath}.reclaim`;
151
+ let fd;
152
+ try {
153
+ fd = openSync(guard, 'wx', 0o600);
154
+ }
155
+ catch {
156
+ return;
157
+ }
158
+ const ownership = JSON.stringify({ pid: process.pid, nonce: randomUUID(), host: hostname(), platform: process.platform, pidns: PID_NAMESPACE, boot: BOOT_ID });
159
+ try {
160
+ writeFileSync(fd, ownership);
161
+ if (lockOwnerIsDead(lockPath))
162
+ unlinkSync(lockPath);
163
+ }
164
+ catch { /* lock changed, unknown identity, or an inaccessible path */ }
165
+ finally {
166
+ closeSync(fd);
167
+ try {
168
+ if (readLockFile(guard) === ownership)
169
+ unlinkSync(guard);
170
+ }
171
+ catch { /* preserve an unknown guard */ }
172
+ }
173
+ }
130
174
  /**
131
175
  * Exported for ADR-406 mission storage, which takes the same per-file lock
132
176
  * (pid, pid namespace and boot id identify a dead owner) rather than a copy.
@@ -136,33 +180,37 @@ export async function acquireLock(lockPath) {
136
180
  while (Date.now() - started < LOCK_WAIT_MS) {
137
181
  try {
138
182
  const fd = openSync(lockPath, 'wx', 0o600);
139
- writeFileSync(fd, JSON.stringify({
183
+ const ownership = JSON.stringify({
140
184
  pid: process.pid,
141
185
  acquiredAt: Date.now(),
142
186
  host: hostname(),
143
187
  platform: process.platform,
144
188
  pidns: PID_NAMESPACE,
145
189
  boot: BOOT_ID,
146
- }));
147
- closeSync(fd);
190
+ nonce: randomUUID(),
191
+ });
192
+ try {
193
+ writeFileSync(fd, ownership);
194
+ }
195
+ finally {
196
+ closeSync(fd);
197
+ }
148
198
  return () => {
149
199
  try {
150
- unlinkSync(lockPath);
200
+ if (readLockFile(lockPath) === ownership)
201
+ unlinkSync(lockPath);
151
202
  }
152
203
  catch { /* already released */ }
153
204
  };
154
205
  }
155
206
  catch {
156
- try {
157
- if (lockOwnerIsDead(lockPath)
158
- || Date.now() - statSync(lockPath).mtimeMs > LOCK_STALE_MS)
159
- unlinkSync(lockPath);
160
- }
161
- catch { /* another process changed the lock */ }
207
+ reclaimDeadLock(lockPath);
162
208
  await sleep(10);
163
209
  }
164
210
  }
165
- throw new Error('policy-state-lock-timeout');
211
+ throw new Error(lockOwnerIsDead(lockPath)
212
+ ? 'policy-state-lock-timeout: dead owner; inspect exact owner of state.lock.reclaim before explicit recovery'
213
+ : 'policy-state-lock-timeout');
166
214
  }
167
215
  // #3398: Windows rename over a file another process holds open fails with
168
216
  // EPERM/EBUSY/EACCES. Retry (~1.3s total, inside LOCK_WAIT_MS), never leave the
@@ -170,17 +218,129 @@ export async function acquireLock(lockPath) {
170
218
  // and consumed approval uses, so dropping it would let an approval be reused.
171
219
  const RENAME_RETRY_DELAYS_MS = [25, 50, 100, 150, 250, 300, 400];
172
220
  const TRANSIENT_RENAME_CODES = new Set(['EPERM', 'EBUSY', 'EACCES']);
221
+ function mkdirDurably(dir) {
222
+ if (existsSync(dir))
223
+ return;
224
+ const parent = dirname(dir);
225
+ if (parent !== dir)
226
+ mkdirDurably(parent);
227
+ try {
228
+ mkdirSync(dir, { mode: 0o700 });
229
+ }
230
+ catch (error) {
231
+ if (error.code !== 'EEXIST')
232
+ throw error;
233
+ }
234
+ syncDirectory(parent);
235
+ }
236
+ function syncDirectory(dir) {
237
+ // Windows does not expose directory fsync through Node. File sync and
238
+ // atomic rename still apply there; POSIX failures must reach the caller.
239
+ if (process.platform === 'win32')
240
+ return;
241
+ const fd = openSync(dir, fsConstants.O_RDONLY | (fsConstants.O_DIRECTORY ?? 0));
242
+ try {
243
+ fsyncSync(fd);
244
+ }
245
+ finally {
246
+ closeSync(fd);
247
+ }
248
+ }
249
+ /** Recover only an incomplete, unacknowledged final append. All inspection,
250
+ * truncation, append and sync use the same nofollow regular-file descriptor. */
251
+ function appendArchiveDurably(file, receipts, verifier) {
252
+ const fd = openSync(file, fsConstants.O_RDWR | fsConstants.O_CREAT
253
+ | (fsConstants.O_NOFOLLOW ?? 0) | (fsConstants.O_NONBLOCK ?? 0), 0o600);
254
+ try {
255
+ const stat = fstatSync(fd);
256
+ if (!stat.isFile())
257
+ throw new Error('policy-ledger-archive-not-regular');
258
+ const limit = 64 * 1024;
259
+ const start = Math.max(0, stat.size - limit);
260
+ const buffer = Buffer.alloc(stat.size - start);
261
+ if (readSync(fd, buffer, 0, buffer.length, start) !== buffer.length)
262
+ throw new Error('policy-ledger-archive-short-read');
263
+ let end = stat.size;
264
+ const lastNewline = buffer.lastIndexOf(10);
265
+ if (buffer.length && buffer[buffer.length - 1] !== 10) {
266
+ if (lastNewline < 0 && start > 0)
267
+ throw new Error('policy-ledger-archive-tail-too-large');
268
+ const fragmentBytes = buffer.subarray(lastNewline + 1);
269
+ const fragment = fragmentBytes.toString('utf8');
270
+ try {
271
+ JSON.parse(fragment);
272
+ throw new Error('policy-ledger-archive-unterminated-record');
273
+ }
274
+ catch (error) {
275
+ if (!(error instanceof SyntaxError))
276
+ throw error;
277
+ }
278
+ // A parse failure alone grants no repair authority: the bytes must
279
+ // be an exact prefix of a receipt still available in the verified hot
280
+ // state and being retried by this prune.
281
+ if (!fragmentBytes.length || !receipts.some(receipt => {
282
+ const expected = Buffer.from(JSON.stringify(receipt));
283
+ return fragmentBytes.length < expected.length && expected.subarray(0, fragmentBytes.length).equals(fragmentBytes);
284
+ })) {
285
+ throw new Error('policy-ledger-archive-unrecoverable-tail');
286
+ }
287
+ end = start + lastNewline + 1;
288
+ }
289
+ const complete = buffer.subarray(0, end - start).toString('utf8');
290
+ const usable = start === 0 ? complete : complete.slice(complete.indexOf('\n') + 1);
291
+ for (const line of usable.split('\n')) {
292
+ if (!line.trim())
293
+ continue;
294
+ let record;
295
+ try {
296
+ record = JSON.parse(line);
297
+ }
298
+ catch {
299
+ throw new Error('policy-ledger-archive-parse-error');
300
+ }
301
+ if (verifier.verifyReceipt(record))
302
+ throw new Error('policy-ledger-archive-conflict');
303
+ }
304
+ if (end < stat.size) {
305
+ ftruncateSync(fd, end);
306
+ fsyncSync(fd);
307
+ }
308
+ const bytes = Buffer.from(`${receipts.map(receipt => JSON.stringify(receipt)).join('\n')}\n`);
309
+ let written = 0;
310
+ while (written < bytes.length) {
311
+ const count = writeSync(fd, bytes, written, bytes.length - written, end + written);
312
+ if (!count)
313
+ throw new Error('policy-ledger-archive-short-write');
314
+ written += count;
315
+ }
316
+ fsyncSync(fd);
317
+ syncDirectory(dirname(file));
318
+ }
319
+ finally {
320
+ closeSync(fd);
321
+ }
322
+ }
173
323
  async function writeJsonAtomic(file, value) {
174
- mkdirSync(dirname(file), { recursive: true, mode: 0o700 });
324
+ mkdirDurably(dirname(file));
175
325
  const temporary = `${file}.${process.pid}.${randomUUID()}.tmp`;
176
326
  let renamed = false;
177
327
  try {
178
- writeFileSync(temporary, `${JSON.stringify(value, null, 2)}\n`, { mode: 0o600 });
328
+ const fd = openSync(temporary, 'wx', 0o600);
329
+ try {
330
+ if (value && typeof value[ATOMIC_JSON_WRITER] === 'function')
331
+ value[ATOMIC_JSON_WRITER](fd);
332
+ else
333
+ writeFileSync(fd, `${JSON.stringify(value, null, 2)}\n`);
334
+ fsyncSync(fd);
335
+ }
336
+ finally {
337
+ closeSync(fd);
338
+ }
179
339
  for (let attempt = 0;; attempt++) {
180
340
  try {
181
341
  renameSync(temporary, file);
182
342
  renamed = true;
183
- return;
343
+ break;
184
344
  }
185
345
  catch (error) {
186
346
  const code = error?.code ?? '';
@@ -189,6 +349,9 @@ async function writeJsonAtomic(file, value) {
189
349
  await sleep(RENAME_RETRY_DELAYS_MS[attempt]);
190
350
  }
191
351
  }
352
+ // A post-rename sync failure is a publication failure, not a rename
353
+ // failure. Never retry with a temporary file already consumed by rename.
354
+ syncDirectory(dirname(file));
192
355
  }
193
356
  finally {
194
357
  if (!renamed) {
@@ -210,8 +373,16 @@ function trustKey(projectRoot, create) {
210
373
  if (!existsSync(key)) {
211
374
  if (!create)
212
375
  return undefined;
213
- mkdirSync(dirname(key), { recursive: true, mode: 0o700 });
214
- writeFileSync(key, randomBytes(32), { mode: 0o600, flag: 'wx' });
376
+ mkdirDurably(dirname(key));
377
+ const fd = openSync(key, 'wx', 0o600);
378
+ try {
379
+ writeFileSync(fd, randomBytes(32));
380
+ fsyncSync(fd);
381
+ }
382
+ finally {
383
+ closeSync(fd);
384
+ }
385
+ syncDirectory(dirname(key));
215
386
  }
216
387
  const material = readFileSync(key);
217
388
  if (material.length !== 32)
@@ -238,11 +409,11 @@ function verifyStateAnchor(projectRoot, state) {
238
409
  throw new Error('policy-state-authentication-failed');
239
410
  }
240
411
  }
241
- async function writePolicyState(projectRoot, statePath, state, anchorEvent = 'append', by) {
412
+ async function writePolicyState(projectRoot, statePath, state, anchorEvent = 'append', by, assessedTip) {
242
413
  await writePolicyStateFiles(projectRoot, statePath, state);
243
414
  // #3602: the second anchor is written after the state, so a crash leaves the
244
415
  // state AHEAD of the log (caught up on the next write), never behind it.
245
- await recordAnchor(projectRoot, state, writeJsonAtomic, anchorEvent, by);
416
+ await recordAnchor(projectRoot, state, writeJsonAtomic, anchorEvent, by, writeJsonAtomic, assessedTip);
246
417
  // ADR-404: the ruflo mod reads Claude Code tool rules from a small
247
418
  // projection, never from state.json. Written only after the state and its
248
419
  // anchor are safely down; a failure here never fails the state write. A
@@ -316,7 +487,7 @@ export function loadPolicyState(projectRoot = process.cwd()) {
316
487
  return createLegacyCompatibleState(detectLegacyCapabilities(projectRoot));
317
488
  }
318
489
  const parsed = JSON.parse(readFileSync(target.state, 'utf8'));
319
- if (parsed.version !== 1 || !Array.isArray(parsed.rules) || !Array.isArray(parsed.receipts)) {
490
+ if ((parsed.version !== 1 && parsed.version !== POLICY_STATE_VERSION) || !Array.isArray(parsed.rules) || !Array.isArray(parsed.receipts)) {
320
491
  throw new Error(`unsupported-policy-state-version:${String(parsed.version)}`);
321
492
  }
322
493
  verifyStateAnchor(projectRoot, parsed);
@@ -337,7 +508,7 @@ export async function autoMigratePolicyStateIfNeeded(projectRoot = process.cwd()
337
508
  if (!existsSync(join(resolve(projectRoot), '.claude-flow'))
338
509
  && !existsSync(join(resolve(projectRoot), '.swarm')))
339
510
  return { migrated: false };
340
- mkdirSync(target.dir, { recursive: true, mode: 0o700 });
511
+ mkdirDurably(target.dir);
341
512
  const release = await acquireLock(target.lock);
342
513
  try {
343
514
  if (!existsSync(target.state)) {
@@ -357,7 +528,7 @@ export async function autoMigratePolicyStateIfNeeded(projectRoot = process.cwd()
357
528
  }
358
529
  export async function withPolicyTransaction(projectRoot, operation, options = {}) {
359
530
  const target = paths(projectRoot);
360
- mkdirSync(target.dir, { recursive: true, mode: 0o700 });
531
+ mkdirDurably(target.dir);
361
532
  const release = await acquireLock(target.lock);
362
533
  try {
363
534
  const loaded = loadPolicyState(projectRoot);
@@ -376,10 +547,30 @@ export async function withPolicyTransaction(projectRoot, operation, options = {}
376
547
  : gate.result.error ?? 'policy-ledger-verification-failed');
377
548
  }
378
549
  const result = await operation(engine);
379
- if (!engine.verifyLedger().valid)
550
+ const verified = engine.verifyLedger();
551
+ if (!verified.valid)
380
552
  throw new Error('policy-ledger-verification-failed');
553
+ // Only archive the prefix already committed in loaded state. Archiving
554
+ // newly generated receipts before their first state publication would
555
+ // leave conflicting same-sequence records when a failed operation is
556
+ // retried with a different timestamp/request. Retention=0 and large
557
+ // batches therefore retain this transaction's new receipts until a
558
+ // subsequent transaction can archive their exact committed bytes.
559
+ const newlyAppended = verified.length - ((loaded.retainedFrom ?? 0) + loaded.receipts.length);
560
+ const pruned = engine.pruneReceipts(Math.max(ledgerRetention(), newlyAppended));
561
+ // #3164: archive BEFORE writing the shrunk state. If the process crashes
562
+ // between the two writes, the archive append already landed but
563
+ // `retainedFrom` on disk still reflects the pre-prune length, so the next
564
+ // prune recomputes and re-appends the SAME receipts — a harmless,
565
+ // detectable duplicate (same sequence, identical content), deduped by
566
+ // `verifyPolicyLedger({ full: true })`. The reverse order (state first)
567
+ // would instead risk a permanent gap if the process died in between,
568
+ // which is strictly worse for an audit ledger — never reorder this.
569
+ if (pruned.length > 0) {
570
+ appendArchiveDurably(target.archive, pruned, engine);
571
+ }
381
572
  const nextState = engine.exportState();
382
- await writePolicyState(projectRoot, target.state, nextState, gate.event);
573
+ await writePolicyState(projectRoot, target.state, nextState, gate.event, undefined, gate.anchorTip);
383
574
  return result;
384
575
  }
385
576
  finally {
@@ -439,7 +630,9 @@ export async function revokePolicyApproval(id, projectRoot = process.cwd()) {
439
630
  */
440
631
  function reconcileLedger(projectRoot, engine, options) {
441
632
  const state = engine.exportState();
442
- const length = state.receipts.length;
633
+ // #3164: the historical length includes anything pruned out of the hot
634
+ // array — `length` here must keep meaning "total receipts ever issued".
635
+ const length = (state.retainedFrom ?? 0) + state.receipts.length;
443
636
  const primary = typeof state.ledgerLength === 'number';
444
637
  const base = engine.verifyLedger();
445
638
  const fail = (error) => ({ result: { valid: false, length, error }, persist: false });
@@ -454,7 +647,7 @@ function reconcileLedger(projectRoot, engine, options) {
454
647
  ? { result: { ...base, secondaryAnchor: 'established-explicitly' }, persist: true, event: 'establish-anchor' }
455
648
  : { result: { ...base, secondaryAnchor: 'recorded-from-state' }, persist: true, event: 'migrated-from-state' };
456
649
  }
457
- return { result: base, persist: assessed.kind === 'ok' && assessed.behind, event: 'append' };
650
+ return { result: base, persist: assessed.kind === 'ok' && assessed.behind, event: 'append', anchorTip: assessed.kind === 'ok' ? assessed.last : undefined };
458
651
  }
459
652
  // No primary anchor.
460
653
  if (length === 0)
@@ -463,7 +656,7 @@ function reconcileLedger(projectRoot, engine, options) {
463
656
  // The second anchor is authentic and agrees with the chain: the deleted
464
657
  // primary can be restored from it. Truncation was already rejected above.
465
658
  engine.verifyLedger({ establishAnchor: true });
466
- return { result: { valid: true, length, secondaryAnchor: 'restored-primary' }, persist: true, event: 'append' };
659
+ return { result: { valid: true, length, secondaryAnchor: 'restored-primary' }, persist: true, event: 'append', anchorTip: assessed.last };
467
660
  }
468
661
  if (options.establishAnchor) {
469
662
  const established = engine.verifyLedger({ establishAnchor: true });
@@ -483,23 +676,51 @@ function reconcileLedger(projectRoot, engine, options) {
483
676
  */
484
677
  export async function verifyPolicyLedger(projectRoot = process.cwd(), options = {}) {
485
678
  const target = paths(projectRoot);
486
- mkdirSync(target.dir, { recursive: true, mode: 0o700 });
679
+ mkdirDurably(target.dir);
680
+ const engineOptions = {
681
+ signingKey: process.env.CLAUDE_FLOW_POLICY_SIGNING_KEY,
682
+ keyId: process.env.CLAUDE_FLOW_POLICY_KEY_ID,
683
+ };
684
+ let outcomeResult;
685
+ let snapshotState;
686
+ let archiveSnapshot;
687
+ // Capture the verified hot state and a checked regular archive descriptor
688
+ // plus immutable byte boundary under the writer lock. Historical read,
689
+ // authentication, deduplication and replay all happen after release, using
690
+ // bounded chunks and private disposable disk scratch instead of O(history)
691
+ // strings, receipt arrays or maps. The descriptor pins path replacements.
487
692
  const release = await acquireLock(target.lock);
488
693
  try {
489
- const engine = AgenticPolicyEngine.fromState(loadPolicyState(projectRoot), {
490
- signingKey: process.env.CLAUDE_FLOW_POLICY_SIGNING_KEY,
491
- keyId: process.env.CLAUDE_FLOW_POLICY_KEY_ID,
492
- });
694
+ const engine = AgenticPolicyEngine.fromState(loadPolicyState(projectRoot), engineOptions);
493
695
  const outcome = reconcileLedger(projectRoot, engine, options);
494
696
  if (outcome.persist) {
495
697
  const by = outcome.event === 'establish-anchor' ? userInfo().username : undefined;
496
- await writePolicyState(projectRoot, target.state, engine.exportState(), outcome.event, by);
698
+ await writePolicyState(projectRoot, target.state, engine.exportState(), outcome.event, by, outcome.anchorTip);
699
+ }
700
+ outcomeResult = outcome.result;
701
+ snapshotState = engine.exportState();
702
+ if (options.full && outcomeResult.valid) {
703
+ try {
704
+ archiveSnapshot = captureArchiveSnapshot(target.archive);
705
+ }
706
+ catch (error) {
707
+ outcomeResult = { valid: false, length: outcomeResult.length, error: error.message };
708
+ }
497
709
  }
498
- return outcome.result;
499
710
  }
500
711
  finally {
501
712
  release();
502
713
  }
714
+ if (!options.full || !outcomeResult.valid)
715
+ return outcomeResult;
716
+ try {
717
+ const full = await verifyFullPolicyLedger(archiveSnapshot, snapshotState, engineOptions);
718
+ return { ...full, secondaryAnchor: outcomeResult.secondaryAnchor };
719
+ }
720
+ finally {
721
+ if (archiveSnapshot !== undefined)
722
+ closeSync(archiveSnapshot.fd);
723
+ }
503
724
  }
504
725
  /**
505
726
  * Resolve the caller identity for `authorizeMcpTool`.
@@ -159,14 +159,14 @@ export declare const SpawnAgentSchema: z.ZodObject<{
159
159
  timeout: z.ZodOptional<z.ZodNumber>;
160
160
  }, "strip", z.ZodTypeAny, {
161
161
  type: "coder" | "reviewer" | "tester" | "planner" | "researcher" | "security-architect" | "security-auditor" | "memory-specialist" | "swarm-specialist" | "integration-architect" | "performance-engineer" | "core-architect" | "test-architect" | "queen-coordinator" | "project-coordinator";
162
- id?: string | undefined;
163
162
  config?: Record<string, unknown> | undefined;
164
163
  timeout?: number | undefined;
164
+ id?: string | undefined;
165
165
  }, {
166
166
  type: "coder" | "reviewer" | "tester" | "planner" | "researcher" | "security-architect" | "security-auditor" | "memory-specialist" | "swarm-specialist" | "integration-architect" | "performance-engineer" | "core-architect" | "test-architect" | "queen-coordinator" | "project-coordinator";
167
- id?: string | undefined;
168
167
  config?: Record<string, unknown> | undefined;
169
168
  timeout?: number | undefined;
169
+ id?: string | undefined;
170
170
  }>;
171
171
  /**
172
172
  * Task input schema
@@ -181,13 +181,13 @@ export declare const TaskInputSchema: z.ZodObject<{
181
181
  taskId: string;
182
182
  content: string;
183
183
  agentType: "coder" | "reviewer" | "tester" | "planner" | "researcher" | "security-architect" | "security-auditor" | "memory-specialist" | "swarm-specialist" | "integration-architect" | "performance-engineer" | "core-architect" | "test-architect" | "queen-coordinator" | "project-coordinator";
184
- priority?: "low" | "medium" | "high" | "critical" | undefined;
184
+ priority?: "critical" | "high" | "medium" | "low" | undefined;
185
185
  metadata?: Record<string, unknown> | undefined;
186
186
  }, {
187
187
  taskId: string;
188
188
  content: string;
189
189
  agentType: "coder" | "reviewer" | "tester" | "planner" | "researcher" | "security-architect" | "security-auditor" | "memory-specialist" | "swarm-specialist" | "integration-architect" | "performance-engineer" | "core-architect" | "test-architect" | "queen-coordinator" | "project-coordinator";
190
- priority?: "low" | "medium" | "high" | "critical" | undefined;
190
+ priority?: "critical" | "high" | "medium" | "low" | undefined;
191
191
  metadata?: Record<string, unknown> | undefined;
192
192
  }>;
193
193
  /**
@@ -234,16 +234,16 @@ export declare const ExecutorConfigSchema: z.ZodObject<{
234
234
  cwd: z.ZodOptional<z.ZodEffects<z.ZodEffects<z.ZodString, string, string>, string, string>>;
235
235
  allowSudo: z.ZodDefault<z.ZodBoolean>;
236
236
  }, "strip", z.ZodTypeAny, {
237
- timeout: number;
238
237
  allowedCommands: string[];
238
+ timeout: number;
239
239
  maxBuffer: number;
240
240
  allowSudo: boolean;
241
241
  blockedPatterns?: string[] | undefined;
242
242
  cwd?: string | undefined;
243
243
  }, {
244
244
  allowedCommands: string[];
245
- timeout?: number | undefined;
246
245
  blockedPatterns?: string[] | undefined;
246
+ timeout?: number | undefined;
247
247
  maxBuffer?: number | undefined;
248
248
  cwd?: string | undefined;
249
249
  allowSudo?: boolean | undefined;
@@ -1,4 +1,4 @@
1
- import type { BudgetLimit, LedgerVerification, PolicyApproval, PolicyDecision, PolicyEngineOptions, PolicyRequest, PolicyRule, PolicyState } from './types.js';
1
+ import type { BudgetLimit, LedgerVerification, PolicyApproval, PolicyDecision, PolicyEngineOptions, PolicyReceipt, PolicyRequest, PolicyRule, PolicyState } from './types.js';
2
2
  export declare class AgenticPolicyEngine {
3
3
  private readonly now;
4
4
  private readonly signingKey?;
@@ -19,6 +19,12 @@ export declare class AgenticPolicyEngine {
19
19
  }): PolicyApproval;
20
20
  revokeApproval(id: string): boolean;
21
21
  evaluate(request: PolicyRequest): PolicyDecision;
22
+ /** Authenticate each physical record before callers deduplicate it.
23
+ * Receipts use one configured key and key ID for this ledger. For existing
24
+ * custom-ID history, restore the original CLAUDE_FLOW_POLICY_KEY_ID when
25
+ * verifying; never rewrite or re-sign history to change its identity.
26
+ * Key rotation/multi-key historical verification is not supported here. */
27
+ verifyReceipt(receipt: unknown): string | undefined;
22
28
  /**
23
29
  * `establishAnchor` is the ONLY way to create an anchor over receipts that
24
30
  * already exist (#3602). A caller must have decided, outside the state file,
@@ -30,6 +36,16 @@ export declare class AgenticPolicyEngine {
30
36
  verifyLedger(options?: {
31
37
  establishAnchor?: boolean;
32
38
  }): LedgerVerification;
39
+ /**
40
+ * Removes the oldest receipts from the hot `receipts` array, keeping only
41
+ * `retain` of the most recent ones in memory (#3164). Pure and in-memory —
42
+ * this class stays side-effect-free; the runtime layer archives the
43
+ * returned receipts to disk and persists the resulting state. Requires an
44
+ * established anchor first: pruning a ledger whose historical length isn't
45
+ * externally anchored would make a future `verifyLedger` unable to tell
46
+ * truncation-by-attacker apart from legitimate pruning.
47
+ */
48
+ pruneReceipts(retain: number): PolicyReceipt[];
33
49
  private applyBudget;
34
50
  private applyApproval;
35
51
  private consumeBudget;
@@ -1 +1 @@
1
- {"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../../src/policy/engine.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EACV,WAAW,EACX,kBAAkB,EAElB,cAAc,EACd,cAAc,EAEd,mBAAmB,EAEnB,aAAa,EACb,UAAU,EACV,WAAW,EACZ,MAAM,YAAY,CAAC;AASpB,qBAAa,mBAAmB;IAC9B,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAe;IACnC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAkB;IAC9C,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAC,CAAgE;IAClG,OAAO,CAAC,QAAQ,CAAC,sBAAsB,CAAC,CAA8B;IACtE,OAAO,CAAC,KAAK,CAAc;gBAEf,OAAO,GAAE,mBAAwB;IAiB7C,MAAM,CAAC,SAAS,CAAC,KAAK,EAAE,WAAW,EAAE,OAAO,GAAE,IAAI,CAAC,mBAAmB,EAAE,MAAM,GAAG,OAAO,GAAG,SAAS,GAAG,OAAO,GAAG,WAAW,GAAG,UAAU,CAAM,GAAG,mBAAmB;IAoBrK,WAAW,IAAI,WAAW;IAI1B,OAAO,CAAC,IAAI,EAAE,WAAW,CAAC,MAAM,CAAC,GAAG,IAAI;IAIxC,iBAAiB,CAAC,IAAI,EAAE,WAAW,CAAC,MAAM,CAAC,GAAG,IAAI;IAMlD,UAAU,CAAC,IAAI,EAAE,UAAU,GAAG,IAAI;IAgBlC,SAAS,CAAC,KAAK,EAAE,WAAW,GAAG,IAAI;IAWnC,aAAa,CAAC,QAAQ,EAAE,IAAI,CAAC,cAAc,EAAE,MAAM,GAAG,UAAU,CAAC,GAAG;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,cAAc;IAmBzH,cAAc,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO;IAOnC,QAAQ,CAAC,OAAO,EAAE,aAAa,GAAG,cAAc;IAoBhD;;;;;;;OAOG;IACH,YAAY,CAAC,OAAO,GAAE;QAAE,eAAe,CAAC,EAAE,OAAO,CAAA;KAAO,GAAG,kBAAkB;IA+C7E,OAAO,CAAC,WAAW;IA2BnB,OAAO,CAAC,aAAa;IAyBrB,OAAO,CAAC,aAAa;IAkBrB,OAAO,CAAC,aAAa;IAuCrB,OAAO,CAAC,eAAe;CAcxB;AAED,wBAAgB,2BAA2B,CAAC,MAAM,SAAgB,GAAG,WAAW,CAY/E"}
1
+ {"version":3,"file":"engine.d.ts","sourceRoot":"","sources":["../../src/policy/engine.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EACV,WAAW,EACX,kBAAkB,EAElB,cAAc,EACd,cAAc,EAEd,mBAAmB,EACnB,aAAa,EACb,aAAa,EACb,UAAU,EACV,WAAW,EACZ,MAAM,YAAY,CAAC;AASpB,qBAAa,mBAAmB;IAC9B,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAe;IACnC,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAC,CAAkB;IAC9C,OAAO,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAS;IAChC,OAAO,CAAC,QAAQ,CAAC,gBAAgB,CAAC,CAAgE;IAClG,OAAO,CAAC,QAAQ,CAAC,sBAAsB,CAAC,CAA8B;IACtE,OAAO,CAAC,KAAK,CAAc;gBAEf,OAAO,GAAE,mBAAwB;IAiB7C,MAAM,CAAC,SAAS,CAAC,KAAK,EAAE,WAAW,EAAE,OAAO,GAAE,IAAI,CAAC,mBAAmB,EAAE,MAAM,GAAG,OAAO,GAAG,SAAS,GAAG,OAAO,GAAG,WAAW,GAAG,UAAU,CAAM,GAAG,mBAAmB;IA2BrK,WAAW,IAAI,WAAW;IAI1B,OAAO,CAAC,IAAI,EAAE,WAAW,CAAC,MAAM,CAAC,GAAG,IAAI;IAIxC,iBAAiB,CAAC,IAAI,EAAE,WAAW,CAAC,MAAM,CAAC,GAAG,IAAI;IAMlD,UAAU,CAAC,IAAI,EAAE,UAAU,GAAG,IAAI;IAgBlC,SAAS,CAAC,KAAK,EAAE,WAAW,GAAG,IAAI;IAWnC,aAAa,CAAC,QAAQ,EAAE,IAAI,CAAC,cAAc,EAAE,MAAM,GAAG,UAAU,CAAC,GAAG;QAAE,IAAI,CAAC,EAAE,MAAM,CAAC;QAAC,QAAQ,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,cAAc;IAmBzH,cAAc,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO;IAOnC,QAAQ,CAAC,OAAO,EAAE,aAAa,GAAG,cAAc;IAoBhD;;;;+EAI2E;IAC3E,aAAa,CAAC,OAAO,EAAE,OAAO,GAAG,MAAM,GAAG,SAAS;IA4BnD;;;;;;;OAOG;IACH,YAAY,CAAC,OAAO,GAAE;QAAE,eAAe,CAAC,EAAE,OAAO,CAAA;KAAO,GAAG,kBAAkB;IA8C7E;;;;;;;;OAQG;IACH,aAAa,CAAC,MAAM,EAAE,MAAM,GAAG,aAAa,EAAE;IAW9C,OAAO,CAAC,WAAW;IA2BnB,OAAO,CAAC,aAAa;IAyBrB,OAAO,CAAC,aAAa;IAkBrB,OAAO,CAAC,aAAa;IAyCrB,OAAO,CAAC,eAAe;CAcxB;AAED,wBAAgB,2BAA2B,CAAC,MAAM,SAAgB,GAAG,WAAW,CAY/E"}