@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.
@@ -243,6 +243,32 @@ var init_config_memory_schema = __esm({
243
243
  // turn of EVERY session, so an unbounded index is a permanent context tax — and the
244
244
  // per-session cap bounds only the RATE, never the total.
245
245
  indexMaxLines: z.number().int().min(1).default(50)
246
+ }).default({}),
247
+ // --- Cross-repo memory surfacing (plan-living-memory-slice-5-cross-repo-surfacing) ---
248
+ // A decision made in one of your repos can surface in another — opt-in per
249
+ // decision AND opt-in per repo, signed, verified, and materialized ONLY on
250
+ // explicit human acceptance. Local transport is FREE and zero-network.
251
+ //
252
+ // ⛔ BOTH switches default OFF, and OFF means NOTHING EXISTS — no registry, no
253
+ // keys, no inbox, no behavioural difference. Two INDEPENDENT opt-ins:
254
+ // • enabled — may this repo EXPORT its shareable decisions? (opt-in #1)
255
+ // • subscribe — which repo LABELS may this repo IMPORT from? (opt-in #2)
256
+ // `subscribe: []` means import NOTHING. There is deliberately NO `subscribe: all`
257
+ // — a repo you never named is a repo Massu never reads from.
258
+ share: z.object({
259
+ enabled: z.boolean().default(false),
260
+ subscribe: z.array(z.string()).default([]),
261
+ // C-04 — recall surfacing of cross-repo memories. `enabled` defaults true but is
262
+ // CONDITIONAL on `subscribe` being non-empty (empty by default), so the effective
263
+ // default is DORMANT: with `subscribe: []` the recall hook output is byte-identical
264
+ // to today's. `maxCrossRepoItems` caps how many accepted cross-repo items may appear
265
+ // per recall block (default 1); `minScore` is an OPTIONAL strictly-higher floor for
266
+ // cross-repo items (they are, by construction, less relevant than local ones).
267
+ recall: z.object({
268
+ enabled: z.boolean().default(true),
269
+ maxCrossRepoItems: z.number().int().min(0).default(1),
270
+ minScore: z.number().optional()
271
+ }).default({})
246
272
  }).default({})
247
273
  }).optional();
248
274
  }
