@claude-flow/cli 3.56.9 → 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 (101) hide show
  1. package/.claude/helpers/.helpers-version +1 -1
  2. package/.claude/helpers/helpers.manifest.json +3 -3
  3. package/.claude/helpers/intelligence.cjs +51 -9
  4. package/.claude/helpers/router.cjs +0 -0
  5. package/.claude/skills/verification-quality/SKILL.md +14 -11
  6. package/README.md +1 -1
  7. package/catalog-manifest.json +2 -2
  8. package/dist/src/autopilot-state.d.ts +2 -0
  9. package/dist/src/autopilot-state.js +36 -8
  10. package/dist/src/command-resolution.d.ts +15 -0
  11. package/dist/src/command-resolution.js +25 -0
  12. package/dist/src/commands/agent.js +17 -17
  13. package/dist/src/commands/claims.js +6 -8
  14. package/dist/src/commands/daemon.js +94 -33
  15. package/dist/src/commands/doctor.js +95 -18
  16. package/dist/src/commands/memory.js +14 -2
  17. package/dist/src/commands/policy.js +6 -1
  18. package/dist/src/commands/security-code-patterns.d.ts +2 -2
  19. package/dist/src/commands/security-code-patterns.js +61 -3
  20. package/dist/src/commands/security.d.ts +24 -0
  21. package/dist/src/commands/security.js +81 -35
  22. package/dist/src/commands/tui.d.ts +9 -0
  23. package/dist/src/commands/tui.js +29 -3
  24. package/dist/src/config/proven-config-refresh.d.ts +9 -0
  25. package/dist/src/config/proven-config-refresh.js +24 -3
  26. package/dist/src/funnel/local-signals.js +2 -1
  27. package/dist/src/index.js +11 -47
  28. package/dist/src/init/settings-generator.js +4 -58
  29. package/dist/src/init/types.js +1 -1
  30. package/dist/src/mcp-tools/agent-execute-core.d.ts +2 -0
  31. package/dist/src/mcp-tools/agent-execute-core.js +15 -0
  32. package/dist/src/mcp-tools/agent-health-metrics.d.ts +22 -0
  33. package/dist/src/mcp-tools/agent-health-metrics.js +33 -0
  34. package/dist/src/mcp-tools/agent-tools.js +45 -22
  35. package/dist/src/mcp-tools/agentbbs-federation.d.ts +1 -1
  36. package/dist/src/mcp-tools/agentbbs-federation.js +7 -2
  37. package/dist/src/mcp-tools/agentdb-tools.js +25 -2
  38. package/dist/src/mcp-tools/aidefence-known-misses.d.ts +22 -0
  39. package/dist/src/mcp-tools/aidefence-known-misses.js +57 -0
  40. package/dist/src/mcp-tools/analyze-tools.js +10 -11
  41. package/dist/src/mcp-tools/autopilot-tools.js +2 -2
  42. package/dist/src/mcp-tools/browser-intent-tools.js +6 -2
  43. package/dist/src/mcp-tools/embeddings-tools.js +29 -7
  44. package/dist/src/mcp-tools/hooks-tools.js +4 -0
  45. package/dist/src/mcp-tools/memory-tools.d.ts +1 -1
  46. package/dist/src/mcp-tools/memory-tools.js +100 -25
  47. package/dist/src/mcp-tools/metaharness-tools.js +1 -1
  48. package/dist/src/mcp-tools/neural-tools.js +48 -54
  49. package/dist/src/mcp-tools/policy-tools.js +5 -3
  50. package/dist/src/mcp-tools/ruvllm-tools.js +6 -0
  51. package/dist/src/mcp-tools/security-tools.js +55 -17
  52. package/dist/src/mcp-tools/workflow-tools.js +66 -17
  53. package/dist/src/memory/intelligence.d.ts +5 -1
  54. package/dist/src/memory/intelligence.js +158 -35
  55. package/dist/src/memory/memory-bridge.js +10 -3
  56. package/dist/src/memory/memory-initializer.js +2 -1
  57. package/dist/src/memory/search-defaults.d.ts +54 -0
  58. package/dist/src/memory/search-defaults.js +37 -0
  59. package/dist/src/memory/sona-optimizer.d.ts +28 -0
  60. package/dist/src/memory/sona-optimizer.js +59 -2
  61. package/dist/src/memory/stats-file-lock.d.ts +7 -0
  62. package/dist/src/memory/stats-file-lock.js +53 -0
  63. package/dist/src/mods/command-registry/schema.d.ts +24 -24
  64. package/dist/src/ruvector/agent-wasm.d.ts +10 -0
  65. package/dist/src/ruvector/agent-wasm.js +27 -1
  66. package/dist/src/ruvector/diff-classifier.d.ts +2 -4
  67. package/dist/src/ruvector/diff-classifier.js +202 -69
  68. package/dist/src/ruvector/ruvllm-wasm.d.ts +4 -0
  69. package/dist/src/ruvector/ruvllm-wasm.js +12 -1
  70. package/dist/src/services/distill-oracle.d.ts +1 -1
  71. package/dist/src/services/distill-oracle.js +2 -2
  72. package/dist/src/services/policy-anchor-log.d.ts +13 -0
  73. package/dist/src/services/policy-anchor-log.js +242 -0
  74. package/dist/src/services/policy-archive-scan.d.ts +7 -0
  75. package/dist/src/services/policy-archive-scan.js +50 -0
  76. package/dist/src/services/policy-ledger-anchor.d.ts +14 -8
  77. package/dist/src/services/policy-ledger-anchor.js +356 -40
  78. package/dist/src/services/policy-ledger-audit.d.ts +22 -0
  79. package/dist/src/services/policy-ledger-audit.js +260 -0
  80. package/dist/src/services/policy-ledger-record.d.ts +3 -0
  81. package/dist/src/services/policy-ledger-record.js +22 -0
  82. package/dist/src/services/policy-runtime.d.ts +2 -1
  83. package/dist/src/services/policy-runtime.js +267 -40
  84. package/dist/src/services/worker-daemon.d.ts +11 -0
  85. package/dist/src/services/worker-daemon.js +37 -0
  86. package/dist/src/transfer/store/discovery.js +16 -17
  87. package/dist/src/transfer/store/types.d.ts +4 -0
  88. package/node_modules/@claude-flow/codex/dist/cli.js +0 -0
  89. package/node_modules/@claude-flow/plugin-agent-federation/dist/bin.js +0 -0
  90. package/node_modules/@claude-flow/security/dist/policy/engine.d.ts +17 -1
  91. package/node_modules/@claude-flow/security/dist/policy/engine.d.ts.map +1 -1
  92. package/node_modules/@claude-flow/security/dist/policy/engine.js +99 -24
  93. package/node_modules/@claude-flow/security/dist/policy/engine.js.map +1 -1
  94. package/node_modules/@claude-flow/security/dist/policy/types.d.ts +14 -1
  95. package/node_modules/@claude-flow/security/dist/policy/types.d.ts.map +1 -1
  96. package/node_modules/@claude-flow/security/dist/policy/types.js +3 -1
  97. package/node_modules/@claude-flow/security/dist/policy/types.js.map +1 -1
  98. package/package.json +5 -5
  99. package/plugins/ruflo-console/dist/tui.mjs +162 -161
  100. package/plugins/ruflo-metaharness/hooks/screen.ts +7 -1
  101. package/plugins/ruflo-metaharness/scripts/_darwin.mjs +4 -4
