@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.
@@ -429,6 +429,136 @@ var init_sqlite_loader = __esm({
429
429
  }
430
430
  });
431
431
 
432
+ // src/db-driver.ts
433
+ import { createRequire as createRequire2 } from "module";
434
+ import { closeSync, existsSync as existsSync3, mkdtempSync, openSync, readFileSync as readFileSync3, rmSync as rmSync3, fsyncSync } from "fs";
435
+ import { tmpdir } from "os";
436
+ import { join as join2 } from "path";
437
+ function resolveDbEngine() {
438
+ return process.env[DB_ENGINE_ENV] === "better-sqlite3" ? "better-sqlite3" : DEFAULT_DB_ENGINE;
439
+ }
440
+ function nodeSqliteCtor() {
441
+ if (!_nodeCtor) _nodeCtor = req2("node:sqlite").DatabaseSync;
442
+ return _nodeCtor;
443
+ }
444
+ function plainRow(row) {
445
+ if (row == null || typeof row !== "object") return row;
446
+ return { ...row };
447
+ }
448
+ function openNodeSqlite(dbPath, opts) {
449
+ const Ctor = nodeSqliteCtor();
450
+ const raw = new Ctor(dbPath, {
451
+ open: true,
452
+ readOnly: !!opts.readonly,
453
+ // FAITHFULNESS: better-sqlite3's ACTUAL default is foreign_keys=ON (empirically
454
+ // verified — SQLite's own default is OFF, but bs3 enables it). massu was written +
455
+ // tested against that default, so the adapter must match it, or a store that relies
456
+ // on FK-ON-by-default (without an explicit pragma) would silently lose enforcement.
457
+ // Explicit `foreign_keys=OFF/ON` pragmas (e.g. memory-db bulk ops) still override.
458
+ enableForeignKeyConstraints: true,
459
+ allowExtension: false
460
+ });
461
+ let savepointSeq = 0;
462
+ const wrapStmt = (sql) => {
463
+ const st = raw.prepare(sql);
464
+ return {
465
+ run: (...p) => {
466
+ const r = st.run(...p);
467
+ return {
468
+ changes: typeof r.changes === "bigint" ? Number(r.changes) : r.changes,
469
+ lastInsertRowid: r.lastInsertRowid
470
+ };
471
+ },
472
+ get: (...p) => plainRow(st.get(...p)),
473
+ all: (...p) => st.all(...p).map(plainRow)
474
+ };
475
+ };
476
+ const pragma = (source, options) => {
477
+ const s = String(source).trim();
478
+ if (s.includes("=")) {
479
+ raw.exec(`PRAGMA ${s}`);
480
+ return void 0;
481
+ }
482
+ const rows = raw.prepare(`PRAGMA ${s}`).all().map(plainRow);
483
+ if (options?.simple) {
484
+ const first = rows[0];
485
+ return first ? Object.values(first)[0] : void 0;
486
+ }
487
+ return rows;
488
+ };
489
+ const transaction = (fn) => {
490
+ const run = (...args) => {
491
+ const nested = raw.isTransaction;
492
+ const name = `msp_${savepointSeq++}`;
493
+ if (nested) raw.exec(`SAVEPOINT ${name}`);
494
+ else raw.exec("BEGIN");
495
+ try {
496
+ const out = fn(...args);
497
+ if (nested) raw.exec(`RELEASE ${name}`);
498
+ else raw.exec("COMMIT");
499
+ return out;
500
+ } catch (e) {
501
+ if (nested) {
502
+ raw.exec(`ROLLBACK TO ${name}`);
503
+ raw.exec(`RELEASE ${name}`);
504
+ } else {
505
+ raw.exec("ROLLBACK");
506
+ }
507
+ throw e;
508
+ }
509
+ };
510
+ return run;
511
+ };
512
+ const serialize = () => {
513
+ if (raw.isTransaction) {
514
+ throw new Error(
515
+ "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."
516
+ );
517
+ }
518
+ const dir = mkdtempSync(join2(tmpdir(), "massu-serialize-"));
519
+ const out = join2(dir, "snapshot.db");
520
+ try {
521
+ raw.exec(`VACUUM INTO '${out.replace(/'/g, "''")}'`);
522
+ const fd = openSync(out, "r");
523
+ try {
524
+ fsyncSync(fd);
525
+ } finally {
526
+ closeSync(fd);
527
+ }
528
+ return readFileSync3(out);
529
+ } finally {
530
+ rmSync3(dir, { recursive: true, force: true });
531
+ }
532
+ };
533
+ const handle = {
534
+ prepare: (sql) => wrapStmt(sql),
535
+ exec: (sql) => raw.exec(sql),
536
+ pragma,
537
+ transaction,
538
+ serialize,
539
+ close: () => raw.close()
540
+ };
541
+ return handle;
542
+ }
543
+ function openBetterSqlite3(dbPath, opts) {
544
+ return openDatabase(dbPath, opts);
545
+ }
546
+ function openDatabase2(dbPath, opts = {}) {
547
+ return resolveDbEngine() === "better-sqlite3" ? openBetterSqlite3(dbPath, opts) : openNodeSqlite(dbPath, opts);
548
+ }
549
+ var req2, DEFAULT_DB_ENGINE, DB_ENGINE_ENV, _nodeCtor;
550
+ var init_db_driver = __esm({
551
+ "src/db-driver.ts"() {
552
+ "use strict";
553
+ init_sqlite_loader();
554
+ init_sqlite_loader();
555
+ req2 = createRequire2(import.meta.url);
556
+ DEFAULT_DB_ENGINE = "node-sqlite";
557
+ DB_ENGINE_ENV = "MASSU_DB_ENGINE";
558
+ _nodeCtor = null;
559
+ }
560
+ });
561
+
432
562
  // src/lib/memory-path.ts