@@ -372,9 +398,13 @@ Hint: run \`massu config refresh\` to regenerate a valid config or fix the liste
372
398
  const resolvedEndpoint = resolveEndpoint({ configEndpoint: _config.cloud?.endpoint });
373
399
  if (resolvedKey.apiKey) {
374
400
  _config.cloud = {
375
- enabled: true,
376
401
  sync: { memory: true, analytics: true, audit: true },
377
402
  ..._config.cloud,
403
+ // Spread FIRST, then decide `enabled`, so a workspace that declares a `cloud:`
404
+ // block only to tune `requestTimeoutMs` does not disable its own sync. An
405
+ // EXPLICIT `enabled: false` is still honoured; `undefined` (not stated) means
406
+ // "a key resolved, so turn it on". (plan-2026-07-20-cloud-sync-timeout)
407
+ enabled: _config.cloud?.enabled ?? true,
378
408
  apiKey: resolvedKey.apiKey,
379
409
  endpoint: resolvedEndpoint
380
410
  };
@@ -492,7 +522,6 @@ var init_config = __esm({
492
522
  custom_patterns: z2.array(CustomPatternSchema).default([])
493
523
  }).optional(),
494
524
  adr: z2.object({
495
- detection_phrases: z2.array(z2.string()).default(["chose", "decided", "switching to", "moving from", "going with"]),
496
525
  template: z2.string().default("default"),
497
526
  storage: z2.string().default("database"),
498
527
  output_dir: z2.string().default("docs/adr")
@@ -594,9 +623,22 @@ var init_config = __esm({
594
623
  })).default([])
595
624
  }).optional();
596
625
  CloudConfigSchema = z2.object({
597
- enabled: z2.boolean().default(false),
626
+ // OPTIONAL, not `.default(false)`. A zod default is indistinguishable from an
627
+ // explicit value once parsed, so `.default(false)` meant that merely declaring a
628
+ // `cloud:` block (to set any OTHER key) silently produced `enabled: false`, which
629
+ // then overrode the `enabled: true` auto-enable below via object spread — turning
630
+ // cloud sync OFF as a side effect of tuning it. `undefined` now means "not stated",
631
+ // and the auto-enable preserves it. (plan-2026-07-20-cloud-sync-timeout)
632
+ enabled: z2.boolean().optional(),
598
633
  apiKey: z2.string().optional(),
599
634
  endpoint: z2.string().optional(),
635
+ // Per-request POST budget for the `/sync` ingest. cloud-sync.ts reads this
636
+ // (`(cloud as { requestTimeoutMs?: number }).requestTimeoutMs`) but it was ABSENT
637
+ // from this schema, and zod strips unknown keys — so the knob was unreachable from
638
+ // massu.config.yaml and the default could never be tuned. Measured 2026-07-20: a
639
+ // 423-observation payload takes ~9.2s against the live ingest. Capped at
640
+ // SYNC_DEADLINE_MS (20s) since the overall deadline clamps each attempt anyway.
641
+ requestTimeoutMs: z2.number().int().positive().max(2e4).optional(),
600
642
  sync: z2.object({
601
643
  memory: z2.boolean().default(true),
602
644
  analytics: z2.boolean().default(true),
@@ -813,6 +855,465 @@ var init_config = __esm({
813
855
  }
814
856
  });
815
857
 
858
+ // src/lib/fileLock.ts
859
+ import { mkdirSync as mkdirSync2, readFileSync as readFileSync3, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
860
+ import { dirname as dirname2 } from "path";
861
+ import * as lockfile from "proper-lockfile";
862
+ function readLockHolderPid(lockPath) {
863
+ try {
864
+ const raw = readFileSync3(`${lockPath}.pid`, "utf-8").trim();
865
+ const pid = Number.parseInt(raw, 10);
866
+ if (!Number.isFinite(pid) || pid <= 0) return null;
867
+ return pid;
868
+ } catch {
869
+ return null;
870
+ }
871
+ }
872
+ function busyWaitSync(ms) {
873
+ if (typeof SharedArrayBuffer === "undefined" || typeof Atomics === "undefined") {
874
+ throw new Error(
875
+ `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.`
876
+ );
877
+ }
878
+ const sab = new SharedArrayBuffer(4);
879
+ const view = new Int32Array(sab);
880
+ Atomics.wait(view, 0, 0, ms);
881
+ }
882
+ function withFileLockSync(lockPath, fn, opts = {}) {
883
+ mkdirSync2(dirname2(lockPath), { recursive: true });
884
+ const staleMs = opts.staleMs ?? 3e4;
885
+ const blockMs = opts.retries === 0 ? 0 : opts.blockMs ?? 3e4;
886
+ const pollIntervalMs = opts.pollIntervalMs ?? 100;
887
+ const now = opts.now ?? Date.now;
888
+ const sleep = opts.sleep ?? busyWaitSync;
889
+ const makeBusyError = opts.errorFactory ?? ((path, pid, retrySeconds, code) => new FileLockBusyError(path, pid, retrySeconds, code));
890
+ let release = null;
891
+ const deadline = now() + blockMs;
892
+ for (; ; ) {
893
+ try {
894
+ release = lockfile.lockSync(lockPath, {
895
+ stale: staleMs,
896
+ retries: 0,
897
+ realpath: false
898
+ });
899
+ try {
900
+ writeFileSync2(`${lockPath}.pid`, String(process.pid), "utf-8");
901
+ } catch {
902
+ }
903
+ break;
904
+ } catch (err) {
905
+ const e = err;
906
+ const code = e.code;
907
+ if (code !== "ELOCKED" && code !== "EBUSY") {
908
+ throw err;
909
+ }
910
+ if (now() >= deadline) {
911
+ const holderPid = readLockHolderPid(lockPath);
912
+ const remainingMs = Math.max(0, deadline - now());
913
+ const retryAfterSeconds = blockMs === 0 ? Math.round(staleMs / 1e3) : Math.round(remainingMs / 1e3);
914
+ throw makeBusyError(lockPath, holderPid, retryAfterSeconds, code);
915
+ }
916
+ sleep(pollIntervalMs);
917
+ }
918
+ }
919
+ try {
920
+ return fn();
921
+ } finally {
922
+ try {
923
+ if (release) release();
924
+ } catch {
925
+ }
926
+ try {
927
+ rmSync2(`${lockPath}.pid`, { force: true });
928
+ } catch {
929
+ }
930
+ }
931
+ }
932
+ var FileLockBusyError;
933
+ var init_fileLock = __esm({
934
+ "src/lib/fileLock.ts"() {
935
+ "use strict";
936
+ FileLockBusyError = class extends Error {
937
+ constructor(lockPath, holderPid, retryAfterSeconds, causeCode) {
938
+ const pidPart = holderPid != null ? `(PID=${holderPid})` : "(PID=unknown)";
939
+ super(`File lock at ${lockPath} held by another process ${pidPart} \u2014 try again in ${retryAfterSeconds}s`);
940
+ this.lockPath = lockPath;
941
+ this.holderPid = holderPid;
942
+ this.retryAfterSeconds = retryAfterSeconds;
943
+ this.causeCode = causeCode;
944
+ this.name = "FileLockBusyError";
945
+ }
946
+ lockPath;
947
+ holderPid;
948
+ retryAfterSeconds;
949
+ causeCode;
950
+ };
951
+ }
952
+ });
953
+
954
+ // src/lib/sqlite-loader.ts
955
+ import { createRequire } from "module";
956
+ import { spawnSync } from "child_process";
957
+ import { accessSync, appendFileSync, chmodSync as chmodSync2, constants as fsConstants, existsSync as existsSync3, mkdirSync as mkdirSync3 } from "fs";
958
+ import { dirname as dirname3, join } from "path";
959
+ function isNativeAbiError(err) {
960
+ const code = err?.code;
961
+ if (code === "ERR_DLOPEN_FAILED") return true;
962
+ const msg = err instanceof Error ? err.message : String(err);
963
+ 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);
964
+ }
965
+ function loadBetterSqlite3(opts = {}) {
966
+ if (_testCtor) return _testCtor;
967
+ if (opts.fresh) bustNativeCache();
968
+ return req("better-sqlite3");
969
+ }
970
+ function bustNativeCache() {
971
+ try {
972
+ const cache = req.cache;
973
+ if (!cache) return;
974
+ for (const key of Object.keys(cache)) {
975
+ if (/better[-_]sqlite3|better_sqlite3\.node|[\\/]bindings[\\/]/.test(key)) {
976
+ delete cache[key];
977
+ }
978
+ }
979
+ } catch {
980
+ }
981
+ }
982
+ function openDatabase(dbPath, opts = {}) {
983
+ const { selfHeal: selfHealOpt = true, ...ctorOpts } = opts;
984
+ const selfHeal = selfHealOpt && process.env.MASSU_HOOK_RUNTIME !== "1";
985
+ const Ctor = loadBetterSqlite3();
986
+ try {
987
+ return new Ctor(dbPath, ctorOpts);
988
+ } catch (err) {
989
+ if (!isNativeAbiError(err)) {
990
+ throw err;
991
+ }
992
+ if (!selfHeal) {
993
+ recordHealEvent({ phase: "skipped", reason: "self-heal-disabled", abiTo: process.versions.modules });
994
+ throw new MemoryEngineUnusableError("abi-mismatch", detailOf(err));
995
+ }
996
+ const result = (_testHeal ?? attemptNativeHeal)(err);
997
+ if (!result.healed && !result.contended) {
998
+ throw new MemoryEngineUnusableError(result.reason ?? "heal-failed", result.detail ?? detailOf(err));
999
+ }
1000
+ try {
1001
+ const FreshCtor = loadBetterSqlite3({ fresh: true });
1002
+ return new FreshCtor(dbPath, ctorOpts);
1003
+ } catch (retryErr) {
1004
+ throw new MemoryEngineUnusableError("heal-failed", detailOf(retryErr));
1005
+ }
1006
+ }
1007
+ }
1008
+ function attemptNativeHeal(err) {
1009
+ const start = Date.now();
1010
+ const abiTo = process.versions.modules;
1011
+ const abiFrom = parseAbiFrom(err);
1012
+ let pkgDir;
1013
+ try {
1014
+ pkgDir = dirname3(req.resolve("better-sqlite3/package.json"));
1015
+ } catch {
1016
+ recordHealEvent({ phase: "skipped", reason: "not-resolvable", abiFrom, abiTo });
1017
+ return { healed: false, reason: "missing", abiFrom, abiTo, detail: "better-sqlite3 not resolvable" };
1018
+ }
1019
+ if (!isWritable(pkgDir)) {
1020
+ recordHealEvent({ phase: "skipped", reason: "dir-not-writable", abiFrom, abiTo });
1021
+ return { healed: false, reason: "heal-failed", abiFrom, abiTo, detail: `install dir not writable: ${pkgDir}` };
1022
+ }
1023
+ recordHealEvent({ phase: "attempt", reason: "abi-mismatch", abiFrom, abiTo });
1024
+ try {
1025
+ return withFileLockSync(
1026
+ join(credentialsDir(), "native-heal.lock"),
1027
+ () => runRebuild(pkgDir, abiFrom, abiTo, start),
1028
+ { blockMs: 6e4, staleMs: 3e5 }
1029
+ );
1030
+ } catch (lockErr) {
1031
+ const detail = lockErr instanceof Error ? lockErr.message : String(lockErr);
1032
+ recordHealEvent({ phase: "failed", reason: "heal-failed", abiFrom, abiTo, detail: `lock: ${detail}` });
1033
+ return { healed: false, contended: true, reason: "heal-failed", abiFrom, abiTo, detail: `heal lock contended: ${detail}` };
1034
+ }
1035
+ }
1036
+ function runRebuild(pkgDir, abiFrom, abiTo, start) {
1037
+ try {
1038
+ const FreshCtor = loadBetterSqlite3({ fresh: true });
1039
+ new FreshCtor(":memory:").close();
1040
+ const res2 = {
1041
+ healed: true,
1042
+ abiFrom,
1043
+ abiTo,
1044
+ durationMs: Date.now() - start,
1045
+ detail: "already healed by a concurrent process"
1046
+ };
1047
+ recordHealEvent({ phase: "success", reason: "already-healed", abiFrom, abiTo, durationMs: res2.durationMs });
1048
+ return res2;
1049
+ } catch (probeErr) {
1050
+ if (!isNativeAbiError(probeErr)) {
1051
+ return { healed: false, reason: "heal-failed", abiFrom, abiTo, durationMs: Date.now() - start, detail: detailOf(probeErr) };
1052
+ }
1053
+ }
1054
+ const prebuildBin = resolveLocalBin(pkgDir, "prebuild-install");
1055
+ if (prebuildBin) {
1056
+ const r = spawnSync(process.execPath, [prebuildBin], {
1057
+ cwd: pkgDir,
1058
+ encoding: "utf-8",
1059
+ timeout: 12e4
1060
+ });
1061
+ if (r.status === 0) {
1062
+ const res2 = { healed: true, method: "prebuild-install", abiFrom, abiTo, durationMs: Date.now() - start };
1063
+ recordHealEvent({ phase: "success", reason: "abi-mismatch", ...res2 });
1064
+ return res2;
1065
+ }
1066
+ }
1067
+ const gypBin = resolveNodeGypBin();
1068
+ if (gypBin && hasCompiler()) {
1069
+ const r = spawnSync(process.execPath, [gypBin, "rebuild", "--release"], {
1070
+ cwd: pkgDir,
1071
+ encoding: "utf-8",
1072
+ timeout: 3e5
1073
+ });
1074
+ if (r.status === 0) {
1075
+ const res2 = { healed: true, method: "node-gyp", abiFrom, abiTo, durationMs: Date.now() - start };
1076
+ recordHealEvent({ phase: "success", reason: "abi-mismatch", ...res2 });
1077
+ return res2;
1078
+ }
1079
+ }
1080
+ const res = {
1081
+ healed: false,
1082
+ reason: "heal-failed",
1083
+ abiFrom,
1084
+ abiTo,
1085
+ durationMs: Date.now() - start,
1086
+ detail: prebuildBin ? "prebuild-install failed; node-gyp fallback unavailable or failed" : "no prebuild-install bin resolvable"
1087
+ };
1088
+ recordHealEvent({ phase: "failed", reason: "heal-failed", abiFrom, abiTo, durationMs: res.durationMs });
1089
+ return res;
1090
+ }
1091
+ function recordHealEvent(event) {
1092
+ try {
1093
+ const rec = {
1094
+ ts: (/* @__PURE__ */ new Date()).toISOString(),
1095
+ node: process.versions.node,
1096
+ platform: process.platform,
1097
+ arch: process.arch,
1098
+ ...event
1099
+ };
1100
+ const dir = credentialsDir();
1101
+ mkdirSync3(dir, { recursive: true });
1102
+ const file = join(dir, "native-heal-events.jsonl");
1103
+ appendFileSync(file, JSON.stringify(rec) + "\n", "utf-8");
1104
+ try {
1105
+ chmodSync2(file, 384);
1106
+ } catch {
1107
+ }
1108
+ } catch {
1109
+ }
1110
+ }
1111
+ function parseAbiFrom(err) {
1112
+ const msg = err instanceof Error ? err.message : String(err ?? "");
1113
+ const m = /NODE_MODULE_VERSION (\d+)/.exec(msg);
1114
+ if (m && /^\d+$/.test(m[1])) return m[1];
1115
+ return void 0;
1116
+ }
1117
+ function detailOf(err) {
1118
+ const msg = err instanceof Error ? err.message : String(err);
1119
+ return msg.slice(0, 300);
1120
+ }
1121
+ function isWritable(dir) {
1122
+ try {
1123
+ accessSync(dir, fsConstants.W_OK);
1124
+ return true;
1125
+ } catch {
1126
+ return false;
1127
+ }
1128
+ }
1129
+ function resolveLocalBin(pkgDir, name) {
1130
+ const candidates = [
1131
+ join(pkgDir, "node_modules", ".bin", name),
1132
+ // nested install
1133
+ join(pkgDir, "..", ".bin", name),
1134
+ // hoisted (node_modules/.bin)
1135
+ join(pkgDir, "..", "..", "node_modules", ".bin", name)
1136
+ // workspace hoist
1137
+ ];
1138
+ for (const c of candidates) {
1139
+ if (existsSync3(c)) return c;
1140
+ }
1141
+ return void 0;
1142
+ }
1143
+ function resolveNodeGypBin() {
1144
+ try {
1145
+ return req.resolve("node-gyp/bin/node-gyp.js");
1146
+ } catch {
1147
+ return void 0;
1148
+ }
1149
+ }
1150
+ function hasCompiler() {
1151
+ for (const cc of ["cc", "clang", "gcc"]) {
1152
+ try {
1153
+ const r = spawnSync(cc, ["--version"], { encoding: "utf-8", timeout: 5e3 });
1154
+ if (r.status === 0) return true;
1155
+ } catch {
1156
+ }
1157
+ }
1158
+ return false;
1159
+ }
1160
+ var req, NATIVE_DB_REMEDY, MemoryEngineUnusableError, _testCtor, _testHeal;
1161
+ var init_sqlite_loader = __esm({
1162
+ "src/lib/sqlite-loader.ts"() {
1163
+ "use strict";
1164
+ init_credentials();
1165
+ init_fileLock();
1166
+ req = createRequire(import.meta.url);
1167
+ NATIVE_DB_REMEDY = "Run 'massu heal' to rebuild the database engine for your Node version, then restart your MCP client / Claude Code.";
1168
+ MemoryEngineUnusableError = class extends Error {
1169
+ reason;
1170
+ remedy;
1171
+ detail;
1172
+ constructor(reason, detail) {
1173
+ super(
1174
+ `Massu memory engine is unusable (${reason}). ${NATIVE_DB_REMEDY}` + (detail ? ` [${detail}]` : "")
1175
+ );
1176
+ this.name = "MemoryEngineUnusableError";
1177
+ this.reason = reason;
1178
+ this.remedy = NATIVE_DB_REMEDY;
1179
+ this.detail = detail;
1180
+ }
1181
+ };
1182
+ _testCtor = null;
1183
+ _testHeal = null;
1184
+ }
1185
+ });
1186
+
1187
+ // src/db-driver.ts
1188
+ import { createRequire as createRequire2 } from "module";
1189
+ import { closeSync, existsSync as existsSync4, mkdtempSync, openSync, readFileSync as readFileSync4, rmSync as rmSync3, fsyncSync } from "fs";
1190
+ import { tmpdir } from "os";
1191
+ import { join as join2 } from "path";
1192
+ function resolveDbEngine() {
1193
+ return process.env[DB_ENGINE_ENV] === "better-sqlite3" ? "better-sqlite3" : DEFAULT_DB_ENGINE;
1194
+ }
1195
+ function nodeSqliteCtor() {
1196
+ if (!_nodeCtor) _nodeCtor = req2("node:sqlite").DatabaseSync;
1197
+ return _nodeCtor;
1198
+ }
1199
+ function plainRow(row) {
1200
+ if (row == null || typeof row !== "object") return row;
1201
+ return { ...row };
1202
+ }
1203
+ function openNodeSqlite(dbPath, opts) {
1204
+ const Ctor = nodeSqliteCtor();
1205
+ const raw = new Ctor(dbPath, {
1206
+ open: true,
1207
+ readOnly: !!opts.readonly,
1208
+ // FAITHFULNESS: better-sqlite3's ACTUAL default is foreign_keys=ON (empirically
1209
+ // verified — SQLite's own default is OFF, but bs3 enables it). massu was written +
1210
+ // tested against that default, so the adapter must match it, or a store that relies
1211
+ // on FK-ON-by-default (without an explicit pragma) would silently lose enforcement.
1212
+ // Explicit `foreign_keys=OFF/ON` pragmas (e.g. memory-db bulk ops) still override.
1213
+ enableForeignKeyConstraints: true,
1214
+ allowExtension: false
1215
+ });
1216
+ let savepointSeq = 0;
1217
+ const wrapStmt = (sql) => {
1218
+ const st = raw.prepare(sql);
1219
+ return {
1220
+ run: (...p) => {
1221
+ const r = st.run(...p);
1222
+ return {
1223
+ changes: typeof r.changes === "bigint" ? Number(r.changes) : r.changes,
1224
+ lastInsertRowid: r.lastInsertRowid
1225
+ };
1226
+ },
1227
+ get: (...p) => plainRow(st.get(...p)),
1228
+ all: (...p) => st.all(...p).map(plainRow)
1229
+ };
1230
+ };
1231
+ const pragma = (source, options) => {
1232
+ const s = String(source).trim();
1233
+ if (s.includes("=")) {
1234
+ raw.exec(`PRAGMA ${s}`);
1235
+ return void 0;
1236
+ }
1237
+ const rows = raw.prepare(`PRAGMA ${s}`).all().map(plainRow);
1238
+ if (options?.simple) {
1239
+ const first = rows[0];
1240
+ return first ? Object.values(first)[0] : void 0;
1241
+ }
1242
+ return rows;
1243
+ };
1244
+ const transaction = (fn) => {
1245
+ const run = (...args) => {
1246
+ const nested = raw.isTransaction;
1247
+ const name = `msp_${savepointSeq++}`;
1248
+ if (nested) raw.exec(`SAVEPOINT ${name}`);
1249
+ else raw.exec("BEGIN");
1250
+ try {
1251
+ const out = fn(...args);
1252
+ if (nested) raw.exec(`RELEASE ${name}`);
1253
+ else raw.exec("COMMIT");
1254
+ return out;
1255
+ } catch (e) {
1256
+ if (nested) {
1257
+ raw.exec(`ROLLBACK TO ${name}`);
1258
+ raw.exec(`RELEASE ${name}`);
1259
+ } else {
1260
+ raw.exec("ROLLBACK");
1261
+ }
1262
+ throw e;
1263
+ }
1264
+ };
1265
+ return run;
1266
+ };
1267
+ const serialize = () => {
1268
+ if (raw.isTransaction) {
1269
+ throw new Error(
1270
+ "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."
1271
+ );
1272
+ }
1273
+ const dir = mkdtempSync(join2(tmpdir(), "massu-serialize-"));
1274
+ const out = join2(dir, "snapshot.db");
1275
+ try {
1276
+ raw.exec(`VACUUM INTO '${out.replace(/'/g, "''")}'`);
1277
+ const fd = openSync(out, "r");
1278
+ try {
1279
+ fsyncSync(fd);
1280
+ } finally {
1281
+ closeSync(fd);
1282
+ }
1283
+ return readFileSync4(out);
1284
+ } finally {
1285
+ rmSync3(dir, { recursive: true, force: true });
1286
+ }
1287
+ };
1288
+ const handle = {
1289
+ prepare: (sql) => wrapStmt(sql),
1290
+ exec: (sql) => raw.exec(sql),
1291
+ pragma,
1292
+ transaction,
1293
+ serialize,
1294
+ close: () => raw.close()
1295
+ };
1296
+ return handle;
1297
+ }
1298
+ function openBetterSqlite3(dbPath, opts) {
1299
+ return openDatabase(dbPath, opts);
1300
+ }
1301
+ function openDatabase2(dbPath, opts = {}) {
1302
+ return resolveDbEngine() === "better-sqlite3" ? openBetterSqlite3(dbPath, opts) : openNodeSqlite(dbPath, opts);
1303
+ }
1304
+ var req2, DEFAULT_DB_ENGINE, DB_ENGINE_ENV, _nodeCtor;
1305
+ var init_db_driver = __esm({
1306
+ "src/db-driver.ts"() {
1307
+ "use strict";
1308
+ init_sqlite_loader();
1309
+ init_sqlite_loader();
1310
+ req2 = createRequire2(import.meta.url);
1311
+ DEFAULT_DB_ENGINE = "node-sqlite";
1312
+ DB_ENGINE_ENV = "MASSU_DB_ENGINE";
1313
+ _nodeCtor = null;
1314
+ }
1315
+ });
1316
+
816
1317
  // src/memory-vector.ts
