@massu/core 1.16.3 → 2.0.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.
@@ -243,6 +243,32 @@ var init_config_memory_schema = __esm({
243
243
  // turn of EVERY session, so an unbounded index is a permanent context tax — and the
244
244
  // per-session cap bounds only the RATE, never the total.
245
245
  indexMaxLines: z.number().int().min(1).default(50)
246
+ }).default({}),
247
+ // --- Cross-repo memory surfacing (plan-living-memory-slice-5-cross-repo-surfacing) ---
248
+ // A decision made in one of your repos can surface in another — opt-in per
249
+ // decision AND opt-in per repo, signed, verified, and materialized ONLY on
250
+ // explicit human acceptance. Local transport is FREE and zero-network.
251
+ //
252
+ // ⛔ BOTH switches default OFF, and OFF means NOTHING EXISTS — no registry, no
253
+ // keys, no inbox, no behavioural difference. Two INDEPENDENT opt-ins:
254
+ // • enabled — may this repo EXPORT its shareable decisions? (opt-in #1)
255
+ // • subscribe — which repo LABELS may this repo IMPORT from? (opt-in #2)
256
+ // `subscribe: []` means import NOTHING. There is deliberately NO `subscribe: all`
257
+ // — a repo you never named is a repo Massu never reads from.
258
+ share: z.object({
259
+ enabled: z.boolean().default(false),
260
+ subscribe: z.array(z.string()).default([]),
261
+ // C-04 — recall surfacing of cross-repo memories. `enabled` defaults true but is
262
+ // CONDITIONAL on `subscribe` being non-empty (empty by default), so the effective
263
+ // default is DORMANT: with `subscribe: []` the recall hook output is byte-identical
264
+ // to today's. `maxCrossRepoItems` caps how many accepted cross-repo items may appear
265
+ // per recall block (default 1); `minScore` is an OPTIONAL strictly-higher floor for
266
+ // cross-repo items (they are, by construction, less relevant than local ones).
267
+ recall: z.object({
268
+ enabled: z.boolean().default(true),
269
+ maxCrossRepoItems: z.number().int().min(0).default(1),
270
+ minScore: z.number().optional()
271
+ }).default({})
246
272
  }).default({})
247
273
  }).optional();
248
274
  }
@@ -496,7 +522,6 @@ var init_config = __esm({
496
522
  custom_patterns: z2.array(CustomPatternSchema).default([])
497
523
  }).optional(),
