@massu/core 1.16.2 → 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.
@@ -32,16 +32,6 @@ var __copyProps = (to, from, except, desc) => {
32
32
  };
33
33
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
34
34
 
35
- // src/lib/memory-path.ts
36
- function encodeMemoryDirName(projectRoot) {
37
- return projectRoot.replace(/\//g, "-");
38
- }
39
- var init_memory_path = __esm({
40
- "src/lib/memory-path.ts"() {
41
- "use strict";
42
- }
43
- });
44
-
45
35
  // src/credentials.ts
46
36
  import { homedir } from "os";
47
37
  import { resolve } from "path";
@@ -110,6 +100,475 @@ var init_credentials = __esm({
110
100
  }
111
101
  });
112
102
 
103
+ // src/lib/fileLock.ts
104
+ import { mkdirSync as mkdirSync2, readFileSync as readFileSync2, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
105
+ import { dirname } from "path";
106
+ import * as lockfile from "proper-lockfile";
107
+ function readLockHolderPid(lockPath) {
108
+ try {
109
+ const raw = readFileSync2(`${lockPath}.pid`, "utf-8").trim();
110
+ const pid = Number.parseInt(raw, 10);
111
+ if (!Number.isFinite(pid) || pid <= 0) return null;
112
+ return pid;
113
+ } catch {
114
+ return null;
115
+ }
116
+ }
117
+ function busyWaitSync(ms) {
118
+ if (typeof SharedArrayBuffer === "undefined" || typeof Atomics === "undefined") {
119
+ throw new Error(
120
+ `withFileLockSync requires SharedArrayBuffer + Atomics for its retry-loop wait. This Node runtime does not provide them \u2014 refusing to fall back to a CPU spinloop. If you hit this in a sandboxed serverless env, the fix is to perform the lock-protected operation in a host runtime that supports Atomics.`
121
+ );
122
+ }
123
+ const sab = new SharedArrayBuffer(4);
124
+ const view = new Int32Array(sab);
125
+ Atomics.wait(view, 0, 0, ms);
126
+ }
127
+ function withFileLockSync(lockPath, fn, opts = {}) {
128
+ mkdirSync2(dirname(lockPath), { recursive: true });
129
+ const staleMs = opts.staleMs ?? 3e4;
130
+ const blockMs = opts.retries === 0 ? 0 : opts.blockMs ?? 3e4;
131
+ const pollIntervalMs = opts.pollIntervalMs ?? 100;
132
+ const now = opts.now ?? Date.now;
133
+ const sleep = opts.sleep ?? busyWaitSync;
134
+ const makeBusyError = opts.errorFactory ?? ((path, pid, retrySeconds, code) => new FileLockBusyError(path, pid, retrySeconds, code));
135
+ let release = null;
136
+ const deadline = now() + blockMs;
137
+ for (; ; ) {
138
+ try {
139
+ release = lockfile.lockSync(lockPath, {
140
+ stale: staleMs,
141
+ retries: 0,
142
+ realpath: false
143
+ });
144
+ try {
145
+ writeFileSync2(`${lockPath}.pid`, String(process.pid), "utf-8");
146
+ } catch {
147
+ }
148
+ break;
149
+ } catch (err) {
150
+ const e = err;
151
+ const code = e.code;
152
+ if (code !== "ELOCKED" && code !== "EBUSY") {
153
+ throw err;
154
+ }
155
+ if (now() >= deadline) {
156
+ const holderPid = readLockHolderPid(lockPath);
157
+ const remainingMs = Math.max(0, deadline - now());
158
+ const retryAfterSeconds = blockMs === 0 ? Math.round(staleMs / 1e3) : Math.round(remainingMs / 1e3);
159
+ throw makeBusyError(lockPath, holderPid, retryAfterSeconds, code);
160
+ }
161
+ sleep(pollIntervalMs);
162
+ }
163
+ }
164
+ try {
165
+ return fn();
166
+ } finally {
167
+ try {
168
+ if (release) release();
169
+ } catch {
170
+ }
171
+ try {
172
+ rmSync2(`${lockPath}.pid`, { force: true });
173
+ } catch {
174
+ }
175
+ }
176
+ }
177
+ var FileLockBusyError;
178
+ var init_fileLock = __esm({
179
+ "src/lib/fileLock.ts"() {
180
+ "use strict";
181
+ FileLockBusyError = class extends Error {
182
+ constructor(lockPath, holderPid, retryAfterSeconds, causeCode) {
183
+ const pidPart = holderPid != null ? `(PID=${holderPid})` : "(PID=unknown)";
184
+ super(`File lock at ${lockPath} held by another process ${pidPart} \u2014 try again in ${retryAfterSeconds}s`);
185
+ this.lockPath = lockPath;
186
+ this.holderPid = holderPid;
187
+ this.retryAfterSeconds = retryAfterSeconds;
188
+ this.causeCode = causeCode;
189
+ this.name = "FileLockBusyError";
190
+ }
191
+ lockPath;
192
+ holderPid;
193
+ retryAfterSeconds;
194
+ causeCode;
195
+ };
196
+ }
197
+ });
198
+
199
+ // src/lib/sqlite-loader.ts
200
+ import { createRequire } from "module";
201
+ import { spawnSync } from "child_process";
202
+ import { accessSync, appendFileSync, chmodSync as chmodSync2, constants as fsConstants, existsSync as existsSync2, mkdirSync as mkdirSync3 } from "fs";
203
+ import { dirname as dirname2, join } from "path";
204
+ function isNativeAbiError(err) {
205
+ const code = err?.code;
206
+ if (code === "ERR_DLOPEN_FAILED") return true;
207
+ const msg = err instanceof Error ? err.message : String(err);
208
+ return /NODE_MODULE_VERSION/.test(msg) || /was compiled against a different Node\.js version/.test(msg) || /ERR_DLOPEN_FAILED/.test(msg) || /\.node\b/.test(msg) && /dlopen|invalid ELF|mach-o|image not found|no such file|not a valid Win32/i.test(msg);
209
+ }
210
+ function loadBetterSqlite3(opts = {}) {
211
+ if (_testCtor) return _testCtor;
212
+ if (opts.fresh) bustNativeCache();
213
+ return req("better-sqlite3");
214
+ }
215
+ function bustNativeCache() {
216
+ try {
217
+ const cache = req.cache;
218
+ if (!cache) return;
219
+ for (const key of Object.keys(cache)) {
220
+ if (/better[-_]sqlite3|better_sqlite3\.node|[\\/]bindings[\\/]/.test(key)) {
221
+ delete cache[key];
222
+ }
223
+ }
224
+ } catch {
225
+ }
226
+ }
227
+ function openDatabase(dbPath, opts = {}) {
228
+ const { selfHeal: selfHealOpt = true, ...ctorOpts } = opts;
229
+ const selfHeal = selfHealOpt && process.env.MASSU_HOOK_RUNTIME !== "1";
230
+ const Ctor = loadBetterSqlite3();
231
+ try {
232
+ return new Ctor(dbPath, ctorOpts);
233
+ } catch (err) {
234
+ if (!isNativeAbiError(err)) {
235
+ throw err;
236
+ }
237
+ if (!selfHeal) {
238
+ recordHealEvent({ phase: "skipped", reason: "self-heal-disabled", abiTo: process.versions.modules });
239
+ throw new MemoryEngineUnusableError("abi-mismatch", detailOf(err));
240
+ }
241
+ const result = (_testHeal ?? attemptNativeHeal)(err);
242
+ if (!result.healed && !result.contended) {
243
+ throw new MemoryEngineUnusableError(result.reason ?? "heal-failed", result.detail ?? detailOf(err));
244
+ }
245
+ try {
246
+ const FreshCtor = loadBetterSqlite3({ fresh: true });
247
+ return new FreshCtor(dbPath, ctorOpts);
248
+ } catch (retryErr) {
249
+ throw new MemoryEngineUnusableError("heal-failed", detailOf(retryErr));
250
+ }
251
+ }
252
+ }
253
+ function attemptNativeHeal(err) {
254
+ const start = Date.now();
255
+ const abiTo = process.versions.modules;
256
+ const abiFrom = parseAbiFrom(err);
257
+ let pkgDir;
258
+ try {
259
+ pkgDir = dirname2(req.resolve("better-sqlite3/package.json"));
260
+ } catch {
261
+ recordHealEvent({ phase: "skipped", reason: "not-resolvable", abiFrom, abiTo });
262
+ return { healed: false, reason: "missing", abiFrom, abiTo, detail: "better-sqlite3 not resolvable" };
263
+ }
264
+ if (!isWritable(pkgDir)) {
265
+ recordHealEvent({ phase: "skipped", reason: "dir-not-writable", abiFrom, abiTo });
266
+ return { healed: false, reason: "heal-failed", abiFrom, abiTo, detail: `install dir not writable: ${pkgDir}` };
267
+ }
268
+ recordHealEvent({ phase: "attempt", reason: "abi-mismatch", abiFrom, abiTo });
269
+ try {
270
+ return withFileLockSync(
271
+ join(credentialsDir(), "native-heal.lock"),
272
+ () => runRebuild(pkgDir, abiFrom, abiTo, start),
273
+ { blockMs: 6e4, staleMs: 3e5 }
274
+ );
275
+ } catch (lockErr) {
276
+ const detail = lockErr instanceof Error ? lockErr.message : String(lockErr);
277
+ recordHealEvent({ phase: "failed", reason: "heal-failed", abiFrom, abiTo, detail: `lock: ${detail}` });
278
+ return { healed: false, contended: true, reason: "heal-failed", abiFrom, abiTo, detail: `heal lock contended: ${detail}` };
279
+ }
280
+ }
281
+ function runRebuild(pkgDir, abiFrom, abiTo, start) {
282
+ try {
283
+ const FreshCtor = loadBetterSqlite3({ fresh: true });
284
+ new FreshCtor(":memory:").close();
285
+ const res2 = {
286
+ healed: true,
287
+ abiFrom,
288
+ abiTo,
289
+ durationMs: Date.now() - start,
290
+ detail: "already healed by a concurrent process"
291
+ };
292
+ recordHealEvent({ phase: "success", reason: "already-healed", abiFrom, abiTo, durationMs: res2.durationMs });
293
+ return res2;
294
+ } catch (probeErr) {
295
+ if (!isNativeAbiError(probeErr)) {
296
+ return { healed: false, reason: "heal-failed", abiFrom, abiTo, durationMs: Date.now() - start, detail: detailOf(probeErr) };
297
+ }
298
+ }
299
+ const prebuildBin = resolveLocalBin(pkgDir, "prebuild-install");
300
+ if (prebuildBin) {
301
+ const r = spawnSync(process.execPath, [prebuildBin], {
302
+ cwd: pkgDir,
303
+ encoding: "utf-8",
304
+ timeout: 12e4
305
+ });
306
+ if (r.status === 0) {
307
+ const res2 = { healed: true, method: "prebuild-install", abiFrom, abiTo, durationMs: Date.now() - start };
308
+ recordHealEvent({ phase: "success", reason: "abi-mismatch", ...res2 });
309
+ return res2;
310
+ }
311
+ }
312
+ const gypBin = resolveNodeGypBin();
313
+ if (gypBin && hasCompiler()) {
314
+ const r = spawnSync(process.execPath, [gypBin, "rebuild", "--release"], {
315
+ cwd: pkgDir,
316
+ encoding: "utf-8",
317
+ timeout: 3e5
318
+ });
319
+ if (r.status === 0) {
320
+ const res2 = { healed: true, method: "node-gyp", abiFrom, abiTo, durationMs: Date.now() - start };
321
+ recordHealEvent({ phase: "success", reason: "abi-mismatch", ...res2 });
322
+ return res2;
323
+ }
324
+ }
325
+ const res = {
326
+ healed: false,
327
+ reason: "heal-failed",
328
+ abiFrom,
329
+ abiTo,
330
+ durationMs: Date.now() - start,
331
+ detail: prebuildBin ? "prebuild-install failed; node-gyp fallback unavailable or failed" : "no prebuild-install bin resolvable"
332
+ };
333
+ recordHealEvent({ phase: "failed", reason: "heal-failed", abiFrom, abiTo, durationMs: res.durationMs });
334
+ return res;
335
+ }
336
+ function recordHealEvent(event) {
337
+ try {
338
+ const rec = {
339
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
340
+ node: process.versions.node,
341
+ platform: process.platform,
342
+ arch: process.arch,
343
+ ...event
344
+ };
345
+ const dir = credentialsDir();
346
+ mkdirSync3(dir, { recursive: true });
347
+ const file = join(dir, "native-heal-events.jsonl");
348
+ appendFileSync(file, JSON.stringify(rec) + "\n", "utf-8");
349
+ try {
350
+ chmodSync2(file, 384);
351
+ } catch {
352
+ }
353
+ } catch {
354
+ }
355
+ }
356
+ function parseAbiFrom(err) {
357
+ const msg = err instanceof Error ? err.message : String(err ?? "");
358
+ const m = /NODE_MODULE_VERSION (\d+)/.exec(msg);
359
+ if (m && /^\d+$/.test(m[1])) return m[1];
360
+ return void 0;
361
+ }
362
+ function detailOf(err) {
363
+ const msg = err instanceof Error ? err.message : String(err);
364
+ return msg.slice(0, 300);
365
+ }
366
+ function isWritable(dir) {
367
+ try {
368
+ accessSync(dir, fsConstants.W_OK);
369
+ return true;
370
+ } catch {
371
+ return false;
372
+ }
373
+ }
374
+ function resolveLocalBin(pkgDir, name) {
375
+ const candidates = [
376
+ join(pkgDir, "node_modules", ".bin", name),
377
+ // nested install
378
+ join(pkgDir, "..", ".bin", name),
379
+ // hoisted (node_modules/.bin)
380
+ join(pkgDir, "..", "..", "node_modules", ".bin", name)
381
+ // workspace hoist
382
+ ];
383
+ for (const c of candidates) {
384
+ if (existsSync2(c)) return c;
385
+ }
386
+ return void 0;
387
+ }
388
+ function resolveNodeGypBin() {
389
+ try {
390
+ return req.resolve("node-gyp/bin/node-gyp.js");
391
+ } catch {
392
+ return void 0;
393
+ }
394
+ }
395
+ function hasCompiler() {
396
+ for (const cc of ["cc", "clang", "gcc"]) {
397
+ try {
398
+ const r = spawnSync(cc, ["--version"], { encoding: "utf-8", timeout: 5e3 });
399
+ if (r.status === 0) return true;
400
+ } catch {
401
+ }
402
+ }
403
+ return false;
404
+ }
405
+ var req, NATIVE_DB_REMEDY, MemoryEngineUnusableError, _testCtor, _testHeal;
406
+ var init_sqlite_loader = __esm({
407
+ "src/lib/sqlite-loader.ts"() {
408
+ "use strict";
409
+ init_credentials();
410
+ init_fileLock();
411
+ req = createRequire(import.meta.url);
412
+ NATIVE_DB_REMEDY = "Run 'massu heal' to rebuild the database engine for your Node version, then restart your MCP client / Claude Code.";
413
+ MemoryEngineUnusableError = class extends Error {
414
+ reason;
415
+ remedy;
416
+ detail;
417
+ constructor(reason, detail) {
418
+ super(
419
+ `Massu memory engine is unusable (${reason}). ${NATIVE_DB_REMEDY}` + (detail ? ` [${detail}]` : "")
420
+ );
421
+ this.name = "MemoryEngineUnusableError";
422
+ this.reason = reason;
423
+ this.remedy = NATIVE_DB_REMEDY;
424
+ this.detail = detail;
425
+ }
426
+ };
427
+ _testCtor = null;
428
+ _testHeal = null;
429
+ }
430
+ });
431
+
432
+ // src/db-driver.ts
433
+ import { createRequire as createRequire2 } from "module";
434
+ import { closeSync, existsSync as existsSync3, mkdtempSync, openSync, readFileSync as readFileSync3, rmSync as rmSync3, fsyncSync } from "fs";
435
+ import { tmpdir } from "os";
436
+ import { join as join2 } from "path";
437
+ function resolveDbEngine() {
438
+ return process.env[DB_ENGINE_ENV] === "better-sqlite3" ? "better-sqlite3" : DEFAULT_DB_ENGINE;
439
+ }
440
+ function nodeSqliteCtor() {
441
+ if (!_nodeCtor) _nodeCtor = req2("node:sqlite").DatabaseSync;
442
+ return _nodeCtor;
443
+ }
444
+ function plainRow(row) {
445
+ if (row == null || typeof row !== "object") return row;
446
+ return { ...row };
447
+ }
448
+ function openNodeSqlite(dbPath, opts) {
449
+ const Ctor = nodeSqliteCtor();
450
+ const raw = new Ctor(dbPath, {
451
+ open: true,
452
+ readOnly: !!opts.readonly,
453
+ // FAITHFULNESS: better-sqlite3's ACTUAL default is foreign_keys=ON (empirically
454
+ // verified — SQLite's own default is OFF, but bs3 enables it). massu was written +
455
+ // tested against that default, so the adapter must match it, or a store that relies
456
+ // on FK-ON-by-default (without an explicit pragma) would silently lose enforcement.
457
+ // Explicit `foreign_keys=OFF/ON` pragmas (e.g. memory-db bulk ops) still override.
458
+ enableForeignKeyConstraints: true,
459
+ allowExtension: false
460
+ });
461
+ let savepointSeq = 0;
462
+ const wrapStmt = (sql) => {
463
+ const st = raw.prepare(sql);
464
+ return {
465
+ run: (...p) => {
466
+ const r = st.run(...p);
467
+ return {
468
+ changes: typeof r.changes === "bigint" ? Number(r.changes) : r.changes,
469
+ lastInsertRowid: r.lastInsertRowid
470
+ };
471
+ },
472
+ get: (...p) => plainRow(st.get(...p)),
473
+ all: (...p) => st.all(...p).map(plainRow)
474
+ };
475
+ };
476
+ const pragma = (source, options) => {
477
+ const s = String(source).trim();
478
+ if (s.includes("=")) {
479
+ raw.exec(`PRAGMA ${s}`);
480
+ return void 0;
481
+ }
482
+ const rows = raw.prepare(`PRAGMA ${s}`).all().map(plainRow);
483
+ if (options?.simple) {
484
+ const first = rows[0];
485
+ return first ? Object.values(first)[0] : void 0;
486
+ }
487
+ return rows;
488
+ };
489
+ const transaction = (fn) => {
490
+ const run = (...args) => {
491
+ const nested = raw.isTransaction;
492
+ const name = `msp_${savepointSeq++}`;
493
+ if (nested) raw.exec(`SAVEPOINT ${name}`);
494
+ else raw.exec("BEGIN");
495
+ try {
496
+ const out = fn(...args);
497
+ if (nested) raw.exec(`RELEASE ${name}`);
498
+ else raw.exec("COMMIT");
499
+ return out;
500
+ } catch (e) {
501
+ if (nested) {
502
+ raw.exec(`ROLLBACK TO ${name}`);
503
+ raw.exec(`RELEASE ${name}`);
504
+ } else {
505
+ raw.exec("ROLLBACK");
506
+ }
507
+ throw e;
508
+ }
509
+ };
510
+ return run;
511
+ };
512
+ const serialize = () => {
513
+ if (raw.isTransaction) {
514
+ throw new Error(
515
+ "db-driver: .serialize() (node:sqlite engine) cannot run inside an open transaction \u2014 VACUUM is forbidden in a transaction. Serialize before BEGIN, or use MASSU_DB_ENGINE=better-sqlite3 for mid-transaction snapshots."
516
+ );
517
+ }
518
+ const dir = mkdtempSync(join2(tmpdir(), "massu-serialize-"));
519
+ const out = join2(dir, "snapshot.db");
520
+ try {
521
+ raw.exec(`VACUUM INTO '${out.replace(/'/g, "''")}'`);
522
+ const fd = openSync(out, "r");
523
+ try {
524
+ fsyncSync(fd);
525
+ } finally {
526
+ closeSync(fd);
527
+ }
528
+ return readFileSync3(out);
529
+ } finally {
530
+ rmSync3(dir, { recursive: true, force: true });
531
+ }
532
+ };
533
+ const handle = {
534
+ prepare: (sql) => wrapStmt(sql),
535
+ exec: (sql) => raw.exec(sql),
536
+ pragma,
537
+ transaction,
538
+ serialize,
539
+ close: () => raw.close()
540
+ };
541
+ return handle;
542
+ }
543
+ function openBetterSqlite3(dbPath, opts) {
544
+ return openDatabase(dbPath, opts);
545
+ }
546
+ function openDatabase2(dbPath, opts = {}) {
547
+ return resolveDbEngine() === "better-sqlite3" ? openBetterSqlite3(dbPath, opts) : openNodeSqlite(dbPath, opts);
548
+ }
549
+ var req2, DEFAULT_DB_ENGINE, DB_ENGINE_ENV, _nodeCtor;
550
+ var init_db_driver = __esm({
551
+ "src/db-driver.ts"() {
552
+ "use strict";
553
+ init_sqlite_loader();
554
+ init_sqlite_loader();
555
+ req2 = createRequire2(import.meta.url);
556
+ DEFAULT_DB_ENGINE = "node-sqlite";
557
+ DB_ENGINE_ENV = "MASSU_DB_ENGINE";
558
+ _nodeCtor = null;
559
+ }
560
+ });
561
+
562
+ // src/lib/memory-path.ts
563
+ function encodeMemoryDirName(projectRoot) {
564
+ return projectRoot.replace(/\//g, "-");
565
+ }
566
+ var init_memory_path = __esm({
567
+ "src/lib/memory-path.ts"() {
568
+ "use strict";
569
+ }
570
+ });
571
+
113
572
  // src/config-memory-schema.ts
114
573
  import { z } from "zod";
115
574
  var MemoryConfigSchema;
@@ -243,14 +702,40 @@ var init_config_memory_schema = __esm({
243
702
  // turn of EVERY session, so an unbounded index is a permanent context tax — and the
244
703
  // per-session cap bounds only the RATE, never the total.
245
704
  indexMaxLines: z.number().int().min(1).default(50)
705
+ }).default({}),
706
+ // --- Cross-repo memory surfacing (plan-living-memory-slice-5-cross-repo-surfacing) ---
707
+ // A decision made in one of your repos can surface in another — opt-in per
708
+ // decision AND opt-in per repo, signed, verified, and materialized ONLY on
709
+ // explicit human acceptance. Local transport is FREE and zero-network.
710
+ //
711
+ // ⛔ BOTH switches default OFF, and OFF means NOTHING EXISTS — no registry, no
712
+ // keys, no inbox, no behavioural difference. Two INDEPENDENT opt-ins:
713
+ // • enabled — may this repo EXPORT its shareable decisions? (opt-in #1)
714
+ // • subscribe — which repo LABELS may this repo IMPORT from? (opt-in #2)
715
+ // `subscribe: []` means import NOTHING. There is deliberately NO `subscribe: all`
716
+ // — a repo you never named is a repo Massu never reads from.
717
+ share: z.object({
718
+ enabled: z.boolean().default(false),
719
+ subscribe: z.array(z.string()).default([]),
720
+ // C-04 — recall surfacing of cross-repo memories. `enabled` defaults true but is
721
+ // CONDITIONAL on `subscribe` being non-empty (empty by default), so the effective
722
+ // default is DORMANT: with `subscribe: []` the recall hook output is byte-identical
723
+ // to today's. `maxCrossRepoItems` caps how many accepted cross-repo items may appear
724
+ // per recall block (default 1); `minScore` is an OPTIONAL strictly-higher floor for
725
+ // cross-repo items (they are, by construction, less relevant than local ones).
726
+ recall: z.object({
727
+ enabled: z.boolean().default(true),
728
+ maxCrossRepoItems: z.number().int().min(0).default(1),
729
+ minScore: z.number().optional()
730
+ }).default({})
246
731
  }).default({})
247
732
  }).optional();
