@massu/core 1.16.3 → 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -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);
@@ -825,7 +981,6 @@ var init_config = __esm({
825
981
  custom_patterns: z2.array(CustomPatternSchema).default([])
826
982
  }).optional(),
827
983
  adr: z2.object({
828
- detection_phrases: z2.array(z2.string()).default(["chose", "decided", "switching to", "moving from", "going with"]),
829
984
  template: z2.string().default("default"),
830
985
  storage: z2.string().default("database"),
831
986
  output_dir: z2.string().default("docs/adr")
@@ -1179,26 +1334,26 @@ var init_memory_vector = __esm({
1179
1334
  });
1180
1335
 
1181
1336
  // src/db-backup.ts
1182
- import { existsSync as existsSync4, mkdirSync as mkdirSync4, readdirSync, statSync, unlinkSync, copyFileSync, rmSync as rmSync3 } from "fs";
1183
- import { resolve as resolve3, join as join2, basename, dirname as dirname4 } from "path";
1337
+ import { existsSync as existsSync5, mkdirSync as mkdirSync4, readdirSync, statSync, unlinkSync, copyFileSync, rmSync as rmSync4 } from "fs";
1338
+ import { resolve as resolve3, join as join3, basename, dirname as dirname4 } from "path";
1184
1339
  import { homedir as homedir3 } from "os";
1185
1340
  function dbBackupsRoot(home = homedir3()) {
1186
1341
  return resolve3(home, ".massu", "db-backups");
1187
1342
  }
1188
1343
  function projectBackupDir(projectRoot, home = homedir3()) {
1189
1344
  const slug = basename(projectRoot).replace(/[^A-Za-z0-9._-]/g, "_") || "project";
1190
- return join2(dbBackupsRoot(home), slug);
1345
+ return join3(dbBackupsRoot(home), slug);
1191
1346
  }
1192
1347
  function backupStamp(nowMs) {
1193
1348
  return new Date(nowMs).toISOString().replace(/[:.]/g, "-");
1194
1349
  }
1195
1350
  function listDbBackups(projectRoot, home = homedir3()) {
1196
1351
  const dir = projectBackupDir(projectRoot, home);
1197
- if (!existsSync4(dir)) return [];
1352
+ if (!existsSync5(dir)) return [];
1198
1353
  const out = [];
1199
1354
  for (const f of readdirSync(dir)) {
1200
1355
  if (!f.endsWith(".db")) continue;
1201
- const p = join2(dir, f);
1356
+ const p = join3(dir, f);
1202
1357
  const st = statSync(p);
1203
1358
  const db = f.replace(/-\d{4}-\d{2}-\d{2}T.*$/, "");
1204
1359
  out.push({ db, path: p, bytes: st.size, mtimeMs: st.mtimeMs });
@@ -1211,13 +1366,13 @@ function hasFreshDbBackup(projectRoot, dbPath, nowMs = Date.now(), home = homedi
1211
1366
  if (backups.length === 0) return false;
1212
1367
  const newest = backups[0];
1213
1368
  if (nowMs - newest.mtimeMs > FRESH_WINDOW_MS) return false;
1214
- if (existsSync4(dbPath) && statSync(dbPath).mtimeMs > newest.mtimeMs) return false;
1369
+ if (existsSync5(dbPath) && statSync(dbPath).mtimeMs > newest.mtimeMs) return false;
1215
1370
  return true;
1216
1371
  }
1217
1372
  function integrityOk(dbPath) {
1218
1373
  let db = null;
1219
1374
  try {
1220
- db = openDatabase(dbPath, { readonly: true });
1375
+ db = openDatabase2(dbPath, { readonly: true });
1221
1376
  const rows = db.pragma("integrity_check");
1222
1377
  return rows.length === 1 && rows[0].integrity_check === "ok";
1223
1378
  } catch {
@@ -1230,16 +1385,16 @@ function integrityOk(dbPath) {
1230
1385
  }
1231
1386
  }
1232
1387
  function backupDb(projectRoot, dbPath, nowMs = Date.now(), home = homedir3()) {
1233
- if (!existsSync4(dbPath)) {
1388
+ if (!existsSync5(dbPath)) {
1234
1389
  throw new DbBackupError(`cannot back up a database that does not exist: ${dbPath}`);
1235
1390
  }
1236
1391
  const dir = projectBackupDir(projectRoot, home);
1237
1392
  mkdirSync4(dir, { recursive: true });
1238
1393
  const name = basename(dbPath, ".db");
1239
- const dest = join2(dir, `${name}-${backupStamp(nowMs)}.db`);
1394
+ const dest = join3(dir, `${name}-${backupStamp(nowMs)}.db`);
1240
1395
  let src = null;
1241
1396
  try {
1242
- src = openDatabase(dbPath, { readonly: true });
1397
+ src = openDatabase2(dbPath, { readonly: true });
1243
1398
  src.exec(`VACUUM INTO '${dest.replace(/'/g, "''")}'`);
1244
1399
  } catch (err) {
1245
1400
  throw new DbBackupError(
@@ -1278,7 +1433,7 @@ function pruneDbBackups(projectRoot, dbName, keep = DEFAULT_RETENTION, home = ho
1278
1433
  }
1279
1434
  function backupBeforeSchemaChange(projectRoot, dbPath, onError, nowMs = Date.now(), home = homedir3()) {
1280
1435
  try {
1281
- if (!existsSync4(dbPath)) return null;
1436
+ if (!existsSync5(dbPath)) return null;
1282
1437
  if (hasFreshDbBackup(projectRoot, dbPath, nowMs, home)) return null;
1283
1438
  return backupDb(projectRoot, dbPath, nowMs, home);
1284
1439
  } catch (err) {
@@ -1290,7 +1445,7 @@ var DEFAULT_RETENTION, FRESH_WINDOW_MS, DbBackupError;
1290
1445
  var init_db_backup = __esm({
1291
1446
  "src/db-backup.ts"() {
1292
1447
  "use strict";
1293
- init_sqlite_loader();
1448
+ init_db_driver();
1294
1449
  DEFAULT_RETENTION = 5;
1295
1450
  FRESH_WINDOW_MS = 24 * 60 * 60 * 1e3;
1296
1451
  DbBackupError = class extends Error {
@@ -1403,21 +1558,21 @@ var init_rule_candidate_store = __esm({
1403
1558
  });
1404
1559
 
1405
1560
  // 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";
1561
+ import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync5, existsSync as existsSync6 } from "fs";
1562
+ import { join as join4, dirname as dirname5 } from "path";
1408
1563
  function resolveFailureLogPath() {
1409
1564
  const explicit = process.env.MASSU_HOOK_FAILURE_LOG;
1410
1565
  if (explicit) return explicit;
1411
1566
  let dir = process.cwd();
1412
1567
  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");
1568
+ if (existsSync6(join4(dir, ".massu")) || existsSync6(join4(dir, "massu.config.yaml"))) {
1569
+ return join4(dir, ".massu", "hook-failures.jsonl");
1415
1570
  }
1416
1571
  const parent = dirname5(dir);
1417
1572
  if (parent === dir) break;
1418
1573
  dir = parent;
1419
1574
  }
1420
- return join3(process.cwd(), ".massu", "hook-failures.jsonl");
1575
+ return join4(process.cwd(), ".massu", "hook-failures.jsonl");
1421
1576
  }
1422
1577
  function recordHookFailure(hook, error, context) {
1423
1578
  const record = {
@@ -1474,9 +1629,9 @@ var init_hook_failure_signal = __esm({
1474
1629
  });
1475
1630
 
1476
1631
  // src/memory-embedder-tokenizer.ts
1477
- import { readFileSync as readFileSync4 } from "fs";
1632
+ import { readFileSync as readFileSync5 } from "fs";
1478
1633
  function loadVocab(vocabPath) {
1479
- const lines = readFileSync4(vocabPath, "utf-8").split("\n");
1634
+ const lines = readFileSync5(vocabPath, "utf-8").split("\n");
1480
1635
  const vocab = /* @__PURE__ */ new Map();
1481
1636
  for (let i = 0; i < lines.length; i++) {
1482
1637
  const tok = lines[i].replace(/\r$/, "");
@@ -1596,9 +1751,9 @@ var init_memory_embedder_tokenizer = __esm({
1596
1751
 
1597
1752
  // src/memory-embedder.ts
1598
1753
  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";
1754
+ import { dirname as dirname6, join as join5 } from "path";
1755
+ import { existsSync as existsSync7 } from "fs";
1756
+ import { createRequire as createRequire3 } from "module";
1602
1757
  function getActiveEmbedModel() {
1603
1758
  return _activeModel;
1604
1759
  }
@@ -1622,7 +1777,7 @@ function resolveModelDir() {
1622
1777
  let dir = dirname6(fileURLToPath(import.meta.url));
1623
1778
  let root = null;
1624
1779
  for (let i = 0; i < 8; i++) {
1625
- if (existsSync6(join4(dir, "package.json"))) {
1780
+ if (existsSync7(join5(dir, "package.json"))) {
1626
1781
  root = dir;
1627
1782
  break;
1628
1783
  }
@@ -1632,8 +1787,8 @@ function resolveModelDir() {
1632
1787
  }
1633
1788
  if (!root) return null;
1634
1789
  for (const rel of ["dist/embedder", "assets/embedder"]) {
1635
- const candidate = join4(root, rel);
1636
- if (existsSync6(join4(candidate, "model_quantized.onnx"))) return candidate;
1790
+ const candidate = join5(root, rel);
1791
+ if (existsSync7(join5(candidate, "model_quantized.onnx"))) return candidate;
1637
1792
  }
1638
1793
  return null;
1639
1794
  } catch {
@@ -1642,8 +1797,8 @@ function resolveModelDir() {
1642
1797
  }
1643
1798
  function resolveWasmDir() {
1644
1799
  try {
1645
- const req2 = createRequire2(import.meta.url);
1646
- const main2 = req2.resolve(ORT_PKG);
1800
+ const req3 = createRequire3(import.meta.url);
1801
+ const main2 = req3.resolve(ORT_PKG);
1647
1802
  return dirname6(main2);
1648
1803
  } catch {
1649
1804
  return null;
@@ -1664,8 +1819,8 @@ async function getSession() {
1664
1819
  ort.env.wasm.proxy = false;
1665
1820
  ort.env.wasm.wasmPaths = wasmDir.endsWith("/") ? wasmDir : wasmDir + "/";
1666
1821
  _ortTensor = ort.Tensor;
1667
- const modelPath = join4(modelDir, "model_quantized.onnx");
1668
- const vocabPath = join4(modelDir, "vocab.txt");
1822
+ const modelPath = join5(modelDir, "model_quantized.onnx");
1823
+ const vocabPath = join5(modelDir, "vocab.txt");
1669
1824
  _vocab = loadVocab(vocabPath);
1670
1825
  const session = await ort.InferenceSession.create(modelPath, {
1671
1826
  executionProviders: ["wasm"],
@@ -1986,6 +2141,7 @@ __export(memory_db_exports, {
1986
2141
  migrateAuditLogCheckExtension: () => migrateAuditLogCheckExtension,
1987
2142
  migrateMemoryFilesFor4B: () => migrateMemoryFilesFor4B,
1988
2143
  migrateObservationEmbeddingChunks: () => migrateObservationEmbeddingChunks,
2144
+ migrateSharedMemoryFor5B: () => migrateSharedMemoryFor5B,
1989
2145
  pruneOldConversationTurns: () => pruneOldConversationTurns,
1990
2146
  pruneOldObservations: () => pruneOldObservations,
1991
2147
  pruneToolCostEvents: () => pruneToolCostEvents,
@@ -2003,7 +2159,7 @@ __export(memory_db_exports, {
2003
2159
  usageWarmupElapsed: () => usageWarmupElapsed
2004
2160
  });
2005
2161
  import { dirname as dirname7, basename as basename2 } from "path";
2006
- import { existsSync as existsSync7, mkdirSync as mkdirSync6 } from "fs";
2162
+ import { existsSync as existsSync8, mkdirSync as mkdirSync6 } from "fs";
2007
2163
  function sanitizeFts5Query(raw) {
2008
2164
  const trimmed = raw.trim();
2009
2165
  if (!trimmed) return '""';
@@ -2020,11 +2176,11 @@ function sanitizeFts5QueryOr(raw) {
2020
2176
  function getMemoryDb() {
2021
2177
  const dbPath = getResolvedPaths().memoryDbPath;
2022
2178
  const dir = dirname7(dbPath);
2023
- if (!existsSync7(dir)) {
2179
+ if (!existsSync8(dir)) {
2024
2180
  mkdirSync6(dir, { recursive: true });
2025
2181
  }
2026
- const preExisting = existsSync7(dbPath);
2027
- const db = openDatabase(dbPath);
2182
+ const preExisting = existsSync8(dbPath);
2183
+ const db = openDatabase2(dbPath);
2028
2184
  db.pragma("journal_mode = WAL");
2029
2185
  db.pragma("foreign_keys = ON");
2030
2186
  const onDisk = db.pragma("user_version", { simple: true });
@@ -2100,6 +2256,39 @@ function migrateMemoryFilesFor4B(db) {
2100
2256
  db.exec(`ALTER TABLE ${add.table} ADD COLUMN ${add.name} ${add.decl}`);
2101
2257
  }
2102
2258
  }
2259
+ function migrateSharedMemoryFor5B(db) {
2260
+ const cols = db.prepare(`PRAGMA table_info(observations)`).all();
2261
+ if (cols.length > 0 && !cols.some((c) => c.name === "shareable")) {
2262
+ db.exec(`ALTER TABLE observations ADD COLUMN shareable INTEGER NOT NULL DEFAULT 0`);
2263
+ }
2264
+ db.exec(`
2265
+ CREATE TABLE IF NOT EXISTS shared_memory_pending (
2266
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
2267
+ record_hash TEXT NOT NULL UNIQUE, -- identity + idempotency key (hex sha256)
2268
+ origin_repo_id TEXT NOT NULL, -- the signed origin repo_id (v4 UUID)
2269
+ origin_repo_label TEXT NOT NULL, -- re-slugged on read; NEVER trusted from the wire
2270
+ envelope_raw TEXT NOT NULL, -- the VERBATIM signed bytes; B-05 re-verifies these
2271
+ record_json TEXT NOT NULL, -- the single record's canonical JSON
2272
+ received_at_epoch INTEGER NOT NULL, -- epoch SECONDS (Slice-2 convention)
2273
+ accepted_at_epoch INTEGER, -- set on accept (B-05); NULL while pending
2274
+ refused_at_epoch INTEGER, -- set on refuse (B-06)
2275
+ expired_at_epoch INTEGER -- revocation \u21D2 EXPIRE, never DELETE (B-07 / S-3)
2276
+ );
2277
+ CREATE INDEX IF NOT EXISTS idx_smp_origin ON shared_memory_pending(origin_repo_id);
2278
+ CREATE INDEX IF NOT EXISTS idx_smp_received ON shared_memory_pending(received_at_epoch DESC);
2279
+ `);
2280
+ db.exec(`
2281
+ CREATE TABLE IF NOT EXISTS shared_memory_outbound (
2282
+ record_hash TEXT PRIMARY KEY, -- the exported record's canonical hash
2283
+ observation_id INTEGER NOT NULL, -- the source observations.id (for B-07 revocation)
2284
+ origin_repo_id TEXT NOT NULL, -- this repo's own repo_id at export time
2285
+ seq INTEGER NOT NULL, -- the envelope seq this record first went out in
2286
+ exported_at_epoch INTEGER NOT NULL, -- epoch SECONDS
2287
+ revoked_at_epoch INTEGER -- set when the source row is superseded/expired (B-07)
2288
+ );
2289
+ CREATE INDEX IF NOT EXISTS idx_smo_obs ON shared_memory_outbound(observation_id);
2290
+ `);
2291
+ }
2103
2292
  function migrateAuditLogCheckExtension(db) {
2104
2293
  const row = db.prepare("SELECT sql FROM sqlite_master WHERE type='table' AND name='audit_log'").get();
2105
2294
  if (!row) return;
@@ -2122,7 +2311,16 @@ function migrateAuditLogCheckExtension(db) {
2122
2311
  "memory_file_adopted_human",
2123
2312
  "memory_file_rendered",
2124
2313
  "memory_file_render_refused",
2125
- "memory_file_tombstoned"
2314
+ "memory_file_tombstoned",
2315
+ // B-10 (Slice 5) — the seven cross-repo shared-memory events. An existing DB whose
2316
+ // CHECK lacks any of these triggers the table rebuild below.
2317
+ "shared_memory_exported",
2318
+ "shared_memory_export_refused",
2319
+ "shared_memory_imported",
2320
+ "shared_memory_dropped",
2321
+ "shared_memory_accepted",
2322
+ "shared_memory_refused",
2323
+ "shared_memory_revoked"
2126
2324
  ];
2127
2325
  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
2326
  if (checkClauseMatch) {
@@ -2141,7 +2339,12 @@ function migrateAuditLogCheckExtension(db) {
2141
2339
  'code_change', 'rule_enforced', 'approval', 'review', 'commit', 'compaction',
2142
2340
  'rule_candidate_emitted', 'rule_promoted', 'rule_dismissed',
2143
2341
  'memory_file_ingested', 'memory_file_expired', 'memory_file_adopted_human',
2144
- 'memory_file_rendered', 'memory_file_render_refused', 'memory_file_tombstoned'
2342
+ 'memory_file_rendered', 'memory_file_render_refused', 'memory_file_tombstoned',
2343
+ -- B-10 (Slice 5) \u2014 cross-repo shared-memory events (must mirror the CREATE +
2344
+ -- the expected[] list above, or the migration would loop rebuilding forever).
2345
+ 'shared_memory_exported', 'shared_memory_export_refused', 'shared_memory_imported',
2346
+ 'shared_memory_dropped', 'shared_memory_accepted', 'shared_memory_refused',
2347
+ 'shared_memory_revoked'
2145
2348
  )),
2146
2349
  actor TEXT NOT NULL DEFAULT 'ai' CHECK(actor IN ('ai', 'human', 'hook', 'agent')),
2147
2350
  model_id TEXT,
@@ -2218,6 +2421,12 @@ function initMemorySchema(db) {
2218
2421
  -- credential, or takes a snapshot. A Slice-5 synced memory arrives here with
2219
2422
  -- origin='team' and must never reach disk without CR-55's gates.
2220
2423
  origin TEXT NOT NULL DEFAULT 'local',
2424
+ -- B-01 (Slice 5, cross-repo surfacing): the per-decision SHARE opt-in. Set to 1
2425
+ -- ONLY by an explicit human act (massu memory share <id>); NEVER by a heuristic,
2426
+ -- score, LLM, hook, or consolidation pass. Export (B-02) reads WHERE shareable=1
2427
+ -- AND origin='local'. Defaults 0 so every pre-existing and machine-written row is
2428
+ -- un-shareable until a human says otherwise (fail-closed).
2429
+ shareable INTEGER NOT NULL DEFAULT 0,
2221
2430
  created_at TEXT NOT NULL,
2222
2431
  created_at_epoch INTEGER NOT NULL,
2223
2432
  FOREIGN KEY(session_id) REFERENCES sessions(session_id) ON DELETE CASCADE
@@ -2491,7 +2700,13 @@ function initMemorySchema(db) {
2491
2700
  'code_change', 'rule_enforced', 'approval', 'review', 'commit', 'compaction',
2492
2701
  'rule_candidate_emitted', 'rule_promoted', 'rule_dismissed',
2493
2702
  'memory_file_ingested', 'memory_file_expired', 'memory_file_adopted_human',
2494
- 'memory_file_rendered', 'memory_file_render_refused', 'memory_file_tombstoned'
2703
+ 'memory_file_rendered', 'memory_file_render_refused', 'memory_file_tombstoned',
2704
+ -- B-10 (Slice 5): cross-repo shared-memory observability. Without these seven,
2705
+ -- nobody could ever answer "what has crossed between my repos?" (and an ingest
2706
+ -- catch would swallow the CHECK violation, silently producing NOTHING).
2707
+ 'shared_memory_exported', 'shared_memory_export_refused', 'shared_memory_imported',
2708
+ 'shared_memory_dropped', 'shared_memory_accepted', 'shared_memory_refused',
2709
+ 'shared_memory_revoked'
2495
2710
  )),
2496
2711
  actor TEXT NOT NULL DEFAULT 'ai' CHECK(actor IN ('ai', 'human', 'hook', 'agent')),
2497
2712
  model_id TEXT,
@@ -2860,6 +3075,7 @@ function initMemorySchema(db) {
2860
3075
  `);
2861
3076
  migrateObservationEmbeddingChunks(db);
2862
3077
  migrateMemoryFilesFor4B(db);
3078
+ migrateSharedMemoryFor5B(db);
2863
3079
  ensureRuleCandidatesTable(db);
2864
3080
  migrateRuleDelivery(db);
2865
3081
  }
@@ -3730,7 +3946,7 @@ var MEMORY_SCHEMA_VERSION, TOOL_COST_EVENTS_RETENTION_DAYS, MAX_DRAFT_TEXT_LEN,
3730
3946
  var init_memory_db = __esm({
3731
3947
  "src/memory-db.ts"() {
3732
3948
  "use strict";
3733
- init_sqlite_loader();
3949
+ init_db_driver();
3734
3950
  init_config();
3735
3951
  init_memory_vector();
3736
3952
  init_db_backup();
@@ -3760,68 +3976,6 @@ function estimateTokens(text) {
3760
3976
 
3761
3977
  // src/observation-extractor.ts
3762
3978
  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
3979
  init_config();
3826
3980
  import { homedir as homedir4 } from "os";
3827
3981
 
@@ -4124,23 +4278,6 @@ function classifyRealTimeToolCall(toolName, toolInput, toolResponse, seenReads2,
4124
4278
  isError: isError ?? normalized.isError
4125
4279
  };
4126
4280
  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
4281
  return classifyToolCall(tc);
4145
4282
  }
4146
4283
  function detectPlanProgress(rawToolResponse) {
@@ -4257,8 +4394,8 @@ init_config();
4257
4394
 
4258
4395
  // src/import-resolver.ts
4259
4396
  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";
4397
+ import { readFileSync as readFileSync6, existsSync as existsSync9, statSync as statSync2 } from "fs";
4398
+ import { resolve as resolve6, dirname as dirname8, join as join6 } from "path";
4262
4399
 
4263
4400
  // src/security-utils.ts
4264
4401
  import { resolve as resolve5, normalize } from "path";
@@ -4321,19 +4458,19 @@ function resolveImportPath(specifier, fromFile) {
4321
4458
  } else {
4322
4459
  basePath = resolve6(dirname8(fromFile), specifier);
4323
4460
  }
4324
- if (existsSync8(basePath) && !isDirectory(basePath)) {
4461
+ if (existsSync9(basePath) && !isDirectory(basePath)) {
4325
4462
  return toRelative(basePath);
4326
4463
  }
4327
4464
  const resolvedPaths = getResolvedPaths();
4328
4465
  for (const ext of resolvedPaths.extensions) {
4329
4466
  const withExt = basePath + ext;
4330
- if (existsSync8(withExt)) {
4467
+ if (existsSync9(withExt)) {
4331
4468
  return toRelative(withExt);
4332
4469
  }
4333
4470
  }
4334
4471
  for (const indexFile of resolvedPaths.indexFiles) {
4335
- const indexPath = join5(basePath, indexFile);
4336
- if (existsSync8(indexPath)) {
4472
+ const indexPath = join6(basePath, indexFile);
4473
+ if (existsSync9(indexPath)) {
4337
4474
  return toRelative(indexPath);
4338
4475
  }
4339
4476
  }
@@ -4355,7 +4492,7 @@ function toRelative(absPath) {
4355
4492
  }
4356
4493
 
4357
4494
  // src/validation-engine.ts
4358
- import { existsSync as existsSync9, readFileSync as readFileSync6 } from "fs";
4495
+ import { existsSync as existsSync10, readFileSync as readFileSync7 } from "fs";
4359
4496
  function getValidationChecks() {
4360
4497
  return getConfig().governance?.validation?.checks ?? {
4361
4498
  rule_compliance: true,
@@ -4383,7 +4520,7 @@ function validateFile(filePath, projectRoot) {
4383
4520
  });
4384
4521
  return checks;
4385
4522
  }
4386
- if (!existsSync9(absPath)) {
4523
+ if (!existsSync10(absPath)) {
4387
4524
  checks.push({
4388
4525
  name: "file_exists",
4389
4526
  severity: "error",
@@ -4392,7 +4529,7 @@ function validateFile(filePath, projectRoot) {
4392
4529
  });
4393
4530
  return checks;
4394
4531
  }
4395
- const source = readFileSync6(absPath, "utf-8");
4532
+ const source = readFileSync7(absPath, "utf-8");
4396
4533
  const lines = source.split("\n");
4397
4534
  if (activeChecks.rule_compliance !== false) {
4398
4535
  for (const ruleSet of config.rules) {
@@ -4491,7 +4628,7 @@ function storeValidationResult(db, filePath, checks, sessionId, validationType =
4491
4628
 
4492
4629
  // src/security-scorer.ts
4493
4630
  init_config();
4494
- import { existsSync as existsSync10, readFileSync as readFileSync7 } from "fs";
4631
+ import { existsSync as existsSync11, readFileSync as readFileSync8 } from "fs";
4495
4632
  var DEFAULT_SECURITY_PATTERNS = [
4496
4633
  {
4497
4634
  regex: /\bexec\s*\(\s*[`"'].*\$\{/,
@@ -4582,12 +4719,12 @@ function scoreFileSecurity(filePath, projectRoot) {
4582
4719
  }]
4583
4720
  };
4584
4721
  }
4585
- if (!existsSync10(absPath)) {
4722
+ if (!existsSync11(absPath)) {
4586
4723
  return { riskScore: 0, findings: [] };
4587
4724
  }
4588
4725
  let source;
4589
4726
  try {
4590
- source = readFileSync7(absPath, "utf-8");
4727
+ source = readFileSync8(absPath, "utf-8");
4591
4728
  } catch {
4592
4729
  return { riskScore: 0, findings: [] };
4593
4730
  }
@@ -4627,8 +4764,8 @@ function storeSecurityScore(db, sessionId, filePath, riskScore, findings) {
4627
4764
 
4628
4765
  // src/hooks/post-tool-use.ts
4629
4766
  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";
4767
+ import { readFileSync as readFileSync10, writeFileSync as writeFileSync3, existsSync as existsSync13, mkdirSync as mkdirSync7, statSync as statSync3 } from "fs";
4768
+ import { join as join7, basename as pathBasename2, dirname as pathDirname, resolve as pathResolve } from "path";
4632
4769
 
4633
4770
  // src/lib/recurrence-incrementer.ts
4634
4771
  var ANSI = /\x1b\[[0-9;]*m/g;
@@ -4692,7 +4829,7 @@ function incrementRecurrenceCountsForScannerFailures(db, sessionId, scannerStdou
4692
4829
  init_hook_failure_signal();
4693
4830
 
4694
4831
  // src/memory-file-ingest.ts
4695
- import { readFileSync as readFileSync8, existsSync as existsSync11, readdirSync as readdirSync2 } from "fs";
4832
+ import { readFileSync as readFileSync9, existsSync as existsSync12, readdirSync as readdirSync2 } from "fs";
4696
4833
  init_memory_db();
4697
4834
  import { parse as parseYaml2 } from "yaml";
4698
4835
  import { basename as pathBasename } from "path";
@@ -4801,8 +4938,8 @@ function upsertMemoryFileMirror(db, f) {
4801
4938
  return "inserted";
4802
4939
  }
4803
4940
  function ingestMemoryFile(db, sessionId, filePath) {
4804
- if (!existsSync11(filePath)) return "skipped";
4805
- const content = readFileSync8(filePath, "utf-8");
4941
+ if (!existsSync12(filePath)) return "skipped";
4942
+ const content = readFileSync9(filePath, "utf-8");
4806
4943
  if (Buffer.byteLength(content, "utf-8") > MAX_MEMORY_FILE_BYTES) {
4807
4944
  process.stderr.write(
4808
4945
  `[massu] memory file exceeds ${MAX_MEMORY_FILE_BYTES} bytes, not ingested: ${filePath}
@@ -4919,27 +5056,6 @@ async function main() {
4919
5056
  } else {
4920
5057
  addObservation(db, session_id, observation.type, observation.title, observation.detail, observation.opts);
4921
5058
  }
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
5059
  if (tool_response) {
4944
5060
  const progress = detectPlanProgress(tool_response);
4945
5061
  if (progress.length > 0) {
@@ -5076,10 +5192,10 @@ async function main() {
5076
5192
  } catch (err) {
5077
5193
  try {
5078
5194
  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") : "";
5195
+ const dir = join7(projectRoot, ".massu", "rule-candidates");
5196
+ if (!existsSync13(dir)) mkdirSync7(dir, { recursive: true });
5197
+ const logPath = join7(dir, ".cr53-increment-failures.jsonl");
5198
+ const pre = existsSync13(logPath) ? readFileSync10(logPath, "utf-8") : "";
5083
5199
  const sep = pre && !pre.endsWith("\n") ? "\n" : "";
5084
5200
  writeFileSync3(logPath, pre + sep + JSON.stringify({
5085
5201
  session_id,
@@ -5184,13 +5300,13 @@ var _conventionDefaults = {
5184
5300
  function readConventions(cwd) {
5185
5301
  try {
5186
5302
  const projectRoot = cwd ?? process.cwd();
5187
- const configPath = join6(projectRoot, "massu.config.yaml");
5188
- if (!existsSync12(configPath)) return _conventionDefaults;
5303
+ const configPath = join7(projectRoot, "massu.config.yaml");
5304
+ if (!existsSync13(configPath)) return _conventionDefaults;
5189
5305
  const mtimeMs = statSync3(configPath).mtimeMs;
5190
5306
  if (_cachedConventions !== null && _cachedConventionsPath === configPath && _cachedConventionsMtimeMs === mtimeMs) {
5191
5307
  return _cachedConventions;
5192
5308
  }
5193
- const content = readFileSync9(configPath, "utf-8");
5309
+ const content = readFileSync10(configPath, "utf-8");
5194
5310
  const parsed = parseYaml3(content);
5195
5311
  if (!parsed || typeof parsed !== "object") return _conventionDefaults;
5196
5312
  const conventions = parsed.conventions;
@@ -5222,11 +5338,11 @@ function isKnowledgeSourceFile(filePath) {
5222
5338
  function checkMemoryFileIntegrity(filePath) {
5223
5339
  const issues = [];
5224
5340
  try {
5225
- if (!existsSync12(filePath)) {
5341
+ if (!existsSync13(filePath)) {
5226
5342
  issues.push("MEMORY.md file does not exist after write");
5227
5343
  return issues;
5228
5344
  }
5229
- const content = readFileSync9(filePath, "utf-8");
5345
+ const content = readFileSync10(filePath, "utf-8");
5230
5346
  const lines = content.split("\n");
5231
5347
  const MAX_LINES = 200;
5232
5348
  if (lines.length > MAX_LINES) {