claude-flow 3.57.1 → 3.58.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 (77) hide show
  1. package/.claude/helpers/intelligence.cjs +3 -5
  2. package/.claude/proven-config.json +1 -1
  3. package/.claude-plugin/marketplace.json +11 -0
  4. package/README.md +1 -0
  5. package/node_modules/@claude-flow/codex/README.md +87 -9
  6. package/node_modules/@claude-flow/codex/dist/activity/cli.d.ts +3 -0
  7. package/node_modules/@claude-flow/codex/dist/activity/cli.d.ts.map +1 -0
  8. package/node_modules/@claude-flow/codex/dist/activity/cli.js +115 -0
  9. package/node_modules/@claude-flow/codex/dist/activity/cli.js.map +1 -0
  10. package/node_modules/@claude-flow/codex/dist/activity/files.d.ts +14 -0
  11. package/node_modules/@claude-flow/codex/dist/activity/files.d.ts.map +1 -0
  12. package/node_modules/@claude-flow/codex/dist/activity/files.js +142 -0
  13. package/node_modules/@claude-flow/codex/dist/activity/files.js.map +1 -0
  14. package/node_modules/@claude-flow/codex/dist/activity/parser.d.ts +21 -0
  15. package/node_modules/@claude-flow/codex/dist/activity/parser.d.ts.map +1 -0
  16. package/node_modules/@claude-flow/codex/dist/activity/parser.js +179 -0
  17. package/node_modules/@claude-flow/codex/dist/activity/parser.js.map +1 -0
  18. package/node_modules/@claude-flow/codex/dist/activity/reader.d.ts +20 -0
  19. package/node_modules/@claude-flow/codex/dist/activity/reader.d.ts.map +1 -0
  20. package/node_modules/@claude-flow/codex/dist/activity/reader.js +106 -0
  21. package/node_modules/@claude-flow/codex/dist/activity/reader.js.map +1 -0
  22. package/node_modules/@claude-flow/codex/dist/activity/sanitize.d.ts +4 -0
  23. package/node_modules/@claude-flow/codex/dist/activity/sanitize.d.ts.map +1 -0
  24. package/node_modules/@claude-flow/codex/dist/activity/sanitize.js +27 -0
  25. package/node_modules/@claude-flow/codex/dist/activity/sanitize.js.map +1 -0
  26. package/node_modules/@claude-flow/codex/dist/activity/types.d.ts +31 -0
  27. package/node_modules/@claude-flow/codex/dist/activity/types.d.ts.map +1 -0
  28. package/node_modules/@claude-flow/codex/dist/activity/types.js +5 -0
  29. package/node_modules/@claude-flow/codex/dist/activity/types.js.map +1 -0
  30. package/node_modules/@claude-flow/codex/dist/activity/viewer.d.ts +21 -0
  31. package/node_modules/@claude-flow/codex/dist/activity/viewer.d.ts.map +1 -0
  32. package/node_modules/@claude-flow/codex/dist/activity/viewer.js +168 -0
  33. package/node_modules/@claude-flow/codex/dist/activity/viewer.js.map +1 -0
  34. package/node_modules/@claude-flow/codex/dist/cli.js +3 -0
  35. package/node_modules/@claude-flow/codex/dist/cli.js.map +1 -1
  36. package/node_modules/@claude-flow/codex/dist/dual-mode/cli.js +3 -3
  37. package/node_modules/@claude-flow/codex/dist/dual-mode/cli.js.map +1 -1
  38. package/node_modules/@claude-flow/mcp/dist/server.d.ts.map +1 -1
  39. package/node_modules/@claude-flow/mcp/dist/server.js +5 -3
  40. package/node_modules/@claude-flow/mcp/dist/server.js.map +1 -1
  41. package/node_modules/@claude-flow/mcp/dist/types.d.ts +11 -0
  42. package/node_modules/@claude-flow/mcp/dist/types.d.ts.map +1 -1
  43. package/node_modules/@claude-flow/mcp/dist/types.js.map +1 -1
  44. package/package.json +3 -2
  45. package/v3/@claude-flow/cli/README.md +1 -0
  46. package/v3/@claude-flow/cli/catalog-manifest.json +2 -2
  47. package/v3/@claude-flow/cli/dist/src/commands/agent.js +4 -1
  48. package/v3/@claude-flow/cli/dist/src/commands/hooks.js +20 -1
  49. package/v3/@claude-flow/cli/dist/src/commands/mcp.js +66 -6
  50. package/v3/@claude-flow/cli/dist/src/commands/memory.js +78 -3
  51. package/v3/@claude-flow/cli/dist/src/commands/workflow.js +55 -15
  52. package/v3/@claude-flow/cli/dist/src/init/executor.js +9 -5
  53. package/v3/@claude-flow/cli/dist/src/init/helper-companions.d.ts +1 -1
  54. package/v3/@claude-flow/cli/dist/src/init/helper-companions.js +4 -3
  55. package/v3/@claude-flow/cli/dist/src/mcp-server.d.ts +5 -0
  56. package/v3/@claude-flow/cli/dist/src/mcp-server.js +39 -0
  57. package/v3/@claude-flow/cli/dist/src/mcp-tools/agent-tools.js +28 -0
  58. package/v3/@claude-flow/cli/dist/src/mcp-tools/claims-tools.js +25 -15
  59. package/v3/@claude-flow/cli/dist/src/mcp-tools/hooks-tools.js +9 -3
  60. package/v3/@claude-flow/cli/dist/src/mcp-tools/memory-tools.js +58 -9
  61. package/v3/@claude-flow/cli/dist/src/mcp-tools/workflow-tools.js +2 -1
  62. package/v3/@claude-flow/cli/dist/src/memory/graph-edge-writer.d.ts +16 -4
  63. package/v3/@claude-flow/cli/dist/src/memory/graph-edge-writer.js +19 -7
  64. package/v3/@claude-flow/cli/dist/src/memory/intelligence.js +4 -0
  65. package/v3/@claude-flow/cli/dist/src/memory/memory-initializer.js +55 -9
  66. package/v3/@claude-flow/cli/dist/src/memory/memory-peek.d.ts +28 -0
  67. package/v3/@claude-flow/cli/dist/src/memory/memory-peek.js +222 -0
  68. package/v3/@claude-flow/cli/dist/src/memory/model-cache-dir.d.ts +11 -0
  69. package/v3/@claude-flow/cli/dist/src/memory/model-cache-dir.js +17 -0
  70. package/v3/@claude-flow/cli/dist/src/ruvector/task-embedder.js +38 -4
  71. package/v3/@claude-flow/cli/dist/src/services/memory-backup.d.ts +6 -0
  72. package/v3/@claude-flow/cli/dist/src/services/memory-backup.js +109 -81
  73. package/v3/@claude-flow/cli/dist/src/services/policy-ledger-anchor.d.ts +2 -0
  74. package/v3/@claude-flow/cli/dist/src/services/policy-ledger-anchor.js +4 -4
  75. package/v3/@claude-flow/cli/dist/src/services/policy-runtime.js +54 -3
  76. package/v3/@claude-flow/cli/dist/src/services/worker-daemon.js +4 -0
  77. package/v3/@claude-flow/cli/package.json +5 -4
