@claude-flow/cli 3.55.0 → 3.56.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 (64) hide show
  1. package/.claude/helpers/helpers.manifest.json +2 -2
  2. package/.claude/settings.json +9 -9
  3. package/catalog-manifest.json +2 -2
  4. package/dist/src/auth/client.js +40 -24
  5. package/dist/src/commands/agent.js +12 -0
  6. package/dist/src/commands/analyze.js +18 -14
  7. package/dist/src/commands/daemon.js +11 -19
  8. package/dist/src/commands/doctor.js +65 -58
  9. package/dist/src/commands/memory-backup.d.ts +1 -1
  10. package/dist/src/commands/memory-backup.js +23 -9
  11. package/dist/src/commands/memory.d.ts +0 -4
  12. package/dist/src/commands/memory.js +10 -2
  13. package/dist/src/commands/plugins.js +13 -1
  14. package/dist/src/commands/task.js +12 -0
  15. package/dist/src/init/settings-generator.js +3 -3
  16. package/dist/src/mcp-tools/agent-execute-core.js +244 -188
  17. package/dist/src/mcp-tools/agent-tools.js +15 -16
  18. package/dist/src/mcp-tools/agentdb-tools.js +22 -3
  19. package/dist/src/mcp-tools/claims-tools.js +3 -11
  20. package/dist/src/mcp-tools/hooks-tools.js +47 -2
  21. package/dist/src/mcp-tools/memory-tools.js +29 -10
  22. package/dist/src/mcp-tools/record-store.d.ts +3 -0
  23. package/dist/src/mcp-tools/record-store.js +21 -0
  24. package/dist/src/mcp-tools/session-tools.js +47 -13
  25. package/dist/src/mcp-tools/task-tools.js +5 -11
  26. package/dist/src/mcp-tools/workflow-tools.js +78 -29
  27. package/dist/src/memory/append-conditions.d.ts +28 -0
  28. package/dist/src/memory/append-conditions.js +56 -0
  29. package/dist/src/memory/graph-edge-writer.d.ts +9 -0
  30. package/dist/src/memory/graph-edge-writer.js +90 -0
  31. package/dist/src/memory/memory-bridge.d.ts +4 -0
  32. package/dist/src/memory/memory-bridge.js +50 -9
  33. package/dist/src/memory/memory-initializer.d.ts +4 -0
  34. package/dist/src/memory/memory-initializer.js +95 -61
  35. package/dist/src/memory/memory-root.d.ts +2 -0
  36. package/dist/src/memory/memory-root.js +28 -0
  37. package/dist/src/memory/sona-optimizer.d.ts +2 -0
  38. package/dist/src/memory/sona-optimizer.js +4 -0
  39. package/dist/src/parser.js +7 -0
  40. package/dist/src/plugins/store/discovery.d.ts +6 -0
  41. package/dist/src/plugins/store/discovery.js +7 -3
  42. package/dist/src/ruvector/neural-router.js +7 -2
  43. package/dist/src/services/claim-service.js +39 -18
  44. package/dist/src/services/claims-store.d.ts +3 -0
  45. package/dist/src/services/claims-store.js +40 -0
  46. package/dist/src/services/distill-tuning.js +17 -10
  47. package/dist/src/services/memory-backup.d.ts +7 -0
  48. package/dist/src/services/memory-backup.js +29 -2
  49. package/dist/src/services/memory-distillation.js +2 -2
  50. package/dist/src/services/worker-daemon.d.ts +4 -2
  51. package/dist/src/services/worker-daemon.js +14 -6
  52. package/node_modules/@claude-flow/codex/dist/cli.js +0 -0
  53. package/node_modules/@claude-flow/security/dist/oauth/callback-server.d.ts +2 -1
  54. package/node_modules/@claude-flow/security/dist/oauth/callback-server.d.ts.map +1 -1
  55. package/node_modules/@claude-flow/security/dist/oauth/callback-server.js +48 -16
  56. package/node_modules/@claude-flow/security/dist/oauth/callback-server.js.map +1 -1
  57. package/node_modules/@claude-flow/security/dist/path-validator.d.ts +2 -2
  58. package/node_modules/@claude-flow/security/dist/path-validator.d.ts.map +1 -1
  59. package/node_modules/@claude-flow/security/dist/path-validator.js +15 -4
  60. package/node_modules/@claude-flow/security/dist/path-validator.js.map +1 -1
  61. package/node_modules/@claude-flow/security/package.json +1 -1
  62. package/package.json +4 -4
  63. package/plugins/ruflo-metaharness/CHANGELOG.md +48 -0
  64. package/.claude/agents/tmp.json +0 -0
