@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.
@@ -423,6 +423,136 @@ var init_sqlite_loader = __esm({
423
423
  }
424
424
  });
425
425
 
426
+ // src/db-driver.ts
427
+ import { createRequire as createRequire2 } from "module";
428
+ import { closeSync, existsSync as existsSync3, mkdtempSync, openSync, readFileSync as readFileSync3, rmSync as rmSync3, fsyncSync } from "fs";
429
+ import { tmpdir } from "os";
430
+ import { join as join2 } from "path";
431
+ function resolveDbEngine() {
432
+ return process.env[DB_ENGINE_ENV] === "better-sqlite3" ? "better-sqlite3" : DEFAULT_DB_ENGINE;
433
+ }
434
+ function nodeSqliteCtor() {
435
+ if (!_nodeCtor) _nodeCtor = req2("node:sqlite").DatabaseSync;
436
+ return _nodeCtor;
437
+ }
438
+ function plainRow(row) {
439
+ if (row == null || typeof row !== "object") return row;
440
+ return { ...row };
441
+ }
442
+ function openNodeSqlite(dbPath, opts) {
443
+ const Ctor = nodeSqliteCtor();
444
+ const raw = new Ctor(dbPath, {
445
+ open: true,
446
+ readOnly: !!opts.readonly,
447
+ // FAITHFULNESS: better-sqlite3's ACTUAL default is foreign_keys=ON (empirically
448
+ // verified — SQLite's own default is OFF, but bs3 enables it). massu was written +
449
+ // tested against that default, so the adapter must match it, or a store that relies
450
+ // on FK-ON-by-default (without an explicit pragma) would silently lose enforcement.
451
+ // Explicit `foreign_keys=OFF/ON` pragmas (e.g. memory-db bulk ops) still override.
452
+ enableForeignKeyConstraints: true,
453
+ allowExtension: false
454
+ });
455
+ let savepointSeq = 0;
456
+ const wrapStmt = (sql) => {
457
+ const st = raw.prepare(sql);
458
+ return {
459
+ run: (...p) => {
460
+ const r = st.run(...p);
461
+ return {
462
+ changes: typeof r.changes === "bigint" ? Number(r.changes) : r.changes,
463
+ lastInsertRowid: r.lastInsertRowid
464
+ };
465
+ },
466
+ get: (...p) => plainRow(st.get(...p)),
467
+ all: (...p) => st.all(...p).map(plainRow)
468
+ };
469
+ };
470
+ const pragma = (source, options) => {
471
+ const s = String(source).trim();
472
+ if (s.includes("=")) {
473
+ raw.exec(`PRAGMA ${s}`);
474
+ return void 0;
475
+ }
476
+ const rows = raw.prepare(`PRAGMA ${s}`).all().map(plainRow);
477
+ if (options?.simple) {
478
+ const first = rows[0];
479
+ return first ? Object.values(first)[0] : void 0;
480
+ }
481
+ return rows;
482
+ };
483
+ const transaction = (fn) => {
484
+ const run = (...args) => {
485
+ const nested = raw.isTransaction;
486
+ const name = `msp_${savepointSeq++}`;
487
+ if (nested) raw.exec(`SAVEPOINT ${name}`);
488
+ else raw.exec("BEGIN");
489
+ try {
490
+ const out = fn(...args);
491
+ if (nested) raw.exec(`RELEASE ${name}`);
492
+ else raw.exec("COMMIT");
493
+ return out;
494
+ } catch (e) {
495
+ if (nested) {
496
+ raw.exec(`ROLLBACK TO ${name}`);
497
+ raw.exec(`RELEASE ${name}`);
498
+ } else {
499
+ raw.exec("ROLLBACK");
500
+ }
501
+ throw e;
502
+ }
503
+ };
504
+ return run;
505
+ };
506
+ const serialize = () => {
507
+ if (raw.isTransaction) {
508
+ throw new Error(
509
+ "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."
510
+ );
511
+ }
512
+ const dir = mkdtempSync(join2(tmpdir(), "massu-serialize-"));
513
+ const out = join2(dir, "snapshot.db");
514
+ try {
515
+ raw.exec(`VACUUM INTO '${out.replace(/'/g, "''")}'`);
516
+ const fd = openSync(out, "r");
517
+ try {
518
+ fsyncSync(fd);
519
+ } finally {
520
+ closeSync(fd);
521
+ }
522
+ return readFileSync3(out);
523
+ } finally {
524
+ rmSync3(dir, { recursive: true, force: true });
525
+ }
526
+ };
527
+ const handle = {
528
+ prepare: (sql) => wrapStmt(sql),
529
+ exec: (sql) => raw.exec(sql),
530
+ pragma,
531
+ transaction,
532
+ serialize,
533
+ close: () => raw.close()
534
+ };
535
+ return handle;
536
+ }
537
+ function openBetterSqlite3(dbPath, opts) {
538
+ return openDatabase(dbPath, opts);
539
+ }
540
+ function openDatabase2(dbPath, opts = {}) {
541
+ return resolveDbEngine() === "better-sqlite3" ? openBetterSqlite3(dbPath, opts) : openNodeSqlite(dbPath, opts);
542
+ }
543
+ var req2, DEFAULT_DB_ENGINE, DB_ENGINE_ENV, _nodeCtor;
544
+ var init_db_driver = __esm({
545
+ "src/db-driver.ts"() {
546
+ "use strict";
547
+ init_sqlite_loader();
548
+ init_sqlite_loader();
549
+ req2 = createRequire2(import.meta.url);
550
+ DEFAULT_DB_ENGINE = "node-sqlite";
551
+ DB_ENGINE_ENV = "MASSU_DB_ENGINE";
552
+ _nodeCtor = null;
553
+ }
554
+ });
555
+
426
556
  // src/lib/memory-path.ts