433
563
  function encodeMemoryDirName(projectRoot) {
434
564
  return projectRoot.replace(/\//g, "-");
@@ -572,6 +702,32 @@ var init_config_memory_schema = __esm({
572
702
  // turn of EVERY session, so an unbounded index is a permanent context tax — and the
573
703
  // per-session cap bounds only the RATE, never the total.
574
704
  indexMaxLines: z.number().int().min(1).default(50)
705
+ }).default({}),
706
+ // --- Cross-repo memory surfacing (plan-living-memory-slice-5-cross-repo-surfacing) ---
707
+ // A decision made in one of your repos can surface in another — opt-in per
708
+ // decision AND opt-in per repo, signed, verified, and materialized ONLY on
709
+ // explicit human acceptance. Local transport is FREE and zero-network.
710
+ //
711
+ // ⛔ BOTH switches default OFF, and OFF means NOTHING EXISTS — no registry, no
712
+ // keys, no inbox, no behavioural difference. Two INDEPENDENT opt-ins:
713
+ // • enabled — may this repo EXPORT its shareable decisions? (opt-in #1)
714
+ // • subscribe — which repo LABELS may this repo IMPORT from? (opt-in #2)
715
+ // `subscribe: []` means import NOTHING. There is deliberately NO `subscribe: all`
716
+ // — a repo you never named is a repo Massu never reads from.
717
+ share: z.object({
718
+ enabled: z.boolean().default(false),
719
+ subscribe: z.array(z.string()).default([]),
720
+ // C-04 — recall surfacing of cross-repo memories. `enabled` defaults true but is
721
+ // CONDITIONAL on `subscribe` being non-empty (empty by default), so the effective
722
+ // default is DORMANT: with `subscribe: []` the recall hook output is byte-identical
723
+ // to today's. `maxCrossRepoItems` caps how many accepted cross-repo items may appear
724
+ // per recall block (default 1); `minScore` is an OPTIONAL strictly-higher floor for
725
+ // cross-repo items (they are, by construction, less relevant than local ones).
726
+ recall: z.object({
727
+ enabled: z.boolean().default(true),
728
+ maxCrossRepoItems: z.number().int().min(0).default(1),
729
+ minScore: z.number().optional()
730
+ }).default({})
575
731
  }).default({})
576
732
  }).optional();
577
733
  }
