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