427
557
  function encodeMemoryDirName(projectRoot) {
428
558
  return projectRoot.replace(/\//g, "-");
@@ -566,6 +696,32 @@ var init_config_memory_schema = __esm({
566
696
  // turn of EVERY session, so an unbounded index is a permanent context tax — and the
567
697
  // per-session cap bounds only the RATE, never the total.
568
698
  indexMaxLines: z.number().int().min(1).default(50)
699
+ }).default({}),
700
+ // --- Cross-repo memory surfacing (plan-living-memory-slice-5-cross-repo-surfacing) ---
701
+ // A decision made in one of your repos can surface in another — opt-in per
702
+ // decision AND opt-in per repo, signed, verified, and materialized ONLY on
703
+ // explicit human acceptance. Local transport is FREE and zero-network.
704
+ //
705
+ // ⛔ BOTH switches default OFF, and OFF means NOTHING EXISTS — no registry, no
706
+ // keys, no inbox, no behavioural difference. Two INDEPENDENT opt-ins:
707
+ // • enabled — may this repo EXPORT its shareable decisions? (opt-in #1)
708
+ // • subscribe — which repo LABELS may this repo IMPORT from? (opt-in #2)
709
+ // `subscribe: []` means import NOTHING. There is deliberately NO `subscribe: all`
710
+ // — a repo you never named is a repo Massu never reads from.
711
+ share: z.object({
712
+ enabled: z.boolean().default(false),
713
+ subscribe: z.array(z.string()).default([]),
714
+ // C-04 — recall surfacing of cross-repo memories. `enabled` defaults true but is
715
+ // CONDITIONAL on `subscribe` being non-empty (empty by default), so the effective
716
+ // default is DORMANT: with `subscribe: []` the recall hook output is byte-identical
717
+ // to today's. `maxCrossRepoItems` caps how many accepted cross-repo items may appear
718
+ // per recall block (default 1); `minScore` is an OPTIONAL strictly-higher floor for
719
+ // cross-repo items (they are, by construction, less relevant than local ones).
720
+ recall: z.object({
721
+ enabled: z.boolean().default(true),
722
+ maxCrossRepoItems: z.number().int().min(0).default(1),
723
+ minScore: z.number().optional()
724
+ }).default({})
569
725
  }).default({})
570
726
  }).optional();
