@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.
@@ -237,6 +237,32 @@ var init_config_memory_schema = __esm({
237
237
  // turn of EVERY session, so an unbounded index is a permanent context tax — and the
238
238
  // per-session cap bounds only the RATE, never the total.
239
239
  indexMaxLines: z.number().int().min(1).default(50)
240
+ }).default({}),
241
+ // --- Cross-repo memory surfacing (plan-living-memory-slice-5-cross-repo-surfacing) ---
242
+ // A decision made in one of your repos can surface in another — opt-in per
243
+ // decision AND opt-in per repo, signed, verified, and materialized ONLY on
244
+ // explicit human acceptance. Local transport is FREE and zero-network.
245
+ //
246
+ // ⛔ BOTH switches default OFF, and OFF means NOTHING EXISTS — no registry, no
247
+ // keys, no inbox, no behavioural difference. Two INDEPENDENT opt-ins:
248
+ // • enabled — may this repo EXPORT its shareable decisions? (opt-in #1)
249
+ // • subscribe — which repo LABELS may this repo IMPORT from? (opt-in #2)
250
+ // `subscribe: []` means import NOTHING. There is deliberately NO `subscribe: all`
251
+ // — a repo you never named is a repo Massu never reads from.
252
+ share: z.object({
253
+ enabled: z.boolean().default(false),
254
+ subscribe: z.array(z.string()).default([]),
255
+ // C-04 — recall surfacing of cross-repo memories. `enabled` defaults true but is
256
+ // CONDITIONAL on `subscribe` being non-empty (empty by default), so the effective
257
+ // default is DORMANT: with `subscribe: []` the recall hook output is byte-identical
258
+ // to today's. `maxCrossRepoItems` caps how many accepted cross-repo items may appear
259
+ // per recall block (default 1); `minScore` is an OPTIONAL strictly-higher floor for
260
+ // cross-repo items (they are, by construction, less relevant than local ones).
261
+ recall: z.object({
262
+ enabled: z.boolean().default(true),
263
+ maxCrossRepoItems: z.number().int().min(0).default(1),
264
+ minScore: z.number().optional()
265
+ }).default({})
240
266
  }).default({})
241
267
  }).optional();
242
268
  }
@@ -413,6 +439,7 @@ function getResolvedPaths() {
413
439
  sessionStatePath: resolve2(root, config.conventions?.sessionStatePath ?? `${claudeDirName}/session-state/CURRENT.md`),
414
440
  sessionArchivePath: resolve2(root, config.conventions?.sessionArchivePath ?? `${claudeDirName}/session-state/archive`),
415
441
  mcpJsonPath: resolve2(root, ".mcp.json"),
442
+ settingsPath: resolve2(root, claudeDirName, "settings.json"),
416
443
  settingsLocalPath: resolve2(root, claudeDirName, "settings.local.json")
417
444
  };
418
445
  }
@@ -490,7 +517,6 @@ var init_config = __esm({
490
517
  custom_patterns: z2.array(CustomPatternSchema).default([])
491
518
  }).optional(),