817
1318
  function float32ToBlob(vec) {
818
1319
  return Buffer.from(new Uint8Array(vec.buffer, vec.byteOffset, vec.byteLength));
@@ -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,9 +1558,9 @@ var init_rule_candidate_store = __esm({
1057
1558
  });
1058
1559
 
1059
1560
  // src/memory-embedder-tokenizer.ts
1060
- import { readFileSync as readFileSync3 } from "fs";
1561
+ import { readFileSync as readFileSync5 } from "fs";
1061
1562
  function loadVocab(vocabPath) {
1062
- const lines = readFileSync3(vocabPath, "utf-8").split("\n");
1563
+ const lines = readFileSync5(vocabPath, "utf-8").split("\n");
1063
1564
  const vocab = /* @__PURE__ */ new Map();
1064
1565
  for (let i = 0; i < lines.length; i++) {
1065
1566
  const tok = lines[i].replace(/\r$/, "");
@@ -1179,9 +1680,9 @@ var init_memory_embedder_tokenizer = __esm({
1179
1680
 
1180
1681
  // src/memory-embedder.ts
1181
1682
  import { fileURLToPath } from "url";
1182
- import { dirname as dirname3, join as join2 } from "path";
1183
- import { existsSync as existsSync4 } from "fs";
1184
- import { createRequire } from "module";
1683
+ import { dirname as dirname5, join as join4 } from "path";
1684
+ import { existsSync as existsSync6 } from "fs";
1685
+ import { createRequire as createRequire3 } from "module";
1185
1686
  function getActiveEmbedModel() {
1186
1687
  return _activeModel;
1187
1688
  }
@@ -1202,21 +1703,21 @@ function embeddingsDisabled() {
1202
1703
  }
1203
1704
  function resolveModelDir() {
1204
1705
  try {
1205
- let dir = dirname3(fileURLToPath(import.meta.url));
1706
+ let dir = dirname5(fileURLToPath(import.meta.url));
1206
1707
  let root = null;
1207
1708
  for (let i = 0; i < 8; i++) {
1208
- if (existsSync4(join2(dir, "package.json"))) {
1709
+ if (existsSync6(join4(dir, "package.json"))) {
1209
1710
  root = dir;
1210
1711
  break;
1211
1712
  }
1212
- const parent = dirname3(dir);
1713
+ const parent = dirname5(dir);
1213
1714
  if (parent === dir) break;
1214
1715
  dir = parent;
1215
1716
  }
1216
1717
  if (!root) return null;
1217
1718
  for (const rel of ["dist/embedder", "assets/embedder"]) {
1218
- const candidate = join2(root, rel);
1219
- if (existsSync4(join2(candidate, "model_quantized.onnx"))) return candidate;
1719
+ const candidate = join4(root, rel);
1720
+ if (existsSync6(join4(candidate, "model_quantized.onnx"))) return candidate;
1220
1721
  }
1221
1722
  return null;
1222
1723
  } catch {
@@ -1225,9 +1726,9 @@ function resolveModelDir() {
1225
1726
  }
1226
1727
  function resolveWasmDir() {
1227
1728
  try {
1228
- const req = createRequire(import.meta.url);
1229
- const main2 = req.resolve(ORT_PKG);
1230
- return dirname3(main2);
1729
+ const req3 = createRequire3(import.meta.url);
1730
+ const main2 = req3.resolve(ORT_PKG);
1731
+ return dirname5(main2);
1231
1732
  } catch {
1232
1733
  return null;
1233
1734
  }
@@ -1247,8 +1748,8 @@ async function getSession() {
1247
1748
  ort.env.wasm.proxy = false;
1248
1749
  ort.env.wasm.wasmPaths = wasmDir.endsWith("/") ? wasmDir : wasmDir + "/";
1249
1750
  _ortTensor = ort.Tensor;
1250
- const modelPath = join2(modelDir, "model_quantized.onnx");
1251
- const vocabPath = join2(modelDir, "vocab.txt");
1751
+ const modelPath = join4(modelDir, "model_quantized.onnx");
1752
+ const vocabPath = join4(modelDir, "vocab.txt");
1252
1753
  _vocab = loadVocab(vocabPath);
1253
1754
  const session = await ort.InferenceSession.create(modelPath, {
1254
1755
  executionProviders: ["wasm"],
@@ -1569,6 +2070,7 @@ __export(memory_db_exports, {
1569
2070
  migrateAuditLogCheckExtension: () => migrateAuditLogCheckExtension,
1570
2071
  migrateMemoryFilesFor4B: () => migrateMemoryFilesFor4B,
1571
2072
  migrateObservationEmbeddingChunks: () => migrateObservationEmbeddingChunks,
2073
+ migrateSharedMemoryFor5B: () => migrateSharedMemoryFor5B,
1572
2074
  pruneOldConversationTurns: () => pruneOldConversationTurns,
1573
2075
  pruneOldObservations: () => pruneOldObservations,
1574
2076
  pruneToolCostEvents: () => pruneToolCostEvents,
@@ -1585,9 +2087,8 @@ __export(memory_db_exports, {
1585
2087
  upsertObservationEmbedding: () => upsertObservationEmbedding,
1586
2088
  usageWarmupElapsed: () => usageWarmupElapsed
1587
2089
  });
1588
- import Database2 from "better-sqlite3";
1589
- import { dirname as dirname4, basename as basename2 } from "path";
1590
- import { existsSync as existsSync5, mkdirSync as mkdirSync3 } from "fs";
2090
+ import { dirname as dirname6, basename as basename2 } from "path";
2091
+ import { existsSync as existsSync7, mkdirSync as mkdirSync5 } from "fs";
1591
2092
  function sanitizeFts5Query(raw) {
1592
2093
  const trimmed = raw.trim();
1593
2094
  if (!trimmed) return '""';
@@ -1603,12 +2104,12 @@ function sanitizeFts5QueryOr(raw) {
1603
2104
  }
1604
2105
  function getMemoryDb() {
1605
2106
  const dbPath = getResolvedPaths().memoryDbPath;
1606
- const dir = dirname4(dbPath);
1607
- if (!existsSync5(dir)) {
1608
- mkdirSync3(dir, { recursive: true });
2107
+ const dir = dirname6(dbPath);
2108
+ if (!existsSync7(dir)) {
2109
+ mkdirSync5(dir, { recursive: true });
1609
2110
  }
1610
- const preExisting = existsSync5(dbPath);
1611
- const db = new Database2(dbPath);
2111
+ const preExisting = existsSync7(dbPath);
2112
+ const db = openDatabase2(dbPath);
1612
2113
  db.pragma("journal_mode = WAL");
1613
2114
  db.pragma("foreign_keys = ON");
1614
2115
  const onDisk = db.pragma("user_version", { simple: true });
@@ -1684,6 +2185,39 @@ function migrateMemoryFilesFor4B(db) {
1684
2185
  db.exec(`ALTER TABLE ${add.table} ADD COLUMN ${add.name} ${add.decl}`);
1685
2186
  }
1686
2187
  }
2188
+ function migrateSharedMemoryFor5B(db) {
2189
+ const cols = db.prepare(`PRAGMA table_info(observations)`).all();
2190
+ if (cols.length > 0 && !cols.some((c) => c.name === "shareable")) {
2191
+ db.exec(`ALTER TABLE observations ADD COLUMN shareable INTEGER NOT NULL DEFAULT 0`);
2192
+ }
2193
+ db.exec(`
2194
+ CREATE TABLE IF NOT EXISTS shared_memory_pending (
2195
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
2196
+ record_hash TEXT NOT NULL UNIQUE, -- identity + idempotency key (hex sha256)
2197
+ origin_repo_id TEXT NOT NULL, -- the signed origin repo_id (v4 UUID)
2198
+ origin_repo_label TEXT NOT NULL, -- re-slugged on read; NEVER trusted from the wire
2199
+ envelope_raw TEXT NOT NULL, -- the VERBATIM signed bytes; B-05 re-verifies these
2200
+ record_json TEXT NOT NULL, -- the single record's canonical JSON
2201
+ received_at_epoch INTEGER NOT NULL, -- epoch SECONDS (Slice-2 convention)
2202
+ accepted_at_epoch INTEGER, -- set on accept (B-05); NULL while pending
2203
+ refused_at_epoch INTEGER, -- set on refuse (B-06)
2204
+ expired_at_epoch INTEGER -- revocation \u21D2 EXPIRE, never DELETE (B-07 / S-3)
2205
+ );
2206
+ CREATE INDEX IF NOT EXISTS idx_smp_origin ON shared_memory_pending(origin_repo_id);
2207
+ CREATE INDEX IF NOT EXISTS idx_smp_received ON shared_memory_pending(received_at_epoch DESC);
2208
+ `);
2209
+ db.exec(`
2210
+ CREATE TABLE IF NOT EXISTS shared_memory_outbound (
2211
+ record_hash TEXT PRIMARY KEY, -- the exported record's canonical hash
2212
+ observation_id INTEGER NOT NULL, -- the source observations.id (for B-07 revocation)
2213
+ origin_repo_id TEXT NOT NULL, -- this repo's own repo_id at export time
2214
+ seq INTEGER NOT NULL, -- the envelope seq this record first went out in
2215
+ exported_at_epoch INTEGER NOT NULL, -- epoch SECONDS
2216
+ revoked_at_epoch INTEGER -- set when the source row is superseded/expired (B-07)
2217
+ );
2218
+ CREATE INDEX IF NOT EXISTS idx_smo_obs ON shared_memory_outbound(observation_id);
2219
+ `);
2220
+ }
1687
2221
  function migrateAuditLogCheckExtension(db) {
1688
2222
  const row = db.prepare("SELECT sql FROM sqlite_master WHERE type='table' AND name='audit_log'").get();
1689
2223
  if (!row) return;
@@ -1706,7 +2240,16 @@ function migrateAuditLogCheckExtension(db) {
1706
2240
  "memory_file_adopted_human",
1707
2241
  "memory_file_rendered",
1708
2242
  "memory_file_render_refused",
1709
- "memory_file_tombstoned"
2243
+ "memory_file_tombstoned",
2244
+ // B-10 (Slice 5) — the seven cross-repo shared-memory events. An existing DB whose
2245
+ // CHECK lacks any of these triggers the table rebuild below.
2246
+ "shared_memory_exported",
2247
+ "shared_memory_export_refused",
2248
+ "shared_memory_imported",
2249
+ "shared_memory_dropped",
2250
+ "shared_memory_accepted",
2251
+ "shared_memory_refused",
2252
+ "shared_memory_revoked"
1710
2253
  ];
1711
2254
  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);
1712
2255
  if (checkClauseMatch) {
@@ -1725,7 +2268,12 @@ function migrateAuditLogCheckExtension(db) {
1725
2268
  'code_change', 'rule_enforced', 'approval', 'review', 'commit', 'compaction',
1726
2269
  'rule_candidate_emitted', 'rule_promoted', 'rule_dismissed',
1727
2270
  'memory_file_ingested', 'memory_file_expired', 'memory_file_adopted_human',
1728
- 'memory_file_rendered', 'memory_file_render_refused', 'memory_file_tombstoned'
2271
+ 'memory_file_rendered', 'memory_file_render_refused', 'memory_file_tombstoned',
2272
+ -- B-10 (Slice 5) \u2014 cross-repo shared-memory events (must mirror the CREATE +
2273
+ -- the expected[] list above, or the migration would loop rebuilding forever).
2274
+ 'shared_memory_exported', 'shared_memory_export_refused', 'shared_memory_imported',
2275
+ 'shared_memory_dropped', 'shared_memory_accepted', 'shared_memory_refused',
2276
+ 'shared_memory_revoked'
1729
2277
  )),
1730
2278
  actor TEXT NOT NULL DEFAULT 'ai' CHECK(actor IN ('ai', 'human', 'hook', 'agent')),
1731
2279
  model_id TEXT,
@@ -1802,6 +2350,12 @@ function initMemorySchema(db) {
1802
2350
  -- credential, or takes a snapshot. A Slice-5 synced memory arrives here with
1803
2351
  -- origin='team' and must never reach disk without CR-55's gates.
1804
2352
  origin TEXT NOT NULL DEFAULT 'local',
2353
+ -- B-01 (Slice 5, cross-repo surfacing): the per-decision SHARE opt-in. Set to 1
2354
+ -- ONLY by an explicit human act (massu memory share <id>); NEVER by a heuristic,
2355
+ -- score, LLM, hook, or consolidation pass. Export (B-02) reads WHERE shareable=1
2356
+ -- AND origin='local'. Defaults 0 so every pre-existing and machine-written row is
2357
+ -- un-shareable until a human says otherwise (fail-closed).
2358
+ shareable INTEGER NOT NULL DEFAULT 0,
1805
2359
  created_at TEXT NOT NULL,
1806
2360
  created_at_epoch INTEGER NOT NULL,
1807
2361
  FOREIGN KEY(session_id) REFERENCES sessions(session_id) ON DELETE CASCADE
@@ -2075,7 +2629,13 @@ function initMemorySchema(db) {
2075
2629
  'code_change', 'rule_enforced', 'approval', 'review', 'commit', 'compaction',
2076
2630
  'rule_candidate_emitted', 'rule_promoted', 'rule_dismissed',
2077
2631
  'memory_file_ingested', 'memory_file_expired', 'memory_file_adopted_human',
2078
- 'memory_file_rendered', 'memory_file_render_refused', 'memory_file_tombstoned'
2632
+ 'memory_file_rendered', 'memory_file_render_refused', 'memory_file_tombstoned',
2633
+ -- B-10 (Slice 5): cross-repo shared-memory observability. Without these seven,
2634
+ -- nobody could ever answer "what has crossed between my repos?" (and an ingest
2635
+ -- catch would swallow the CHECK violation, silently producing NOTHING).
2636
+ 'shared_memory_exported', 'shared_memory_export_refused', 'shared_memory_imported',
2637
+ 'shared_memory_dropped', 'shared_memory_accepted', 'shared_memory_refused',
2638
+ 'shared_memory_revoked'
2079
2639
  )),
2080
2640
  actor TEXT NOT NULL DEFAULT 'ai' CHECK(actor IN ('ai', 'human', 'hook', 'agent')),
2081
2641
  model_id TEXT,
@@ -2444,6 +3004,7 @@ function initMemorySchema(db) {
2444
3004
  `);
2445
3005
  migrateObservationEmbeddingChunks(db);
2446
3006
  migrateMemoryFilesFor4B(db);
3007
+ migrateSharedMemoryFor5B(db);
2447
3008
  ensureRuleCandidatesTable(db);
2448
3009
  migrateRuleDelivery(db);
2449
3010
  }
@@ -3314,6 +3875,7 @@ var MEMORY_SCHEMA_VERSION, TOOL_COST_EVENTS_RETENTION_DAYS, MAX_DRAFT_TEXT_LEN,
3314
3875
  var init_memory_db = __esm({
3315
3876
  "src/memory-db.ts"() {
3316
3877
  "use strict";
3878
+ init_db_driver();
3317
3879
  init_config();
3318
3880
  init_memory_vector();
3319
3881
  init_db_backup();
@@ -3334,21 +3896,21 @@ var init_memory_db = __esm({
3334
3896
  });
3335
3897
 
3336
3898
  // src/hooks/lib/hook-failure-signal.ts
3337
- import { appendFileSync, mkdirSync as mkdirSync4, existsSync as existsSync6 } from "fs";
3338
- import { join as join3, dirname as dirname5 } from "path";
3899
+ import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync6, existsSync as existsSync8 } from "fs";
3900
+ import { join as join5, dirname as dirname7 } from "path";
3339
3901
  function resolveFailureLogPath() {
3340
3902
  const explicit = process.env.MASSU_HOOK_FAILURE_LOG;
3341
3903
  if (explicit) return explicit;
3342
3904
  let dir = process.cwd();
3343
3905
  for (let i = 0; i < 12; i++) {
3344
- if (existsSync6(join3(dir, ".massu")) || existsSync6(join3(dir, "massu.config.yaml"))) {
3345
- return join3(dir, ".massu", "hook-failures.jsonl");
3906
+ if (existsSync8(join5(dir, ".massu")) || existsSync8(join5(dir, "massu.config.yaml"))) {
3907
+ return join5(dir, ".massu", "hook-failures.jsonl");
3346
3908
  }
3347
- const parent = dirname5(dir);
3909
+ const parent = dirname7(dir);
3348
3910
  if (parent === dir) break;
3349
3911
  dir = parent;
3350
3912
  }
3351
- return join3(process.cwd(), ".massu", "hook-failures.jsonl");
3913
+ return join5(process.cwd(), ".massu", "hook-failures.jsonl");
3352
3914
  }
3353
3915
  function recordHookFailure(hook, error, context) {
3354
3916
  const record = {
@@ -3361,8 +3923,8 @@ function recordHookFailure(hook, error, context) {
3361
3923
  let wroteSomething = false;
3362
3924
  try {
3363
3925
  const path = resolveFailureLogPath();
3364
- mkdirSync4(dirname5(path), { recursive: true });
3365
- appendFileSync(path, JSON.stringify(record) + "\n", "utf-8");
3926
+ mkdirSync6(dirname7(path), { recursive: true });
3927
+ appendFileSync2(path, JSON.stringify(record) + "\n", "utf-8");
3366
3928
  wroteSomething = true;
3367
3929
  } catch {
3368
3930
  }
@@ -3407,9 +3969,9 @@ var init_hook_failure_signal = __esm({
3407
3969
  // src/hooks/fix-detector.ts
3408
3970
  init_config();
3409
3971
  import { execFileSync } from "child_process";
3410
- import { existsSync as existsSync7, appendFileSync as appendFileSync2, mkdirSync as mkdirSync5, readFileSync as readFileSync4 } from "fs";
3411
- import { tmpdir } from "os";
3412
- import { join as join4 } from "path";
3972
+ import { existsSync as existsSync9, appendFileSync as appendFileSync3, mkdirSync as mkdirSync7, readFileSync as readFileSync6 } from "fs";
3973
+ import { tmpdir as tmpdir2 } from "os";
3974
+ import { join as join6 } from "path";
3413
3975
 
3414
3976
  // src/hooks/lib/write-hook-message.ts
3415
3977
  function writeHookMessage(message) {
@@ -3460,16 +4022,16 @@ var FIX_HEURISTICS = [
3460
4022
  }
3461
4023
  ];
3462
4024
  function getSessionFlagPath(sessionId) {
3463
- const dir = join4(tmpdir(), "massu-auto-learning");
3464
- if (!existsSync7(dir)) {
3465
- mkdirSync5(dir, { recursive: true });
4025
+ const dir = join6(tmpdir2(), "massu-auto-learning");
4026
+ if (!existsSync9(dir)) {
4027
+ mkdirSync7(dir, { recursive: true });
3466
4028
  }
3467
- return join4(dir, `fixes-${sessionId.slice(0, 12)}.jsonl`);
4029
+ return join6(dir, `fixes-${sessionId.slice(0, 12)}.jsonl`);
3468
4030
  }
3469
4031
  function _stateDir() {
3470
- const dir = join4(tmpdir(), "massu-fix-detector-state");
3471
- if (!existsSync7(dir)) {
3472
- mkdirSync5(dir, { recursive: true });
4032
+ const dir = join6(tmpdir2(), "massu-fix-detector-state");
4033
+ if (!existsSync9(dir)) {
4034
+ mkdirSync7(dir, { recursive: true });
3473
4035
  }
3474
4036
  return dir;
3475
4037
  }
@@ -3482,9 +4044,9 @@ function _cwdHash(cwd) {
3482
4044
  }
3483
4045
  function isGitWorkTreeFast(cwd) {
3484
4046
  try {
3485
- const cachePath = join4(_stateDir(), `worktree-${_cwdHash(cwd)}.json`);
3486
- if (existsSync7(cachePath)) {
3487
- const cached = JSON.parse(readFileSync4(cachePath, "utf-8"));
4047
+ const cachePath = join6(_stateDir(), `worktree-${_cwdHash(cwd)}.json`);
4048
+ if (existsSync9(cachePath)) {
4049
+ const cached = JSON.parse(readFileSync6(cachePath, "utf-8"));
3488
4050
  const ageMs = Date.now() - cached.ts;
3489
4051
  if (ageMs < 36e5) return cached.isWorkTree;
3490
4052
  }
@@ -3496,7 +4058,7 @@ function isGitWorkTreeFast(cwd) {
3496
4058
  isWorkTree = false;
3497
4059
  }
3498
4060
  try {
3499
- appendFileSync2(cachePath, "");
4061
+ appendFileSync3(cachePath, "");
3500
4062
  const fs = __require("fs");
3501
4063
  fs.writeFileSync(cachePath, JSON.stringify({ isWorkTree, ts: Date.now() }));
3502
4064
  } catch {
@@ -3507,11 +4069,11 @@ function isGitWorkTreeFast(cwd) {
3507
4069
  }
3508
4070
  }
3509
4071
  function _disabledFlagPath(sessionId) {
3510
- return join4(_stateDir(), `disabled-${sessionId.slice(0, 12)}.flag`);
4072
+ return join6(_stateDir(), `disabled-${sessionId.slice(0, 12)}.flag`);
3511
4073
  }
3512
4074
  function isSessionAutoDisabled(sessionId) {
3513
4075
  try {
3514
- return existsSync7(_disabledFlagPath(sessionId));
4076
+ return existsSync9(_disabledFlagPath(sessionId));
3515
4077
  } catch {
3516
4078
  return false;
3517
4079
  }
@@ -3529,7 +4091,7 @@ async function main() {
3529
4091
  const input = await readStdin();
3530
4092
  const hookInput = JSON.parse(input);
3531
4093
  const filePath = hookInput.tool_input?.file_path;
3532
- if (!filePath || !existsSync7(filePath)) {
4094
+ if (!filePath || !existsSync9(filePath)) {
3533
4095
  process.exit(0);
3534
4096
  return;
3535
4097
  }
@@ -3589,8 +4151,8 @@ async function main() {
3589
4151
  timestamp: (/* @__PURE__ */ new Date()).toISOString()
3590
4152
  };
3591
4153
  const flagPath = getSessionFlagPath(hookInput.session_id);
3592
- appendFileSync2(flagPath, JSON.stringify(signal) + "\n");
3593
- const lines = readFileSync4(flagPath, "utf-8").split("\n").filter(Boolean);
4154
+ appendFileSync3(flagPath, JSON.stringify(signal) + "\n");
4155
+ const lines = readFileSync6(flagPath, "utf-8").split("\n").filter(Boolean);
3594
4156
  if (lines.length === 1) {
3595
4157
  writeHookMessage(
3596
4158
  `[Massu Auto-Learning] Bug fix detected in ${filePath} (signals: ${detected.join(", ")}). The auto-learning pipeline will prompt you at session end to create an incident report, derive a prevention rule, and add enforcement.`