@@ -1,16 +1,36 @@
1
- import { AgenticPolicyEngine, createLegacyCompatibleState, isMcpCallerAuthEnabled, decodeTokenEnvelope, publicKeyFromHex, verifyInvocationToken, } from '@claude-flow/security';
1
+ // Narrow subpath imports (not the '@claude-flow/security' barrel): this module
2
+ // is loaded eagerly by the MCP server, and the barrel also evaluates the OAuth
3
+ // callback server (an ESM `node:http` import that materialises http's lazy
4
+ // undici getters), bcryptjs, keychain and credential helpers that the policy
5
+ // runtime never uses.
6
+ import { AgenticPolicyEngine, POLICY_STATE_VERSION, createLegacyCompatibleState, } from '@claude-flow/security/policy/index';
7
+ import { isMcpCallerAuthEnabled, decodeTokenEnvelope, publicKeyFromHex, verifyInvocationToken, } from '@claude-flow/security/mcp-caller-identity';
2
8
  import { createHash, createHmac, randomBytes, randomUUID, timingSafeEqual } from 'node:crypto';
3
9
  import { execFileSync } from 'node:child_process';
4
- 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';
5
11
  import { dirname, join, resolve } from 'node:path';
6
12
  import { hostname, userInfo } from 'node:os';