@@ -0,0 +1,28 @@
1
+ export type AppendCondition = {
2
+ namespace: string;
3
+ } & ({
4
+ key: string;
5
+ sha256: string;
6
+ latestPrefix?: string;
7
+ } | {
8
+ absent: true;
9
+ latestPrefix: string;
10
+ } | {
11
+ keysSha256: string;
12
+ count: number;
13
+ });
14
+ export declare class AppendConditionFailed extends Error {
15
+ }
16
+ export declare function validateAppendConditions(input: unknown): AppendCondition[];
17
+ interface NativeReader {
18
+ prepare(sql: string): {
19
+ all(...parameters: string[]): Array<{
20
+ key: string;
21
+ content: string;
22
+ status: string | null;
23
+ }>;
24
+ };
25
+ }
26
+ export declare function assertAppendConditions(db: NativeReader, conditions: AppendCondition[]): void;
27
+ export {};
28
+ //# sourceMappingURL=append-conditions.d.ts.map
@@ -0,0 +1,56 @@
1
+ /** Native append predicates checked in the SAME write transaction as an immutable insert. */
2
+ import { createHash } from 'node:crypto';
3
+ export class AppendConditionFailed extends Error {
4
+ }
5
+ const text = (value, limit) => typeof value === 'string' && value.length > 0 && value.length <= limit;
6
+ const digest = (value) => typeof value === 'string' && /^[a-f0-9]{64}$/.test(value);
7
+ export function validateAppendConditions(input) {
8
+ if (!Array.isArray(input) || input.length < 1 || input.length > 8)
9
+ throw new TypeError('append conditions must contain 1-8 predicates');
10
+ return input.map(condition => {
11
+ if (!condition || typeof condition !== 'object' || Array.isArray(condition))
12
+ throw new TypeError('invalid append condition');
13
+ const row = condition;
14
+ const fields = Object.keys(row).sort().join(',');
15
+ const validRecord = ['key,namespace,sha256', 'key,latestPrefix,namespace,sha256'].includes(fields)
16
+ && text(row.key, 1024) && digest(row.sha256) && (row.latestPrefix === undefined || text(row.latestPrefix, 1024));
17
+ const validAbsent = fields === 'absent,latestPrefix,namespace' && row.absent === true && text(row.latestPrefix, 1024);
18
+ const validKeys = fields === 'count,keysSha256,namespace' && digest(row.keysSha256)
19
+ && Number.isSafeInteger(row.count) && row.count >= 0 && row.count <= 100_000;
20
+ if (!text(row.namespace, 256) || !(validRecord || validAbsent || validKeys))
21
+ throw new TypeError('invalid append condition');
22
+ return row;
23
+ });
24
+ }
25
+ const sha = (value) => createHash('sha256').update(value).digest('hex');
26
+ export function assertAppendConditions(db, conditions) {
27
+ for (const condition of conditions) {
28
+ if ('keysSha256' in condition) {
29
+ const rows = db.prepare("SELECT key FROM memory_entries WHERE namespace = ? AND (status = 'active' OR status IS NULL)")
30
+ .all(condition.namespace);
31
+ const keys = rows.map(row => row.key).sort();
32
+ if (keys.length !== condition.count || sha(JSON.stringify(keys)) !== condition.keysSha256) {
33
+ throw new AppendConditionFailed('immutable append rejected: namespace membership changed');
34
+ }
35
+ continue;
36
+ }
37
+ if ('key' in condition) {
38
+ const rows = db.prepare('SELECT key, content, status FROM memory_entries WHERE namespace = ? AND key = ?')
39
+ .all(condition.namespace, condition.key);
40
+ if (rows.length !== 1 || !['active', null].includes(rows[0].status)
41
+ || typeof rows[0].content !== 'string' || sha(rows[0].content) !== condition.sha256) {
42
+ throw new AppendConditionFailed('immutable append rejected: expected parent/authority bytes changed or absent');
43
+ }
44
+ }
45
+ if (condition.latestPrefix !== undefined) {
46
+ // Include tombstones: deleting a new authority slot must never rewind to an older authority.
47
+ const pattern = condition.latestPrefix.replace(/[\\%_]/g, character => `\\${character}`) + '%';
48
+ const latest = db.prepare("SELECT key, content, status FROM memory_entries WHERE namespace = ? AND key LIKE ? ESCAPE '\\' ORDER BY key COLLATE BINARY DESC LIMIT 1")
49
+ .all(condition.namespace, pattern);
50
+ if ('absent' in condition ? latest.length !== 0 : latest.length !== 1 || latest[0].key !== condition.key) {
51
+ throw new AppendConditionFailed('immutable append rejected: newer parent/authority slot exists');
52
+ }
53
+ }
54
+ }
55
+ }
56
+ //# sourceMappingURL=append-conditions.js.map
@@ -29,6 +29,15 @@
29
29
  *
30
30
  * @module v3/cli/memory/graph-edge-writer
31
31
  */
