@massu/core 1.16.1 → 1.16.3
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 +95 -0
- package/dist/cli.js +1090 -697
- package/dist/hooks/auto-learning-pipeline.js +401 -55
- package/dist/hooks/classify-failure.js +399 -53
- package/dist/hooks/cost-tracker.js +411 -65
- package/dist/hooks/fix-detector.js +410 -64
- package/dist/hooks/incident-pipeline.js +396 -50
- package/dist/hooks/intent-suggester.js +411 -65
- package/dist/hooks/memory-recall.js +415 -69
- package/dist/hooks/post-edit-context.js +413 -67
- package/dist/hooks/post-tool-use.js +439 -93
- package/dist/hooks/pre-compact.js +411 -65
- package/dist/hooks/pre-delete-check.js +415 -69
- package/dist/hooks/pre-tool-use-gate.js +415 -69
- package/dist/hooks/quality-event.js +411 -65
- package/dist/hooks/rule-enforcement-pipeline.js +397 -51
- package/dist/hooks/security-gate.js +411 -65
- package/dist/hooks/session-end.js +441 -95
- package/dist/hooks/session-start.js +533 -283
- package/dist/hooks/user-prompt.js +445 -99
- package/package.json +1 -1
|
@@ -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,345 @@ 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/lib/memory-path.ts
|
|
427
|
+
function encodeMemoryDirName(projectRoot) {
|
|
428
|
+
return projectRoot.replace(/\//g, "-");
|
|
429
|
+
}
|
|
430
|
+
var init_memory_path = __esm({
|
|
431
|
+
"src/lib/memory-path.ts"() {
|
|
432
|
+
"use strict";
|
|
433
|
+
}
|
|
434
|
+
});
|
|
435
|
+
|
|
107
436
|
// src/config-memory-schema.ts
|
|
108
437
|
import { z } from "zod";
|
|
109
438
|
var MemoryConfigSchema;
|
|
@@ -243,8 +572,8 @@ var init_config_memory_schema = __esm({
|
|
|
243
572
|
});
|
|
244
573
|
|
|
245
574
|
// src/config.ts
|
|
246
|
-
import { resolve as resolve2, dirname } from "path";
|
|
247
|
-
import { existsSync as
|
|
575
|
+
import { resolve as resolve2, dirname as dirname3 } from "path";
|
|
576
|
+
import { existsSync as existsSync3, readFileSync as readFileSync3 } from "fs";
|
|
248
577
|
import { homedir as homedir2 } from "os";
|
|
249
578
|
import { parse as parseYaml } from "yaml";
|
|
250
579
|
import { z as z2 } from "zod";
|
|
@@ -252,22 +581,22 @@ function findProjectRoot() {
|
|
|
252
581
|
const cwd = process.cwd();
|
|
253
582
|
let dir = cwd;
|
|
254
583
|
while (true) {
|
|
255
|
-
if (
|
|
584
|
+
if (existsSync3(resolve2(dir, "massu.config.yaml"))) {
|
|
256
585
|
return dir;
|
|
257
586
|
}
|
|
258
|
-
const parent =
|
|
587
|
+
const parent = dirname3(dir);
|
|
259
588
|
if (parent === dir) break;
|
|
260
589
|
dir = parent;
|
|
261
590
|
}
|
|
262
591
|
dir = cwd;
|
|
263
592
|
while (true) {
|
|
264
|
-
if (
|
|
593
|
+
if (existsSync3(resolve2(dir, "package.json"))) {
|
|
265
594
|
return dir;
|
|
266
595
|
}
|
|
267
|
-
if (
|
|
596
|
+
if (existsSync3(resolve2(dir, ".git"))) {
|
|
268
597
|
return dir;
|
|
269
598
|
}
|
|
270
|
-
const parent =
|
|
599
|
+
const parent = dirname3(dir);
|
|
271
600
|
if (parent === dir) break;
|
|
272
601
|
dir = parent;
|
|
273
602
|
}
|
|
@@ -284,8 +613,8 @@ function getConfig() {
|
|
|
284
613
|
const root = getProjectRoot();
|
|
285
614
|
const configPath = resolve2(root, "massu.config.yaml");
|
|
286
615
|
let rawYaml = {};
|
|
287
|
-
if (
|
|
288
|
-
const content =
|
|
616
|
+
if (existsSync3(configPath)) {
|
|
617
|
+
const content = readFileSync3(configPath, "utf-8");
|
|
289
618
|
rawYaml = parseYaml(content) ?? {};
|
|
290
619
|
}
|
|
291
620
|
const result = RawConfigSchema.safeParse(rawYaml);
|
|
@@ -366,9 +695,13 @@ Hint: run \`massu config refresh\` to regenerate a valid config or fix the liste
|
|
|
366
695
|
const resolvedEndpoint = resolveEndpoint({ configEndpoint: _config.cloud?.endpoint });
|
|
367
696
|
if (resolvedKey.apiKey) {
|
|
368
697
|
_config.cloud = {
|
|
369
|
-
enabled: true,
|
|
370
698
|
sync: { memory: true, analytics: true, audit: true },
|
|
371
699
|
..._config.cloud,
|
|
700
|
+
// Spread FIRST, then decide `enabled`, so a workspace that declares a `cloud:`
|
|
701
|
+
// block only to tune `requestTimeoutMs` does not disable its own sync. An
|
|
702
|
+
// EXPLICIT `enabled: false` is still honoured; `undefined` (not stated) means
|
|
703
|
+
// "a key resolved, so turn it on". (plan-2026-07-20-cloud-sync-timeout)
|
|
704
|
+
enabled: _config.cloud?.enabled ?? true,
|
|
372
705
|
apiKey: resolvedKey.apiKey,
|
|
373
706
|
endpoint: resolvedEndpoint
|
|
374
707
|
};
|
|
@@ -588,9 +921,22 @@ var init_config = __esm({
|
|
|
588
921
|
})).default([])
|
|
589
922
|
}).optional();
|
|
590
923
|
CloudConfigSchema = z2.object({
|
|
591
|
-
|
|
924
|
+
// OPTIONAL, not `.default(false)`. A zod default is indistinguishable from an
|
|
925
|
+
// explicit value once parsed, so `.default(false)` meant that merely declaring a
|
|
926
|
+
// `cloud:` block (to set any OTHER key) silently produced `enabled: false`, which
|
|
927
|
+
// then overrode the `enabled: true` auto-enable below via object spread — turning
|
|
928
|
+
// cloud sync OFF as a side effect of tuning it. `undefined` now means "not stated",
|
|
929
|
+
// and the auto-enable preserves it. (plan-2026-07-20-cloud-sync-timeout)
|
|
930
|
+
enabled: z2.boolean().optional(),
|
|
592
931
|
apiKey: z2.string().optional(),
|
|
593
932
|
endpoint: z2.string().optional(),
|
|
933
|
+
// Per-request POST budget for the `/sync` ingest. cloud-sync.ts reads this
|
|
934
|
+
// (`(cloud as { requestTimeoutMs?: number }).requestTimeoutMs`) but it was ABSENT
|
|
935
|
+
// from this schema, and zod strips unknown keys — so the knob was unreachable from
|
|
936
|
+
// massu.config.yaml and the default could never be tuned. Measured 2026-07-20: a
|
|
937
|
+
// 423-observation payload takes ~9.2s against the live ingest. Capped at
|
|
938
|
+
// SYNC_DEADLINE_MS (20s) since the overall deadline clamps each attempt anyway.
|
|
939
|
+
requestTimeoutMs: z2.number().int().positive().max(2e4).optional(),
|
|
594
940
|
sync: z2.object({
|
|
595
941
|
memory: z2.boolean().default(true),
|
|
596
942
|
analytics: z2.boolean().default(true),
|
|
@@ -827,27 +1173,26 @@ var init_memory_vector = __esm({
|
|
|
827
1173
|
});
|
|
828
1174
|
|
|
829
1175
|
// src/db-backup.ts
|
|
830
|
-
import
|
|
831
|
-
import {
|
|
832
|
-
import { resolve as resolve3, join, basename, dirname as dirname2 } from "path";
|
|
1176
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync4, readdirSync, statSync, unlinkSync, copyFileSync, rmSync as rmSync3 } from "fs";
|
|
1177
|
+
import { resolve as resolve3, join as join2, basename, dirname as dirname4 } from "path";
|
|
833
1178
|
import { homedir as homedir3 } from "os";
|
|
834
1179
|
function dbBackupsRoot(home = homedir3()) {
|
|
835
1180
|
return resolve3(home, ".massu", "db-backups");
|
|
836
1181
|
}
|
|
837
1182
|
function projectBackupDir(projectRoot, home = homedir3()) {
|
|
838
1183
|
const slug = basename(projectRoot).replace(/[^A-Za-z0-9._-]/g, "_") || "project";
|
|
839
|
-
return
|
|
1184
|
+
return join2(dbBackupsRoot(home), slug);
|
|
840
1185
|
}
|
|
841
1186
|
function backupStamp(nowMs) {
|
|
842
1187
|
return new Date(nowMs).toISOString().replace(/[:.]/g, "-");
|
|
843
1188
|
}
|
|
844
1189
|
function listDbBackups(projectRoot, home = homedir3()) {
|
|
845
1190
|
const dir = projectBackupDir(projectRoot, home);
|
|
846
|
-
if (!
|
|
1191
|
+
if (!existsSync4(dir)) return [];
|
|
847
1192
|
const out = [];
|
|
848
1193
|
for (const f of readdirSync(dir)) {
|
|
849
1194
|
if (!f.endsWith(".db")) continue;
|
|
850
|
-
const p =
|
|
1195
|
+
const p = join2(dir, f);
|
|
851
1196
|
const st = statSync(p);
|
|
852
1197
|
const db = f.replace(/-\d{4}-\d{2}-\d{2}T.*$/, "");
|
|
853
1198
|
out.push({ db, path: p, bytes: st.size, mtimeMs: st.mtimeMs });
|
|
@@ -860,13 +1205,13 @@ function hasFreshDbBackup(projectRoot, dbPath, nowMs = Date.now(), home = homedi
|
|
|
860
1205
|
if (backups.length === 0) return false;
|
|
861
1206
|
const newest = backups[0];
|
|
862
1207
|
if (nowMs - newest.mtimeMs > FRESH_WINDOW_MS) return false;
|
|
863
|
-
if (
|
|
1208
|
+
if (existsSync4(dbPath) && statSync(dbPath).mtimeMs > newest.mtimeMs) return false;
|
|
864
1209
|
return true;
|
|
865
1210
|
}
|
|
866
1211
|
function integrityOk(dbPath) {
|
|
867
1212
|
let db = null;
|
|
868
1213
|
try {
|
|
869
|
-
db =
|
|
1214
|
+
db = openDatabase(dbPath, { readonly: true });
|
|
870
1215
|
const rows = db.pragma("integrity_check");
|
|
871
1216
|
return rows.length === 1 && rows[0].integrity_check === "ok";
|
|
872
1217
|
} catch {
|
|
@@ -879,16 +1224,16 @@ function integrityOk(dbPath) {
|
|
|
879
1224
|
}
|
|
880
1225
|
}
|
|
881
1226
|
function backupDb(projectRoot, dbPath, nowMs = Date.now(), home = homedir3()) {
|
|
882
|
-
if (!
|
|
1227
|
+
if (!existsSync4(dbPath)) {
|
|
883
1228
|
throw new DbBackupError(`cannot back up a database that does not exist: ${dbPath}`);
|
|
884
1229
|
}
|
|
885
1230
|
const dir = projectBackupDir(projectRoot, home);
|
|
886
|
-
|
|
1231
|
+
mkdirSync4(dir, { recursive: true });
|
|
887
1232
|
const name = basename(dbPath, ".db");
|
|
888
|
-
const dest =
|
|
1233
|
+
const dest = join2(dir, `${name}-${backupStamp(nowMs)}.db`);
|
|
889
1234
|
let src = null;
|
|
890
1235
|
try {
|
|
891
|
-
src =
|
|
1236
|
+
src = openDatabase(dbPath, { readonly: true });
|
|
892
1237
|
src.exec(`VACUUM INTO '${dest.replace(/'/g, "''")}'`);
|
|
893
1238
|
} catch (err) {
|
|
894
1239
|
throw new DbBackupError(
|
|
@@ -927,7 +1272,7 @@ function pruneDbBackups(projectRoot, dbName, keep = DEFAULT_RETENTION, home = ho
|
|
|
927
1272
|
}
|
|
928
1273
|
function backupBeforeSchemaChange(projectRoot, dbPath, onError, nowMs = Date.now(), home = homedir3()) {
|
|
929
1274
|
try {
|
|
930
|
-
if (!
|
|
1275
|
+
if (!existsSync4(dbPath)) return null;
|
|
931
1276
|
if (hasFreshDbBackup(projectRoot, dbPath, nowMs, home)) return null;
|
|
932
1277
|
return backupDb(projectRoot, dbPath, nowMs, home);
|
|
933
1278
|
} catch (err) {
|
|
@@ -939,6 +1284,7 @@ var DEFAULT_RETENTION, FRESH_WINDOW_MS, DbBackupError;
|
|
|
939
1284
|
var init_db_backup = __esm({
|
|
940
1285
|
"src/db-backup.ts"() {
|
|
941
1286
|
"use strict";
|
|
1287
|
+
init_sqlite_loader();
|
|
942
1288
|
DEFAULT_RETENTION = 5;
|
|
943
1289
|
FRESH_WINDOW_MS = 24 * 60 * 60 * 1e3;
|
|
944
1290
|
DbBackupError = class extends Error {
|
|
@@ -1026,8 +1372,8 @@ var init_rule_delivery = __esm({
|
|
|
1026
1372
|
});
|
|
1027
1373
|
|
|
1028
1374
|
// src/rule-candidate-store.ts
|
|
1029
|
-
import { existsSync as
|
|
1030
|
-
import { join as
|
|
1375
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync5, writeFileSync as writeFileSync3, readdirSync as readdirSync2, readFileSync as readFileSync4 } from "node:fs";
|
|
1376
|
+
import { join as join3 } from "node:path";
|
|
1031
1377
|
function ensureRuleCandidatesTable(db) {
|
|
1032
1378
|
db.exec(`
|
|
1033
1379
|
CREATE TABLE IF NOT EXISTS rule_candidates (
|
|
@@ -1047,7 +1393,7 @@ function ensureRuleCandidatesTable(db) {
|
|
|
1047
1393
|
`);
|
|
1048
1394
|
}
|
|
1049
1395
|
function sidecarPath(projectRoot, promptHash) {
|
|
1050
|
-
return
|
|
1396
|
+
return join3(projectRoot, ".massu", "rule-candidates", `${promptHash}.json`);
|
|
1051
1397
|
}
|
|
1052
1398
|
function upsertCandidate(db, projectRoot, candidate, opts = {}) {
|
|
1053
1399
|
ensureRuleCandidatesTable(db);
|
|
@@ -1062,11 +1408,11 @@ function upsertCandidate(db, projectRoot, candidate, opts = {}) {
|
|
|
1062
1408
|
origin = excluded.origin,
|
|
1063
1409
|
updated_at = datetime('now')
|
|
1064
1410
|
`).run(candidate.prompt_hash, origin, score, JSON.stringify(candidate));
|
|
1065
|
-
const dir =
|
|
1066
|
-
|
|
1411
|
+
const dir = join3(projectRoot, ".massu", "rule-candidates");
|
|
1412
|
+
mkdirSync5(dir, { recursive: true });
|
|
1067
1413
|
const path = sidecarPath(projectRoot, candidate.prompt_hash);
|
|
1068
|
-
if (!
|
|
1069
|
-
|
|
1414
|
+
if (!existsSync5(path)) {
|
|
1415
|
+
writeFileSync3(path, JSON.stringify(candidate, null, 2), "utf-8");
|
|
1070
1416
|
}
|
|
1071
1417
|
}
|
|
1072
1418
|
var init_rule_candidate_store = __esm({
|
|
@@ -1076,21 +1422,21 @@ var init_rule_candidate_store = __esm({
|
|
|
1076
1422
|
});
|
|
1077
1423
|
|
|
1078
1424
|
// src/hooks/lib/hook-failure-signal.ts
|
|
1079
|
-
import { appendFileSync, mkdirSync as
|
|
1080
|
-
import { join as
|
|
1425
|
+
import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync6, existsSync as existsSync6 } from "fs";
|
|
1426
|
+
import { join as join4, dirname as dirname5 } from "path";
|
|
1081
1427
|
function resolveFailureLogPath() {
|
|
1082
1428
|
const explicit = process.env.MASSU_HOOK_FAILURE_LOG;
|
|
1083
1429
|
if (explicit) return explicit;
|
|
1084
1430
|
let dir = process.cwd();
|
|
1085
1431
|
for (let i = 0; i < 12; i++) {
|
|
1086
|
-
if (
|
|
1087
|
-
return
|
|
1432
|
+
if (existsSync6(join4(dir, ".massu")) || existsSync6(join4(dir, "massu.config.yaml"))) {
|
|
1433
|
+
return join4(dir, ".massu", "hook-failures.jsonl");
|
|
1088
1434
|
}
|
|
1089
|
-
const parent =
|
|
1435
|
+
const parent = dirname5(dir);
|
|
1090
1436
|
if (parent === dir) break;
|
|
1091
1437
|
dir = parent;
|
|
1092
1438
|
}
|
|
1093
|
-
return
|
|
1439
|
+
return join4(process.cwd(), ".massu", "hook-failures.jsonl");
|
|
1094
1440
|
}
|
|
1095
1441
|
function recordHookFailure(hook, error, context) {
|
|
1096
1442
|
const record = {
|
|
@@ -1103,8 +1449,8 @@ function recordHookFailure(hook, error, context) {
|
|
|
1103
1449
|
let wroteSomething = false;
|
|
1104
1450
|
try {
|
|
1105
1451
|
const path = resolveFailureLogPath();
|
|
1106
|
-
|
|
1107
|
-
|
|
1452
|
+
mkdirSync6(dirname5(path), { recursive: true });
|
|
1453
|
+
appendFileSync2(path, JSON.stringify(record) + "\n", "utf-8");
|
|
1108
1454
|
wroteSomething = true;
|
|
1109
1455
|
} catch {
|
|
1110
1456
|
}
|
|
@@ -1147,9 +1493,9 @@ var init_hook_failure_signal = __esm({
|
|
|
1147
1493
|
});
|
|
1148
1494
|
|
|
1149
1495
|
// src/memory-embedder-tokenizer.ts
|
|
1150
|
-
import { readFileSync as
|
|
1496
|
+
import { readFileSync as readFileSync5 } from "fs";
|
|
1151
1497
|
function loadVocab(vocabPath) {
|
|
1152
|
-
const lines =
|
|
1498
|
+
const lines = readFileSync5(vocabPath, "utf-8").split("\n");
|
|
1153
1499
|
const vocab = /* @__PURE__ */ new Map();
|
|
1154
1500
|
for (let i = 0; i < lines.length; i++) {
|
|
1155
1501
|
const tok = lines[i].replace(/\r$/, "");
|
|
@@ -1269,9 +1615,9 @@ var init_memory_embedder_tokenizer = __esm({
|
|
|
1269
1615
|
|
|
1270
1616
|
// src/memory-embedder.ts
|
|
1271
1617
|
import { fileURLToPath } from "url";
|
|
1272
|
-
import { dirname as
|
|
1273
|
-
import { existsSync as
|
|
1274
|
-
import { createRequire } from "module";
|
|
1618
|
+
import { dirname as dirname6, join as join5 } from "path";
|
|
1619
|
+
import { existsSync as existsSync7 } from "fs";
|
|
1620
|
+
import { createRequire as createRequire2 } from "module";
|
|
1275
1621
|
function getActiveEmbedModel() {
|
|
1276
1622
|
return _activeModel;
|
|
1277
1623
|
}
|
|
@@ -1292,21 +1638,21 @@ function embeddingsDisabled() {
|
|
|
1292
1638
|
}
|
|
1293
1639
|
function resolveModelDir() {
|
|
1294
1640
|
try {
|
|
1295
|
-
let dir =
|
|
1641
|
+
let dir = dirname6(fileURLToPath(import.meta.url));
|
|
1296
1642
|
let root = null;
|
|
1297
1643
|
for (let i = 0; i < 8; i++) {
|
|
1298
|
-
if (
|
|
1644
|
+
if (existsSync7(join5(dir, "package.json"))) {
|
|
1299
1645
|
root = dir;
|
|
1300
1646
|
break;
|
|
1301
1647
|
}
|
|
1302
|
-
const parent =
|
|
1648
|
+
const parent = dirname6(dir);
|
|
1303
1649
|
if (parent === dir) break;
|
|
1304
1650
|
dir = parent;
|
|
1305
1651
|
}
|
|
1306
1652
|
if (!root) return null;
|
|
1307
1653
|
for (const rel of ["dist/embedder", "assets/embedder"]) {
|
|
1308
|
-
const candidate =
|
|
1309
|
-
if (
|
|
1654
|
+
const candidate = join5(root, rel);
|
|
1655
|
+
if (existsSync7(join5(candidate, "model_quantized.onnx"))) return candidate;
|
|
1310
1656
|
}
|
|
1311
1657
|
return null;
|
|
1312
1658
|
} catch {
|
|
@@ -1315,9 +1661,9 @@ function resolveModelDir() {
|
|
|
1315
1661
|
}
|
|
1316
1662
|
function resolveWasmDir() {
|
|
1317
1663
|
try {
|
|
1318
|
-
const
|
|
1319
|
-
const main2 =
|
|
1320
|
-
return
|
|
1664
|
+
const req2 = createRequire2(import.meta.url);
|
|
1665
|
+
const main2 = req2.resolve(ORT_PKG);
|
|
1666
|
+
return dirname6(main2);
|
|
1321
1667
|
} catch {
|
|
1322
1668
|
return null;
|
|
1323
1669
|
}
|
|
@@ -1337,8 +1683,8 @@ async function getSession() {
|
|
|
1337
1683
|
ort.env.wasm.proxy = false;
|
|
1338
1684
|
ort.env.wasm.wasmPaths = wasmDir.endsWith("/") ? wasmDir : wasmDir + "/";
|
|
1339
1685
|
_ortTensor = ort.Tensor;
|
|
1340
|
-
const modelPath =
|
|
1341
|
-
const vocabPath =
|
|
1686
|
+
const modelPath = join5(modelDir, "model_quantized.onnx");
|
|
1687
|
+
const vocabPath = join5(modelDir, "vocab.txt");
|
|
1342
1688
|
_vocab = loadVocab(vocabPath);
|
|
1343
1689
|
const session = await ort.InferenceSession.create(modelPath, {
|
|
1344
1690
|
executionProviders: ["wasm"],
|
|
@@ -1675,9 +2021,8 @@ __export(memory_db_exports, {
|
|
|
1675
2021
|
upsertObservationEmbedding: () => upsertObservationEmbedding,
|
|
1676
2022
|
usageWarmupElapsed: () => usageWarmupElapsed
|
|
1677
2023
|
});
|
|
1678
|
-
import
|
|
1679
|
-
import {
|
|
1680
|
-
import { existsSync as existsSync7, mkdirSync as mkdirSync5 } from "fs";
|
|
2024
|
+
import { dirname as dirname7, basename as basename2 } from "path";
|
|
2025
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync7 } from "fs";
|
|
1681
2026
|
function sanitizeFts5Query(raw) {
|
|
1682
2027
|
const trimmed = raw.trim();
|
|
1683
2028
|
if (!trimmed) return '""';
|
|
@@ -1693,12 +2038,12 @@ function sanitizeFts5QueryOr(raw) {
|
|
|
1693
2038
|
}
|
|
1694
2039
|
function getMemoryDb() {
|
|
1695
2040
|
const dbPath = getResolvedPaths().memoryDbPath;
|
|
1696
|
-
const dir =
|
|
1697
|
-
if (!
|
|
1698
|
-
|
|
2041
|
+
const dir = dirname7(dbPath);
|
|
2042
|
+
if (!existsSync8(dir)) {
|
|
2043
|
+
mkdirSync7(dir, { recursive: true });
|
|
1699
2044
|
}
|
|
1700
|
-
const preExisting =
|
|
1701
|
-
const db =
|
|
2045
|
+
const preExisting = existsSync8(dbPath);
|
|
2046
|
+
const db = openDatabase(dbPath);
|
|
1702
2047
|
db.pragma("journal_mode = WAL");
|
|
1703
2048
|
db.pragma("foreign_keys = ON");
|
|
1704
2049
|
const onDisk = db.pragma("user_version", { simple: true });
|
|
@@ -3404,6 +3749,7 @@ var MEMORY_SCHEMA_VERSION, TOOL_COST_EVENTS_RETENTION_DAYS, MAX_DRAFT_TEXT_LEN,
|
|
|
3404
3749
|
var init_memory_db = __esm({
|
|
3405
3750
|
"src/memory-db.ts"() {
|
|
3406
3751
|
"use strict";
|
|
3752
|
+
init_sqlite_loader();
|
|
3407
3753
|
init_config();
|
|
3408
3754
|
init_memory_vector();
|
|
3409
3755
|
init_db_backup();
|
|
@@ -3425,9 +3771,10 @@ var init_memory_db = __esm({
|
|
|
3425
3771
|
|
|
3426
3772
|
// src/hooks/user-prompt.ts
|
|
3427
3773
|
init_memory_db();
|
|
3774
|
+
init_sqlite_loader();
|
|
3428
3775
|
init_config();
|
|
3429
|
-
import { existsSync as
|
|
3430
|
-
import { join as
|
|
3776
|
+
import { existsSync as existsSync9, mkdirSync as mkdirSync8, writeFileSync as writeFileSync4, readFileSync as readFileSync6, readdirSync as readdirSync3, openSync, fstatSync, readSync, closeSync } from "fs";
|
|
3777
|
+
import { join as join6 } from "path";
|
|
3431
3778
|
|
|
3432
3779
|
// src/rule-candidate-detector.ts
|
|
3433
3780
|
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;
|
|
@@ -3635,7 +3982,7 @@ function verifyLicenseResponse(payload) {
|
|
|
3635
3982
|
);
|
|
3636
3983
|
}
|
|
3637
3984
|
function isLicenseSignatureRequired() {
|
|
3638
|
-
return process.env.MASSU_REQUIRE_SIGNED_LICENSE
|
|
3985
|
+
return process.env.MASSU_REQUIRE_SIGNED_LICENSE !== "false";
|
|
3639
3986
|
}
|
|
3640
3987
|
|
|
3641
3988
|
// src/license.ts
|
|
@@ -3768,9 +4115,8 @@ async function main() {
|
|
|
3768
4115
|
const fileRefs = extractFileReferences(prompt);
|
|
3769
4116
|
if (fileRefs.length > 0) {
|
|
3770
4117
|
const knowledgeDbPath = getResolvedPaths().knowledgeDbPath;
|
|
3771
|
-
if (knowledgeDbPath &&
|
|
3772
|
-
const
|
|
3773
|
-
const kdb = new BetterSqlite3Ctor(knowledgeDbPath, { readonly: true });
|
|
4118
|
+
if (knowledgeDbPath && existsSync9(knowledgeDbPath)) {
|
|
4119
|
+
const kdb = openDatabase(knowledgeDbPath, { readonly: true, selfHeal: false });
|
|
3774
4120
|
try {
|
|
3775
4121
|
const placeholders = fileRefs.map(() => "?").join(",");
|
|
3776
4122
|
const matches = kdb.prepare(
|
|
@@ -3822,14 +4168,14 @@ async function main() {
|
|
|
3822
4168
|
category: categorizePrompt(prompt),
|
|
3823
4169
|
blacklist
|
|
3824
4170
|
});
|
|
3825
|
-
const candidateDir =
|
|
4171
|
+
const candidateDir = join6(hookInput.cwd, ".massu", "rule-candidates");
|
|
3826
4172
|
if (scoreResult.emitCandidate) {
|
|
3827
4173
|
const cachedTier = getCachedTierReadOnly(db);
|
|
3828
4174
|
if (entitledForAutoLearning(cachedTier)) {
|
|
3829
|
-
|
|
4175
|
+
mkdirSync8(candidateDir, { recursive: true });
|
|
3830
4176
|
const promptHash = hashPrompt(prompt);
|
|
3831
|
-
const candidatePath =
|
|
3832
|
-
if (!
|
|
4177
|
+
const candidatePath = join6(candidateDir, `${promptHash}.json`);
|
|
4178
|
+
if (!existsSync9(candidatePath)) {
|
|
3833
4179
|
upsertCandidate(db, hookInput.cwd, {
|
|
3834
4180
|
prompt,
|
|
3835
4181
|
prompt_hash: promptHash,
|
|
@@ -3853,28 +4199,28 @@ async function main() {
|
|
|
3853
4199
|
}
|
|
3854
4200
|
}
|
|
3855
4201
|
} else {
|
|
3856
|
-
const nudgePath =
|
|
3857
|
-
if (!
|
|
3858
|
-
|
|
4202
|
+
const nudgePath = join6(candidateDir, ".last-tier-nudge");
|
|
4203
|
+
if (!existsSync9(nudgePath)) {
|
|
4204
|
+
mkdirSync8(candidateDir, { recursive: true });
|
|
3859
4205
|
process.stderr.write(
|
|
3860
4206
|
`
|
|
3861
4207
|
[RULE CANDIDATE] ${autoLearningUpgradeMessage(cachedTier)}
|
|
3862
4208
|
|
|
3863
4209
|
`
|
|
3864
4210
|
);
|
|
3865
|
-
|
|
4211
|
+
writeFileSync4(nudgePath, (/* @__PURE__ */ new Date()).toISOString());
|
|
3866
4212
|
}
|
|
3867
4213
|
}
|
|
3868
4214
|
}
|
|
3869
|
-
if (
|
|
4215
|
+
if (existsSync9(candidateDir)) {
|
|
3870
4216
|
const candidates = readdirSync3(candidateDir).filter(
|
|
3871
4217
|
(f) => f.endsWith(".json") && !f.startsWith(".")
|
|
3872
4218
|
);
|
|
3873
4219
|
const candidateCount = candidates.length;
|
|
3874
|
-
const surfacedPath =
|
|
4220
|
+
const surfacedPath = join6(candidateDir, ".last-surfaced");
|
|
3875
4221
|
let lastSurfaced = 0;
|
|
3876
|
-
if (
|
|
3877
|
-
const raw =
|
|
4222
|
+
if (existsSync9(surfacedPath)) {
|
|
4223
|
+
const raw = readFileSync6(surfacedPath, "utf-8").trim();
|
|
3878
4224
|
const parsed = parseInt(raw, 10);
|
|
3879
4225
|
if (!Number.isNaN(parsed)) lastSurfaced = parsed;
|
|
3880
4226
|
}
|
|
@@ -3885,17 +4231,17 @@ async function main() {
|
|
|
3885
4231
|
|
|
3886
4232
|
`
|
|
3887
4233
|
);
|
|
3888
|
-
|
|
4234
|
+
writeFileSync4(surfacedPath, String(candidateCount));
|
|
3889
4235
|
}
|
|
3890
4236
|
}
|
|
3891
4237
|
} catch (candidateErr) {
|
|
3892
4238
|
try {
|
|
3893
|
-
const dir =
|
|
3894
|
-
if (!
|
|
3895
|
-
const logPath =
|
|
3896
|
-
const pre =
|
|
4239
|
+
const dir = join6(hookInput.cwd, ".massu", "rule-candidates");
|
|
4240
|
+
if (!existsSync9(dir)) mkdirSync8(dir, { recursive: true });
|
|
4241
|
+
const logPath = join6(dir, ".detector-failures.jsonl");
|
|
4242
|
+
const pre = existsSync9(logPath) ? readFileSync6(logPath, "utf-8") : "";
|
|
3897
4243
|
const sep = pre && !pre.endsWith("\n") ? "\n" : "";
|
|
3898
|
-
|
|
4244
|
+
writeFileSync4(logPath, pre + sep + JSON.stringify({
|
|
3899
4245
|
session_id,
|
|
3900
4246
|
// Security review (plan-2026-05-27): log the prompt HASH, never a
|
|
3901
4247
|
// raw prompt excerpt — the failure log must not persist PII/secrets.
|
|
@@ -3916,7 +4262,7 @@ async function main() {
|
|
|
3916
4262
|
}
|
|
3917
4263
|
function detectPriorAssistantTurn(transcriptPath) {
|
|
3918
4264
|
try {
|
|
3919
|
-
if (!transcriptPath || !
|
|
4265
|
+
if (!transcriptPath || !existsSync9(transcriptPath)) {
|
|
3920
4266
|
return { hadEditOrWrite: false, files: [] };
|
|
3921
4267
|
}
|
|
3922
4268
|
if (!transcriptPath.includes("/.claude/projects/")) {
|
|
@@ -3992,8 +4338,8 @@ function extractFileReferences(prompt) {
|
|
|
3992
4338
|
}
|
|
3993
4339
|
async function getGitBranch() {
|
|
3994
4340
|
try {
|
|
3995
|
-
const { spawnSync } = await import("child_process");
|
|
3996
|
-
const result =
|
|
4341
|
+
const { spawnSync: spawnSync2 } = await import("child_process");
|
|
4342
|
+
const result = spawnSync2("git", ["rev-parse", "--abbrev-ref", "HEAD"], {
|
|
3997
4343
|
encoding: "utf-8",
|
|
3998
4344
|
timeout: 5e3
|
|
3999
4345
|
});
|