7
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';
8
16
  import { assessAnchors, policyTrustRoot, recordAnchor } from './policy-ledger-anchor.js';
9
17
  const POLICY_DIR = join('.claude-flow', 'policy');
10
18
  const POLICY_FILE = 'state.json';
11
19
  const LOCK_FILE = 'state.lock';
12
- const LOCK_STALE_MS = 30_000;
20
+ const RECEIPT_ARCHIVE_FILE = 'receipts.ledger.jsonl';
13
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
+ }
14
34
  // A lock is ~170 bytes plus the hostname (at most 255 bytes).
15
35
  const LOCK_MAX_BYTES = 1_024;
16
36
  // Linux PID namespace of this process, e.g. `pid:[4026531836]`; undefined on
@@ -39,7 +59,7 @@ const BOOT_ID = (() => {
39
59
  function paths(projectRoot) {
40
60
  const root = resolve(projectRoot);
41
61
  const dir = join(root, POLICY_DIR);
42
- 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) };
43
63
  }
44
64
  function sleep(ms) {
45
65
  return new Promise((resolveSleep) => setTimeout(resolveSleep, ms));
@@ -62,7 +82,7 @@ function isPidAlive(pid) {
62
82
  /**
63
83
  * Reads a lock only if it is a small regular file: never follows a symlink,
64
84
  * never blocks on a FIFO or device, never reads more than LOCK_MAX_BYTES.
65
- * Anything else returns undefined and the caller keeps the mtime rule.
85
+ * Anything else returns undefined; unknown owners are preserved.
66
86
  */
67
87
  function readLockFile(lockPath) {
68
88
  if (!lstatSync(lockPath).isFile())
@@ -88,8 +108,8 @@ function readLockFile(lockPath) {
88
108
  * boot and in this PID namespace: anywhere else (a bwrap --unshare-pid
89
109
  * sandbox, a container, another machine on a shared filesystem) a live owner
90
110
  * also answers ESRCH. Locks without that identity (older ruflo, a crash
91
- * between create and write, anything unparsable) return false and keep the
92
- * mtime rule.
111
+ * between create and write, anything unparsable) return false and require
112
+ * explicit operator inspection rather than age-only takeover.
93
113
  */
94
114
  function lockOwnerIsDead(lockPath) {
95
115
  try {
@@ -121,6 +141,36 @@ function lockOwnerIsDead(lockPath) {
121
141
  return false;
122
142
  }
123
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
+ }
124
174
  /**
125
175
  * Exported for ADR-406 mission storage, which takes the same per-file lock
126
176
  * (pid, pid namespace and boot id identify a dead owner) rather than a copy.
@@ -130,33 +180,37 @@ export async function acquireLock(lockPath) {
130
180
  while (Date.now() - started < LOCK_WAIT_MS) {
131
181
  try {
132
182
  const fd = openSync(lockPath, 'wx', 0o600);
133
- writeFileSync(fd, JSON.stringify({
183
+ const ownership = JSON.stringify({
134
184
  pid: process.pid,
135
185
  acquiredAt: Date.now(),
136
186
  host: hostname(),
137
187
  platform: process.platform,
138
188
  pidns: PID_NAMESPACE,
139
189
  boot: BOOT_ID,
140
- }));
141
- closeSync(fd);
190
+ nonce: randomUUID(),
191
+ });
192
+ try {
193
+ writeFileSync(fd, ownership);
194
+ }
195
+ finally {
196
+ closeSync(fd);
197
+ }
142
198
  return () => {
143
199
  try {
144
- unlinkSync(lockPath);
200
+ if (readLockFile(lockPath) === ownership)
201
+ unlinkSync(lockPath);
145
202
  }
146
203
  catch { /* already released */ }
147
204
  };
148
205
  }
149
206
  catch {
150
- try {
151
- if (lockOwnerIsDead(lockPath)
152
- || Date.now() - statSync(lockPath).mtimeMs > LOCK_STALE_MS)
153
- unlinkSync(lockPath);
154
- }
155
- catch { /* another process changed the lock */ }
207
+ reclaimDeadLock(lockPath);
156
208
  await sleep(10);
157
209
  }
158
210
  }
