@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.
- package/CHANGELOG.md +158 -0
- package/dist/cli.js +2266 -863
- package/dist/hooks/auto-learning-pipeline.js +622 -60
- package/dist/hooks/classify-failure.js +622 -60
- package/dist/hooks/cost-tracker.js +631 -69
- package/dist/hooks/fix-detector.js +631 -69
- package/dist/hooks/incident-pipeline.js +616 -54
- package/dist/hooks/intent-suggester.js +631 -69
- package/dist/hooks/memory-recall.js +771 -75
- package/dist/hooks/post-edit-context.js +633 -71
- package/dist/hooks/post-tool-use.js +659 -197
- package/dist/hooks/pre-compact.js +631 -69
- package/dist/hooks/pre-delete-check.js +635 -73
- package/dist/hooks/pre-tool-use-gate.js +635 -73
- package/dist/hooks/quality-event.js +631 -69
- package/dist/hooks/rule-enforcement-pipeline.js +617 -55
- package/dist/hooks/security-gate.js +631 -69
- package/dist/hooks/session-end.js +1729 -281
- package/dist/hooks/session-start.js +1018 -425
- package/dist/hooks/user-prompt.js +666 -104
- package/package.json +2 -2
|
@@ -26,16 +26,6 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
26
26
|
};
|
|
27
27
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
28
28
|
|
|
29
|
-
// src/lib/memory-path.ts
|
|
30
|
-
function encodeMemoryDirName(projectRoot) {
|
|
31
|
-
return projectRoot.replace(/\//g, "-");
|
|
32
|
-
}
|
|
33
|
-
var init_memory_path = __esm({
|
|
34
|
-
"src/lib/memory-path.ts"() {
|
|
35
|
-
"use strict";
|
|
36
|
-
}
|
|
37
|
-
});
|
|
38
|
-
|
|
39
29
|
// src/credentials.ts
|
|
40
30
|
import { homedir } from "os";
|
|
41
31
|
import { resolve } from "path";
|
|
@@ -104,6 +94,475 @@ var init_credentials = __esm({
|
|
|
104
94
|
}
|
|
105
95
|
});
|
|
106
96
|
|
|
97
|
+
// src/lib/fileLock.ts
|
|
98
|
+
import { mkdirSync as mkdirSync2, readFileSync as readFileSync2, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
99
|
+
import { dirname } from "path";
|
|
100
|
+
import * as lockfile from "proper-lockfile";
|
|
101
|
+
function readLockHolderPid(lockPath) {
|
|
102
|
+
try {
|
|
103
|
+
const raw = readFileSync2(`${lockPath}.pid`, "utf-8").trim();
|
|
104
|
+
const pid = Number.parseInt(raw, 10);
|
|
105
|
+
if (!Number.isFinite(pid) || pid <= 0) return null;
|
|
106
|
+
return pid;
|
|
107
|
+
} catch {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
function busyWaitSync(ms) {
|
|
112
|
+
if (typeof SharedArrayBuffer === "undefined" || typeof Atomics === "undefined") {
|
|
113
|
+
throw new Error(
|
|
114
|
+
`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.`
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
const sab = new SharedArrayBuffer(4);
|
|
118
|
+
const view = new Int32Array(sab);
|
|
119
|
+
Atomics.wait(view, 0, 0, ms);
|
|
120
|
+
}
|
|
121
|
+
function withFileLockSync(lockPath, fn, opts = {}) {
|
|
122
|
+
mkdirSync2(dirname(lockPath), { recursive: true });
|
|
123
|
+
const staleMs = opts.staleMs ?? 3e4;
|
|
124
|
+
const blockMs = opts.retries === 0 ? 0 : opts.blockMs ?? 3e4;
|
|
125
|
+
const pollIntervalMs = opts.pollIntervalMs ?? 100;
|
|
126
|
+
const now = opts.now ?? Date.now;
|
|
127
|
+
const sleep = opts.sleep ?? busyWaitSync;
|
|
128
|
+
const makeBusyError = opts.errorFactory ?? ((path, pid, retrySeconds, code) => new FileLockBusyError(path, pid, retrySeconds, code));
|
|
129
|
+
let release = null;
|
|
130
|
+
const deadline = now() + blockMs;
|
|
131
|
+
for (; ; ) {
|
|
132
|
+
try {
|
|
133
|
+
release = lockfile.lockSync(lockPath, {
|
|
134
|
+
stale: staleMs,
|
|
135
|
+
retries: 0,
|
|
136
|
+
realpath: false
|
|
137
|
+
});
|
|
138
|
+
try {
|
|
139
|
+
writeFileSync2(`${lockPath}.pid`, String(process.pid), "utf-8");
|
|
140
|
+
} catch {
|
|
141
|
+
}
|
|
142
|
+
break;
|
|
143
|
+
} catch (err) {
|
|
144
|
+
const e = err;
|
|
145
|
+
const code = e.code;
|
|
146
|
+
if (code !== "ELOCKED" && code !== "EBUSY") {
|
|
147
|
+
throw err;
|
|
148
|
+
}
|
|
149
|
+
if (now() >= deadline) {
|
|
150
|
+
const holderPid = readLockHolderPid(lockPath);
|
|
151
|
+
const remainingMs = Math.max(0, deadline - now());
|
|
152
|
+
const retryAfterSeconds = blockMs === 0 ? Math.round(staleMs / 1e3) : Math.round(remainingMs / 1e3);
|
|
153
|
+
throw makeBusyError(lockPath, holderPid, retryAfterSeconds, code);
|
|
154
|
+
}
|
|
155
|
+
sleep(pollIntervalMs);
|
|
156
|
+
}
|
|
157
|
+
}
|
|
158
|
+
try {
|
|
159
|
+
return fn();
|
|
160
|
+
} finally {
|
|
161
|
+
try {
|
|
162
|
+
if (release) release();
|
|
163
|
+
} catch {
|
|
164
|
+
}
|
|
165
|
+
try {
|
|
166
|
+
rmSync2(`${lockPath}.pid`, { force: true });
|
|
167
|
+
} catch {
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
var FileLockBusyError;
|
|
172
|
+
var init_fileLock = __esm({
|
|
173
|
+
"src/lib/fileLock.ts"() {
|
|
174
|
+
"use strict";
|
|
175
|
+
FileLockBusyError = class extends Error {
|
|
176
|
+
constructor(lockPath, holderPid, retryAfterSeconds, causeCode) {
|
|
177
|
+
const pidPart = holderPid != null ? `(PID=${holderPid})` : "(PID=unknown)";
|
|
178
|
+
super(`File lock at ${lockPath} held by another process ${pidPart} \u2014 try again in ${retryAfterSeconds}s`);
|
|
179
|
+
this.lockPath = lockPath;
|
|
180
|
+
this.holderPid = holderPid;
|
|
181
|
+
this.retryAfterSeconds = retryAfterSeconds;
|
|
182
|
+
this.causeCode = causeCode;
|
|
183
|
+
this.name = "FileLockBusyError";
|
|
184
|
+
}
|
|
185
|
+
lockPath;
|
|
186
|
+
holderPid;
|
|
187
|
+
retryAfterSeconds;
|
|
188
|
+
causeCode;
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
// src/lib/sqlite-loader.ts
|
|
194
|
+
import { createRequire } from "module";
|
|
195
|
+
import { spawnSync } from "child_process";
|
|
196
|
+
import { accessSync, appendFileSync, chmodSync as chmodSync2, constants as fsConstants, existsSync as existsSync2, mkdirSync as mkdirSync3 } from "fs";
|
|
197
|
+
import { dirname as dirname2, join } from "path";
|
|
198
|
+
function isNativeAbiError(err) {
|
|
199
|
+
const code = err?.code;
|
|
200
|
+
if (code === "ERR_DLOPEN_FAILED") return true;
|
|
201
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
202
|
+
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);
|
|
203
|
+
}
|
|
204
|
+
function loadBetterSqlite3(opts = {}) {
|
|
205
|
+
if (_testCtor) return _testCtor;
|
|
206
|
+
if (opts.fresh) bustNativeCache();
|
|
207
|
+
return req("better-sqlite3");
|
|
208
|
+
}
|
|
209
|
+
function bustNativeCache() {
|
|
210
|
+
try {
|
|
211
|
+
const cache = req.cache;
|
|
212
|
+
if (!cache) return;
|
|
213
|
+
for (const key of Object.keys(cache)) {
|
|
214
|
+
if (/better[-_]sqlite3|better_sqlite3\.node|[\\/]bindings[\\/]/.test(key)) {
|
|
215
|
+
delete cache[key];
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
} catch {
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
function openDatabase(dbPath, opts = {}) {
|
|
222
|
+
const { selfHeal: selfHealOpt = true, ...ctorOpts } = opts;
|
|
223
|
+
const selfHeal = selfHealOpt && process.env.MASSU_HOOK_RUNTIME !== "1";
|
|
224
|
+
const Ctor = loadBetterSqlite3();
|
|
225
|
+
try {
|
|
226
|
+
return new Ctor(dbPath, ctorOpts);
|
|
227
|
+
} catch (err) {
|
|
228
|
+
if (!isNativeAbiError(err)) {
|
|
229
|
+
throw err;
|
|
230
|
+
}
|
|
231
|
+
if (!selfHeal) {
|
|
232
|
+
recordHealEvent({ phase: "skipped", reason: "self-heal-disabled", abiTo: process.versions.modules });
|
|
233
|
+
throw new MemoryEngineUnusableError("abi-mismatch", detailOf(err));
|
|
234
|
+
}
|
|
235
|
+
const result = (_testHeal ?? attemptNativeHeal)(err);
|
|
236
|
+
if (!result.healed && !result.contended) {
|
|
237
|
+
throw new MemoryEngineUnusableError(result.reason ?? "heal-failed", result.detail ?? detailOf(err));
|
|
238
|
+
}
|
|
239
|
+
try {
|
|
240
|
+
const FreshCtor = loadBetterSqlite3({ fresh: true });
|
|
241
|
+
return new FreshCtor(dbPath, ctorOpts);
|
|
242
|
+
} catch (retryErr) {
|
|
243
|
+
throw new MemoryEngineUnusableError("heal-failed", detailOf(retryErr));
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
function attemptNativeHeal(err) {
|
|
248
|
+
const start = Date.now();
|
|
249
|
+
const abiTo = process.versions.modules;
|
|
250
|
+
const abiFrom = parseAbiFrom(err);
|
|
251
|
+
let pkgDir;
|
|
252
|
+
try {
|
|
253
|
+
pkgDir = dirname2(req.resolve("better-sqlite3/package.json"));
|
|
254
|
+
} catch {
|
|
255
|
+
recordHealEvent({ phase: "skipped", reason: "not-resolvable", abiFrom, abiTo });
|
|
256
|
+
return { healed: false, reason: "missing", abiFrom, abiTo, detail: "better-sqlite3 not resolvable" };
|
|
257
|
+
}
|
|
258
|
+
if (!isWritable(pkgDir)) {
|
|
259
|
+
recordHealEvent({ phase: "skipped", reason: "dir-not-writable", abiFrom, abiTo });
|
|
260
|
+
return { healed: false, reason: "heal-failed", abiFrom, abiTo, detail: `install dir not writable: ${pkgDir}` };
|
|
261
|
+
}
|
|
262
|
+
recordHealEvent({ phase: "attempt", reason: "abi-mismatch", abiFrom, abiTo });
|
|
263
|
+
try {
|
|
264
|
+
return withFileLockSync(
|
|
265
|
+
join(credentialsDir(), "native-heal.lock"),
|
|
266
|
+
() => runRebuild(pkgDir, abiFrom, abiTo, start),
|
|
267
|
+
{ blockMs: 6e4, staleMs: 3e5 }
|
|
268
|
+
);
|
|
269
|
+
} catch (lockErr) {
|
|
270
|
+
const detail = lockErr instanceof Error ? lockErr.message : String(lockErr);
|
|
271
|
+
recordHealEvent({ phase: "failed", reason: "heal-failed", abiFrom, abiTo, detail: `lock: ${detail}` });
|
|
272
|
+
return { healed: false, contended: true, reason: "heal-failed", abiFrom, abiTo, detail: `heal lock contended: ${detail}` };
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
function runRebuild(pkgDir, abiFrom, abiTo, start) {
|
|
276
|
+
try {
|
|
277
|
+
const FreshCtor = loadBetterSqlite3({ fresh: true });
|
|
278
|
+
new FreshCtor(":memory:").close();
|
|
279
|
+
const res2 = {
|
|
280
|
+
healed: true,
|
|
281
|
+
abiFrom,
|
|
282
|
+
abiTo,
|
|
283
|
+
durationMs: Date.now() - start,
|
|
284
|
+
detail: "already healed by a concurrent process"
|
|
285
|
+
};
|
|
286
|
+
recordHealEvent({ phase: "success", reason: "already-healed", abiFrom, abiTo, durationMs: res2.durationMs });
|
|
287
|
+
return res2;
|
|
288
|
+
} catch (probeErr) {
|
|
289
|
+
if (!isNativeAbiError(probeErr)) {
|
|
290
|
+
return { healed: false, reason: "heal-failed", abiFrom, abiTo, durationMs: Date.now() - start, detail: detailOf(probeErr) };
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
const prebuildBin = resolveLocalBin(pkgDir, "prebuild-install");
|
|
294
|
+
if (prebuildBin) {
|
|
295
|
+
const r = spawnSync(process.execPath, [prebuildBin], {
|
|
296
|
+
cwd: pkgDir,
|
|
297
|
+
encoding: "utf-8",
|
|
298
|
+
timeout: 12e4
|
|
299
|
+
});
|
|
300
|
+
if (r.status === 0) {
|
|
301
|
+
const res2 = { healed: true, method: "prebuild-install", abiFrom, abiTo, durationMs: Date.now() - start };
|
|
302
|
+
recordHealEvent({ phase: "success", reason: "abi-mismatch", ...res2 });
|
|
303
|
+
return res2;
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
const gypBin = resolveNodeGypBin();
|
|
307
|
+
if (gypBin && hasCompiler()) {
|
|
308
|
+
const r = spawnSync(process.execPath, [gypBin, "rebuild", "--release"], {
|
|
309
|
+
cwd: pkgDir,
|
|
310
|
+
encoding: "utf-8",
|
|
311
|
+
timeout: 3e5
|
|
312
|
+
});
|
|
313
|
+
if (r.status === 0) {
|
|
314
|
+
const res2 = { healed: true, method: "node-gyp", abiFrom, abiTo, durationMs: Date.now() - start };
|
|
315
|
+
recordHealEvent({ phase: "success", reason: "abi-mismatch", ...res2 });
|
|
316
|
+
return res2;
|
|
317
|
+
}
|
|
318
|
+
}
|
|
319
|
+
const res = {
|
|
320
|
+
healed: false,
|
|
321
|
+
reason: "heal-failed",
|
|
322
|
+
abiFrom,
|
|
323
|
+
abiTo,
|
|
324
|
+
durationMs: Date.now() - start,
|
|
325
|
+
detail: prebuildBin ? "prebuild-install failed; node-gyp fallback unavailable or failed" : "no prebuild-install bin resolvable"
|
|
326
|
+
};
|
|
327
|
+
recordHealEvent({ phase: "failed", reason: "heal-failed", abiFrom, abiTo, durationMs: res.durationMs });
|
|
328
|
+
return res;
|
|
329
|
+
}
|
|
330
|
+
function recordHealEvent(event) {
|
|
331
|
+
try {
|
|
332
|
+
const rec = {
|
|
333
|
+
ts: (/* @__PURE__ */ new Date()).toISOString(),
|
|
334
|
+
node: process.versions.node,
|
|
335
|
+
platform: process.platform,
|
|
336
|
+
arch: process.arch,
|
|
337
|
+
...event
|
|
338
|
+
};
|
|
339
|
+
const dir = credentialsDir();
|
|
340
|
+
mkdirSync3(dir, { recursive: true });
|
|
341
|
+
const file = join(dir, "native-heal-events.jsonl");
|
|
342
|
+
appendFileSync(file, JSON.stringify(rec) + "\n", "utf-8");
|
|
343
|
+
try {
|
|
344
|
+
chmodSync2(file, 384);
|
|
345
|
+
} catch {
|
|
346
|
+
}
|
|
347
|
+
} catch {
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
function parseAbiFrom(err) {
|
|
351
|
+
const msg = err instanceof Error ? err.message : String(err ?? "");
|
|
352
|
+
const m = /NODE_MODULE_VERSION (\d+)/.exec(msg);
|
|
353
|
+
if (m && /^\d+$/.test(m[1])) return m[1];
|
|
354
|
+
return void 0;
|
|
355
|
+
}
|
|
356
|
+
function detailOf(err) {
|
|
357
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
358
|
+
return msg.slice(0, 300);
|
|
359
|
+
}
|
|
360
|
+
function isWritable(dir) {
|
|
361
|
+
try {
|
|
362
|
+
accessSync(dir, fsConstants.W_OK);
|
|
363
|
+
return true;
|
|
364
|
+
} catch {
|
|
365
|
+
return false;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
function resolveLocalBin(pkgDir, name) {
|
|
369
|
+
const candidates = [
|
|
370
|
+
join(pkgDir, "node_modules", ".bin", name),
|
|
371
|
+
// nested install
|
|
372
|
+
join(pkgDir, "..", ".bin", name),
|
|
373
|
+
// hoisted (node_modules/.bin)
|
|
374
|
+
join(pkgDir, "..", "..", "node_modules", ".bin", name)
|
|
375
|
+
// workspace hoist
|
|
376
|
+
];
|
|
377
|
+
for (const c of candidates) {
|
|
378
|
+
if (existsSync2(c)) return c;
|
|
379
|
+
}
|
|
380
|
+
return void 0;
|
|
381
|
+
}
|
|
382
|
+
function resolveNodeGypBin() {
|
|
383
|
+
try {
|
|
384
|
+
return req.resolve("node-gyp/bin/node-gyp.js");
|
|
385
|
+
} catch {
|
|
386
|
+
return void 0;
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
function hasCompiler() {
|
|
390
|
+
for (const cc of ["cc", "clang", "gcc"]) {
|
|
391
|
+
try {
|
|
392
|
+
const r = spawnSync(cc, ["--version"], { encoding: "utf-8", timeout: 5e3 });
|
|
393
|
+
if (r.status === 0) return true;
|
|
394
|
+
} catch {
|
|
395
|
+
}
|
|
396
|
+
}
|
|
397
|
+
return false;
|
|
398
|
+
}
|
|
399
|
+
var req, NATIVE_DB_REMEDY, MemoryEngineUnusableError, _testCtor, _testHeal;
|
|
400
|
+
var init_sqlite_loader = __esm({
|
|
401
|
+
"src/lib/sqlite-loader.ts"() {
|
|
402
|
+
"use strict";
|
|
403
|
+
init_credentials();
|
|
404
|
+
init_fileLock();
|
|
405
|
+
req = createRequire(import.meta.url);
|
|
406
|
+
NATIVE_DB_REMEDY = "Run 'massu heal' to rebuild the database engine for your Node version, then restart your MCP client / Claude Code.";
|
|
407
|
+
MemoryEngineUnusableError = class extends Error {
|
|
408
|
+
reason;
|
|
409
|
+
remedy;
|
|
410
|
+
detail;
|
|
411
|
+
constructor(reason, detail) {
|
|
412
|
+
super(
|
|
413
|
+
`Massu memory engine is unusable (${reason}). ${NATIVE_DB_REMEDY}` + (detail ? ` [${detail}]` : "")
|
|
414
|
+
);
|
|
415
|
+
this.name = "MemoryEngineUnusableError";
|
|
416
|
+
this.reason = reason;
|
|
417
|
+
this.remedy = NATIVE_DB_REMEDY;
|
|
418
|
+
this.detail = detail;
|
|
419
|
+
}
|
|
420
|
+
};
|
|
421
|
+
_testCtor = null;
|
|
422
|
+
_testHeal = null;
|
|
423
|
+
}
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
// src/db-driver.ts
|
|
427
|
+
import { createRequire as createRequire2 } from "module";
|
|
428
|
+
import { closeSync, existsSync as existsSync3, mkdtempSync, openSync, readFileSync as readFileSync3, rmSync as rmSync3, fsyncSync } from "fs";
|
|
429
|
+
import { tmpdir } from "os";
|
|
430
|
+
import { join as join2 } from "path";
|
|
431
|
+
function resolveDbEngine() {
|
|
432
|
+
return process.env[DB_ENGINE_ENV] === "better-sqlite3" ? "better-sqlite3" : DEFAULT_DB_ENGINE;
|
|
433
|
+
}
|
|
434
|
+
function nodeSqliteCtor() {
|
|
435
|
+
if (!_nodeCtor) _nodeCtor = req2("node:sqlite").DatabaseSync;
|
|
436
|
+
return _nodeCtor;
|
|
437
|
+
}
|
|
438
|
+
function plainRow(row) {
|
|
439
|
+
if (row == null || typeof row !== "object") return row;
|
|
440
|
+
return { ...row };
|
|
441
|
+
}
|
|
442
|
+
function openNodeSqlite(dbPath, opts) {
|
|
443
|
+
const Ctor = nodeSqliteCtor();
|
|
444
|
+
const raw = new Ctor(dbPath, {
|
|
445
|
+
open: true,
|
|
446
|
+
readOnly: !!opts.readonly,
|
|
447
|
+
// FAITHFULNESS: better-sqlite3's ACTUAL default is foreign_keys=ON (empirically
|
|
448
|
+
// verified — SQLite's own default is OFF, but bs3 enables it). massu was written +
|
|
449
|
+
// tested against that default, so the adapter must match it, or a store that relies
|
|
450
|
+
// on FK-ON-by-default (without an explicit pragma) would silently lose enforcement.
|
|
451
|
+
// Explicit `foreign_keys=OFF/ON` pragmas (e.g. memory-db bulk ops) still override.
|
|
452
|
+
enableForeignKeyConstraints: true,
|
|
453
|
+
allowExtension: false
|
|
454
|
+
});
|
|
455
|
+
let savepointSeq = 0;
|
|
456
|
+
const wrapStmt = (sql) => {
|
|
457
|
+
const st = raw.prepare(sql);
|
|
458
|
+
return {
|
|
459
|
+
run: (...p) => {
|
|
460
|
+
const r = st.run(...p);
|
|
461
|
+
return {
|
|
462
|
+
changes: typeof r.changes === "bigint" ? Number(r.changes) : r.changes,
|
|
463
|
+
lastInsertRowid: r.lastInsertRowid
|
|
464
|
+
};
|
|
465
|
+
},
|
|
466
|
+
get: (...p) => plainRow(st.get(...p)),
|
|
467
|
+
all: (...p) => st.all(...p).map(plainRow)
|
|
468
|
+
};
|
|
469
|
+
};
|
|
470
|
+
const pragma = (source, options) => {
|
|
471
|
+
const s = String(source).trim();
|
|
472
|
+
if (s.includes("=")) {
|
|
473
|
+
raw.exec(`PRAGMA ${s}`);
|
|
474
|
+
return void 0;
|
|
475
|
+
}
|
|
476
|
+
const rows = raw.prepare(`PRAGMA ${s}`).all().map(plainRow);
|
|
477
|
+
if (options?.simple) {
|
|
478
|
+
const first = rows[0];
|
|
479
|
+
return first ? Object.values(first)[0] : void 0;
|
|
480
|
+
}
|
|
481
|
+
return rows;
|
|
482
|
+
};
|
|
483
|
+
const transaction = (fn) => {
|
|
484
|
+
const run = (...args) => {
|
|
485
|
+
const nested = raw.isTransaction;
|
|
486
|
+
const name = `msp_${savepointSeq++}`;
|
|
487
|
+
if (nested) raw.exec(`SAVEPOINT ${name}`);
|
|
488
|
+
else raw.exec("BEGIN");
|
|
489
|
+
try {
|
|
490
|
+
const out = fn(...args);
|
|
491
|
+
if (nested) raw.exec(`RELEASE ${name}`);
|
|
492
|
+
else raw.exec("COMMIT");
|
|
493
|
+
return out;
|
|
494
|
+
} catch (e) {
|
|
495
|
+
if (nested) {
|
|
496
|
+
raw.exec(`ROLLBACK TO ${name}`);
|
|
497
|
+
raw.exec(`RELEASE ${name}`);
|
|
498
|
+
} else {
|
|
499
|
+
raw.exec("ROLLBACK");
|
|
500
|
+
}
|
|
501
|
+
throw e;
|
|
502
|
+
}
|
|
503
|
+
};
|
|
504
|
+
return run;
|
|
505
|
+
};
|
|
506
|
+
const serialize = () => {
|
|
507
|
+
if (raw.isTransaction) {
|
|
508
|
+
throw new Error(
|
|
509
|
+
"db-driver: .serialize() (node:sqlite engine) cannot run inside an open transaction \u2014 VACUUM is forbidden in a transaction. Serialize before BEGIN, or use MASSU_DB_ENGINE=better-sqlite3 for mid-transaction snapshots."
|
|
510
|
+
);
|
|
511
|
+
}
|
|
512
|
+
const dir = mkdtempSync(join2(tmpdir(), "massu-serialize-"));
|
|
513
|
+
const out = join2(dir, "snapshot.db");
|
|
514
|
+
try {
|
|
515
|
+
raw.exec(`VACUUM INTO '${out.replace(/'/g, "''")}'`);
|
|
516
|
+
const fd = openSync(out, "r");
|
|
517
|
+
try {
|
|
518
|
+
fsyncSync(fd);
|
|
519
|
+
} finally {
|
|
520
|
+
closeSync(fd);
|
|
521
|
+
}
|
|
522
|
+
return readFileSync3(out);
|
|
523
|
+
} finally {
|
|
524
|
+
rmSync3(dir, { recursive: true, force: true });
|
|
525
|
+
}
|
|
526
|
+
};
|
|
527
|
+
const handle = {
|
|
528
|
+
prepare: (sql) => wrapStmt(sql),
|
|
529
|
+
exec: (sql) => raw.exec(sql),
|
|
530
|
+
pragma,
|
|
531
|
+
transaction,
|
|
532
|
+
serialize,
|
|
533
|
+
close: () => raw.close()
|
|
534
|
+
};
|
|
535
|
+
return handle;
|
|
536
|
+
}
|
|
537
|
+
function openBetterSqlite3(dbPath, opts) {
|
|
538
|
+
return openDatabase(dbPath, opts);
|
|
539
|
+
}
|
|
540
|
+
function openDatabase2(dbPath, opts = {}) {
|
|
541
|
+
return resolveDbEngine() === "better-sqlite3" ? openBetterSqlite3(dbPath, opts) : openNodeSqlite(dbPath, opts);
|
|
542
|
+
}
|
|
543
|
+
var req2, DEFAULT_DB_ENGINE, DB_ENGINE_ENV, _nodeCtor;
|
|
544
|
+
var init_db_driver = __esm({
|
|
545
|
+
"src/db-driver.ts"() {
|
|
546
|
+
"use strict";
|
|
547
|
+
init_sqlite_loader();
|
|
548
|
+
init_sqlite_loader();
|
|
549
|
+
req2 = createRequire2(import.meta.url);
|
|
550
|
+
DEFAULT_DB_ENGINE = "node-sqlite";
|
|
551
|
+
DB_ENGINE_ENV = "MASSU_DB_ENGINE";
|
|
552
|
+
_nodeCtor = null;
|
|
553
|
+
}
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
// src/lib/memory-path.ts
|
|
557
|
+
function encodeMemoryDirName(projectRoot) {
|
|
558
|
+
return projectRoot.replace(/\//g, "-");
|
|
559
|
+
}
|
|
560
|
+
var init_memory_path = __esm({
|
|
561
|
+
"src/lib/memory-path.ts"() {
|
|
562
|
+
"use strict";
|
|
563
|
+
}
|
|
564
|
+
});
|
|
565
|
+
|
|
107
566
|
// src/config-memory-schema.ts
|
|
108
567
|
import { z } from "zod";
|
|
109
568
|
var MemoryConfigSchema;
|
|
@@ -237,14 +696,40 @@ var init_config_memory_schema = __esm({
|
|
|
237
696
|
// turn of EVERY session, so an unbounded index is a permanent context tax — and the
|
|
238
697
|
// per-session cap bounds only the RATE, never the total.
|
|
239
698
|
indexMaxLines: z.number().int().min(1).default(50)
|
|
699
|
+
}).default({}),
|
|
700
|
+
// --- Cross-repo memory surfacing (plan-living-memory-slice-5-cross-repo-surfacing) ---
|
|
701
|
+
// A decision made in one of your repos can surface in another — opt-in per
|
|
702
|
+
// decision AND opt-in per repo, signed, verified, and materialized ONLY on
|
|
703
|
+
// explicit human acceptance. Local transport is FREE and zero-network.
|
|
704
|
+
//
|
|
705
|
+
// ⛔ BOTH switches default OFF, and OFF means NOTHING EXISTS — no registry, no
|
|
706
|
+
// keys, no inbox, no behavioural difference. Two INDEPENDENT opt-ins:
|
|
707
|
+
// • enabled — may this repo EXPORT its shareable decisions? (opt-in #1)
|
|
708
|
+
// • subscribe — which repo LABELS may this repo IMPORT from? (opt-in #2)
|
|
709
|
+
// `subscribe: []` means import NOTHING. There is deliberately NO `subscribe: all`
|
|
710
|
+
// — a repo you never named is a repo Massu never reads from.
|
|
711
|
+
share: z.object({
|
|
712
|
+
enabled: z.boolean().default(false),
|
|
713
|
+
subscribe: z.array(z.string()).default([]),
|
|
714
|
+
// C-04 — recall surfacing of cross-repo memories. `enabled` defaults true but is
|
|
715
|
+
// CONDITIONAL on `subscribe` being non-empty (empty by default), so the effective
|
|
716
|
+
// default is DORMANT: with `subscribe: []` the recall hook output is byte-identical
|
|
717
|
+
// to today's. `maxCrossRepoItems` caps how many accepted cross-repo items may appear
|
|
718
|
+
// per recall block (default 1); `minScore` is an OPTIONAL strictly-higher floor for
|
|
719
|
+
// cross-repo items (they are, by construction, less relevant than local ones).
|
|
720
|
+
recall: z.object({
|
|
721
|
+
enabled: z.boolean().default(true),
|
|
722
|
+
maxCrossRepoItems: z.number().int().min(0).default(1),
|
|
723
|
+
minScore: z.number().optional()
|
|
724
|
+
}).default({})
|
|
240
725
|
}).default({})
|
|
241
726
|
}).optional();
|
|
242
727
|
}
|
|
243
728
|
});
|
|
244
729
|
|
|
245
730
|
// src/config.ts
|
|
246
|
-
import { resolve as resolve2, dirname } from "path";
|
|
247
|
-
import { existsSync as
|
|
731
|
+
import { resolve as resolve2, dirname as dirname3 } from "path";
|
|
732
|
+
import { existsSync as existsSync4, readFileSync as readFileSync4 } from "fs";
|
|
248
733
|
import { homedir as homedir2 } from "os";
|
|
249
734
|
import { parse as parseYaml } from "yaml";
|
|
250
735
|
import { z as z2 } from "zod";
|
|
@@ -252,22 +737,22 @@ function findProjectRoot() {
|
|
|
252
737
|
const cwd = process.cwd();
|
|
253
738
|
let dir = cwd;
|
|
254
739
|
while (true) {
|
|
255
|
-
if (
|
|
740
|
+
if (existsSync4(resolve2(dir, "massu.config.yaml"))) {
|
|
256
741
|
return dir;
|
|
257
742
|
}
|
|
258
|
-
const parent =
|
|
743
|
+
const parent = dirname3(dir);
|
|
259
744
|
if (parent === dir) break;
|
|
260
745
|
dir = parent;
|
|
261
746
|
}
|
|
262
747
|
dir = cwd;
|
|
263
748
|
while (true) {
|
|
264
|
-
if (
|
|
749
|
+
if (existsSync4(resolve2(dir, "package.json"))) {
|
|
265
750
|
return dir;
|
|
266
751
|
}
|
|
267
|
-
if (
|
|
752
|
+
if (existsSync4(resolve2(dir, ".git"))) {
|
|
268
753
|
return dir;
|
|
269
754
|
}
|
|
270
|
-
const parent =
|
|
755
|
+
const parent = dirname3(dir);
|
|
271
756
|
if (parent === dir) break;
|
|
272
757
|
dir = parent;
|
|
273
758
|
}
|
|
@@ -284,8 +769,8 @@ function getConfig() {
|
|
|
284
769
|
const root = getProjectRoot();
|
|
285
770
|
const configPath = resolve2(root, "massu.config.yaml");
|
|
286
771
|
let rawYaml = {};
|
|
287
|
-
if (
|
|
288
|
-
const content =
|
|
772
|
+
if (existsSync4(configPath)) {
|
|
773
|
+
const content = readFileSync4(configPath, "utf-8");
|
|
289
774
|
rawYaml = parseYaml(content) ?? {};
|
|
290
775
|
}
|
|
291
776
|
const result = RawConfigSchema.safeParse(rawYaml);
|
|
@@ -366,9 +851,13 @@ Hint: run \`massu config refresh\` to regenerate a valid config or fix the liste
|
|
|
366
851
|
const resolvedEndpoint = resolveEndpoint({ configEndpoint: _config.cloud?.endpoint });
|
|
367
852
|
if (resolvedKey.apiKey) {
|
|
368
853
|
_config.cloud = {
|
|
369
|
-
enabled: true,
|
|
370
854
|
sync: { memory: true, analytics: true, audit: true },
|
|
371
855
|
..._config.cloud,
|
|
856
|
+
// Spread FIRST, then decide `enabled`, so a workspace that declares a `cloud:`
|
|
857
|
+
// block only to tune `requestTimeoutMs` does not disable its own sync. An
|
|
858
|
+
// EXPLICIT `enabled: false` is still honoured; `undefined` (not stated) means
|
|
859
|
+
// "a key resolved, so turn it on". (plan-2026-07-20-cloud-sync-timeout)
|
|
860
|
+
enabled: _config.cloud?.enabled ?? true,
|
|
372
861
|
apiKey: resolvedKey.apiKey,
|
|
373
862
|
endpoint: resolvedEndpoint
|
|
374
863
|
};
|
|
@@ -486,7 +975,6 @@ var init_config = __esm({
|
|
|
486
975
|
custom_patterns: z2.array(CustomPatternSchema).default([])
|
|
487
976
|
}).optional(),
|
|
488
977
|
adr: z2.object({
|
|
489
|
-
detection_phrases: z2.array(z2.string()).default(["chose", "decided", "switching to", "moving from", "going with"]),
|
|
490
978
|
template: z2.string().default("default"),
|
|
491
979
|
storage: z2.string().default("database"),
|
|
492
980
|
output_dir: z2.string().default("docs/adr")
|
|
@@ -588,9 +1076,22 @@ var init_config = __esm({
|
|
|
588
1076
|
})).default([])
|
|
589
1077
|
}).optional();
|
|
590
1078
|
CloudConfigSchema = z2.object({
|
|
591
|
-
|
|
1079
|
+
// OPTIONAL, not `.default(false)`. A zod default is indistinguishable from an
|
|
1080
|
+
// explicit value once parsed, so `.default(false)` meant that merely declaring a
|
|
1081
|
+
// `cloud:` block (to set any OTHER key) silently produced `enabled: false`, which
|
|
1082
|
+
// then overrode the `enabled: true` auto-enable below via object spread — turning
|
|
1083
|
+
// cloud sync OFF as a side effect of tuning it. `undefined` now means "not stated",
|
|
1084
|
+
// and the auto-enable preserves it. (plan-2026-07-20-cloud-sync-timeout)
|
|
1085
|
+
enabled: z2.boolean().optional(),
|
|
592
1086
|
apiKey: z2.string().optional(),
|
|
593
1087
|
endpoint: z2.string().optional(),
|
|
1088
|
+
// Per-request POST budget for the `/sync` ingest. cloud-sync.ts reads this
|
|
1089
|
+
// (`(cloud as { requestTimeoutMs?: number }).requestTimeoutMs`) but it was ABSENT
|
|
1090
|
+
// from this schema, and zod strips unknown keys — so the knob was unreachable from
|
|
1091
|
+
// massu.config.yaml and the default could never be tuned. Measured 2026-07-20: a
|
|
1092
|
+
// 423-observation payload takes ~9.2s against the live ingest. Capped at
|
|
1093
|
+
// SYNC_DEADLINE_MS (20s) since the overall deadline clamps each attempt anyway.
|
|
1094
|
+
requestTimeoutMs: z2.number().int().positive().max(2e4).optional(),
|
|
594
1095
|
sync: z2.object({
|
|
595
1096
|
memory: z2.boolean().default(true),
|
|
596
1097
|
analytics: z2.boolean().default(true),
|
|
@@ -827,27 +1328,26 @@ var init_memory_vector = __esm({
|
|
|
827
1328
|
});
|
|
828
1329
|
|
|
829
1330
|
// src/db-backup.ts
|
|
830
|
-
import
|
|
831
|
-
import {
|
|
832
|
-
import { resolve as resolve3, join, basename, dirname as dirname2 } from "path";
|
|
1331
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync4, readdirSync, statSync, unlinkSync, copyFileSync, rmSync as rmSync4 } from "fs";
|
|
1332
|
+
import { resolve as resolve3, join as join3, basename, dirname as dirname4 } from "path";
|
|
833
1333
|
import { homedir as homedir3 } from "os";
|
|
834
1334
|
function dbBackupsRoot(home = homedir3()) {
|
|
835
1335
|
return resolve3(home, ".massu", "db-backups");
|
|
836
1336
|
}
|
|
837
1337
|
function projectBackupDir(projectRoot, home = homedir3()) {
|
|
838
1338
|
const slug = basename(projectRoot).replace(/[^A-Za-z0-9._-]/g, "_") || "project";
|
|
839
|
-
return
|
|
1339
|
+
return join3(dbBackupsRoot(home), slug);
|
|
840
1340
|
}
|
|
841
1341
|
function backupStamp(nowMs) {
|
|
842
1342
|
return new Date(nowMs).toISOString().replace(/[:.]/g, "-");
|
|
843
1343
|
}
|
|
844
1344
|
function listDbBackups(projectRoot, home = homedir3()) {
|
|
845
1345
|
const dir = projectBackupDir(projectRoot, home);
|
|
846
|
-
if (!
|
|
1346
|
+
if (!existsSync5(dir)) return [];
|
|
847
1347
|
const out = [];
|
|
848
1348
|
for (const f of readdirSync(dir)) {
|
|
849
1349
|
if (!f.endsWith(".db")) continue;
|
|
850
|
-
const p =
|
|
1350
|
+
const p = join3(dir, f);
|
|
851
1351
|
const st = statSync(p);
|
|
852
1352
|
const db = f.replace(/-\d{4}-\d{2}-\d{2}T.*$/, "");
|
|
853
1353
|
out.push({ db, path: p, bytes: st.size, mtimeMs: st.mtimeMs });
|
|
@@ -860,13 +1360,13 @@ function hasFreshDbBackup(projectRoot, dbPath, nowMs = Date.now(), home = homedi
|
|
|
860
1360
|
if (backups.length === 0) return false;
|
|
861
1361
|
const newest = backups[0];
|
|
862
1362
|
if (nowMs - newest.mtimeMs > FRESH_WINDOW_MS) return false;
|
|
863
|
-
if (
|
|
1363
|
+
if (existsSync5(dbPath) && statSync(dbPath).mtimeMs > newest.mtimeMs) return false;
|
|
864
1364
|
return true;
|
|
865
1365
|
}
|
|
866
1366
|
function integrityOk(dbPath) {
|
|
867
1367
|
let db = null;
|
|
868
1368
|
try {
|
|
869
|
-
db =
|
|
1369
|
+
db = openDatabase2(dbPath, { readonly: true });
|
|
870
1370
|
const rows = db.pragma("integrity_check");
|
|
871
1371
|
return rows.length === 1 && rows[0].integrity_check === "ok";
|
|
872
1372
|
} catch {
|
|
@@ -879,16 +1379,16 @@ function integrityOk(dbPath) {
|
|
|
879
1379
|
}
|
|
880
1380
|
}
|
|
881
1381
|
function backupDb(projectRoot, dbPath, nowMs = Date.now(), home = homedir3()) {
|
|
882
|
-
if (!
|
|
1382
|
+
if (!existsSync5(dbPath)) {
|
|
883
1383
|
throw new DbBackupError(`cannot back up a database that does not exist: ${dbPath}`);
|
|
884
1384
|
}
|
|
885
1385
|
const dir = projectBackupDir(projectRoot, home);
|
|
886
|
-
|
|
1386
|
+
mkdirSync4(dir, { recursive: true });
|
|
887
1387
|
const name = basename(dbPath, ".db");
|
|
888
|
-
const dest =
|
|
1388
|
+
const dest = join3(dir, `${name}-${backupStamp(nowMs)}.db`);
|
|
889
1389
|
let src = null;
|
|
890
1390
|
try {
|
|
891
|
-
src =
|
|
1391
|
+
src = openDatabase2(dbPath, { readonly: true });
|
|
892
1392
|
src.exec(`VACUUM INTO '${dest.replace(/'/g, "''")}'`);
|
|
893
1393
|
} catch (err) {
|
|
894
1394
|
throw new DbBackupError(
|
|
@@ -927,7 +1427,7 @@ function pruneDbBackups(projectRoot, dbName, keep = DEFAULT_RETENTION, home = ho
|
|
|
927
1427
|
}
|
|
928
1428
|
function backupBeforeSchemaChange(projectRoot, dbPath, onError, nowMs = Date.now(), home = homedir3()) {
|
|
929
1429
|
try {
|
|
930
|
-
if (!
|
|
1430
|
+
if (!existsSync5(dbPath)) return null;
|
|
931
1431
|
if (hasFreshDbBackup(projectRoot, dbPath, nowMs, home)) return null;
|
|
932
1432
|
return backupDb(projectRoot, dbPath, nowMs, home);
|
|
933
1433
|
} catch (err) {
|
|
@@ -939,6 +1439,7 @@ var DEFAULT_RETENTION, FRESH_WINDOW_MS, DbBackupError;
|
|
|
939
1439
|
var init_db_backup = __esm({
|
|
940
1440
|
"src/db-backup.ts"() {
|
|
941
1441
|
"use strict";
|
|
1442
|
+
init_db_driver();
|
|
942
1443
|
DEFAULT_RETENTION = 5;
|
|
943
1444
|
FRESH_WINDOW_MS = 24 * 60 * 60 * 1e3;
|
|
944
1445
|
DbBackupError = class extends Error {
|
|
@@ -1026,8 +1527,8 @@ var init_rule_delivery = __esm({
|
|
|
1026
1527
|
});
|
|
1027
1528
|
|
|
1028
1529
|
// src/rule-candidate-store.ts
|
|
1029
|
-
import { existsSync as
|
|
1030
|
-
import { join as
|
|
1530
|
+
import { existsSync as existsSync6, mkdirSync as mkdirSync5, writeFileSync as writeFileSync3, readdirSync as readdirSync2, readFileSync as readFileSync5 } from "node:fs";
|
|
1531
|
+
import { join as join4 } from "node:path";
|
|
1031
1532
|
function ensureRuleCandidatesTable(db) {
|
|
1032
1533
|
db.exec(`
|
|
1033
1534
|
CREATE TABLE IF NOT EXISTS rule_candidates (
|
|
@@ -1047,7 +1548,7 @@ function ensureRuleCandidatesTable(db) {
|
|
|
1047
1548
|
`);
|
|
1048
1549
|
}
|
|
1049
1550
|
function sidecarPath(projectRoot, promptHash) {
|
|
1050
|
-
return
|
|
1551
|
+
return join4(projectRoot, ".massu", "rule-candidates", `${promptHash}.json`);
|
|
1051
1552
|
}
|
|
1052
1553
|
function upsertCandidate(db, projectRoot, candidate, opts = {}) {
|
|
1053
1554
|
ensureRuleCandidatesTable(db);
|
|
@@ -1062,11 +1563,11 @@ function upsertCandidate(db, projectRoot, candidate, opts = {}) {
|
|
|
1062
1563
|
origin = excluded.origin,
|
|
1063
1564
|
updated_at = datetime('now')
|
|
1064
1565
|
`).run(candidate.prompt_hash, origin, score, JSON.stringify(candidate));
|
|
1065
|
-
const dir =
|
|
1066
|
-
|
|
1566
|
+
const dir = join4(projectRoot, ".massu", "rule-candidates");
|
|
1567
|
+
mkdirSync5(dir, { recursive: true });
|
|
1067
1568
|
const path = sidecarPath(projectRoot, candidate.prompt_hash);
|
|
1068
|
-
if (!
|
|
1069
|
-
|
|
1569
|
+
if (!existsSync6(path)) {
|
|
1570
|
+
writeFileSync3(path, JSON.stringify(candidate, null, 2), "utf-8");
|
|
1070
1571
|
}
|
|
1071
1572
|
}
|
|
1072
1573
|
var init_rule_candidate_store = __esm({
|
|
@@ -1076,21 +1577,21 @@ var init_rule_candidate_store = __esm({
|
|
|
1076
1577
|
});
|
|
1077
1578
|
|
|
1078
1579
|
// src/hooks/lib/hook-failure-signal.ts
|
|
1079
|
-
import { appendFileSync, mkdirSync as
|
|
1080
|
-
import { join as
|
|
1580
|
+
import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync6, existsSync as existsSync7 } from "fs";
|
|
1581
|
+
import { join as join5, dirname as dirname5 } from "path";
|
|
1081
1582
|
function resolveFailureLogPath() {
|
|
1082
1583
|
const explicit = process.env.MASSU_HOOK_FAILURE_LOG;
|
|
1083
1584
|
if (explicit) return explicit;
|
|
1084
1585
|
let dir = process.cwd();
|
|
1085
1586
|
for (let i = 0; i < 12; i++) {
|
|
1086
|
-
if (
|
|
1087
|
-
return
|
|
1587
|
+
if (existsSync7(join5(dir, ".massu")) || existsSync7(join5(dir, "massu.config.yaml"))) {
|
|
1588
|
+
return join5(dir, ".massu", "hook-failures.jsonl");
|
|
1088
1589
|
}
|
|
1089
|
-
const parent =
|
|
1590
|
+
const parent = dirname5(dir);
|
|
1090
1591
|
if (parent === dir) break;
|
|
1091
1592
|
dir = parent;
|
|
1092
1593
|
}
|
|
1093
|
-
return
|
|
1594
|
+
return join5(process.cwd(), ".massu", "hook-failures.jsonl");
|
|
1094
1595
|
}
|
|
1095
1596
|
function recordHookFailure(hook, error, context) {
|
|
1096
1597
|
const record = {
|
|
@@ -1103,8 +1604,8 @@ function recordHookFailure(hook, error, context) {
|
|
|
1103
1604
|
let wroteSomething = false;
|
|
1104
1605
|
try {
|
|
1105
1606
|
const path = resolveFailureLogPath();
|
|
1106
|
-
|
|
1107
|
-
|
|
1607
|
+
mkdirSync6(dirname5(path), { recursive: true });
|
|
1608
|
+
appendFileSync2(path, JSON.stringify(record) + "\n", "utf-8");
|
|
1108
1609
|
wroteSomething = true;
|
|
1109
1610
|
} catch {
|
|
1110
1611
|
}
|
|
@@ -1147,9 +1648,9 @@ var init_hook_failure_signal = __esm({
|
|
|
1147
1648
|
});
|
|
1148
1649
|
|
|
1149
1650
|
// src/memory-embedder-tokenizer.ts
|
|
1150
|
-
import { readFileSync as
|
|
1651
|
+
import { readFileSync as readFileSync6 } from "fs";
|
|
1151
1652
|
function loadVocab(vocabPath) {
|
|
1152
|
-
const lines =
|
|
1653
|
+
const lines = readFileSync6(vocabPath, "utf-8").split("\n");
|
|
1153
1654
|
const vocab = /* @__PURE__ */ new Map();
|
|
1154
1655
|
for (let i = 0; i < lines.length; i++) {
|
|
1155
1656
|
const tok = lines[i].replace(/\r$/, "");
|
|
@@ -1269,9 +1770,9 @@ var init_memory_embedder_tokenizer = __esm({
|
|
|
1269
1770
|
|
|
1270
1771
|
// src/memory-embedder.ts
|
|
1271
1772
|
import { fileURLToPath } from "url";
|
|
1272
|
-
import { dirname as
|
|
1273
|
-
import { existsSync as
|
|
1274
|
-
import { createRequire } from "module";
|
|
1773
|
+
import { dirname as dirname6, join as join6 } from "path";
|
|
1774
|
+
import { existsSync as existsSync8 } from "fs";
|
|
1775
|
+
import { createRequire as createRequire3 } from "module";
|
|
1275
1776
|
function getActiveEmbedModel() {
|
|
1276
1777
|
return _activeModel;
|
|
1277
1778
|
}
|
|
@@ -1292,21 +1793,21 @@ function embeddingsDisabled() {
|
|
|
1292
1793
|
}
|
|
1293
1794
|
function resolveModelDir() {
|
|
1294
1795
|
try {
|
|
1295
|
-
let dir =
|
|
1796
|
+
let dir = dirname6(fileURLToPath(import.meta.url));
|
|
1296
1797
|
let root = null;
|
|
1297
1798
|
for (let i = 0; i < 8; i++) {
|
|
1298
|
-
if (
|
|
1799
|
+
if (existsSync8(join6(dir, "package.json"))) {
|
|
1299
1800
|
root = dir;
|
|
1300
1801
|
break;
|
|
1301
1802
|
}
|
|
1302
|
-
const parent =
|
|
1803
|
+
const parent = dirname6(dir);
|
|
1303
1804
|
if (parent === dir) break;
|
|
1304
1805
|
dir = parent;
|
|
1305
1806
|
}
|
|
1306
1807
|
if (!root) return null;
|
|
1307
1808
|
for (const rel of ["dist/embedder", "assets/embedder"]) {
|
|
1308
|
-
const candidate =
|
|
1309
|
-
if (
|
|
1809
|
+
const candidate = join6(root, rel);
|
|
1810
|
+
if (existsSync8(join6(candidate, "model_quantized.onnx"))) return candidate;
|
|
1310
1811
|
}
|
|
1311
1812
|
return null;
|
|
1312
1813
|
} catch {
|
|
@@ -1315,9 +1816,9 @@ function resolveModelDir() {
|
|
|
1315
1816
|
}
|
|
1316
1817
|
function resolveWasmDir() {
|
|
1317
1818
|
try {
|
|
1318
|
-
const
|
|
1319
|
-
const main2 =
|
|
1320
|
-
return
|
|
1819
|
+
const req3 = createRequire3(import.meta.url);
|
|
1820
|
+
const main2 = req3.resolve(ORT_PKG);
|
|
1821
|
+
return dirname6(main2);
|
|
1321
1822
|
} catch {
|
|
1322
1823
|
return null;
|
|
1323
1824
|
}
|
|
@@ -1337,8 +1838,8 @@ async function getSession() {
|
|
|
1337
1838
|
ort.env.wasm.proxy = false;
|
|
1338
1839
|
ort.env.wasm.wasmPaths = wasmDir.endsWith("/") ? wasmDir : wasmDir + "/";
|
|
1339
1840
|
_ortTensor = ort.Tensor;
|
|
1340
|
-
const modelPath =
|
|
1341
|
-
const vocabPath =
|
|
1841
|
+
const modelPath = join6(modelDir, "model_quantized.onnx");
|
|
1842
|
+
const vocabPath = join6(modelDir, "vocab.txt");
|
|
1342
1843
|
_vocab = loadVocab(vocabPath);
|
|
1343
1844
|
const session = await ort.InferenceSession.create(modelPath, {
|
|
1344
1845
|
executionProviders: ["wasm"],
|
|
@@ -1659,6 +2160,7 @@ __export(memory_db_exports, {
|
|
|
1659
2160
|
migrateAuditLogCheckExtension: () => migrateAuditLogCheckExtension,
|
|
1660
2161
|
migrateMemoryFilesFor4B: () => migrateMemoryFilesFor4B,
|
|
1661
2162
|
migrateObservationEmbeddingChunks: () => migrateObservationEmbeddingChunks,
|
|
2163
|
+
migrateSharedMemoryFor5B: () => migrateSharedMemoryFor5B,
|
|
1662
2164
|
pruneOldConversationTurns: () => pruneOldConversationTurns,
|
|
1663
2165
|
pruneOldObservations: () => pruneOldObservations,
|
|
1664
2166
|
pruneToolCostEvents: () => pruneToolCostEvents,
|
|
@@ -1675,9 +2177,8 @@ __export(memory_db_exports, {
|
|
|
1675
2177
|
upsertObservationEmbedding: () => upsertObservationEmbedding,
|
|
1676
2178
|
usageWarmupElapsed: () => usageWarmupElapsed
|
|
1677
2179
|
});
|
|
1678
|
-
import
|
|
1679
|
-
import {
|
|
1680
|
-
import { existsSync as existsSync7, mkdirSync as mkdirSync5 } from "fs";
|
|
2180
|
+
import { dirname as dirname7, basename as basename2 } from "path";
|
|
2181
|
+
import { existsSync as existsSync9, mkdirSync as mkdirSync7 } from "fs";
|
|
1681
2182
|
function sanitizeFts5Query(raw) {
|
|
1682
2183
|
const trimmed = raw.trim();
|
|
1683
2184
|
if (!trimmed) return '""';
|
|
@@ -1693,12 +2194,12 @@ function sanitizeFts5QueryOr(raw) {
|
|
|
1693
2194
|
}
|
|
1694
2195
|
function getMemoryDb() {
|
|
1695
2196
|
const dbPath = getResolvedPaths().memoryDbPath;
|
|
1696
|
-
const dir =
|
|
1697
|
-
if (!
|
|
1698
|
-
|
|
2197
|
+
const dir = dirname7(dbPath);
|
|
2198
|
+
if (!existsSync9(dir)) {
|
|
2199
|
+
mkdirSync7(dir, { recursive: true });
|
|
1699
2200
|
}
|
|
1700
|
-
const preExisting =
|
|
1701
|
-
const db =
|
|
2201
|
+
const preExisting = existsSync9(dbPath);
|
|
2202
|
+
const db = openDatabase2(dbPath);
|
|
1702
2203
|
db.pragma("journal_mode = WAL");
|
|
1703
2204
|
db.pragma("foreign_keys = ON");
|
|
1704
2205
|
const onDisk = db.pragma("user_version", { simple: true });
|
|
@@ -1774,6 +2275,39 @@ function migrateMemoryFilesFor4B(db) {
|
|
|
1774
2275
|
db.exec(`ALTER TABLE ${add.table} ADD COLUMN ${add.name} ${add.decl}`);
|
|
1775
2276
|
}
|
|
1776
2277
|
}
|
|
2278
|
+
function migrateSharedMemoryFor5B(db) {
|
|
2279
|
+
const cols = db.prepare(`PRAGMA table_info(observations)`).all();
|
|
2280
|
+
if (cols.length > 0 && !cols.some((c) => c.name === "shareable")) {
|
|
2281
|
+
db.exec(`ALTER TABLE observations ADD COLUMN shareable INTEGER NOT NULL DEFAULT 0`);
|
|
2282
|
+
}
|
|
2283
|
+
db.exec(`
|
|
2284
|
+
CREATE TABLE IF NOT EXISTS shared_memory_pending (
|
|
2285
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
2286
|
+
record_hash TEXT NOT NULL UNIQUE, -- identity + idempotency key (hex sha256)
|
|
2287
|
+
origin_repo_id TEXT NOT NULL, -- the signed origin repo_id (v4 UUID)
|
|
2288
|
+
origin_repo_label TEXT NOT NULL, -- re-slugged on read; NEVER trusted from the wire
|
|
2289
|
+
envelope_raw TEXT NOT NULL, -- the VERBATIM signed bytes; B-05 re-verifies these
|
|
2290
|
+
record_json TEXT NOT NULL, -- the single record's canonical JSON
|
|
2291
|
+
received_at_epoch INTEGER NOT NULL, -- epoch SECONDS (Slice-2 convention)
|
|
2292
|
+
accepted_at_epoch INTEGER, -- set on accept (B-05); NULL while pending
|
|
2293
|
+
refused_at_epoch INTEGER, -- set on refuse (B-06)
|
|
2294
|
+
expired_at_epoch INTEGER -- revocation \u21D2 EXPIRE, never DELETE (B-07 / S-3)
|
|
2295
|
+
);
|
|
2296
|
+
CREATE INDEX IF NOT EXISTS idx_smp_origin ON shared_memory_pending(origin_repo_id);
|
|
2297
|
+
CREATE INDEX IF NOT EXISTS idx_smp_received ON shared_memory_pending(received_at_epoch DESC);
|
|
2298
|
+
`);
|
|
2299
|
+
db.exec(`
|
|
2300
|
+
CREATE TABLE IF NOT EXISTS shared_memory_outbound (
|
|
2301
|
+
record_hash TEXT PRIMARY KEY, -- the exported record's canonical hash
|
|
2302
|
+
observation_id INTEGER NOT NULL, -- the source observations.id (for B-07 revocation)
|
|
2303
|
+
origin_repo_id TEXT NOT NULL, -- this repo's own repo_id at export time
|
|
2304
|
+
seq INTEGER NOT NULL, -- the envelope seq this record first went out in
|
|
2305
|
+
exported_at_epoch INTEGER NOT NULL, -- epoch SECONDS
|
|
2306
|
+
revoked_at_epoch INTEGER -- set when the source row is superseded/expired (B-07)
|
|
2307
|
+
);
|
|
2308
|
+
CREATE INDEX IF NOT EXISTS idx_smo_obs ON shared_memory_outbound(observation_id);
|
|
2309
|
+
`);
|
|
2310
|
+
}
|
|
1777
2311
|
function migrateAuditLogCheckExtension(db) {
|
|
1778
2312
|
const row = db.prepare("SELECT sql FROM sqlite_master WHERE type='table' AND name='audit_log'").get();
|
|
1779
2313
|
if (!row) return;
|
|
@@ -1796,7 +2330,16 @@ function migrateAuditLogCheckExtension(db) {
|
|
|
1796
2330
|
"memory_file_adopted_human",
|
|
1797
2331
|
"memory_file_rendered",
|
|
1798
2332
|
"memory_file_render_refused",
|
|
1799
|
-
"memory_file_tombstoned"
|
|
2333
|
+
"memory_file_tombstoned",
|
|
2334
|
+
// B-10 (Slice 5) — the seven cross-repo shared-memory events. An existing DB whose
|
|
2335
|
+
// CHECK lacks any of these triggers the table rebuild below.
|
|
2336
|
+
"shared_memory_exported",
|
|
2337
|
+
"shared_memory_export_refused",
|
|
2338
|
+
"shared_memory_imported",
|
|
2339
|
+
"shared_memory_dropped",
|
|
2340
|
+
"shared_memory_accepted",
|
|
2341
|
+
"shared_memory_refused",
|
|
2342
|
+
"shared_memory_revoked"
|
|
1800
2343
|
];
|
|
1801
2344
|
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);
|
|
1802
2345
|
if (checkClauseMatch) {
|
|
@@ -1815,7 +2358,12 @@ function migrateAuditLogCheckExtension(db) {
|
|
|
1815
2358
|
'code_change', 'rule_enforced', 'approval', 'review', 'commit', 'compaction',
|
|
1816
2359
|
'rule_candidate_emitted', 'rule_promoted', 'rule_dismissed',
|
|
1817
2360
|
'memory_file_ingested', 'memory_file_expired', 'memory_file_adopted_human',
|
|
1818
|
-
'memory_file_rendered', 'memory_file_render_refused', 'memory_file_tombstoned'
|
|
2361
|
+
'memory_file_rendered', 'memory_file_render_refused', 'memory_file_tombstoned',
|
|
2362
|
+
-- B-10 (Slice 5) \u2014 cross-repo shared-memory events (must mirror the CREATE +
|
|
2363
|
+
-- the expected[] list above, or the migration would loop rebuilding forever).
|
|
2364
|
+
'shared_memory_exported', 'shared_memory_export_refused', 'shared_memory_imported',
|
|
2365
|
+
'shared_memory_dropped', 'shared_memory_accepted', 'shared_memory_refused',
|
|
2366
|
+
'shared_memory_revoked'
|
|
1819
2367
|
)),
|
|
1820
2368
|
actor TEXT NOT NULL DEFAULT 'ai' CHECK(actor IN ('ai', 'human', 'hook', 'agent')),
|
|
1821
2369
|
model_id TEXT,
|
|
@@ -1892,6 +2440,12 @@ function initMemorySchema(db) {
|
|
|
1892
2440
|
-- credential, or takes a snapshot. A Slice-5 synced memory arrives here with
|
|
1893
2441
|
-- origin='team' and must never reach disk without CR-55's gates.
|
|
1894
2442
|
origin TEXT NOT NULL DEFAULT 'local',
|
|
2443
|
+
-- B-01 (Slice 5, cross-repo surfacing): the per-decision SHARE opt-in. Set to 1
|
|
2444
|
+
-- ONLY by an explicit human act (massu memory share <id>); NEVER by a heuristic,
|
|
2445
|
+
-- score, LLM, hook, or consolidation pass. Export (B-02) reads WHERE shareable=1
|
|
2446
|
+
-- AND origin='local'. Defaults 0 so every pre-existing and machine-written row is
|
|
2447
|
+
-- un-shareable until a human says otherwise (fail-closed).
|
|
2448
|
+
shareable INTEGER NOT NULL DEFAULT 0,
|
|
1895
2449
|
created_at TEXT NOT NULL,
|
|
1896
2450
|
created_at_epoch INTEGER NOT NULL,
|
|
1897
2451
|
FOREIGN KEY(session_id) REFERENCES sessions(session_id) ON DELETE CASCADE
|
|
@@ -2165,7 +2719,13 @@ function initMemorySchema(db) {
|
|
|
2165
2719
|
'code_change', 'rule_enforced', 'approval', 'review', 'commit', 'compaction',
|
|
2166
2720
|
'rule_candidate_emitted', 'rule_promoted', 'rule_dismissed',
|
|
2167
2721
|
'memory_file_ingested', 'memory_file_expired', 'memory_file_adopted_human',
|
|
2168
|
-
'memory_file_rendered', 'memory_file_render_refused', 'memory_file_tombstoned'
|
|
2722
|
+
'memory_file_rendered', 'memory_file_render_refused', 'memory_file_tombstoned',
|
|
2723
|
+
-- B-10 (Slice 5): cross-repo shared-memory observability. Without these seven,
|
|
2724
|
+
-- nobody could ever answer "what has crossed between my repos?" (and an ingest
|
|
2725
|
+
-- catch would swallow the CHECK violation, silently producing NOTHING).
|
|
2726
|
+
'shared_memory_exported', 'shared_memory_export_refused', 'shared_memory_imported',
|
|
2727
|
+
'shared_memory_dropped', 'shared_memory_accepted', 'shared_memory_refused',
|
|
2728
|
+
'shared_memory_revoked'
|
|
2169
2729
|
)),
|
|
2170
2730
|
actor TEXT NOT NULL DEFAULT 'ai' CHECK(actor IN ('ai', 'human', 'hook', 'agent')),
|
|
2171
2731
|
model_id TEXT,
|
|
@@ -2534,6 +3094,7 @@ function initMemorySchema(db) {
|
|
|
2534
3094
|
`);
|
|
2535
3095
|
migrateObservationEmbeddingChunks(db);
|
|
2536
3096
|
migrateMemoryFilesFor4B(db);
|
|
3097
|
+
migrateSharedMemoryFor5B(db);
|
|
2537
3098
|
ensureRuleCandidatesTable(db);
|
|
2538
3099
|
migrateRuleDelivery(db);
|
|
2539
3100
|
}
|
|
@@ -3404,6 +3965,7 @@ var MEMORY_SCHEMA_VERSION, TOOL_COST_EVENTS_RETENTION_DAYS, MAX_DRAFT_TEXT_LEN,
|
|
|
3404
3965
|
var init_memory_db = __esm({
|
|
3405
3966
|
"src/memory-db.ts"() {
|
|
3406
3967
|
"use strict";
|
|
3968
|
+
init_db_driver();
|
|
3407
3969
|
init_config();
|
|
3408
3970
|
init_memory_vector();
|
|
3409
3971
|
init_db_backup();
|
|
@@ -3425,9 +3987,10 @@ var init_memory_db = __esm({
|
|
|
3425
3987
|
|
|
3426
3988
|
// src/hooks/user-prompt.ts
|
|
3427
3989
|
init_memory_db();
|
|
3990
|
+
init_db_driver();
|
|
3428
3991
|
init_config();
|
|
3429
|
-
import { existsSync as
|
|
3430
|
-
import { join as
|
|
3992
|
+
import { existsSync as existsSync10, mkdirSync as mkdirSync8, writeFileSync as writeFileSync4, readFileSync as readFileSync7, readdirSync as readdirSync3, openSync as openSync2, fstatSync, readSync, closeSync as closeSync2 } from "fs";
|
|
3993
|
+
import { join as join7 } from "path";
|
|
3431
3994
|
|
|
3432
3995
|
// src/rule-candidate-detector.ts
|
|
3433
3996
|
var CORRECTION_DISMISSAL_PATTERN = /\b(nevermind|never\s+mind|forget\s+it|ignore\s+(that|it)|actually\s+you('?re|\s+were)\s+right|on\s+second\s+thought|no\s+wait|scratch\s+that|disregard|abandon\s+(that|it))\b/i;
|
|
@@ -3768,9 +4331,8 @@ async function main() {
|
|
|
3768
4331
|
const fileRefs = extractFileReferences(prompt);
|
|
3769
4332
|
if (fileRefs.length > 0) {
|
|
3770
4333
|
const knowledgeDbPath = getResolvedPaths().knowledgeDbPath;
|
|
3771
|
-
if (knowledgeDbPath &&
|
|
3772
|
-
const
|
|
3773
|
-
const kdb = new BetterSqlite3Ctor(knowledgeDbPath, { readonly: true });
|
|
4334
|
+
if (knowledgeDbPath && existsSync10(knowledgeDbPath)) {
|
|
4335
|
+
const kdb = openDatabase2(knowledgeDbPath, { readonly: true, selfHeal: false });
|
|
3774
4336
|
try {
|
|
3775
4337
|
const placeholders = fileRefs.map(() => "?").join(",");
|
|
3776
4338
|
const matches = kdb.prepare(
|
|
@@ -3822,14 +4384,14 @@ async function main() {
|
|
|
3822
4384
|
category: categorizePrompt(prompt),
|
|
3823
4385
|
blacklist
|
|
3824
4386
|
});
|
|
3825
|
-
const candidateDir =
|
|
4387
|
+
const candidateDir = join7(hookInput.cwd, ".massu", "rule-candidates");
|
|
3826
4388
|
if (scoreResult.emitCandidate) {
|
|
3827
4389
|
const cachedTier = getCachedTierReadOnly(db);
|
|
3828
4390
|
if (entitledForAutoLearning(cachedTier)) {
|
|
3829
|
-
|
|
4391
|
+
mkdirSync8(candidateDir, { recursive: true });
|
|
3830
4392
|
const promptHash = hashPrompt(prompt);
|
|
3831
|
-
const candidatePath =
|
|
3832
|
-
if (!
|
|
4393
|
+
const candidatePath = join7(candidateDir, `${promptHash}.json`);
|
|
4394
|
+
if (!existsSync10(candidatePath)) {
|
|
3833
4395
|
upsertCandidate(db, hookInput.cwd, {
|
|
3834
4396
|
prompt,
|
|
3835
4397
|
prompt_hash: promptHash,
|
|
@@ -3853,28 +4415,28 @@ async function main() {
|
|
|
3853
4415
|
}
|
|
3854
4416
|
}
|
|
3855
4417
|
} else {
|
|
3856
|
-
const nudgePath =
|
|
3857
|
-
if (!
|
|
3858
|
-
|
|
4418
|
+
const nudgePath = join7(candidateDir, ".last-tier-nudge");
|
|
4419
|
+
if (!existsSync10(nudgePath)) {
|
|
4420
|
+
mkdirSync8(candidateDir, { recursive: true });
|
|
3859
4421
|
process.stderr.write(
|
|
3860
4422
|
`
|
|
3861
4423
|
[RULE CANDIDATE] ${autoLearningUpgradeMessage(cachedTier)}
|
|
3862
4424
|
|
|
3863
4425
|
`
|
|
3864
4426
|
);
|
|
3865
|
-
|
|
4427
|
+
writeFileSync4(nudgePath, (/* @__PURE__ */ new Date()).toISOString());
|
|
3866
4428
|
}
|
|
3867
4429
|
}
|
|
3868
4430
|
}
|
|
3869
|
-
if (
|
|
4431
|
+
if (existsSync10(candidateDir)) {
|
|
3870
4432
|
const candidates = readdirSync3(candidateDir).filter(
|
|
3871
4433
|
(f) => f.endsWith(".json") && !f.startsWith(".")
|
|
3872
4434
|
);
|
|
3873
4435
|
const candidateCount = candidates.length;
|
|
3874
|
-
const surfacedPath =
|
|
4436
|
+
const surfacedPath = join7(candidateDir, ".last-surfaced");
|
|
3875
4437
|
let lastSurfaced = 0;
|
|
3876
|
-
if (
|
|
3877
|
-
const raw =
|
|
4438
|
+
if (existsSync10(surfacedPath)) {
|
|
4439
|
+
const raw = readFileSync7(surfacedPath, "utf-8").trim();
|
|
3878
4440
|
const parsed = parseInt(raw, 10);
|
|
3879
4441
|
if (!Number.isNaN(parsed)) lastSurfaced = parsed;
|
|
3880
4442
|
}
|
|
@@ -3885,17 +4447,17 @@ async function main() {
|
|
|
3885
4447
|
|
|
3886
4448
|
`
|
|
3887
4449
|
);
|
|
3888
|
-
|
|
4450
|
+
writeFileSync4(surfacedPath, String(candidateCount));
|
|
3889
4451
|
}
|
|
3890
4452
|
}
|
|
3891
4453
|
} catch (candidateErr) {
|
|
3892
4454
|
try {
|
|
3893
|
-
const dir =
|
|
3894
|
-
if (!
|
|
3895
|
-
const logPath =
|
|
3896
|
-
const pre =
|
|
4455
|
+
const dir = join7(hookInput.cwd, ".massu", "rule-candidates");
|
|
4456
|
+
if (!existsSync10(dir)) mkdirSync8(dir, { recursive: true });
|
|
4457
|
+
const logPath = join7(dir, ".detector-failures.jsonl");
|
|
4458
|
+
const pre = existsSync10(logPath) ? readFileSync7(logPath, "utf-8") : "";
|
|
3897
4459
|
const sep = pre && !pre.endsWith("\n") ? "\n" : "";
|
|
3898
|
-
|
|
4460
|
+
writeFileSync4(logPath, pre + sep + JSON.stringify({
|
|
3899
4461
|
session_id,
|
|
3900
4462
|
// Security review (plan-2026-05-27): log the prompt HASH, never a
|
|
3901
4463
|
// raw prompt excerpt — the failure log must not persist PII/secrets.
|
|
@@ -3916,13 +4478,13 @@ async function main() {
|
|
|
3916
4478
|
}
|
|
3917
4479
|
function detectPriorAssistantTurn(transcriptPath) {
|
|
3918
4480
|
try {
|
|
3919
|
-
if (!transcriptPath || !
|
|
4481
|
+
if (!transcriptPath || !existsSync10(transcriptPath)) {
|
|
3920
4482
|
return { hadEditOrWrite: false, files: [] };
|
|
3921
4483
|
}
|
|
3922
4484
|
if (!transcriptPath.includes("/.claude/projects/")) {
|
|
3923
4485
|
return { hadEditOrWrite: false, files: [] };
|
|
3924
4486
|
}
|
|
3925
|
-
const fd =
|
|
4487
|
+
const fd = openSync2(transcriptPath, "r");
|
|
3926
4488
|
let buf;
|
|
3927
4489
|
try {
|
|
3928
4490
|
const stats = fstatSync(fd);
|
|
@@ -3931,7 +4493,7 @@ function detectPriorAssistantTurn(transcriptPath) {
|
|
|
3931
4493
|
buf = Buffer.alloc(readLen);
|
|
3932
4494
|
readSync(fd, buf, 0, readLen, offset);
|
|
3933
4495
|
} finally {
|
|
3934
|
-
|
|
4496
|
+
closeSync2(fd);
|
|
3935
4497
|
}
|
|
3936
4498
|
const lines = buf.toString("utf-8").split("\n").filter(Boolean);
|
|
3937
4499
|
const files = [];
|
|
@@ -3992,8 +4554,8 @@ function extractFileReferences(prompt) {
|
|
|
3992
4554
|
}
|
|
3993
4555
|
async function getGitBranch() {
|
|
3994
4556
|
try {
|
|
3995
|
-
const { spawnSync } = await import("child_process");
|
|
3996
|
-
const result =
|
|
4557
|
+
const { spawnSync: spawnSync2 } = await import("child_process");
|
|
4558
|
+
const result = spawnSync2("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
|
|
3997
4559
|
encoding: "utf-8",
|
|
3998
4560
|
timeout: 5e3
|
|
3999
4561
|
});
|