498
524
  adr: z2.object({
499
- detection_phrases: z2.array(z2.string()).default(["chose", "decided", "switching to", "moving from", "going with"]),
500
525
  template: z2.string().default("default"),
501
526
  storage: z2.string().default("database"),
502
527
  output_dir: z2.string().default("docs/adr")
@@ -1159,6 +1184,136 @@ var init_sqlite_loader = __esm({
1159
1184
  }
1160
1185
  });
1161
1186
 
1187
+ // src/db-driver.ts
1188
+ import { createRequire as createRequire2 } from "module";
1189
+ import { closeSync, existsSync as existsSync4, mkdtempSync, openSync, readFileSync as readFileSync4, rmSync as rmSync3, fsyncSync } from "fs";
1190
+ import { tmpdir } from "os";
1191
+ import { join as join2 } from "path";
1192
+ function resolveDbEngine() {
1193
+ return process.env[DB_ENGINE_ENV] === "better-sqlite3" ? "better-sqlite3" : DEFAULT_DB_ENGINE;
1194
+ }
1195
+ function nodeSqliteCtor() {
1196
+ if (!_nodeCtor) _nodeCtor = req2("node:sqlite").DatabaseSync;
1197
+ return _nodeCtor;
1198
+ }
1199
+ function plainRow(row) {
1200
+ if (row == null || typeof row !== "object") return row;
1201
+ return { ...row };
1202
+ }
1203
+ function openNodeSqlite(dbPath, opts) {
1204
+ const Ctor = nodeSqliteCtor();
1205
+ const raw = new Ctor(dbPath, {
1206
+ open: true,
1207
+ readOnly: !!opts.readonly,
1208
+ // FAITHFULNESS: better-sqlite3's ACTUAL default is foreign_keys=ON (empirically
1209
+ // verified — SQLite's own default is OFF, but bs3 enables it). massu was written +
1210
+ // tested against that default, so the adapter must match it, or a store that relies
1211
+ // on FK-ON-by-default (without an explicit pragma) would silently lose enforcement.
1212
+ // Explicit `foreign_keys=OFF/ON` pragmas (e.g. memory-db bulk ops) still override.
1213
+ enableForeignKeyConstraints: true,
1214
+ allowExtension: false
1215
+ });
1216
+ let savepointSeq = 0;
1217
+ const wrapStmt = (sql) => {
1218
+ const st = raw.prepare(sql);
1219
+ return {
1220
+ run: (...p) => {
1221
+ const r = st.run(...p);
1222
+ return {
1223
+ changes: typeof r.changes === "bigint" ? Number(r.changes) : r.changes,
1224
+ lastInsertRowid: r.lastInsertRowid
1225
+ };
1226
+ },
1227
+ get: (...p) => plainRow(st.get(...p)),
1228
+ all: (...p) => st.all(...p).map(plainRow)
1229
+ };
1230
+ };
1231
+ const pragma = (source, options) => {
1232
+ const s = String(source).trim();
1233
+ if (s.includes("=")) {
1234
+ raw.exec(`PRAGMA ${s}`);
1235
+ return void 0;
1236
+ }
1237
+ const rows = raw.prepare(`PRAGMA ${s}`).all().map(plainRow);
1238
+ if (options?.simple) {
1239
+ const first = rows[0];
1240
+ return first ? Object.values(first)[0] : void 0;
1241
+ }
1242
+ return rows;
1243
+ };
1244
+ const transaction = (fn) => {
1245
+ const run = (...args) => {
1246
+ const nested = raw.isTransaction;
1247
+ const name = `msp_${savepointSeq++}`;
1248
+ if (nested) raw.exec(`SAVEPOINT ${name}`);
1249
+ else raw.exec("BEGIN");
1250
+ try {
1251
+ const out = fn(...args);
1252
+ if (nested) raw.exec(`RELEASE ${name}`);
1253
+ else raw.exec("COMMIT");
1254
+ return out;
1255
+ } catch (e) {
1256
+ if (nested) {
1257
+ raw.exec(`ROLLBACK TO ${name}`);
1258
+ raw.exec(`RELEASE ${name}`);
1259
+ } else {
1260
+ raw.exec("ROLLBACK");
1261
+ }
1262
+ throw e;
1263
+ }
1264
+ };
1265
+ return run;
1266
+ };
1267
+ const serialize = () => {
1268
+ if (raw.isTransaction) {
1269
+ throw new Error(
1270
+ "db-driver: .serialize() (node:sqlite engine) cannot run inside an open transaction \u2014 VACUUM is forbidden in a transaction. Serialize before BEGIN, or use MASSU_DB_ENGINE=better-sqlite3 for mid-transaction snapshots."
1271
+ );
1272
+ }
1273
+ const dir = mkdtempSync(join2(tmpdir(), "massu-serialize-"));
1274
+ const out = join2(dir, "snapshot.db");
1275
+ try {
1276
+ raw.exec(`VACUUM INTO '${out.replace(/'/g, "''")}'`);
1277
+ const fd = openSync(out, "r");
1278
+ try {
1279
+ fsyncSync(fd);
1280
+ } finally {
1281
+ closeSync(fd);
1282
+ }
1283
+ return readFileSync4(out);
1284
+ } finally {
1285
+ rmSync3(dir, { recursive: true, force: true });
1286
+ }
1287
+ };
1288
+ const handle = {
1289
+ prepare: (sql) => wrapStmt(sql),
1290
+ exec: (sql) => raw.exec(sql),
1291
+ pragma,
1292
+ transaction,
1293
+ serialize,
1294
+ close: () => raw.close()
1295
+ };
1296
+ return handle;
1297
+ }
1298
+ function openBetterSqlite3(dbPath, opts) {
1299
+ return openDatabase(dbPath, opts);
1300
+ }
1301
+ function openDatabase2(dbPath, opts = {}) {
1302
+ return resolveDbEngine() === "better-sqlite3" ? openBetterSqlite3(dbPath, opts) : openNodeSqlite(dbPath, opts);
1303
+ }
1304
+ var req2, DEFAULT_DB_ENGINE, DB_ENGINE_ENV, _nodeCtor;
1305
+ var init_db_driver = __esm({
1306
+ "src/db-driver.ts"() {
1307
+ "use strict";
1308
+ init_sqlite_loader();
1309
+ init_sqlite_loader();
1310
+ req2 = createRequire2(import.meta.url);
1311
+ DEFAULT_DB_ENGINE = "node-sqlite";
1312
+ DB_ENGINE_ENV = "MASSU_DB_ENGINE";
1313
+ _nodeCtor = null;
1314
+ }
1315
+ });
1316
+
1162
1317
  // src/memory-vector.ts
1163
1318
  function float32ToBlob(vec) {
1164
1319
  return Buffer.from(new Uint8Array(vec.buffer, vec.byteOffset, vec.byteLength));
@@ -1179,26 +1334,26 @@ var init_memory_vector = __esm({
1179
1334
  });
1180
1335
 
1181
1336
  // src/db-backup.ts
1182
- import { existsSync as existsSync4, mkdirSync as mkdirSync4, readdirSync, statSync, unlinkSync, copyFileSync, rmSync as rmSync3 } from "fs";
1183
- import { resolve as resolve3, join as join2, basename, dirname as dirname4 } from "path";
1337
+ import { existsSync as existsSync5, mkdirSync as mkdirSync4, readdirSync, statSync, unlinkSync, copyFileSync, rmSync as rmSync4 } from "fs";
1338
+ import { resolve as resolve3, join as join3, basename, dirname as dirname4 } from "path";
1184
1339
  import { homedir as homedir3 } from "os";
1185
1340
  function dbBackupsRoot(home = homedir3()) {
1186
1341
  return resolve3(home, ".massu", "db-backups");
1187
1342
  }
1188
1343
  function projectBackupDir(projectRoot, home = homedir3()) {
1189
1344
  const slug = basename(projectRoot).replace(/[^A-Za-z0-9._-]/g, "_") || "project";
1190
- return join2(dbBackupsRoot(home), slug);
1345
+ return join3(dbBackupsRoot(home), slug);
1191
1346
  }
1192
1347
  function backupStamp(nowMs) {
1193
1348
  return new Date(nowMs).toISOString().replace(/[:.]/g, "-");
1194
1349
  }
1195
1350
  function listDbBackups(projectRoot, home = homedir3()) {
1196
1351
  const dir = projectBackupDir(projectRoot, home);
1197
- if (!existsSync4(dir)) return [];
1352
+ if (!existsSync5(dir)) return [];
1198
1353
  const out = [];
1199
1354
  for (const f of readdirSync(dir)) {
1200
1355
  if (!f.endsWith(".db")) continue;
1201
- const p = join2(dir, f);
1356
+ const p = join3(dir, f);
1202
1357
  const st = statSync(p);
1203
1358
  const db = f.replace(/-\d{4}-\d{2}-\d{2}T.*$/, "");
1204
1359
  out.push({ db, path: p, bytes: st.size, mtimeMs: st.mtimeMs });
@@ -1211,13 +1366,13 @@ function hasFreshDbBackup(projectRoot, dbPath, nowMs = Date.now(), home = homedi
1211
1366
  if (backups.length === 0) return false;
1212
1367
  const newest = backups[0];
1213
1368
  if (nowMs - newest.mtimeMs > FRESH_WINDOW_MS) return false;
1214
- if (existsSync4(dbPath) && statSync(dbPath).mtimeMs > newest.mtimeMs) return false;
1369
+ if (existsSync5(dbPath) && statSync(dbPath).mtimeMs > newest.mtimeMs) return false;
1215
1370
  return true;
1216
1371
  }
1217
1372
  function integrityOk(dbPath) {
1218
1373
  let db = null;
1219
1374
  try {
1220
- db = openDatabase(dbPath, { readonly: true });
1375
+ db = openDatabase2(dbPath, { readonly: true });
1221
1376
  const rows = db.pragma("integrity_check");
1222
1377
  return rows.length === 1 && rows[0].integrity_check === "ok";
1223
1378
  } catch {
@@ -1230,16 +1385,16 @@ function integrityOk(dbPath) {
1230
1385
  }
1231
1386
  }
1232
1387
  function backupDb(projectRoot, dbPath, nowMs = Date.now(), home = homedir3()) {
1233
- if (!existsSync4(dbPath)) {
1388
+ if (!existsSync5(dbPath)) {
1234
1389
  throw new DbBackupError(`cannot back up a database that does not exist: ${dbPath}`);
1235
1390
  }
1236
1391
  const dir = projectBackupDir(projectRoot, home);
1237
1392
  mkdirSync4(dir, { recursive: true });
1238
1393
  const name = basename(dbPath, ".db");
1239
- const dest = join2(dir, `${name}-${backupStamp(nowMs)}.db`);
1394
+ const dest = join3(dir, `${name}-${backupStamp(nowMs)}.db`);
1240
1395
  let src = null;
1241
1396
  try {
1242
- src = openDatabase(dbPath, { readonly: true });
1397
+ src = openDatabase2(dbPath, { readonly: true });
1243
1398
  src.exec(`VACUUM INTO '${dest.replace(/'/g, "''")}'`);
1244
1399
  } catch (err) {
1245
1400
  throw new DbBackupError(
@@ -1278,7 +1433,7 @@ function pruneDbBackups(projectRoot, dbName, keep = DEFAULT_RETENTION, home = ho
1278
1433
  }
1279
1434
  function backupBeforeSchemaChange(projectRoot, dbPath, onError, nowMs = Date.now(), home = homedir3()) {
1280
1435
  try {
1281
- if (!existsSync4(dbPath)) return null;
1436
+ if (!existsSync5(dbPath)) return null;
1282
1437
  if (hasFreshDbBackup(projectRoot, dbPath, nowMs, home)) return null;
1283
1438
  return backupDb(projectRoot, dbPath, nowMs, home);
1284
1439
  } catch (err) {
@@ -1290,7 +1445,7 @@ var DEFAULT_RETENTION, FRESH_WINDOW_MS, DbBackupError;
1290
1445
  var init_db_backup = __esm({
1291
1446
  "src/db-backup.ts"() {
1292
1447
  "use strict";
1293
- init_sqlite_loader();
1448
+ init_db_driver();
1294
1449
  DEFAULT_RETENTION = 5;
1295
1450
  FRESH_WINDOW_MS = 24 * 60 * 60 * 1e3;
1296
1451
  DbBackupError = class extends Error {
@@ -1403,9 +1558,9 @@ var init_rule_candidate_store = __esm({
1403
1558
  });
1404
1559
 
1405
1560
  // src/memory-embedder-tokenizer.ts
1406
- import { readFileSync as readFileSync4 } from "fs";
1561
+ import { readFileSync as readFileSync5 } from "fs";
1407
1562
  function loadVocab(vocabPath) {
1408
- const lines = readFileSync4(vocabPath, "utf-8").split("\n");
1563
+ const lines = readFileSync5(vocabPath, "utf-8").split("\n");
1409
1564
  const vocab = /* @__PURE__ */ new Map();
1410
1565
  for (let i = 0; i < lines.length; i++) {
1411
1566
  const tok = lines[i].replace(/\r$/, "");
@@ -1525,9 +1680,9 @@ var init_memory_embedder_tokenizer = __esm({
1525
1680
 
1526
1681
  // src/memory-embedder.ts
1527
1682
  import { fileURLToPath } from "url";
1528
- import { dirname as dirname5, join as join3 } from "path";
1529
- import { existsSync as existsSync5 } from "fs";
1530
- import { createRequire as createRequire2 } from "module";
1683
+ import { dirname as dirname5, join as join4 } from "path";
1684
+ import { existsSync as existsSync6 } from "fs";
1685
+ import { createRequire as createRequire3 } from "module";
1531
1686
  function getActiveEmbedModel() {
1532
1687
  return _activeModel;
1533
1688
  }
@@ -1551,7 +1706,7 @@ function resolveModelDir() {
1551
1706
  let dir = dirname5(fileURLToPath(import.meta.url));
1552
1707
  let root = null;
1553
1708
  for (let i = 0; i < 8; i++) {
1554
- if (existsSync5(join3(dir, "package.json"))) {
1709
+ if (existsSync6(join4(dir, "package.json"))) {
1555
1710
  root = dir;
1556
1711
  break;
1557
1712
  }
@@ -1561,8 +1716,8 @@ function resolveModelDir() {
1561
1716
  }
1562
1717
  if (!root) return null;
1563
1718
  for (const rel of ["dist/embedder", "assets/embedder"]) {
1564
- const candidate = join3(root, rel);
1565
- if (existsSync5(join3(candidate, "model_quantized.onnx"))) return candidate;
1719
+ const candidate = join4(root, rel);
1720
+ if (existsSync6(join4(candidate, "model_quantized.onnx"))) return candidate;
1566
1721
  }
1567
1722
  return null;
1568
1723
  } catch {
@@ -1571,8 +1726,8 @@ function resolveModelDir() {
1571
1726
  }
1572
1727
  function resolveWasmDir() {
1573
1728
  try {
1574
- const req2 = createRequire2(import.meta.url);
1575
- const main2 = req2.resolve(ORT_PKG);
1729
+ const req3 = createRequire3(import.meta.url);
1730
+ const main2 = req3.resolve(ORT_PKG);
1576
1731
  return dirname5(main2);
1577
1732
  } catch {
1578
1733
  return null;
@@ -1593,8 +1748,8 @@ async function getSession() {
1593
1748
  ort.env.wasm.proxy = false;
1594
1749
  ort.env.wasm.wasmPaths = wasmDir.endsWith("/") ? wasmDir : wasmDir + "/";
1595
1750
  _ortTensor = ort.Tensor;
1596
- const modelPath = join3(modelDir, "model_quantized.onnx");
1597
- const vocabPath = join3(modelDir, "vocab.txt");
1751
+ const modelPath = join4(modelDir, "model_quantized.onnx");
1752
+ const vocabPath = join4(modelDir, "vocab.txt");
1598
1753
  _vocab = loadVocab(vocabPath);
1599
1754
  const session = await ort.InferenceSession.create(modelPath, {
1600
1755
  executionProviders: ["wasm"],
@@ -1915,6 +2070,7 @@ __export(memory_db_exports, {
1915
2070
  migrateAuditLogCheckExtension: () => migrateAuditLogCheckExtension,
1916
2071
  migrateMemoryFilesFor4B: () => migrateMemoryFilesFor4B,
1917
2072
  migrateObservationEmbeddingChunks: () => migrateObservationEmbeddingChunks,
2073
+ migrateSharedMemoryFor5B: () => migrateSharedMemoryFor5B,
1918
2074
  pruneOldConversationTurns: () => pruneOldConversationTurns,
1919
2075
  pruneOldObservations: () => pruneOldObservations,
1920
2076
  pruneToolCostEvents: () => pruneToolCostEvents,
@@ -1932,7 +2088,7 @@ __export(memory_db_exports, {
1932
2088
  usageWarmupElapsed: () => usageWarmupElapsed
1933
2089
  });
1934
2090
  import { dirname as dirname6, basename as basename2 } from "path";
1935
- import { existsSync as existsSync6, mkdirSync as mkdirSync5 } from "fs";
2091
+ import { existsSync as existsSync7, mkdirSync as mkdirSync5 } from "fs";
1936
2092
  function sanitizeFts5Query(raw) {
1937
2093
  const trimmed = raw.trim();
1938
2094
  if (!trimmed) return '""';
@@ -1949,11 +2105,11 @@ function sanitizeFts5QueryOr(raw) {
1949
2105
  function getMemoryDb() {
1950
2106
  const dbPath = getResolvedPaths().memoryDbPath;
1951
2107
  const dir = dirname6(dbPath);
1952
- if (!existsSync6(dir)) {
2108
+ if (!existsSync7(dir)) {
1953
2109
  mkdirSync5(dir, { recursive: true });
1954
2110
  }
1955
- const preExisting = existsSync6(dbPath);
1956
- const db = openDatabase(dbPath);
2111
+ const preExisting = existsSync7(dbPath);
2112
+ const db = openDatabase2(dbPath);
1957
2113
  db.pragma("journal_mode = WAL");
1958
2114
  db.pragma("foreign_keys = ON");
1959
2115
  const onDisk = db.pragma("user_version", { simple: true });
@@ -2029,6 +2185,39 @@ function migrateMemoryFilesFor4B(db) {
2029
2185
  db.exec(`ALTER TABLE ${add.table} ADD COLUMN ${add.name} ${add.decl}`);
2030
2186
  }
2031
2187
  }
2188
+ function migrateSharedMemoryFor5B(db) {
2189
+ const cols = db.prepare(`PRAGMA table_info(observations)`).all();
2190
+ if (cols.length > 0 && !cols.some((c) => c.name === "shareable")) {
2191
+ db.exec(`ALTER TABLE observations ADD COLUMN shareable INTEGER NOT NULL DEFAULT 0`);
2192
+ }
2193
+ db.exec(`
2194
+ CREATE TABLE IF NOT EXISTS shared_memory_pending (
2195
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
2196
+ record_hash TEXT NOT NULL UNIQUE, -- identity + idempotency key (hex sha256)
2197
+ origin_repo_id TEXT NOT NULL, -- the signed origin repo_id (v4 UUID)
2198
+ origin_repo_label TEXT NOT NULL, -- re-slugged on read; NEVER trusted from the wire
2199
+ envelope_raw TEXT NOT NULL, -- the VERBATIM signed bytes; B-05 re-verifies these
2200
+ record_json TEXT NOT NULL, -- the single record's canonical JSON
2201
+ received_at_epoch INTEGER NOT NULL, -- epoch SECONDS (Slice-2 convention)
2202
+ accepted_at_epoch INTEGER, -- set on accept (B-05); NULL while pending
2203
+ refused_at_epoch INTEGER, -- set on refuse (B-06)
2204
+ expired_at_epoch INTEGER -- revocation \u21D2 EXPIRE, never DELETE (B-07 / S-3)
2205
+ );
2206
+ CREATE INDEX IF NOT EXISTS idx_smp_origin ON shared_memory_pending(origin_repo_id);
2207
+ CREATE INDEX IF NOT EXISTS idx_smp_received ON shared_memory_pending(received_at_epoch DESC);
2208
+ `);
2209
+ db.exec(`
2210
+ CREATE TABLE IF NOT EXISTS shared_memory_outbound (
2211
+ record_hash TEXT PRIMARY KEY, -- the exported record's canonical hash
2212
+ observation_id INTEGER NOT NULL, -- the source observations.id (for B-07 revocation)
2213
+ origin_repo_id TEXT NOT NULL, -- this repo's own repo_id at export time
2214
+ seq INTEGER NOT NULL, -- the envelope seq this record first went out in
2215
+ exported_at_epoch INTEGER NOT NULL, -- epoch SECONDS
2216
+ revoked_at_epoch INTEGER -- set when the source row is superseded/expired (B-07)
2217
+ );
2218
+ CREATE INDEX IF NOT EXISTS idx_smo_obs ON shared_memory_outbound(observation_id);
2219
+ `);
2220
+ }
2032
2221
  function migrateAuditLogCheckExtension(db) {
2033
2222
  const row = db.prepare("SELECT sql FROM sqlite_master WHERE type='table' AND name='audit_log'").get();
2034
2223
  if (!row) return;
@@ -2051,7 +2240,16 @@ function migrateAuditLogCheckExtension(db) {
2051
2240
  "memory_file_adopted_human",
2052
2241
  "memory_file_rendered",
2053
2242
  "memory_file_render_refused",
2054
- "memory_file_tombstoned"
2243
+ "memory_file_tombstoned",
2244
+ // B-10 (Slice 5) — the seven cross-repo shared-memory events. An existing DB whose
2245
+ // CHECK lacks any of these triggers the table rebuild below.
2246
+ "shared_memory_exported",
2247
+ "shared_memory_export_refused",
2248
+ "shared_memory_imported",
2249
+ "shared_memory_dropped",
2250
+ "shared_memory_accepted",
2251
+ "shared_memory_refused",
2252
+ "shared_memory_revoked"
2055
2253
  ];
2056
2254
  const checkClauseMatch = row.sql.match(/event_type\s+TEXT\s+NOT\s+NULL\s+CHECK\s*\(\s*event_type\s+IN\s*\(([\s\S]*?)\)\s*\)/i);
2057
2255
  if (checkClauseMatch) {
@@ -2070,7 +2268,12 @@ function migrateAuditLogCheckExtension(db) {
2070
2268
  'code_change', 'rule_enforced', 'approval', 'review', 'commit', 'compaction',
2071
2269
  'rule_candidate_emitted', 'rule_promoted', 'rule_dismissed',
2072
2270
  'memory_file_ingested', 'memory_file_expired', 'memory_file_adopted_human',
2073
- 'memory_file_rendered', 'memory_file_render_refused', 'memory_file_tombstoned'
2271
+ 'memory_file_rendered', 'memory_file_render_refused', 'memory_file_tombstoned',
2272
+ -- B-10 (Slice 5) \u2014 cross-repo shared-memory events (must mirror the CREATE +
2273
+ -- the expected[] list above, or the migration would loop rebuilding forever).
2274
+ 'shared_memory_exported', 'shared_memory_export_refused', 'shared_memory_imported',
2275
+ 'shared_memory_dropped', 'shared_memory_accepted', 'shared_memory_refused',
2276
+ 'shared_memory_revoked'
2074
2277
  )),
2075
2278
  actor TEXT NOT NULL DEFAULT 'ai' CHECK(actor IN ('ai', 'human', 'hook', 'agent')),
2076
2279
  model_id TEXT,
@@ -2147,6 +2350,12 @@ function initMemorySchema(db) {
2147
2350
  -- credential, or takes a snapshot. A Slice-5 synced memory arrives here with
2148
2351
  -- origin='team' and must never reach disk without CR-55's gates.
2149
2352
  origin TEXT NOT NULL DEFAULT 'local',
2353
+ -- B-01 (Slice 5, cross-repo surfacing): the per-decision SHARE opt-in. Set to 1
2354
+ -- ONLY by an explicit human act (massu memory share <id>); NEVER by a heuristic,
2355
+ -- score, LLM, hook, or consolidation pass. Export (B-02) reads WHERE shareable=1
2356
+ -- AND origin='local'. Defaults 0 so every pre-existing and machine-written row is
2357
+ -- un-shareable until a human says otherwise (fail-closed).
2358
+ shareable INTEGER NOT NULL DEFAULT 0,
2150
2359
  created_at TEXT NOT NULL,
2151
2360
  created_at_epoch INTEGER NOT NULL,
2152
2361
  FOREIGN KEY(session_id) REFERENCES sessions(session_id) ON DELETE CASCADE
@@ -2420,7 +2629,13 @@ function initMemorySchema(db) {
2420
2629
  'code_change', 'rule_enforced', 'approval', 'review', 'commit', 'compaction',
2421
2630
  'rule_candidate_emitted', 'rule_promoted', 'rule_dismissed',
2422
2631
  'memory_file_ingested', 'memory_file_expired', 'memory_file_adopted_human',
2423
- 'memory_file_rendered', 'memory_file_render_refused', 'memory_file_tombstoned'
2632
+ 'memory_file_rendered', 'memory_file_render_refused', 'memory_file_tombstoned',
2633
+ -- B-10 (Slice 5): cross-repo shared-memory observability. Without these seven,
2634
+ -- nobody could ever answer "what has crossed between my repos?" (and an ingest
2635
+ -- catch would swallow the CHECK violation, silently producing NOTHING).
2636
+ 'shared_memory_exported', 'shared_memory_export_refused', 'shared_memory_imported',
2637
+ 'shared_memory_dropped', 'shared_memory_accepted', 'shared_memory_refused',
2638
+ 'shared_memory_revoked'
2424
2639
  )),
2425
2640
  actor TEXT NOT NULL DEFAULT 'ai' CHECK(actor IN ('ai', 'human', 'hook', 'agent')),
2426
2641
  model_id TEXT,
@@ -2789,6 +3004,7 @@ function initMemorySchema(db) {
2789
3004
  `);
2790
3005
  migrateObservationEmbeddingChunks(db);
2791
3006
  migrateMemoryFilesFor4B(db);
3007
+ migrateSharedMemoryFor5B(db);
2792
3008
  ensureRuleCandidatesTable(db);
2793
3009
  migrateRuleDelivery(db);
2794
3010
  }
@@ -3659,7 +3875,7 @@ var MEMORY_SCHEMA_VERSION, TOOL_COST_EVENTS_RETENTION_DAYS, MAX_DRAFT_TEXT_LEN,
3659
3875
  var init_memory_db = __esm({
3660
3876
  "src/memory-db.ts"() {
3661
3877
  "use strict";
3662
- init_sqlite_loader();
3878
+ init_db_driver();
3663
3879
  init_config();
3664
3880
  init_memory_vector();
3665
3881
  init_db_backup();
@@ -3680,21 +3896,21 @@ var init_memory_db = __esm({
3680
3896
  });
3681
3897
 
3682
3898
  // src/hooks/lib/hook-failure-signal.ts
3683
- import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync6, existsSync as existsSync7 } from "fs";
3684
- import { join as join4, dirname as dirname7 } from "path";
3899
+ import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync6, existsSync as existsSync8 } from "fs";
3900
+ import { join as join5, dirname as dirname7 } from "path";
3685
3901
  function resolveFailureLogPath() {
3686
3902
  const explicit = process.env.MASSU_HOOK_FAILURE_LOG;
3687
3903
  if (explicit) return explicit;
3688
3904
  let dir = process.cwd();
3689
3905
  for (let i = 0; i < 12; i++) {
3690
- if (existsSync7(join4(dir, ".massu")) || existsSync7(join4(dir, "massu.config.yaml"))) {
3691
- return join4(dir, ".massu", "hook-failures.jsonl");
3906
+ if (existsSync8(join5(dir, ".massu")) || existsSync8(join5(dir, "massu.config.yaml"))) {
3907
+ return join5(dir, ".massu", "hook-failures.jsonl");
3692
3908
  }
3693
3909
  const parent = dirname7(dir);
3694
3910
  if (parent === dir) break;
3695
3911
  dir = parent;
3696
3912
  }
3697
- return join4(process.cwd(), ".massu", "hook-failures.jsonl");
3913
+ return join5(process.cwd(), ".massu", "hook-failures.jsonl");
3698
3914
  }
3699
3915
  function recordHookFailure(hook, error, context) {
3700
3916
  const record = {
@@ -3753,9 +3969,9 @@ var init_hook_failure_signal = __esm({
3753
3969
  // src/hooks/fix-detector.ts
3754
3970
  init_config();
3755
3971
  import { execFileSync } from "child_process";
3756
- import { existsSync as existsSync8, appendFileSync as appendFileSync3, mkdirSync as mkdirSync7, readFileSync as readFileSync5 } from "fs";
3757
- import { tmpdir } from "os";
3758
- import { join as join5 } from "path";
3972
+ import { existsSync as existsSync9, appendFileSync as appendFileSync3, mkdirSync as mkdirSync7, readFileSync as readFileSync6 } from "fs";
3973
+ import { tmpdir as tmpdir2 } from "os";
3974
+ import { join as join6 } from "path";
3759
3975
 
3760
3976
  // src/hooks/lib/write-hook-message.ts
3761
3977
  function writeHookMessage(message) {
@@ -3806,15 +4022,15 @@ var FIX_HEURISTICS = [
3806
4022
  }
3807
4023
  ];
3808
4024
  function getSessionFlagPath(sessionId) {
3809
- const dir = join5(tmpdir(), "massu-auto-learning");
3810
- if (!existsSync8(dir)) {
4025
+ const dir = join6(tmpdir2(), "massu-auto-learning");
4026
+ if (!existsSync9(dir)) {
3811
4027
  mkdirSync7(dir, { recursive: true });
3812
4028
  }
3813
- return join5(dir, `fixes-${sessionId.slice(0, 12)}.jsonl`);
4029
+ return join6(dir, `fixes-${sessionId.slice(0, 12)}.jsonl`);
3814
4030
  }
3815
4031
  function _stateDir() {
3816
- const dir = join5(tmpdir(), "massu-fix-detector-state");
3817
- if (!existsSync8(dir)) {
4032
+ const dir = join6(tmpdir2(), "massu-fix-detector-state");
4033
+ if (!existsSync9(dir)) {
3818
4034
  mkdirSync7(dir, { recursive: true });
3819
4035
  }
3820
4036
  return dir;
@@ -3828,9 +4044,9 @@ function _cwdHash(cwd) {
3828
4044
  }
3829
4045
  function isGitWorkTreeFast(cwd) {
3830
4046
  try {
3831
- const cachePath = join5(_stateDir(), `worktree-${_cwdHash(cwd)}.json`);
3832
- if (existsSync8(cachePath)) {
3833
- const cached = JSON.parse(readFileSync5(cachePath, "utf-8"));
4047
+ const cachePath = join6(_stateDir(), `worktree-${_cwdHash(cwd)}.json`);
4048
+ if (existsSync9(cachePath)) {
4049
+ const cached = JSON.parse(readFileSync6(cachePath, "utf-8"));
3834
4050
  const ageMs = Date.now() - cached.ts;
3835
4051
  if (ageMs < 36e5) return cached.isWorkTree;
3836
4052
  }
@@ -3853,11 +4069,11 @@ function isGitWorkTreeFast(cwd) {
3853
4069
  }
3854
4070
  }
3855
4071
  function _disabledFlagPath(sessionId) {
3856
- return join5(_stateDir(), `disabled-${sessionId.slice(0, 12)}.flag`);
4072
+ return join6(_stateDir(), `disabled-${sessionId.slice(0, 12)}.flag`);
3857
4073
  }
3858
4074
  function isSessionAutoDisabled(sessionId) {
3859
4075
  try {
3860
- return existsSync8(_disabledFlagPath(sessionId));
4076
+ return existsSync9(_disabledFlagPath(sessionId));
3861
4077
  } catch {
3862
4078
  return false;
3863
4079
  }
@@ -3875,7 +4091,7 @@ async function main() {
3875
4091
  const input = await readStdin();
3876
4092
  const hookInput = JSON.parse(input);
3877
4093
  const filePath = hookInput.tool_input?.file_path;
3878
- if (!filePath || !existsSync8(filePath)) {
4094
+ if (!filePath || !existsSync9(filePath)) {
3879
4095
  process.exit(0);
3880
4096
  return;
3881
4097
  }
@@ -3936,7 +4152,7 @@ async function main() {
3936
4152
  };
3937
4153
  const flagPath = getSessionFlagPath(hookInput.session_id);
3938
4154
  appendFileSync3(flagPath, JSON.stringify(signal) + "\n");
3939
- const lines = readFileSync5(flagPath, "utf-8").split("\n").filter(Boolean);
4155
+ const lines = readFileSync6(flagPath, "utf-8").split("\n").filter(Boolean);
3940
4156
  if (lines.length === 1) {
3941
4157
  writeHookMessage(
3942
4158
  `[Massu Auto-Learning] Bug fix detected in ${filePath} (signals: ${detected.join(", ")}). The auto-learning pipeline will prompt you at session end to create an incident report, derive a prevention rule, and add enforcement.`