@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.
@@ -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);
@@ -819,7 +975,6 @@ var init_config = __esm({
819
975
  custom_patterns: z2.array(CustomPatternSchema).default([])
820
976
  }).optional(),
821
977
  adr: z2.object({
822
- detection_phrases: z2.array(z2.string()).default(["chose", "decided", "switching to", "moving from", "going with"]),
823
978
  template: z2.string().default("default"),
824
979
  storage: z2.string().default("database"),
825
980
  output_dir: z2.string().default("docs/adr")
@@ -1194,26 +1349,26 @@ var init_memory_vector = __esm({
1194
1349
  });
1195
1350
 
1196
1351
  // src/db-backup.ts
1197
- import { existsSync as existsSync4, mkdirSync as mkdirSync4, readdirSync, statSync, unlinkSync, copyFileSync, rmSync as rmSync3 } from "fs";
1198
- import { resolve as resolve3, join as join2, basename, dirname as dirname4 } from "path";
1352
+ import { existsSync as existsSync5, mkdirSync as mkdirSync4, readdirSync, statSync, unlinkSync, copyFileSync, rmSync as rmSync4 } from "fs";
1353
+ import { resolve as resolve3, join as join3, basename, dirname as dirname4 } from "path";
1199
1354
  import { homedir as homedir3 } from "os";
1200
1355
  function dbBackupsRoot(home = homedir3()) {
1201
1356
  return resolve3(home, ".massu", "db-backups");
1202
1357
  }
1203
1358
  function projectBackupDir(projectRoot, home = homedir3()) {
1204
1359
  const slug = basename(projectRoot).replace(/[^A-Za-z0-9._-]/g, "_") || "project";
1205
- return join2(dbBackupsRoot(home), slug);
1360
+ return join3(dbBackupsRoot(home), slug);
1206
1361
  }
1207
1362
  function backupStamp(nowMs) {
1208
1363
  return new Date(nowMs).toISOString().replace(/[:.]/g, "-");
1209
1364
  }
1210
1365
  function listDbBackups(projectRoot, home = homedir3()) {
1211
1366
  const dir = projectBackupDir(projectRoot, home);
1212
- if (!existsSync4(dir)) return [];
1367
+ if (!existsSync5(dir)) return [];
1213
1368
  const out = [];
1214
1369
  for (const f of readdirSync(dir)) {
1215
1370
  if (!f.endsWith(".db")) continue;
1216
- const p = join2(dir, f);
1371
+ const p = join3(dir, f);
1217
1372
  const st = statSync(p);
1218
1373
  const db = f.replace(/-\d{4}-\d{2}-\d{2}T.*$/, "");
1219
1374
  out.push({ db, path: p, bytes: st.size, mtimeMs: st.mtimeMs });
@@ -1226,13 +1381,13 @@ function hasFreshDbBackup(projectRoot, dbPath, nowMs = Date.now(), home = homedi
1226
1381
  if (backups.length === 0) return false;
1227
1382
  const newest = backups[0];
1228
1383
  if (nowMs - newest.mtimeMs > FRESH_WINDOW_MS) return false;
1229
- if (existsSync4(dbPath) && statSync(dbPath).mtimeMs > newest.mtimeMs) return false;
1384
+ if (existsSync5(dbPath) && statSync(dbPath).mtimeMs > newest.mtimeMs) return false;
1230
1385
  return true;
1231
1386
  }
1232
1387
  function integrityOk(dbPath) {
1233
1388
  let db = null;
1234
1389
  try {
1235
- db = openDatabase(dbPath, { readonly: true });
1390
+ db = openDatabase2(dbPath, { readonly: true });
1236
1391
  const rows = db.pragma("integrity_check");
1237
1392
  return rows.length === 1 && rows[0].integrity_check === "ok";
1238
1393
  } catch {
@@ -1245,16 +1400,16 @@ function integrityOk(dbPath) {
1245
1400
  }
1246
1401
  }
1247
1402
  function backupDb(projectRoot, dbPath, nowMs = Date.now(), home = homedir3()) {
1248
- if (!existsSync4(dbPath)) {
1403
+ if (!existsSync5(dbPath)) {
1249
1404
  throw new DbBackupError(`cannot back up a database that does not exist: ${dbPath}`);
1250
1405
  }
1251
1406
  const dir = projectBackupDir(projectRoot, home);
1252
1407
  mkdirSync4(dir, { recursive: true });
1253
1408
  const name = basename(dbPath, ".db");
1254
- const dest = join2(dir, `${name}-${backupStamp(nowMs)}.db`);
1409
+ const dest = join3(dir, `${name}-${backupStamp(nowMs)}.db`);
1255
1410
  let src = null;
1256
1411
  try {
1257
- src = openDatabase(dbPath, { readonly: true });
1412
+ src = openDatabase2(dbPath, { readonly: true });
1258
1413
  src.exec(`VACUUM INTO '${dest.replace(/'/g, "''")}'`);
1259
1414
  } catch (err) {
1260
1415
  throw new DbBackupError(
@@ -1293,7 +1448,7 @@ function pruneDbBackups(projectRoot, dbName, keep = DEFAULT_RETENTION, home = ho
1293
1448
  }
1294
1449
  function backupBeforeSchemaChange(projectRoot, dbPath, onError, nowMs = Date.now(), home = homedir3()) {
1295
1450
  try {
1296
- if (!existsSync4(dbPath)) return null;
1451
+ if (!existsSync5(dbPath)) return null;
1297
1452
  if (hasFreshDbBackup(projectRoot, dbPath, nowMs, home)) return null;
1298
1453
  return backupDb(projectRoot, dbPath, nowMs, home);
1299
1454
  } catch (err) {
@@ -1305,7 +1460,7 @@ var DEFAULT_RETENTION, FRESH_WINDOW_MS, DbBackupError;
1305
1460
  var init_db_backup = __esm({
1306
1461
  "src/db-backup.ts"() {
1307
1462
  "use strict";
1308
- init_sqlite_loader();
1463
+ init_db_driver();
1309
1464
  DEFAULT_RETENTION = 5;
1310
1465
  FRESH_WINDOW_MS = 24 * 60 * 60 * 1e3;
1311
1466
  DbBackupError = class extends Error {
@@ -1565,21 +1720,21 @@ var init_rule_candidate_store = __esm({
1565
1720
  });
1566
1721
 
1567
1722
  // src/hooks/lib/hook-failure-signal.ts
1568
- import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync5, existsSync as existsSync5 } from "fs";
1569
- import { join as join3, dirname as dirname5 } from "path";
1723
+ import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync5, existsSync as existsSync6 } from "fs";
1724
+ import { join as join4, dirname as dirname5 } from "path";
1570
1725
  function resolveFailureLogPath() {
1571
1726
  const explicit = process.env.MASSU_HOOK_FAILURE_LOG;
1572
1727
  if (explicit) return explicit;
1573
1728
  let dir = process.cwd();
1574
1729
  for (let i = 0; i < 12; i++) {
1575
- if (existsSync5(join3(dir, ".massu")) || existsSync5(join3(dir, "massu.config.yaml"))) {
1576
- return join3(dir, ".massu", "hook-failures.jsonl");
1730
+ if (existsSync6(join4(dir, ".massu")) || existsSync6(join4(dir, "massu.config.yaml"))) {
1731
+ return join4(dir, ".massu", "hook-failures.jsonl");
1577
1732
  }
1578
1733
  const parent = dirname5(dir);
1579
1734
  if (parent === dir) break;
1580
1735
  dir = parent;
1581
1736
  }
1582
- return join3(process.cwd(), ".massu", "hook-failures.jsonl");
1737
+ return join4(process.cwd(), ".massu", "hook-failures.jsonl");
1583
1738
  }
1584
1739
  function recordHookFailure(hook, error, context) {
1585
1740
  const record = {
@@ -1636,9 +1791,9 @@ var init_hook_failure_signal = __esm({
1636
1791
  });
1637
1792
 
1638
1793
  // src/memory-embedder-tokenizer.ts
1639
- import { readFileSync as readFileSync4 } from "fs";
1794
+ import { readFileSync as readFileSync5 } from "fs";
1640
1795
  function loadVocab(vocabPath) {
1641
- const lines = readFileSync4(vocabPath, "utf-8").split("\n");
1796
+ const lines = readFileSync5(vocabPath, "utf-8").split("\n");
1642
1797
  const vocab = /* @__PURE__ */ new Map();
1643
1798
  for (let i = 0; i < lines.length; i++) {
1644
1799
  const tok = lines[i].replace(/\r$/, "");
@@ -1758,9 +1913,9 @@ var init_memory_embedder_tokenizer = __esm({
1758
1913
 
1759
1914
  // src/memory-embedder.ts
1760
1915
  import { fileURLToPath } from "url";
1761
- import { dirname as dirname6, join as join4 } from "path";
1762
- import { existsSync as existsSync6 } from "fs";
1763
- import { createRequire as createRequire2 } from "module";
1916
+ import { dirname as dirname6, join as join5 } from "path";
1917
+ import { existsSync as existsSync7 } from "fs";
1918
+ import { createRequire as createRequire3 } from "module";
1764
1919
  function getActiveEmbedModel() {
1765
1920
  return _activeModel;
1766
1921
  }
@@ -1784,7 +1939,7 @@ function resolveModelDir() {
1784
1939
  let dir = dirname6(fileURLToPath(import.meta.url));
1785
1940
  let root = null;
1786
1941
  for (let i = 0; i < 8; i++) {
1787
- if (existsSync6(join4(dir, "package.json"))) {
1942
+ if (existsSync7(join5(dir, "package.json"))) {
1788
1943
  root = dir;
1789
1944
  break;
1790
1945
  }
@@ -1794,8 +1949,8 @@ function resolveModelDir() {
1794
1949
  }
1795
1950
  if (!root) return null;
1796
1951
  for (const rel of ["dist/embedder", "assets/embedder"]) {
1797
- const candidate = join4(root, rel);
1798
- if (existsSync6(join4(candidate, "model_quantized.onnx"))) return candidate;
1952
+ const candidate = join5(root, rel);
1953
+ if (existsSync7(join5(candidate, "model_quantized.onnx"))) return candidate;
1799
1954
  }
1800
1955
  return null;
1801
1956
  } catch {
@@ -1804,8 +1959,8 @@ function resolveModelDir() {
1804
1959
  }
1805
1960
  function resolveWasmDir() {
1806
1961
  try {
1807
- const req2 = createRequire2(import.meta.url);
1808
- const main2 = req2.resolve(ORT_PKG);
1962
+ const req3 = createRequire3(import.meta.url);
1963
+ const main2 = req3.resolve(ORT_PKG);
1809
1964
  return dirname6(main2);
1810
1965
  } catch {
1811
1966
  return null;
@@ -1826,8 +1981,8 @@ async function getSession() {
1826
1981
  ort.env.wasm.proxy = false;
1827
1982
  ort.env.wasm.wasmPaths = wasmDir.endsWith("/") ? wasmDir : wasmDir + "/";
1828
1983
  _ortTensor = ort.Tensor;
1829
- const modelPath = join4(modelDir, "model_quantized.onnx");
1830
- const vocabPath = join4(modelDir, "vocab.txt");
1984
+ const modelPath = join5(modelDir, "model_quantized.onnx");
1985
+ const vocabPath = join5(modelDir, "vocab.txt");
1831
1986
  _vocab = loadVocab(vocabPath);
1832
1987
  const session = await ort.InferenceSession.create(modelPath, {
1833
1988
  executionProviders: ["wasm"],
@@ -2163,6 +2318,7 @@ __export(memory_db_exports, {
2163
2318
  migrateAuditLogCheckExtension: () => migrateAuditLogCheckExtension,
2164
2319
  migrateMemoryFilesFor4B: () => migrateMemoryFilesFor4B,
2165
2320
  migrateObservationEmbeddingChunks: () => migrateObservationEmbeddingChunks,
2321
+ migrateSharedMemoryFor5B: () => migrateSharedMemoryFor5B,
2166
2322
  pruneOldConversationTurns: () => pruneOldConversationTurns,
2167
2323
  pruneOldObservations: () => pruneOldObservations,
2168
2324
  pruneToolCostEvents: () => pruneToolCostEvents,
@@ -2180,7 +2336,7 @@ __export(memory_db_exports, {
2180
2336
  usageWarmupElapsed: () => usageWarmupElapsed
2181
2337
  });
2182
2338
  import { dirname as dirname7, basename as basename2 } from "path";
2183
- import { existsSync as existsSync7, mkdirSync as mkdirSync6 } from "fs";
2339
+ import { existsSync as existsSync8, mkdirSync as mkdirSync6 } from "fs";
2184
2340
  function sanitizeFts5Query(raw) {
2185
2341
  const trimmed = raw.trim();
2186
2342
  if (!trimmed) return '""';
@@ -2197,11 +2353,11 @@ function sanitizeFts5QueryOr(raw) {
2197
2353
  function getMemoryDb() {
2198
2354
  const dbPath = getResolvedPaths().memoryDbPath;
2199
2355
  const dir = dirname7(dbPath);
2200
- if (!existsSync7(dir)) {
2356
+ if (!existsSync8(dir)) {
2201
2357
  mkdirSync6(dir, { recursive: true });
2202
2358
  }
2203
- const preExisting = existsSync7(dbPath);
2204
- const db = openDatabase(dbPath);
2359
+ const preExisting = existsSync8(dbPath);
2360
+ const db = openDatabase2(dbPath);
2205
2361
  db.pragma("journal_mode = WAL");
2206
2362
  db.pragma("foreign_keys = ON");
2207
2363
  const onDisk = db.pragma("user_version", { simple: true });
@@ -2277,6 +2433,39 @@ function migrateMemoryFilesFor4B(db) {
2277
2433
  db.exec(`ALTER TABLE ${add.table} ADD COLUMN ${add.name} ${add.decl}`);
2278
2434
  }
2279
2435
  }
2436
+ function migrateSharedMemoryFor5B(db) {
2437
+ const cols = db.prepare(`PRAGMA table_info(observations)`).all();
2438
+ if (cols.length > 0 && !cols.some((c) => c.name === "shareable")) {
2439
+ db.exec(`ALTER TABLE observations ADD COLUMN shareable INTEGER NOT NULL DEFAULT 0`);
2440
+ }
2441
+ db.exec(`
2442
+ CREATE TABLE IF NOT EXISTS shared_memory_pending (
2443
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
2444
+ record_hash TEXT NOT NULL UNIQUE, -- identity + idempotency key (hex sha256)
2445
+ origin_repo_id TEXT NOT NULL, -- the signed origin repo_id (v4 UUID)
2446
+ origin_repo_label TEXT NOT NULL, -- re-slugged on read; NEVER trusted from the wire
2447
+ envelope_raw TEXT NOT NULL, -- the VERBATIM signed bytes; B-05 re-verifies these
2448
+ record_json TEXT NOT NULL, -- the single record's canonical JSON
2449
+ received_at_epoch INTEGER NOT NULL, -- epoch SECONDS (Slice-2 convention)
2450
+ accepted_at_epoch INTEGER, -- set on accept (B-05); NULL while pending
2451
+ refused_at_epoch INTEGER, -- set on refuse (B-06)
2452
+ expired_at_epoch INTEGER -- revocation \u21D2 EXPIRE, never DELETE (B-07 / S-3)
2453
+ );
2454
+ CREATE INDEX IF NOT EXISTS idx_smp_origin ON shared_memory_pending(origin_repo_id);
2455
+ CREATE INDEX IF NOT EXISTS idx_smp_received ON shared_memory_pending(received_at_epoch DESC);
2456
+ `);
2457
+ db.exec(`
2458
+ CREATE TABLE IF NOT EXISTS shared_memory_outbound (
2459
+ record_hash TEXT PRIMARY KEY, -- the exported record's canonical hash
2460
+ observation_id INTEGER NOT NULL, -- the source observations.id (for B-07 revocation)
2461
+ origin_repo_id TEXT NOT NULL, -- this repo's own repo_id at export time
2462
+ seq INTEGER NOT NULL, -- the envelope seq this record first went out in
2463
+ exported_at_epoch INTEGER NOT NULL, -- epoch SECONDS
2464
+ revoked_at_epoch INTEGER -- set when the source row is superseded/expired (B-07)
2465
+ );
2466
+ CREATE INDEX IF NOT EXISTS idx_smo_obs ON shared_memory_outbound(observation_id);
2467
+ `);
2468
+ }
2280
2469
  function migrateAuditLogCheckExtension(db) {
2281
2470
  const row = db.prepare("SELECT sql FROM sqlite_master WHERE type='table' AND name='audit_log'").get();
2282
2471
  if (!row) return;
@@ -2299,7 +2488,16 @@ function migrateAuditLogCheckExtension(db) {
2299
2488
  "memory_file_adopted_human",
2300
2489
  "memory_file_rendered",
2301
2490
  "memory_file_render_refused",
2302
- "memory_file_tombstoned"
2491
+ "memory_file_tombstoned",
2492
+ // B-10 (Slice 5) — the seven cross-repo shared-memory events. An existing DB whose
2493
+ // CHECK lacks any of these triggers the table rebuild below.
2494
+ "shared_memory_exported",
2495
+ "shared_memory_export_refused",
2496
+ "shared_memory_imported",
2497
+ "shared_memory_dropped",
2498
+ "shared_memory_accepted",
2499
+ "shared_memory_refused",
2500
+ "shared_memory_revoked"
2303
2501
  ];
2304
2502
  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);
2305
2503
  if (checkClauseMatch) {
@@ -2318,7 +2516,12 @@ function migrateAuditLogCheckExtension(db) {
2318
2516
  'code_change', 'rule_enforced', 'approval', 'review', 'commit', 'compaction',
2319
2517
  'rule_candidate_emitted', 'rule_promoted', 'rule_dismissed',
2320
2518
  'memory_file_ingested', 'memory_file_expired', 'memory_file_adopted_human',
2321
- 'memory_file_rendered', 'memory_file_render_refused', 'memory_file_tombstoned'
2519
+ 'memory_file_rendered', 'memory_file_render_refused', 'memory_file_tombstoned',
2520
+ -- B-10 (Slice 5) \u2014 cross-repo shared-memory events (must mirror the CREATE +
2521
+ -- the expected[] list above, or the migration would loop rebuilding forever).
2522
+ 'shared_memory_exported', 'shared_memory_export_refused', 'shared_memory_imported',
2523
+ 'shared_memory_dropped', 'shared_memory_accepted', 'shared_memory_refused',
2524
+ 'shared_memory_revoked'
2322
2525
  )),
2323
2526
  actor TEXT NOT NULL DEFAULT 'ai' CHECK(actor IN ('ai', 'human', 'hook', 'agent')),
2324
2527
  model_id TEXT,
@@ -2395,6 +2598,12 @@ function initMemorySchema(db) {
2395
2598
  -- credential, or takes a snapshot. A Slice-5 synced memory arrives here with
2396
2599
  -- origin='team' and must never reach disk without CR-55's gates.
2397
2600
  origin TEXT NOT NULL DEFAULT 'local',
2601
+ -- B-01 (Slice 5, cross-repo surfacing): the per-decision SHARE opt-in. Set to 1
2602
+ -- ONLY by an explicit human act (massu memory share <id>); NEVER by a heuristic,
2603
+ -- score, LLM, hook, or consolidation pass. Export (B-02) reads WHERE shareable=1
2604
+ -- AND origin='local'. Defaults 0 so every pre-existing and machine-written row is
2605
+ -- un-shareable until a human says otherwise (fail-closed).
2606
+ shareable INTEGER NOT NULL DEFAULT 0,
2398
2607
  created_at TEXT NOT NULL,
2399
2608
  created_at_epoch INTEGER NOT NULL,
2400
2609
  FOREIGN KEY(session_id) REFERENCES sessions(session_id) ON DELETE CASCADE
@@ -2668,7 +2877,13 @@ function initMemorySchema(db) {
2668
2877
  'code_change', 'rule_enforced', 'approval', 'review', 'commit', 'compaction',
2669
2878
  'rule_candidate_emitted', 'rule_promoted', 'rule_dismissed',
2670
2879
  'memory_file_ingested', 'memory_file_expired', 'memory_file_adopted_human',
2671
- 'memory_file_rendered', 'memory_file_render_refused', 'memory_file_tombstoned'
2880
+ 'memory_file_rendered', 'memory_file_render_refused', 'memory_file_tombstoned',
2881
+ -- B-10 (Slice 5): cross-repo shared-memory observability. Without these seven,
2882
+ -- nobody could ever answer "what has crossed between my repos?" (and an ingest
2883
+ -- catch would swallow the CHECK violation, silently producing NOTHING).
2884
+ 'shared_memory_exported', 'shared_memory_export_refused', 'shared_memory_imported',
2885
+ 'shared_memory_dropped', 'shared_memory_accepted', 'shared_memory_refused',
2886
+ 'shared_memory_revoked'
2672
2887
  )),
2673
2888
  actor TEXT NOT NULL DEFAULT 'ai' CHECK(actor IN ('ai', 'human', 'hook', 'agent')),
2674
2889
  model_id TEXT,
@@ -3037,6 +3252,7 @@ function initMemorySchema(db) {
3037
3252
  `);
3038
3253
  migrateObservationEmbeddingChunks(db);
3039
3254
  migrateMemoryFilesFor4B(db);
3255
+ migrateSharedMemoryFor5B(db);
3040
3256
  ensureRuleCandidatesTable(db);
3041
3257
  migrateRuleDelivery(db);
3042
3258
  }
@@ -3907,7 +4123,7 @@ var MEMORY_SCHEMA_VERSION, TOOL_COST_EVENTS_RETENTION_DAYS, MAX_DRAFT_TEXT_LEN,
3907
4123
  var init_memory_db = __esm({
3908
4124
  "src/memory-db.ts"() {
3909
4125
  "use strict";
3910
- init_sqlite_loader();
4126
+ init_db_driver();
3911
4127
  init_config();
3912
4128
  init_memory_vector();
3913
4129
  init_db_backup();
@@ -3932,7 +4148,7 @@ init_memory_db();
3932
4148
  init_rule_delivery();
3933
4149
 
3934
4150
  // src/session-archiver.ts
3935
- import { existsSync as existsSync8, readFileSync as readFileSync5, writeFileSync as writeFileSync3, mkdirSync as mkdirSync7, renameSync } from "fs";
4151
+ import { existsSync as existsSync9, readFileSync as readFileSync6, writeFileSync as writeFileSync3, mkdirSync as mkdirSync7, renameSync } from "fs";
3936
4152
  import { resolve as resolve5, dirname as dirname8 } from "path";
3937
4153
 
3938
4154
  // src/session-state-generator.ts
@@ -4085,12 +4301,12 @@ function archiveAndRegenerate(db, sessionId) {
4085
4301
  const archiveDir = resolved.sessionArchivePath;
4086
4302
  let archived = false;
4087
4303
  let archivePath;
4088
- if (existsSync8(currentMdPath)) {
4089
- const existingContent = readFileSync5(currentMdPath, "utf-8");
4304
+ if (existsSync9(currentMdPath)) {
4305
+ const existingContent = readFileSync6(currentMdPath, "utf-8");
4090
4306
  if (existingContent.trim().length > 10) {
4091
4307
  const { date, slug } = extractArchiveInfo(existingContent);
4092
4308
  archivePath = resolve5(archiveDir, `${date}-${slug}.md`);
4093
- if (!existsSync8(archiveDir)) {
4309
+ if (!existsSync9(archiveDir)) {
4094
4310
  mkdirSync7(archiveDir, { recursive: true });
4095
4311
  }
4096
4312
  try {
@@ -4104,7 +4320,7 @@ function archiveAndRegenerate(db, sessionId) {
4104
4320
  }
4105
4321
  const newContent = generateCurrentMd(db, sessionId);
4106
4322
  const dir = dirname8(currentMdPath);
4107
- if (!existsSync8(dir)) {
4323
+ if (!existsSync9(dir)) {
4108
4324
  mkdirSync7(dir, { recursive: true });
4109
4325
  }
4110
4326
  writeFileSync3(currentMdPath, newContent, "utf-8");
@@ -4214,11 +4430,6 @@ init_rule_delivery();
4214
4430
 
4215
4431
  // src/observation-extractor.ts
4216
4432
  init_memory_db();
4217
-
4218
- // src/adr-generator.ts
4219
- init_config();
4220
-
4221
- // src/observation-extractor.ts
4222
4433
  init_config();
4223
4434
  var PRIVATE_PATTERNS = [
4224
4435
  /\/Users\/\w+/,
@@ -4425,13 +4636,80 @@ async function drainSyncQueue(db) {
4425
4636
  }
4426
4637
  }
4427
4638
  function sleep(ms) {
4428
- return new Promise((resolve6) => setTimeout(resolve6, ms));
4639
+ return new Promise((resolve8) => setTimeout(resolve8, ms));
4640
+ }
4641
+
4642
+ // src/team-rule-sync.ts
4643
+ import { existsSync as existsSync12, writeFileSync as writeFileSync6, unlinkSync as unlinkSync3, mkdirSync as mkdirSync9 } from "fs";
4644
+ import { homedir as homedir6 } from "os";
4645
+ import { join as join6, dirname as dirname10 } from "path";
4646
+
4647
+ // src/security/promotion-apply-mac.ts
4648
+ import { createHmac as createHmac2, timingSafeEqual as timingSafeEqual2 } from "crypto";
4649
+ import { homedir as homedir5 } from "os";
4650
+
4651
+ // src/memory-authorship.ts
4652
+ import { createHmac, randomBytes, timingSafeEqual } from "crypto";
4653
+ import { readFileSync as readFileSync7, writeFileSync as writeFileSync4, existsSync as existsSync10, mkdirSync as mkdirSync8, chmodSync as chmodSync3, statSync as statSync2 } from "fs";
4654
+ import { homedir as homedir4 } from "os";
4655
+ import { resolve as resolve6 } from "path";
4656
+ function renderKeyPath(home = homedir4()) {
4657
+ return resolve6(home, ".massu", "render-key");
4658
+ }
4659
+ function readRenderKey(home = homedir4()) {
4660
+ const p = renderKeyPath(home);
4661
+ try {
4662
+ if (!existsSync10(p)) return void 0;
4663
+ const raw = readFileSync7(p);
4664
+ if (raw.length !== 32) return void 0;
4665
+ return raw;
4666
+ } catch {
4667
+ return void 0;
4668
+ }
4669
+ }
4670
+ function ensureRenderKey(home = homedir4()) {
4671
+ const existing = readRenderKey(home);
4672
+ if (existing) return existing;
4673
+ const p = renderKeyPath(home);
4674
+ try {
4675
+ const dir = resolve6(home, ".massu");
4676
+ mkdirSync8(dir, { recursive: true, mode: 448 });
4677
+ try {
4678
+ chmodSync3(dir, 448);
4679
+ } catch {
4680
+ }
4681
+ const key = randomBytes(32);
4682
+ writeFileSync4(p, key, { mode: 384 });
4683
+ try {
4684
+ chmodSync3(p, 384);
4685
+ } catch {
4686
+ }
4687
+ return key;
4688
+ } catch {
4689
+ return void 0;
4690
+ }
4691
+ }
4692
+
4693
+ // src/security/promotion-apply-mac.ts
4694
+ var DOMAIN = "massu.promotion-apply.v1";
4695
+ function canonical(f) {
4696
+ return [DOMAIN, f.origin, f.org_id, f.prompt_hash, f.destination, f.draft_text].join("\n");
4697
+ }
4698
+ function deriveApplyKey(renderKey) {
4699
+ return createHmac2("sha256", renderKey).update(DOMAIN, "utf8").digest();
4700
+ }
4701
+ function computePromotionApplyMac(fields, home = homedir5()) {
4702
+ try {
4703
+ const key = ensureRenderKey(home);
4704
+ if (!key) return null;
4705
+ return createHmac2("sha256", deriveApplyKey(key)).update(canonical(fields), "utf8").digest("hex");
4706
+ } catch {
4707
+ return null;
4708
+ }
4429
4709
  }
4430
4710
 
4431
4711
  // src/team-rule-sync.ts
4432
4712
  init_config();
4433
- import { existsSync as existsSync9, writeFileSync as writeFileSync4, unlinkSync as unlinkSync2, mkdirSync as mkdirSync8 } from "fs";
4434
- import { join as join5, dirname as dirname9 } from "path";
4435
4713
 
4436
4714
  // src/license.ts
4437
4715
  init_config();
@@ -4482,13 +4760,13 @@ function verifyEd25519SignedEnvelope(key, payload) {
4482
4760
  if (typeof k !== "string") continue;
4483
4761
  canonicalObj[k] = payload[k];
4484
4762
  }
4485
- const canonical = JSON.stringify(canonicalObj, [...payloadKeys].sort());
4763
+ const canonical2 = JSON.stringify(canonicalObj, [...payloadKeys].sort());
4486
4764
  try {
4487
4765
  const der = Buffer.concat([SPKI_ED25519_PREFIX, Buffer.from(key.pubkeyBytes)]);
4488
4766
  const pubkey = createPublicKey({ key: der, format: "der", type: "spki" });
4489
4767
  const ok = cryptoVerify(
4490
4768
  null,
4491
- Buffer.from(canonical, "utf-8"),
4769
+ Buffer.from(canonical2, "utf-8"),
4492
4770
  pubkey,
4493
4771
  Buffer.from(sig, "base64")
4494
4772
  );
@@ -4646,6 +4924,10 @@ var TEAM_SHARED_PROMOTION_MIN_TIER = "team";
4646
4924
  function entitledForTeamSharedPromotion(tier) {
4647
4925
  return tierLevel(tier) >= tierLevel(TEAM_SHARED_PROMOTION_MIN_TIER);
4648
4926
  }
4927
+ var CROSS_REPO_SURFACING_MIN_TIER = "free";
4928
+ function entitledForCrossRepoSurfacing(tier) {
4929
+ return tierLevel(tier) >= tierLevel(CROSS_REPO_SURFACING_MIN_TIER);
4930
+ }
4649
4931
 
4650
4932
  // src/security/promotion-pubkey.generated.ts
4651
4933
  var PROMOTION_PUBKEY_ED25519 = new Uint8Array([107, 161, 33, 17, 189, 44, 193, 128, 252, 155, 188, 236, 100, 163, 23, 146, 219, 155, 216, 139, 134, 72, 211, 182, 151, 122, 209, 151, 135, 65, 167, 26]);
@@ -4667,6 +4949,136 @@ function verifyPromotionEnvelope(payload) {
4667
4949
  );
4668
4950
  }
4669
4951
 
4952
+ // src/lib/safe-write.ts
4953
+ import {
4954
+ writeFileSync as writeFileSync5,
4955
+ renameSync as renameSync2,
4956
+ openSync as openSync2,
4957
+ fsyncSync as fsyncSync2,
4958
+ closeSync as closeSync2,
4959
+ existsSync as existsSync11,
4960
+ realpathSync,
4961
+ unlinkSync as unlinkSync2,
4962
+ accessSync as accessSync2,
4963
+ statSync as statSync3,
4964
+ chmodSync as chmodSync4,
4965
+ constants
4966
+ } from "fs";
4967
+ import { dirname as dirname9, resolve as resolve7, relative, isAbsolute, basename as basename3, sep } from "path";
4968
+ var PathEscapeError = class extends Error {
4969
+ constructor(message) {
4970
+ super(message);
4971
+ this.name = "PathEscapeError";
4972
+ }
4973
+ };
4974
+ var RESERVED_DEVICE_NAMES = /* @__PURE__ */ new Set([
4975
+ "con",
4976
+ "prn",
4977
+ "aux",
4978
+ "nul",
4979
+ "com1",
4980
+ "com2",
4981
+ "com3",
4982
+ "com4",
4983
+ "com5",
4984
+ "com6",
4985
+ "com7",
4986
+ "com8",
4987
+ "com9",
4988
+ "lpt1",
4989
+ "lpt2",
4990
+ "lpt3",
4991
+ "lpt4",
4992
+ "lpt5",
4993
+ "lpt6",
4994
+ "lpt7",
4995
+ "lpt8",
4996
+ "lpt9"
4997
+ ]);
4998
+ function assertContainedIn(rootDir, candidate, opts = {}) {
4999
+ if (candidate.includes("\0")) {
5000
+ throw new PathEscapeError("path contains a NUL byte");
5001
+ }
5002
+ const root = resolve7(rootDir);
5003
+ const abs = resolve7(root, candidate);
5004
+ if (!opts.allowNested) {
5005
+ const rel = relative(root, abs);
5006
+ if (rel.includes(sep)) {
5007
+ throw new PathEscapeError(`path must be a plain basename inside ${root}: ${candidate}`);
5008
+ }
5009
+ }
5010
+ const base = basename3(abs);
5011
+ const stem = base.replace(/\.[^.]*$/, "").toLowerCase();
5012
+ if (RESERVED_DEVICE_NAMES.has(stem)) {
5013
+ throw new PathEscapeError(`reserved device name: ${base}`);
5014
+ }
5015
+ if (!isContainedIn(root, abs)) {
5016
+ throw new PathEscapeError(`path escapes ${root}: ${candidate}`);
5017
+ }
5018
+ return abs;
5019
+ }
5020
+ function isContainedIn(rootDir, candidate, opts = {}) {
5021
+ if (candidate.includes("\0")) return false;
5022
+ const root = resolve7(rootDir);
5023
+ const abs = resolve7(root, candidate);
5024
+ const rel = relative(root, abs);
5025
+ if (rel === "" && !opts.allowRoot) return false;
5026
+ if (rel.startsWith("..") || isAbsolute(rel)) return false;
5027
+ let probe = abs;
5028
+ while (!existsSync11(probe) && probe.length > 1) {
5029
+ const parent = dirname9(probe);
5030
+ if (parent === probe) break;
5031
+ probe = parent;
5032
+ }
5033
+ let realRoot;
5034
+ let realProbe;
5035
+ try {
5036
+ realRoot = realpathSync(root);
5037
+ realProbe = realpathSync(probe);
5038
+ } catch {
5039
+ return true;
5040
+ }
5041
+ const realRel = relative(realRoot, realProbe);
5042
+ if (realRel === "") return true;
5043
+ return !realRel.startsWith("..") && !isAbsolute(realRel);
5044
+ }
5045
+ function atomicWriteFileSync(destPath, contents) {
5046
+ const dir = dirname9(destPath);
5047
+ const tmp = resolve7(dir, `.${basename3(destPath)}.massu-tmp-${process.pid}`);
5048
+ let destMode;
5049
+ if (existsSync11(destPath)) {
5050
+ accessSync2(destPath, constants.W_OK);
5051
+ destMode = statSync3(destPath).mode & 511;
5052
+ }
5053
+ let fd;
5054
+ try {
5055
+ writeFileSync5(tmp, contents, "utf-8");
5056
+ if (destMode !== void 0) chmodSync4(tmp, destMode);
5057
+ fd = openSync2(tmp, "r+");
5058
+ fsyncSync2(fd);
5059
+ closeSync2(fd);
5060
+ fd = void 0;
5061
+ renameSync2(tmp, destPath);
5062
+ } catch (err) {
5063
+ if (fd !== void 0) {
5064
+ try {
5065
+ closeSync2(fd);
5066
+ } catch {
5067
+ }
5068
+ }
5069
+ try {
5070
+ if (existsSync11(tmp)) unlinkSync2(tmp);
5071
+ } catch {
5072
+ }
5073
+ throw err;
5074
+ }
5075
+ }
5076
+ var SLUG_ALLOWED = /^[a-z0-9_]+$/;
5077
+ function deriveSlug(input) {
5078
+ const cleaned = input.toLowerCase().replace(/[^a-z0-9\s]+/g, " ").replace(/\s+/g, "_").replace(/^_+|_+$/g, "");
5079
+ return cleaned.slice(0, 60) || "rule_candidate";
5080
+ }
5081
+
4670
5082
  // src/memory-index-region.ts
4671
5083
  init_fileLock();
4672
5084
 
@@ -4680,6 +5092,12 @@ init_config();
4680
5092
  // src/rule-candidate-applier.ts
4681
5093
  init_memory_db();
4682
5094
 
5095
+ // src/memory-origin.ts
5096
+ var LOCAL_ORIGIN = "local";
5097
+ function isLocalOrigin(o) {
5098
+ return o === LOCAL_ORIGIN;
5099
+ }
5100
+
4683
5101
  // src/rule-candidate-funnel.ts
4684
5102
  init_memory_db();
4685
5103
 
@@ -4805,11 +5223,11 @@ async function pullTeamPromotions(db, opts = {}) {
4805
5223
  result.revoked_handled += 1;
4806
5224
  continue;
4807
5225
  }
4808
- if (existsSync9(candidatePath) || alreadyApplied(db, p.prompt_hash)) {
5226
+ if (existsSync12(candidatePath) || alreadyApplied(db, p.prompt_hash)) {
4809
5227
  result.skipped += 1;
4810
5228
  continue;
4811
5229
  }
4812
- materializeCandidate(db, projectRoot, candidatePath, p, signedOrgId);
5230
+ materializeCandidate(db, projectRoot, candidatePath, p, signedOrgId, opts.home ?? homedir6());
4813
5231
  result.materialized += 1;
4814
5232
  }
4815
5233
  const serverCursor = typeof envelope.cursor === "number" ? envelope.cursor : 0;
@@ -4841,7 +5259,7 @@ function isValidWirePromotion(p) {
4841
5259
  return typeof r.prompt_hash === "string" && PROMPT_HASH_RE.test(r.prompt_hash) && typeof r.destination === "string" && typeof r.draft_text === "string" && typeof r.promoted_by === "string" && typeof r.promoted_at === "string";
4842
5260
  }
4843
5261
  function sidecarPath(projectRoot, promptHash) {
4844
- return join5(projectRoot, ".massu", "rule-candidates", `${promptHash}.json`);
5262
+ return join6(projectRoot, ".massu", "rule-candidates", `${promptHash}.json`);
4845
5263
  }
4846
5264
  function alreadyApplied(db, promptHash) {
4847
5265
  try {
@@ -4855,9 +5273,9 @@ function alreadyApplied(db, promptHash) {
4855
5273
  }
4856
5274
  }
4857
5275
  function handleRevocation(db, projectRoot, candidatePath, promptHash) {
4858
- if (existsSync9(candidatePath)) {
5276
+ if (existsSync12(candidatePath)) {
4859
5277
  try {
4860
- unlinkSync2(candidatePath);
5278
+ unlinkSync3(candidatePath);
4861
5279
  } catch {
4862
5280
  }
4863
5281
  return;
@@ -4869,8 +5287,12 @@ function handleRevocation(db, projectRoot, candidatePath, promptHash) {
4869
5287
  );
4870
5288
  }
4871
5289
  }
4872
- function materializeCandidate(db, projectRoot, candidatePath, p, orgId) {
5290
+ function materializeCandidate(db, projectRoot, candidatePath, p, orgId, home) {
4873
5291
  const promptText = p.draft_text.replace(/\n+/g, " ").slice(0, 200) || `team rule ${p.prompt_hash}`;
5292
+ const applyMac = computePromotionApplyMac(
5293
+ { origin: "team", org_id: orgId, prompt_hash: p.prompt_hash, destination: p.destination, draft_text: p.draft_text },
5294
+ home
5295
+ );
4874
5296
  const sidecar = {
4875
5297
  // Standard RuleCandidatePayload fields (so `/massu-rule approve` → readCandidate
4876
5298
  // → validateCandidatePayload passes), synthesized from the promotion.
@@ -4893,6 +5315,7 @@ function materializeCandidate(db, projectRoot, candidatePath, p, orgId) {
4893
5315
  promoted_by: p.promoted_by,
4894
5316
  promoted_at: p.promoted_at,
4895
5317
  signature_verified: true,
5318
+ ...applyMac ? { apply_mac: applyMac } : {},
4896
5319
  ...p.hardened === true ? { hardened: true } : {}
4897
5320
  },
4898
5321
  // Extra fields the `/massu-rule approve` flow reads to drive the apply (the
@@ -4903,9 +5326,9 @@ function materializeCandidate(db, projectRoot, candidatePath, p, orgId) {
4903
5326
  draft_text: p.draft_text,
4904
5327
  ...p.review_attestation !== void 0 ? { publisher_review_attestation: p.review_attestation } : {}
4905
5328
  };
4906
- const dir = dirname9(candidatePath);
4907
- if (!existsSync9(dir)) mkdirSync8(dir, { recursive: true });
4908
- writeFileSync4(candidatePath, JSON.stringify(sidecar, null, 2), "utf-8");
5329
+ const dir = dirname10(candidatePath);
5330
+ if (!existsSync12(dir)) mkdirSync9(dir, { recursive: true });
5331
+ writeFileSync6(candidatePath, JSON.stringify(sidecar, null, 2), "utf-8");
4909
5332
  try {
4910
5333
  shareObservation(db, p.promoted_by, getProjectName(), "rule_promotion", promptText, {
4911
5334
  filePath: void 0,
@@ -4956,124 +5379,948 @@ function failSync(db, reason) {
4956
5379
  return { ...ZERO, sync_error: reason };
4957
5380
  }
4958
5381
 
4959
- // src/hooks/session-end.ts
5382
+ // src/shared-memory-sync.ts
4960
5383
  init_config();
5384
+ import { homedir as homedir10 } from "os";
4961
5385
 
4962
- // src/analytics.ts
5386
+ // src/memory-repo-identity.ts
4963
5387
  init_config();
4964
- var DEFAULT_WEIGHTS = {
4965
- bug_found: -5,
4966
- vr_failure: -10,
4967
- incident: -20,
4968
- cr_violation: -3,
4969
- vr_pass: 2,
4970
- clean_commit: 5,
4971
- successful_verification: 3
4972
- };
4973
- var DEFAULT_CATEGORIES = ["security", "architecture", "coupling", "tests", "rule_compliance"];
4974
- function getWeights() {
4975
- return getConfig().analytics?.quality?.weights ?? DEFAULT_WEIGHTS;
4976
- }
4977
- function getCategories() {
4978
- return getConfig().analytics?.quality?.categories ?? DEFAULT_CATEGORIES;
5388
+ init_memory_db();
5389
+ import { randomUUID as randomUUID2 } from "crypto";
5390
+ var REPO_ID_META_KEY = "repo_id";
5391
+ var SHARED_PIN_META_PREFIX = "shared_pin:";
5392
+ var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
5393
+ function getRepoId(db) {
5394
+ const v = getMemoryMeta(db, REPO_ID_META_KEY);
5395
+ return v && UUID_RE.test(v) ? v : null;
5396
+ }
5397
+ function mintRepoId(db) {
5398
+ const existing = getRepoId(db);
5399
+ if (existing) return existing;
5400
+ const id = randomUUID2();
5401
+ setMemoryMeta(db, REPO_ID_META_KEY, id);
5402
+ return id;
5403
+ }
5404
+ function deriveRepoLabel(projectName) {
5405
+ if (!projectName || !projectName.trim()) return "repo";
5406
+ const slug = deriveSlug(projectName);
5407
+ return slug && SLUG_ALLOWED.test(slug) ? slug : "repo";
5408
+ }
5409
+ function getRepoLabel() {
5410
+ return deriveRepoLabel(getConfig().project.name);
5411
+ }
5412
+ function getSharedPin(db, originRepoId) {
5413
+ return getMemoryMeta(db, SHARED_PIN_META_PREFIX + originRepoId);
5414
+ }
5415
+ function tofuPinSharedFingerprint(db, originRepoId, fingerprint) {
5416
+ const existing = getSharedPin(db, originRepoId);
5417
+ if (existing) return existing;
5418
+ setMemoryMeta(db, SHARED_PIN_META_PREFIX + originRepoId, fingerprint);
5419
+ return fingerprint;
4979
5420
  }
4980
- function calculateQualityScore(db, sessionId) {
4981
- const weights = getWeights();
4982
- const categories = getCategories();
4983
- const observations = db.prepare(
4984
- "SELECT type, detail FROM observations WHERE session_id = ? LIMIT 10000"
4985
- ).all(sessionId);
4986
- let score = 50;
4987
- const breakdown = Object.fromEntries(
4988
- categories.map((c) => [c, 0])
4989
- );
4990
- for (const obs of observations) {
4991
- const weight = weights[obs.type] ?? 0;
4992
- score += weight;
4993
- const desc = (obs.detail ?? "").toLowerCase();
4994
- for (const category of categories) {
4995
- if (desc.includes(category)) {
4996
- breakdown[category] += weight;
4997
- }
4998
- }
5421
+
5422
+ // src/memory-repos-registry.ts
5423
+ import {
5424
+ chmodSync as chmodSync5,
5425
+ existsSync as existsSync13,
5426
+ mkdirSync as mkdirSync10,
5427
+ readFileSync as readFileSync9,
5428
+ writeFileSync as writeFileSync7
5429
+ } from "fs";
5430
+ import { homedir as homedir7 } from "os";
5431
+ import { dirname as dirname11, join as join7 } from "path";
5432
+ var REPOS_REGISTRY_VERSION = 1;
5433
+ function reposRegistryPath(home = homedir7()) {
5434
+ return join7(home, ".massu", "repos.json");
5435
+ }
5436
+ function emptyRegistry() {
5437
+ return { version: REPOS_REGISTRY_VERSION, repos: [] };
5438
+ }
5439
+ function readReposRegistry(home = homedir7()) {
5440
+ const p = reposRegistryPath(home);
5441
+ if (!existsSync13(p)) return emptyRegistry();
5442
+ try {
5443
+ const parsed = JSON.parse(readFileSync9(p, "utf-8"));
5444
+ if (!parsed || typeof parsed !== "object") return emptyRegistry();
5445
+ const r = parsed;
5446
+ if (!Array.isArray(r.repos)) return emptyRegistry();
5447
+ const repos = r.repos.filter(
5448
+ (e) => !!e && typeof e === "object" && typeof e.repo_id === "string" && typeof e.label === "string"
5449
+ );
5450
+ return { version: typeof r.version === "number" ? r.version : REPOS_REGISTRY_VERSION, repos };
5451
+ } catch {
5452
+ return emptyRegistry();
4999
5453
  }
5454
+ }
5455
+ function findRepoByLabel(home, label) {
5456
+ return readReposRegistry(home).repos.find((e) => e.label === label) ?? null;
5457
+ }
5458
+ function upsertRepoRegistration(entry, home = homedir7()) {
5459
+ const registry = readReposRegistry(home);
5460
+ const next = registry.repos.filter((e) => e.repo_id !== entry.repo_id);
5461
+ next.push(entry);
5462
+ const out = { version: REPOS_REGISTRY_VERSION, repos: next };
5463
+ const p = reposRegistryPath(home);
5464
+ mkdirSync10(dirname11(p), { recursive: true, mode: 448 });
5465
+ writeFileSync7(p, JSON.stringify(out, null, 2), { mode: 384 });
5466
+ chmodSync5(p, 384);
5467
+ }
5468
+
5469
+ // src/security/local-share-verifier.ts
5470
+ import { homedir as homedir9 } from "os";
5471
+
5472
+ // src/security/local-share-signer.ts
5473
+ import {
5474
+ chmodSync as chmodSync6,
5475
+ existsSync as existsSync14,
5476
+ mkdirSync as mkdirSync11,
5477
+ readFileSync as readFileSync10,
5478
+ writeFileSync as writeFileSync8
5479
+ } from "fs";
5480
+ import { homedir as homedir8 } from "os";
5481
+ import { join as join8 } from "path";
5482
+ import {
5483
+ createHash as createHash3,
5484
+ createPrivateKey,
5485
+ createPublicKey as createPublicKey2,
5486
+ generateKeyPairSync,
5487
+ sign as cryptoSign
5488
+ } from "crypto";
5489
+
5490
+ // src/shared-memory-envelope.ts
5491
+ import { createHash as createHash2 } from "crypto";
5492
+ var SHARED_MEMORY_KIND = "massu.shared-memory.v1";
5493
+ var SHARED_MEMORY_SIGNATURE_PAYLOAD_KEYS = [
5494
+ "issued_at",
5495
+ "kind",
5496
+ "origin_repo_id",
5497
+ "origin_repo_label",
5498
+ "records_json",
5499
+ "revokes_json",
5500
+ "seq"
5501
+ ];
5502
+ function canonicalizeSharedMemoryEnvelope(body) {
5503
+ const keys = SHARED_MEMORY_SIGNATURE_PAYLOAD_KEYS;
5504
+ const canonicalObj = {};
5505
+ for (const k of keys) {
5506
+ canonicalObj[k] = body[k];
5507
+ }
5508
+ return JSON.stringify(canonicalObj, [...keys].sort());
5509
+ }
5510
+ function hashSharedMemoryRecord(record) {
5511
+ const canonical2 = JSON.stringify({
5512
+ created_at_epoch: record.created_at_epoch,
5513
+ detail: record.detail,
5514
+ importance: record.importance,
5515
+ superseded_by_hash: record.superseded_by_hash,
5516
+ title: record.title,
5517
+ type: record.type
5518
+ });
5519
+ return createHash2("sha256").update(canonical2, "utf-8").digest("hex");
5520
+ }
5521
+
5522
+ // src/security/local-share-signer.ts
5523
+ function localShareKeyDir(home = homedir8()) {
5524
+ return join8(home, ".massu", "keys");
5525
+ }
5526
+ function localSharePrivKeyPath(home = homedir8()) {
5527
+ return join8(localShareKeyDir(home), "local-share.key");
5528
+ }
5529
+ function localSharePubKeyPath(home = homedir8()) {
5530
+ return join8(localShareKeyDir(home), "local-share.pub");
5531
+ }
5532
+ var SPKI_ED25519_PREFIX_LEN = 12;
5533
+ function ensureLocalShareKeypair(home = homedir8()) {
5534
+ const privPath = localSharePrivKeyPath(home);
5535
+ if (existsSync14(privPath)) return;
5536
+ const { publicKey, privateKey } = generateKeyPairSync("ed25519");
5537
+ const privPem = privateKey.export({ format: "pem", type: "pkcs8" });
5538
+ const pubPem = publicKey.export({ format: "pem", type: "spki" });
5539
+ mkdirSync11(localShareKeyDir(home), { recursive: true, mode: 448 });
5540
+ writeFileSync8(privPath, privPem, { mode: 384 });
5541
+ chmodSync6(privPath, 384);
5542
+ writeFileSync8(localSharePubKeyPath(home), pubPem, { mode: 420 });
5543
+ }
5544
+ function readLocalSharePublicKeyRaw(home = homedir8()) {
5545
+ const pubPem = readFileSync10(localSharePubKeyPath(home), "utf-8");
5546
+ const der = createPublicKey2(pubPem).export({ format: "der", type: "spki" });
5547
+ return Uint8Array.from(der.subarray(SPKI_ED25519_PREFIX_LEN));
5548
+ }
5549
+ function localSharePubkeyFingerprint(home = homedir8()) {
5550
+ return createHash3("sha256").update(Buffer.from(readLocalSharePublicKeyRaw(home))).digest("hex");
5551
+ }
5552
+ function signSharedMemoryEnvelope(body, home = homedir8()) {
5553
+ ensureLocalShareKeypair(home);
5554
+ const privPem = readFileSync10(localSharePrivKeyPath(home), "utf-8");
5555
+ const privKey = createPrivateKey(privPem);
5556
+ const canonical2 = canonicalizeSharedMemoryEnvelope(body);
5557
+ const signature = cryptoSign(null, Buffer.from(canonical2, "utf-8"), privKey).toString("base64");
5000
5558
  return {
5001
- score: Math.max(0, Math.min(100, score)),
5002
- breakdown
5559
+ ...body,
5560
+ _signature: signature,
5561
+ _signature_alg: "ed25519",
5562
+ _signature_payload_keys: SHARED_MEMORY_SIGNATURE_PAYLOAD_KEYS,
5563
+ _signature_pubkey_fingerprint: localSharePubkeyFingerprint(home)
5003
5564
  };
5004
5565
  }
5005
- function storeQualityScore(db, sessionId, score, breakdown) {
5006
- db.prepare(`
5007
- INSERT INTO session_quality_scores
5008
- (session_id, score, security_score, architecture_score, coupling_score, test_score, rule_compliance_score)
5009
- VALUES (?, ?, ?, ?, ?, ?, ?)
5010
- `).run(
5011
- sessionId,
5012
- score,
5013
- breakdown.security ?? 0,
5014
- breakdown.architecture ?? 0,
5015
- breakdown.coupling ?? 0,
5016
- breakdown.tests ?? 0,
5017
- breakdown.rule_compliance ?? 0
5018
- );
5019
- }
5020
- function backfillQualityScores(db) {
5021
- const sessions = db.prepare(`
5022
- SELECT DISTINCT s.session_id
5023
- FROM sessions s
5024
- LEFT JOIN session_quality_scores q ON s.session_id = q.session_id
5025
- WHERE q.session_id IS NULL
5026
- LIMIT 100000
5027
- `).all();
5028
- let backfilled = 0;
5029
- for (const session of sessions) {
5030
- const { score, breakdown } = calculateQualityScore(db, session.session_id);
5031
- storeQualityScore(db, session.session_id, score, breakdown);
5032
- backfilled++;
5566
+
5567
+ // src/security/local-share-verifier.ts
5568
+ function verifyLocalShareEnvelope(envelope, pinnedFingerprint, home = homedir9()) {
5569
+ let key;
5570
+ try {
5571
+ const pubkeyBytes = readLocalSharePublicKeyRaw(home);
5572
+ key = {
5573
+ pubkeyBytes,
5574
+ // The fingerprint of the key we are ABOUT to verify with (the on-disk key).
5575
+ fingerprintHex: localSharePubkeyFingerprint(home),
5576
+ // The trusted set comes from the PIN — a different artifact than the key.
5577
+ // If the on-disk key was swapped, fingerprintHex ∉ this set → the core's
5578
+ // self-check fails → hard drop. This Set is deliberately NOT built from
5579
+ // `fingerprintHex`/`pubkeyBytes` (that would be vacuous — defect D4).
5580
+ knownFingerprints: /* @__PURE__ */ new Set([pinnedFingerprint]),
5581
+ keyLabel: "local-share"
5582
+ };
5583
+ } catch (err) {
5584
+ return {
5585
+ kind: "error",
5586
+ reason: `local-share pubkey unavailable: ${err instanceof Error ? err.message : String(err)}`
5587
+ };
5033
5588
  }
5034
- return backfilled;
5589
+ return verifyEd25519SignedEnvelope(key, envelope);
5035
5590
  }
5036
5591
 
5037
- // src/cost-tracker.ts
5038
- init_config();
5039
- var DEFAULT_MODEL_PRICING = {
5040
- "claude-opus-4-6": { input_per_million: 15, output_per_million: 75, cache_read_per_million: 1.5, cache_write_per_million: 18.75 },
5041
- "claude-sonnet-4-6": { input_per_million: 3, output_per_million: 15, cache_read_per_million: 0.3, cache_write_per_million: 3.75 },
5042
- "claude-sonnet-4-5": { input_per_million: 3, output_per_million: 15, cache_read_per_million: 0.3, cache_write_per_million: 3.75 },
5043
- "claude-haiku-4-5-20251001": { input_per_million: 0.8, output_per_million: 4, cache_read_per_million: 0.08, cache_write_per_million: 1 },
5044
- "default": { input_per_million: 3, output_per_million: 15, cache_read_per_million: 0.3, cache_write_per_million: 3.75 }
5045
- };
5046
- function getModelPricing() {
5047
- return getConfig().analytics?.cost?.models ?? DEFAULT_MODEL_PRICING;
5048
- }
5049
- function getCurrency() {
5050
- return getConfig().analytics?.cost?.currency ?? "USD";
5592
+ // src/shared-memory-sync.ts
5593
+ init_memory_db();
5594
+
5595
+ // src/shared-memory-audit.ts
5596
+ function latestSessionId(db) {
5597
+ try {
5598
+ const row = db.prepare(`SELECT session_id FROM sessions ORDER BY started_at_epoch DESC LIMIT 1`).get();
5599
+ return row?.session_id ?? null;
5600
+ } catch {
5601
+ return null;
5602
+ }
5051
5603
  }
5052
- function extractTokenUsage(entries) {
5053
- let inputTokens = 0;
5054
- let outputTokens = 0;
5055
- let cacheReadTokens = 0;
5056
- let cacheWriteTokens = 0;
5057
- let model = "unknown";
5058
- for (const entry of entries) {
5059
- const msg = entry.message;
5060
- if (entry.type === "assistant" && msg?.usage) {
5061
- const usage = msg.usage;
5062
- inputTokens += usage.input_tokens ?? 0;
5063
- outputTokens += usage.output_tokens ?? 0;
5064
- cacheReadTokens += usage.cache_read_input_tokens ?? usage.cache_read_tokens ?? 0;
5065
- cacheWriteTokens += usage.cache_creation_input_tokens ?? usage.cache_write_tokens ?? 0;
5066
- }
5067
- if (entry.type === "assistant" && msg?.model) {
5068
- model = msg.model;
5069
- }
5604
+ function writeSharedAudit(db, eventType, evidence, metadata, actor = "hook") {
5605
+ const sessionId = latestSessionId(db);
5606
+ if (!sessionId) return;
5607
+ try {
5608
+ db.prepare(
5609
+ `INSERT INTO audit_log (session_id, event_type, actor, evidence, metadata)
5610
+ VALUES (?, ?, ?, ?, ?)`
5611
+ ).run(sessionId, eventType, actor, evidence, JSON.stringify(metadata));
5612
+ } catch {
5070
5613
  }
5071
- return { inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens, model };
5072
5614
  }
5073
- function calculateCost(usage) {
5074
- const pricing = getModelPricing();
5075
- const modelPricing = pricing[usage.model] ?? pricing["default"] ?? pricing["claude-sonnet-4-5"] ?? { input_per_million: 3, output_per_million: 15 };
5076
- const inputCost = usage.inputTokens / 1e6 * modelPricing.input_per_million;
5615
+
5616
+ // src/shared-memory-sync.ts
5617
+ var REPO_ID_RE = /^[0-9a-f-]{36}$/;
5618
+ var SHARED_CURSOR_PREFIX = "shared_cursor:";
5619
+ var ZERO_IMPORT = { imported: 0, dropped: 0, revoked: 0, skipped: 0 };
5620
+ async function importSharedMemories(db, transport, opts = {}) {
5621
+ const home = opts.home ?? homedir10();
5622
+ const nowEpoch = opts.nowEpoch ?? Math.floor(Date.now() / 1e3);
5623
+ const config = getConfig();
5624
+ const tier = opts.tier ?? getCachedTierReadOnly(db);
5625
+ if (!entitledForCrossRepoSurfacing(tier)) return { ...ZERO_IMPORT };
5626
+ const subscribe = opts.subscribe ?? config.memory?.share?.subscribe ?? [];
5627
+ if (!Array.isArray(subscribe) || subscribe.length === 0) return { ...ZERO_IMPORT };
5628
+ const ownRepoId = getRepoId(db);
5629
+ const result = { ...ZERO_IMPORT };
5630
+ for (const subscribedLabel of subscribe) {
5631
+ const entry = findRepoByLabel(home, subscribedLabel);
5632
+ if (!entry || !REPO_ID_RE.test(entry.repo_id)) continue;
5633
+ const originRepoId = entry.repo_id;
5634
+ if (ownRepoId && originRepoId === ownRepoId) continue;
5635
+ const pinned = getSharedPin(db, originRepoId) ?? tofuPinSharedFingerprint(db, originRepoId, entry.pubkey_fingerprint);
5636
+ const cursorKey = SHARED_CURSOR_PREFIX + originRepoId;
5637
+ const since = parseCursor2(getMemoryMeta(db, cursorKey));
5638
+ let envelopes;
5639
+ try {
5640
+ envelopes = await transport.fetchSince(originRepoId, since);
5641
+ } catch {
5642
+ continue;
5643
+ }
5644
+ let maxSeq = since;
5645
+ const advancePast = (env) => {
5646
+ if (typeof env.seq === "number" && env.seq > maxSeq) maxSeq = env.seq;
5647
+ };
5648
+ for (const env of envelopes) {
5649
+ const verdict = verifyLocalShareEnvelope(env, pinned, home);
5650
+ if (verdict.kind !== "valid") {
5651
+ result.dropped += 1;
5652
+ drop(db, "bad_signature", { kind: verdict.kind, origin: originRepoId });
5653
+ if (verdict.kind === "bad_signature") advancePast(env);
5654
+ continue;
5655
+ }
5656
+ if (!hasSignedLoadBearingKeys(env)) {
5657
+ result.dropped += 1;
5658
+ drop(db, "unsigned_field", { origin: originRepoId });
5659
+ advancePast(env);
5660
+ continue;
5661
+ }
5662
+ if (env.origin_repo_id !== originRepoId) {
5663
+ result.dropped += 1;
5664
+ drop(db, "origin_mismatch", { expected: originRepoId, got: env.origin_repo_id });
5665
+ advancePast(env);
5666
+ continue;
5667
+ }
5668
+ if (ownRepoId && env.origin_repo_id === ownRepoId) {
5669
+ result.dropped += 1;
5670
+ advancePast(env);
5671
+ continue;
5672
+ }
5673
+ advancePast(env);
5674
+ for (const revokedHash of parseStringArray(env.revokes_json)) {
5675
+ if (applyRevocation(db, revokedHash, nowEpoch)) result.revoked += 1;
5676
+ }
5677
+ const originLabel = deriveRepoLabel(entry.label);
5678
+ for (const rec of parseRecords(env.records_json)) {
5679
+ if (!isWellFormedRecord(rec)) {
5680
+ result.dropped += 1;
5681
+ drop(db, "malformed_record", { origin: originRepoId });
5682
+ continue;
5683
+ }
5684
+ const computed = hashSharedMemoryRecord(stripHash(rec));
5685
+ if (computed !== rec.record_hash) {
5686
+ result.dropped += 1;
5687
+ drop(db, "record_hash_mismatch", { origin: originRepoId });
5688
+ continue;
5689
+ }
5690
+ if (pendingExists(db, rec.record_hash)) {
5691
+ result.skipped += 1;
5692
+ continue;
5693
+ }
5694
+ insertPending(db, rec, env, originRepoId, originLabel, nowEpoch);
5695
+ result.imported += 1;
5696
+ writeSharedAudit(db, "shared_memory_imported", `pending from ${originLabel}`, {
5697
+ record_hash: rec.record_hash,
5698
+ origin_repo_id: originRepoId
5699
+ });
5700
+ }
5701
+ }
5702
+ if (maxSeq > since) setMemoryMeta(db, cursorKey, String(maxSeq));
5703
+ }
5704
+ return result;
5705
+ }
5706
+ var SIGNED_LOAD_BEARING_KEYS = ["origin_repo_id", "records_json", "revokes_json"];
5707
+ function hasSignedLoadBearingKeys(env) {
5708
+ const signed = Array.isArray(env._signature_payload_keys) ? env._signature_payload_keys : [];
5709
+ return SIGNED_LOAD_BEARING_KEYS.every((k) => signed.includes(k));
5710
+ }
5711
+ function parseCursor2(raw) {
5712
+ if (raw === null) return 0;
5713
+ const n = Number(raw);
5714
+ return Number.isInteger(n) && n >= 0 ? n : 0;
5715
+ }
5716
+ function parseStringArray(json) {
5717
+ try {
5718
+ const v = JSON.parse(json);
5719
+ return Array.isArray(v) ? v.filter((x) => typeof x === "string") : [];
5720
+ } catch {
5721
+ return [];
5722
+ }
5723
+ }
5724
+ function parseRecords(json) {
5725
+ try {
5726
+ const v = JSON.parse(json);
5727
+ return Array.isArray(v) ? v : [];
5728
+ } catch {
5729
+ return [];
5730
+ }
5731
+ }
5732
+ function isWellFormedRecord(r) {
5733
+ if (!r || typeof r !== "object") return false;
5734
+ const o = r;
5735
+ return typeof o.record_hash === "string" && typeof o.type === "string" && typeof o.title === "string" && typeof o.detail === "string" && typeof o.importance === "number" && typeof o.created_at_epoch === "number" && (o.superseded_by_hash === null || typeof o.superseded_by_hash === "string");
5736
+ }
5737
+ function stripHash(r) {
5738
+ return {
5739
+ type: r.type,
5740
+ title: r.title,
5741
+ detail: r.detail,
5742
+ importance: r.importance,
5743
+ created_at_epoch: r.created_at_epoch,
5744
+ superseded_by_hash: r.superseded_by_hash
5745
+ };
5746
+ }
5747
+ function pendingExists(db, recordHash) {
5748
+ return !!db.prepare(`SELECT 1 FROM shared_memory_pending WHERE record_hash = ? LIMIT 1`).get(recordHash);
5749
+ }
5750
+ function loadPending(db, recordHash) {
5751
+ return db.prepare(
5752
+ `SELECT id, record_hash, origin_repo_id, origin_repo_label, envelope_raw, record_json,
5753
+ accepted_at_epoch, refused_at_epoch, expired_at_epoch
5754
+ FROM shared_memory_pending WHERE record_hash = ? LIMIT 1`
5755
+ ).get(recordHash) ?? null;
5756
+ }
5757
+ function insertPending(db, rec, env, originRepoId, originLabel, nowEpoch) {
5758
+ db.prepare(
5759
+ `INSERT OR IGNORE INTO shared_memory_pending
5760
+ (record_hash, origin_repo_id, origin_repo_label, envelope_raw, record_json, received_at_epoch)
5761
+ VALUES (?, ?, ?, ?, ?, ?)`
5762
+ ).run(rec.record_hash, originRepoId, originLabel, JSON.stringify(env), JSON.stringify(rec), nowEpoch);
5763
+ }
5764
+ function applyRevocation(db, recordHash, nowEpoch) {
5765
+ const row = loadPending(db, recordHash);
5766
+ if (!row) return false;
5767
+ let changed = false;
5768
+ if (row.accepted_at_epoch !== null) {
5769
+ const iso = new Date(nowEpoch * 1e3).toISOString();
5770
+ const res = db.prepare(
5771
+ `UPDATE observations
5772
+ SET valid_to = ?, expired_at = ?, valid_to_epoch = ?, expired_at_epoch = ?
5773
+ WHERE origin = ?
5774
+ AND expired_at IS NULL
5775
+ AND json_extract(evidence, '$.record_hash') = ?`
5776
+ ).run(iso, iso, nowEpoch, nowEpoch, `repo:${row.origin_repo_id}`, recordHash);
5777
+ changed = res.changes > 0;
5778
+ }
5779
+ const p = db.prepare(`UPDATE shared_memory_pending SET expired_at_epoch = ? WHERE record_hash = ? AND expired_at_epoch IS NULL`).run(nowEpoch, recordHash);
5780
+ changed = changed || p.changes > 0;
5781
+ if (changed) writeSharedAudit(db, "shared_memory_revoked", `revoked ${recordHash}`, { record_hash: recordHash });
5782
+ return changed;
5783
+ }
5784
+ function drop(db, reason, meta) {
5785
+ recordTelemetry(db, "shared_memory_dropped", { reason, ...meta });
5786
+ writeSharedAudit(db, "shared_memory_dropped", `dropped: ${reason}`, { reason, ...meta });
5787
+ }
5788
+
5789
+ // src/shared-memory-export.ts
5790
+ init_config();
5791
+ import { homedir as homedir11 } from "os";
5792
+ import { join as join9, dirname as dirname12 } from "path";
5793
+ import { mkdirSync as mkdirSync12, chmodSync as chmodSync7, writeFileSync as writeFileSync9, renameSync as renameSync3, openSync as openSync3, fsyncSync as fsyncSync3, closeSync as closeSync3 } from "fs";
5794
+
5795
+ // src/consolidation-config.ts
5796
+ init_config();
5797
+ var DEFAULT_CONSOLIDATION_CONFIG = {
5798
+ enabled: true,
5799
+ sessionSweepEnabled: true,
5800
+ // llmEndpoint / llmModel deliberately UNSET: the shipped default is
5801
+ // zero-LLM, zero-network, works offline on any machine.
5802
+ summarizeAfterDays: 5,
5803
+ // inside the 7-day conversation_turns prune window
5804
+ retentionDays: 90,
5805
+ importanceFloor: 2,
5806
+ protectedTypes: ["decision", "cr_violation", "incident_near_miss"],
5807
+ usageWarmupDays: 30,
5808
+ usageDecay: 0.9,
5809
+ reweightIntervalDays: 1,
5810
+ promoteMinOccurrences: 3,
5811
+ budgetMs: 3e3,
5812
+ suggestUpgrades: true,
5813
+ suggestIntervalDays: 30
5814
+ };
5815
+ function resolveConsolidationConfig() {
5816
+ try {
5817
+ const c = getConfig().memory?.consolidation;
5818
+ if (!c) return { ...DEFAULT_CONSOLIDATION_CONFIG };
5819
+ return { ...DEFAULT_CONSOLIDATION_CONFIG, ...c };
5820
+ } catch {
5821
+ return { ...DEFAULT_CONSOLIDATION_CONFIG };
5822
+ }
5823
+ }
5824
+
5825
+ // src/memory-llm.ts
5826
+ var LLM_API_KEY_ENV = "MASSU_MEMORY_LLM_API_KEY";
5827
+ var DEFAULT_BUDGET_MS = 2e4;
5828
+ var DEFAULT_MAX_CHARS = 900;
5829
+ var NOISE_PATTERNS = [
5830
+ /<command-(name|message|args)>/i,
5831
+ /<local-command-[^>]*>/i,
5832
+ /^\s*<[a-z-]+>\s*$/i,
5833
+ /^\s*(ok|okay|thanks|thank you|yes|no|yep|nope|sure|continue|proceed|go ahead)\s*[.!]?\s*$/i,
5834
+ /^\s*\/[a-z-]+\s*$/i,
5835
+ // a bare slash-command invocation
5836
+ /system-reminder/i
5837
+ ];
5838
+ function isSummarizableSignal(text) {
5839
+ const t = text.trim();
5840
+ if (t.length < 25) return false;
5841
+ return !NOISE_PATTERNS.some((re) => re.test(t));
5842
+ }
5843
+ var CREDENTIAL_LABELS = ["api[_-]?key", "secret", "password", "token"];
5844
+ var LABELLED_CREDENTIAL = new RegExp(
5845
+ `\\b[A-Za-z0-9_-]*(?:${CREDENTIAL_LABELS.join("|")})["'\\s:=]+[A-Za-z0-9_\\-/+]{16,}`,
5846
+ "gi"
5847
+ );
5848
+ var SECRET_PATTERNS = [
5849
+ [/\bms_live_[A-Za-z0-9_-]{6,}/g, "ms_live_[REDACTED]", "MASSU_LIVE_KEY"],
5850
+ [/\bsk-[A-Za-z0-9_-]{16,}/g, "sk-[REDACTED]", "OPENAI_STYLE_KEY"],
5851
+ [/\b(gh[pousr]|github_pat)_[A-Za-z0-9_]{16,}/g, "[REDACTED_TOKEN]", "GITHUB_TOKEN"],
5852
+ [/\bsbp_[A-Za-z0-9]{16,}/g, "sbp_[REDACTED]", "SUPABASE_TOKEN"],
5853
+ [/\bAKIA[0-9A-Z]{12,}/g, "[REDACTED_AWS_KEY]", "AWS_ACCESS_KEY"],
5854
+ [
5855
+ /\bey[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{6,}/g,
5856
+ "[REDACTED_JWT]",
5857
+ "JWT"
5858
+ ],
5859
+ [LABELLED_CREDENTIAL, "[REDACTED_CREDENTIAL]", "LABELLED_CREDENTIAL"]
5860
+ ];
5861
+ function redactSecrets(text) {
5862
+ let out = text;
5863
+ for (const [re, replacement] of SECRET_PATTERNS) out = out.replace(re, replacement);
5864
+ return out;
5865
+ }
5866
+ function containsSecret(text) {
5867
+ for (const [re, , name] of SECRET_PATTERNS) {
5868
+ re.lastIndex = 0;
5869
+ const hit = re.test(text);
5870
+ re.lastIndex = 0;
5871
+ if (hit) return { matched: true, patternName: name };
5872
+ }
5873
+ return { matched: false };
5874
+ }
5875
+ function extractiveSummary(sources, maxChars = DEFAULT_MAX_CHARS) {
5876
+ const cleaned = sources.map((s, i) => ({ ...s, i, text: s.text.replace(/\s+/g, " ").trim() })).filter((s) => isSummarizableSignal(s.text));
5877
+ if (cleaned.length === 0) return "";
5878
+ const ranked = [...cleaned].sort((a, b) => b.weight - a.weight || a.i - b.i);
5879
+ const picked = [];
5880
+ let used = 0;
5881
+ for (const s of ranked) {
5882
+ const cost = s.text.length + 1;
5883
+ if (used + cost > maxChars) continue;
5884
+ picked.push(s);
5885
+ used += cost;
5886
+ }
5887
+ if (picked.length === 0) {
5888
+ return ranked[0].text.slice(0, maxChars);
5889
+ }
5890
+ picked.sort((a, b) => a.i - b.i);
5891
+ return picked.map((s) => s.text).join(" ");
5892
+ }
5893
+ async function summarizeViaEndpoint(endpoint, model, material, budgetMs, maxChars) {
5894
+ try {
5895
+ const controller = new AbortController();
5896
+ const timer = setTimeout(() => controller.abort(), budgetMs);
5897
+ const apiKey = process.env[LLM_API_KEY_ENV];
5898
+ const headers = { "content-type": "application/json" };
5899
+ if (apiKey) headers["authorization"] = `Bearer ${apiKey}`;
5900
+ let resp;
5901
+ try {
5902
+ resp = await fetch(`${endpoint.replace(/\/+$/, "")}/v1/chat/completions`, {
5903
+ method: "POST",
5904
+ headers,
5905
+ body: JSON.stringify({
5906
+ model,
5907
+ // a NAME/ALIAS the user configured — never a physical model id
5908
+ messages: [
5909
+ {
5910
+ role: "system",
5911
+ content: "You distill a software engineering session into ONE durable lesson a developer will read months later. Be concrete and factual. State what broke, what was tried and rejected, and what actually worked. Invent nothing that is not in the input."
5912
+ },
5913
+ { role: "user", content: material }
5914
+ ],
5915
+ max_tokens: 400,
5916
+ temperature: 0.2
5917
+ }),
5918
+ signal: controller.signal
5919
+ });
5920
+ } finally {
5921
+ clearTimeout(timer);
5922
+ }
5923
+ if (!resp.ok) return null;
5924
+ const data = await resp.json();
5925
+ const text = data.choices?.[0]?.message?.content;
5926
+ if (typeof text !== "string" || !text.trim()) return null;
5927
+ return text.trim().slice(0, maxChars);
5928
+ } catch {
5929
+ return null;
5930
+ }
5931
+ }
5932
+ async function summarizeText(sources, opts = {}) {
5933
+ const cfg = opts.config ?? resolveConsolidationConfig();
5934
+ const maxChars = opts.maxChars ?? DEFAULT_MAX_CHARS;
5935
+ const budgetMs = opts.budgetMs ?? DEFAULT_BUDGET_MS;
5936
+ const extractive = extractiveSummary(sources, maxChars);
5937
+ if (!cfg.llmEndpoint || !cfg.llmModel || !extractive) {
5938
+ return { text: extractive, tier: "extractive" };
5939
+ }
5940
+ const material = redactSecrets(
5941
+ sources.map((s) => s.text.replace(/\s+/g, " ").trim()).filter(Boolean).join("\n")
5942
+ );
5943
+ const viaModel = await summarizeViaEndpoint(
5944
+ cfg.llmEndpoint,
5945
+ cfg.llmModel,
5946
+ material,
5947
+ budgetMs,
5948
+ maxChars
5949
+ );
5950
+ return viaModel ? { text: viaModel, tier: "model" } : { text: extractive, tier: "extractive" };
5951
+ }
5952
+
5953
+ // src/shared-memory-export.ts
5954
+ init_memory_db();
5955
+ var DEFAULT_EXPORT_CAPS = {
5956
+ maxRecords: 200,
5957
+ maxBytesPerRecord: 16384,
5958
+ maxTotalBytes: 512e3
5959
+ };
5960
+ var EXPORT_SEQ_KEY = "shared_export_seq";
5961
+ var BACKUP_DONE_KEY = "shared_backup_at";
5962
+ function zero(enabled) {
5963
+ return { enabled, published: false, exported: 0, refused: 0, refusals: [], seq: null };
5964
+ }
5965
+ function ensureSharingBackup(db, repoId, home, nowEpoch, backupDirOverride) {
5966
+ if (getMemoryMeta(db, BACKUP_DONE_KEY)) return;
5967
+ const backupDir = backupDirOverride ?? join9(home, ".massu", "shared", "backups", repoId);
5968
+ const dest = join9(backupDir, `memory-${nowEpoch}.db`);
5969
+ const snapshot = db.serialize();
5970
+ mkdirSync12(dirname12(dest), { recursive: true, mode: 448 });
5971
+ const tmp = `${dest}.tmp-${process.pid}`;
5972
+ writeFileSync9(tmp, snapshot, { mode: 384 });
5973
+ const fd = openSync3(tmp, "r+");
5974
+ fsyncSync3(fd);
5975
+ closeSync3(fd);
5976
+ renameSync3(tmp, dest);
5977
+ chmodSync7(dest, 384);
5978
+ setMemoryMeta(db, BACKUP_DONE_KEY, String(nowEpoch));
5979
+ }
5980
+ async function exportSharedMemories(db, transport, opts = {}) {
5981
+ const home = opts.home ?? homedir11();
5982
+ const nowEpoch = opts.nowEpoch ?? Math.floor(Date.now() / 1e3);
5983
+ const caps = opts.caps ?? DEFAULT_EXPORT_CAPS;
5984
+ const config = getConfig();
5985
+ const shareEnabled = opts.shareEnabled ?? config.memory?.share?.enabled ?? false;
5986
+ if (!shareEnabled) return zero(false);
5987
+ const tier = opts.tier ?? getCachedTierReadOnly(db);
5988
+ if (!entitledForCrossRepoSurfacing(tier)) return zero(true);
5989
+ const repoId = getRepoId(db) ?? mintRepoId(db);
5990
+ const label = getRepoLabel();
5991
+ ensureLocalShareKeypair(home);
5992
+ const fingerprint = localSharePubkeyFingerprint(home);
5993
+ upsertRepoRegistration(
5994
+ {
5995
+ repo_id: repoId,
5996
+ label,
5997
+ pubkey_fingerprint: fingerprint,
5998
+ last_seen_path: getProjectRoot(),
5999
+ share_enabled: true
6000
+ },
6001
+ home
6002
+ );
6003
+ try {
6004
+ ensureSharingBackup(db, repoId, home, nowEpoch, opts.backupDir);
6005
+ } catch (err) {
6006
+ recordTelemetry(db, "shared_memory_export_refused", {
6007
+ reason: "backup_failed",
6008
+ detail: err instanceof Error ? err.message : String(err)
6009
+ });
6010
+ auditExportRefused(db, null, "backup_failed");
6011
+ return { ...zero(true), refused: 1, refusals: [{ observation_id: null, reason: "backup_failed" }] };
6012
+ }
6013
+ const rows = db.prepare(
6014
+ `SELECT id, type, title, detail, importance, created_at_epoch, origin
6015
+ FROM observations
6016
+ WHERE shareable = 1
6017
+ ORDER BY id
6018
+ LIMIT ?`
6019
+ ).all(caps.maxRecords + 1);
6020
+ if (rows.length > caps.maxRecords) {
6021
+ recordTelemetry(db, "shared_memory_export_refused", { reason: "too_many_records", count: rows.length });
6022
+ auditExportRefused(db, null, "too_many_records");
6023
+ return { ...zero(true), refused: 1, refusals: [{ observation_id: null, reason: "too_many_records" }] };
6024
+ }
6025
+ const wasExported = db.prepare(`SELECT 1 FROM shared_memory_outbound WHERE record_hash = ? LIMIT 1`);
6026
+ const refusals = [];
6027
+ const records = [];
6028
+ const outboundInserts = [];
6029
+ for (const r of rows) {
6030
+ if (!isLocalOrigin(r.origin)) {
6031
+ refusals.push({ observation_id: r.id, reason: "non_local_origin" });
6032
+ auditExportRefused(db, r.id, "non_local_origin");
6033
+ continue;
6034
+ }
6035
+ const title = r.title ?? "";
6036
+ const detail = r.detail ?? "";
6037
+ const sTitle = containsSecret(title);
6038
+ const sDetail = containsSecret(detail);
6039
+ if (sTitle.matched || sDetail.matched) {
6040
+ const reason = `secret:${sTitle.patternName ?? sDetail.patternName}`;
6041
+ refusals.push({ observation_id: r.id, reason });
6042
+ auditExportRefused(db, r.id, reason);
6043
+ continue;
6044
+ }
6045
+ if (home && (title.includes(home) || detail.includes(home))) {
6046
+ refusals.push({ observation_id: r.id, reason: "home_path" });
6047
+ auditExportRefused(db, r.id, "home_path");
6048
+ continue;
6049
+ }
6050
+ const base = {
6051
+ type: r.type,
6052
+ title,
6053
+ detail,
6054
+ importance: r.importance,
6055
+ created_at_epoch: r.created_at_epoch,
6056
+ superseded_by_hash: null
6057
+ };
6058
+ const record_hash = hashSharedMemoryRecord(base);
6059
+ if (wasExported.get(record_hash)) continue;
6060
+ const recordBytes = Buffer.byteLength(JSON.stringify({ ...base, record_hash }), "utf-8");
6061
+ if (recordBytes > caps.maxBytesPerRecord) {
6062
+ refusals.push({ observation_id: r.id, reason: "record_too_large" });
6063
+ auditExportRefused(db, r.id, "record_too_large");
6064
+ continue;
6065
+ }
6066
+ records.push({ ...base, record_hash });
6067
+ outboundInserts.push({ hash: record_hash, id: r.id });
6068
+ }
6069
+ const revokeRows = db.prepare(
6070
+ `SELECT o.record_hash AS h
6071
+ FROM shared_memory_outbound o
6072
+ JOIN observations obs ON obs.id = o.observation_id
6073
+ WHERE o.revoked_at_epoch IS NULL
6074
+ AND obs.expired_at IS NOT NULL
6075
+ LIMIT ?`
6076
+ ).all(caps.maxRecords);
6077
+ const revokedHashes = revokeRows.map((r) => r.h);
6078
+ if (records.length === 0 && revokedHashes.length === 0) {
6079
+ return { ...zero(true), refused: refusals.length, refusals };
6080
+ }
6081
+ const recordsJson = JSON.stringify(records);
6082
+ if (Buffer.byteLength(recordsJson, "utf-8") > caps.maxTotalBytes) {
6083
+ recordTelemetry(db, "shared_memory_export_refused", { reason: "envelope_too_large" });
6084
+ auditExportRefused(db, null, "envelope_too_large");
6085
+ return { ...zero(true), refused: refusals.length + 1, refusals: [...refusals, { observation_id: null, reason: "envelope_too_large" }] };
6086
+ }
6087
+ const seq = nextSeq(db);
6088
+ const body = {
6089
+ kind: SHARED_MEMORY_KIND,
6090
+ origin_repo_id: repoId,
6091
+ origin_repo_label: label,
6092
+ seq,
6093
+ issued_at: new Date(nowEpoch * 1e3).toISOString(),
6094
+ records_json: recordsJson,
6095
+ revokes_json: JSON.stringify(revokedHashes)
6096
+ };
6097
+ const envelope = signSharedMemoryEnvelope(body, home);
6098
+ await transport.publish(envelope);
6099
+ const insertOutbound = db.prepare(
6100
+ `INSERT OR IGNORE INTO shared_memory_outbound (record_hash, observation_id, origin_repo_id, seq, exported_at_epoch)
6101
+ VALUES (?, ?, ?, ?, ?)`
6102
+ );
6103
+ const markRevoked = db.prepare(
6104
+ `UPDATE shared_memory_outbound SET revoked_at_epoch = ? WHERE record_hash = ? AND revoked_at_epoch IS NULL`
6105
+ );
6106
+ const commit = db.transaction(() => {
6107
+ for (const o of outboundInserts) insertOutbound.run(o.hash, o.id, repoId, seq, nowEpoch);
6108
+ for (const h of revokedHashes) markRevoked.run(nowEpoch, h);
6109
+ setMemoryMeta(db, EXPORT_SEQ_KEY, String(seq));
6110
+ });
6111
+ commit();
6112
+ recordTelemetry(db, "shared_memory_exported", { count: records.length, revoked: revokedHashes.length, seq });
6113
+ auditExported(db, records.length, seq);
6114
+ return { enabled: true, published: true, exported: records.length, refused: refusals.length, refusals, seq };
6115
+ }
6116
+ function nextSeq(db) {
6117
+ const raw = getMemoryMeta(db, EXPORT_SEQ_KEY);
6118
+ const last = raw !== null && Number.isInteger(Number(raw)) ? Number(raw) : 0;
6119
+ return last + 1;
6120
+ }
6121
+ function auditExported(db, count, seq) {
6122
+ writeSharedAudit(db, "shared_memory_exported", `exported ${count} record(s) at seq ${seq}`, { count, seq });
6123
+ }
6124
+ function auditExportRefused(db, obsId, reason) {
6125
+ writeSharedAudit(db, "shared_memory_export_refused", `export refused: ${reason}`, { observation_id: obsId, reason });
6126
+ }
6127
+
6128
+ // src/shared-memory-transport.ts
6129
+ import {
6130
+ chmodSync as chmodSync8,
6131
+ existsSync as existsSync15,
6132
+ mkdirSync as mkdirSync13,
6133
+ readdirSync as readdirSync2,
6134
+ readFileSync as readFileSync11
6135
+ } from "fs";
6136
+ import { homedir as homedir12 } from "os";
6137
+ import { join as join10 } from "path";
6138
+ import { createHash as createHash4 } from "crypto";
6139
+ var REPO_ID_PATH_RE = /^[0-9a-f-]{36}$/;
6140
+ var CONTENT_HASH_RE = /^[0-9a-f]{64}$/;
6141
+ function sharedRootDir(home = homedir12()) {
6142
+ return join10(home, ".massu", "shared");
6143
+ }
6144
+ function outboxDirFor(originRepoId, home = homedir12()) {
6145
+ if (!REPO_ID_PATH_RE.test(originRepoId)) {
6146
+ throw new Error(`refusing to use an unvalidated repo_id as a path component: ${originRepoId}`);
6147
+ }
6148
+ const rel = join10("outbox", originRepoId);
6149
+ return assertContainedIn(sharedRootDir(home), rel, { allowNested: true });
6150
+ }
6151
+ function envelopeContentHash(env) {
6152
+ return createHash4("sha256").update(JSON.stringify(env), "utf-8").digest("hex");
6153
+ }
6154
+ var LocalFsTransport = class {
6155
+ constructor(home = homedir12()) {
6156
+ this.home = home;
6157
+ }
6158
+ home;
6159
+ async publish(env) {
6160
+ const dir = outboxDirFor(env.origin_repo_id, this.home);
6161
+ mkdirSync13(dir, { recursive: true, mode: 448 });
6162
+ const hash = envelopeContentHash(env);
6163
+ if (!CONTENT_HASH_RE.test(hash)) {
6164
+ throw new Error(`refusing to write an envelope with a malformed content hash: ${hash}`);
6165
+ }
6166
+ const dest = assertContainedIn(dir, `${seqPrefix(env.seq)}-${hash}.json`, { allowNested: false });
6167
+ atomicWriteFileSync(dest, JSON.stringify(env, null, 2));
6168
+ chmodSync8(dest, 384);
6169
+ }
6170
+ async fetchSince(originRepoId, sinceSeq) {
6171
+ if (!REPO_ID_PATH_RE.test(originRepoId)) return [];
6172
+ let dir;
6173
+ try {
6174
+ dir = outboxDirFor(originRepoId, this.home);
6175
+ } catch {
6176
+ return [];
6177
+ }
6178
+ if (!existsSync15(dir)) return [];
6179
+ const out = [];
6180
+ for (const name of readdirSync2(dir)) {
6181
+ if (!name.endsWith(".json")) continue;
6182
+ const seqFromName = parseSeqPrefix(name);
6183
+ if (seqFromName !== null && seqFromName <= sinceSeq) continue;
6184
+ let env;
6185
+ try {
6186
+ const p = assertContainedIn(dir, name, { allowNested: false });
6187
+ env = JSON.parse(readFileSync11(p, "utf-8"));
6188
+ } catch {
6189
+ continue;
6190
+ }
6191
+ if (typeof env?.seq === "number" && env.seq > sinceSeq) out.push(env);
6192
+ }
6193
+ out.sort((a, b) => a.seq - b.seq);
6194
+ return out;
6195
+ }
6196
+ };
6197
+ function seqPrefix(seq) {
6198
+ const s = Number.isInteger(seq) && seq >= 0 ? seq : 0;
6199
+ return String(s).padStart(16, "0");
6200
+ }
6201
+ function parseSeqPrefix(name) {
6202
+ const m = name.match(/^(\d{1,16})-/);
6203
+ return m ? Number(m[1]) : null;
6204
+ }
6205
+
6206
+ // src/hooks/session-end.ts
6207
+ init_config();
6208
+
6209
+ // src/analytics.ts
6210
+ init_config();
6211
+ var DEFAULT_WEIGHTS = {
6212
+ bug_found: -5,
6213
+ vr_failure: -10,
6214
+ incident: -20,
6215
+ cr_violation: -3,
6216
+ vr_pass: 2,
6217
+ clean_commit: 5,
6218
+ successful_verification: 3
6219
+ };
6220
+ var DEFAULT_CATEGORIES = ["security", "architecture", "coupling", "tests", "rule_compliance"];
6221
+ function getWeights() {
6222
+ return getConfig().analytics?.quality?.weights ?? DEFAULT_WEIGHTS;
6223
+ }
6224
+ function getCategories() {
6225
+ return getConfig().analytics?.quality?.categories ?? DEFAULT_CATEGORIES;
6226
+ }
6227
+ function calculateQualityScore(db, sessionId) {
6228
+ const weights = getWeights();
6229
+ const categories = getCategories();
6230
+ const observations = db.prepare(
6231
+ "SELECT type, detail FROM observations WHERE session_id = ? LIMIT 10000"
6232
+ ).all(sessionId);
6233
+ let score = 50;
6234
+ const breakdown = Object.fromEntries(
6235
+ categories.map((c) => [c, 0])
6236
+ );
6237
+ for (const obs of observations) {
6238
+ const weight = weights[obs.type] ?? 0;
6239
+ score += weight;
6240
+ const desc = (obs.detail ?? "").toLowerCase();
6241
+ for (const category of categories) {
6242
+ if (desc.includes(category)) {
6243
+ breakdown[category] += weight;
6244
+ }
6245
+ }
6246
+ }
6247
+ return {
6248
+ score: Math.max(0, Math.min(100, score)),
6249
+ breakdown
6250
+ };
6251
+ }
6252
+ function storeQualityScore(db, sessionId, score, breakdown) {
6253
+ db.prepare(`
6254
+ INSERT INTO session_quality_scores
6255
+ (session_id, score, security_score, architecture_score, coupling_score, test_score, rule_compliance_score)
6256
+ VALUES (?, ?, ?, ?, ?, ?, ?)
6257
+ `).run(
6258
+ sessionId,
6259
+ score,
6260
+ breakdown.security ?? 0,
6261
+ breakdown.architecture ?? 0,
6262
+ breakdown.coupling ?? 0,
6263
+ breakdown.tests ?? 0,
6264
+ breakdown.rule_compliance ?? 0
6265
+ );
6266
+ }
6267
+ function backfillQualityScores(db) {
6268
+ const sessions = db.prepare(`
6269
+ SELECT DISTINCT s.session_id
6270
+ FROM sessions s
6271
+ LEFT JOIN session_quality_scores q ON s.session_id = q.session_id
6272
+ WHERE q.session_id IS NULL
6273
+ LIMIT 100000
6274
+ `).all();
6275
+ let backfilled = 0;
6276
+ for (const session of sessions) {
6277
+ const { score, breakdown } = calculateQualityScore(db, session.session_id);
6278
+ storeQualityScore(db, session.session_id, score, breakdown);
6279
+ backfilled++;
6280
+ }
6281
+ return backfilled;
6282
+ }
6283
+
6284
+ // src/cost-tracker.ts
6285
+ init_config();
6286
+ var DEFAULT_MODEL_PRICING = {
6287
+ "claude-opus-4-6": { input_per_million: 15, output_per_million: 75, cache_read_per_million: 1.5, cache_write_per_million: 18.75 },
6288
+ "claude-sonnet-4-6": { input_per_million: 3, output_per_million: 15, cache_read_per_million: 0.3, cache_write_per_million: 3.75 },
6289
+ "claude-sonnet-4-5": { input_per_million: 3, output_per_million: 15, cache_read_per_million: 0.3, cache_write_per_million: 3.75 },
6290
+ "claude-haiku-4-5-20251001": { input_per_million: 0.8, output_per_million: 4, cache_read_per_million: 0.08, cache_write_per_million: 1 },
6291
+ "default": { input_per_million: 3, output_per_million: 15, cache_read_per_million: 0.3, cache_write_per_million: 3.75 }
6292
+ };
6293
+ function getModelPricing() {
6294
+ return getConfig().analytics?.cost?.models ?? DEFAULT_MODEL_PRICING;
6295
+ }
6296
+ function getCurrency() {
6297
+ return getConfig().analytics?.cost?.currency ?? "USD";
6298
+ }
6299
+ function extractTokenUsage(entries) {
6300
+ let inputTokens = 0;
6301
+ let outputTokens = 0;
6302
+ let cacheReadTokens = 0;
6303
+ let cacheWriteTokens = 0;
6304
+ let model = "unknown";
6305
+ for (const entry of entries) {
6306
+ const msg = entry.message;
6307
+ if (entry.type === "assistant" && msg?.usage) {
6308
+ const usage = msg.usage;
6309
+ inputTokens += usage.input_tokens ?? 0;
6310
+ outputTokens += usage.output_tokens ?? 0;
6311
+ cacheReadTokens += usage.cache_read_input_tokens ?? usage.cache_read_tokens ?? 0;
6312
+ cacheWriteTokens += usage.cache_creation_input_tokens ?? usage.cache_write_tokens ?? 0;
6313
+ }
6314
+ if (entry.type === "assistant" && msg?.model) {
6315
+ model = msg.model;
6316
+ }
6317
+ }
6318
+ return { inputTokens, outputTokens, cacheReadTokens, cacheWriteTokens, model };
6319
+ }
6320
+ function calculateCost(usage) {
6321
+ const pricing = getModelPricing();
6322
+ const modelPricing = pricing[usage.model] ?? pricing["default"] ?? pricing["claude-sonnet-4-5"] ?? { input_per_million: 3, output_per_million: 15 };
6323
+ const inputCost = usage.inputTokens / 1e6 * modelPricing.input_per_million;
5077
6324
  const outputCost = usage.outputTokens / 1e6 * modelPricing.output_per_million;
5078
6325
  const cacheReadCost = usage.cacheReadTokens / 1e6 * (modelPricing.cache_read_per_million ?? 0);
5079
6326
  const cacheWriteCost = usage.cacheWriteTokens / 1e6 * (modelPricing.cache_write_per_million ?? 0);
@@ -5107,7 +6354,7 @@ function storeSessionCost(db, sessionId, usage, cost) {
5107
6354
 
5108
6355
  // src/prompt-analyzer.ts
5109
6356
  init_config();
5110
- import { createHash as createHash2 } from "crypto";
6357
+ import { createHash as createHash5 } from "crypto";
5111
6358
 
5112
6359
  // src/security-utils.ts
5113
6360
  function escapeRegex(str) {
@@ -5131,7 +6378,7 @@ function categorizePrompt(promptText) {
5131
6378
  }
5132
6379
  function hashPrompt(promptText) {
5133
6380
  const normalized = promptText.toLowerCase().replace(/\s+/g, " ").trim();
5134
- return createHash2("sha256").update(normalized).digest("hex").slice(0, 16);
6381
+ return createHash5("sha256").update(normalized).digest("hex").slice(0, 16);
5135
6382
  }
5136
6383
  function detectOutcome(followUpPrompts, assistantResponses) {
5137
6384
  let correctionsNeeded = 0;
@@ -5744,160 +6991,9 @@ ${d.decision}`.trim(),
5744
6991
 
5745
6992
  // src/memory-consolidate.ts
5746
6993
  init_memory_db();
5747
- import { createHash as createHash3 } from "crypto";
5748
- import { existsSync as existsSync10, mkdirSync as mkdirSync9, writeFileSync as writeFileSync5 } from "fs";
5749
- import { join as join6 } from "path";
5750
-
5751
- // src/consolidation-config.ts
5752
- init_config();
5753
- var DEFAULT_CONSOLIDATION_CONFIG = {
5754
- enabled: true,
5755
- sessionSweepEnabled: true,
5756
- // llmEndpoint / llmModel deliberately UNSET: the shipped default is
5757
- // zero-LLM, zero-network, works offline on any machine.
5758
- summarizeAfterDays: 5,
5759
- // inside the 7-day conversation_turns prune window
5760
- retentionDays: 90,
5761
- importanceFloor: 2,
5762
- protectedTypes: ["decision", "cr_violation", "incident_near_miss"],
5763
- usageWarmupDays: 30,
5764
- usageDecay: 0.9,
5765
- reweightIntervalDays: 1,
5766
- promoteMinOccurrences: 3,
5767
- budgetMs: 3e3,
5768
- suggestUpgrades: true,
5769
- suggestIntervalDays: 30
5770
- };
5771
- function resolveConsolidationConfig() {
5772
- try {
5773
- const c = getConfig().memory?.consolidation;
5774
- if (!c) return { ...DEFAULT_CONSOLIDATION_CONFIG };
5775
- return { ...DEFAULT_CONSOLIDATION_CONFIG, ...c };
5776
- } catch {
5777
- return { ...DEFAULT_CONSOLIDATION_CONFIG };
5778
- }
5779
- }
5780
-
5781
- // src/memory-llm.ts
5782
- var LLM_API_KEY_ENV = "MASSU_MEMORY_LLM_API_KEY";
5783
- var DEFAULT_BUDGET_MS = 2e4;
5784
- var DEFAULT_MAX_CHARS = 900;
5785
- var NOISE_PATTERNS = [
5786
- /<command-(name|message|args)>/i,
5787
- /<local-command-[^>]*>/i,
5788
- /^\s*<[a-z-]+>\s*$/i,
5789
- /^\s*(ok|okay|thanks|thank you|yes|no|yep|nope|sure|continue|proceed|go ahead)\s*[.!]?\s*$/i,
5790
- /^\s*\/[a-z-]+\s*$/i,
5791
- // a bare slash-command invocation
5792
- /system-reminder/i
5793
- ];
5794
- function isSummarizableSignal(text) {
5795
- const t = text.trim();
5796
- if (t.length < 25) return false;
5797
- return !NOISE_PATTERNS.some((re) => re.test(t));
5798
- }
5799
- var CREDENTIAL_LABELS = ["api[_-]?key", "secret", "password", "token"];
5800
- var LABELLED_CREDENTIAL = new RegExp(
5801
- `\\b[A-Za-z0-9_-]*(?:${CREDENTIAL_LABELS.join("|")})["'\\s:=]+[A-Za-z0-9_\\-/+]{16,}`,
5802
- "gi"
5803
- );
5804
- var SECRET_PATTERNS = [
5805
- [/\bms_live_[A-Za-z0-9_-]{6,}/g, "ms_live_[REDACTED]", "MASSU_LIVE_KEY"],
5806
- [/\bsk-[A-Za-z0-9_-]{16,}/g, "sk-[REDACTED]", "OPENAI_STYLE_KEY"],
5807
- [/\b(gh[pousr]|github_pat)_[A-Za-z0-9_]{16,}/g, "[REDACTED_TOKEN]", "GITHUB_TOKEN"],
5808
- [/\bsbp_[A-Za-z0-9]{16,}/g, "sbp_[REDACTED]", "SUPABASE_TOKEN"],
5809
- [/\bAKIA[0-9A-Z]{12,}/g, "[REDACTED_AWS_KEY]", "AWS_ACCESS_KEY"],
5810
- [
5811
- /\bey[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{6,}/g,
5812
- "[REDACTED_JWT]",
5813
- "JWT"
5814
- ],
5815
- [LABELLED_CREDENTIAL, "[REDACTED_CREDENTIAL]", "LABELLED_CREDENTIAL"]
5816
- ];
5817
- function redactSecrets(text) {
5818
- let out = text;
5819
- for (const [re, replacement] of SECRET_PATTERNS) out = out.replace(re, replacement);
5820
- return out;
5821
- }
5822
- function extractiveSummary(sources, maxChars = DEFAULT_MAX_CHARS) {
5823
- const cleaned = sources.map((s, i) => ({ ...s, i, text: s.text.replace(/\s+/g, " ").trim() })).filter((s) => isSummarizableSignal(s.text));
5824
- if (cleaned.length === 0) return "";
5825
- const ranked = [...cleaned].sort((a, b) => b.weight - a.weight || a.i - b.i);
5826
- const picked = [];
5827
- let used = 0;
5828
- for (const s of ranked) {
5829
- const cost = s.text.length + 1;
5830
- if (used + cost > maxChars) continue;
5831
- picked.push(s);
5832
- used += cost;
5833
- }
5834
- if (picked.length === 0) {
5835
- return ranked[0].text.slice(0, maxChars);
5836
- }
5837
- picked.sort((a, b) => a.i - b.i);
5838
- return picked.map((s) => s.text).join(" ");
5839
- }
5840
- async function summarizeViaEndpoint(endpoint, model, material, budgetMs, maxChars) {
5841
- try {
5842
- const controller = new AbortController();
5843
- const timer = setTimeout(() => controller.abort(), budgetMs);
5844
- const apiKey = process.env[LLM_API_KEY_ENV];
5845
- const headers = { "content-type": "application/json" };
5846
- if (apiKey) headers["authorization"] = `Bearer ${apiKey}`;
5847
- let resp;
5848
- try {
5849
- resp = await fetch(`${endpoint.replace(/\/+$/, "")}/v1/chat/completions`, {
5850
- method: "POST",
5851
- headers,
5852
- body: JSON.stringify({
5853
- model,
5854
- // a NAME/ALIAS the user configured — never a physical model id
5855
- messages: [
5856
- {
5857
- role: "system",
5858
- content: "You distill a software engineering session into ONE durable lesson a developer will read months later. Be concrete and factual. State what broke, what was tried and rejected, and what actually worked. Invent nothing that is not in the input."
5859
- },
5860
- { role: "user", content: material }
5861
- ],
5862
- max_tokens: 400,
5863
- temperature: 0.2
5864
- }),
5865
- signal: controller.signal
5866
- });
5867
- } finally {
5868
- clearTimeout(timer);
5869
- }
5870
- if (!resp.ok) return null;
5871
- const data = await resp.json();
5872
- const text = data.choices?.[0]?.message?.content;
5873
- if (typeof text !== "string" || !text.trim()) return null;
5874
- return text.trim().slice(0, maxChars);
5875
- } catch {
5876
- return null;
5877
- }
5878
- }
5879
- async function summarizeText(sources, opts = {}) {
5880
- const cfg = opts.config ?? resolveConsolidationConfig();
5881
- const maxChars = opts.maxChars ?? DEFAULT_MAX_CHARS;
5882
- const budgetMs = opts.budgetMs ?? DEFAULT_BUDGET_MS;
5883
- const extractive = extractiveSummary(sources, maxChars);
5884
- if (!cfg.llmEndpoint || !cfg.llmModel || !extractive) {
5885
- return { text: extractive, tier: "extractive" };
5886
- }
5887
- const material = redactSecrets(
5888
- sources.map((s) => s.text.replace(/\s+/g, " ").trim()).filter(Boolean).join("\n")
5889
- );
5890
- const viaModel = await summarizeViaEndpoint(
5891
- cfg.llmEndpoint,
5892
- cfg.llmModel,
5893
- material,
5894
- budgetMs,
5895
- maxChars
5896
- );
5897
- return viaModel ? { text: viaModel, tier: "model" } : { text: extractive, tier: "extractive" };
5898
- }
5899
-
5900
- // src/memory-consolidate.ts
6994
+ import { createHash as createHash6 } from "crypto";
6995
+ import { existsSync as existsSync16, mkdirSync as mkdirSync14, writeFileSync as writeFileSync10 } from "fs";
6996
+ import { join as join11 } from "path";
5901
6997
  init_memory_embedder();
5902
6998
  init_memory_vector();
5903
6999
  var LEASE_KEY = "consolidate_lease";
@@ -6064,7 +7160,7 @@ ${r.detail ?? ""}`.trim());
6064
7160
  }
6065
7161
  clusters.push(cluster);
6066
7162
  }
6067
- const candidateDir = join6(projectRoot, ".massu", "rule-candidates");
7163
+ const candidateDir = join11(projectRoot, ".massu", "rule-candidates");
6068
7164
  let promoted = 0;
6069
7165
  for (const cluster of clusters) {
6070
7166
  const occurrences = cluster.reduce((n, c) => n + (c.recurrence_count || 1), 0);
@@ -6076,15 +7172,15 @@ ${r.detail ?? ""}`.trim());
6076
7172
  const promptText = `${representative.title}${representative.detail ? `
6077
7173
  ${representative.detail}` : ""}`;
6078
7174
  const clusterKey = cluster.map((c) => c.title.toLowerCase().replace(/\s+/g, " ").trim()).sort().join("|");
6079
- const promptHash = createHash3("sha256").update(clusterKey).digest("hex").slice(0, 16);
6080
- const candidatePath = join6(candidateDir, `${promptHash}.json`);
6081
- if (existsSync10(candidatePath)) continue;
7175
+ const promptHash = createHash6("sha256").update(clusterKey).digest("hex").slice(0, 16);
7176
+ const candidatePath = join11(candidateDir, `${promptHash}.json`);
7177
+ if (existsSync16(candidatePath)) continue;
6082
7178
  if (dryRun) {
6083
7179
  promoted++;
6084
7180
  continue;
6085
7181
  }
6086
- mkdirSync9(candidateDir, { recursive: true });
6087
- writeFileSync5(
7182
+ mkdirSync14(candidateDir, { recursive: true });
7183
+ writeFileSync10(
6088
7184
  candidatePath,
6089
7185
  JSON.stringify(
6090
7186
  {
@@ -6149,7 +7245,7 @@ function stageReweight(db, cfg, now) {
6149
7245
  MAX_ROWS_PER_STAGE
6150
7246
  );
6151
7247
  const bump = db.prepare(`UPDATE observations SET importance = importance + 1 WHERE id = ?`);
6152
- const drop = db.prepare(`UPDATE observations SET importance = importance - 1 WHERE id = ?`);
7248
+ const drop2 = db.prepare(`UPDATE observations SET importance = importance - 1 WHERE id = ?`);
6153
7249
  const mark = db.prepare(
6154
7250
  `INSERT INTO memory_usage (source, record_id, hit_count, hits_windowed, last_reweight_epoch)
6155
7251
  VALUES ('observation', ?, 0, 0, ?)
@@ -6162,7 +7258,7 @@ function stageReweight(db, cfg, now) {
6162
7258
  changed++;
6163
7259
  }
6164
7260
  for (const r of demote) {
6165
- drop.run(r.id);
7261
+ drop2.run(r.id);
6166
7262
  mark.run(r.id, now);
6167
7263
  changed++;
6168
7264
  }
@@ -6321,6 +7417,12 @@ async function main() {
6321
7417
  await pullTeamPromotions(db);
6322
7418
  } catch (_pullErr) {
6323
7419
  }
7420
+ try {
7421
+ const shareTransport = new LocalFsTransport();
7422
+ await exportSharedMemories(db, shareTransport);
7423
+ await importSharedMemories(db, shareTransport);
7424
+ } catch (_shareErr) {
7425
+ }
6324
7426
  } catch (syncErr) {
6325
7427
  const msg = syncErr instanceof Error ? syncErr.message : String(syncErr);
6326
7428
  if (openLease) nackLearning(db, openLease, msg);
@@ -6583,14 +7685,14 @@ function extractFilesFromToolCall(toolName, input) {
6583
7685
  return [];
6584
7686
  }
6585
7687
  function readStdin() {
6586
- return new Promise((resolve6) => {
7688
+ return new Promise((resolve8) => {
6587
7689
  let data = "";
6588
7690
  process.stdin.setEncoding("utf-8");
6589
7691
  process.stdin.on("data", (chunk) => {
6590
7692
  data += chunk;
6591
7693
  });
6592
- process.stdin.on("end", () => resolve6(data));
6593
- setTimeout(() => resolve6(data), 5e3);
7694
+ process.stdin.on("end", () => resolve8(data));
7695
+ setTimeout(() => resolve8(data), 5e3);
6594
7696
  });
6595
7697
  }
6596
7698
  main();