571
727
  }
@@ -573,7 +729,7 @@ var init_config_memory_schema = __esm({
573
729
 
574
730
  // src/config.ts
575
731
  import { resolve as resolve2, dirname as dirname3 } from "path";
576
- import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
732
+ import { existsSync as existsSync4, readFileSync as readFileSync4 } from "fs";
577
733
  import { homedir as homedir2 } from "os";
578
734
  import { parse as parseYaml } from "yaml";
579
735
  import { z as z2 } from "zod";
@@ -581,7 +737,7 @@ function findProjectRoot() {
581
737
  const cwd = process.cwd();
582
738
  let dir = cwd;
583
739
  while (true) {
584
- if (existsSync3(resolve2(dir, "massu.config.yaml"))) {
740
+ if (existsSync4(resolve2(dir, "massu.config.yaml"))) {
585
741
  return dir;
586
742
  }
587
743
  const parent = dirname3(dir);
@@ -590,10 +746,10 @@ function findProjectRoot() {
590
746
  }
591
747
  dir = cwd;
592
748
  while (true) {
593
- if (existsSync3(resolve2(dir, "package.json"))) {
749
+ if (existsSync4(resolve2(dir, "package.json"))) {
594
750
  return dir;
595
751
  }
596
- if (existsSync3(resolve2(dir, ".git"))) {
752
+ if (existsSync4(resolve2(dir, ".git"))) {
597
753
  return dir;
598
754
  }
599
755
  const parent = dirname3(dir);
@@ -613,8 +769,8 @@ function getConfig() {
613
769
  const root = getProjectRoot();
614
770
  const configPath = resolve2(root, "massu.config.yaml");
615
771
  let rawYaml = {};
616
- if (existsSync3(configPath)) {
617
- const content = readFileSync3(configPath, "utf-8");
772
+ if (existsSync4(configPath)) {
773
+ const content = readFileSync4(configPath, "utf-8");
618
774
  rawYaml = parseYaml(content) ?? {};
619
775
  }
620
776
  const result = RawConfigSchema.safeParse(rawYaml);
@@ -742,6 +898,7 @@ function getResolvedPaths() {
742
898
  sessionStatePath: resolve2(root, config.conventions?.sessionStatePath ?? `${claudeDirName}/session-state/CURRENT.md`),
743
899
  sessionArchivePath: resolve2(root, config.conventions?.sessionArchivePath ?? `${claudeDirName}/session-state/archive`),
744
900
  mcpJsonPath: resolve2(root, ".mcp.json"),
901
+ settingsPath: resolve2(root, claudeDirName, "settings.json"),
745
902
  settingsLocalPath: resolve2(root, claudeDirName, "settings.local.json")
746
903
  };
747
904
  }
@@ -819,7 +976,6 @@ var init_config = __esm({
819
976
  custom_patterns: z2.array(CustomPatternSchema).default([])
820
977
  }).optional(),
821
978
  adr: z2.object({
822
- detection_phrases: z2.array(z2.string()).default(["chose", "decided", "switching to", "moving from", "going with"]),
823
979
  template: z2.string().default("default"),
824
980
  storage: z2.string().default("database"),
825
981
  output_dir: z2.string().default("docs/adr")
@@ -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,9 +1553,9 @@ var init_rule_candidate_store = __esm({
1397
1553
  });
1398
1554
 
1399
1555
  // src/memory-embedder-tokenizer.ts
1400
- import { readFileSync as readFileSync4 } from "fs";
1556
+ import { readFileSync as readFileSync5 } from "fs";
1401
1557
  function loadVocab(vocabPath) {
1402
- const lines = readFileSync4(vocabPath, "utf-8").split("\n");
1558
+ const lines = readFileSync5(vocabPath, "utf-8").split("\n");
1403
1559
  const vocab = /* @__PURE__ */ new Map();
1404
1560
  for (let i = 0; i < lines.length; i++) {
1405
1561
  const tok = lines[i].replace(/\r$/, "");
@@ -1519,9 +1675,9 @@ var init_memory_embedder_tokenizer = __esm({
1519
1675
 
1520
1676
  // src/memory-embedder.ts
1521
1677
  import { fileURLToPath } from "url";
1522
- import { dirname as dirname5, join as join3 } from "path";
1523
- import { existsSync as existsSync5 } from "fs";
1524
- import { createRequire as createRequire2 } from "module";
1678
+ import { dirname as dirname5, join as join4 } from "path";
1679
+ import { existsSync as existsSync6 } from "fs";
1680
+ import { createRequire as createRequire3 } from "module";
1525
1681
  function getActiveEmbedModel() {
1526
1682
  return _activeModel;
1527
1683
  }
@@ -1545,7 +1701,7 @@ function resolveModelDir() {
1545
1701
  let dir = dirname5(fileURLToPath(import.meta.url));
1546
1702
  let root = null;
1547
1703
  for (let i = 0; i < 8; i++) {
1548
- if (existsSync5(join3(dir, "package.json"))) {
1704
+ if (existsSync6(join4(dir, "package.json"))) {
1549
1705
  root = dir;
1550
1706
  break;
1551
1707
  }
@@ -1555,8 +1711,8 @@ function resolveModelDir() {
1555
1711
  }
1556
1712
  if (!root) return null;
1557
1713
  for (const rel of ["dist/embedder", "assets/embedder"]) {
1558
- const candidate = join3(root, rel);
1559
- if (existsSync5(join3(candidate, "model_quantized.onnx"))) return candidate;
1714
+ const candidate = join4(root, rel);
1715
+ if (existsSync6(join4(candidate, "model_quantized.onnx"))) return candidate;
1560
1716
  }
1561
1717
  return null;
1562
1718
  } catch {
@@ -1565,8 +1721,8 @@ function resolveModelDir() {
1565
1721
  }
1566
1722
  function resolveWasmDir() {
1567
1723
  try {
1568
- const req2 = createRequire2(import.meta.url);
1569
- const main2 = req2.resolve(ORT_PKG);
1724
+ const req3 = createRequire3(import.meta.url);
1725
+ const main2 = req3.resolve(ORT_PKG);
1570
1726
  return dirname5(main2);
1571
1727
  } catch {
1572
1728
  return null;
@@ -1587,8 +1743,8 @@ async function getSession() {
1587
1743
  ort.env.wasm.proxy = false;
1588
1744
  ort.env.wasm.wasmPaths = wasmDir.endsWith("/") ? wasmDir : wasmDir + "/";
1589
1745
  _ortTensor = ort.Tensor;
1590
- const modelPath = join3(modelDir, "model_quantized.onnx");
1591
- const vocabPath = join3(modelDir, "vocab.txt");
1746
+ const modelPath = join4(modelDir, "model_quantized.onnx");
1747
+ const vocabPath = join4(modelDir, "vocab.txt");
1592
1748
  _vocab = loadVocab(vocabPath);
1593
1749
  const session = await ort.InferenceSession.create(modelPath, {
1594
1750
  executionProviders: ["wasm"],
@@ -1909,6 +2065,7 @@ __export(memory_db_exports, {
1909
2065
  migrateAuditLogCheckExtension: () => migrateAuditLogCheckExtension,
1910
2066
  migrateMemoryFilesFor4B: () => migrateMemoryFilesFor4B,
1911
2067
  migrateObservationEmbeddingChunks: () => migrateObservationEmbeddingChunks,
2068
+ migrateSharedMemoryFor5B: () => migrateSharedMemoryFor5B,
1912
2069
  pruneOldConversationTurns: () => pruneOldConversationTurns,
1913
2070
  pruneOldObservations: () => pruneOldObservations,
1914
2071
  pruneToolCostEvents: () => pruneToolCostEvents,
@@ -1926,7 +2083,7 @@ __export(memory_db_exports, {
1926
2083
  usageWarmupElapsed: () => usageWarmupElapsed
1927
2084
  });
1928
2085
  import { dirname as dirname6, basename as basename2 } from "path";
1929
- import { existsSync as existsSync6, mkdirSync as mkdirSync5 } from "fs";
2086
+ import { existsSync as existsSync7, mkdirSync as mkdirSync5 } from "fs";
1930
2087
  function sanitizeFts5Query(raw) {
1931
2088
  const trimmed = raw.trim();
1932
2089
  if (!trimmed) return '""';
@@ -1943,11 +2100,11 @@ function sanitizeFts5QueryOr(raw) {
1943
2100
  function getMemoryDb() {
1944
2101
  const dbPath = getResolvedPaths().memoryDbPath;
1945
2102
  const dir = dirname6(dbPath);
1946
- if (!existsSync6(dir)) {
2103
+ if (!existsSync7(dir)) {
1947
2104
  mkdirSync5(dir, { recursive: true });
1948
2105
  }
1949
- const preExisting = existsSync6(dbPath);
1950
- const db = openDatabase(dbPath);
2106
+ const preExisting = existsSync7(dbPath);
2107
+ const db = openDatabase2(dbPath);
1951
2108
  db.pragma("journal_mode = WAL");
1952
2109
  db.pragma("foreign_keys = ON");
1953
2110
  const onDisk = db.pragma("user_version", { simple: true });
@@ -2023,6 +2180,39 @@ function migrateMemoryFilesFor4B(db) {
2023
2180
  db.exec(`ALTER TABLE ${add.table} ADD COLUMN ${add.name} ${add.decl}`);
2024
2181
  }
2025
2182
  }
2183
+ function migrateSharedMemoryFor5B(db) {
2184
+ const cols = db.prepare(`PRAGMA table_info(observations)`).all();
2185
+ if (cols.length > 0 && !cols.some((c) => c.name === "shareable")) {
2186
+ db.exec(`ALTER TABLE observations ADD COLUMN shareable INTEGER NOT NULL DEFAULT 0`);
2187
+ }
2188
+ db.exec(`
2189
+ CREATE TABLE IF NOT EXISTS shared_memory_pending (
2190
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
2191
+ record_hash TEXT NOT NULL UNIQUE, -- identity + idempotency key (hex sha256)
2192
+ origin_repo_id TEXT NOT NULL, -- the signed origin repo_id (v4 UUID)
2193
+ origin_repo_label TEXT NOT NULL, -- re-slugged on read; NEVER trusted from the wire
2194
+ envelope_raw TEXT NOT NULL, -- the VERBATIM signed bytes; B-05 re-verifies these
2195
+ record_json TEXT NOT NULL, -- the single record's canonical JSON
2196
+ received_at_epoch INTEGER NOT NULL, -- epoch SECONDS (Slice-2 convention)
2197
+ accepted_at_epoch INTEGER, -- set on accept (B-05); NULL while pending
2198
+ refused_at_epoch INTEGER, -- set on refuse (B-06)
2199
+ expired_at_epoch INTEGER -- revocation \u21D2 EXPIRE, never DELETE (B-07 / S-3)
2200
+ );
2201
+ CREATE INDEX IF NOT EXISTS idx_smp_origin ON shared_memory_pending(origin_repo_id);
2202
+ CREATE INDEX IF NOT EXISTS idx_smp_received ON shared_memory_pending(received_at_epoch DESC);
2203
+ `);
2204
+ db.exec(`
2205
+ CREATE TABLE IF NOT EXISTS shared_memory_outbound (
2206
+ record_hash TEXT PRIMARY KEY, -- the exported record's canonical hash
2207
+ observation_id INTEGER NOT NULL, -- the source observations.id (for B-07 revocation)
2208
+ origin_repo_id TEXT NOT NULL, -- this repo's own repo_id at export time
2209
+ seq INTEGER NOT NULL, -- the envelope seq this record first went out in
2210
+ exported_at_epoch INTEGER NOT NULL, -- epoch SECONDS
2211
+ revoked_at_epoch INTEGER -- set when the source row is superseded/expired (B-07)
2212
+ );
2213
+ CREATE INDEX IF NOT EXISTS idx_smo_obs ON shared_memory_outbound(observation_id);
2214
+ `);
2215
+ }
2026
2216
  function migrateAuditLogCheckExtension(db) {
2027
2217
  const row = db.prepare("SELECT sql FROM sqlite_master WHERE type='table' AND name='audit_log'").get();
2028
2218
  if (!row) return;
@@ -2045,7 +2235,16 @@ function migrateAuditLogCheckExtension(db) {
2045
2235
  "memory_file_adopted_human",
2046
2236
  "memory_file_rendered",
2047
2237
  "memory_file_render_refused",
2048
- "memory_file_tombstoned"
2238
+ "memory_file_tombstoned",
2239
+ // B-10 (Slice 5) — the seven cross-repo shared-memory events. An existing DB whose
2240
+ // CHECK lacks any of these triggers the table rebuild below.
2241
+ "shared_memory_exported",
2242
+ "shared_memory_export_refused",
2243
+ "shared_memory_imported",
2244
+ "shared_memory_dropped",
2245
+ "shared_memory_accepted",
2246
+ "shared_memory_refused",
2247
+ "shared_memory_revoked"
2049
2248
  ];
2050
2249
  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);
2051
2250
  if (checkClauseMatch) {
@@ -2064,7 +2263,12 @@ function migrateAuditLogCheckExtension(db) {
2064
2263
  'code_change', 'rule_enforced', 'approval', 'review', 'commit', 'compaction',
2065
2264
  'rule_candidate_emitted', 'rule_promoted', 'rule_dismissed',
2066
2265
  'memory_file_ingested', 'memory_file_expired', 'memory_file_adopted_human',
2067
- 'memory_file_rendered', 'memory_file_render_refused', 'memory_file_tombstoned'
2266
+ 'memory_file_rendered', 'memory_file_render_refused', 'memory_file_tombstoned',
2267
+ -- B-10 (Slice 5) \u2014 cross-repo shared-memory events (must mirror the CREATE +
2268
+ -- the expected[] list above, or the migration would loop rebuilding forever).
2269
+ 'shared_memory_exported', 'shared_memory_export_refused', 'shared_memory_imported',
2270
+ 'shared_memory_dropped', 'shared_memory_accepted', 'shared_memory_refused',
2271
+ 'shared_memory_revoked'
2068
2272
  )),
2069
2273
  actor TEXT NOT NULL DEFAULT 'ai' CHECK(actor IN ('ai', 'human', 'hook', 'agent')),
2070
2274
  model_id TEXT,
@@ -2141,6 +2345,12 @@ function initMemorySchema(db) {
2141
2345
  -- credential, or takes a snapshot. A Slice-5 synced memory arrives here with
2142
2346
  -- origin='team' and must never reach disk without CR-55's gates.
2143
2347
  origin TEXT NOT NULL DEFAULT 'local',
2348
+ -- B-01 (Slice 5, cross-repo surfacing): the per-decision SHARE opt-in. Set to 1
2349
+ -- ONLY by an explicit human act (massu memory share <id>); NEVER by a heuristic,
2350
+ -- score, LLM, hook, or consolidation pass. Export (B-02) reads WHERE shareable=1
2351
+ -- AND origin='local'. Defaults 0 so every pre-existing and machine-written row is
2352
+ -- un-shareable until a human says otherwise (fail-closed).
2353
+ shareable INTEGER NOT NULL DEFAULT 0,
2144
2354
  created_at TEXT NOT NULL,
2145
2355
  created_at_epoch INTEGER NOT NULL,
2146
2356
  FOREIGN KEY(session_id) REFERENCES sessions(session_id) ON DELETE CASCADE
@@ -2414,7 +2624,13 @@ function initMemorySchema(db) {
2414
2624
  'code_change', 'rule_enforced', 'approval', 'review', 'commit', 'compaction',
2415
2625
  'rule_candidate_emitted', 'rule_promoted', 'rule_dismissed',
2416
2626
  'memory_file_ingested', 'memory_file_expired', 'memory_file_adopted_human',
2417
- 'memory_file_rendered', 'memory_file_render_refused', 'memory_file_tombstoned'
2627
+ 'memory_file_rendered', 'memory_file_render_refused', 'memory_file_tombstoned',
2628
+ -- B-10 (Slice 5): cross-repo shared-memory observability. Without these seven,
2629
+ -- nobody could ever answer "what has crossed between my repos?" (and an ingest
2630
+ -- catch would swallow the CHECK violation, silently producing NOTHING).
2631
+ 'shared_memory_exported', 'shared_memory_export_refused', 'shared_memory_imported',
2632
+ 'shared_memory_dropped', 'shared_memory_accepted', 'shared_memory_refused',
2633
+ 'shared_memory_revoked'
2418
2634
  )),
2419
2635
  actor TEXT NOT NULL DEFAULT 'ai' CHECK(actor IN ('ai', 'human', 'hook', 'agent')),
2420
2636
  model_id TEXT,
@@ -2783,6 +2999,7 @@ function initMemorySchema(db) {
2783
2999
  `);
2784
3000
  migrateObservationEmbeddingChunks(db);
2785
3001
  migrateMemoryFilesFor4B(db);
3002
+ migrateSharedMemoryFor5B(db);
2786
3003
  ensureRuleCandidatesTable(db);
2787
3004
  migrateRuleDelivery(db);
2788
3005
  }
@@ -3653,7 +3870,7 @@ var MEMORY_SCHEMA_VERSION, TOOL_COST_EVENTS_RETENTION_DAYS, MAX_DRAFT_TEXT_LEN,
3653
3870
  var init_memory_db = __esm({
3654
3871
  "src/memory-db.ts"() {
3655
3872
  "use strict";
3656
- init_sqlite_loader();
3873
+ init_db_driver();
3657
3874
  init_config();
3658
3875
  init_memory_vector();
3659
3876
  init_db_backup();
@@ -3674,21 +3891,21 @@ var init_memory_db = __esm({
3674
3891
  });
3675
3892
 
3676
3893
  // src/hooks/lib/hook-failure-signal.ts
3677
- import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync6, existsSync as existsSync7 } from "fs";
3678
- import { join as join4, dirname as dirname7 } from "path";
3894
+ import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync6, existsSync as existsSync8 } from "fs";
3895
+ import { join as join5, dirname as dirname7 } from "path";
3679
3896
  function resolveFailureLogPath() {
3680
3897
  const explicit = process.env.MASSU_HOOK_FAILURE_LOG;
3681
3898
  if (explicit) return explicit;
3682
3899
  let dir = process.cwd();
3683
3900
  for (let i = 0; i < 12; i++) {
3684
- if (existsSync7(join4(dir, ".massu")) || existsSync7(join4(dir, "massu.config.yaml"))) {
3685
- return join4(dir, ".massu", "hook-failures.jsonl");
3901
+ if (existsSync8(join5(dir, ".massu")) || existsSync8(join5(dir, "massu.config.yaml"))) {
3902
+ return join5(dir, ".massu", "hook-failures.jsonl");
3686
3903
  }
3687
3904
  const parent = dirname7(dir);
3688
3905
  if (parent === dir) break;
3689
3906
  dir = parent;
3690
3907
  }
3691
- return join4(process.cwd(), ".massu", "hook-failures.jsonl");
3908
+ return join5(process.cwd(), ".massu", "hook-failures.jsonl");
3692
3909
  }
3693
3910
  function recordHookFailure(hook, error, context) {
3694
3911
  const record = {