@@ -579,7 +735,7 @@ var init_config_memory_schema = __esm({
579
735
 
580
736
  // src/config.ts
581
737
  import { resolve as resolve2, dirname as dirname3 } from "path";
582
- import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
738
+ import { existsSync as existsSync4, readFileSync as readFileSync4 } from "fs";
583
739
  import { homedir as homedir2 } from "os";
584
740
  import { parse as parseYaml } from "yaml";
585
741
  import { z as z2 } from "zod";
@@ -587,7 +743,7 @@ function findProjectRoot() {
587
743
  const cwd = process.cwd();
588
744
  let dir = cwd;
589
745
  while (true) {
590
- if (existsSync3(resolve2(dir, "massu.config.yaml"))) {
746
+ if (existsSync4(resolve2(dir, "massu.config.yaml"))) {
591
747
  return dir;
592
748
  }
593
749
  const parent = dirname3(dir);
@@ -596,10 +752,10 @@ function findProjectRoot() {
596
752
  }
597
753
  dir = cwd;
598
754
  while (true) {
599
- if (existsSync3(resolve2(dir, "package.json"))) {
755
+ if (existsSync4(resolve2(dir, "package.json"))) {
600
756
  return dir;
601
757
  }
602
- if (existsSync3(resolve2(dir, ".git"))) {
758
+ if (existsSync4(resolve2(dir, ".git"))) {
603
759
  return dir;
604
760
  }
605
761
  const parent = dirname3(dir);
@@ -619,8 +775,8 @@ function getConfig() {
619
775
  const root = getProjectRoot();
620
776
  const configPath = resolve2(root, "massu.config.yaml");
621
777
  let rawYaml = {};
622
- if (existsSync3(configPath)) {
623
- const content = readFileSync3(configPath, "utf-8");
778
+ if (existsSync4(configPath)) {
779
+ const content = readFileSync4(configPath, "utf-8");
624
780
  rawYaml = parseYaml(content) ?? {};
625
781
  }
626
782
  const result = RawConfigSchema.safeParse(rawYaml);
@@ -748,6 +904,7 @@ function getResolvedPaths() {
748
904
  sessionStatePath: resolve2(root, config.conventions?.sessionStatePath ?? `${claudeDirName}/session-state/CURRENT.md`),
749
905
  sessionArchivePath: resolve2(root, config.conventions?.sessionArchivePath ?? `${claudeDirName}/session-state/archive`),
750
906
  mcpJsonPath: resolve2(root, ".mcp.json"),
907
+ settingsPath: resolve2(root, claudeDirName, "settings.json"),
751
908
  settingsLocalPath: resolve2(root, claudeDirName, "settings.local.json")
752
909
  };
753
910
  }
@@ -825,7 +982,6 @@ var init_config = __esm({
825
982
  custom_patterns: z2.array(CustomPatternSchema).default([])
826
983
  }).optional(),
827
984
  adr: z2.object({
828
- detection_phrases: z2.array(z2.string()).default(["chose", "decided", "switching to", "moving from", "going with"]),
829
985
  template: z2.string().default("default"),
830
986
  storage: z2.string().default("database"),
831
987
  output_dir: z2.string().default("docs/adr")
@@ -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,21 +1559,21 @@ var init_rule_candidate_store = __esm({
1403
1559
  });
1404
1560
 
1405
1561
  // src/hooks/lib/hook-failure-signal.ts
1406
- import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync5, existsSync as existsSync5 } from "fs";
1407
- import { join as join3, dirname as dirname5 } from "path";
1562
+ import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync5, existsSync as existsSync6 } from "fs";
1563
+ import { join as join4, dirname as dirname5 } from "path";
1408
1564
  function resolveFailureLogPath() {
1409
1565
  const explicit = process.env.MASSU_HOOK_FAILURE_LOG;
1410
1566
  if (explicit) return explicit;
1411
1567
  let dir = process.cwd();
1412
1568
  for (let i = 0; i < 12; i++) {
1413
- if (existsSync5(join3(dir, ".massu")) || existsSync5(join3(dir, "massu.config.yaml"))) {
1414
- return join3(dir, ".massu", "hook-failures.jsonl");
1569
+ if (existsSync6(join4(dir, ".massu")) || existsSync6(join4(dir, "massu.config.yaml"))) {
1570
+ return join4(dir, ".massu", "hook-failures.jsonl");
1415
1571
  }
1416
1572
  const parent = dirname5(dir);
1417
1573
  if (parent === dir) break;
1418
1574
  dir = parent;
1419
1575
  }
1420
- return join3(process.cwd(), ".massu", "hook-failures.jsonl");
1576
+ return join4(process.cwd(), ".massu", "hook-failures.jsonl");
1421
1577
  }
1422
1578
  function recordHookFailure(hook, error, context) {
1423
1579
  const record = {
@@ -1474,9 +1630,9 @@ var init_hook_failure_signal = __esm({
1474
1630
  });
1475
1631
 
1476
1632
  // src/memory-embedder-tokenizer.ts
1477
- import { readFileSync as readFileSync4 } from "fs";
1633
+ import { readFileSync as readFileSync5 } from "fs";
1478
1634
  function loadVocab(vocabPath) {
1479
- const lines = readFileSync4(vocabPath, "utf-8").split("\n");
1635
+ const lines = readFileSync5(vocabPath, "utf-8").split("\n");
1480
1636
  const vocab = /* @__PURE__ */ new Map();
1481
1637
  for (let i = 0; i < lines.length; i++) {
1482
1638
  const tok = lines[i].replace(/\r$/, "");
@@ -1596,9 +1752,9 @@ var init_memory_embedder_tokenizer = __esm({
1596
1752
 
1597
1753
  // src/memory-embedder.ts
1598
1754
  import { fileURLToPath } from "url";
1599
- import { dirname as dirname6, join as join4 } from "path";
1600
- import { existsSync as existsSync6 } from "fs";
1601
- import { createRequire as createRequire2 } from "module";
1755
+ import { dirname as dirname6, join as join5 } from "path";
1756
+ import { existsSync as existsSync7 } from "fs";
1757
+ import { createRequire as createRequire3 } from "module";
1602
1758
  function getActiveEmbedModel() {
1603
1759
  return _activeModel;
1604
1760
  }
@@ -1622,7 +1778,7 @@ function resolveModelDir() {
1622
1778
  let dir = dirname6(fileURLToPath(import.meta.url));
1623
1779
  let root = null;
1624
1780
  for (let i = 0; i < 8; i++) {
1625
- if (existsSync6(join4(dir, "package.json"))) {
1781
+ if (existsSync7(join5(dir, "package.json"))) {
1626
1782
  root = dir;
1627
1783
  break;
1628
1784
  }
@@ -1632,8 +1788,8 @@ function resolveModelDir() {
1632
1788
  }
1633
1789
  if (!root) return null;
1634
1790
  for (const rel of ["dist/embedder", "assets/embedder"]) {
1635
- const candidate = join4(root, rel);
1636
- if (existsSync6(join4(candidate, "model_quantized.onnx"))) return candidate;
1791
+ const candidate = join5(root, rel);
1792
+ if (existsSync7(join5(candidate, "model_quantized.onnx"))) return candidate;
1637
1793
  }
1638
1794
  return null;
1639
1795
  } catch {
@@ -1642,8 +1798,8 @@ function resolveModelDir() {
1642
1798
  }
1643
1799
  function resolveWasmDir() {
1644
1800
  try {
1645
- const req2 = createRequire2(import.meta.url);
1646
- const main2 = req2.resolve(ORT_PKG);
1801
+ const req3 = createRequire3(import.meta.url);
1802
+ const main2 = req3.resolve(ORT_PKG);
1647
1803
  return dirname6(main2);
1648
1804
  } catch {
1649
1805
  return null;
@@ -1664,8 +1820,8 @@ async function getSession() {
1664
1820
  ort.env.wasm.proxy = false;
1665
1821
  ort.env.wasm.wasmPaths = wasmDir.endsWith("/") ? wasmDir : wasmDir + "/";
1666
1822
  _ortTensor = ort.Tensor;
1667
- const modelPath = join4(modelDir, "model_quantized.onnx");
1668
- const vocabPath = join4(modelDir, "vocab.txt");
1823
+ const modelPath = join5(modelDir, "model_quantized.onnx");
1824
+ const vocabPath = join5(modelDir, "vocab.txt");
1669
1825
  _vocab = loadVocab(vocabPath);
1670
1826
  const session = await ort.InferenceSession.create(modelPath, {
1671
1827
  executionProviders: ["wasm"],
@@ -1986,6 +2142,7 @@ __export(memory_db_exports, {
1986
2142
  migrateAuditLogCheckExtension: () => migrateAuditLogCheckExtension,
1987
2143
  migrateMemoryFilesFor4B: () => migrateMemoryFilesFor4B,
1988
2144
  migrateObservationEmbeddingChunks: () => migrateObservationEmbeddingChunks,
2145
+ migrateSharedMemoryFor5B: () => migrateSharedMemoryFor5B,
1989
2146
  pruneOldConversationTurns: () => pruneOldConversationTurns,
1990
2147
  pruneOldObservations: () => pruneOldObservations,
1991
2148
  pruneToolCostEvents: () => pruneToolCostEvents,
@@ -2003,7 +2160,7 @@ __export(memory_db_exports, {
2003
2160
  usageWarmupElapsed: () => usageWarmupElapsed
2004
2161
  });
2005
2162
  import { dirname as dirname7, basename as basename2 } from "path";
2006
- import { existsSync as existsSync7, mkdirSync as mkdirSync6 } from "fs";
2163
+ import { existsSync as existsSync8, mkdirSync as mkdirSync6 } from "fs";
2007
2164
  function sanitizeFts5Query(raw) {
2008
2165
  const trimmed = raw.trim();
2009
2166
  if (!trimmed) return '""';
@@ -2020,11 +2177,11 @@ function sanitizeFts5QueryOr(raw) {
2020
2177
  function getMemoryDb() {
2021
2178
  const dbPath = getResolvedPaths().memoryDbPath;
2022
2179
  const dir = dirname7(dbPath);
2023
- if (!existsSync7(dir)) {
2180
+ if (!existsSync8(dir)) {
2024
2181
  mkdirSync6(dir, { recursive: true });
2025
2182
  }
2026
- const preExisting = existsSync7(dbPath);
2027
- const db = openDatabase(dbPath);
2183
+ const preExisting = existsSync8(dbPath);
2184
+ const db = openDatabase2(dbPath);
2028
2185
  db.pragma("journal_mode = WAL");
2029
2186
  db.pragma("foreign_keys = ON");
2030
2187
  const onDisk = db.pragma("user_version", { simple: true });
@@ -2100,6 +2257,39 @@ function migrateMemoryFilesFor4B(db) {
2100
2257
  db.exec(`ALTER TABLE ${add.table} ADD COLUMN ${add.name} ${add.decl}`);
2101
2258
  }
2102
2259
  }
2260
+ function migrateSharedMemoryFor5B(db) {
2261
+ const cols = db.prepare(`PRAGMA table_info(observations)`).all();
2262
+ if (cols.length > 0 && !cols.some((c) => c.name === "shareable")) {
2263
+ db.exec(`ALTER TABLE observations ADD COLUMN shareable INTEGER NOT NULL DEFAULT 0`);
2264
+ }
2265
+ db.exec(`
2266
+ CREATE TABLE IF NOT EXISTS shared_memory_pending (
2267
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
2268
+ record_hash TEXT NOT NULL UNIQUE, -- identity + idempotency key (hex sha256)
2269
+ origin_repo_id TEXT NOT NULL, -- the signed origin repo_id (v4 UUID)
2270
+ origin_repo_label TEXT NOT NULL, -- re-slugged on read; NEVER trusted from the wire
2271
+ envelope_raw TEXT NOT NULL, -- the VERBATIM signed bytes; B-05 re-verifies these
2272
+ record_json TEXT NOT NULL, -- the single record's canonical JSON
2273
+ received_at_epoch INTEGER NOT NULL, -- epoch SECONDS (Slice-2 convention)
2274
+ accepted_at_epoch INTEGER, -- set on accept (B-05); NULL while pending
2275
+ refused_at_epoch INTEGER, -- set on refuse (B-06)
2276
+ expired_at_epoch INTEGER -- revocation \u21D2 EXPIRE, never DELETE (B-07 / S-3)
2277
+ );
2278
+ CREATE INDEX IF NOT EXISTS idx_smp_origin ON shared_memory_pending(origin_repo_id);
2279
+ CREATE INDEX IF NOT EXISTS idx_smp_received ON shared_memory_pending(received_at_epoch DESC);
2280
+ `);
2281
+ db.exec(`
2282
+ CREATE TABLE IF NOT EXISTS shared_memory_outbound (
2283
+ record_hash TEXT PRIMARY KEY, -- the exported record's canonical hash
2284
+ observation_id INTEGER NOT NULL, -- the source observations.id (for B-07 revocation)
2285
+ origin_repo_id TEXT NOT NULL, -- this repo's own repo_id at export time
2286
+ seq INTEGER NOT NULL, -- the envelope seq this record first went out in
2287
+ exported_at_epoch INTEGER NOT NULL, -- epoch SECONDS
2288
+ revoked_at_epoch INTEGER -- set when the source row is superseded/expired (B-07)
2289
+ );
2290
+ CREATE INDEX IF NOT EXISTS idx_smo_obs ON shared_memory_outbound(observation_id);
2291
+ `);
2292
+ }
2103
2293
  function migrateAuditLogCheckExtension(db) {
2104
2294
  const row = db.prepare("SELECT sql FROM sqlite_master WHERE type='table' AND name='audit_log'").get();
2105
2295
  if (!row) return;
@@ -2122,7 +2312,16 @@ function migrateAuditLogCheckExtension(db) {
2122
2312
  "memory_file_adopted_human",
2123
2313
  "memory_file_rendered",
2124
2314
  "memory_file_render_refused",
2125
- "memory_file_tombstoned"
2315
+ "memory_file_tombstoned",
2316
+ // B-10 (Slice 5) — the seven cross-repo shared-memory events. An existing DB whose
2317
+ // CHECK lacks any of these triggers the table rebuild below.
2318
+ "shared_memory_exported",
2319
+ "shared_memory_export_refused",
2320
+ "shared_memory_imported",
2321
+ "shared_memory_dropped",
2322
+ "shared_memory_accepted",
2323
+ "shared_memory_refused",
2324
+ "shared_memory_revoked"
2126
2325
  ];
2127
2326
  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);
2128
2327
  if (checkClauseMatch) {
@@ -2141,7 +2340,12 @@ function migrateAuditLogCheckExtension(db) {
2141
2340
  'code_change', 'rule_enforced', 'approval', 'review', 'commit', 'compaction',
2142
2341
  'rule_candidate_emitted', 'rule_promoted', 'rule_dismissed',
2143
2342
  'memory_file_ingested', 'memory_file_expired', 'memory_file_adopted_human',
2144
- 'memory_file_rendered', 'memory_file_render_refused', 'memory_file_tombstoned'
2343
+ 'memory_file_rendered', 'memory_file_render_refused', 'memory_file_tombstoned',
2344
+ -- B-10 (Slice 5) \u2014 cross-repo shared-memory events (must mirror the CREATE +
2345
+ -- the expected[] list above, or the migration would loop rebuilding forever).
2346
+ 'shared_memory_exported', 'shared_memory_export_refused', 'shared_memory_imported',
2347
+ 'shared_memory_dropped', 'shared_memory_accepted', 'shared_memory_refused',
2348
+ 'shared_memory_revoked'
2145
2349
  )),
2146
2350
  actor TEXT NOT NULL DEFAULT 'ai' CHECK(actor IN ('ai', 'human', 'hook', 'agent')),
2147
2351
  model_id TEXT,
@@ -2218,6 +2422,12 @@ function initMemorySchema(db) {
2218
2422
  -- credential, or takes a snapshot. A Slice-5 synced memory arrives here with
2219
2423
  -- origin='team' and must never reach disk without CR-55's gates.
2220
2424
  origin TEXT NOT NULL DEFAULT 'local',
2425
+ -- B-01 (Slice 5, cross-repo surfacing): the per-decision SHARE opt-in. Set to 1
2426
+ -- ONLY by an explicit human act (massu memory share <id>); NEVER by a heuristic,
2427
+ -- score, LLM, hook, or consolidation pass. Export (B-02) reads WHERE shareable=1
2428
+ -- AND origin='local'. Defaults 0 so every pre-existing and machine-written row is
2429
+ -- un-shareable until a human says otherwise (fail-closed).
2430
+ shareable INTEGER NOT NULL DEFAULT 0,
2221
2431
  created_at TEXT NOT NULL,
2222
2432
  created_at_epoch INTEGER NOT NULL,
2223
2433
  FOREIGN KEY(session_id) REFERENCES sessions(session_id) ON DELETE CASCADE
@@ -2491,7 +2701,13 @@ function initMemorySchema(db) {
2491
2701
  'code_change', 'rule_enforced', 'approval', 'review', 'commit', 'compaction',
2492
2702
  'rule_candidate_emitted', 'rule_promoted', 'rule_dismissed',
2493
2703
  'memory_file_ingested', 'memory_file_expired', 'memory_file_adopted_human',
2494
- 'memory_file_rendered', 'memory_file_render_refused', 'memory_file_tombstoned'
2704
+ 'memory_file_rendered', 'memory_file_render_refused', 'memory_file_tombstoned',
2705
+ -- B-10 (Slice 5): cross-repo shared-memory observability. Without these seven,
2706
+ -- nobody could ever answer "what has crossed between my repos?" (and an ingest
2707
+ -- catch would swallow the CHECK violation, silently producing NOTHING).
2708
+ 'shared_memory_exported', 'shared_memory_export_refused', 'shared_memory_imported',
2709
+ 'shared_memory_dropped', 'shared_memory_accepted', 'shared_memory_refused',
2710
+ 'shared_memory_revoked'
2495
2711
  )),
2496
2712
  actor TEXT NOT NULL DEFAULT 'ai' CHECK(actor IN ('ai', 'human', 'hook', 'agent')),
2497
2713
  model_id TEXT,
@@ -2860,6 +3076,7 @@ function initMemorySchema(db) {
2860
3076
  `);
2861
3077
  migrateObservationEmbeddingChunks(db);
2862
3078
  migrateMemoryFilesFor4B(db);
3079
+ migrateSharedMemoryFor5B(db);
2863
3080
  ensureRuleCandidatesTable(db);
2864
3081
  migrateRuleDelivery(db);
2865
3082
  }
@@ -3730,7 +3947,7 @@ var MEMORY_SCHEMA_VERSION, TOOL_COST_EVENTS_RETENTION_DAYS, MAX_DRAFT_TEXT_LEN,
3730
3947
  var init_memory_db = __esm({
3731
3948
  "src/memory-db.ts"() {
3732
3949
  "use strict";
3733
- init_sqlite_loader();
3950
+ init_db_driver();
3734
3951
  init_config();
3735
3952
  init_memory_vector();
3736
3953
  init_db_backup();
@@ -3760,68 +3977,6 @@ function estimateTokens(text) {
3760
3977
 
3761
3978
  // src/observation-extractor.ts
3762
3979
  init_memory_db();
3763
-
3764
- // src/adr-generator.ts
3765
- init_config();
3766
- var DEFAULT_DETECTION_PHRASES = ["chose", "decided", "switching to", "moving from", "going with"];
3767
- function getDetectionPhrases() {
3768
- return getConfig().governance?.adr?.detection_phrases ?? DEFAULT_DETECTION_PHRASES;
3769
- }
3770
- function detectDecisionPatterns(text) {
3771
- const phrases = getDetectionPhrases();
3772
- const lower = text.toLowerCase();
3773
- return phrases.some((phrase) => lower.includes(phrase));
3774
- }
3775
- function extractAlternatives(description) {
3776
- const alternatives = [];
3777
- const overMatch = description.match(/(\w[\w\s-]+)\s+over\s+(\w[\w\s-]+)/i);
3778
- if (overMatch) {
3779
- alternatives.push(overMatch[1].trim());
3780
- alternatives.push(overMatch[2].trim());
3781
- }
3782
- const insteadMatch = description.match(/(\w[\w\s-]+)\s+instead\s+of\s+(\w[\w\s-]+)/i);
3783
- if (insteadMatch) {
3784
- alternatives.push(insteadMatch[1].trim());
3785
- alternatives.push(insteadMatch[2].trim());
3786
- }
3787
- const switchMatch = description.match(/switching\s+from\s+(\w[\w\s-]+)\s+to\s+(\w[\w\s-]+)/i);
3788
- if (switchMatch) {
3789
- alternatives.push(switchMatch[2].trim());
3790
- alternatives.push(switchMatch[1].trim());
3791
- }
3792
- return [...new Set(alternatives)];
3793
- }
3794
- function storeDecision(db, decision) {
3795
- const existing = new Set(
3796
- db.prepare(`PRAGMA table_info(architecture_decisions)`).all().map(
3797
- (c) => c.name
3798
- )
3799
- );
3800
- const cols = ["title", "context", "decision", "alternatives", "consequences", "session_id", "status"];
3801
- const vals = [
3802
- decision.title,
3803
- decision.context,
3804
- decision.decision,
3805
- JSON.stringify(decision.alternatives),
3806
- decision.consequences,
3807
- decision.sessionId ?? null,
3808
- decision.status ?? "accepted"
3809
- ];
3810
- if (existing.has("affected_files")) {
3811
- cols.push("affected_files");
3812
- vals.push(decision.affectedFiles ? JSON.stringify(decision.affectedFiles) : null);
3813
- }
3814
- const hasTemporal = existing.has("valid_from_epoch");
3815
- const extraSql = hasTemporal ? `, valid_from, ingested_at, valid_from_epoch, ingested_at_epoch` : "";
3816
- const extraVals = hasTemporal ? `, datetime('now'), datetime('now'), CAST(strftime('%s','now') AS INTEGER), CAST(strftime('%s','now') AS INTEGER)` : "";
3817
- const result = db.prepare(
3818
- `INSERT INTO architecture_decisions (${cols.join(", ")}${extraSql})
3819
- VALUES (${cols.map(() => "?").join(", ")}${extraVals})`
3820
- ).run(...vals);
3821
- return Number(result.lastInsertRowid);
3822
- }
3823
-
3824
- // src/observation-extractor.ts
3825
3980
  init_config();
3826
3981
  import { homedir as homedir4 } from "os";
3827
3982
 
@@ -4124,23 +4279,6 @@ function classifyRealTimeToolCall(toolName, toolInput, toolResponse, seenReads2,
4124
4279
  isError: isError ?? normalized.isError
4125
4280
  };
4126
4281
  if (isNoisyToolCall(tc, seenReads2)) return null;
4127
- const responseText = normalized.text;
4128
- if (responseText && detectDecisionPatterns(responseText)) {
4129
- const firstLine = responseText.split("\n")[0].slice(0, 200);
4130
- const title = `Architecture decision: ${firstLine}`;
4131
- const detail = responseText.slice(0, 1e3);
4132
- return {
4133
- type: "decision",
4134
- title,
4135
- detail,
4136
- visibility: classifyVisibility(title, detail),
4137
- opts: {
4138
- importance: assignImportance("decision"),
4139
- originalTokens: estimateTokens(responseText),
4140
- ...extractLinkedReferences(responseText)
4141
- }
4142
- };
4143
- }
4144
4282
  return classifyToolCall(tc);
4145
4283
  }
4146
4284
  function detectPlanProgress(rawToolResponse) {
@@ -4257,8 +4395,8 @@ init_config();
4257
4395
 
4258
4396
  // src/import-resolver.ts
4259
4397
  init_config();
4260
- import { readFileSync as readFileSync5, existsSync as existsSync8, statSync as statSync2 } from "fs";
4261
- import { resolve as resolve6, dirname as dirname8, join as join5 } from "path";
4398
+ import { readFileSync as readFileSync6, existsSync as existsSync9, statSync as statSync2 } from "fs";
4399
+ import { resolve as resolve6, dirname as dirname8, join as join6 } from "path";
4262
4400
 
4263
4401
  // src/security-utils.ts
4264
4402
  import { resolve as resolve5, normalize } from "path";
@@ -4321,19 +4459,19 @@ function resolveImportPath(specifier, fromFile) {
4321
4459
  } else {
4322
4460
  basePath = resolve6(dirname8(fromFile), specifier);
4323
4461
  }
4324
- if (existsSync8(basePath) && !isDirectory(basePath)) {
4462
+ if (existsSync9(basePath) && !isDirectory(basePath)) {
4325
4463
  return toRelative(basePath);
4326
4464
  }
4327
4465
  const resolvedPaths = getResolvedPaths();
4328
4466
  for (const ext of resolvedPaths.extensions) {
4329
4467
  const withExt = basePath + ext;
4330
- if (existsSync8(withExt)) {
4468
+ if (existsSync9(withExt)) {
4331
4469
  return toRelative(withExt);
4332
4470
  }
4333
4471
  }
4334
4472
  for (const indexFile of resolvedPaths.indexFiles) {
4335
- const indexPath = join5(basePath, indexFile);
4336
- if (existsSync8(indexPath)) {
4473
+ const indexPath = join6(basePath, indexFile);
4474
+ if (existsSync9(indexPath)) {
4337
4475
  return toRelative(indexPath);
4338
4476
  }
4339
4477
  }
@@ -4355,7 +4493,7 @@ function toRelative(absPath) {
4355
4493
  }
4356
4494
 
4357
4495
  // src/validation-engine.ts
4358
- import { existsSync as existsSync9, readFileSync as readFileSync6 } from "fs";
4496
+ import { existsSync as existsSync10, readFileSync as readFileSync7 } from "fs";
4359
4497
  function getValidationChecks() {
4360
4498
  return getConfig().governance?.validation?.checks ?? {
4361
4499
  rule_compliance: true,
@@ -4383,7 +4521,7 @@ function validateFile(filePath, projectRoot) {
4383
4521
  });
4384
4522
  return checks;
4385
4523
  }
4386
- if (!existsSync9(absPath)) {
4524
+ if (!existsSync10(absPath)) {
4387
4525
  checks.push({
4388
4526
  name: "file_exists",
4389
4527
  severity: "error",
@@ -4392,7 +4530,7 @@ function validateFile(filePath, projectRoot) {
4392
4530
  });
4393
4531
  return checks;
4394
4532
  }
4395
- const source = readFileSync6(absPath, "utf-8");
4533
+ const source = readFileSync7(absPath, "utf-8");
4396
4534
  const lines = source.split("\n");
4397
4535
  if (activeChecks.rule_compliance !== false) {
4398
4536
  for (const ruleSet of config.rules) {
@@ -4491,7 +4629,7 @@ function storeValidationResult(db, filePath, checks, sessionId, validationType =
4491
4629
 
4492
4630
  // src/security-scorer.ts
4493
4631
  init_config();
4494
- import { existsSync as existsSync10, readFileSync as readFileSync7 } from "fs";
4632
+ import { existsSync as existsSync11, readFileSync as readFileSync8 } from "fs";
4495
4633
  var DEFAULT_SECURITY_PATTERNS = [
4496
4634
  {
4497
4635
  regex: /\bexec\s*\(\s*[`"'].*\$\{/,
@@ -4582,12 +4720,12 @@ function scoreFileSecurity(filePath, projectRoot) {
4582
4720
  }]
4583
4721
  };
4584
4722
  }
4585
- if (!existsSync10(absPath)) {
4723
+ if (!existsSync11(absPath)) {
4586
4724
  return { riskScore: 0, findings: [] };
4587
4725
  }
4588
4726
  let source;
4589
4727
  try {
4590
- source = readFileSync7(absPath, "utf-8");
4728
+ source = readFileSync8(absPath, "utf-8");
4591
4729
  } catch {
4592
4730
  return { riskScore: 0, findings: [] };
4593
4731
  }
@@ -4627,8 +4765,8 @@ function storeSecurityScore(db, sessionId, filePath, riskScore, findings) {
4627
4765
 
4628
4766
  // src/hooks/post-tool-use.ts
4629
4767
  init_config();
4630
- import { readFileSync as readFileSync9, writeFileSync as writeFileSync3, existsSync as existsSync12, mkdirSync as mkdirSync7, statSync as statSync3 } from "fs";
4631
- import { join as join6, basename as pathBasename2, dirname as pathDirname, resolve as pathResolve } from "path";
4768
+ import { readFileSync as readFileSync10, writeFileSync as writeFileSync3, existsSync as existsSync13, mkdirSync as mkdirSync7, statSync as statSync3 } from "fs";
4769
+ import { join as join7, basename as pathBasename2, dirname as pathDirname, resolve as pathResolve } from "path";
4632
4770
 
4633
4771
  // src/lib/recurrence-incrementer.ts
4634
4772
  var ANSI = /\x1b\[[0-9;]*m/g;
@@ -4692,7 +4830,7 @@ function incrementRecurrenceCountsForScannerFailures(db, sessionId, scannerStdou
4692
4830
  init_hook_failure_signal();
4693
4831
 
4694
4832
  // src/memory-file-ingest.ts
4695
- import { readFileSync as readFileSync8, existsSync as existsSync11, readdirSync as readdirSync2 } from "fs";
4833
+ import { readFileSync as readFileSync9, existsSync as existsSync12, readdirSync as readdirSync2 } from "fs";
4696
4834
  init_memory_db();
4697
4835
  import { parse as parseYaml2 } from "yaml";
4698
4836
  import { basename as pathBasename } from "path";
@@ -4801,8 +4939,8 @@ function upsertMemoryFileMirror(db, f) {
4801
4939
  return "inserted";
4802
4940
  }
4803
4941
  function ingestMemoryFile(db, sessionId, filePath) {
4804
- if (!existsSync11(filePath)) return "skipped";
4805
- const content = readFileSync8(filePath, "utf-8");
4942
+ if (!existsSync12(filePath)) return "skipped";
4943
+ const content = readFileSync9(filePath, "utf-8");
4806
4944
  if (Buffer.byteLength(content, "utf-8") > MAX_MEMORY_FILE_BYTES) {
4807
4945
  process.stderr.write(
4808
4946
  `[massu] memory file exceeds ${MAX_MEMORY_FILE_BYTES} bytes, not ingested: ${filePath}
@@ -4919,27 +5057,6 @@ async function main() {
4919
5057
  } else {
4920
5058
  addObservation(db, session_id, observation.type, observation.title, observation.detail, observation.opts);
4921
5059
  }
4922
- if (observation.type === "decision") {
4923
- try {
4924
- const decisionText = observation.detail ?? observation.title;
4925
- const alternatives = extractAlternatives(decisionText);
4926
- const existing = db.prepare(
4927
- `SELECT id FROM architecture_decisions WHERE session_id = ? AND title = ? LIMIT 1`
4928
- ).get(session_id, observation.title);
4929
- if (!existing) {
4930
- storeDecision(db, {
4931
- title: observation.title,
4932
- context: "",
4933
- decision: decisionText,
4934
- alternatives,
4935
- consequences: "",
4936
- sessionId: session_id,
4937
- affectedFiles: observation.opts?.filesInvolved
4938
- });
4939
- }
4940
- } catch {
4941
- }
4942
- }
4943
5060
  if (tool_response) {
4944
5061
  const progress = detectPlanProgress(tool_response);
4945
5062
  if (progress.length > 0) {
@@ -5076,10 +5193,10 @@ async function main() {
5076
5193
  } catch (err) {
5077
5194
  try {
5078
5195
  const projectRoot = process.cwd();
5079
- const dir = join6(projectRoot, ".massu", "rule-candidates");
5080
- if (!existsSync12(dir)) mkdirSync7(dir, { recursive: true });
5081
- const logPath = join6(dir, ".cr53-increment-failures.jsonl");
5082
- const pre = existsSync12(logPath) ? readFileSync9(logPath, "utf-8") : "";
5196
+ const dir = join7(projectRoot, ".massu", "rule-candidates");
5197
+ if (!existsSync13(dir)) mkdirSync7(dir, { recursive: true });
5198
+ const logPath = join7(dir, ".cr53-increment-failures.jsonl");
5199
+ const pre = existsSync13(logPath) ? readFileSync10(logPath, "utf-8") : "";
5083
5200
  const sep = pre && !pre.endsWith("\n") ? "\n" : "";
5084
5201
  writeFileSync3(logPath, pre + sep + JSON.stringify({
5085
5202
  session_id,
@@ -5184,13 +5301,13 @@ var _conventionDefaults = {
5184
5301
  function readConventions(cwd) {
5185
5302
  try {
5186
5303
  const projectRoot = cwd ?? process.cwd();
5187
- const configPath = join6(projectRoot, "massu.config.yaml");
5188
- if (!existsSync12(configPath)) return _conventionDefaults;
5304
+ const configPath = join7(projectRoot, "massu.config.yaml");
5305
+ if (!existsSync13(configPath)) return _conventionDefaults;
5189
5306
  const mtimeMs = statSync3(configPath).mtimeMs;
5190
5307
  if (_cachedConventions !== null && _cachedConventionsPath === configPath && _cachedConventionsMtimeMs === mtimeMs) {
5191
5308
  return _cachedConventions;
5192
5309
  }
5193
- const content = readFileSync9(configPath, "utf-8");
5310
+ const content = readFileSync10(configPath, "utf-8");
5194
5311
  const parsed = parseYaml3(content);
5195
5312
  if (!parsed || typeof parsed !== "object") return _conventionDefaults;
5196
5313
  const conventions = parsed.conventions;
@@ -5222,11 +5339,11 @@ function isKnowledgeSourceFile(filePath) {
5222
5339
  function checkMemoryFileIntegrity(filePath) {
5223
5340
  const issues = [];
5224
5341
  try {
5225
- if (!existsSync12(filePath)) {
5342
+ if (!existsSync13(filePath)) {
5226
5343
  issues.push("MEMORY.md file does not exist after write");
5227
5344
  return issues;
5228
5345
  }
5229
- const content = readFileSync9(filePath, "utf-8");
5346
+ const content = readFileSync10(filePath, "utf-8");
5230
5347
  const lines = content.split("\n");
5231
5348
  const MAX_LINES = 200;
5232
5349
  if (lines.length > MAX_LINES) {