159
- 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');
160
214
  }
161
215
  // #3398: Windows rename over a file another process holds open fails with
162
216
  // EPERM/EBUSY/EACCES. Retry (~1.3s total, inside LOCK_WAIT_MS), never leave the
@@ -164,17 +218,129 @@ export async function acquireLock(lockPath) {
164
218
  // and consumed approval uses, so dropping it would let an approval be reused.
165
219
  const RENAME_RETRY_DELAYS_MS = [25, 50, 100, 150, 250, 300, 400];
166
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
+ }
167
323
  async function writeJsonAtomic(file, value) {
168
- mkdirSync(dirname(file), { recursive: true, mode: 0o700 });
324
+ mkdirDurably(dirname(file));
169
325
  const temporary = `${file}.${process.pid}.${randomUUID()}.tmp`;
170
326
  let renamed = false;
171
327
  try {
172
- 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
+ }
173
339
  for (let attempt = 0;; attempt++) {
174
340
  try {
175
341
  renameSync(temporary, file);
176
342
  renamed = true;
177
- return;
343
+ break;
178
344
  }
179
345
  catch (error) {
180
346
  const code = error?.code ?? '';
@@ -183,6 +349,9 @@ async function writeJsonAtomic(file, value) {
183
349
  await sleep(RENAME_RETRY_DELAYS_MS[attempt]);
184
350
  }
185
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));
186
355
  }
187
356
  finally {
188
357
  if (!renamed) {
@@ -204,8 +373,16 @@ function trustKey(projectRoot, create) {
204
373
  if (!existsSync(key)) {
205
374
  if (!create)
206
375
  return undefined;
207
- mkdirSync(dirname(key), { recursive: true, mode: 0o700 });
208
- 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));
209
386
  }
210
387
  const material = readFileSync(key);
211
388
  if (material.length !== 32)
@@ -232,11 +409,11 @@ function verifyStateAnchor(projectRoot, state) {
232
409
  throw new Error('policy-state-authentication-failed');
233
410
  }
234
411
  }
235
- async function writePolicyState(projectRoot, statePath, state, anchorEvent = 'append', by) {
412
+ async function writePolicyState(projectRoot, statePath, state, anchorEvent = 'append', by, assessedTip) {
236
413
  await writePolicyStateFiles(projectRoot, statePath, state);
237
414
  // #3602: the second anchor is written after the state, so a crash leaves the
238
415
  // state AHEAD of the log (caught up on the next write), never behind it.
239
- await recordAnchor(projectRoot, state, writeJsonAtomic, anchorEvent, by);
416
+ await recordAnchor(projectRoot, state, writeJsonAtomic, anchorEvent, by, writeJsonAtomic, assessedTip);
240
417
  // ADR-404: the ruflo mod reads Claude Code tool rules from a small
241
418
  // projection, never from state.json. Written only after the state and its
242
419
  // anchor are safely down; a failure here never fails the state write. A
@@ -310,7 +487,7 @@ export function loadPolicyState(projectRoot = process.cwd()) {
310
487
  return createLegacyCompatibleState(detectLegacyCapabilities(projectRoot));
311
488
  }
312
489
  const parsed = JSON.parse(readFileSync(target.state, 'utf8'));
313
- 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)) {
314
491
  throw new Error(`unsupported-policy-state-version:${String(parsed.version)}`);
315
492
  }
316
493
  verifyStateAnchor(projectRoot, parsed);
