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