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
@@ -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`.
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@claude-flow/cli",
3
- "version": "3.56.10",
3
+ "version": "3.57.1",
4
4
  "type": "module",
5
5
  "description": "Ruflo CLI - Enterprise AI agent orchestration with 60+ specialized agents, swarm coordination, MCP server, self-learning hooks, and vector memory for Claude Code",
6
6
  "main": "dist/src/index.js",
@@ -98,7 +98,7 @@
98
98
  "vitest": "^4.1.0"
99
99
  },
100
100
  "dependencies": {
101
- "@claude-flow/cli-core": "3.7.3",
101
+ "@claude-flow/cli-core": "3.7.4",
102
102
  "@claude-flow/codex": "3.0.5",
103
103
  "@claude-flow/mcp": "3.1.1",
104
104
  "@claude-flow/neural": "3.0.0",
@@ -130,7 +130,7 @@
130
130
  "optionalDependencies": {
131
131
  "@agntcy/slim-bindings": "2.0.0-alpha.5",
132
132
  "@claude-flow/memory": "3.0.5",
133
- "@metaharness/darwin": "~0.10.2",
133
+ "@metaharness/darwin": "~0.12.0",
134
134
  "@metaharness/flywheel": "~0.1.10",
135
135
  "@metaharness/radio": "~0.1.0",
136
136
  "@metaharness/turn-credit": "~0.1.0",