32
+ /**
33
+ * True iff the memory.db bridge at `customDbPath` (or the default location)
34
+ * is currently RFE1-encrypted-at-rest. Callers that receive `null` from
35
+ * getBridgeDb()/insertGraphEdge() can use this to avoid recommending the
36
+ * destructive `memory init` command for this specific, non-corrupt case.
37
+ */
38
+ export declare function isBridgeDbEncryptedAtRest(customDbPath?: string): boolean;
39
+ /** Test-only: clear the once-per-path warning dedup set. */
40
+ export declare function _resetRfe1WarningFlag(): void;
32
41
  /**
33
42
  * Checkpoint and close the cached handle if it is open (optionally only if it
34
43
  * is open on `dbPath`). Returns true if a handle was released. Never throws.
@@ -35,6 +35,87 @@ import * as crypto from 'crypto';
35
35
  import { getMemoryRoot } from './memory-initializer.js';
36
36
  import { encodeEmbedding } from './embedding-quantization.js';
37
37
  import { loadBetterSqlite3 } from './shared-sqlite.js';
38
+ import { isEncryptedBlob } from '../encryption/vault.js';
39
+ // ============================================================================
40
+ // #2889 — RFE1 encryption-at-rest guard
41
+ // ============================================================================
42
+ //
43
+ // When CLAUDE_FLOW_ENCRYPT_AT_REST is on, memory.db is an opaque RFE1 blob
44
+ // (see encryption/vault.ts). `new BetterSqlite3(dbPath)` has no RFE1 sniff
45
+ // and throws SQLITE_NOTADB on it, which the generic catch below swallows —
46
+ // indistinguishable from genuine corruption, a missing native module, or a
47
+ // permissions failure. That ambiguity is what let #2889 happen: nothing
48
+ // stopped a caller from concluding the store was broken and running the
49
+ // destructive `memory init`, which creates a fresh EMPTY store instead of
50
+ // using the existing encrypted data.
51
+ //
52
+ // Fix: sniff the header bytes (never buffer the whole file — memory.db can
53
+ // be multi-GB) before attempting the native open. If it's RFE1-encrypted,
54
+ // skip the native open (it would only throw) and warn once per distinct
55
+ // dbPath (not once per process — a process can legitimately touch more
56
+ // than one encrypted store via a custom `dbPath` argument).
57
+ /** Read just the first `len` bytes of a file without loading the whole file
58
+ * into memory. Mirrors the pattern in commands/doctor.ts's readHeaderBytes
59
+ * (not imported from there — this is a memory-layer module and must not
60
+ * depend on the command layer). */
61
+ function readHeaderBytes(filePath, len) {
62
+ const fd = fs.openSync(filePath, 'r');
63
+ try {
64
+ const buf = Buffer.alloc(len);
65
+ const bytesRead = fs.readSync(fd, buf, 0, len, 0);
66
+ return buf.subarray(0, bytesRead);
67
+ }
68
+ finally {
69
+ fs.closeSync(fd);
70
+ }
71
+ }
72
+ // RFE1 wire format is magic(4) + iv(12) + ciphertext(N) + tag(16); the
73
+ // shortest possible blob is 32 bytes. 64 gives headroom.
74
+ const ENCRYPTION_SNIFF_LEN = 64;
75
+ /** True iff `filePath` is a legitimately RFE1-encrypted-at-rest file. A read
76
+ * failure (permissions, races) is treated as "not encrypted" — the caller's
77
+ * own open/query error handling still runs right after. */
78
+ function isRfe1EncryptedAtRest(filePath) {
79
+ try {
80
+ return isEncryptedBlob(readHeaderBytes(filePath, ENCRYPTION_SNIFF_LEN));
81
+ }
82
+ catch {
83
+ return false;
84
+ }
85
+ }
86
+ // Keyed by resolved path (not a single process-wide boolean) — a process
87
+ // that touches more than one RFE1-encrypted dbPath (a custom `dbPath` passed
88
+ // to insertGraphEdge/queryEdgesBySource/countGraphEdges, or any multi-store
89
+ // scenario) must still get the warning for EACH distinct path, not just the
90
+ // first one it happens to see.
91
+ const _warnedRfe1EncryptedPaths = new Set();
92
+ function warnRfe1EncryptedOnce(dbPath) {
93
+ const resolved = path.resolve(dbPath);
94
+ if (_warnedRfe1EncryptedPaths.has(resolved))
95
+ return;
96
+ _warnedRfe1EncryptedPaths.add(resolved);
97
+ // eslint-disable-next-line no-console
98
+ console.error(`[graph-edge-writer] ${dbPath} is encrypted at rest (CLAUDE_FLOW_ENCRYPT_AT_REST). ` +
99
+ 'The native graph-edge bridge cannot open an RFE1-encrypted store directly — ' +
100
+ 'this is a known limitation, not corruption. Your existing data is NOT lost ' +
101
+ 'or damaged. Do NOT run `memory init` in response to this — that would create ' +
102
+ 'a fresh, empty store instead of using your existing encrypted data. ' +
103
+ 'See #2889 (https://github.com/ruvnet/ruflo/issues/2889) for details and recovery options.');
104
+ }
105
+ /**
106
+ * True iff the memory.db bridge at `customDbPath` (or the default location)
107
+ * is currently RFE1-encrypted-at-rest. Callers that receive `null` from
108
+ * getBridgeDb()/insertGraphEdge() can use this to avoid recommending the
109
+ * destructive `memory init` command for this specific, non-corrupt case.
110
+ */
111
+ export function isBridgeDbEncryptedAtRest(customDbPath) {
112
+ const dbPath = customDbPath ?? path.join(getMemoryRoot(), 'memory.db');
113
+ return fs.existsSync(dbPath) && isRfe1EncryptedAtRest(dbPath);
114
+ }
115
+ /** Test-only: clear the once-per-path warning dedup set. */
116
+ export function _resetRfe1WarningFlag() {
117
+ _warnedRfe1EncryptedPaths.clear();
118
+ }
38
119
  // ============================================================================