@@ -331,7 +508,7 @@ export async function autoMigratePolicyStateIfNeeded(projectRoot = process.cwd()
331
508
  if (!existsSync(join(resolve(projectRoot), '.claude-flow'))
332
509
  && !existsSync(join(resolve(projectRoot), '.swarm')))
333
510
  return { migrated: false };
334
- mkdirSync(target.dir, { recursive: true, mode: 0o700 });
511
+ mkdirDurably(target.dir);
335
512
  const release = await acquireLock(target.lock);
336
513
  try {
337
514
  if (!existsSync(target.state)) {
@@ -351,7 +528,7 @@ export async function autoMigratePolicyStateIfNeeded(projectRoot = process.cwd()
351
528
  }
352
529
  export async function withPolicyTransaction(projectRoot, operation, options = {}) {
353
530
  const target = paths(projectRoot);
354
- mkdirSync(target.dir, { recursive: true, mode: 0o700 });
531
+ mkdirDurably(target.dir);
355
532
  const release = await acquireLock(target.lock);
356
533
  try {
357
534
  const loaded = loadPolicyState(projectRoot);
@@ -370,10 +547,30 @@ export async function withPolicyTransaction(projectRoot, operation, options = {}
370
547
  : gate.result.error ?? 'policy-ledger-verification-failed');
371
548
  }
372
549
  const result = await operation(engine);
373
- if (!engine.verifyLedger().valid)
550
+ const verified = engine.verifyLedger();
551
+ if (!verified.valid)
374
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
+ }
375
572
  const nextState = engine.exportState();
376
- await writePolicyState(projectRoot, target.state, nextState, gate.event);
573
+ await writePolicyState(projectRoot, target.state, nextState, gate.event, undefined, gate.anchorTip);
377
574
  return result;
378
575
  }
379
576
  finally {
@@ -433,7 +630,9 @@ export async function revokePolicyApproval(id, projectRoot = process.cwd()) {
433
630
  */
434
631
  function reconcileLedger(projectRoot, engine, options) {
435
632
  const state = engine.exportState();
436
- 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;
437
636
  const primary = typeof state.ledgerLength === 'number';
438
637
  const base = engine.verifyLedger();
439
638
  const fail = (error) => ({ result: { valid: false, length, error }, persist: false });
@@ -448,7 +647,7 @@ function reconcileLedger(projectRoot, engine, options) {
448
647
  ? { result: { ...base, secondaryAnchor: 'established-explicitly' }, persist: true, event: 'establish-anchor' }
449
648
  : { result: { ...base, secondaryAnchor: 'recorded-from-state' }, persist: true, event: 'migrated-from-state' };
450
649
  }
451
- 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 };
452
651
  }
453
652
  // No primary anchor.
454
653
  if (length === 0)
@@ -457,7 +656,7 @@ function reconcileLedger(projectRoot, engine, options) {
457
656
  // The second anchor is authentic and agrees with the chain: the deleted
458
657
  // primary can be restored from it. Truncation was already rejected above.
459
658
  engine.verifyLedger({ establishAnchor: true });
460
- 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 };
461
660
  }
462
661
  if (options.establishAnchor) {
463
662
  const established = engine.verifyLedger({ establishAnchor: true });
@@ -477,23 +676,51 @@ function reconcileLedger(projectRoot, engine, options) {
477
676
  */
478
677
  export async function verifyPolicyLedger(projectRoot = process.cwd(), options = {}) {
479
678
  const target = paths(projectRoot);
480
- 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.
481
692
  const release = await acquireLock(target.lock);
482
693
  try {
483
- const engine = AgenticPolicyEngine.fromState(loadPolicyState(projectRoot), {
484
- signingKey: process.env.CLAUDE_FLOW_POLICY_SIGNING_KEY,
485
- keyId: process.env.CLAUDE_FLOW_POLICY_KEY_ID,
486
- });
694
+ const engine = AgenticPolicyEngine.fromState(loadPolicyState(projectRoot), engineOptions);
487
695
  const outcome = reconcileLedger(projectRoot, engine, options);
488
696
  if (outcome.persist) {
489
697
  const by = outcome.event === 'establish-anchor' ? userInfo().username : undefined;
490
- 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
+ }
491
709
  }
492
- return outcome.result;
493
710
  }
494
711
  finally {
495
712
  release();
496
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
+ }
497
724
  }
498
725
  /**
499
726
  * Resolve the caller identity for `authorizeMcpTool`.
@@ -316,6 +316,17 @@ export declare class WorkerDaemon extends EventEmitter {
316
316
  * Execute a worker with timeout protection
317
317
  */
318
318
  private executeWorker;
319
+ /**
320
+ * Feed a finished worker job into the self-learning pipeline as a one-step
321
+ * trajectory (SONA + ReasoningBank + @ruvector/ruvllm forwarding, persisted
322
+ * to `.claude-flow/neural/stats.json`). Before this, the daemon could run
323
+ * hundreds of jobs at "100% success" while `trajectoriesRecorded` stayed 0:
324
+ * no daemon code path ever called the intelligence module.
325
+ *
326
+ * Best-effort and bounded: never throws, never changes the WorkerResult.
327
+ * Opt-out: RUFLO_DAEMON_NO_LEARN=1.
328
+ */
329
+ private recordWorkerTrajectory;
319
330
  /**
320
331
  * Run a function with timeout (P1 fix)
321
332
  * @param fn - The async function to execute
@@ -75,6 +75,9 @@ const CONSOLIDATE_DEDUP_DISTANCE = 0.2;
75
75
  // `--no-distill` flag on `daemon start` (see commands/daemon.ts), which sets
76
76
  // this env var on the forked/foreground daemon process.
77
77
  const NO_DISTILL_ENV = 'RUFLO_DAEMON_NO_DISTILL';
78
+ // Opt-out for recording each finished worker job as a learning trajectory
79
+ // (see WorkerDaemon.recordWorkerTrajectory).
80
+ const NO_LEARN_ENV = 'RUFLO_DAEMON_NO_LEARN';
78
81
  // #2356 — Self-terminating lifecycle defaults. A background daemon with no
79
82
  // upper bound on its lifetime runs until the box reboots; in the field this
80
83
  // leaked tens of thousands of headless `claude --print` sweeps over many days
@@ -1234,6 +1237,7 @@ export class WorkerDaemon extends EventEmitter {
1234
1237
  this.emit('worker:complete', result);
1235
1238
  this.log('info', `Worker ${workerConfig.type} completed in ${durationMs}ms`);
1236
1239
  this.saveState();
1240
+ await this.recordWorkerTrajectory(result);
1237
1241
  return result;
1238
1242
  }
1239
1243
  catch (error) {
@@ -1253,6 +1257,7 @@ export class WorkerDaemon extends EventEmitter {
1253
1257
  this.emit('worker:error', result);
1254
1258
  this.log('error', `Worker ${workerConfig.type} failed: ${result.error}`);
1255
1259
  this.saveState();
1260
+ await this.recordWorkerTrajectory(result);
1256
1261
  return result;
1257
1262
  }
1258
1263
  finally {
@@ -1261,6 +1266,38 @@ export class WorkerDaemon extends EventEmitter {
1261
1266
  this.processPendingWorkers();
1262
1267
  }
1263
1268
  }
1269
+ /**
1270
+ * Feed a finished worker job into the self-learning pipeline as a one-step
1271
+ * trajectory (SONA + ReasoningBank + @ruvector/ruvllm forwarding, persisted
1272
+ * to `.claude-flow/neural/stats.json`). Before this, the daemon could run
1273
+ * hundreds of jobs at "100% success" while `trajectoriesRecorded` stayed 0:
1274
+ * no daemon code path ever called the intelligence module.
1275
+ *
1276
+ * Best-effort and bounded: never throws, never changes the WorkerResult.
1277
+ * Opt-out: RUFLO_DAEMON_NO_LEARN=1.
1278
+ */
1279
+ async recordWorkerTrajectory(result) {
1280
+ if (process.env[NO_LEARN_ENV] === '1')
1281
+ return;
1282
+ try {
1283
+ const intel = await import('../memory/intelligence.js');
1284
+ await intel.recordTrajectory([{
1285
+ type: 'result',
1286
+ // A worker error is free text (it can quote command output): keep only its first line, cut, so it cannot fill the learning store.
1287
+ content: `daemon worker ${result.type} ${result.success ? 'succeeded' : `failed: ${String(result.error ?? 'unknown error').split('\n')[0].slice(0, 200)}`}`,
1288
+ metadata: {
1289
+ source: 'worker-daemon',
1290
+ worker: result.type,
1291
+ success: result.success,
1292
+ durationMs: result.durationMs,
1293
+ },
1294
+ timestamp: Date.now(),
1295
+ }], result.success ? 'success' : 'failure');
1296
+ }
1297
+ catch (error) {
1298
+ this.log('warn', `Worker ${result.type}: trajectory recording skipped: ${error instanceof Error ? error.message : String(error)}`);
1299
+ }
1300
+ }
1264
1301
  /**
1265
1302
  * Run a function with timeout (P1 fix)
1266
1303
  * @param fn - The async function to execute
@@ -251,9 +251,12 @@ export class PatternDiscovery {
251
251
  * Get built-in genesis registry (always available offline)
252
252
  */
253
253
  getGenesisRegistry(cid) {
254
+ // #4157: this is an offline placeholder, so it claims nothing it cannot back: no
255
+ // verification, no signature, no popularity numbers, and fixed (not "now") dates.
254
256
  return {
257
+ source: 'builtin-fallback',
255
258
  version: '1.0.0',
256
- updatedAt: new Date().toISOString(),
259
+ updatedAt: '2026-01-08T18:42:31.126Z',
257
260
  ipnsName: 'k51qzi5uqu5dj0w8q1xvqn8ql2g4p7x8qpk9vz3xm1y2n3o4p5q6r7s8t9u0v',
258
261
  previousCid: undefined,
259
262
  patterns: [
@@ -269,25 +272,24 @@ export class PatternDiscovery {
269
272
  author: {
270
273
  id: 'claude-flow-team',
271
274
  displayName: 'Claude Flow Team',
272
- verified: true,
275
+ verified: false,
273
276
  patterns: 1,
274
- totalDownloads: 1000,
277
+ totalDownloads: 0,
275
278
  },
276
279
  license: 'MIT',
277
280
  categories: ['routing', 'coordination'],
278
281
  tags: ['genesis', 'foundational', 'routing', 'swarm', 'coordination', 'multi-agent', 'hello-world'],
279
282
  language: 'typescript',
280
283
  framework: 'claude-flow',
281
- downloads: 1000,
282
- rating: 5.0,
283
- ratingCount: 42,
284
- lastUpdated: new Date().toISOString(),
284
+ downloads: 0,
285
+ rating: 0,
286
+ ratingCount: 0,
287
+ lastUpdated: '2026-01-08T18:42:31.126Z',
285
288
  createdAt: '2026-01-08T18:42:31.126Z',
286
289
  minClaudeFlowVersion: '3.0.0',
287
- verified: true,
288
- trustLevel: 'verified',
289
- signature: 'ed25519:genesis-pattern-signature',
290
- publicKey: 'ed25519:claude-flow-team-key',
290
+ verified: false,
291
+ trustLevel: 'unverified',
292
+ source: 'builtin-fallback',
291
293
  },
292
294
  ],
293
295
  categories: [
@@ -301,20 +303,17 @@ export class PatternDiscovery {
301
303
  {
302
304
  id: 'claude-flow-team',
303
305
  displayName: 'Claude Flow Team',
304
- publicKey: 'ed25519:claude-flow-team-key',
305
- verified: true,
306
+ verified: false,
306
307
  patterns: 1,
307
- totalDownloads: 1000,
308
+ totalDownloads: 0,
308
309
  },
309
310
  ],
310
311
  totalPatterns: 1,
311
- totalDownloads: 1000,
312
+ totalDownloads: 0,
312
313
  totalAuthors: 1,
313
314
  featured: ['seraphine-genesis-v1'],
314
315
  trending: ['seraphine-genesis-v1'],
315
316
  newest: ['seraphine-genesis-v1'],
316
- registrySignature: crypto.randomBytes(32).toString('hex'),
317
- registryPublicKey: 'ed25519:claude-flow-registry-key',
318
317
  };
319
318
  }
320
319
  /**
@@ -32,6 +32,8 @@ export interface PatternEntry {
32
32
  trustLevel: TrustLevel;
33
33
  signature?: string;
34
34
  publicKey?: string;
35
+ /** Where the entry came from. 'builtin-fallback' = the offline placeholder, not live registry data (#4157). */
36
+ source?: 'registry' | 'builtin-fallback';
35
37
  }
36
38
  /**
37
39
  * Pattern author info
@@ -60,6 +62,8 @@ export interface PatternCategory {
60
62
  * Stored on IPFS with IPNS pointer for updates
61
63
  */
62
64
  export interface PatternRegistry {
65
+ /** 'builtin-fallback' when discovery could not reach a registry and used the offline placeholder (#4157). */
66
+ source?: 'registry' | 'builtin-fallback';
63
67
  version: string;
64
68
  updatedAt: string;
65
69
  ipnsName: string;
File without changes