@@ -0,0 +1,222 @@
1
+ /**
2
+ * #3508: read-only memory route/peek.
3
+ *
4
+ * A health check needs to know which memory store files a cwd resolves to,
5
+ * and what is in them, without changing anything to find out. `memory stats`
6
+ * (even after #3463/#3448) still calls `ensureInitialized()` when no explicit
7
+ * path is given, and answers for one file per call. This module reports
8
+ * BOTH files — the sql.js `memory.db` the CLI writes and its native
9
+ * `agentdb-memory.db` sibling the MCP/bridge path writes — in one read-only
10
+ * pass, and says which resolution rule picked the root.
11
+ *
12
+ * Contract (do not relax without re-reading #3508):
13
+ * - never creates, initializes, or migrates anything;
14
+ * - never opens a file that does not exist (`existsSync` gated before any
15
+ * open attempt, for both engines);
16
+ * - every open is `readonly`/`fileMustExist` (native) or a one-shot byte
17
+ * buffer handed to sql.js, which is never written back to disk;
18
+ * - `rows: null` means "could not read" and is never a count of 0, so an
19
+ * empty store and an unreadable one never look the same;
20
+ * - an encrypted-at-rest `memory.db` reports `encrypted: true` and
21
+ * `rows: null`, but its size is still reported.
22
+ */
23
+ import { closeSync, existsSync, openSync, readFileSync, readSync, statSync } from 'node:fs';
24
+ import path from 'node:path';
25
+ import { getMemoryRoot, resolveDbPath } from './memory-initializer.js';
26
+ import { siblingAgentDbPath } from './memory-bridge.js';
27
+ import { isEncryptedBlob } from '../encryption/vault.js';
28
+ // Mirrors the `ACTIVE_MEMORY_ROW_SQL` convention in memory-initializer.ts:
29
+ // legacy rows with a NULL status predate soft-delete semantics and count.
30
+ const ACTIVE_ROW_SQL = `(status = 'active' OR status IS NULL)`;
31
+ const ENCRYPTION_SNIFF_LEN = 64; // RFE1 magic(4)+iv(12)+tag(16); 64 gives headroom.
32
+ function safeStatSize(filePath) {
33
+ try {
34
+ return statSync(filePath).size;
35
+ }
36
+ catch {
37
+ return null;
38
+ }
39
+ }
40
+ function readHeaderBytes(filePath, len) {
41
+ const fd = openSync(filePath, 'r');
42
+ try {
43
+ const buf = Buffer.alloc(len);
44
+ const bytesRead = readSync(fd, buf, 0, len, 0);
45
+ return buf.subarray(0, bytesRead);
46
+ }
47
+ finally {
48
+ closeSync(fd);
49
+ }
50
+ }
51
+ function latestMillis(values) {
52
+ let latest = -Infinity;
53
+ for (const raw of values) {
54
+ const millis = typeof raw === 'number' ? raw : Number(raw);
55
+ if (Number.isFinite(millis))
56
+ latest = Math.max(latest, millis);
57
+ }
58
+ return latest;
59
+ }
60
+ /** sql.js is read as a one-shot in-memory byte buffer — nothing is ever
61
+ * written back, so reading this file has no on-disk side effect. */
62
+ async function probeSqlJsStore(dbPath) {
63
+ try {
64
+ if (isEncryptedBlob(readHeaderBytes(dbPath, ENCRYPTION_SNIFF_LEN))) {
65
+ return { rows: null, lastWrite: null, keys: null, encrypted: true };
66
+ }
67
+ }
68
+ catch {
69
+ /* header read failed (permissions/race) — fall through to the open attempt */
70
+ }
71
+ try {
72
+ const initSqlJs = (await import('sql.js')).default ?? (await import('sql.js'));
73
+ const SQL = await (typeof initSqlJs === 'function' ? initSqlJs() : initSqlJs.default());
74
+ const buf = readFileSync(dbPath);
75
+ const db = new SQL.Database(new Uint8Array(buf));
76
+ try {
77
+ const tables = db.exec("SELECT name FROM sqlite_master WHERE type='table' AND name='memory_entries'");
78
+ if (!tables.length)
79
+ return { rows: null, lastWrite: null, keys: null };
80
+ const result = db.exec(`SELECT namespace, key, created_at, updated_at FROM memory_entries WHERE ${ACTIVE_ROW_SQL}`);
81
+ const values = result[0]?.values ?? [];
82
+ const keys = new Set();
83
+ let last = -Infinity;
84
+ for (const [namespace, key, createdAt, updatedAt] of values) {
85
+ keys.add(`${String(namespace)}\u0000${String(key)}`);
86
+ last = Math.max(last, latestMillis([createdAt, updatedAt]));
87
+ }
88
+ return {
89
+ rows: values.length,
90
+ lastWrite: Number.isFinite(last) ? new Date(last).toISOString() : null,
91
+ keys,
92
+ };
93
+ }
94
+ finally {
95
+ try {
96
+ db.close();
97
+ }
98
+ catch { /* best effort */ }
99
+ }
100
+ }
101
+ catch {
102
+ return { rows: null, lastWrite: null, keys: null };
103
+ }
104
+ }
105
+ /** better-sqlite3 is an optional native dependency — absence must degrade
106
+ * to "could not read", never throw. Opened `readonly`/`fileMustExist`, same
107
+ * as `countSiblingStoreRows` in sibling-store.ts. */
108
+ async function probeNativeStore(dbPath) {
109
+ try {
110
+ const require = (await import('node:module')).createRequire(import.meta.url);
111
+ const Database = require('better-sqlite3');
112
+ const db = new Database(dbPath, { readonly: true, fileMustExist: true });
113
+ try {
114
+ const table = db
115
+ .prepare("SELECT name FROM sqlite_master WHERE type='table' AND name='memory_entries'")
116
+ .get();
117
+ if (!table)
118
+ return { rows: null, lastWrite: null, keys: null };
119
+ const rows = db
120
+ .prepare(`SELECT namespace, key, created_at, updated_at FROM memory_entries WHERE ${ACTIVE_ROW_SQL}`)
121
+ .all();
122
+ const keys = new Set();
123
+ let last = -Infinity;
124
+ for (const row of rows) {
125
+ keys.add(`${String(row.namespace)}\u0000${String(row.key)}`);
126
+ last = Math.max(last, latestMillis([row.created_at, row.updated_at]));
127
+ }
128
+ return {
129
+ rows: rows.length,
130
+ lastWrite: Number.isFinite(last) ? new Date(last).toISOString() : null,
131
+ keys,
132
+ };
133
+ }
134
+ finally {
135
+ try {
136
+ db.close();
137
+ }
138
+ catch { /* best effort */ }
139
+ }
140
+ }
141
+ catch {
142
+ return { rows: null, lastWrite: null, keys: null };
143
+ }
144
+ }
145
+ /** Same precedence `getMemoryRoot()`/`resolveDbPath()` already own — this
146
+ * only *labels* which rule decided, it never re-derives the path itself. */
147
+ function decideSource(cliFlag) {
148
+ if (cliFlag && cliFlag.trim().length > 0)
149
+ return 'path';
150
+ if (process.env.CLAUDE_FLOW_DB_PATH && process.env.CLAUDE_FLOW_DB_PATH.trim().length > 0) {
151
+ return 'CLAUDE_FLOW_DB_PATH';
152
+ }
153
+ if (process.env.CLAUDE_FLOW_MEMORY_PATH && process.env.CLAUDE_FLOW_MEMORY_PATH.trim().length > 0) {
154
+ return 'CLAUDE_FLOW_MEMORY_PATH';
155
+ }
156
+ const configCandidates = [
157
+ path.resolve(process.cwd(), 'claude-flow.config.json'),
158
+ path.resolve(process.cwd(), '.claude-flow', 'config.json'),
159
+ ];
160
+ for (const configPath of configCandidates) {
161
+ if (!existsSync(configPath))
162
+ continue;
163
+ try {
164
+ const raw = JSON.parse(readFileSync(configPath, 'utf-8'));
165
+ const fromConfig = raw?.memory?.persistPath ?? raw?.memory?.path;
166
+ if (typeof fromConfig === 'string' && fromConfig.trim().length > 0)
167
+ return 'config';
168
+ }
169
+ catch {
170
+ /* malformed config — getMemoryRoot() falls through to default too */
171
+ }
172
+ }
173
+ return 'default';
174
+ }
175
+ async function describeStore(role, storePath, probe) {
176
+ if (!existsSync(storePath)) {
177
+ return {
178
+ store: { role, path: storePath, exists: false, rows: null, lastWrite: null, sizeBytes: null, walBytes: null },
179
+ keys: null,
180
+ };
181
+ }
182
+ const result = await probe(storePath);
183
+ const sizeBytes = safeStatSize(storePath);
184
+ const walBytes = safeStatSize(`${storePath}-wal`) ?? 0;
185
+ return {
186
+ store: {
187
+ role,
188
+ path: storePath,
189
+ exists: true,
190
+ rows: result.rows,
191
+ lastWrite: result.lastWrite,
192
+ sizeBytes,
193
+ walBytes,
194
+ ...(result.encrypted ? { encrypted: true } : {}),
195
+ },
196
+ keys: result.keys,
197
+ };
198
+ }
199
+ /**
200
+ * Resolve the route and report both stores, read-only. Never creates,
201
+ * initializes, or migrates anything, on an empty project or any other.
202
+ */
203
+ export async function memoryPeek(opts) {
204
+ const cliFlag = opts?.path;
205
+ const cliPath = resolveDbPath(cliFlag);
206
+ const mcpPath = siblingAgentDbPath(cliPath);
207
+ const root = { path: cliFlag ? path.dirname(cliPath) : getMemoryRoot(), source: decideSource(cliFlag) };
208
+ const cliDescribed = await describeStore('cli', cliPath, probeSqlJsStore);
209
+ const stores = [cliDescribed.store];
210
+ let mcpKeys = null;
211
+ if (mcpPath) {
212
+ const mcpDescribed = await describeStore('mcp', mcpPath, probeNativeStore);
213
+ stores.push(mcpDescribed.store);
214
+ mcpKeys = mcpDescribed.keys;
215
+ }
216
+ const cliKeys = cliDescribed.keys;
217
+ const sharedKeys = cliKeys && mcpKeys
218
+ ? [...cliKeys].filter((key) => mcpKeys.has(key)).length
219
+ : null;
220
+ return { cwd: process.cwd(), root, stores, sharedKeys };
221
+ }
222
+ //# sourceMappingURL=memory-peek.js.map
@@ -0,0 +1,11 @@
1
+ /**
2
+ * Resolve a writable cache directory for ONNX model downloads.
3
+ *
4
+ * ADR-454: duplicated intentionally from
5
+ * `@claude-flow/embeddings/src/transformers-loader.ts`'s
6
+ * `resolveModelCacheDir()` rather than imported, since `@claude-flow/cli`
7
+ * has no dependency on `@claude-flow/embeddings` (see the circular
8
+ * optional-dep note in `memory-initializer.ts`).
9
+ */
10
+ export declare function resolveModelCacheDir(): string;
11
+ //# sourceMappingURL=model-cache-dir.d.ts.map
@@ -0,0 +1,17 @@
1
+ import * as os from 'node:os';
2
+ import * as path from 'node:path';
3
+ /**
4
+ * Resolve a writable cache directory for ONNX model downloads.
5
+ *
6
+ * ADR-454: duplicated intentionally from
7
+ * `@claude-flow/embeddings/src/transformers-loader.ts`'s
8
+ * `resolveModelCacheDir()` rather than imported, since `@claude-flow/cli`
9
+ * has no dependency on `@claude-flow/embeddings` (see the circular
10
+ * optional-dep note in `memory-initializer.ts`).
11
+ */
12
+ export function resolveModelCacheDir() {
13
+ return (process.env.TRANSFORMERS_CACHE ||
14
+ process.env.HF_HOME ||
15
+ path.join(os.homedir(), '.cache', 'ruflo', 'models'));
16
+ }
17
+ //# sourceMappingURL=model-cache-dir.js.map
@@ -19,6 +19,8 @@
19
19
  *