39
120
  // Lazy-cached better-sqlite3 db handle
40
121
  // ============================================================================
@@ -134,6 +215,15 @@ export async function getBridgeDb(customDbPath, opts) {
134
215
  const dbExists = fs.existsSync(dbPath);
135
216
  if (!dbExists && !createIfMissing)
136
217
  return null;
218
+ // #2889 — detect RFE1 encryption-at-rest before even attempting the
219
+ // native open: `new BetterSqlite3(dbPath)` on an encrypted file would
220
+ // only throw SQLITE_NOTADB, caught by the generic catch below with no
221
+ // way to tell it apart from genuine corruption. Skip straight to a
222
+ // clear, actionable warning instead.
223
+ if (dbExists && isRfe1EncryptedAtRest(dbPath)) {
224
+ warnRfe1EncryptedOnce(dbPath);
225
+ return null;
226
+ }
137
227
  // Ensure parent dir exists for createIfMissing case.
138
228
  if (!dbExists && createIfMissing) {
139
229
  const dir = path.dirname(dbPath);
@@ -16,6 +16,7 @@
16
16
  *
17
17
  * @module v3/cli/memory-bridge
18
18
  */
19
+ import { type AppendCondition } from './append-conditions.js';
19
20
  /**
20
21
  * #3024: AgentDB's optional native controller stack can abort the whole Node
21
22
  * process on Windows during registry initialization (a Rust allocation panic),
@@ -67,6 +68,9 @@ export declare function bridgeStoreEntry(options: {
67
68
  ttl?: number;
68
69
  dbPath?: string;
69
70
  upsert?: boolean;
71
+ requireNative?: boolean;
72
+ appendOnly?: boolean;
73
+ appendConditions?: AppendCondition[];
70
74
  /** ADR-323: defaults to 'unknown' when omitted. */
71
75
  provenanceType?: string;
72
76
  }): Promise<{
@@ -16,6 +16,7 @@
16
16
  *
17
17
  * @module v3/cli/memory-bridge
18
18
  */
19
+ import { AppendConditionFailed, assertAppendConditions, validateAppendConditions } from './append-conditions.js';
19
20
  import { liveMemoryRowSql } from './live-memory-row.js';
20
21
  import { resolveAgentdbBetterSqlite3 } from './shared-sqlite.js';
21
22
  import { encodeEmbeddingQ8, MAX_LIST_EMBEDDINGS } from './embedding-q8.js';
@@ -1025,13 +1026,24 @@ export async function bridgeStoreEntry(options) {
1025
1026
  const ctx = getDb(registry);
1026
1027
  if (!ctx)
1027
1028
  return null;
1029
+ if (options.appendOnly && !options.requireNative) {
1030
+ return { success: false, id: '', error: 'Immutable append requires a native writer' };
1031
+ }
1032
+ if (options.appendConditions && (!options.requireNative || !options.appendOnly)) {
1033
+ return { success: false, id: "", error: "Conditional append requires native append-only storage" };
1034
+ }
1035
+ if (options.requireNative && (ctx.agentdb?.isWasm === true || typeof ctx.db.inTransaction !== 'boolean'
1036
+ || typeof ctx.db.transaction !== 'function' || ctx.db.memory !== false || typeof ctx.db.name !== 'string'
1037
+ || !ctx.db.name || canonicalDbPath(ctx.db.name) !== canonicalDbPath(options.dbPath))) {
1038
+ return { success: false, id: '', error: 'Native memory writer required; refusing non-native bridge driver' };
1039
+ }
1028
1040
  try {
1029
1041
  const { key, value, namespace = 'default', tags = [], ttl } = options;
1030
1042
  let provenanceType = options.provenanceType ?? 'unknown';
1031
1043
  // An omitted type on an upsert means "update the value", not "erase the
1032
1044
  // existing trust label". New rows still receive the backward-compatible
1033
1045
  // unknown default.
1034
- if (options.upsert && options.provenanceType === undefined) {
1046
+ if (options.upsert && !options.appendOnly && options.provenanceType === undefined) {
1035
1047
  try {
1036
1048
  const existing = ctx.db.prepare('SELECT provenance_type FROM memory_entries WHERE namespace = ? AND key = ? LIMIT 1').get(namespace, key);
1037
1049
  provenanceType = existing?.provenance_type || 'unknown';
@@ -1089,7 +1101,12 @@ export async function bridgeStoreEntry(options) {
1089
1101
  // below → typed "already exists"
1090
1102
  // error, NEVER a null demotion).
1091
1103
  // Upsert path (INSERT OR REPLACE) is unchanged.
1092
- const insertSql = options.upsert
1104
+ const immutableInsertSql = `INSERT INTO memory_entries (
1105
+ id, key, namespace, content, type,
1106
+ embedding, embedding_dimensions, embedding_model,
1107
+ tags, metadata, provenance_type, created_at, updated_at, expires_at, status
1108
+ ) VALUES (?, ?, ?, ?, 'semantic', ?, ?, ?, ?, ?, ?, ?, ?, ?, 'active')`;
1109
+ const insertSql = options.appendOnly ? immutableInsertSql : options.upsert
1093
1110
  ? `INSERT OR REPLACE INTO memory_entries (
1094
1111
  id, key, namespace, content, type,
1095
1112
  embedding, embedding_dimensions, embedding_model,
@@ -1126,7 +1143,28 @@ export async function bridgeStoreEntry(options) {
1126
1143
  }
1127
1144
  catch { /* vector_indexes may not exist on legacy DBs — fall through */ }
1128
1145
  const stmt = ctx.db.prepare(insertSql);
1129
- const runResult = stmt.run(id, key, namespace, value, embeddingJson, dimensions || null, model, tags.length > 0 ? JSON.stringify(tags) : null, '{}', provenanceType, now, now, ttl ? now + (ttl * 1000) : null);
1146
+ const insert = () => stmt.run(id, key, namespace, value, embeddingJson, dimensions || null, model, tags.length > 0 ? JSON.stringify(tags) : null, '{}', provenanceType, now, now, ttl ? now + (ttl * 1000) : null);
1147
+ let runResult;
1148
+ try {
1149
+ runResult = options.appendOnly
1150
+ ? ctx.db.transaction(() => {
1151
+ // Legacy native tables may predate UNIQUE(namespace,key). Serialize the exact
1152
+ // logical-slot check with INSERT, including tombstones, instead of trusting DDL.
1153
+ if (ctx.db.prepare('SELECT key FROM memory_entries WHERE namespace = ? AND key = ?').all(namespace, key).length) {
1154
+ throw new AppendConditionFailed('immutable append rejected: logical key already exists');
1155
+ }
1156
+ if (options.appendConditions)
1157
+ assertAppendConditions(ctx.db, validateAppendConditions(options.appendConditions));
1158
+ return insert();
1159
+ }).immediate()
1160
+ : insert();
1161
+ }
1162
+ catch (error) {
1163
+ if (error instanceof AppendConditionFailed || error instanceof TypeError) {
1164
+ return { success: false, id: '', error: error.message };
1165
+ }
1166
+ throw error;
1167
+ }
1130
1168
  // A completed native write proves the bridge is currently healthy. Do not
1131
1169
  // retain a diagnostic from an earlier transient failure and append it to a
1132
1170
  // later, unrelated sql.js fallback refusal.
@@ -1143,18 +1181,21 @@ export async function bridgeStoreEntry(options) {
1143
1181
  error: `key "${key}" already exists in namespace "${namespace}" — pass upsert=true (--upsert on the CLI) to update it`,
1144
1182
  };
1145
1183
  }
1184
+ // A non-vector immutable INSERT cannot change the vector count, even with concurrent
1185
+ // writers. Unlike historical strict mode, it never resurrects a vector tombstone.
1146
1186
  // #2558: keep `vector_indexes.total_vectors` accurate so status/tooling
1147
1187
  // stop reporting "HNSW index: 0 vectors" while embedded entries exist.
1148
- try {
1149
- ctx.db
1150
- .prepare(`UPDATE vector_indexes SET
1188
+ if (!(options.appendOnly && embeddingJson === null))
1189
+ try {
1190
+ ctx.db
1191
+ .prepare(`UPDATE vector_indexes SET
1151
1192
  total_vectors = (SELECT COUNT(*) FROM memory_entries
1152
1193
  WHERE namespace = ? AND embedding IS NOT NULL),
1153
1194
  updated_at = ?
1154
1195
  WHERE name = ?`)
1155
- .run(namespace, now, namespace);
1156
- }
1157
- catch { /* vector_indexes may not exist on legacy DBs — non-fatal */ }
1196
+ .run(namespace, now, namespace);
1197
+ }
1198
+ catch { /* vector_indexes may not exist on legacy DBs — non-fatal */ }
1158
1199
  // #2558: better-sqlite3 opens the DB in WAL mode and, for the small write
1159
1200
  // volumes typical of CLI usage, may never reach the auto-checkpoint
1160
1201
  // threshold — leaving committed rows only in the -wal file. WAL-blind
@@ -8,6 +8,7 @@
8
8
  *
9
9
  * @module v3/cli/memory-initializer
10
10
  */
11
+ import type { AppendCondition } from './append-conditions.js';
11
12
  import { type EmbeddingQ8 } from './embedding-q8.js';
12
13
  /**
13
14
  * ADR-323 — typed memory provenance. Distinguishes WHO/WHAT wrote a memory
@@ -442,6 +443,9 @@ export declare function storeEntry(options: {
442
443
  ttl?: number;
443
444
  dbPath?: string;
444
445
  upsert?: boolean;
446
+ requireNative?: boolean;
447
+ appendOnly?: boolean;
448
+ appendConditions?: AppendCondition[];
445
449
  /** ADR-323: defaults to 'unknown' when omitted. */
446
450
  provenanceType?: string;
447
451
  }): Promise<{
@@ -1,14 +1,5 @@
1
- /**
2
- * V3 Memory Initializer
3
- * Properly initializes the memory database with sql.js (WASM SQLite)
4
- * Includes pattern tables, vector embeddings, migration state tracking
5
- *
6
- * ADR-053: Routes through ControllerRegistry → AgentDB v3 when available,
7
- * falls back to raw sql.js for backwards compatibility.
8
- *
9
- * @module v3/cli/memory-initializer
10
- */
11
1
  import { loadBetterSqlite3 } from './shared-sqlite.js';
2
+ import { resolveMemoryRoot } from './memory-root.js';
12
3
  import { liveMemoryRowSql } from './live-memory-row.js';
13
4
  import { encodeEmbeddingQ8, MAX_LIST_EMBEDDINGS } from './embedding-q8.js';
14
5
  import * as fs from 'fs';
@@ -59,25 +50,88 @@ function isRuvectorCoreResolvable() {
59
50
  return _ruvectorCoreResolvable;
60
51
  }
61
52
  /**
62
- * #2735 — before a whole-image sql.js read-modify-persist (export() +
63
- * rename over the live database path), refuse if there is evidence of a
64
- * live native (better-sqlite3) WAL connection: `-wal`/`-shm` sidecar files
65
- * on disk. A native connection in WAL mode keeps its sidecars present for
66
- * its entire lifetime (removed only on the last connection's clean close),
67
- * so their presence is strong evidence of a live native holder — and their
68
- * absence means the image is a clean, checkpointed, standalone file that
69
- * sql.js can safely read-modify-write.
53
+ * #2735 / #3161 — before a whole-image sql.js read-modify-persist (export() +
54
+ * rename over the live database path), refuse if a native (better-sqlite3)
55
+ * WAL connection is actually LIVE. `-wal`/`-shm` sidecar presence alone is
56
+ * not proof of that: #3161 found `ruflo doctor`'s readonly diagnostic
57
+ * connections leave orphaned sidecars behind after a plain `.close()` (WAL
58
+ * mode does not checkpoint on close unless the closing connection was the
59
+ * last holder in a state that triggers one), permanently blocking every
60
+ * later write in that project.
61
+ *
62
+ * Liveness is probed in two steps, because a single `wal_checkpoint`
63
+ * busy-check is not sufficient on its own:
70
64
  *
71
- * This is a scoped-down version of the fuller "scan live process holders"
72
- * design discussed in #2735: it does not close the narrow assess-then-write
73
- * race (a native opener could still attach in the gap between this check
74
- * and the write), but it directly closes the demonstrated corruption
75
- * mechanism — a whole-image write proceeding while an ALREADY-OPEN native
76
- * connection's sidecars are on disk — with no platform-specific process
77
- * scanning. Fails closed (treats a stat error as "unsafe") because this is
78
- * a safety gate, not a best-effort probe.
65
+ * 1. Open our own probe connection and run `PRAGMA wal_checkpoint(TRUNCATE)`.
66
+ * `busy !== 0` means another connection genuinely holds a lock blocking
67
+ * the checkpoint RIGHT NOW (an open transaction) — unsafe, refuse
68
+ * immediately. This also truncates a genuinely-orphaned WAL as a side
69
+ * effect.
70
+ * 2. If `busy === 0`, close our probe connection and re-check whether the
71
+ * sidecars are still on disk. SQLite only deletes `-wal`/`-shm` when the
72
+ * LAST connection to the database closes. If nothing else is attached,
73
+ * OUR close is that last close and the sidecars vanish — safe, proceed.
74
+ * If they are still present after our own close, some OTHER connection
75
+ * is still attached (even idle, holding no lock — e.g. a foreign native
76
+ * handle that already finished its last statement), so step 1's
77
+ * busy-check alone would have missed it. Refuse.
78
+ *
79
+ * Step 2 is what makes this safe against an idle-but-attached native
80
+ * connection (an open handle holding no lock does not block checkpointing,
81
+ * so step 1 alone would report "safe" even though a real holder is still
82
+ * attached) without resorting to platform-specific process scanning.
83
+ *
84
+ * The common case (no sidecars at all) stays a couple of cheap `existsSync`
85
+ * calls — a DB connection is only opened when a sidecar is actually present
86
+ * to investigate.
87
+ *
88
+ * This is still a scoped-down version of the fuller "scan live process
89
+ * holders" design discussed in #2735: it does not close the narrow
90
+ * assess-then-write race (a native opener could still attach in the gap
91
+ * between this check and the write), but it directly closes the
92
+ * demonstrated corruption mechanism — a whole-image write proceeding while
93
+ * an ALREADY-OPEN native connection genuinely holds the file — with no
94
+ * platform-specific process scanning. Fails closed (treats any probe error
95
+ * as "unsafe") because this is a safety gate, not a best-effort probe.
79
96
  */
80
- function hasNativeWalSidecars(dbPath) {
97
+ async function hasNativeWalSidecars(dbPath) {
98
+ try {
99
+ if (!fs.existsSync(`${dbPath}-wal`) && !fs.existsSync(`${dbPath}-shm`)) {
100
+ return false;
101
+ }
102
+ }
103
+ catch {
104
+ return true;
105
+ }
106
+ let db;
107
+ try {
108
+ const Database = await loadBetterSqlite3();
109
+ const opened = new Database(dbPath);
110
+ db = opened;
111
+ // Report busy immediately instead of retrying for the driver's default
112
+ // (multi-second) busy_timeout — a live holder's checkpoint-blocking
113
+ // lock is a steady-state fact at probe time, not a transient we should
114
+ // wait out, and this is a safety gate on a hot write path.
115
+ opened.pragma('busy_timeout = 0');
116
+ const [{ busy }] = opened.pragma('wal_checkpoint(TRUNCATE)');
117
+ if (busy !== 0)
118
+ return true;
119
+ }
120
+ catch {
121
+ return true;
122
+ }
123
+ finally {
124
+ try {
125
+ db?.close();
126
+ }
127
+ catch {
128
+ // best-effort
129
+ }
130
+ }
131
+ // Nothing was mid-transaction at probe time, and our own (now-closed)
132
+ // probe connection just truncated the WAL. If the sidecars are still
133
+ // here, another connection is still attached and kept them alive through
134
+ // our close.
81
135
  try {
82
136
  return fs.existsSync(`${dbPath}-wal`) || fs.existsSync(`${dbPath}-shm`);
83
137
  }
@@ -123,34 +177,7 @@ let _memoryRootCache;
123
177
  export function getMemoryRoot() {
124
178
  if (_memoryRootCache !== undefined)
125
179
  return _memoryRootCache;
126
- // 1. Env var
127
- const envPath = process.env.CLAUDE_FLOW_MEMORY_PATH;
128
- if (envPath && envPath.trim().length > 0) {
129
- _memoryRootCache = path.resolve(envPath);
130
- return _memoryRootCache;
131
- }
132
- // 2. Config file (claude-flow.config.json)
133
- const configCandidates = [
134
- path.resolve(process.cwd(), 'claude-flow.config.json'),
135
- path.resolve(process.cwd(), '.claude-flow', 'config.json'),
136
- ];
137
- for (const configPath of configCandidates) {
138
- if (!fs.existsSync(configPath))
139
- continue;
140
- try {
141
- const raw = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
142
- const fromConfig = raw?.memory?.persistPath ?? raw?.memory?.path;
143
- if (typeof fromConfig === 'string' && fromConfig.trim().length > 0) {
144
- _memoryRootCache = path.resolve(fromConfig);
145
- return _memoryRootCache;
146
- }
147
- }
148
- catch {
149
- /* malformed config — fall through to default */
150
- }
151
- }
152
- // 3. Default
153
- _memoryRootCache = path.resolve(process.cwd(), '.swarm');
180
+ _memoryRootCache = resolveMemoryRoot(process.cwd());
154
181
  return _memoryRootCache;
155
182
  }
156
183
  /** For tests + the `memory configure` flow that mutates the config at runtime. */
@@ -2532,6 +2559,9 @@ export async function verifyMemoryInit(dbPath, options) {
2532
2559
  * This bypasses MCP and writes directly to the database
2533
2560
  */
2534
2561
  export async function storeEntry(options) {
2562
+ if (options.appendOnly && !options.requireNative) {
2563
+ return { success: false, id: '', error: 'Immutable append requires a native writer' };
2564
+ }
2535
2565
  // ADR-323: validate before touching either backend so an invalid value
2536
2566
  // gets one clear error instead of a raw SQLite CHECK-constraint failure
2537
2567
  // from whichever path (bridge vs sql.js) happens to run.
@@ -2563,8 +2593,12 @@ export async function storeEntry(options) {
2563
2593
  return bridgeResult;
2564
2594
  }
2565
2595
  }
2596
+ if (options.requireNative || options.appendConditions) {
2597
+ return { success: false, id: '', error: 'Native memory writer required; refusing whole-image sql.js fallback' };
2598
+ }
2566
2599
  // Fallback: raw sql.js
2567
- const { key, value, namespace = 'default', generateEmbeddingFlag = true, tags = [], ttl, dbPath: customPath, upsert = false, provenanceType } = options;
2600
+ const { key, value, namespace = 'default', generateEmbeddingFlag = true, tags = [], ttl, dbPath: customPath, upsert: requestedUpsert = false, provenanceType } = options;
2601
+ const upsert = requestedUpsert && !options.appendOnly;
2568
2602
  const swarmDir = getMemoryRoot();
2569
2603
  const dbPath = customPath ? path.resolve(customPath) : path.join(swarmDir, 'memory.db');
2570
2604
  try {
@@ -2578,7 +2612,7 @@ export async function storeEntry(options) {
2578
2612
  // race). This check gates ensureSchemaColumns()'s own whole-image
2579
2613
  // write below too, not just this function's.
2580
2614
  await releaseOwnNativeHandle(dbPath);
2581
- if (hasNativeWalSidecars(dbPath)) {
2615
+ if (await hasNativeWalSidecars(dbPath)) {
2582
2616
  return {
2583
2617
  success: false,
2584
2618
  id: '',
@@ -2985,7 +3019,7 @@ export async function listEntries(options) {
2985
3019
  // Listing can migrate/backfill the schema, so it is also a whole-image
2986
3020
  // writer. The newly selected native mirror may still have a live WAL.
2987
3021
  await releaseOwnNativeHandle(dbPath);
2988
- if (hasNativeWalSidecars(dbPath)) {
3022
+ if (await hasNativeWalSidecars(dbPath)) {
2989
3023
  return { success: false, entries: [], total: 0, error: await walRefusalError('read/write') };
2990
3024
  }
2991
3025
  // Ensure schema has all required columns (migration for older DBs)
@@ -3097,7 +3131,7 @@ export async function getEntry(options) {
3097
3131
  // bump is itself a whole-image write, not a lightweight read, even
3098
3132
  // though this function's contract reads as a "get".
3099
3133
  await releaseOwnNativeHandle(dbPath);
3100
- if (hasNativeWalSidecars(dbPath)) {
3134
+ if (await hasNativeWalSidecars(dbPath)) {
3101
3135
  return {
3102
3136
  success: false,
3103
3137
  found: false,
@@ -3217,7 +3251,7 @@ export async function deleteEntry(options) {
3217
3251
  // #2735 — see storeEntry's identical gate for the corruption mechanism
3218
3252
  // this closes.
3219
3253
  await releaseOwnNativeHandle(dbPath);
3220
- if (hasNativeWalSidecars(dbPath)) {
3254
+ if (await hasNativeWalSidecars(dbPath)) {
3221
3255
  return {
3222
3256
  success: false,
3223
3257
  deleted: false,
@@ -3437,7 +3471,7 @@ export async function purgeNamespace(options) {
3437
3471
  }
3438
3472
  // Recheck at mutation time even when the CLI already read a preview.
3439
3473
  await releaseOwnNativeHandle(dbPath);
3440
- if (hasNativeWalSidecars(dbPath)) {
3474
+ if (await hasNativeWalSidecars(dbPath)) {
3441
3475
  return { success: false, deletedCount: 0, remainingEntries: 0, error: await walRefusalError('write') };
3442
3476
  }
3443
3477
  await ensureSchemaColumns(dbPath, options);
@@ -0,0 +1,2 @@
1
+ export declare function resolveMemoryRoot(cwd: string): string;
2
+ //# sourceMappingURL=memory-root.d.ts.map
@@ -0,0 +1,28 @@
1
+ /** Uncached memory-directory authority for an explicit project directory. */
2
+ import * as fs from 'fs';
3
+ import * as path from 'path';
4
+ export function resolveMemoryRoot(cwd) {
5
+ const envPath = process.env.CLAUDE_FLOW_MEMORY_PATH;
6
+ if (envPath && envPath.trim().length > 0)
7
+ return path.resolve(cwd, envPath);
8
+ const configCandidates = [
9
+ path.resolve(cwd, 'claude-flow.config.json'),
10
+ path.resolve(cwd, '.claude-flow', 'config.json'),
11
+ ];
12
+ for (const configPath of configCandidates) {
13
+ if (!fs.existsSync(configPath))
14
+ continue;
15
+ try {
16
+ const raw = JSON.parse(fs.readFileSync(configPath, 'utf-8'));
17
+ const fromConfig = raw?.memory?.persistPath ?? raw?.memory?.path;
18
+ if (typeof fromConfig === 'string' && fromConfig.trim().length > 0) {
19
+ return path.resolve(cwd, fromConfig);
20
+ }
21
+ }
22
+ catch {
23
+ /* malformed config — fall through to default */
24
+ }
25
+ }
26
+ return path.resolve(cwd, '.swarm');
27
+ }
28
+ //# sourceMappingURL=memory-root.js.map
@@ -93,6 +93,8 @@ export interface SONAStats {
93
93
  agents: number;
94
94
  } | 'unavailable';
95
95
  }
96
+ /** Agent roles supported by the SONA routing optimizer. */
97
+ export declare function isSupportedSonaAgent(agent: string): boolean;
96
98
  /**
97
99
  * SONA Optimizer for adaptive routing based on trajectory outcomes
98
100
  *
@@ -61,6 +61,10 @@ const AGENT_TYPES = [
61
61
  'security-architect',
62
62
  'performance-engineer',
63
63
  ];
64
+ /** Agent roles supported by the SONA routing optimizer. */
65
+ export function isSupportedSonaAgent(agent) {
66
+ return AGENT_TYPES.includes(agent);
67
+ }
64
68
  /**
65
69
  * Task keywords for pattern extraction
66
70
  */
@@ -568,6 +568,13 @@ export class CommandParser {
568
568
  if (opt.required && (flags[key] === undefined || flags[key] === '')) {
569
569
  errors.push(`Required option missing: --${opt.name}`);
570
570
  }
571
+ // A value-taking option given with nothing after it (`--task`, `--key` at the end, or
572
+ // followed by another flag) is parsed as boolean `true`. Commands then treat `true` as
573
+ // the value: `hooks route --task` crashed with "task.trim is not a function" and
574
+ // `memory retrieve --key` looked up the key "true". Report it instead.
575
+ if ((opt.type === 'string' || opt.type === 'number') && flags[key] === true) {
576
+ errors.push(`Option --${opt.name} needs a value`);
577
+ }
571
578
  // Check choices
572
579
  if (opt.choices && flags[key] !== undefined) {
573
580
  const value = String(flags[key]);