248
733
  }
249
734
  });
250
735
 
251
736
  // src/config.ts
252
- import { resolve as resolve2, dirname } from "path";
253
- import { existsSync as existsSync2, readFileSync as readFileSync2 } from "fs";
737
+ import { resolve as resolve2, dirname as dirname3 } from "path";
738
+ import { existsSync as existsSync4, readFileSync as readFileSync4 } from "fs";
254
739
  import { homedir as homedir2 } from "os";
255
740
  import { parse as parseYaml } from "yaml";
256
741
  import { z as z2 } from "zod";
@@ -258,22 +743,22 @@ function findProjectRoot() {
258
743
  const cwd = process.cwd();
259
744
  let dir = cwd;
260
745
  while (true) {
261
- if (existsSync2(resolve2(dir, "massu.config.yaml"))) {
746
+ if (existsSync4(resolve2(dir, "massu.config.yaml"))) {
262
747
  return dir;
263
748
  }
264
- const parent = dirname(dir);
749
+ const parent = dirname3(dir);
265
750
  if (parent === dir) break;
266
751
  dir = parent;
267
752
  }
268
753
  dir = cwd;
269
754
  while (true) {
270
- if (existsSync2(resolve2(dir, "package.json"))) {
755
+ if (existsSync4(resolve2(dir, "package.json"))) {
271
756
  return dir;
272
757
  }
273
- if (existsSync2(resolve2(dir, ".git"))) {
758
+ if (existsSync4(resolve2(dir, ".git"))) {
274
759
  return dir;
275
760
  }
276
- const parent = dirname(dir);
761
+ const parent = dirname3(dir);
277
762
  if (parent === dir) break;
278
763
  dir = parent;
279
764
  }
@@ -290,8 +775,8 @@ function getConfig() {
290
775
  const root = getProjectRoot();
291
776
  const configPath = resolve2(root, "massu.config.yaml");
292
777
  let rawYaml = {};
293
- if (existsSync2(configPath)) {
294
- const content = readFileSync2(configPath, "utf-8");
778
+ if (existsSync4(configPath)) {
779
+ const content = readFileSync4(configPath, "utf-8");
295
780
  rawYaml = parseYaml(content) ?? {};
296
781
  }
297
782
  const result = RawConfigSchema.safeParse(rawYaml);
@@ -372,9 +857,13 @@ Hint: run \`massu config refresh\` to regenerate a valid config or fix the liste
372
857
  const resolvedEndpoint = resolveEndpoint({ configEndpoint: _config.cloud?.endpoint });
373
858
  if (resolvedKey.apiKey) {
374
859
  _config.cloud = {
375
- enabled: true,
376
860
  sync: { memory: true, analytics: true, audit: true },
377
861
  ..._config.cloud,
862
+ // Spread FIRST, then decide `enabled`, so a workspace that declares a `cloud:`
863
+ // block only to tune `requestTimeoutMs` does not disable its own sync. An
864
+ // EXPLICIT `enabled: false` is still honoured; `undefined` (not stated) means
865
+ // "a key resolved, so turn it on". (plan-2026-07-20-cloud-sync-timeout)
866
+ enabled: _config.cloud?.enabled ?? true,
378
867
  apiKey: resolvedKey.apiKey,
379
868
  endpoint: resolvedEndpoint
380
869
  };
@@ -492,7 +981,6 @@ var init_config = __esm({
492
981
  custom_patterns: z2.array(CustomPatternSchema).default([])
493
982
  }).optional(),
494
983
  adr: z2.object({
495
- detection_phrases: z2.array(z2.string()).default(["chose", "decided", "switching to", "moving from", "going with"]),
496
984
  template: z2.string().default("default"),
497
985
  storage: z2.string().default("database"),
498
986
  output_dir: z2.string().default("docs/adr")
@@ -594,9 +1082,22 @@ var init_config = __esm({
594
1082
  })).default([])
595
1083
  }).optional();
596
1084
  CloudConfigSchema = z2.object({
597
- enabled: z2.boolean().default(false),
1085
+ // OPTIONAL, not `.default(false)`. A zod default is indistinguishable from an
1086
+ // explicit value once parsed, so `.default(false)` meant that merely declaring a
1087
+ // `cloud:` block (to set any OTHER key) silently produced `enabled: false`, which
1088
+ // then overrode the `enabled: true` auto-enable below via object spread — turning
1089
+ // cloud sync OFF as a side effect of tuning it. `undefined` now means "not stated",
1090
+ // and the auto-enable preserves it. (plan-2026-07-20-cloud-sync-timeout)
1091
+ enabled: z2.boolean().optional(),
598
1092
  apiKey: z2.string().optional(),
599
1093
  endpoint: z2.string().optional(),
1094
+ // Per-request POST budget for the `/sync` ingest. cloud-sync.ts reads this
1095
+ // (`(cloud as { requestTimeoutMs?: number }).requestTimeoutMs`) but it was ABSENT
1096
+ // from this schema, and zod strips unknown keys — so the knob was unreachable from
1097
+ // massu.config.yaml and the default could never be tuned. Measured 2026-07-20: a
1098
+ // 423-observation payload takes ~9.2s against the live ingest. Capped at
1099
+ // SYNC_DEADLINE_MS (20s) since the overall deadline clamps each attempt anyway.
1100
+ requestTimeoutMs: z2.number().int().positive().max(2e4).optional(),
600
1101
  sync: z2.object({
601
1102
  memory: z2.boolean().default(true),
602
1103
  analytics: z2.boolean().default(true),
@@ -833,27 +1334,26 @@ var init_memory_vector = __esm({
833
1334
  });
834
1335
 
835
1336
  // src/db-backup.ts
836
- import Database from "better-sqlite3";
837
- import { existsSync as existsSync3, mkdirSync as mkdirSync2, readdirSync, statSync, unlinkSync, copyFileSync, rmSync as rmSync2 } from "fs";
838
- import { resolve as resolve3, join, basename, dirname as dirname2 } from "path";
1337
+ import { existsSync as existsSync5, mkdirSync as mkdirSync4, readdirSync, statSync, unlinkSync, copyFileSync, rmSync as rmSync4 } from "fs";
1338
+ import { resolve as resolve3, join as join3, basename, dirname as dirname4 } from "path";
839
1339
  import { homedir as homedir3 } from "os";
840
1340
  function dbBackupsRoot(home = homedir3()) {
841
1341
  return resolve3(home, ".massu", "db-backups");
842
1342
  }
843
1343
  function projectBackupDir(projectRoot, home = homedir3()) {
844
1344
  const slug = basename(projectRoot).replace(/[^A-Za-z0-9._-]/g, "_") || "project";
845
- return join(dbBackupsRoot(home), slug);
1345
+ return join3(dbBackupsRoot(home), slug);
846
1346
  }
847
1347
  function backupStamp(nowMs) {
848
1348
  return new Date(nowMs).toISOString().replace(/[:.]/g, "-");
849
1349
  }
850
1350
  function listDbBackups(projectRoot, home = homedir3()) {
851
1351
  const dir = projectBackupDir(projectRoot, home);
852
- if (!existsSync3(dir)) return [];
1352
+ if (!existsSync5(dir)) return [];
853
1353
  const out = [];
854
1354
  for (const f of readdirSync(dir)) {
855
1355
  if (!f.endsWith(".db")) continue;
856
- const p = join(dir, f);
1356
+ const p = join3(dir, f);
857
1357
  const st = statSync(p);
858
1358
  const db = f.replace(/-\d{4}-\d{2}-\d{2}T.*$/, "");
859
1359
  out.push({ db, path: p, bytes: st.size, mtimeMs: st.mtimeMs });
@@ -866,13 +1366,13 @@ function hasFreshDbBackup(projectRoot, dbPath, nowMs = Date.now(), home = homedi
866
1366
  if (backups.length === 0) return false;
867
1367
  const newest = backups[0];
868
1368
  if (nowMs - newest.mtimeMs > FRESH_WINDOW_MS) return false;
869
- if (existsSync3(dbPath) && statSync(dbPath).mtimeMs > newest.mtimeMs) return false;
1369
+ if (existsSync5(dbPath) && statSync(dbPath).mtimeMs > newest.mtimeMs) return false;
870
1370
  return true;
871
1371
  }
872
1372
  function integrityOk(dbPath) {
873
1373
  let db = null;
874
1374
  try {
875
- db = new Database(dbPath, { readonly: true });
1375
+ db = openDatabase2(dbPath, { readonly: true });
876
1376
  const rows = db.pragma("integrity_check");
877
1377
  return rows.length === 1 && rows[0].integrity_check === "ok";
878
1378
  } catch {
@@ -885,16 +1385,16 @@ function integrityOk(dbPath) {
885
1385
  }
886
1386
  }
887
1387
  function backupDb(projectRoot, dbPath, nowMs = Date.now(), home = homedir3()) {
888
- if (!existsSync3(dbPath)) {
1388
+ if (!existsSync5(dbPath)) {
889
1389
  throw new DbBackupError(`cannot back up a database that does not exist: ${dbPath}`);
890
1390
  }
891
1391
  const dir = projectBackupDir(projectRoot, home);
892
- mkdirSync2(dir, { recursive: true });
1392
+ mkdirSync4(dir, { recursive: true });
893
1393
  const name = basename(dbPath, ".db");
894
- const dest = join(dir, `${name}-${backupStamp(nowMs)}.db`);
1394
+ const dest = join3(dir, `${name}-${backupStamp(nowMs)}.db`);
895
1395
  let src = null;
896
1396
  try {
897
- src = new Database(dbPath, { readonly: true });
1397
+ src = openDatabase2(dbPath, { readonly: true });
898
1398
  src.exec(`VACUUM INTO '${dest.replace(/'/g, "''")}'`);
899
1399
  } catch (err) {
900
1400
  throw new DbBackupError(
@@ -933,7 +1433,7 @@ function pruneDbBackups(projectRoot, dbName, keep = DEFAULT_RETENTION, home = ho
933
1433
  }
934
1434
  function backupBeforeSchemaChange(projectRoot, dbPath, onError, nowMs = Date.now(), home = homedir3()) {
935
1435
  try {
936
- if (!existsSync3(dbPath)) return null;
1436
+ if (!existsSync5(dbPath)) return null;
937
1437
  if (hasFreshDbBackup(projectRoot, dbPath, nowMs, home)) return null;
938
1438
  return backupDb(projectRoot, dbPath, nowMs, home);
939
1439
  } catch (err) {
@@ -945,6 +1445,7 @@ var DEFAULT_RETENTION, FRESH_WINDOW_MS, DbBackupError;
945
1445
  var init_db_backup = __esm({
946
1446
  "src/db-backup.ts"() {
947
1447
  "use strict";
1448
+ init_db_driver();
948
1449
  DEFAULT_RETENTION = 5;
949
1450
  FRESH_WINDOW_MS = 24 * 60 * 60 * 1e3;
950
1451
  DbBackupError = class extends Error {
@@ -1057,21 +1558,21 @@ var init_rule_candidate_store = __esm({
1057
1558
  });
1058
1559
 
1059
1560
  // src/hooks/lib/hook-failure-signal.ts
1060
- import { appendFileSync, mkdirSync as mkdirSync3, existsSync as existsSync4 } from "fs";
1061
- import { join as join2, dirname as dirname3 } from "path";
1561
+ import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync5, existsSync as existsSync6 } from "fs";
1562
+ import { join as join4, dirname as dirname5 } from "path";
1062
1563
  function resolveFailureLogPath() {
1063
1564
  const explicit = process.env.MASSU_HOOK_FAILURE_LOG;
1064
1565
  if (explicit) return explicit;
1065
1566
  let dir = process.cwd();
1066
1567
  for (let i = 0; i < 12; i++) {
1067
- if (existsSync4(join2(dir, ".massu")) || existsSync4(join2(dir, "massu.config.yaml"))) {
1068
- return join2(dir, ".massu", "hook-failures.jsonl");
1568
+ if (existsSync6(join4(dir, ".massu")) || existsSync6(join4(dir, "massu.config.yaml"))) {
1569
+ return join4(dir, ".massu", "hook-failures.jsonl");
1069
1570
  }
1070
- const parent = dirname3(dir);
1571
+ const parent = dirname5(dir);
1071
1572
  if (parent === dir) break;
1072
1573
  dir = parent;
1073
1574
  }
1074
- return join2(process.cwd(), ".massu", "hook-failures.jsonl");
1575
+ return join4(process.cwd(), ".massu", "hook-failures.jsonl");
1075
1576
  }
1076
1577
  function recordHookFailure(hook, error, context) {
1077
1578
  const record = {
@@ -1084,8 +1585,8 @@ function recordHookFailure(hook, error, context) {
1084
1585
  let wroteSomething = false;
1085
1586
  try {
1086
1587
  const path = resolveFailureLogPath();
1087
- mkdirSync3(dirname3(path), { recursive: true });
1088
- appendFileSync(path, JSON.stringify(record) + "\n", "utf-8");
1588
+ mkdirSync5(dirname5(path), { recursive: true });
1589
+ appendFileSync2(path, JSON.stringify(record) + "\n", "utf-8");
1089
1590
  wroteSomething = true;
1090
1591
  } catch {
1091
1592
  }
@@ -1128,9 +1629,9 @@ var init_hook_failure_signal = __esm({
1128
1629
  });
1129
1630
 
1130
1631
  // src/memory-embedder-tokenizer.ts
1131
- import { readFileSync as readFileSync3 } from "fs";
1632
+ import { readFileSync as readFileSync5 } from "fs";
1132
1633
  function loadVocab(vocabPath) {
1133
- const lines = readFileSync3(vocabPath, "utf-8").split("\n");
1634
+ const lines = readFileSync5(vocabPath, "utf-8").split("\n");
1134
1635
  const vocab = /* @__PURE__ */ new Map();
1135
1636
  for (let i = 0; i < lines.length; i++) {
1136
1637
  const tok = lines[i].replace(/\r$/, "");
@@ -1250,9 +1751,9 @@ var init_memory_embedder_tokenizer = __esm({
1250
1751
 
1251
1752
  // src/memory-embedder.ts
1252
1753
  import { fileURLToPath } from "url";
1253
- import { dirname as dirname4, join as join3 } from "path";
1254
- import { existsSync as existsSync5 } from "fs";
1255
- import { createRequire } from "module";
1754
+ import { dirname as dirname6, join as join5 } from "path";
1755
+ import { existsSync as existsSync7 } from "fs";
1756
+ import { createRequire as createRequire3 } from "module";
1256
1757
  function getActiveEmbedModel() {
1257
1758
  return _activeModel;
1258
1759
  }
@@ -1273,21 +1774,21 @@ function embeddingsDisabled() {
1273
1774
  }
1274
1775
  function resolveModelDir() {
1275
1776
  try {
1276
- let dir = dirname4(fileURLToPath(import.meta.url));
1777
+ let dir = dirname6(fileURLToPath(import.meta.url));
1277
1778
  let root = null;
1278
1779
  for (let i = 0; i < 8; i++) {
1279
- if (existsSync5(join3(dir, "package.json"))) {
1780
+ if (existsSync7(join5(dir, "package.json"))) {
1280
1781
  root = dir;
1281
1782
  break;
1282
1783
  }
1283
- const parent = dirname4(dir);
1784
+ const parent = dirname6(dir);
1284
1785
  if (parent === dir) break;
1285
1786
  dir = parent;
1286
1787
  }
1287
1788
  if (!root) return null;
1288
1789
  for (const rel of ["dist/embedder", "assets/embedder"]) {
1289
- const candidate = join3(root, rel);
1290
- if (existsSync5(join3(candidate, "model_quantized.onnx"))) return candidate;
1790
+ const candidate = join5(root, rel);
1791
+ if (existsSync7(join5(candidate, "model_quantized.onnx"))) return candidate;
1291
1792
  }
1292
1793
  return null;
1293
1794
  } catch {
@@ -1296,9 +1797,9 @@ function resolveModelDir() {
1296
1797
  }
1297
1798
  function resolveWasmDir() {
1298
1799
  try {
1299
- const req = createRequire(import.meta.url);
1300
- const main2 = req.resolve(ORT_PKG);
1301
- return dirname4(main2);
1800
+ const req3 = createRequire3(import.meta.url);
1801
+ const main2 = req3.resolve(ORT_PKG);
1802
+ return dirname6(main2);
1302
1803
  } catch {
1303
1804
  return null;
1304
1805
  }
@@ -1318,8 +1819,8 @@ async function getSession() {
1318
1819
  ort.env.wasm.proxy = false;
1319
1820
  ort.env.wasm.wasmPaths = wasmDir.endsWith("/") ? wasmDir : wasmDir + "/";
1320
1821
  _ortTensor = ort.Tensor;
1321
- const modelPath = join3(modelDir, "model_quantized.onnx");
1322
- const vocabPath = join3(modelDir, "vocab.txt");
1822
+ const modelPath = join5(modelDir, "model_quantized.onnx");
1823
+ const vocabPath = join5(modelDir, "vocab.txt");
1323
1824
  _vocab = loadVocab(vocabPath);
1324
1825
  const session = await ort.InferenceSession.create(modelPath, {
1325
1826
  executionProviders: ["wasm"],
@@ -1640,6 +2141,7 @@ __export(memory_db_exports, {
1640
2141
  migrateAuditLogCheckExtension: () => migrateAuditLogCheckExtension,
1641
2142
  migrateMemoryFilesFor4B: () => migrateMemoryFilesFor4B,
1642
2143
  migrateObservationEmbeddingChunks: () => migrateObservationEmbeddingChunks,
2144
+ migrateSharedMemoryFor5B: () => migrateSharedMemoryFor5B,
1643
2145
  pruneOldConversationTurns: () => pruneOldConversationTurns,
1644
2146
  pruneOldObservations: () => pruneOldObservations,
1645
2147
  pruneToolCostEvents: () => pruneToolCostEvents,
@@ -1656,9 +2158,8 @@ __export(memory_db_exports, {
1656
2158
  upsertObservationEmbedding: () => upsertObservationEmbedding,
1657
2159
  usageWarmupElapsed: () => usageWarmupElapsed
1658
2160
  });
1659
- import Database2 from "better-sqlite3";
1660
- import { dirname as dirname5, basename as basename2 } from "path";
1661
- import { existsSync as existsSync6, mkdirSync as mkdirSync4 } from "fs";
2161
+ import { dirname as dirname7, basename as basename2 } from "path";
2162
+ import { existsSync as existsSync8, mkdirSync as mkdirSync6 } from "fs";
1662
2163
  function sanitizeFts5Query(raw) {
1663
2164
  const trimmed = raw.trim();
1664
2165
  if (!trimmed) return '""';
@@ -1674,12 +2175,12 @@ function sanitizeFts5QueryOr(raw) {
1674
2175
  }
1675
2176
  function getMemoryDb() {
1676
2177
  const dbPath = getResolvedPaths().memoryDbPath;
1677
- const dir = dirname5(dbPath);
1678
- if (!existsSync6(dir)) {
1679
- mkdirSync4(dir, { recursive: true });
2178
+ const dir = dirname7(dbPath);
2179
+ if (!existsSync8(dir)) {
2180
+ mkdirSync6(dir, { recursive: true });
1680
2181
  }
1681
- const preExisting = existsSync6(dbPath);
1682
- const db = new Database2(dbPath);
2182
+ const preExisting = existsSync8(dbPath);
2183
+ const db = openDatabase2(dbPath);
1683
2184
  db.pragma("journal_mode = WAL");
1684
2185
  db.pragma("foreign_keys = ON");
1685
2186
  const onDisk = db.pragma("user_version", { simple: true });
@@ -1755,6 +2256,39 @@ function migrateMemoryFilesFor4B(db) {
1755
2256
  db.exec(`ALTER TABLE ${add.table} ADD COLUMN ${add.name} ${add.decl}`);
1756
2257
  }
1757
2258
  }
2259
+ function migrateSharedMemoryFor5B(db) {
2260
+ const cols = db.prepare(`PRAGMA table_info(observations)`).all();
2261
+ if (cols.length > 0 && !cols.some((c) => c.name === "shareable")) {
2262
+ db.exec(`ALTER TABLE observations ADD COLUMN shareable INTEGER NOT NULL DEFAULT 0`);
2263
+ }
2264
+ db.exec(`
2265
+ CREATE TABLE IF NOT EXISTS shared_memory_pending (
2266
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
2267
+ record_hash TEXT NOT NULL UNIQUE, -- identity + idempotency key (hex sha256)
2268
+ origin_repo_id TEXT NOT NULL, -- the signed origin repo_id (v4 UUID)
2269
+ origin_repo_label TEXT NOT NULL, -- re-slugged on read; NEVER trusted from the wire
2270
+ envelope_raw TEXT NOT NULL, -- the VERBATIM signed bytes; B-05 re-verifies these
2271
+ record_json TEXT NOT NULL, -- the single record's canonical JSON
2272
+ received_at_epoch INTEGER NOT NULL, -- epoch SECONDS (Slice-2 convention)
2273
+ accepted_at_epoch INTEGER, -- set on accept (B-05); NULL while pending
2274
+ refused_at_epoch INTEGER, -- set on refuse (B-06)
2275
+ expired_at_epoch INTEGER -- revocation \u21D2 EXPIRE, never DELETE (B-07 / S-3)
2276
+ );
2277
+ CREATE INDEX IF NOT EXISTS idx_smp_origin ON shared_memory_pending(origin_repo_id);
2278
+ CREATE INDEX IF NOT EXISTS idx_smp_received ON shared_memory_pending(received_at_epoch DESC);
2279
+ `);
2280
+ db.exec(`
2281
+ CREATE TABLE IF NOT EXISTS shared_memory_outbound (
2282
+ record_hash TEXT PRIMARY KEY, -- the exported record's canonical hash
2283
+ observation_id INTEGER NOT NULL, -- the source observations.id (for B-07 revocation)
2284
+ origin_repo_id TEXT NOT NULL, -- this repo's own repo_id at export time
2285
+ seq INTEGER NOT NULL, -- the envelope seq this record first went out in
2286
+ exported_at_epoch INTEGER NOT NULL, -- epoch SECONDS
2287
+ revoked_at_epoch INTEGER -- set when the source row is superseded/expired (B-07)
2288
+ );
2289
+ CREATE INDEX IF NOT EXISTS idx_smo_obs ON shared_memory_outbound(observation_id);
2290
+ `);
2291
+ }
1758
2292
  function migrateAuditLogCheckExtension(db) {
1759
2293
  const row = db.prepare("SELECT sql FROM sqlite_master WHERE type='table' AND name='audit_log'").get();
1760
2294
  if (!row) return;
@@ -1777,7 +2311,16 @@ function migrateAuditLogCheckExtension(db) {
1777
2311
  "memory_file_adopted_human",
1778
2312
  "memory_file_rendered",
1779
2313
  "memory_file_render_refused",
1780
- "memory_file_tombstoned"
2314
+ "memory_file_tombstoned",
2315
+ // B-10 (Slice 5) — the seven cross-repo shared-memory events. An existing DB whose
2316
+ // CHECK lacks any of these triggers the table rebuild below.
2317
+ "shared_memory_exported",
2318
+ "shared_memory_export_refused",
2319
+ "shared_memory_imported",
2320
+ "shared_memory_dropped",
2321
+ "shared_memory_accepted",
2322
+ "shared_memory_refused",
2323
+ "shared_memory_revoked"
1781
2324
  ];
1782
2325
  const checkClauseMatch = row.sql.match(/event_type\s+TEXT\s+NOT\s+NULL\s+CHECK\s*\(\s*event_type\s+IN\s*\(([\s\S]*?)\)\s*\)/i);
1783
2326
  if (checkClauseMatch) {
@@ -1796,7 +2339,12 @@ function migrateAuditLogCheckExtension(db) {
1796
2339
  'code_change', 'rule_enforced', 'approval', 'review', 'commit', 'compaction',
1797
2340
  'rule_candidate_emitted', 'rule_promoted', 'rule_dismissed',
1798
2341
  'memory_file_ingested', 'memory_file_expired', 'memory_file_adopted_human',
1799
- 'memory_file_rendered', 'memory_file_render_refused', 'memory_file_tombstoned'
2342
+ 'memory_file_rendered', 'memory_file_render_refused', 'memory_file_tombstoned',
2343
+ -- B-10 (Slice 5) \u2014 cross-repo shared-memory events (must mirror the CREATE +
2344
+ -- the expected[] list above, or the migration would loop rebuilding forever).
2345
+ 'shared_memory_exported', 'shared_memory_export_refused', 'shared_memory_imported',
2346
+ 'shared_memory_dropped', 'shared_memory_accepted', 'shared_memory_refused',
2347
+ 'shared_memory_revoked'
1800
2348
  )),
1801
2349
  actor TEXT NOT NULL DEFAULT 'ai' CHECK(actor IN ('ai', 'human', 'hook', 'agent')),
1802
2350
  model_id TEXT,
@@ -1873,6 +2421,12 @@ function initMemorySchema(db) {
1873
2421
  -- credential, or takes a snapshot. A Slice-5 synced memory arrives here with
1874
2422
  -- origin='team' and must never reach disk without CR-55's gates.
1875
2423
  origin TEXT NOT NULL DEFAULT 'local',
2424
+ -- B-01 (Slice 5, cross-repo surfacing): the per-decision SHARE opt-in. Set to 1
2425
+ -- ONLY by an explicit human act (massu memory share <id>); NEVER by a heuristic,
2426
+ -- score, LLM, hook, or consolidation pass. Export (B-02) reads WHERE shareable=1
2427
+ -- AND origin='local'. Defaults 0 so every pre-existing and machine-written row is
2428
+ -- un-shareable until a human says otherwise (fail-closed).
2429
+ shareable INTEGER NOT NULL DEFAULT 0,
1876
2430
  created_at TEXT NOT NULL,
1877
2431
  created_at_epoch INTEGER NOT NULL,
1878
2432
  FOREIGN KEY(session_id) REFERENCES sessions(session_id) ON DELETE CASCADE
@@ -2146,7 +2700,13 @@ function initMemorySchema(db) {
2146
2700
  'code_change', 'rule_enforced', 'approval', 'review', 'commit', 'compaction',
2147
2701
  'rule_candidate_emitted', 'rule_promoted', 'rule_dismissed',
2148
2702
  'memory_file_ingested', 'memory_file_expired', 'memory_file_adopted_human',
2149
- 'memory_file_rendered', 'memory_file_render_refused', 'memory_file_tombstoned'
2703
+ 'memory_file_rendered', 'memory_file_render_refused', 'memory_file_tombstoned',
2704
+ -- B-10 (Slice 5): cross-repo shared-memory observability. Without these seven,
2705
+ -- nobody could ever answer "what has crossed between my repos?" (and an ingest
2706
+ -- catch would swallow the CHECK violation, silently producing NOTHING).
2707
+ 'shared_memory_exported', 'shared_memory_export_refused', 'shared_memory_imported',
2708
+ 'shared_memory_dropped', 'shared_memory_accepted', 'shared_memory_refused',
2709
+ 'shared_memory_revoked'
2150
2710
  )),
2151
2711
  actor TEXT NOT NULL DEFAULT 'ai' CHECK(actor IN ('ai', 'human', 'hook', 'agent')),
2152
2712
  model_id TEXT,
@@ -2515,6 +3075,7 @@ function initMemorySchema(db) {
2515
3075
  `);
2516
3076
  migrateObservationEmbeddingChunks(db);
2517
3077
  migrateMemoryFilesFor4B(db);
3078
+ migrateSharedMemoryFor5B(db);
2518
3079
  ensureRuleCandidatesTable(db);
2519
3080
  migrateRuleDelivery(db);
2520
3081
  }
@@ -3385,6 +3946,7 @@ var MEMORY_SCHEMA_VERSION, TOOL_COST_EVENTS_RETENTION_DAYS, MAX_DRAFT_TEXT_LEN,
3385
3946
  var init_memory_db = __esm({
3386
3947
  "src/memory-db.ts"() {
3387
3948
  "use strict";
3949
+ init_db_driver();
3388
3950
  init_config();
3389
3951
  init_memory_vector();
3390
3952
  init_db_backup();
@@ -3414,68 +3976,6 @@ function estimateTokens(text) {
3414
3976
 
3415
3977
  // src/observation-extractor.ts
3416
3978
  init_memory_db();
3417
-
3418
- // src/adr-generator.ts
3419
- init_config();
3420
- var DEFAULT_DETECTION_PHRASES = ["chose", "decided", "switching to", "moving from", "going with"];
3421
- function getDetectionPhrases() {
3422
- return getConfig().governance?.adr?.detection_phrases ?? DEFAULT_DETECTION_PHRASES;
3423
- }
3424
- function detectDecisionPatterns(text) {
3425
- const phrases = getDetectionPhrases();
3426
- const lower = text.toLowerCase();
3427
- return phrases.some((phrase) => lower.includes(phrase));
3428
- }
3429
- function extractAlternatives(description) {
3430
- const alternatives = [];
3431
- const overMatch = description.match(/(\w[\w\s-]+)\s+over\s+(\w[\w\s-]+)/i);
3432
- if (overMatch) {
3433
- alternatives.push(overMatch[1].trim());
3434
- alternatives.push(overMatch[2].trim());
3435
- }
3436
- const insteadMatch = description.match(/(\w[\w\s-]+)\s+instead\s+of\s+(\w[\w\s-]+)/i);
3437
- if (insteadMatch) {
3438
- alternatives.push(insteadMatch[1].trim());
3439
- alternatives.push(insteadMatch[2].trim());
3440
- }
3441
- const switchMatch = description.match(/switching\s+from\s+(\w[\w\s-]+)\s+to\s+(\w[\w\s-]+)/i);
3442
- if (switchMatch) {
3443
- alternatives.push(switchMatch[2].trim());
3444
- alternatives.push(switchMatch[1].trim());
3445
- }
3446
- return [...new Set(alternatives)];
3447
- }
3448
- function storeDecision(db, decision) {
3449
- const existing = new Set(
3450
- db.prepare(`PRAGMA table_info(architecture_decisions)`).all().map(
3451
- (c) => c.name
3452
- )
3453
- );
3454
- const cols = ["title", "context", "decision", "alternatives", "consequences", "session_id", "status"];
3455
- const vals = [
3456
- decision.title,
3457
- decision.context,
3458
- decision.decision,
3459
- JSON.stringify(decision.alternatives),
3460
- decision.consequences,
3461
- decision.sessionId ?? null,
3462
- decision.status ?? "accepted"
3463
- ];
3464
- if (existing.has("affected_files")) {
3465
- cols.push("affected_files");
3466
- vals.push(decision.affectedFiles ? JSON.stringify(decision.affectedFiles) : null);
3467
- }
3468
- const hasTemporal = existing.has("valid_from_epoch");
3469
- const extraSql = hasTemporal ? `, valid_from, ingested_at, valid_from_epoch, ingested_at_epoch` : "";
3470
- const extraVals = hasTemporal ? `, datetime('now'), datetime('now'), CAST(strftime('%s','now') AS INTEGER), CAST(strftime('%s','now') AS INTEGER)` : "";
3471
- const result = db.prepare(
3472
- `INSERT INTO architecture_decisions (${cols.join(", ")}${extraSql})
3473
- VALUES (${cols.map(() => "?").join(", ")}${extraVals})`
3474
- ).run(...vals);
3475
- return Number(result.lastInsertRowid);
3476
- }
3477
-
3478
- // src/observation-extractor.ts
3479
3979
  init_config();
3480
3980
  import { homedir as homedir4 } from "os";
3481
3981
 
@@ -3778,23 +4278,6 @@ function classifyRealTimeToolCall(toolName, toolInput, toolResponse, seenReads2,
3778
4278
  isError: isError ?? normalized.isError
3779
4279
  };
3780
4280
  if (isNoisyToolCall(tc, seenReads2)) return null;
3781
- const responseText = normalized.text;
3782
- if (responseText && detectDecisionPatterns(responseText)) {
3783
- const firstLine = responseText.split("\n")[0].slice(0, 200);
3784
- const title = `Architecture decision: ${firstLine}`;
3785
- const detail = responseText.slice(0, 1e3);
3786
- return {
3787
- type: "decision",
3788
- title,
3789
- detail,
3790
- visibility: classifyVisibility(title, detail),
3791
- opts: {
3792
- importance: assignImportance("decision"),
3793
- originalTokens: estimateTokens(responseText),
3794
- ...extractLinkedReferences(responseText)
3795
- }
3796
- };
3797
- }
3798
4281
  return classifyToolCall(tc);
3799
4282
  }
3800
4283
  function detectPlanProgress(rawToolResponse) {
@@ -3911,8 +4394,8 @@ init_config();
3911
4394
 
3912
4395
  // src/import-resolver.ts
3913
4396
  init_config();
3914
- import { readFileSync as readFileSync4, existsSync as existsSync7, statSync as statSync2 } from "fs";
3915
- import { resolve as resolve6, dirname as dirname6, join as join4 } from "path";
4397
+ import { readFileSync as readFileSync6, existsSync as existsSync9, statSync as statSync2 } from "fs";
4398
+ import { resolve as resolve6, dirname as dirname8, join as join6 } from "path";
3916
4399
 
3917
4400
  // src/security-utils.ts
3918
4401
  import { resolve as resolve5, normalize } from "path";
@@ -3973,21 +4456,21 @@ function resolveImportPath(specifier, fromFile) {
3973
4456
  const paths = getResolvedPaths();
3974
4457
  basePath = resolve6(paths.pathAlias["@"] ?? paths.srcDir, specifier.slice(2));
3975
4458
  } else {
3976
- basePath = resolve6(dirname6(fromFile), specifier);
4459
+ basePath = resolve6(dirname8(fromFile), specifier);
3977
4460
  }
3978
- if (existsSync7(basePath) && !isDirectory(basePath)) {
4461
+ if (existsSync9(basePath) && !isDirectory(basePath)) {
3979
4462
  return toRelative(basePath);
3980
4463
  }
3981
4464
  const resolvedPaths = getResolvedPaths();
3982
4465
  for (const ext of resolvedPaths.extensions) {
3983
4466
  const withExt = basePath + ext;
3984
- if (existsSync7(withExt)) {
4467
+ if (existsSync9(withExt)) {
3985
4468
  return toRelative(withExt);
3986
4469
  }
3987
4470
  }
3988
4471
  for (const indexFile of resolvedPaths.indexFiles) {
3989
- const indexPath = join4(basePath, indexFile);
3990
- if (existsSync7(indexPath)) {
4472
+ const indexPath = join6(basePath, indexFile);
4473
+ if (existsSync9(indexPath)) {
3991
4474
  return toRelative(indexPath);
3992
4475
  }
3993
4476
  }
@@ -4009,7 +4492,7 @@ function toRelative(absPath) {
4009
4492
  }
4010
4493
 
4011
4494
  // src/validation-engine.ts
4012
- import { existsSync as existsSync8, readFileSync as readFileSync5 } from "fs";
4495
+ import { existsSync as existsSync10, readFileSync as readFileSync7 } from "fs";
4013
4496
  function getValidationChecks() {
4014
4497
  return getConfig().governance?.validation?.checks ?? {
4015
4498
  rule_compliance: true,
@@ -4037,7 +4520,7 @@ function validateFile(filePath, projectRoot) {
4037
4520
  });
4038
4521
  return checks;
4039
4522
  }
4040
- if (!existsSync8(absPath)) {
4523
+ if (!existsSync10(absPath)) {
4041
4524
  checks.push({
4042
4525
  name: "file_exists",
4043
4526
  severity: "error",
@@ -4046,7 +4529,7 @@ function validateFile(filePath, projectRoot) {
4046
4529
  });
4047
4530
  return checks;
4048
4531
  }
4049
- const source = readFileSync5(absPath, "utf-8");
4532
+ const source = readFileSync7(absPath, "utf-8");
4050
4533
  const lines = source.split("\n");
4051
4534
  if (activeChecks.rule_compliance !== false) {
4052
4535
  for (const ruleSet of config.rules) {
@@ -4145,7 +4628,7 @@ function storeValidationResult(db, filePath, checks, sessionId, validationType =
4145
4628
 
4146
4629
  // src/security-scorer.ts
4147
4630
  init_config();
4148
- import { existsSync as existsSync9, readFileSync as readFileSync6 } from "fs";
4631
+ import { existsSync as existsSync11, readFileSync as readFileSync8 } from "fs";
4149
4632
  var DEFAULT_SECURITY_PATTERNS = [
4150
4633
  {
4151
4634
  regex: /\bexec\s*\(\s*[`"'].*\$\{/,
@@ -4236,12 +4719,12 @@ function scoreFileSecurity(filePath, projectRoot) {
4236
4719
  }]
4237
4720
  };
4238
4721
  }
4239
- if (!existsSync9(absPath)) {
4722
+ if (!existsSync11(absPath)) {
4240
4723
  return { riskScore: 0, findings: [] };
4241
4724
  }
4242
4725
  let source;
4243
4726
  try {
4244
- source = readFileSync6(absPath, "utf-8");
4727
+ source = readFileSync8(absPath, "utf-8");
4245
4728
  } catch {
4246
4729
  return { riskScore: 0, findings: [] };
4247
4730
  }
@@ -4281,8 +4764,8 @@ function storeSecurityScore(db, sessionId, filePath, riskScore, findings) {
4281
4764
 
4282
4765
  // src/hooks/post-tool-use.ts
4283
4766
  init_config();
4284
- import { readFileSync as readFileSync8, writeFileSync as writeFileSync2, existsSync as existsSync11, mkdirSync as mkdirSync5, statSync as statSync3 } from "fs";
4285
- import { join as join5, basename as pathBasename2, dirname as pathDirname, resolve as pathResolve } from "path";
4767
+ import { readFileSync as readFileSync10, writeFileSync as writeFileSync3, existsSync as existsSync13, mkdirSync as mkdirSync7, statSync as statSync3 } from "fs";
4768
+ import { join as join7, basename as pathBasename2, dirname as pathDirname, resolve as pathResolve } from "path";
4286
4769
 
4287
4770
  // src/lib/recurrence-incrementer.ts
4288
4771
  var ANSI = /\x1b\[[0-9;]*m/g;
@@ -4346,7 +4829,7 @@ function incrementRecurrenceCountsForScannerFailures(db, sessionId, scannerStdou
4346
4829
  init_hook_failure_signal();
4347
4830
 
4348
4831
  // src/memory-file-ingest.ts
4349
- import { readFileSync as readFileSync7, existsSync as existsSync10, readdirSync as readdirSync2 } from "fs";
4832
+ import { readFileSync as readFileSync9, existsSync as existsSync12, readdirSync as readdirSync2 } from "fs";
4350
4833
  init_memory_db();
4351
4834
  import { parse as parseYaml2 } from "yaml";
4352
4835
  import { basename as pathBasename } from "path";
@@ -4455,8 +4938,8 @@ function upsertMemoryFileMirror(db, f) {
4455
4938
  return "inserted";
4456
4939
  }
4457
4940
  function ingestMemoryFile(db, sessionId, filePath) {
4458
- if (!existsSync10(filePath)) return "skipped";
4459
- const content = readFileSync7(filePath, "utf-8");
4941
+ if (!existsSync12(filePath)) return "skipped";
4942
+ const content = readFileSync9(filePath, "utf-8");
4460
4943
  if (Buffer.byteLength(content, "utf-8") > MAX_MEMORY_FILE_BYTES) {
4461
4944
  process.stderr.write(
4462
4945
  `[massu] memory file exceeds ${MAX_MEMORY_FILE_BYTES} bytes, not ingested: ${filePath}
@@ -4573,27 +5056,6 @@ async function main() {
4573
5056
  } else {
4574
5057
  addObservation(db, session_id, observation.type, observation.title, observation.detail, observation.opts);
4575
5058
  }
4576
- if (observation.type === "decision") {
4577
- try {
4578
- const decisionText = observation.detail ?? observation.title;
4579
- const alternatives = extractAlternatives(decisionText);
4580
- const existing = db.prepare(
4581
- `SELECT id FROM architecture_decisions WHERE session_id = ? AND title = ? LIMIT 1`
4582
- ).get(session_id, observation.title);
4583
- if (!existing) {
4584
- storeDecision(db, {
4585
- title: observation.title,
4586
- context: "",
4587
- decision: decisionText,
4588
- alternatives,
4589
- consequences: "",
4590
- sessionId: session_id,
4591
- affectedFiles: observation.opts?.filesInvolved
4592
- });
4593
- }
4594
- } catch {
4595
- }
4596
- }
4597
5059
  if (tool_response) {
4598
5060
  const progress = detectPlanProgress(tool_response);
4599
5061
  if (progress.length > 0) {
@@ -4730,12 +5192,12 @@ async function main() {
4730
5192
  } catch (err) {
4731
5193
  try {
4732
5194
  const projectRoot = process.cwd();
4733
- const dir = join5(projectRoot, ".massu", "rule-candidates");
4734
- if (!existsSync11(dir)) mkdirSync5(dir, { recursive: true });
4735
- const logPath = join5(dir, ".cr53-increment-failures.jsonl");
4736
- const pre = existsSync11(logPath) ? readFileSync8(logPath, "utf-8") : "";
5195
+ const dir = join7(projectRoot, ".massu", "rule-candidates");
5196
+ if (!existsSync13(dir)) mkdirSync7(dir, { recursive: true });
5197
+ const logPath = join7(dir, ".cr53-increment-failures.jsonl");
5198
+ const pre = existsSync13(logPath) ? readFileSync10(logPath, "utf-8") : "";
4737
5199
  const sep = pre && !pre.endsWith("\n") ? "\n" : "";
4738
- writeFileSync2(logPath, pre + sep + JSON.stringify({
5200
+ writeFileSync3(logPath, pre + sep + JSON.stringify({
4739
5201
  session_id,
4740
5202
  scanner_output_excerpt: (tool_response ?? "").slice(0, 200),
4741
5203
  error: err instanceof Error ? err.message : String(err),
@@ -4838,13 +5300,13 @@ var _conventionDefaults = {
4838
5300
  function readConventions(cwd) {
4839
5301
  try {
4840
5302
  const projectRoot = cwd ?? process.cwd();
4841
- const configPath = join5(projectRoot, "massu.config.yaml");
4842
- if (!existsSync11(configPath)) return _conventionDefaults;
5303
+ const configPath = join7(projectRoot, "massu.config.yaml");
5304
+ if (!existsSync13(configPath)) return _conventionDefaults;
4843
5305
  const mtimeMs = statSync3(configPath).mtimeMs;
4844
5306
  if (_cachedConventions !== null && _cachedConventionsPath === configPath && _cachedConventionsMtimeMs === mtimeMs) {
4845
5307
  return _cachedConventions;
4846
5308
  }
4847
- const content = readFileSync8(configPath, "utf-8");
5309
+ const content = readFileSync10(configPath, "utf-8");
4848
5310
  const parsed = parseYaml3(content);
4849
5311
  if (!parsed || typeof parsed !== "object") return _conventionDefaults;
4850
5312
  const conventions = parsed.conventions;
@@ -4876,11 +5338,11 @@ function isKnowledgeSourceFile(filePath) {
4876
5338
  function checkMemoryFileIntegrity(filePath) {
4877
5339
  const issues = [];
4878
5340
  try {
4879
- if (!existsSync11(filePath)) {
5341
+ if (!existsSync13(filePath)) {
4880
5342
  issues.push("MEMORY.md file does not exist after write");
4881
5343
  return issues;
4882
5344
  }
4883
- const content = readFileSync8(filePath, "utf-8");
5345
+ const content = readFileSync10(filePath, "utf-8");
4884
5346
  const lines = content.split("\n");
4885
5347
  const MAX_LINES = 200;
4886
5348
  if (lines.length > MAX_LINES) {