20
20
  * @module task-embedder
21
21
  */
22
+ import * as fs from 'node:fs';
23
+ import { resolveModelCacheDir } from '../memory/model-cache-dir.js';
22
24
  // ============================================================================
23
25
  // FNV-1a-32 hash (matches scripts/gen-seed-corpus.mjs + router-trajectory.ts)
24
26
  // ============================================================================
@@ -38,18 +40,48 @@ function cacheKey(task) {
38
40
  // same function (xenova's pipeline returns a callable that accepts either),
39
41
  // so we don't pay the ONNX model-load cost twice.
40
42
  let _extractorPromise = null;
43
+ /**
44
+ * ADR-454: distinguishes a null-extractor result caused by a genuinely
45
+ * missing dependency (permanent — nothing changes without a reinstall) from
46
+ * one caused by an environmental/fixable failure during `pipeline(...)`
47
+ * init, such as EACCES on the model cache dir (retryable).
48
+ */
49
+ let _extractorDegradedReason = null;
50
+ let _extractorLastAttemptAt = 0;
51
+ const EXTRACTOR_RETRY_INTERVAL_MS = 60_000;
41
52
  function loadExtractor() {
42
- if (_extractorPromise !== null)
43
- return _extractorPromise;
53
+ if (_extractorPromise !== null) {
54
+ const isRetryableDue = _extractorDegradedReason === 'retryable' &&
55
+ Date.now() - _extractorLastAttemptAt >= EXTRACTOR_RETRY_INTERVAL_MS;
56
+ if (!isRetryableDue)
57
+ return _extractorPromise;
58
+ // Fall through to re-attempt — the retryable failure may now be fixed.
59
+ }
60
+ _extractorLastAttemptAt = Date.now();
44
61
  _extractorPromise = (async () => {
62
+ let packageImportable = false;
45
63
  try {
46
64
  const specifier = '@xenova/transformers';
47
65
  const mod = await import(/* @vite-ignore */ specifier).catch(() => null);
48
- if (!mod || typeof mod.pipeline !== 'function')
66
+ if (!mod || typeof mod.pipeline !== 'function') {
67
+ _extractorDegradedReason = 'permanent';
49
68
  return null;
50
- return await mod.pipeline('feature-extraction', 'Xenova/all-MiniLM-L6-v2', { quantized: true });
69
+ }
70
+ packageImportable = true;
71
+ // ADR-454: route model downloads to a user-writable cache dir — the
72
+ // package-relative default is root-owned under a system-prefix
73
+ // global install, which throws EACCES for the runtime user.
74
+ const cacheDir = resolveModelCacheDir();
75
+ fs.mkdirSync(cacheDir, { recursive: true });
76
+ const env = mod.env;
77
+ if (env)
78
+ env.cacheDir = cacheDir;
79
+ const extractor = await mod.pipeline('feature-extraction', 'Xenova/all-MiniLM-L6-v2', { quantized: true });
80
+ _extractorDegradedReason = null;
81
+ return extractor;
51
82
  }
52
83
  catch {
84
+ _extractorDegradedReason = packageImportable ? 'retryable' : 'permanent';
53
85
  return null;
54
86
  }
55
87
  })();
@@ -233,5 +265,7 @@ export function __resetTaskEmbedderForTests() {
233
265
  _hits = 0;
234
266
  _misses = 0;
235
267
  _extractorPromise = null;
268
+ _extractorDegradedReason = null;
269
+ _extractorLastAttemptAt = 0;
236
270
  }
237
271
  //# sourceMappingURL=task-embedder.js.map
@@ -10,7 +10,12 @@ export interface BackupOptions {
10
10
  /** Injected epoch millis (tests pass a fixed value; avoids Date.now in logic). */
11
11
  timestamp?: number;
12
12
  verbose?: boolean;
13
+ /** Test seam: inject a fake better-sqlite3-shaped constructor instead of importing the real module. */
14
+ Database?: new (path: string, opts?: Record<string, unknown>) => any;
13
15
  }
16
+ /** Which method actually produced the snapshot — downstream migration tooling
17
+ * should require `native-online-backup` rather than trusting a blanket `backedUp: true`. */
18
+ export type BackupMethod = 'native-online-backup' | 'raw-copy-fallback';
14
19
  export interface BackupResult {
15
20
  backedUp: boolean;
16
21
  path?: string;
@@ -18,6 +23,7 @@ export interface BackupResult {
18
23
  rotatedAway?: string[];
19
24
  gcsUri?: string;
20
25
  skipped?: string;
26
+ method?: BackupMethod;
21
27
  }
22
28
  export declare function defaultMemoryDbPath(cwd?: string): string;
23
29
  export interface ProjectBackupResult extends BackupResult {
@@ -14,7 +14,7 @@
14
14
  */
15
15
  import * as fs from 'fs';
16
16
  import * as path from 'path';
17
- import { readFileMaybeEncrypted } from '../fs-secure.js';
17
+ import { existingFileIsEncrypted, readFileMaybeEncrypted } from '../fs-secure.js';
18
18
  import { loadBetterSqlite3 } from '../memory/shared-sqlite.js';
19
19
  import { resolveMemoryRoot } from '../memory/memory-root.js';
20
20
  export function defaultMemoryDbPath(cwd = process.cwd()) {
@@ -37,12 +37,18 @@ export async function backupProjectMemoryDbs(cwd, opts = {}) {
37
37
  }
38
38
  const successful = backups.filter(result => result.backedUp);
39
39
  const failed = backups.find(result => !result.backedUp && result.skipped !== 'no-db');
40
+ // Only report an aggregate `method` when every successful store agrees — a
41
+ // mixed native/raw-copy result must not collapse to whichever store
42
+ // happened to be first, the exact false-confidence #4287 is about.
43
+ const methods = new Set(successful.map(result => result.method));
44
+ const aggregateMethod = methods.size === 1 ? successful[0]?.method : undefined;
40
45
  return {
41
46
  ...successful[0],
42
47
  backedUp: successful.length > 0 && !failed,
43
48
  sizeBytes: successful.reduce((sum, result) => sum + (result.sizeBytes ?? 0), 0),
44
49
  rotatedAway: successful.flatMap(result => result.rotatedAway ?? []),
45
50
  skipped: failed?.skipped ?? (successful.length === 0 ? 'no-db' : undefined),
51
+ method: aggregateMethod,
46
52
  backups,
47
53
  };
48
54
  }
@@ -63,88 +69,25 @@ function isSnapshotFor(file, dbPath) {
63
69
  return file.startsWith(prefix)
64
70
  && /^\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}-\d{3}Z\.db$/.test(file.slice(prefix.length));
65
71
  }
66
- export async function backupMemoryDb(opts = {}) {
67
- const dbPath = opts.dbPath ?? defaultMemoryDbPath();
68
- if (!dbPath || !fs.existsSync(dbPath))
69
- return { backedUp: false, skipped: 'no-db' };
70
- let Database;
72
+ /**
73
+ * A plain file copy of a live WAL-mode SQLite database is not a consistent
74
+ * snapshot — committed data can live only in the `-wal` sidecar until it's
75
+ * checkpointed back into the main file. We can't open an RFE1-encrypted
76
+ * source natively to run a real `wal_checkpoint` probe (it isn't a SQLite
77
+ * image), so for the raw-copy-fallback path the only signal available is
78
+ * sidecar presence: a non-empty `-wal` file means there are frames that a
79
+ * copy of the main file alone would silently drop. Fail closed on that.
80
+ */
81
+ function hasActiveWal(dbPath) {
71
82
  try {
72
- const mod = 'better-sqlite3';
73
- Database = (await import(mod)).default;
83
+ return fs.statSync(`${dbPath}-wal`).size > 0;
74
84
  }
75
85
  catch {
76
- return { backedUp: false, skipped: 'better-sqlite3 unavailable' };
86
+ return false;
77
87
  }
78
- const destDir = opts.destDir ?? path.join(path.dirname(dbPath), 'backups');
79
- try {
80
- fs.mkdirSync(destDir, { recursive: true });
81
- }
82
- catch { /* */ }
83
- const destPath = path.join(destDir, `${snapshotPrefix(dbPath)}${fileStamp(opts.timestamp ?? Date.now())}.db`);
84
- // WAL-safe online backup: read-only source, consistent snapshot to destPath.
85
- let db;
86
- try {
87
- db = new Database(dbPath, { readonly: true });
88
- await db.backup(destPath);
89
- db.close();
90
- }
91
- catch (e) {
92
- try {
93
- db?.close();
94
- }
95
- catch { /* */ }
96
- // #2798 — under CLAUDE_FLOW_ENCRYPT_AT_REST the source is an RFE1
97
- // blob, not native SQLite, so better-sqlite3's online-backup API
98
- // throws "file is not a database" and the run was silently recorded
99
- // as skipped (users with encryption had ZERO backups behind a green
100
- // status). The online-backup API only exists to avoid tearing a live
101
- // WAL-mode DB; that concern doesn't apply to the encrypted file (sql.js
102
- // rewrites it wholesale per flush), so a plain byte copy is a valid
103
- // snapshot. Fall back to it before giving up.
104
- try {
105
- fs.copyFileSync(dbPath, destPath);
106
- const copiedBytes = fs.statSync(destPath).size;
107
- if (copiedBytes > 0) {
108
- // Fall through to rotation/offsite/return below via a flag path.
109
- // (Mirror the success path's tail by re-using the same code.)
110
- const keep = typeof opts.keep === 'number' && opts.keep > 0 ? opts.keep : 7;
111
- const rotatedAway = [];
112
- try {
113
- const snaps = fs.readdirSync(destDir).filter(f => isSnapshotFor(f, dbPath)).sort();
114
- while (snaps.length > keep) {
115
- const old = snaps.shift();
116
- try {
117
- fs.rmSync(path.join(destDir, old), { force: true });
118
- rotatedAway.push(old);
119
- }
120
- catch { /* */ }
121
- }
122
- }
123
- catch { /* */ }
124
- let gcsUri;
125
- if (opts.gcs) {
126
- try {
127
- const { execFileSync } = await import('child_process');
128
- const dest = opts.gcs.replace(/\/+$/, '') + '/' + path.basename(destPath);
129
- execFileSync('gcloud', ['storage', 'cp', destPath, dest], { stdio: ['ignore', 'ignore', 'inherit'] });
130
- gcsUri = dest;
131
- }
132
- catch { /* offsite failed — local snapshot stands */ }
133
- }
134
- if (opts.verbose) {
135
- console.log(`memory DB backed up (byte-copy, encrypted-at-rest) → ${destPath} (${Math.round(copiedBytes / 1024)} KB)`);
136
- }
137
- return { backedUp: true, path: destPath, sizeBytes: copiedBytes, rotatedAway, gcsUri };
138
- }
139
- }
140
- catch { /* byte-copy also failed — report the original error */ }
141
- return { backedUp: false, skipped: `backup failed: ${e?.message ?? e}` };
142
- }
143
- let sizeBytes = 0;
144
- try {
145
- sizeBytes = fs.statSync(destPath).size;
146
- }
147
- catch { /* */ }
88
+ }
89
+ /** Shared tail: rotation + optional GCS offsite + verbose logging + the success receipt. */
90
+ async function finishBackup(destPath, dbPath, destDir, opts, method, sizeBytes) {
148
91
  // Rotation — ISO-stamped names sort chronologically, so keep the tail.
149
92
  const keep = typeof opts.keep === 'number' && opts.keep > 0 ? opts.keep : 7;
150
93
  const rotatedAway = [];
@@ -172,11 +115,96 @@ export async function backupMemoryDb(opts = {}) {
172
115
  catch { /* offsite failed — local snapshot stands */ }
173
116
  }
174
117
  if (opts.verbose) {
175
- console.log(`memory DB backed up → ${destPath} (${Math.round(sizeBytes / 1024)} KB)` +
118
+ const label = method === 'raw-copy-fallback' ? ' (byte-copy, encrypted-at-rest)' : '';
119
+ console.log(`memory DB backed up${label} → ${destPath} (${Math.round(sizeBytes / 1024)} KB)` +
176
120
  (rotatedAway.length ? `, rotated ${rotatedAway.length} old` : '') +
177
121
  (gcsUri ? `, offsite ${gcsUri}` : ''));
178
122
  }
179
- return { backedUp: true, path: destPath, sizeBytes, rotatedAway, gcsUri };
123
+ return { backedUp: true, path: destPath, sizeBytes, rotatedAway, gcsUri, method };
124
+ }
125
+ export async function backupMemoryDb(opts = {}) {
126
+ const dbPath = opts.dbPath ?? defaultMemoryDbPath();
127
+ if (!dbPath || !fs.existsSync(dbPath))
128
+ return { backedUp: false, skipped: 'no-db' };
129
+ const destDir = opts.destDir ?? path.join(path.dirname(dbPath), 'backups');
130
+ try {
131
+ fs.mkdirSync(destDir, { recursive: true });
132
+ }
133
+ catch { /* */ }
134
+ const destPath = path.join(destDir, `${snapshotPrefix(dbPath)}${fileStamp(opts.timestamp ?? Date.now())}.db`);
135
+ let Database = opts.Database;
136
+ let nativeError = null;
137
+ if (!Database) {
138
+ try {
139
+ const mod = 'better-sqlite3';
140
+ Database = (await import(mod)).default;
141
+ }
142
+ catch {
143
+ nativeError = 'better-sqlite3 unavailable';
144
+ }
145
+ }
146
+ if (!nativeError) {
147
+ // WAL-safe online backup: read-only source, consistent snapshot to destPath.
148
+ let db;
149
+ try {
150
+ db = new Database(dbPath, { readonly: true });
151
+ await db.backup(destPath);
152
+ db.close();
153
+ let sizeBytes = 0;
154
+ try {
155
+ sizeBytes = fs.statSync(destPath).size;
156
+ }
157
+ catch { /* */ }
158
+ return await finishBackup(destPath, dbPath, destDir, opts, 'native-online-backup', sizeBytes);
159
+ }
160
+ catch (e) {
161
+ try {
162
+ db?.close();
163
+ }
164
+ catch { /* */ }
165
+ try {
166
+ fs.rmSync(destPath, { force: true });
167
+ }
168
+ catch { /* a native backup() that throws mid-write can leave a torn destPath */ }
169
+ nativeError = e?.message ?? String(e);
170
+ }
171
+ }
172
+ // #2798 — under CLAUDE_FLOW_ENCRYPT_AT_REST the source is an RFE1 blob, not
173
+ // native SQLite, so better-sqlite3's online-backup API throws "file is not
174
+ // a database" (or isn't even importable on a WASM-only host). The
175
+ // online-backup API exists to avoid tearing a live WAL-mode DB; that
176
+ // concern doesn't apply to the encrypted file (sql.js rewrites it wholesale
177
+ // per flush), so a plain byte copy IS a valid snapshot there.
178
+ //
179
+ // But this fallback must never fire for an ORDINARY (non-encrypted) source
180
+ // just because the native backup threw — SQLITE_BUSY, a disk error, or any
181
+ // other native-backup failure on a perfectly normal database must fail
182
+ // closed, not silently report `backedUp: true` off a raw copy. Positively
183
+ // verify encryption (4-byte RFE1 magic sniff) before ever taking this path;
184
+ // "the native backup threw" is never by itself evidence the source is
185
+ // encrypted.
186
+ if (existingFileIsEncrypted(dbPath) !== true) {
187
+ return { backedUp: false, skipped: `native backup failed: ${nativeError}` };
188
+ }
189
+ // Even for a confirmed-encrypted source, refuse the raw-copy fallback if
190
+ // there's an active WAL — a plain copy of the main file alone would miss
191
+ // uncommitted WAL frames and silently produce an inconsistent snapshot.
192
+ if (hasActiveWal(dbPath)) {
193
+ return { backedUp: false, skipped: 'active WAL present — refusing raw-copy fallback (would be inconsistent)' };
194
+ }
195
+ try {
196
+ fs.copyFileSync(dbPath, destPath);
197
+ const copiedBytes = fs.statSync(destPath).size;
198
+ if (copiedBytes > 0) {
199
+ return await finishBackup(destPath, dbPath, destDir, opts, 'raw-copy-fallback', copiedBytes);
200
+ }
201
+ }
202
+ catch { /* byte-copy also failed — report the original native error below */ }
203
+ try {
204
+ fs.rmSync(destPath, { force: true });
205
+ }
206
+ catch { /* */ }
207
+ return { backedUp: false, skipped: `native backup failed: ${nativeError}` };
180
208
  }
181
209
  /** Verify a snapshot without modifying it, including RFE1 images and WASM-only hosts. */
182
210
  async function verifiedBackupRows(file, Database) {
@@ -32,6 +32,8 @@ export type AnchorAssessment = {
32
32
  };
33
33
  export type AnchorWriter = (file: string, value: unknown) => Promise<void>;
34
34
  export type StreamingAnchorWriter = (file: string, value: AtomicJsonSource) => Promise<void>;
35
+ export declare const ANCHOR_LOG_FILE = "ledger-anchors.json";
36
+ export declare const ANCHOR_MIRROR_FILE = "ledger-anchor-head.json";
35
37
  export declare const anchorLogPath: (projectRoot: string) => string;
36
38
  /**
37
39
  * Directory holding per-project trust material (anchor mirror, HMAC key).
@@ -33,9 +33,9 @@ import { closeSync, constants as fsConstants, fstatSync, existsSync, mkdtempSync
33
33
  import { isAbsolute, join, resolve } from 'node:path';
34
34
  import { homedir, tmpdir, userInfo } from 'node:os';
35
35
  import { AgenticPolicyEngine, canonicalizePolicy } from '@claude-flow/security/policy/index';
36
- const LOG_FILE = 'ledger-anchors.json';
37
- const MIRROR_FILE = 'ledger-anchor-head.json';
38
- export const anchorLogPath = (projectRoot) => join(resolve(projectRoot), '.claude-flow', 'policy', LOG_FILE);
36
+ export const ANCHOR_LOG_FILE = 'ledger-anchors.json';
37
+ export const ANCHOR_MIRROR_FILE = 'ledger-anchor-head.json';
38
+ export const anchorLogPath = (projectRoot) => join(resolve(projectRoot), '.claude-flow', 'policy', ANCHOR_LOG_FILE);
39
39
  /**
40
40
  * The home directory trust material lives under: $HOME when it is an absolute path, else the account's home. An empty or relative
41
41
  * $HOME (an unset variable in a wrapper script, `HOME=. cmd`) would make join() yield a path relative to the working directory and
@@ -71,7 +71,7 @@ export function policyTrustRoot() {
71
71
  }
72
72
  function mirrorPath(projectRoot) {
73
73
  const id = createHash('sha256').update(realpathSync(projectRoot)).digest('hex');
74
- return join(policyTrustRoot(), id, MIRROR_FILE);
74
+ return join(policyTrustRoot(), id, ANCHOR_MIRROR_FILE);
75
75
  }
76
76
  // Duplicated from policy-runtime.ts's RECEIPT_ARCHIVE_FILE rather than
77
77
  // imported, to avoid a circular dependency (policy-runtime.ts imports this