492
519
  adr: z2.object({
493
- detection_phrases: z2.array(z2.string()).default(["chose", "decided", "switching to", "moving from", "going with"]),
494
520
  template: z2.string().default("default"),
495
521
  storage: z2.string().default("database"),
496
522
  output_dir: z2.string().default("docs/adr")
@@ -1153,6 +1179,136 @@ var init_sqlite_loader = __esm({
1153
1179
  }
1154
1180
  });
1155
1181
 
1182
+ // src/db-driver.ts
1183
+ import { createRequire as createRequire2 } from "module";
1184
+ import { closeSync, existsSync as existsSync4, mkdtempSync, openSync, readFileSync as readFileSync4, rmSync as rmSync3, fsyncSync } from "fs";
1185
+ import { tmpdir } from "os";
1186
+ import { join as join2 } from "path";
1187
+ function resolveDbEngine() {
1188
+ return process.env[DB_ENGINE_ENV] === "better-sqlite3" ? "better-sqlite3" : DEFAULT_DB_ENGINE;
1189
+ }
1190
+ function nodeSqliteCtor() {
1191
+ if (!_nodeCtor) _nodeCtor = req2("node:sqlite").DatabaseSync;
1192
+ return _nodeCtor;
1193
+ }
1194
+ function plainRow(row) {
1195
+ if (row == null || typeof row !== "object") return row;
1196
+ return { ...row };
1197
+ }
1198
+ function openNodeSqlite(dbPath, opts) {
1199
+ const Ctor = nodeSqliteCtor();
1200
+ const raw = new Ctor(dbPath, {
1201
+ open: true,
1202
+ readOnly: !!opts.readonly,
1203
+ // FAITHFULNESS: better-sqlite3's ACTUAL default is foreign_keys=ON (empirically
1204
+ // verified — SQLite's own default is OFF, but bs3 enables it). massu was written +
1205
+ // tested against that default, so the adapter must match it, or a store that relies
1206
+ // on FK-ON-by-default (without an explicit pragma) would silently lose enforcement.
1207
+ // Explicit `foreign_keys=OFF/ON` pragmas (e.g. memory-db bulk ops) still override.
1208
+ enableForeignKeyConstraints: true,
1209
+ allowExtension: false
1210
+ });
1211
+ let savepointSeq = 0;
1212
+ const wrapStmt = (sql) => {
1213
+ const st = raw.prepare(sql);
1214
+ return {
1215
+ run: (...p) => {
1216
+ const r = st.run(...p);
1217
+ return {
1218
+ changes: typeof r.changes === "bigint" ? Number(r.changes) : r.changes,
1219
+ lastInsertRowid: r.lastInsertRowid
1220
+ };
1221
+ },
1222
+ get: (...p) => plainRow(st.get(...p)),
1223
+ all: (...p) => st.all(...p).map(plainRow)
1224
+ };
1225
+ };
1226
+ const pragma = (source, options) => {
1227
+ const s = String(source).trim();
1228
+ if (s.includes("=")) {
1229
+ raw.exec(`PRAGMA ${s}`);
1230
+ return void 0;
1231
+ }
1232
+ const rows = raw.prepare(`PRAGMA ${s}`).all().map(plainRow);
1233
+ if (options?.simple) {
1234
+ const first = rows[0];
1235
+ return first ? Object.values(first)[0] : void 0;
1236
+ }
1237
+ return rows;
1238
+ };
1239
+ const transaction = (fn) => {
1240
+ const run = (...args) => {
1241
+ const nested = raw.isTransaction;
1242
+ const name = `msp_${savepointSeq++}`;
1243
+ if (nested) raw.exec(`SAVEPOINT ${name}`);
1244
+ else raw.exec("BEGIN");
1245
+ try {
1246
+ const out = fn(...args);
1247
+ if (nested) raw.exec(`RELEASE ${name}`);
1248
+ else raw.exec("COMMIT");
1249
+ return out;
1250
+ } catch (e) {
1251
+ if (nested) {
1252
+ raw.exec(`ROLLBACK TO ${name}`);
1253
+ raw.exec(`RELEASE ${name}`);
1254
+ } else {
1255
+ raw.exec("ROLLBACK");
1256
+ }
1257
+ throw e;
1258
+ }
1259
+ };
1260
+ return run;
1261
+ };
1262
+ const serialize = () => {
1263
+ if (raw.isTransaction) {
1264
+ throw new Error(
1265
+ "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."
1266
+ );
1267
+ }
1268
+ const dir = mkdtempSync(join2(tmpdir(), "massu-serialize-"));
1269
+ const out = join2(dir, "snapshot.db");
1270
+ try {
1271
+ raw.exec(`VACUUM INTO '${out.replace(/'/g, "''")}'`);
1272
+ const fd = openSync(out, "r");
1273
+ try {
1274
+ fsyncSync(fd);
1275
+ } finally {
1276
+ closeSync(fd);
1277
+ }
1278
+ return readFileSync4(out);
1279
+ } finally {
1280
+ rmSync3(dir, { recursive: true, force: true });
1281
+ }
1282
+ };
1283
+ const handle = {
1284
+ prepare: (sql) => wrapStmt(sql),
1285
+ exec: (sql) => raw.exec(sql),
1286
+ pragma,
1287
+ transaction,
1288
+ serialize,
1289
+ close: () => raw.close()
1290
+ };
1291
+ return handle;
1292
+ }
1293
+ function openBetterSqlite3(dbPath, opts) {
1294
+ return openDatabase(dbPath, opts);
1295
+ }
1296
+ function openDatabase2(dbPath, opts = {}) {
1297
+ return resolveDbEngine() === "better-sqlite3" ? openBetterSqlite3(dbPath, opts) : openNodeSqlite(dbPath, opts);
1298
+ }
1299
+ var req2, DEFAULT_DB_ENGINE, DB_ENGINE_ENV, _nodeCtor;
1300
+ var init_db_driver = __esm({
1301
+ "src/db-driver.ts"() {
1302
+ "use strict";
1303
+ init_sqlite_loader();
1304
+ init_sqlite_loader();
1305
+ req2 = createRequire2(import.meta.url);
1306
+ DEFAULT_DB_ENGINE = "node-sqlite";
1307
+ DB_ENGINE_ENV = "MASSU_DB_ENGINE";
1308
+ _nodeCtor = null;
1309
+ }
1310
+ });
1311
+
1156
1312
  // src/memory-vector.ts
1157
1313
  function float32ToBlob(vec) {
1158
1314
  return Buffer.from(new Uint8Array(vec.buffer, vec.byteOffset, vec.byteLength));
@@ -1173,26 +1329,26 @@ var init_memory_vector = __esm({
1173
1329
  });
1174
1330
 
1175
1331
  // src/db-backup.ts
1176
- import { existsSync as existsSync4, mkdirSync as mkdirSync4, readdirSync, statSync, unlinkSync, copyFileSync, rmSync as rmSync3 } from "fs";
1177
- import { resolve as resolve3, join as join2, basename, dirname as dirname4 } from "path";
1332
+ import { existsSync as existsSync5, mkdirSync as mkdirSync4, readdirSync, statSync, unlinkSync, copyFileSync, rmSync as rmSync4 } from "fs";
1333
+ import { resolve as resolve3, join as join3, basename, dirname as dirname4 } from "path";
1178
1334
  import { homedir as homedir3 } from "os";
1179
1335
  function dbBackupsRoot(home = homedir3()) {
1180
1336
  return resolve3(home, ".massu", "db-backups");
1181
1337
  }
1182
1338
  function projectBackupDir(projectRoot, home = homedir3()) {
1183
1339
  const slug = basename(projectRoot).replace(/[^A-Za-z0-9._-]/g, "_") || "project";
1184
- return join2(dbBackupsRoot(home), slug);
1340
+ return join3(dbBackupsRoot(home), slug);
1185
1341
  }
1186
1342
  function backupStamp(nowMs) {
1187
1343
  return new Date(nowMs).toISOString().replace(/[:.]/g, "-");
1188
1344
  }
1189
1345
  function listDbBackups(projectRoot, home = homedir3()) {
1190
1346
  const dir = projectBackupDir(projectRoot, home);
1191
- if (!existsSync4(dir)) return [];
1347
+ if (!existsSync5(dir)) return [];
1192
1348
  const out = [];
1193
1349
  for (const f of readdirSync(dir)) {
1194
1350
  if (!f.endsWith(".db")) continue;
1195
- const p = join2(dir, f);
1351
+ const p = join3(dir, f);
1196
1352
  const st = statSync(p);
1197
1353
  const db = f.replace(/-\d{4}-\d{2}-\d{2}T.*$/, "");
1198
1354
  out.push({ db, path: p, bytes: st.size, mtimeMs: st.mtimeMs });
@@ -1205,13 +1361,13 @@ function hasFreshDbBackup(projectRoot, dbPath, nowMs = Date.now(), home = homedi
1205
1361
  if (backups.length === 0) return false;
1206
1362
  const newest = backups[0];
1207
1363
  if (nowMs - newest.mtimeMs > FRESH_WINDOW_MS) return false;
1208
- if (existsSync4(dbPath) && statSync(dbPath).mtimeMs > newest.mtimeMs) return false;
1364
+ if (existsSync5(dbPath) && statSync(dbPath).mtimeMs > newest.mtimeMs) return false;
1209
1365
  return true;
1210
1366
  }
1211
1367
  function integrityOk(dbPath) {
1212
1368
  let db = null;
1213
1369
  try {
1214
- db = openDatabase(dbPath, { readonly: true });
1370
+ db = openDatabase2(dbPath, { readonly: true });
1215
1371
  const rows = db.pragma("integrity_check");
1216
1372
  return rows.length === 1 && rows[0].integrity_check === "ok";
1217
1373
  } catch {
@@ -1224,16 +1380,16 @@ function integrityOk(dbPath) {
1224
1380
  }
1225
1381
  }
1226
1382
  function backupDb(projectRoot, dbPath, nowMs = Date.now(), home = homedir3()) {
1227
- if (!existsSync4(dbPath)) {
1383
+ if (!existsSync5(dbPath)) {
1228
1384
  throw new DbBackupError(`cannot back up a database that does not exist: ${dbPath}`);
1229
1385
  }
1230
1386
  const dir = projectBackupDir(projectRoot, home);
1231
1387
  mkdirSync4(dir, { recursive: true });
1232
1388
  const name = basename(dbPath, ".db");
1233
- const dest = join2(dir, `${name}-${backupStamp(nowMs)}.db`);
1389
+ const dest = join3(dir, `${name}-${backupStamp(nowMs)}.db`);
1234
1390
  let src = null;
1235
1391
  try {
1236
- src = openDatabase(dbPath, { readonly: true });
1392
+ src = openDatabase2(dbPath, { readonly: true });
1237
1393
  src.exec(`VACUUM INTO '${dest.replace(/'/g, "''")}'`);
1238
1394
  } catch (err) {
1239
1395
  throw new DbBackupError(
@@ -1272,7 +1428,7 @@ function pruneDbBackups(projectRoot, dbName, keep = DEFAULT_RETENTION, home = ho
1272
1428
  }
1273
1429
  function backupBeforeSchemaChange(projectRoot, dbPath, onError, nowMs = Date.now(), home = homedir3()) {
1274
1430
  try {
1275
- if (!existsSync4(dbPath)) return null;
1431
+ if (!existsSync5(dbPath)) return null;
1276
1432
  if (hasFreshDbBackup(projectRoot, dbPath, nowMs, home)) return null;
1277
1433
  return backupDb(projectRoot, dbPath, nowMs, home);
1278
1434
  } catch (err) {
@@ -1284,7 +1440,7 @@ var DEFAULT_RETENTION, FRESH_WINDOW_MS, DbBackupError;
1284
1440
  var init_db_backup = __esm({
1285
1441
  "src/db-backup.ts"() {
1286
1442
  "use strict";
1287
- init_sqlite_loader();
1443
+ init_db_driver();
1288
1444
  DEFAULT_RETENTION = 5;
1289
1445
  FRESH_WINDOW_MS = 24 * 60 * 60 * 1e3;
1290
1446
  DbBackupError = class extends Error {
@@ -1397,21 +1553,21 @@ var init_rule_candidate_store = __esm({
1397
1553
  });
1398
1554
 
1399
1555
  // src/hooks/lib/hook-failure-signal.ts
1400
- import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync5, existsSync as existsSync5 } from "fs";
1401
- import { join as join3, dirname as dirname5 } from "path";
1556
+ import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync5, existsSync as existsSync6 } from "fs";
1557
+ import { join as join4, dirname as dirname5 } from "path";
1402
1558
  function resolveFailureLogPath() {
1403
1559
  const explicit = process.env.MASSU_HOOK_FAILURE_LOG;
1404
1560
  if (explicit) return explicit;
1405
1561
  let dir = process.cwd();
1406
1562
  for (let i = 0; i < 12; i++) {
1407
- if (existsSync5(join3(dir, ".massu")) || existsSync5(join3(dir, "massu.config.yaml"))) {
1408
- return join3(dir, ".massu", "hook-failures.jsonl");
1563
+ if (existsSync6(join4(dir, ".massu")) || existsSync6(join4(dir, "massu.config.yaml"))) {
1564
+ return join4(dir, ".massu", "hook-failures.jsonl");
1409
1565
  }
1410
1566
  const parent = dirname5(dir);
1411
1567
  if (parent === dir) break;
1412
1568
  dir = parent;
1413
1569
  }
1414
- return join3(process.cwd(), ".massu", "hook-failures.jsonl");
1570
+ return join4(process.cwd(), ".massu", "hook-failures.jsonl");
1415
1571
  }
1416
1572
  function recordHookFailure(hook, error, context) {
1417
1573
  const record = {
@@ -1468,9 +1624,9 @@ var init_hook_failure_signal = __esm({
1468
1624
  });
1469
1625
 
1470
1626
  // src/memory-embedder-tokenizer.ts
1471
- import { readFileSync as readFileSync4 } from "fs";
1627
+ import { readFileSync as readFileSync5 } from "fs";
1472
1628
  function loadVocab(vocabPath) {
1473
- const lines = readFileSync4(vocabPath, "utf-8").split("\n");
1629
+ const lines = readFileSync5(vocabPath, "utf-8").split("\n");
1474
1630
  const vocab = /* @__PURE__ */ new Map();
1475
1631
  for (let i = 0; i < lines.length; i++) {
1476
1632
  const tok = lines[i].replace(/\r$/, "");
@@ -1590,9 +1746,9 @@ var init_memory_embedder_tokenizer = __esm({
1590
1746
 
1591
1747
  // src/memory-embedder.ts
1592
1748
  import { fileURLToPath } from "url";
1593
- import { dirname as dirname6, join as join4 } from "path";
1594
- import { existsSync as existsSync6 } from "fs";
1595
- import { createRequire as createRequire2 } from "module";
1749
+ import { dirname as dirname6, join as join5 } from "path";
1750
+ import { existsSync as existsSync7 } from "fs";
1751
+ import { createRequire as createRequire3 } from "module";
1596
1752
  function getActiveEmbedModel() {
1597
1753
  return _activeModel;
1598
1754
  }
@@ -1616,7 +1772,7 @@ function resolveModelDir() {
1616
1772
  let dir = dirname6(fileURLToPath(import.meta.url));
1617
1773
  let root = null;
1618
1774
  for (let i = 0; i < 8; i++) {
1619
- if (existsSync6(join4(dir, "package.json"))) {
1775
+ if (existsSync7(join5(dir, "package.json"))) {
1620
1776
  root = dir;
1621
1777
  break;
1622
1778
  }
@@ -1626,8 +1782,8 @@ function resolveModelDir() {
1626
1782
  }
1627
1783
  if (!root) return null;
1628
1784
  for (const rel of ["dist/embedder", "assets/embedder"]) {
1629
- const candidate = join4(root, rel);
1630
- if (existsSync6(join4(candidate, "model_quantized.onnx"))) return candidate;
1785
+ const candidate = join5(root, rel);
1786
+ if (existsSync7(join5(candidate, "model_quantized.onnx"))) return candidate;
1631
1787
  }
1632
1788
  return null;
1633
1789
  } catch {
@@ -1636,8 +1792,8 @@ function resolveModelDir() {
1636
1792
  }
1637
1793
  function resolveWasmDir() {
1638
1794
  try {
1639
- const req2 = createRequire2(import.meta.url);
1640
- const main2 = req2.resolve(ORT_PKG);
1795
+ const req3 = createRequire3(import.meta.url);
1796
+ const main2 = req3.resolve(ORT_PKG);
1641
1797
  return dirname6(main2);
1642
1798
  } catch {
1643
1799
  return null;
@@ -1658,8 +1814,8 @@ async function getSession() {
1658
1814
  ort.env.wasm.proxy = false;
1659
1815
  ort.env.wasm.wasmPaths = wasmDir.endsWith("/") ? wasmDir : wasmDir + "/";
1660
1816
  _ortTensor = ort.Tensor;
1661
- const modelPath = join4(modelDir, "model_quantized.onnx");
1662
- const vocabPath = join4(modelDir, "vocab.txt");
1817
+ const modelPath = join5(modelDir, "model_quantized.onnx");
1818
+ const vocabPath = join5(modelDir, "vocab.txt");
1663
1819
  _vocab = loadVocab(vocabPath);
1664
1820
  const session = await ort.InferenceSession.create(modelPath, {
1665
1821
  executionProviders: ["wasm"],
@@ -1980,6 +2136,7 @@ __export(memory_db_exports, {
1980
2136
  migrateAuditLogCheckExtension: () => migrateAuditLogCheckExtension,
1981
2137
  migrateMemoryFilesFor4B: () => migrateMemoryFilesFor4B,
1982
2138
  migrateObservationEmbeddingChunks: () => migrateObservationEmbeddingChunks,
2139
+ migrateSharedMemoryFor5B: () => migrateSharedMemoryFor5B,
1983
2140
  pruneOldConversationTurns: () => pruneOldConversationTurns,
1984
2141
  pruneOldObservations: () => pruneOldObservations,
1985
2142
  pruneToolCostEvents: () => pruneToolCostEvents,
@@ -1997,7 +2154,7 @@ __export(memory_db_exports, {
1997
2154
  usageWarmupElapsed: () => usageWarmupElapsed
1998
2155
  });
1999
2156
  import { dirname as dirname7, basename as basename2 } from "path";
2000
- import { existsSync as existsSync7, mkdirSync as mkdirSync6 } from "fs";
2157
+ import { existsSync as existsSync8, mkdirSync as mkdirSync6 } from "fs";
2001
2158
  function sanitizeFts5Query(raw) {
2002
2159
  const trimmed = raw.trim();
2003
2160
  if (!trimmed) return '""';
@@ -2014,11 +2171,11 @@ function sanitizeFts5QueryOr(raw) {
2014
2171
  function getMemoryDb() {
2015
2172
  const dbPath = getResolvedPaths().memoryDbPath;
2016
2173
  const dir = dirname7(dbPath);
2017
- if (!existsSync7(dir)) {
2174
+ if (!existsSync8(dir)) {
2018
2175
  mkdirSync6(dir, { recursive: true });
2019
2176
  }
2020
- const preExisting = existsSync7(dbPath);
2021
- const db = openDatabase(dbPath);
2177
+ const preExisting = existsSync8(dbPath);
2178
+ const db = openDatabase2(dbPath);
2022
2179
  db.pragma("journal_mode = WAL");
2023
2180
  db.pragma("foreign_keys = ON");
2024
2181
  const onDisk = db.pragma("user_version", { simple: true });
@@ -2094,6 +2251,39 @@ function migrateMemoryFilesFor4B(db) {
2094
2251
  db.exec(`ALTER TABLE ${add.table} ADD COLUMN ${add.name} ${add.decl}`);
2095
2252
  }
2096
2253
  }
2254
+ function migrateSharedMemoryFor5B(db) {
2255
+ const cols = db.prepare(`PRAGMA table_info(observations)`).all();
2256
+ if (cols.length > 0 && !cols.some((c) => c.name === "shareable")) {
2257
+ db.exec(`ALTER TABLE observations ADD COLUMN shareable INTEGER NOT NULL DEFAULT 0`);
2258
+ }
2259
+ db.exec(`
2260
+ CREATE TABLE IF NOT EXISTS shared_memory_pending (
2261
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
2262
+ record_hash TEXT NOT NULL UNIQUE, -- identity + idempotency key (hex sha256)
2263
+ origin_repo_id TEXT NOT NULL, -- the signed origin repo_id (v4 UUID)
2264
+ origin_repo_label TEXT NOT NULL, -- re-slugged on read; NEVER trusted from the wire
2265
+ envelope_raw TEXT NOT NULL, -- the VERBATIM signed bytes; B-05 re-verifies these
2266
+ record_json TEXT NOT NULL, -- the single record's canonical JSON
2267
+ received_at_epoch INTEGER NOT NULL, -- epoch SECONDS (Slice-2 convention)
2268
+ accepted_at_epoch INTEGER, -- set on accept (B-05); NULL while pending
2269
+ refused_at_epoch INTEGER, -- set on refuse (B-06)
2270
+ expired_at_epoch INTEGER -- revocation \u21D2 EXPIRE, never DELETE (B-07 / S-3)
2271
+ );
2272
+ CREATE INDEX IF NOT EXISTS idx_smp_origin ON shared_memory_pending(origin_repo_id);
2273
+ CREATE INDEX IF NOT EXISTS idx_smp_received ON shared_memory_pending(received_at_epoch DESC);
2274
+ `);
2275
+ db.exec(`
2276
+ CREATE TABLE IF NOT EXISTS shared_memory_outbound (
2277
+ record_hash TEXT PRIMARY KEY, -- the exported record's canonical hash
2278
+ observation_id INTEGER NOT NULL, -- the source observations.id (for B-07 revocation)
2279
+ origin_repo_id TEXT NOT NULL, -- this repo's own repo_id at export time
2280
+ seq INTEGER NOT NULL, -- the envelope seq this record first went out in
2281
+ exported_at_epoch INTEGER NOT NULL, -- epoch SECONDS
2282
+ revoked_at_epoch INTEGER -- set when the source row is superseded/expired (B-07)
2283
+ );
2284
+ CREATE INDEX IF NOT EXISTS idx_smo_obs ON shared_memory_outbound(observation_id);
2285
+ `);
2286
+ }
2097
2287
  function migrateAuditLogCheckExtension(db) {
2098
2288
  const row = db.prepare("SELECT sql FROM sqlite_master WHERE type='table' AND name='audit_log'").get();
2099
2289
  if (!row) return;
@@ -2116,7 +2306,16 @@ function migrateAuditLogCheckExtension(db) {
2116
2306
  "memory_file_adopted_human",
2117
2307
  "memory_file_rendered",
2118
2308
  "memory_file_render_refused",
2119
- "memory_file_tombstoned"
2309
+ "memory_file_tombstoned",
2310
+ // B-10 (Slice 5) — the seven cross-repo shared-memory events. An existing DB whose
2311
+ // CHECK lacks any of these triggers the table rebuild below.
2312
+ "shared_memory_exported",
2313
+ "shared_memory_export_refused",
2314
+ "shared_memory_imported",
2315
+ "shared_memory_dropped",
2316
+ "shared_memory_accepted",
2317
+ "shared_memory_refused",
2318
+ "shared_memory_revoked"
2120
2319
  ];
2121
2320
  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);
2122
2321
  if (checkClauseMatch) {
@@ -2135,7 +2334,12 @@ function migrateAuditLogCheckExtension(db) {
2135
2334
  'code_change', 'rule_enforced', 'approval', 'review', 'commit', 'compaction',
2136
2335
  'rule_candidate_emitted', 'rule_promoted', 'rule_dismissed',
2137
2336
  'memory_file_ingested', 'memory_file_expired', 'memory_file_adopted_human',
2138
- 'memory_file_rendered', 'memory_file_render_refused', 'memory_file_tombstoned'
2337
+ 'memory_file_rendered', 'memory_file_render_refused', 'memory_file_tombstoned',
2338
+ -- B-10 (Slice 5) \u2014 cross-repo shared-memory events (must mirror the CREATE +
2339
+ -- the expected[] list above, or the migration would loop rebuilding forever).
2340
+ 'shared_memory_exported', 'shared_memory_export_refused', 'shared_memory_imported',
2341
+ 'shared_memory_dropped', 'shared_memory_accepted', 'shared_memory_refused',
2342
+ 'shared_memory_revoked'
2139
2343
  )),
2140
2344
  actor TEXT NOT NULL DEFAULT 'ai' CHECK(actor IN ('ai', 'human', 'hook', 'agent')),
2141
2345
  model_id TEXT,
@@ -2212,6 +2416,12 @@ function initMemorySchema(db) {
2212
2416
  -- credential, or takes a snapshot. A Slice-5 synced memory arrives here with
2213
2417
  -- origin='team' and must never reach disk without CR-55's gates.
2214
2418
  origin TEXT NOT NULL DEFAULT 'local',
2419
+ -- B-01 (Slice 5, cross-repo surfacing): the per-decision SHARE opt-in. Set to 1
2420
+ -- ONLY by an explicit human act (massu memory share <id>); NEVER by a heuristic,
2421
+ -- score, LLM, hook, or consolidation pass. Export (B-02) reads WHERE shareable=1
2422
+ -- AND origin='local'. Defaults 0 so every pre-existing and machine-written row is
2423
+ -- un-shareable until a human says otherwise (fail-closed).
2424
+ shareable INTEGER NOT NULL DEFAULT 0,
2215
2425
  created_at TEXT NOT NULL,
2216
2426
  created_at_epoch INTEGER NOT NULL,
2217
2427
  FOREIGN KEY(session_id) REFERENCES sessions(session_id) ON DELETE CASCADE
@@ -2485,7 +2695,13 @@ function initMemorySchema(db) {
2485
2695
  'code_change', 'rule_enforced', 'approval', 'review', 'commit', 'compaction',
2486
2696
  'rule_candidate_emitted', 'rule_promoted', 'rule_dismissed',
2487
2697
  'memory_file_ingested', 'memory_file_expired', 'memory_file_adopted_human',
2488
- 'memory_file_rendered', 'memory_file_render_refused', 'memory_file_tombstoned'
2698
+ 'memory_file_rendered', 'memory_file_render_refused', 'memory_file_tombstoned',
2699
+ -- B-10 (Slice 5): cross-repo shared-memory observability. Without these seven,
2700
+ -- nobody could ever answer "what has crossed between my repos?" (and an ingest
2701
+ -- catch would swallow the CHECK violation, silently producing NOTHING).
2702
+ 'shared_memory_exported', 'shared_memory_export_refused', 'shared_memory_imported',
2703
+ 'shared_memory_dropped', 'shared_memory_accepted', 'shared_memory_refused',
2704
+ 'shared_memory_revoked'
2489
2705
  )),
2490
2706
  actor TEXT NOT NULL DEFAULT 'ai' CHECK(actor IN ('ai', 'human', 'hook', 'agent')),
2491
2707
  model_id TEXT,
@@ -2854,6 +3070,7 @@ function initMemorySchema(db) {
2854
3070
  `);
2855
3071
  migrateObservationEmbeddingChunks(db);
2856
3072
  migrateMemoryFilesFor4B(db);
3073
+ migrateSharedMemoryFor5B(db);
2857
3074
  ensureRuleCandidatesTable(db);
2858
3075
  migrateRuleDelivery(db);
2859
3076
  }
@@ -3724,7 +3941,7 @@ var MEMORY_SCHEMA_VERSION, TOOL_COST_EVENTS_RETENTION_DAYS, MAX_DRAFT_TEXT_LEN,
3724
3941
  var init_memory_db = __esm({
3725
3942
  "src/memory-db.ts"() {
3726
3943
  "use strict";
3727
- init_sqlite_loader();
3944
+ init_db_driver();
3728
3945
  init_config();
3729
3946
  init_memory_vector();
3730
3947
  init_db_backup();
@@ -3747,7 +3964,7 @@ var init_memory_db = __esm({
3747
3964
  // src/hooks/incident-pipeline.ts
3748
3965
  init_config();
3749
3966
  init_memory_db();
3750
- import { existsSync as existsSync8, readFileSync as readFileSync5, readdirSync as readdirSync2 } from "fs";
3967
+ import { existsSync as existsSync9, readFileSync as readFileSync6, readdirSync as readdirSync2 } from "fs";
3751
3968
  import { basename as basename3, resolve as resolve5 } from "path";
3752
3969
 
3753
3970
  // src/hooks/lib/write-hook-message.ts
@@ -3779,17 +3996,17 @@ async function main() {
3779
3996
  process.exit(0);
3780
3997
  return;
3781
3998
  }
3782
- if (!existsSync8(filePath)) {
3999
+ if (!existsSync9(filePath)) {
3783
4000
  process.exit(0);
3784
4001
  return;
3785
4002
  }
3786
- const content = readFileSync5(filePath, "utf-8");
4003
+ const content = readFileSync6(filePath, "utf-8");
3787
4004
  const titleMatch = content.match(/^#\s+(.+)/m);
3788
4005
  const title = titleMatch?.[1] ?? basename3(filePath, ".md");
3789
4006
  const slug = basename3(filePath, ".md").replace(/^\d{4}-\d{2}-\d{2}-?/, "").toLowerCase();
3790
4007
  const memoryDirAbs = resolve5(root, memoryDir);
3791
4008
  let hasExistingRule = false;
3792
- if (existsSync8(memoryDirAbs)) {
4009
+ if (existsSync9(memoryDirAbs)) {
3793
4010
  const ruleFiles = readdirSync2(memoryDirAbs).filter((f) => f.startsWith("feedback_"));
3794
4011
  for (const ruleFile of ruleFiles) {
3795
4012
  if (ruleFile.toLowerCase().includes(slug.slice(0, 20))) {