@massu/core 1.16.2 → 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 +68 -0
- package/dist/cli.js +1089 -696
- 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 +440 -94
- package/dist/hooks/session-start.js +532 -282
- package/dist/hooks/user-prompt.js +444 -98
- 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 sleep2 = 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
|
+
sleep2(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),
|
|
@@ -848,27 +1194,26 @@ var init_memory_vector = __esm({
|
|
|
848
1194
|
});
|
|
849
1195
|
|
|
850
1196
|
// src/db-backup.ts
|
|
851
|
-
import
|
|
852
|
-
import {
|
|
853
|
-
import { resolve as resolve3, join, basename, dirname as dirname2 } from "path";
|
|
1197
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync4, readdirSync, statSync, unlinkSync, copyFileSync, rmSync as rmSync3 } from "fs";
|
|
1198
|
+
import { resolve as resolve3, join as join2, basename, dirname as dirname4 } from "path";
|
|
854
1199
|
import { homedir as homedir3 } from "os";
|
|
855
1200
|
function dbBackupsRoot(home = homedir3()) {
|
|
856
1201
|
return resolve3(home, ".massu", "db-backups");
|
|
857
1202
|
}
|
|
858
1203
|
function projectBackupDir(projectRoot, home = homedir3()) {
|
|
859
1204
|
const slug = basename(projectRoot).replace(/[^A-Za-z0-9._-]/g, "_") || "project";
|
|
860
|
-
return
|
|
1205
|
+
return join2(dbBackupsRoot(home), slug);
|
|
861
1206
|
}
|
|
862
1207
|
function backupStamp(nowMs) {
|
|
863
1208
|
return new Date(nowMs).toISOString().replace(/[:.]/g, "-");
|
|
864
1209
|
}
|
|
865
1210
|
function listDbBackups(projectRoot, home = homedir3()) {
|
|
866
1211
|
const dir = projectBackupDir(projectRoot, home);
|
|
867
|
-
if (!
|
|
1212
|
+
if (!existsSync4(dir)) return [];
|
|
868
1213
|
const out = [];
|
|
869
1214
|
for (const f of readdirSync(dir)) {
|
|
870
1215
|
if (!f.endsWith(".db")) continue;
|
|
871
|
-
const p =
|
|
1216
|
+
const p = join2(dir, f);
|
|
872
1217
|
const st = statSync(p);
|
|
873
1218
|
const db = f.replace(/-\d{4}-\d{2}-\d{2}T.*$/, "");
|
|
874
1219
|
out.push({ db, path: p, bytes: st.size, mtimeMs: st.mtimeMs });
|
|
@@ -881,13 +1226,13 @@ function hasFreshDbBackup(projectRoot, dbPath, nowMs = Date.now(), home = homedi
|
|
|
881
1226
|
if (backups.length === 0) return false;
|
|
882
1227
|
const newest = backups[0];
|
|
883
1228
|
if (nowMs - newest.mtimeMs > FRESH_WINDOW_MS) return false;
|
|
884
|
-
if (
|
|
1229
|
+
if (existsSync4(dbPath) && statSync(dbPath).mtimeMs > newest.mtimeMs) return false;
|
|
885
1230
|
return true;
|
|
886
1231
|
}
|
|
887
1232
|
function integrityOk(dbPath) {
|
|
888
1233
|
let db = null;
|
|
889
1234
|
try {
|
|
890
|
-
db =
|
|
1235
|
+
db = openDatabase(dbPath, { readonly: true });
|
|
891
1236
|
const rows = db.pragma("integrity_check");
|
|
892
1237
|
return rows.length === 1 && rows[0].integrity_check === "ok";
|
|
893
1238
|
} catch {
|
|
@@ -900,16 +1245,16 @@ function integrityOk(dbPath) {
|
|
|
900
1245
|
}
|
|
901
1246
|
}
|
|
902
1247
|
function backupDb(projectRoot, dbPath, nowMs = Date.now(), home = homedir3()) {
|
|
903
|
-
if (!
|
|
1248
|
+
if (!existsSync4(dbPath)) {
|
|
904
1249
|
throw new DbBackupError(`cannot back up a database that does not exist: ${dbPath}`);
|
|
905
1250
|
}
|
|
906
1251
|
const dir = projectBackupDir(projectRoot, home);
|
|
907
|
-
|
|
1252
|
+
mkdirSync4(dir, { recursive: true });
|
|
908
1253
|
const name = basename(dbPath, ".db");
|
|
909
|
-
const dest =
|
|
1254
|
+
const dest = join2(dir, `${name}-${backupStamp(nowMs)}.db`);
|
|
910
1255
|
let src = null;
|
|
911
1256
|
try {
|
|
912
|
-
src =
|
|
1257
|
+
src = openDatabase(dbPath, { readonly: true });
|
|
913
1258
|
src.exec(`VACUUM INTO '${dest.replace(/'/g, "''")}'`);
|
|
914
1259
|
} catch (err) {
|
|
915
1260
|
throw new DbBackupError(
|
|
@@ -948,7 +1293,7 @@ function pruneDbBackups(projectRoot, dbName, keep = DEFAULT_RETENTION, home = ho
|
|
|
948
1293
|
}
|
|
949
1294
|
function backupBeforeSchemaChange(projectRoot, dbPath, onError, nowMs = Date.now(), home = homedir3()) {
|
|
950
1295
|
try {
|
|
951
|
-
if (!
|
|
1296
|
+
if (!existsSync4(dbPath)) return null;
|
|
952
1297
|
if (hasFreshDbBackup(projectRoot, dbPath, nowMs, home)) return null;
|
|
953
1298
|
return backupDb(projectRoot, dbPath, nowMs, home);
|
|
954
1299
|
} catch (err) {
|
|
@@ -960,6 +1305,7 @@ var DEFAULT_RETENTION, FRESH_WINDOW_MS, DbBackupError;
|
|
|
960
1305
|
var init_db_backup = __esm({
|
|
961
1306
|
"src/db-backup.ts"() {
|
|
962
1307
|
"use strict";
|
|
1308
|
+
init_sqlite_loader();
|
|
963
1309
|
DEFAULT_RETENTION = 5;
|
|
964
1310
|
FRESH_WINDOW_MS = 24 * 60 * 60 * 1e3;
|
|
965
1311
|
DbBackupError = class extends Error {
|
|
@@ -1219,21 +1565,21 @@ var init_rule_candidate_store = __esm({
|
|
|
1219
1565
|
});
|
|
1220
1566
|
|
|
1221
1567
|
// src/hooks/lib/hook-failure-signal.ts
|
|
1222
|
-
import { appendFileSync, mkdirSync as
|
|
1223
|
-
import { join as
|
|
1568
|
+
import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync5, existsSync as existsSync5 } from "fs";
|
|
1569
|
+
import { join as join3, dirname as dirname5 } from "path";
|
|
1224
1570
|
function resolveFailureLogPath() {
|
|
1225
1571
|
const explicit = process.env.MASSU_HOOK_FAILURE_LOG;
|
|
1226
1572
|
if (explicit) return explicit;
|
|
1227
1573
|
let dir = process.cwd();
|
|
1228
1574
|
for (let i = 0; i < 12; i++) {
|
|
1229
|
-
if (
|
|
1230
|
-
return
|
|
1575
|
+
if (existsSync5(join3(dir, ".massu")) || existsSync5(join3(dir, "massu.config.yaml"))) {
|
|
1576
|
+
return join3(dir, ".massu", "hook-failures.jsonl");
|
|
1231
1577
|
}
|
|
1232
|
-
const parent =
|
|
1578
|
+
const parent = dirname5(dir);
|
|
1233
1579
|
if (parent === dir) break;
|
|
1234
1580
|
dir = parent;
|
|
1235
1581
|
}
|
|
1236
|
-
return
|
|
1582
|
+
return join3(process.cwd(), ".massu", "hook-failures.jsonl");
|
|
1237
1583
|
}
|
|
1238
1584
|
function recordHookFailure(hook, error, context) {
|
|
1239
1585
|
const record = {
|
|
@@ -1246,8 +1592,8 @@ function recordHookFailure(hook, error, context) {
|
|
|
1246
1592
|
let wroteSomething = false;
|
|
1247
1593
|
try {
|
|
1248
1594
|
const path = resolveFailureLogPath();
|
|
1249
|
-
|
|
1250
|
-
|
|
1595
|
+
mkdirSync5(dirname5(path), { recursive: true });
|
|
1596
|
+
appendFileSync2(path, JSON.stringify(record) + "\n", "utf-8");
|
|
1251
1597
|
wroteSomething = true;
|
|
1252
1598
|
} catch {
|
|
1253
1599
|
}
|
|
@@ -1290,9 +1636,9 @@ var init_hook_failure_signal = __esm({
|
|
|
1290
1636
|
});
|
|
1291
1637
|
|
|
1292
1638
|
// src/memory-embedder-tokenizer.ts
|
|
1293
|
-
import { readFileSync as
|
|
1639
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
1294
1640
|
function loadVocab(vocabPath) {
|
|
1295
|
-
const lines =
|
|
1641
|
+
const lines = readFileSync4(vocabPath, "utf-8").split("\n");
|
|
1296
1642
|
const vocab = /* @__PURE__ */ new Map();
|
|
1297
1643
|
for (let i = 0; i < lines.length; i++) {
|
|
1298
1644
|
const tok = lines[i].replace(/\r$/, "");
|
|
@@ -1412,9 +1758,9 @@ var init_memory_embedder_tokenizer = __esm({
|
|
|
1412
1758
|
|
|
1413
1759
|
// src/memory-embedder.ts
|
|
1414
1760
|
import { fileURLToPath } from "url";
|
|
1415
|
-
import { dirname as
|
|
1416
|
-
import { existsSync as
|
|
1417
|
-
import { createRequire } from "module";
|
|
1761
|
+
import { dirname as dirname6, join as join4 } from "path";
|
|
1762
|
+
import { existsSync as existsSync6 } from "fs";
|
|
1763
|
+
import { createRequire as createRequire2 } from "module";
|
|
1418
1764
|
function getActiveEmbedModel() {
|
|
1419
1765
|
return _activeModel;
|
|
1420
1766
|
}
|
|
@@ -1435,21 +1781,21 @@ function embeddingsDisabled() {
|
|
|
1435
1781
|
}
|
|
1436
1782
|
function resolveModelDir() {
|
|
1437
1783
|
try {
|
|
1438
|
-
let dir =
|
|
1784
|
+
let dir = dirname6(fileURLToPath(import.meta.url));
|
|
1439
1785
|
let root = null;
|
|
1440
1786
|
for (let i = 0; i < 8; i++) {
|
|
1441
|
-
if (
|
|
1787
|
+
if (existsSync6(join4(dir, "package.json"))) {
|
|
1442
1788
|
root = dir;
|
|
1443
1789
|
break;
|
|
1444
1790
|
}
|
|
1445
|
-
const parent =
|
|
1791
|
+
const parent = dirname6(dir);
|
|
1446
1792
|
if (parent === dir) break;
|
|
1447
1793
|
dir = parent;
|
|
1448
1794
|
}
|
|
1449
1795
|
if (!root) return null;
|
|
1450
1796
|
for (const rel of ["dist/embedder", "assets/embedder"]) {
|
|
1451
|
-
const candidate =
|
|
1452
|
-
if (
|
|
1797
|
+
const candidate = join4(root, rel);
|
|
1798
|
+
if (existsSync6(join4(candidate, "model_quantized.onnx"))) return candidate;
|
|
1453
1799
|
}
|
|
1454
1800
|
return null;
|
|
1455
1801
|
} catch {
|
|
@@ -1458,9 +1804,9 @@ function resolveModelDir() {
|
|
|
1458
1804
|
}
|
|
1459
1805
|
function resolveWasmDir() {
|
|
1460
1806
|
try {
|
|
1461
|
-
const
|
|
1462
|
-
const main2 =
|
|
1463
|
-
return
|
|
1807
|
+
const req2 = createRequire2(import.meta.url);
|
|
1808
|
+
const main2 = req2.resolve(ORT_PKG);
|
|
1809
|
+
return dirname6(main2);
|
|
1464
1810
|
} catch {
|
|
1465
1811
|
return null;
|
|
1466
1812
|
}
|
|
@@ -1480,8 +1826,8 @@ async function getSession() {
|
|
|
1480
1826
|
ort.env.wasm.proxy = false;
|
|
1481
1827
|
ort.env.wasm.wasmPaths = wasmDir.endsWith("/") ? wasmDir : wasmDir + "/";
|
|
1482
1828
|
_ortTensor = ort.Tensor;
|
|
1483
|
-
const modelPath =
|
|
1484
|
-
const vocabPath =
|
|
1829
|
+
const modelPath = join4(modelDir, "model_quantized.onnx");
|
|
1830
|
+
const vocabPath = join4(modelDir, "vocab.txt");
|
|
1485
1831
|
_vocab = loadVocab(vocabPath);
|
|
1486
1832
|
const session = await ort.InferenceSession.create(modelPath, {
|
|
1487
1833
|
executionProviders: ["wasm"],
|
|
@@ -1833,9 +2179,8 @@ __export(memory_db_exports, {
|
|
|
1833
2179
|
upsertObservationEmbedding: () => upsertObservationEmbedding,
|
|
1834
2180
|
usageWarmupElapsed: () => usageWarmupElapsed
|
|
1835
2181
|
});
|
|
1836
|
-
import
|
|
1837
|
-
import {
|
|
1838
|
-
import { existsSync as existsSync6, mkdirSync as mkdirSync4 } from "fs";
|
|
2182
|
+
import { dirname as dirname7, basename as basename2 } from "path";
|
|
2183
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync6 } from "fs";
|
|
1839
2184
|
function sanitizeFts5Query(raw) {
|
|
1840
2185
|
const trimmed = raw.trim();
|
|
1841
2186
|
if (!trimmed) return '""';
|
|
@@ -1851,12 +2196,12 @@ function sanitizeFts5QueryOr(raw) {
|
|
|
1851
2196
|
}
|
|
1852
2197
|
function getMemoryDb() {
|
|
1853
2198
|
const dbPath = getResolvedPaths().memoryDbPath;
|
|
1854
|
-
const dir =
|
|
1855
|
-
if (!
|
|
1856
|
-
|
|
2199
|
+
const dir = dirname7(dbPath);
|
|
2200
|
+
if (!existsSync7(dir)) {
|
|
2201
|
+
mkdirSync6(dir, { recursive: true });
|
|
1857
2202
|
}
|
|
1858
|
-
const preExisting =
|
|
1859
|
-
const db =
|
|
2203
|
+
const preExisting = existsSync7(dbPath);
|
|
2204
|
+
const db = openDatabase(dbPath);
|
|
1860
2205
|
db.pragma("journal_mode = WAL");
|
|
1861
2206
|
db.pragma("foreign_keys = ON");
|
|
1862
2207
|
const onDisk = db.pragma("user_version", { simple: true });
|
|
@@ -3562,6 +3907,7 @@ var MEMORY_SCHEMA_VERSION, TOOL_COST_EVENTS_RETENTION_DAYS, MAX_DRAFT_TEXT_LEN,
|
|
|
3562
3907
|
var init_memory_db = __esm({
|
|
3563
3908
|
"src/memory-db.ts"() {
|
|
3564
3909
|
"use strict";
|
|
3910
|
+
init_sqlite_loader();
|
|
3565
3911
|
init_config();
|
|
3566
3912
|
init_memory_vector();
|
|
3567
3913
|
init_db_backup();
|
|
@@ -3586,8 +3932,8 @@ init_memory_db();
|
|
|
3586
3932
|
init_rule_delivery();
|
|
3587
3933
|
|
|
3588
3934
|
// src/session-archiver.ts
|
|
3589
|
-
import { existsSync as
|
|
3590
|
-
import { resolve as resolve5, dirname as
|
|
3935
|
+
import { existsSync as existsSync8, readFileSync as readFileSync5, writeFileSync as writeFileSync3, mkdirSync as mkdirSync7, renameSync } from "fs";
|
|
3936
|
+
import { resolve as resolve5, dirname as dirname8 } from "path";
|
|
3591
3937
|
|
|
3592
3938
|
// src/session-state-generator.ts
|
|
3593
3939
|
function generateCurrentMd(db, sessionId) {
|
|
@@ -3739,29 +4085,29 @@ function archiveAndRegenerate(db, sessionId) {
|
|
|
3739
4085
|
const archiveDir = resolved.sessionArchivePath;
|
|
3740
4086
|
let archived = false;
|
|
3741
4087
|
let archivePath;
|
|
3742
|
-
if (
|
|
3743
|
-
const existingContent =
|
|
4088
|
+
if (existsSync8(currentMdPath)) {
|
|
4089
|
+
const existingContent = readFileSync5(currentMdPath, "utf-8");
|
|
3744
4090
|
if (existingContent.trim().length > 10) {
|
|
3745
4091
|
const { date, slug } = extractArchiveInfo(existingContent);
|
|
3746
4092
|
archivePath = resolve5(archiveDir, `${date}-${slug}.md`);
|
|
3747
|
-
if (!
|
|
3748
|
-
|
|
4093
|
+
if (!existsSync8(archiveDir)) {
|
|
4094
|
+
mkdirSync7(archiveDir, { recursive: true });
|
|
3749
4095
|
}
|
|
3750
4096
|
try {
|
|
3751
4097
|
renameSync(currentMdPath, archivePath);
|
|
3752
4098
|
archived = true;
|
|
3753
4099
|
} catch (_e) {
|
|
3754
|
-
|
|
4100
|
+
writeFileSync3(archivePath, existingContent);
|
|
3755
4101
|
archived = true;
|
|
3756
4102
|
}
|
|
3757
4103
|
}
|
|
3758
4104
|
}
|
|
3759
4105
|
const newContent = generateCurrentMd(db, sessionId);
|
|
3760
|
-
const dir =
|
|
3761
|
-
if (!
|
|
3762
|
-
|
|
4106
|
+
const dir = dirname8(currentMdPath);
|
|
4107
|
+
if (!existsSync8(dir)) {
|
|
4108
|
+
mkdirSync7(dir, { recursive: true });
|
|
3763
4109
|
}
|
|
3764
|
-
|
|
4110
|
+
writeFileSync3(currentMdPath, newContent, "utf-8");
|
|
3765
4111
|
return { archived, archivePath, newContent };
|
|
3766
4112
|
}
|
|
3767
4113
|
function extractArchiveInfo(content) {
|
|
@@ -3903,7 +4249,7 @@ function classifyVisibility(title, detail) {
|
|
|
3903
4249
|
// src/cloud-sync.ts
|
|
3904
4250
|
var MAX_RETRIES = 3;
|
|
3905
4251
|
var RETRY_DELAYS = [1e3, 2e3, 4e3];
|
|
3906
|
-
var DEFAULT_CLOUD_REQUEST_TIMEOUT_MS =
|
|
4252
|
+
var DEFAULT_CLOUD_REQUEST_TIMEOUT_MS = 15e3;
|
|
3907
4253
|
var SYNC_DEADLINE_MS = 2e4;
|
|
3908
4254
|
var MIN_ATTEMPT_BUDGET_MS = 1e3;
|
|
3909
4255
|
async function syncToCloud(db, payload) {
|
|
@@ -4084,8 +4430,8 @@ function sleep(ms) {
|
|
|
4084
4430
|
|
|
4085
4431
|
// src/team-rule-sync.ts
|
|
4086
4432
|
init_config();
|
|
4087
|
-
import { existsSync as
|
|
4088
|
-
import { join as
|
|
4433
|
+
import { existsSync as existsSync9, writeFileSync as writeFileSync4, unlinkSync as unlinkSync2, mkdirSync as mkdirSync8 } from "fs";
|
|
4434
|
+
import { join as join5, dirname as dirname9 } from "path";
|
|
4089
4435
|
|
|
4090
4436
|
// src/license.ts
|
|
4091
4437
|
init_config();
|
|
@@ -4321,8 +4667,8 @@ function verifyPromotionEnvelope(payload) {
|
|
|
4321
4667
|
);
|
|
4322
4668
|
}
|
|
4323
4669
|
|
|
4324
|
-
// src/
|
|
4325
|
-
|
|
4670
|
+
// src/memory-index-region.ts
|
|
4671
|
+
init_fileLock();
|
|
4326
4672
|
|
|
4327
4673
|
// src/rule-candidate-applier.ts
|
|
4328
4674
|
init_config();
|
|
@@ -4459,7 +4805,7 @@ async function pullTeamPromotions(db, opts = {}) {
|
|
|
4459
4805
|
result.revoked_handled += 1;
|
|
4460
4806
|
continue;
|
|
4461
4807
|
}
|
|
4462
|
-
if (
|
|
4808
|
+
if (existsSync9(candidatePath) || alreadyApplied(db, p.prompt_hash)) {
|
|
4463
4809
|
result.skipped += 1;
|
|
4464
4810
|
continue;
|
|
4465
4811
|
}
|
|
@@ -4495,7 +4841,7 @@ function isValidWirePromotion(p) {
|
|
|
4495
4841
|
return typeof r.prompt_hash === "string" && PROMPT_HASH_RE.test(r.prompt_hash) && typeof r.destination === "string" && typeof r.draft_text === "string" && typeof r.promoted_by === "string" && typeof r.promoted_at === "string";
|
|
4496
4842
|
}
|
|
4497
4843
|
function sidecarPath(projectRoot, promptHash) {
|
|
4498
|
-
return
|
|
4844
|
+
return join5(projectRoot, ".massu", "rule-candidates", `${promptHash}.json`);
|
|
4499
4845
|
}
|
|
4500
4846
|
function alreadyApplied(db, promptHash) {
|
|
4501
4847
|
try {
|
|
@@ -4509,7 +4855,7 @@ function alreadyApplied(db, promptHash) {
|
|
|
4509
4855
|
}
|
|
4510
4856
|
}
|
|
4511
4857
|
function handleRevocation(db, projectRoot, candidatePath, promptHash) {
|
|
4512
|
-
if (
|
|
4858
|
+
if (existsSync9(candidatePath)) {
|
|
4513
4859
|
try {
|
|
4514
4860
|
unlinkSync2(candidatePath);
|
|
4515
4861
|
} catch {
|
|
@@ -4557,9 +4903,9 @@ function materializeCandidate(db, projectRoot, candidatePath, p, orgId) {
|
|
|
4557
4903
|
draft_text: p.draft_text,
|
|
4558
4904
|
...p.review_attestation !== void 0 ? { publisher_review_attestation: p.review_attestation } : {}
|
|
4559
4905
|
};
|
|
4560
|
-
const dir =
|
|
4561
|
-
if (!
|
|
4562
|
-
|
|
4906
|
+
const dir = dirname9(candidatePath);
|
|
4907
|
+
if (!existsSync9(dir)) mkdirSync8(dir, { recursive: true });
|
|
4908
|
+
writeFileSync4(candidatePath, JSON.stringify(sidecar, null, 2), "utf-8");
|
|
4563
4909
|
try {
|
|
4564
4910
|
shareObservation(db, p.promoted_by, getProjectName(), "rule_promotion", promptText, {
|
|
4565
4911
|
filePath: void 0,
|
|
@@ -5399,8 +5745,8 @@ ${d.decision}`.trim(),
|
|
|
5399
5745
|
// src/memory-consolidate.ts
|
|
5400
5746
|
init_memory_db();
|
|
5401
5747
|
import { createHash as createHash3 } from "crypto";
|
|
5402
|
-
import { existsSync as
|
|
5403
|
-
import { join as
|
|
5748
|
+
import { existsSync as existsSync10, mkdirSync as mkdirSync9, writeFileSync as writeFileSync5 } from "fs";
|
|
5749
|
+
import { join as join6 } from "path";
|
|
5404
5750
|
|
|
5405
5751
|
// src/consolidation-config.ts
|
|
5406
5752
|
init_config();
|
|
@@ -5718,7 +6064,7 @@ ${r.detail ?? ""}`.trim());
|
|
|
5718
6064
|
}
|
|
5719
6065
|
clusters.push(cluster);
|
|
5720
6066
|
}
|
|
5721
|
-
const candidateDir =
|
|
6067
|
+
const candidateDir = join6(projectRoot, ".massu", "rule-candidates");
|
|
5722
6068
|
let promoted = 0;
|
|
5723
6069
|
for (const cluster of clusters) {
|
|
5724
6070
|
const occurrences = cluster.reduce((n, c) => n + (c.recurrence_count || 1), 0);
|
|
@@ -5731,14 +6077,14 @@ ${r.detail ?? ""}`.trim());
|
|
|
5731
6077
|
${representative.detail}` : ""}`;
|
|
5732
6078
|
const clusterKey = cluster.map((c) => c.title.toLowerCase().replace(/\s+/g, " ").trim()).sort().join("|");
|
|
5733
6079
|
const promptHash = createHash3("sha256").update(clusterKey).digest("hex").slice(0, 16);
|
|
5734
|
-
const candidatePath =
|
|
5735
|
-
if (
|
|
6080
|
+
const candidatePath = join6(candidateDir, `${promptHash}.json`);
|
|
6081
|
+
if (existsSync10(candidatePath)) continue;
|
|
5736
6082
|
if (dryRun) {
|
|
5737
6083
|
promoted++;
|
|
5738
6084
|
continue;
|
|
5739
6085
|
}
|
|
5740
|
-
|
|
5741
|
-
|
|
6086
|
+
mkdirSync9(candidateDir, { recursive: true });
|
|
6087
|
+
writeFileSync5(
|
|
5742
6088
|
candidatePath,
|
|
5743
6089
|
JSON.stringify(
|
|
5744
6090
|
{
|