@massu/core 1.16.2 → 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +158 -0
- package/dist/cli.js +2266 -863
- package/dist/hooks/auto-learning-pipeline.js +622 -60
- package/dist/hooks/classify-failure.js +622 -60
- package/dist/hooks/cost-tracker.js +631 -69
- package/dist/hooks/fix-detector.js +631 -69
- package/dist/hooks/incident-pipeline.js +616 -54
- package/dist/hooks/intent-suggester.js +631 -69
- package/dist/hooks/memory-recall.js +771 -75
- package/dist/hooks/post-edit-context.js +633 -71
- package/dist/hooks/post-tool-use.js +659 -197
- package/dist/hooks/pre-compact.js +631 -69
- package/dist/hooks/pre-delete-check.js +635 -73
- package/dist/hooks/pre-tool-use-gate.js +635 -73
- package/dist/hooks/quality-event.js +631 -69
- package/dist/hooks/rule-enforcement-pipeline.js +617 -55
- package/dist/hooks/security-gate.js +631 -69
- package/dist/hooks/session-end.js +1729 -281
- package/dist/hooks/session-start.js +1018 -425
- package/dist/hooks/user-prompt.js +666 -104
- package/package.json +2 -2
|
@@ -26,16 +26,6 @@ var __copyProps = (to, from, except, desc) => {
|
|
|
26
26
|
};
|
|
27
27
|
var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
|
|
28
28
|
|
|
29
|
-
// src/lib/memory-path.ts
|
|
30
|
-
function encodeMemoryDirName(projectRoot) {
|
|
31
|
-
return projectRoot.replace(/\//g, "-");
|
|
32
|
-
}
|
|
33
|
-
var init_memory_path = __esm({
|
|
34
|
-
"src/lib/memory-path.ts"() {
|
|
35
|
-
"use strict";
|
|
36
|
-
}
|
|
37
|
-
});
|
|
38
|
-
|
|
39
29
|
// src/credentials.ts
|
|
40
30
|
import { homedir } from "os";
|
|
41
31
|
import { resolve } from "path";
|
|
@@ -104,6 +94,475 @@ var init_credentials = __esm({
|
|
|
104
94
|
}
|
|
105
95
|
});
|
|
106
96
|
|
|
97
|
+
// src/lib/fileLock.ts
|
|
98
|
+
import { mkdirSync as mkdirSync2, readFileSync as readFileSync2, rmSync as rmSync2, writeFileSync as writeFileSync2 } from "fs";
|
|
99
|
+
import { dirname } from "path";
|
|
100
|
+
import * as lockfile from "proper-lockfile";
|
|
101
|
+
function readLockHolderPid(lockPath) {
|
|
102
|
+
try {
|
|
103
|
+
const raw = readFileSync2(`${lockPath}.pid`, "utf-8").trim();
|
|
104
|
+
const pid = Number.parseInt(raw, 10);
|
|
105
|
+
if (!Number.isFinite(pid) || pid <= 0) return null;
|
|
106
|
+
return pid;
|
|
107
|
+
} catch {
|
|
108
|
+
return null;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
function busyWaitSync(ms) {
|
|
112
|
+
if (typeof SharedArrayBuffer === "undefined" || typeof Atomics === "undefined") {
|
|
113
|
+
throw new Error(
|
|
114
|
+
`withFileLockSync requires SharedArrayBuffer + Atomics for its retry-loop wait. This Node runtime does not provide them \u2014 refusing to fall back to a CPU spinloop. If you hit this in a sandboxed serverless env, the fix is to perform the lock-protected operation in a host runtime that supports Atomics.`
|
|
115
|
+
);
|
|
116
|
+
}
|
|
117
|
+
const sab = new SharedArrayBuffer(4);
|
|
118
|
+
const view = new Int32Array(sab);
|
|
119
|
+
Atomics.wait(view, 0, 0, ms);
|
|
120
|
+
}
|
|
121
|
+
function withFileLockSync(lockPath, fn, opts = {}) {
|
|
122
|
+
mkdirSync2(dirname(lockPath), { recursive: true });
|
|
123
|
+
const staleMs = opts.staleMs ?? 3e4;
|
|
124
|
+
const blockMs = opts.retries === 0 ? 0 : opts.blockMs ?? 3e4;
|
|
125
|
+
const pollIntervalMs = opts.pollIntervalMs ?? 100;
|
|
126
|
+
const now = opts.now ?? Date.now;
|
|
127
|
+
const 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/db-driver.ts
|
|
427
|
+
import { createRequire as createRequire2 } from "module";
|
|
428
|
+
import { closeSync, existsSync as existsSync3, mkdtempSync, openSync, readFileSync as readFileSync3, rmSync as rmSync3, fsyncSync } from "fs";
|
|
429
|
+
import { tmpdir } from "os";
|
|
430
|
+
import { join as join2 } from "path";
|
|
431
|
+
function resolveDbEngine() {
|
|
432
|
+
return process.env[DB_ENGINE_ENV] === "better-sqlite3" ? "better-sqlite3" : DEFAULT_DB_ENGINE;
|
|
433
|
+
}
|
|
434
|
+
function nodeSqliteCtor() {
|
|
435
|
+
if (!_nodeCtor) _nodeCtor = req2("node:sqlite").DatabaseSync;
|
|
436
|
+
return _nodeCtor;
|
|
437
|
+
}
|
|
438
|
+
function plainRow(row) {
|
|
439
|
+
if (row == null || typeof row !== "object") return row;
|
|
440
|
+
return { ...row };
|
|
441
|
+
}
|
|
442
|
+
function openNodeSqlite(dbPath, opts) {
|
|
443
|
+
const Ctor = nodeSqliteCtor();
|
|
444
|
+
const raw = new Ctor(dbPath, {
|
|
445
|
+
open: true,
|
|
446
|
+
readOnly: !!opts.readonly,
|
|
447
|
+
// FAITHFULNESS: better-sqlite3's ACTUAL default is foreign_keys=ON (empirically
|
|
448
|
+
// verified — SQLite's own default is OFF, but bs3 enables it). massu was written +
|
|
449
|
+
// tested against that default, so the adapter must match it, or a store that relies
|
|
450
|
+
// on FK-ON-by-default (without an explicit pragma) would silently lose enforcement.
|
|
451
|
+
// Explicit `foreign_keys=OFF/ON` pragmas (e.g. memory-db bulk ops) still override.
|
|
452
|
+
enableForeignKeyConstraints: true,
|
|
453
|
+
allowExtension: false
|
|
454
|
+
});
|
|
455
|
+
let savepointSeq = 0;
|
|
456
|
+
const wrapStmt = (sql) => {
|
|
457
|
+
const st = raw.prepare(sql);
|
|
458
|
+
return {
|
|
459
|
+
run: (...p) => {
|
|
460
|
+
const r = st.run(...p);
|
|
461
|
+
return {
|
|
462
|
+
changes: typeof r.changes === "bigint" ? Number(r.changes) : r.changes,
|
|
463
|
+
lastInsertRowid: r.lastInsertRowid
|
|
464
|
+
};
|
|
465
|
+
},
|
|
466
|
+
get: (...p) => plainRow(st.get(...p)),
|
|
467
|
+
all: (...p) => st.all(...p).map(plainRow)
|
|
468
|
+
};
|
|
469
|
+
};
|
|
470
|
+
const pragma = (source, options) => {
|
|
471
|
+
const s = String(source).trim();
|
|
472
|
+
if (s.includes("=")) {
|
|
473
|
+
raw.exec(`PRAGMA ${s}`);
|
|
474
|
+
return void 0;
|
|
475
|
+
}
|
|
476
|
+
const rows = raw.prepare(`PRAGMA ${s}`).all().map(plainRow);
|
|
477
|
+
if (options?.simple) {
|
|
478
|
+
const first = rows[0];
|
|
479
|
+
return first ? Object.values(first)[0] : void 0;
|
|
480
|
+
}
|
|
481
|
+
return rows;
|
|
482
|
+
};
|
|
483
|
+
const transaction = (fn) => {
|
|
484
|
+
const run = (...args) => {
|
|
485
|
+
const nested = raw.isTransaction;
|
|
486
|
+
const name = `msp_${savepointSeq++}`;
|
|
487
|
+
if (nested) raw.exec(`SAVEPOINT ${name}`);
|
|
488
|
+
else raw.exec("BEGIN");
|
|
489
|
+
try {
|
|
490
|
+
const out = fn(...args);
|
|
491
|
+
if (nested) raw.exec(`RELEASE ${name}`);
|
|
492
|
+
else raw.exec("COMMIT");
|
|
493
|
+
return out;
|
|
494
|
+
} catch (e) {
|
|
495
|
+
if (nested) {
|
|
496
|
+
raw.exec(`ROLLBACK TO ${name}`);
|
|
497
|
+
raw.exec(`RELEASE ${name}`);
|
|
498
|
+
} else {
|
|
499
|
+
raw.exec("ROLLBACK");
|
|
500
|
+
}
|
|
501
|
+
throw e;
|
|
502
|
+
}
|
|
503
|
+
};
|
|
504
|
+
return run;
|
|
505
|
+
};
|
|
506
|
+
const serialize = () => {
|
|
507
|
+
if (raw.isTransaction) {
|
|
508
|
+
throw new Error(
|
|
509
|
+
"db-driver: .serialize() (node:sqlite engine) cannot run inside an open transaction \u2014 VACUUM is forbidden in a transaction. Serialize before BEGIN, or use MASSU_DB_ENGINE=better-sqlite3 for mid-transaction snapshots."
|
|
510
|
+
);
|
|
511
|
+
}
|
|
512
|
+
const dir = mkdtempSync(join2(tmpdir(), "massu-serialize-"));
|
|
513
|
+
const out = join2(dir, "snapshot.db");
|
|
514
|
+
try {
|
|
515
|
+
raw.exec(`VACUUM INTO '${out.replace(/'/g, "''")}'`);
|
|
516
|
+
const fd = openSync(out, "r");
|
|
517
|
+
try {
|
|
518
|
+
fsyncSync(fd);
|
|
519
|
+
} finally {
|
|
520
|
+
closeSync(fd);
|
|
521
|
+
}
|
|
522
|
+
return readFileSync3(out);
|
|
523
|
+
} finally {
|
|
524
|
+
rmSync3(dir, { recursive: true, force: true });
|
|
525
|
+
}
|
|
526
|
+
};
|
|
527
|
+
const handle = {
|
|
528
|
+
prepare: (sql) => wrapStmt(sql),
|
|
529
|
+
exec: (sql) => raw.exec(sql),
|
|
530
|
+
pragma,
|
|
531
|
+
transaction,
|
|
532
|
+
serialize,
|
|
533
|
+
close: () => raw.close()
|
|
534
|
+
};
|
|
535
|
+
return handle;
|
|
536
|
+
}
|
|
537
|
+
function openBetterSqlite3(dbPath, opts) {
|
|
538
|
+
return openDatabase(dbPath, opts);
|
|
539
|
+
}
|
|
540
|
+
function openDatabase2(dbPath, opts = {}) {
|
|
541
|
+
return resolveDbEngine() === "better-sqlite3" ? openBetterSqlite3(dbPath, opts) : openNodeSqlite(dbPath, opts);
|
|
542
|
+
}
|
|
543
|
+
var req2, DEFAULT_DB_ENGINE, DB_ENGINE_ENV, _nodeCtor;
|
|
544
|
+
var init_db_driver = __esm({
|
|
545
|
+
"src/db-driver.ts"() {
|
|
546
|
+
"use strict";
|
|
547
|
+
init_sqlite_loader();
|
|
548
|
+
init_sqlite_loader();
|
|
549
|
+
req2 = createRequire2(import.meta.url);
|
|
550
|
+
DEFAULT_DB_ENGINE = "node-sqlite";
|
|
551
|
+
DB_ENGINE_ENV = "MASSU_DB_ENGINE";
|
|
552
|
+
_nodeCtor = null;
|
|
553
|
+
}
|
|
554
|
+
});
|
|
555
|
+
|
|
556
|
+
// src/lib/memory-path.ts
|
|
557
|
+
function encodeMemoryDirName(projectRoot) {
|
|
558
|
+
return projectRoot.replace(/\//g, "-");
|
|
559
|
+
}
|
|
560
|
+
var init_memory_path = __esm({
|
|
561
|
+
"src/lib/memory-path.ts"() {
|
|
562
|
+
"use strict";
|
|
563
|
+
}
|
|
564
|
+
});
|
|
565
|
+
|
|
107
566
|
// src/config-memory-schema.ts
|
|
108
567
|
import { z } from "zod";
|
|
109
568
|
var MemoryConfigSchema;
|
|
@@ -237,14 +696,40 @@ var init_config_memory_schema = __esm({
|
|
|
237
696
|
// turn of EVERY session, so an unbounded index is a permanent context tax — and the
|
|
238
697
|
// per-session cap bounds only the RATE, never the total.
|
|
239
698
|
indexMaxLines: z.number().int().min(1).default(50)
|
|
699
|
+
}).default({}),
|
|
700
|
+
// --- Cross-repo memory surfacing (plan-living-memory-slice-5-cross-repo-surfacing) ---
|
|
701
|
+
// A decision made in one of your repos can surface in another — opt-in per
|
|
702
|
+
// decision AND opt-in per repo, signed, verified, and materialized ONLY on
|
|
703
|
+
// explicit human acceptance. Local transport is FREE and zero-network.
|
|
704
|
+
//
|
|
705
|
+
// ⛔ BOTH switches default OFF, and OFF means NOTHING EXISTS — no registry, no
|
|
706
|
+
// keys, no inbox, no behavioural difference. Two INDEPENDENT opt-ins:
|
|
707
|
+
// • enabled — may this repo EXPORT its shareable decisions? (opt-in #1)
|
|
708
|
+
// • subscribe — which repo LABELS may this repo IMPORT from? (opt-in #2)
|
|
709
|
+
// `subscribe: []` means import NOTHING. There is deliberately NO `subscribe: all`
|
|
710
|
+
// — a repo you never named is a repo Massu never reads from.
|
|
711
|
+
share: z.object({
|
|
712
|
+
enabled: z.boolean().default(false),
|
|
713
|
+
subscribe: z.array(z.string()).default([]),
|
|
714
|
+
// C-04 — recall surfacing of cross-repo memories. `enabled` defaults true but is
|
|
715
|
+
// CONDITIONAL on `subscribe` being non-empty (empty by default), so the effective
|
|
716
|
+
// default is DORMANT: with `subscribe: []` the recall hook output is byte-identical
|
|
717
|
+
// to today's. `maxCrossRepoItems` caps how many accepted cross-repo items may appear
|
|
718
|
+
// per recall block (default 1); `minScore` is an OPTIONAL strictly-higher floor for
|
|
719
|
+
// cross-repo items (they are, by construction, less relevant than local ones).
|
|
720
|
+
recall: z.object({
|
|
721
|
+
enabled: z.boolean().default(true),
|
|
722
|
+
maxCrossRepoItems: z.number().int().min(0).default(1),
|
|
723
|
+
minScore: z.number().optional()
|
|
724
|
+
}).default({})
|
|
240
725
|
}).default({})
|
|
241
726
|
}).optional();
|
|
242
727
|
}
|
|
243
728
|
});
|
|
244
729
|
|
|
245
730
|
// src/config.ts
|
|
246
|
-
import { resolve as resolve2, dirname } from "path";
|
|
247
|
-
import { existsSync as
|
|
731
|
+
import { resolve as resolve2, dirname as dirname3 } from "path";
|
|
732
|
+
import { existsSync as existsSync4, readFileSync as readFileSync4 } from "fs";
|
|
248
733
|
import { homedir as homedir2 } from "os";
|
|
249
734
|
import { parse as parseYaml } from "yaml";
|
|
250
735
|
import { z as z2 } from "zod";
|
|
@@ -252,22 +737,22 @@ function findProjectRoot() {
|
|
|
252
737
|
const cwd = process.cwd();
|
|
253
738
|
let dir = cwd;
|
|
254
739
|
while (true) {
|
|
255
|
-
if (
|
|
740
|
+
if (existsSync4(resolve2(dir, "massu.config.yaml"))) {
|
|
256
741
|
return dir;
|
|
257
742
|
}
|
|
258
|
-
const parent =
|
|
743
|
+
const parent = dirname3(dir);
|
|
259
744
|
if (parent === dir) break;
|
|
260
745
|
dir = parent;
|
|
261
746
|
}
|
|
262
747
|
dir = cwd;
|
|
263
748
|
while (true) {
|
|
264
|
-
if (
|
|
749
|
+
if (existsSync4(resolve2(dir, "package.json"))) {
|
|
265
750
|
return dir;
|
|
266
751
|
}
|
|
267
|
-
if (
|
|
752
|
+
if (existsSync4(resolve2(dir, ".git"))) {
|
|
268
753
|
return dir;
|
|
269
754
|
}
|
|
270
|
-
const parent =
|
|
755
|
+
const parent = dirname3(dir);
|
|
271
756
|
if (parent === dir) break;
|
|
272
757
|
dir = parent;
|
|
273
758
|
}
|
|
@@ -284,8 +769,8 @@ function getConfig() {
|
|
|
284
769
|
const root = getProjectRoot();
|
|
285
770
|
const configPath = resolve2(root, "massu.config.yaml");
|
|
286
771
|
let rawYaml = {};
|
|
287
|
-
if (
|
|
288
|
-
const content =
|
|
772
|
+
if (existsSync4(configPath)) {
|
|
773
|
+
const content = readFileSync4(configPath, "utf-8");
|
|
289
774
|
rawYaml = parseYaml(content) ?? {};
|
|
290
775
|
}
|
|
291
776
|
const result = RawConfigSchema.safeParse(rawYaml);
|
|
@@ -366,9 +851,13 @@ Hint: run \`massu config refresh\` to regenerate a valid config or fix the liste
|
|
|
366
851
|
const resolvedEndpoint = resolveEndpoint({ configEndpoint: _config.cloud?.endpoint });
|
|
367
852
|
if (resolvedKey.apiKey) {
|
|
368
853
|
_config.cloud = {
|
|
369
|
-
enabled: true,
|
|
370
854
|
sync: { memory: true, analytics: true, audit: true },
|
|
371
855
|
..._config.cloud,
|
|
856
|
+
// Spread FIRST, then decide `enabled`, so a workspace that declares a `cloud:`
|
|
857
|
+
// block only to tune `requestTimeoutMs` does not disable its own sync. An
|
|
858
|
+
// EXPLICIT `enabled: false` is still honoured; `undefined` (not stated) means
|
|
859
|
+
// "a key resolved, so turn it on". (plan-2026-07-20-cloud-sync-timeout)
|
|
860
|
+
enabled: _config.cloud?.enabled ?? true,
|
|
372
861
|
apiKey: resolvedKey.apiKey,
|
|
373
862
|
endpoint: resolvedEndpoint
|
|
374
863
|
};
|
|
@@ -486,7 +975,6 @@ var init_config = __esm({
|
|
|
486
975
|
custom_patterns: z2.array(CustomPatternSchema).default([])
|
|
487
976
|
}).optional(),
|
|
488
977
|
adr: z2.object({
|
|
489
|
-
detection_phrases: z2.array(z2.string()).default(["chose", "decided", "switching to", "moving from", "going with"]),
|
|
490
978
|
template: z2.string().default("default"),
|
|
491
979
|
storage: z2.string().default("database"),
|
|
492
980
|
output_dir: z2.string().default("docs/adr")
|
|
@@ -588,9 +1076,22 @@ var init_config = __esm({
|
|
|
588
1076
|
})).default([])
|
|
589
1077
|
}).optional();
|
|
590
1078
|
CloudConfigSchema = z2.object({
|
|
591
|
-
|
|
1079
|
+
// OPTIONAL, not `.default(false)`. A zod default is indistinguishable from an
|
|
1080
|
+
// explicit value once parsed, so `.default(false)` meant that merely declaring a
|
|
1081
|
+
// `cloud:` block (to set any OTHER key) silently produced `enabled: false`, which
|
|
1082
|
+
// then overrode the `enabled: true` auto-enable below via object spread — turning
|
|
1083
|
+
// cloud sync OFF as a side effect of tuning it. `undefined` now means "not stated",
|
|
1084
|
+
// and the auto-enable preserves it. (plan-2026-07-20-cloud-sync-timeout)
|
|
1085
|
+
enabled: z2.boolean().optional(),
|
|
592
1086
|
apiKey: z2.string().optional(),
|
|
593
1087
|
endpoint: z2.string().optional(),
|
|
1088
|
+
// Per-request POST budget for the `/sync` ingest. cloud-sync.ts reads this
|
|
1089
|
+
// (`(cloud as { requestTimeoutMs?: number }).requestTimeoutMs`) but it was ABSENT
|
|
1090
|
+
// from this schema, and zod strips unknown keys — so the knob was unreachable from
|
|
1091
|
+
// massu.config.yaml and the default could never be tuned. Measured 2026-07-20: a
|
|
1092
|
+
// 423-observation payload takes ~9.2s against the live ingest. Capped at
|
|
1093
|
+
// SYNC_DEADLINE_MS (20s) since the overall deadline clamps each attempt anyway.
|
|
1094
|
+
requestTimeoutMs: z2.number().int().positive().max(2e4).optional(),
|
|
594
1095
|
sync: z2.object({
|
|
595
1096
|
memory: z2.boolean().default(true),
|
|
596
1097
|
analytics: z2.boolean().default(true),
|
|
@@ -848,27 +1349,26 @@ var init_memory_vector = __esm({
|
|
|
848
1349
|
});
|
|
849
1350
|
|
|
850
1351
|
// src/db-backup.ts
|
|
851
|
-
import
|
|
852
|
-
import {
|
|
853
|
-
import { resolve as resolve3, join, basename, dirname as dirname2 } from "path";
|
|
1352
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync4, readdirSync, statSync, unlinkSync, copyFileSync, rmSync as rmSync4 } from "fs";
|
|
1353
|
+
import { resolve as resolve3, join as join3, basename, dirname as dirname4 } from "path";
|
|
854
1354
|
import { homedir as homedir3 } from "os";
|
|
855
1355
|
function dbBackupsRoot(home = homedir3()) {
|
|
856
1356
|
return resolve3(home, ".massu", "db-backups");
|
|
857
1357
|
}
|
|
858
1358
|
function projectBackupDir(projectRoot, home = homedir3()) {
|
|
859
1359
|
const slug = basename(projectRoot).replace(/[^A-Za-z0-9._-]/g, "_") || "project";
|
|
860
|
-
return
|
|
1360
|
+
return join3(dbBackupsRoot(home), slug);
|
|
861
1361
|
}
|
|
862
1362
|
function backupStamp(nowMs) {
|
|
863
1363
|
return new Date(nowMs).toISOString().replace(/[:.]/g, "-");
|
|
864
1364
|
}
|
|
865
1365
|
function listDbBackups(projectRoot, home = homedir3()) {
|
|
866
1366
|
const dir = projectBackupDir(projectRoot, home);
|
|
867
|
-
if (!
|
|
1367
|
+
if (!existsSync5(dir)) return [];
|
|
868
1368
|
const out = [];
|
|
869
1369
|
for (const f of readdirSync(dir)) {
|
|
870
1370
|
if (!f.endsWith(".db")) continue;
|
|
871
|
-
const p =
|
|
1371
|
+
const p = join3(dir, f);
|
|
872
1372
|
const st = statSync(p);
|
|
873
1373
|
const db = f.replace(/-\d{4}-\d{2}-\d{2}T.*$/, "");
|
|
874
1374
|
out.push({ db, path: p, bytes: st.size, mtimeMs: st.mtimeMs });
|
|
@@ -881,13 +1381,13 @@ function hasFreshDbBackup(projectRoot, dbPath, nowMs = Date.now(), home = homedi
|
|
|
881
1381
|
if (backups.length === 0) return false;
|
|
882
1382
|
const newest = backups[0];
|
|
883
1383
|
if (nowMs - newest.mtimeMs > FRESH_WINDOW_MS) return false;
|
|
884
|
-
if (
|
|
1384
|
+
if (existsSync5(dbPath) && statSync(dbPath).mtimeMs > newest.mtimeMs) return false;
|
|
885
1385
|
return true;
|
|
886
1386
|
}
|
|
887
1387
|
function integrityOk(dbPath) {
|
|
888
1388
|
let db = null;
|
|
889
1389
|
try {
|
|
890
|
-
db =
|
|
1390
|
+
db = openDatabase2(dbPath, { readonly: true });
|
|
891
1391
|
const rows = db.pragma("integrity_check");
|
|
892
1392
|
return rows.length === 1 && rows[0].integrity_check === "ok";
|
|
893
1393
|
} catch {
|
|
@@ -900,16 +1400,16 @@ function integrityOk(dbPath) {
|
|
|
900
1400
|
}
|
|
901
1401
|
}
|
|
902
1402
|
function backupDb(projectRoot, dbPath, nowMs = Date.now(), home = homedir3()) {
|
|
903
|
-
if (!
|
|
1403
|
+
if (!existsSync5(dbPath)) {
|
|
904
1404
|
throw new DbBackupError(`cannot back up a database that does not exist: ${dbPath}`);
|
|
905
1405
|
}
|
|
906
1406
|
const dir = projectBackupDir(projectRoot, home);
|
|
907
|
-
|
|
1407
|
+
mkdirSync4(dir, { recursive: true });
|
|
908
1408
|
const name = basename(dbPath, ".db");
|
|
909
|
-
const dest =
|
|
1409
|
+
const dest = join3(dir, `${name}-${backupStamp(nowMs)}.db`);
|
|
910
1410
|
let src = null;
|
|
911
1411
|
try {
|
|
912
|
-
src =
|
|
1412
|
+
src = openDatabase2(dbPath, { readonly: true });
|
|
913
1413
|
src.exec(`VACUUM INTO '${dest.replace(/'/g, "''")}'`);
|
|
914
1414
|
} catch (err) {
|
|
915
1415
|
throw new DbBackupError(
|
|
@@ -948,7 +1448,7 @@ function pruneDbBackups(projectRoot, dbName, keep = DEFAULT_RETENTION, home = ho
|
|
|
948
1448
|
}
|
|
949
1449
|
function backupBeforeSchemaChange(projectRoot, dbPath, onError, nowMs = Date.now(), home = homedir3()) {
|
|
950
1450
|
try {
|
|
951
|
-
if (!
|
|
1451
|
+
if (!existsSync5(dbPath)) return null;
|
|
952
1452
|
if (hasFreshDbBackup(projectRoot, dbPath, nowMs, home)) return null;
|
|
953
1453
|
return backupDb(projectRoot, dbPath, nowMs, home);
|
|
954
1454
|
} catch (err) {
|
|
@@ -960,6 +1460,7 @@ var DEFAULT_RETENTION, FRESH_WINDOW_MS, DbBackupError;
|
|
|
960
1460
|
var init_db_backup = __esm({
|
|
961
1461
|
"src/db-backup.ts"() {
|
|
962
1462
|
"use strict";
|
|
1463
|
+
init_db_driver();
|
|
963
1464
|
DEFAULT_RETENTION = 5;
|
|
964
1465
|
FRESH_WINDOW_MS = 24 * 60 * 60 * 1e3;
|
|
965
1466
|
DbBackupError = class extends Error {
|
|
@@ -1219,21 +1720,21 @@ var init_rule_candidate_store = __esm({
|
|
|
1219
1720
|
});
|
|
1220
1721
|
|
|
1221
1722
|
// src/hooks/lib/hook-failure-signal.ts
|
|
1222
|
-
import { appendFileSync, mkdirSync as
|
|
1223
|
-
import { join as
|
|
1723
|
+
import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync5, existsSync as existsSync6 } from "fs";
|
|
1724
|
+
import { join as join4, dirname as dirname5 } from "path";
|
|
1224
1725
|
function resolveFailureLogPath() {
|
|
1225
1726
|
const explicit = process.env.MASSU_HOOK_FAILURE_LOG;
|
|
1226
1727
|
if (explicit) return explicit;
|
|
1227
1728
|
let dir = process.cwd();
|
|
1228
1729
|
for (let i = 0; i < 12; i++) {
|
|
1229
|
-
if (
|
|
1230
|
-
return
|
|
1730
|
+
if (existsSync6(join4(dir, ".massu")) || existsSync6(join4(dir, "massu.config.yaml"))) {
|
|
1731
|
+
return join4(dir, ".massu", "hook-failures.jsonl");
|
|
1231
1732
|
}
|
|
1232
|
-
const parent =
|
|
1733
|
+
const parent = dirname5(dir);
|
|
1233
1734
|
if (parent === dir) break;
|
|
1234
1735
|
dir = parent;
|
|
1235
1736
|
}
|
|
1236
|
-
return
|
|
1737
|
+
return join4(process.cwd(), ".massu", "hook-failures.jsonl");
|
|
1237
1738
|
}
|
|
1238
1739
|
function recordHookFailure(hook, error, context) {
|
|
1239
1740
|
const record = {
|
|
@@ -1246,8 +1747,8 @@ function recordHookFailure(hook, error, context) {
|
|
|
1246
1747
|
let wroteSomething = false;
|
|
1247
1748
|
try {
|
|
1248
1749
|
const path = resolveFailureLogPath();
|
|
1249
|
-
|
|
1250
|
-
|
|
1750
|
+
mkdirSync5(dirname5(path), { recursive: true });
|
|
1751
|
+
appendFileSync2(path, JSON.stringify(record) + "\n", "utf-8");
|
|
1251
1752
|
wroteSomething = true;
|
|
1252
1753
|
} catch {
|
|
1253
1754
|
}
|
|
@@ -1290,9 +1791,9 @@ var init_hook_failure_signal = __esm({
|
|
|
1290
1791
|
});
|
|
1291
1792
|
|
|
1292
1793
|
// src/memory-embedder-tokenizer.ts
|
|
1293
|
-
import { readFileSync as
|
|
1794
|
+
import { readFileSync as readFileSync5 } from "fs";
|
|
1294
1795
|
function loadVocab(vocabPath) {
|
|
1295
|
-
const lines =
|
|
1796
|
+
const lines = readFileSync5(vocabPath, "utf-8").split("\n");
|
|
1296
1797
|
const vocab = /* @__PURE__ */ new Map();
|
|
1297
1798
|
for (let i = 0; i < lines.length; i++) {
|
|
1298
1799
|
const tok = lines[i].replace(/\r$/, "");
|
|
@@ -1412,9 +1913,9 @@ var init_memory_embedder_tokenizer = __esm({
|
|
|
1412
1913
|
|
|
1413
1914
|
// src/memory-embedder.ts
|
|
1414
1915
|
import { fileURLToPath } from "url";
|
|
1415
|
-
import { dirname as
|
|
1416
|
-
import { existsSync as
|
|
1417
|
-
import { createRequire } from "module";
|
|
1916
|
+
import { dirname as dirname6, join as join5 } from "path";
|
|
1917
|
+
import { existsSync as existsSync7 } from "fs";
|
|
1918
|
+
import { createRequire as createRequire3 } from "module";
|
|
1418
1919
|
function getActiveEmbedModel() {
|
|
1419
1920
|
return _activeModel;
|
|
1420
1921
|
}
|
|
@@ -1435,21 +1936,21 @@ function embeddingsDisabled() {
|
|
|
1435
1936
|
}
|
|
1436
1937
|
function resolveModelDir() {
|
|
1437
1938
|
try {
|
|
1438
|
-
let dir =
|
|
1939
|
+
let dir = dirname6(fileURLToPath(import.meta.url));
|
|
1439
1940
|
let root = null;
|
|
1440
1941
|
for (let i = 0; i < 8; i++) {
|
|
1441
|
-
if (
|
|
1942
|
+
if (existsSync7(join5(dir, "package.json"))) {
|
|
1442
1943
|
root = dir;
|
|
1443
1944
|
break;
|
|
1444
1945
|
}
|
|
1445
|
-
const parent =
|
|
1946
|
+
const parent = dirname6(dir);
|
|
1446
1947
|
if (parent === dir) break;
|
|
1447
1948
|
dir = parent;
|
|
1448
1949
|
}
|
|
1449
1950
|
if (!root) return null;
|
|
1450
1951
|
for (const rel of ["dist/embedder", "assets/embedder"]) {
|
|
1451
|
-
const candidate =
|
|
1452
|
-
if (
|
|
1952
|
+
const candidate = join5(root, rel);
|
|
1953
|
+
if (existsSync7(join5(candidate, "model_quantized.onnx"))) return candidate;
|
|
1453
1954
|
}
|
|
1454
1955
|
return null;
|
|
1455
1956
|
} catch {
|
|
@@ -1458,9 +1959,9 @@ function resolveModelDir() {
|
|
|
1458
1959
|
}
|
|
1459
1960
|
function resolveWasmDir() {
|
|
1460
1961
|
try {
|
|
1461
|
-
const
|
|
1462
|
-
const main2 =
|
|
1463
|
-
return
|
|
1962
|
+
const req3 = createRequire3(import.meta.url);
|
|
1963
|
+
const main2 = req3.resolve(ORT_PKG);
|
|
1964
|
+
return dirname6(main2);
|
|
1464
1965
|
} catch {
|
|
1465
1966
|
return null;
|
|
1466
1967
|
}
|
|
@@ -1480,8 +1981,8 @@ async function getSession() {
|
|
|
1480
1981
|
ort.env.wasm.proxy = false;
|
|
1481
1982
|
ort.env.wasm.wasmPaths = wasmDir.endsWith("/") ? wasmDir : wasmDir + "/";
|
|
1482
1983
|
_ortTensor = ort.Tensor;
|
|
1483
|
-
const modelPath =
|
|
1484
|
-
const vocabPath =
|
|
1984
|
+
const modelPath = join5(modelDir, "model_quantized.onnx");
|
|
1985
|
+
const vocabPath = join5(modelDir, "vocab.txt");
|
|
1485
1986
|
_vocab = loadVocab(vocabPath);
|
|
1486
1987
|
const session = await ort.InferenceSession.create(modelPath, {
|
|
1487
1988
|
executionProviders: ["wasm"],
|
|
@@ -1817,6 +2318,7 @@ __export(memory_db_exports, {
|
|
|
1817
2318
|
migrateAuditLogCheckExtension: () => migrateAuditLogCheckExtension,
|
|
1818
2319
|
migrateMemoryFilesFor4B: () => migrateMemoryFilesFor4B,
|
|
1819
2320
|
migrateObservationEmbeddingChunks: () => migrateObservationEmbeddingChunks,
|
|
2321
|
+
migrateSharedMemoryFor5B: () => migrateSharedMemoryFor5B,
|
|
1820
2322
|
pruneOldConversationTurns: () => pruneOldConversationTurns,
|
|
1821
2323
|
pruneOldObservations: () => pruneOldObservations,
|
|
1822
2324
|
pruneToolCostEvents: () => pruneToolCostEvents,
|
|
@@ -1833,9 +2335,8 @@ __export(memory_db_exports, {
|
|
|
1833
2335
|
upsertObservationEmbedding: () => upsertObservationEmbedding,
|
|
1834
2336
|
usageWarmupElapsed: () => usageWarmupElapsed
|
|
1835
2337
|
});
|
|
1836
|
-
import
|
|
1837
|
-
import {
|
|
1838
|
-
import { existsSync as existsSync6, mkdirSync as mkdirSync4 } from "fs";
|
|
2338
|
+
import { dirname as dirname7, basename as basename2 } from "path";
|
|
2339
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync6 } from "fs";
|
|
1839
2340
|
function sanitizeFts5Query(raw) {
|
|
1840
2341
|
const trimmed = raw.trim();
|
|
1841
2342
|
if (!trimmed) return '""';
|
|
@@ -1851,12 +2352,12 @@ function sanitizeFts5QueryOr(raw) {
|
|
|
1851
2352
|
}
|
|
1852
2353
|
function getMemoryDb() {
|
|
1853
2354
|
const dbPath = getResolvedPaths().memoryDbPath;
|
|
1854
|
-
const dir =
|
|
1855
|
-
if (!
|
|
1856
|
-
|
|
2355
|
+
const dir = dirname7(dbPath);
|
|
2356
|
+
if (!existsSync8(dir)) {
|
|
2357
|
+
mkdirSync6(dir, { recursive: true });
|
|
1857
2358
|
}
|
|
1858
|
-
const preExisting =
|
|
1859
|
-
const db =
|
|
2359
|
+
const preExisting = existsSync8(dbPath);
|
|
2360
|
+
const db = openDatabase2(dbPath);
|
|
1860
2361
|
db.pragma("journal_mode = WAL");
|
|
1861
2362
|
db.pragma("foreign_keys = ON");
|
|
1862
2363
|
const onDisk = db.pragma("user_version", { simple: true });
|
|
@@ -1932,6 +2433,39 @@ function migrateMemoryFilesFor4B(db) {
|
|
|
1932
2433
|
db.exec(`ALTER TABLE ${add.table} ADD COLUMN ${add.name} ${add.decl}`);
|
|
1933
2434
|
}
|
|
1934
2435
|
}
|
|
2436
|
+
function migrateSharedMemoryFor5B(db) {
|
|
2437
|
+
const cols = db.prepare(`PRAGMA table_info(observations)`).all();
|
|
2438
|
+
if (cols.length > 0 && !cols.some((c) => c.name === "shareable")) {
|
|
2439
|
+
db.exec(`ALTER TABLE observations ADD COLUMN shareable INTEGER NOT NULL DEFAULT 0`);
|
|
2440
|
+
}
|
|
2441
|
+
db.exec(`
|
|
2442
|
+
CREATE TABLE IF NOT EXISTS shared_memory_pending (
|
|
2443
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
2444
|
+
record_hash TEXT NOT NULL UNIQUE, -- identity + idempotency key (hex sha256)
|
|
2445
|
+
origin_repo_id TEXT NOT NULL, -- the signed origin repo_id (v4 UUID)
|
|
2446
|
+
origin_repo_label TEXT NOT NULL, -- re-slugged on read; NEVER trusted from the wire
|
|
2447
|
+
envelope_raw TEXT NOT NULL, -- the VERBATIM signed bytes; B-05 re-verifies these
|
|
2448
|
+
record_json TEXT NOT NULL, -- the single record's canonical JSON
|
|
2449
|
+
received_at_epoch INTEGER NOT NULL, -- epoch SECONDS (Slice-2 convention)
|
|
2450
|
+
accepted_at_epoch INTEGER, -- set on accept (B-05); NULL while pending
|
|
2451
|
+
refused_at_epoch INTEGER, -- set on refuse (B-06)
|
|
2452
|
+
expired_at_epoch INTEGER -- revocation \u21D2 EXPIRE, never DELETE (B-07 / S-3)
|
|
2453
|
+
);
|
|
2454
|
+
CREATE INDEX IF NOT EXISTS idx_smp_origin ON shared_memory_pending(origin_repo_id);
|
|
2455
|
+
CREATE INDEX IF NOT EXISTS idx_smp_received ON shared_memory_pending(received_at_epoch DESC);
|
|
2456
|
+
`);
|
|
2457
|
+
db.exec(`
|
|
2458
|
+
CREATE TABLE IF NOT EXISTS shared_memory_outbound (
|
|
2459
|
+
record_hash TEXT PRIMARY KEY, -- the exported record's canonical hash
|
|
2460
|
+
observation_id INTEGER NOT NULL, -- the source observations.id (for B-07 revocation)
|
|
2461
|
+
origin_repo_id TEXT NOT NULL, -- this repo's own repo_id at export time
|
|
2462
|
+
seq INTEGER NOT NULL, -- the envelope seq this record first went out in
|
|
2463
|
+
exported_at_epoch INTEGER NOT NULL, -- epoch SECONDS
|
|
2464
|
+
revoked_at_epoch INTEGER -- set when the source row is superseded/expired (B-07)
|
|
2465
|
+
);
|
|
2466
|
+
CREATE INDEX IF NOT EXISTS idx_smo_obs ON shared_memory_outbound(observation_id);
|
|
2467
|
+
`);
|
|
2468
|
+
}
|
|
1935
2469
|
function migrateAuditLogCheckExtension(db) {
|
|
1936
2470
|
const row = db.prepare("SELECT sql FROM sqlite_master WHERE type='table' AND name='audit_log'").get();
|
|
1937
2471
|
if (!row) return;
|
|
@@ -1954,7 +2488,16 @@ function migrateAuditLogCheckExtension(db) {
|
|
|
1954
2488
|
"memory_file_adopted_human",
|
|
1955
2489
|
"memory_file_rendered",
|
|
1956
2490
|
"memory_file_render_refused",
|
|
1957
|
-
"memory_file_tombstoned"
|
|
2491
|
+
"memory_file_tombstoned",
|
|
2492
|
+
// B-10 (Slice 5) — the seven cross-repo shared-memory events. An existing DB whose
|
|
2493
|
+
// CHECK lacks any of these triggers the table rebuild below.
|
|
2494
|
+
"shared_memory_exported",
|
|
2495
|
+
"shared_memory_export_refused",
|
|
2496
|
+
"shared_memory_imported",
|
|
2497
|
+
"shared_memory_dropped",
|
|
2498
|
+
"shared_memory_accepted",
|
|
2499
|
+
"shared_memory_refused",
|
|
2500
|
+
"shared_memory_revoked"
|
|
1958
2501
|
];
|
|
1959
2502
|
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);
|
|
1960
2503
|
if (checkClauseMatch) {
|
|
@@ -1973,7 +2516,12 @@ function migrateAuditLogCheckExtension(db) {
|
|
|
1973
2516
|
'code_change', 'rule_enforced', 'approval', 'review', 'commit', 'compaction',
|
|
1974
2517
|
'rule_candidate_emitted', 'rule_promoted', 'rule_dismissed',
|
|
1975
2518
|
'memory_file_ingested', 'memory_file_expired', 'memory_file_adopted_human',
|
|
1976
|
-
'memory_file_rendered', 'memory_file_render_refused', 'memory_file_tombstoned'
|
|
2519
|
+
'memory_file_rendered', 'memory_file_render_refused', 'memory_file_tombstoned',
|
|
2520
|
+
-- B-10 (Slice 5) \u2014 cross-repo shared-memory events (must mirror the CREATE +
|
|
2521
|
+
-- the expected[] list above, or the migration would loop rebuilding forever).
|
|
2522
|
+
'shared_memory_exported', 'shared_memory_export_refused', 'shared_memory_imported',
|
|
2523
|
+
'shared_memory_dropped', 'shared_memory_accepted', 'shared_memory_refused',
|
|
2524
|
+
'shared_memory_revoked'
|
|
1977
2525
|
)),
|
|
1978
2526
|
actor TEXT NOT NULL DEFAULT 'ai' CHECK(actor IN ('ai', 'human', 'hook', 'agent')),
|
|
1979
2527
|
model_id TEXT,
|
|
@@ -2050,6 +2598,12 @@ function initMemorySchema(db) {
|
|
|
2050
2598
|
-- credential, or takes a snapshot. A Slice-5 synced memory arrives here with
|
|
2051
2599
|
-- origin='team' and must never reach disk without CR-55's gates.
|
|
2052
2600
|
origin TEXT NOT NULL DEFAULT 'local',
|
|
2601
|
+
-- B-01 (Slice 5, cross-repo surfacing): the per-decision SHARE opt-in. Set to 1
|
|
2602
|
+
-- ONLY by an explicit human act (massu memory share <id>); NEVER by a heuristic,
|
|
2603
|
+
-- score, LLM, hook, or consolidation pass. Export (B-02) reads WHERE shareable=1
|
|
2604
|
+
-- AND origin='local'. Defaults 0 so every pre-existing and machine-written row is
|
|
2605
|
+
-- un-shareable until a human says otherwise (fail-closed).
|
|
2606
|
+
shareable INTEGER NOT NULL DEFAULT 0,
|
|
2053
2607
|
created_at TEXT NOT NULL,
|
|
2054
2608
|
created_at_epoch INTEGER NOT NULL,
|
|
2055
2609
|
FOREIGN KEY(session_id) REFERENCES sessions(session_id) ON DELETE CASCADE
|
|
@@ -2323,7 +2877,13 @@ function initMemorySchema(db) {
|
|
|
2323
2877
|
'code_change', 'rule_enforced', 'approval', 'review', 'commit', 'compaction',
|
|
2324
2878
|
'rule_candidate_emitted', 'rule_promoted', 'rule_dismissed',
|
|
2325
2879
|
'memory_file_ingested', 'memory_file_expired', 'memory_file_adopted_human',
|
|
2326
|
-
'memory_file_rendered', 'memory_file_render_refused', 'memory_file_tombstoned'
|
|
2880
|
+
'memory_file_rendered', 'memory_file_render_refused', 'memory_file_tombstoned',
|
|
2881
|
+
-- B-10 (Slice 5): cross-repo shared-memory observability. Without these seven,
|
|
2882
|
+
-- nobody could ever answer "what has crossed between my repos?" (and an ingest
|
|
2883
|
+
-- catch would swallow the CHECK violation, silently producing NOTHING).
|
|
2884
|
+
'shared_memory_exported', 'shared_memory_export_refused', 'shared_memory_imported',
|
|
2885
|
+
'shared_memory_dropped', 'shared_memory_accepted', 'shared_memory_refused',
|
|
2886
|
+
'shared_memory_revoked'
|
|
2327
2887
|
)),
|
|
2328
2888
|
actor TEXT NOT NULL DEFAULT 'ai' CHECK(actor IN ('ai', 'human', 'hook', 'agent')),
|
|
2329
2889
|
model_id TEXT,
|
|
@@ -2692,6 +3252,7 @@ function initMemorySchema(db) {
|
|
|
2692
3252
|
`);
|
|
2693
3253
|
migrateObservationEmbeddingChunks(db);
|
|
2694
3254
|
migrateMemoryFilesFor4B(db);
|
|
3255
|
+
migrateSharedMemoryFor5B(db);
|
|
2695
3256
|
ensureRuleCandidatesTable(db);
|
|
2696
3257
|
migrateRuleDelivery(db);
|
|
2697
3258
|
}
|
|
@@ -3562,6 +4123,7 @@ var MEMORY_SCHEMA_VERSION, TOOL_COST_EVENTS_RETENTION_DAYS, MAX_DRAFT_TEXT_LEN,
|
|
|
3562
4123
|
var init_memory_db = __esm({
|
|
3563
4124
|
"src/memory-db.ts"() {
|
|
3564
4125
|
"use strict";
|
|
4126
|
+
init_db_driver();
|
|
3565
4127
|
init_config();
|
|
3566
4128
|
init_memory_vector();
|
|
3567
4129
|
init_db_backup();
|
|
@@ -3586,8 +4148,8 @@ init_memory_db();
|
|
|
3586
4148
|
init_rule_delivery();
|
|
3587
4149
|
|
|
3588
4150
|
// src/session-archiver.ts
|
|
3589
|
-
import { existsSync as
|
|
3590
|
-
import { resolve as resolve5, dirname as
|
|
4151
|
+
import { existsSync as existsSync9, readFileSync as readFileSync6, writeFileSync as writeFileSync3, mkdirSync as mkdirSync7, renameSync } from "fs";
|
|
4152
|
+
import { resolve as resolve5, dirname as dirname8 } from "path";
|
|
3591
4153
|
|
|
3592
4154
|
// src/session-state-generator.ts
|
|
3593
4155
|
function generateCurrentMd(db, sessionId) {
|
|
@@ -3739,29 +4301,29 @@ function archiveAndRegenerate(db, sessionId) {
|
|
|
3739
4301
|
const archiveDir = resolved.sessionArchivePath;
|
|
3740
4302
|
let archived = false;
|
|
3741
4303
|
let archivePath;
|
|
3742
|
-
if (
|
|
3743
|
-
const existingContent =
|
|
4304
|
+
if (existsSync9(currentMdPath)) {
|
|
4305
|
+
const existingContent = readFileSync6(currentMdPath, "utf-8");
|
|
3744
4306
|
if (existingContent.trim().length > 10) {
|
|
3745
4307
|
const { date, slug } = extractArchiveInfo(existingContent);
|
|
3746
4308
|
archivePath = resolve5(archiveDir, `${date}-${slug}.md`);
|
|
3747
|
-
if (!
|
|
3748
|
-
|
|
4309
|
+
if (!existsSync9(archiveDir)) {
|
|
4310
|
+
mkdirSync7(archiveDir, { recursive: true });
|
|
3749
4311
|
}
|
|
3750
4312
|
try {
|
|
3751
4313
|
renameSync(currentMdPath, archivePath);
|
|
3752
4314
|
archived = true;
|
|
3753
4315
|
} catch (_e) {
|
|
3754
|
-
|
|
4316
|
+
writeFileSync3(archivePath, existingContent);
|
|
3755
4317
|
archived = true;
|
|
3756
4318
|
}
|
|
3757
4319
|
}
|
|
3758
4320
|
}
|
|
3759
4321
|
const newContent = generateCurrentMd(db, sessionId);
|
|
3760
|
-
const dir =
|
|
3761
|
-
if (!
|
|
3762
|
-
|
|
4322
|
+
const dir = dirname8(currentMdPath);
|
|
4323
|
+
if (!existsSync9(dir)) {
|
|
4324
|
+
mkdirSync7(dir, { recursive: true });
|
|
3763
4325
|
}
|
|
3764
|
-
|
|
4326
|
+
writeFileSync3(currentMdPath, newContent, "utf-8");
|
|
3765
4327
|
return { archived, archivePath, newContent };
|
|
3766
4328
|
}
|
|
3767
4329
|
function extractArchiveInfo(content) {
|
|
@@ -3868,11 +4430,6 @@ init_rule_delivery();
|
|
|
3868
4430
|
|
|
3869
4431
|
// src/observation-extractor.ts
|
|
3870
4432
|
init_memory_db();
|
|
3871
|
-
|
|
3872
|
-
// src/adr-generator.ts
|
|
3873
|
-
init_config();
|
|
3874
|
-
|
|
3875
|
-
// src/observation-extractor.ts
|
|
3876
4433
|
init_config();
|
|
3877
4434
|
var PRIVATE_PATTERNS = [
|
|
3878
4435
|
/\/Users\/\w+/,
|
|
@@ -3903,7 +4460,7 @@ function classifyVisibility(title, detail) {
|
|
|
3903
4460
|
// src/cloud-sync.ts
|
|
3904
4461
|
var MAX_RETRIES = 3;
|
|
3905
4462
|
var RETRY_DELAYS = [1e3, 2e3, 4e3];
|
|
3906
|
-
var DEFAULT_CLOUD_REQUEST_TIMEOUT_MS =
|
|
4463
|
+
var DEFAULT_CLOUD_REQUEST_TIMEOUT_MS = 15e3;
|
|
3907
4464
|
var SYNC_DEADLINE_MS = 2e4;
|
|
3908
4465
|
var MIN_ATTEMPT_BUDGET_MS = 1e3;
|
|
3909
4466
|
async function syncToCloud(db, payload) {
|
|
@@ -4079,13 +4636,80 @@ async function drainSyncQueue(db) {
|
|
|
4079
4636
|
}
|
|
4080
4637
|
}
|
|
4081
4638
|
function sleep(ms) {
|
|
4082
|
-
return new Promise((
|
|
4639
|
+
return new Promise((resolve8) => setTimeout(resolve8, ms));
|
|
4640
|
+
}
|
|
4641
|
+
|
|
4642
|
+
// src/team-rule-sync.ts
|
|
4643
|
+
import { existsSync as existsSync12, writeFileSync as writeFileSync6, unlinkSync as unlinkSync3, mkdirSync as mkdirSync9 } from "fs";
|
|
4644
|
+
import { homedir as homedir6 } from "os";
|
|
4645
|
+
import { join as join6, dirname as dirname10 } from "path";
|
|
4646
|
+
|
|
4647
|
+
// src/security/promotion-apply-mac.ts
|
|
4648
|
+
import { createHmac as createHmac2, timingSafeEqual as timingSafeEqual2 } from "crypto";
|
|
4649
|
+
import { homedir as homedir5 } from "os";
|
|
4650
|
+
|
|
4651
|
+
// src/memory-authorship.ts
|
|
4652
|
+
import { createHmac, randomBytes, timingSafeEqual } from "crypto";
|
|
4653
|
+
import { readFileSync as readFileSync7, writeFileSync as writeFileSync4, existsSync as existsSync10, mkdirSync as mkdirSync8, chmodSync as chmodSync3, statSync as statSync2 } from "fs";
|
|
4654
|
+
import { homedir as homedir4 } from "os";
|
|
4655
|
+
import { resolve as resolve6 } from "path";
|
|
4656
|
+
function renderKeyPath(home = homedir4()) {
|
|
4657
|
+
return resolve6(home, ".massu", "render-key");
|
|
4658
|
+
}
|
|
4659
|
+
function readRenderKey(home = homedir4()) {
|
|
4660
|
+
const p = renderKeyPath(home);
|
|
4661
|
+
try {
|
|
4662
|
+
if (!existsSync10(p)) return void 0;
|
|
4663
|
+
const raw = readFileSync7(p);
|
|
4664
|
+
if (raw.length !== 32) return void 0;
|
|
4665
|
+
return raw;
|
|
4666
|
+
} catch {
|
|
4667
|
+
return void 0;
|
|
4668
|
+
}
|
|
4669
|
+
}
|
|
4670
|
+
function ensureRenderKey(home = homedir4()) {
|
|
4671
|
+
const existing = readRenderKey(home);
|
|
4672
|
+
if (existing) return existing;
|
|
4673
|
+
const p = renderKeyPath(home);
|
|
4674
|
+
try {
|
|
4675
|
+
const dir = resolve6(home, ".massu");
|
|
4676
|
+
mkdirSync8(dir, { recursive: true, mode: 448 });
|
|
4677
|
+
try {
|
|
4678
|
+
chmodSync3(dir, 448);
|
|
4679
|
+
} catch {
|
|
4680
|
+
}
|
|
4681
|
+
const key = randomBytes(32);
|
|
4682
|
+
writeFileSync4(p, key, { mode: 384 });
|
|
4683
|
+
try {
|
|
4684
|
+
chmodSync3(p, 384);
|
|
4685
|
+
} catch {
|
|
4686
|
+
}
|
|
4687
|
+
return key;
|
|
4688
|
+
} catch {
|
|
4689
|
+
return void 0;
|
|
4690
|
+
}
|
|
4691
|
+
}
|
|
4692
|
+
|
|
4693
|
+
// src/security/promotion-apply-mac.ts
|
|
4694
|
+
var DOMAIN = "massu.promotion-apply.v1";
|
|
4695
|
+
function canonical(f) {
|
|
4696
|
+
return [DOMAIN, f.origin, f.org_id, f.prompt_hash, f.destination, f.draft_text].join("\n");
|
|
4697
|
+
}
|
|
4698
|
+
function deriveApplyKey(renderKey) {
|
|
4699
|
+
return createHmac2("sha256", renderKey).update(DOMAIN, "utf8").digest();
|
|
4700
|
+
}
|
|
4701
|
+
function computePromotionApplyMac(fields, home = homedir5()) {
|
|
4702
|
+
try {
|
|
4703
|
+
const key = ensureRenderKey(home);
|
|
4704
|
+
if (!key) return null;
|
|
4705
|
+
return createHmac2("sha256", deriveApplyKey(key)).update(canonical(fields), "utf8").digest("hex");
|
|
4706
|
+
} catch {
|
|
4707
|
+
return null;
|
|
4708
|
+
}
|
|
4083
4709
|
}
|
|
4084
4710
|
|
|
4085
4711
|
// src/team-rule-sync.ts
|
|
4086
4712
|
init_config();
|
|
4087
|
-
import { existsSync as existsSync8, writeFileSync as writeFileSync3, unlinkSync as unlinkSync2, mkdirSync as mkdirSync6 } from "fs";
|
|
4088
|
-
import { join as join4, dirname as dirname7 } from "path";
|
|
4089
4713
|
|
|
4090
4714
|
// src/license.ts
|
|
4091
4715
|
init_config();
|
|
@@ -4136,13 +4760,13 @@ function verifyEd25519SignedEnvelope(key, payload) {
|
|
|
4136
4760
|
if (typeof k !== "string") continue;
|
|
4137
4761
|
canonicalObj[k] = payload[k];
|
|
4138
4762
|
}
|
|
4139
|
-
const
|
|
4763
|
+
const canonical2 = JSON.stringify(canonicalObj, [...payloadKeys].sort());
|
|
4140
4764
|
try {
|
|
4141
4765
|
const der = Buffer.concat([SPKI_ED25519_PREFIX, Buffer.from(key.pubkeyBytes)]);
|
|
4142
4766
|
const pubkey = createPublicKey({ key: der, format: "der", type: "spki" });
|
|
4143
4767
|
const ok = cryptoVerify(
|
|
4144
4768
|
null,
|
|
4145
|
-
Buffer.from(
|
|
4769
|
+
Buffer.from(canonical2, "utf-8"),
|
|
4146
4770
|
pubkey,
|
|
4147
4771
|
Buffer.from(sig, "base64")
|
|
4148
4772
|
);
|
|
@@ -4300,6 +4924,10 @@ var TEAM_SHARED_PROMOTION_MIN_TIER = "team";
|
|
|
4300
4924
|
function entitledForTeamSharedPromotion(tier) {
|
|
4301
4925
|
return tierLevel(tier) >= tierLevel(TEAM_SHARED_PROMOTION_MIN_TIER);
|
|
4302
4926
|
}
|
|
4927
|
+
var CROSS_REPO_SURFACING_MIN_TIER = "free";
|
|
4928
|
+
function entitledForCrossRepoSurfacing(tier) {
|
|
4929
|
+
return tierLevel(tier) >= tierLevel(CROSS_REPO_SURFACING_MIN_TIER);
|
|
4930
|
+
}
|
|
4303
4931
|
|
|
4304
4932
|
// src/security/promotion-pubkey.generated.ts
|
|
4305
4933
|
var PROMOTION_PUBKEY_ED25519 = new Uint8Array([107, 161, 33, 17, 189, 44, 193, 128, 252, 155, 188, 236, 100, 163, 23, 146, 219, 155, 216, 139, 134, 72, 211, 182, 151, 122, 209, 151, 135, 65, 167, 26]);
|
|
@@ -4321,8 +4949,138 @@ function verifyPromotionEnvelope(payload) {
|
|
|
4321
4949
|
);
|
|
4322
4950
|
}
|
|
4323
4951
|
|
|
4324
|
-
// src/lib/
|
|
4325
|
-
import
|
|
4952
|
+
// src/lib/safe-write.ts
|
|
4953
|
+
import {
|
|
4954
|
+
writeFileSync as writeFileSync5,
|
|
4955
|
+
renameSync as renameSync2,
|
|
4956
|
+
openSync as openSync2,
|
|
4957
|
+
fsyncSync as fsyncSync2,
|
|
4958
|
+
closeSync as closeSync2,
|
|
4959
|
+
existsSync as existsSync11,
|
|
4960
|
+
realpathSync,
|
|
4961
|
+
unlinkSync as unlinkSync2,
|
|
4962
|
+
accessSync as accessSync2,
|
|
4963
|
+
statSync as statSync3,
|
|
4964
|
+
chmodSync as chmodSync4,
|
|
4965
|
+
constants
|
|
4966
|
+
} from "fs";
|
|
4967
|
+
import { dirname as dirname9, resolve as resolve7, relative, isAbsolute, basename as basename3, sep } from "path";
|
|
4968
|
+
var PathEscapeError = class extends Error {
|
|
4969
|
+
constructor(message) {
|
|
4970
|
+
super(message);
|
|
4971
|
+
this.name = "PathEscapeError";
|
|
4972
|
+
}
|
|
4973
|
+
};
|
|
4974
|
+
var RESERVED_DEVICE_NAMES = /* @__PURE__ */ new Set([
|
|
4975
|
+
"con",
|
|
4976
|
+
"prn",
|
|
4977
|
+
"aux",
|
|
4978
|
+
"nul",
|
|
4979
|
+
"com1",
|
|
4980
|
+
"com2",
|
|
4981
|
+
"com3",
|
|
4982
|
+
"com4",
|
|
4983
|
+
"com5",
|
|
4984
|
+
"com6",
|
|
4985
|
+
"com7",
|
|
4986
|
+
"com8",
|
|
4987
|
+
"com9",
|
|
4988
|
+
"lpt1",
|
|
4989
|
+
"lpt2",
|
|
4990
|
+
"lpt3",
|
|
4991
|
+
"lpt4",
|
|
4992
|
+
"lpt5",
|
|
4993
|
+
"lpt6",
|
|
4994
|
+
"lpt7",
|
|
4995
|
+
"lpt8",
|
|
4996
|
+
"lpt9"
|
|
4997
|
+
]);
|
|
4998
|
+
function assertContainedIn(rootDir, candidate, opts = {}) {
|
|
4999
|
+
if (candidate.includes("\0")) {
|
|
5000
|
+
throw new PathEscapeError("path contains a NUL byte");
|
|
5001
|
+
}
|
|
5002
|
+
const root = resolve7(rootDir);
|
|
5003
|
+
const abs = resolve7(root, candidate);
|
|
5004
|
+
if (!opts.allowNested) {
|
|
5005
|
+
const rel = relative(root, abs);
|
|
5006
|
+
if (rel.includes(sep)) {
|
|
5007
|
+
throw new PathEscapeError(`path must be a plain basename inside ${root}: ${candidate}`);
|
|
5008
|
+
}
|
|
5009
|
+
}
|
|
5010
|
+
const base = basename3(abs);
|
|
5011
|
+
const stem = base.replace(/\.[^.]*$/, "").toLowerCase();
|
|
5012
|
+
if (RESERVED_DEVICE_NAMES.has(stem)) {
|
|
5013
|
+
throw new PathEscapeError(`reserved device name: ${base}`);
|
|
5014
|
+
}
|
|
5015
|
+
if (!isContainedIn(root, abs)) {
|
|
5016
|
+
throw new PathEscapeError(`path escapes ${root}: ${candidate}`);
|
|
5017
|
+
}
|
|
5018
|
+
return abs;
|
|
5019
|
+
}
|
|
5020
|
+
function isContainedIn(rootDir, candidate, opts = {}) {
|
|
5021
|
+
if (candidate.includes("\0")) return false;
|
|
5022
|
+
const root = resolve7(rootDir);
|
|
5023
|
+
const abs = resolve7(root, candidate);
|
|
5024
|
+
const rel = relative(root, abs);
|
|
5025
|
+
if (rel === "" && !opts.allowRoot) return false;
|
|
5026
|
+
if (rel.startsWith("..") || isAbsolute(rel)) return false;
|
|
5027
|
+
let probe = abs;
|
|
5028
|
+
while (!existsSync11(probe) && probe.length > 1) {
|
|
5029
|
+
const parent = dirname9(probe);
|
|
5030
|
+
if (parent === probe) break;
|
|
5031
|
+
probe = parent;
|
|
5032
|
+
}
|
|
5033
|
+
let realRoot;
|
|
5034
|
+
let realProbe;
|
|
5035
|
+
try {
|
|
5036
|
+
realRoot = realpathSync(root);
|
|
5037
|
+
realProbe = realpathSync(probe);
|
|
5038
|
+
} catch {
|
|
5039
|
+
return true;
|
|
5040
|
+
}
|
|
5041
|
+
const realRel = relative(realRoot, realProbe);
|
|
5042
|
+
if (realRel === "") return true;
|
|
5043
|
+
return !realRel.startsWith("..") && !isAbsolute(realRel);
|
|
5044
|
+
}
|
|
5045
|
+
function atomicWriteFileSync(destPath, contents) {
|
|
5046
|
+
const dir = dirname9(destPath);
|
|
5047
|
+
const tmp = resolve7(dir, `.${basename3(destPath)}.massu-tmp-${process.pid}`);
|
|
5048
|
+
let destMode;
|
|
5049
|
+
if (existsSync11(destPath)) {
|
|
5050
|
+
accessSync2(destPath, constants.W_OK);
|
|
5051
|
+
destMode = statSync3(destPath).mode & 511;
|
|
5052
|
+
}
|
|
5053
|
+
let fd;
|
|
5054
|
+
try {
|
|
5055
|
+
writeFileSync5(tmp, contents, "utf-8");
|
|
5056
|
+
if (destMode !== void 0) chmodSync4(tmp, destMode);
|
|
5057
|
+
fd = openSync2(tmp, "r+");
|
|
5058
|
+
fsyncSync2(fd);
|
|
5059
|
+
closeSync2(fd);
|
|
5060
|
+
fd = void 0;
|
|
5061
|
+
renameSync2(tmp, destPath);
|
|
5062
|
+
} catch (err) {
|
|
5063
|
+
if (fd !== void 0) {
|
|
5064
|
+
try {
|
|
5065
|
+
closeSync2(fd);
|
|
5066
|
+
} catch {
|
|
5067
|
+
}
|
|
5068
|
+
}
|
|
5069
|
+
try {
|
|
5070
|
+
if (existsSync11(tmp)) unlinkSync2(tmp);
|
|
5071
|
+
} catch {
|
|
5072
|
+
}
|
|
5073
|
+
throw err;
|
|
5074
|
+
}
|
|
5075
|
+
}
|
|
5076
|
+
var SLUG_ALLOWED = /^[a-z0-9_]+$/;
|
|
5077
|
+
function deriveSlug(input) {
|
|
5078
|
+
const cleaned = input.toLowerCase().replace(/[^a-z0-9\s]+/g, " ").replace(/\s+/g, "_").replace(/^_+|_+$/g, "");
|
|
5079
|
+
return cleaned.slice(0, 60) || "rule_candidate";
|
|
5080
|
+
}
|
|
5081
|
+
|
|
5082
|
+
// src/memory-index-region.ts
|
|
5083
|
+
init_fileLock();
|
|
4326
5084
|
|
|
4327
5085
|
// src/rule-candidate-applier.ts
|
|
4328
5086
|
init_config();
|
|
@@ -4334,6 +5092,12 @@ init_config();
|
|
|
4334
5092
|
// src/rule-candidate-applier.ts
|
|
4335
5093
|
init_memory_db();
|
|
4336
5094
|
|
|
5095
|
+
// src/memory-origin.ts
|
|
5096
|
+
var LOCAL_ORIGIN = "local";
|
|
5097
|
+
function isLocalOrigin(o) {
|
|
5098
|
+
return o === LOCAL_ORIGIN;
|
|
5099
|
+
}
|
|
5100
|
+
|
|
4337
5101
|
// src/rule-candidate-funnel.ts
|
|
4338
5102
|
init_memory_db();
|
|
4339
5103
|
|
|
@@ -4459,11 +5223,11 @@ async function pullTeamPromotions(db, opts = {}) {
|
|
|
4459
5223
|
result.revoked_handled += 1;
|
|
4460
5224
|
continue;
|
|
4461
5225
|
}
|
|
4462
|
-
if (
|
|
5226
|
+
if (existsSync12(candidatePath) || alreadyApplied(db, p.prompt_hash)) {
|
|
4463
5227
|
result.skipped += 1;
|
|
4464
5228
|
continue;
|
|
4465
5229
|
}
|
|
4466
|
-
materializeCandidate(db, projectRoot, candidatePath, p, signedOrgId);
|
|
5230
|
+
materializeCandidate(db, projectRoot, candidatePath, p, signedOrgId, opts.home ?? homedir6());
|
|
4467
5231
|
result.materialized += 1;
|
|
4468
5232
|
}
|
|
4469
5233
|
const serverCursor = typeof envelope.cursor === "number" ? envelope.cursor : 0;
|
|
@@ -4495,7 +5259,7 @@ function isValidWirePromotion(p) {
|
|
|
4495
5259
|
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
5260
|
}
|
|
4497
5261
|
function sidecarPath(projectRoot, promptHash) {
|
|
4498
|
-
return
|
|
5262
|
+
return join6(projectRoot, ".massu", "rule-candidates", `${promptHash}.json`);
|
|
4499
5263
|
}
|
|
4500
5264
|
function alreadyApplied(db, promptHash) {
|
|
4501
5265
|
try {
|
|
@@ -4509,9 +5273,9 @@ function alreadyApplied(db, promptHash) {
|
|
|
4509
5273
|
}
|
|
4510
5274
|
}
|
|
4511
5275
|
function handleRevocation(db, projectRoot, candidatePath, promptHash) {
|
|
4512
|
-
if (
|
|
5276
|
+
if (existsSync12(candidatePath)) {
|
|
4513
5277
|
try {
|
|
4514
|
-
|
|
5278
|
+
unlinkSync3(candidatePath);
|
|
4515
5279
|
} catch {
|
|
4516
5280
|
}
|
|
4517
5281
|
return;
|
|
@@ -4523,8 +5287,12 @@ function handleRevocation(db, projectRoot, candidatePath, promptHash) {
|
|
|
4523
5287
|
);
|
|
4524
5288
|
}
|
|
4525
5289
|
}
|
|
4526
|
-
function materializeCandidate(db, projectRoot, candidatePath, p, orgId) {
|
|
5290
|
+
function materializeCandidate(db, projectRoot, candidatePath, p, orgId, home) {
|
|
4527
5291
|
const promptText = p.draft_text.replace(/\n+/g, " ").slice(0, 200) || `team rule ${p.prompt_hash}`;
|
|
5292
|
+
const applyMac = computePromotionApplyMac(
|
|
5293
|
+
{ origin: "team", org_id: orgId, prompt_hash: p.prompt_hash, destination: p.destination, draft_text: p.draft_text },
|
|
5294
|
+
home
|
|
5295
|
+
);
|
|
4528
5296
|
const sidecar = {
|
|
4529
5297
|
// Standard RuleCandidatePayload fields (so `/massu-rule approve` → readCandidate
|
|
4530
5298
|
// → validateCandidatePayload passes), synthesized from the promotion.
|
|
@@ -4547,6 +5315,7 @@ function materializeCandidate(db, projectRoot, candidatePath, p, orgId) {
|
|
|
4547
5315
|
promoted_by: p.promoted_by,
|
|
4548
5316
|
promoted_at: p.promoted_at,
|
|
4549
5317
|
signature_verified: true,
|
|
5318
|
+
...applyMac ? { apply_mac: applyMac } : {},
|
|
4550
5319
|
...p.hardened === true ? { hardened: true } : {}
|
|
4551
5320
|
},
|
|
4552
5321
|
// Extra fields the `/massu-rule approve` flow reads to drive the apply (the
|
|
@@ -4557,9 +5326,9 @@ function materializeCandidate(db, projectRoot, candidatePath, p, orgId) {
|
|
|
4557
5326
|
draft_text: p.draft_text,
|
|
4558
5327
|
...p.review_attestation !== void 0 ? { publisher_review_attestation: p.review_attestation } : {}
|
|
4559
5328
|
};
|
|
4560
|
-
const dir =
|
|
4561
|
-
if (!
|
|
4562
|
-
|
|
5329
|
+
const dir = dirname10(candidatePath);
|
|
5330
|
+
if (!existsSync12(dir)) mkdirSync9(dir, { recursive: true });
|
|
5331
|
+
writeFileSync6(candidatePath, JSON.stringify(sidecar, null, 2), "utf-8");
|
|
4563
5332
|
try {
|
|
4564
5333
|
shareObservation(db, p.promoted_by, getProjectName(), "rule_promotion", promptText, {
|
|
4565
5334
|
filePath: void 0,
|
|
@@ -4610,21 +5379,845 @@ function failSync(db, reason) {
|
|
|
4610
5379
|
return { ...ZERO, sync_error: reason };
|
|
4611
5380
|
}
|
|
4612
5381
|
|
|
4613
|
-
// src/
|
|
5382
|
+
// src/shared-memory-sync.ts
|
|
4614
5383
|
init_config();
|
|
5384
|
+
import { homedir as homedir10 } from "os";
|
|
4615
5385
|
|
|
4616
|
-
// src/
|
|
5386
|
+
// src/memory-repo-identity.ts
|
|
4617
5387
|
init_config();
|
|
4618
|
-
|
|
4619
|
-
|
|
4620
|
-
|
|
4621
|
-
|
|
4622
|
-
|
|
4623
|
-
|
|
4624
|
-
|
|
4625
|
-
|
|
4626
|
-
}
|
|
4627
|
-
|
|
5388
|
+
init_memory_db();
|
|
5389
|
+
import { randomUUID as randomUUID2 } from "crypto";
|
|
5390
|
+
var REPO_ID_META_KEY = "repo_id";
|
|
5391
|
+
var SHARED_PIN_META_PREFIX = "shared_pin:";
|
|
5392
|
+
var UUID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/;
|
|
5393
|
+
function getRepoId(db) {
|
|
5394
|
+
const v = getMemoryMeta(db, REPO_ID_META_KEY);
|
|
5395
|
+
return v && UUID_RE.test(v) ? v : null;
|
|
5396
|
+
}
|
|
5397
|
+
function mintRepoId(db) {
|
|
5398
|
+
const existing = getRepoId(db);
|
|
5399
|
+
if (existing) return existing;
|
|
5400
|
+
const id = randomUUID2();
|
|
5401
|
+
setMemoryMeta(db, REPO_ID_META_KEY, id);
|
|
5402
|
+
return id;
|
|
5403
|
+
}
|
|
5404
|
+
function deriveRepoLabel(projectName) {
|
|
5405
|
+
if (!projectName || !projectName.trim()) return "repo";
|
|
5406
|
+
const slug = deriveSlug(projectName);
|
|
5407
|
+
return slug && SLUG_ALLOWED.test(slug) ? slug : "repo";
|
|
5408
|
+
}
|
|
5409
|
+
function getRepoLabel() {
|
|
5410
|
+
return deriveRepoLabel(getConfig().project.name);
|
|
5411
|
+
}
|
|
5412
|
+
function getSharedPin(db, originRepoId) {
|
|
5413
|
+
return getMemoryMeta(db, SHARED_PIN_META_PREFIX + originRepoId);
|
|
5414
|
+
}
|
|
5415
|
+
function tofuPinSharedFingerprint(db, originRepoId, fingerprint) {
|
|
5416
|
+
const existing = getSharedPin(db, originRepoId);
|
|
5417
|
+
if (existing) return existing;
|
|
5418
|
+
setMemoryMeta(db, SHARED_PIN_META_PREFIX + originRepoId, fingerprint);
|
|
5419
|
+
return fingerprint;
|
|
5420
|
+
}
|
|
5421
|
+
|
|
5422
|
+
// src/memory-repos-registry.ts
|
|
5423
|
+
import {
|
|
5424
|
+
chmodSync as chmodSync5,
|
|
5425
|
+
existsSync as existsSync13,
|
|
5426
|
+
mkdirSync as mkdirSync10,
|
|
5427
|
+
readFileSync as readFileSync9,
|
|
5428
|
+
writeFileSync as writeFileSync7
|
|
5429
|
+
} from "fs";
|
|
5430
|
+
import { homedir as homedir7 } from "os";
|
|
5431
|
+
import { dirname as dirname11, join as join7 } from "path";
|
|
5432
|
+
var REPOS_REGISTRY_VERSION = 1;
|
|
5433
|
+
function reposRegistryPath(home = homedir7()) {
|
|
5434
|
+
return join7(home, ".massu", "repos.json");
|
|
5435
|
+
}
|
|
5436
|
+
function emptyRegistry() {
|
|
5437
|
+
return { version: REPOS_REGISTRY_VERSION, repos: [] };
|
|
5438
|
+
}
|
|
5439
|
+
function readReposRegistry(home = homedir7()) {
|
|
5440
|
+
const p = reposRegistryPath(home);
|
|
5441
|
+
if (!existsSync13(p)) return emptyRegistry();
|
|
5442
|
+
try {
|
|
5443
|
+
const parsed = JSON.parse(readFileSync9(p, "utf-8"));
|
|
5444
|
+
if (!parsed || typeof parsed !== "object") return emptyRegistry();
|
|
5445
|
+
const r = parsed;
|
|
5446
|
+
if (!Array.isArray(r.repos)) return emptyRegistry();
|
|
5447
|
+
const repos = r.repos.filter(
|
|
5448
|
+
(e) => !!e && typeof e === "object" && typeof e.repo_id === "string" && typeof e.label === "string"
|
|
5449
|
+
);
|
|
5450
|
+
return { version: typeof r.version === "number" ? r.version : REPOS_REGISTRY_VERSION, repos };
|
|
5451
|
+
} catch {
|
|
5452
|
+
return emptyRegistry();
|
|
5453
|
+
}
|
|
5454
|
+
}
|
|
5455
|
+
function findRepoByLabel(home, label) {
|
|
5456
|
+
return readReposRegistry(home).repos.find((e) => e.label === label) ?? null;
|
|
5457
|
+
}
|
|
5458
|
+
function upsertRepoRegistration(entry, home = homedir7()) {
|
|
5459
|
+
const registry = readReposRegistry(home);
|
|
5460
|
+
const next = registry.repos.filter((e) => e.repo_id !== entry.repo_id);
|
|
5461
|
+
next.push(entry);
|
|
5462
|
+
const out = { version: REPOS_REGISTRY_VERSION, repos: next };
|
|
5463
|
+
const p = reposRegistryPath(home);
|
|
5464
|
+
mkdirSync10(dirname11(p), { recursive: true, mode: 448 });
|
|
5465
|
+
writeFileSync7(p, JSON.stringify(out, null, 2), { mode: 384 });
|
|
5466
|
+
chmodSync5(p, 384);
|
|
5467
|
+
}
|
|
5468
|
+
|
|
5469
|
+
// src/security/local-share-verifier.ts
|
|
5470
|
+
import { homedir as homedir9 } from "os";
|
|
5471
|
+
|
|
5472
|
+
// src/security/local-share-signer.ts
|
|
5473
|
+
import {
|
|
5474
|
+
chmodSync as chmodSync6,
|
|
5475
|
+
existsSync as existsSync14,
|
|
5476
|
+
mkdirSync as mkdirSync11,
|
|
5477
|
+
readFileSync as readFileSync10,
|
|
5478
|
+
writeFileSync as writeFileSync8
|
|
5479
|
+
} from "fs";
|
|
5480
|
+
import { homedir as homedir8 } from "os";
|
|
5481
|
+
import { join as join8 } from "path";
|
|
5482
|
+
import {
|
|
5483
|
+
createHash as createHash3,
|
|
5484
|
+
createPrivateKey,
|
|
5485
|
+
createPublicKey as createPublicKey2,
|
|
5486
|
+
generateKeyPairSync,
|
|
5487
|
+
sign as cryptoSign
|
|
5488
|
+
} from "crypto";
|
|
5489
|
+
|
|
5490
|
+
// src/shared-memory-envelope.ts
|
|
5491
|
+
import { createHash as createHash2 } from "crypto";
|
|
5492
|
+
var SHARED_MEMORY_KIND = "massu.shared-memory.v1";
|
|
5493
|
+
var SHARED_MEMORY_SIGNATURE_PAYLOAD_KEYS = [
|
|
5494
|
+
"issued_at",
|
|
5495
|
+
"kind",
|
|
5496
|
+
"origin_repo_id",
|
|
5497
|
+
"origin_repo_label",
|
|
5498
|
+
"records_json",
|
|
5499
|
+
"revokes_json",
|
|
5500
|
+
"seq"
|
|
5501
|
+
];
|
|
5502
|
+
function canonicalizeSharedMemoryEnvelope(body) {
|
|
5503
|
+
const keys = SHARED_MEMORY_SIGNATURE_PAYLOAD_KEYS;
|
|
5504
|
+
const canonicalObj = {};
|
|
5505
|
+
for (const k of keys) {
|
|
5506
|
+
canonicalObj[k] = body[k];
|
|
5507
|
+
}
|
|
5508
|
+
return JSON.stringify(canonicalObj, [...keys].sort());
|
|
5509
|
+
}
|
|
5510
|
+
function hashSharedMemoryRecord(record) {
|
|
5511
|
+
const canonical2 = JSON.stringify({
|
|
5512
|
+
created_at_epoch: record.created_at_epoch,
|
|
5513
|
+
detail: record.detail,
|
|
5514
|
+
importance: record.importance,
|
|
5515
|
+
superseded_by_hash: record.superseded_by_hash,
|
|
5516
|
+
title: record.title,
|
|
5517
|
+
type: record.type
|
|
5518
|
+
});
|
|
5519
|
+
return createHash2("sha256").update(canonical2, "utf-8").digest("hex");
|
|
5520
|
+
}
|
|
5521
|
+
|
|
5522
|
+
// src/security/local-share-signer.ts
|
|
5523
|
+
function localShareKeyDir(home = homedir8()) {
|
|
5524
|
+
return join8(home, ".massu", "keys");
|
|
5525
|
+
}
|
|
5526
|
+
function localSharePrivKeyPath(home = homedir8()) {
|
|
5527
|
+
return join8(localShareKeyDir(home), "local-share.key");
|
|
5528
|
+
}
|
|
5529
|
+
function localSharePubKeyPath(home = homedir8()) {
|
|
5530
|
+
return join8(localShareKeyDir(home), "local-share.pub");
|
|
5531
|
+
}
|
|
5532
|
+
var SPKI_ED25519_PREFIX_LEN = 12;
|
|
5533
|
+
function ensureLocalShareKeypair(home = homedir8()) {
|
|
5534
|
+
const privPath = localSharePrivKeyPath(home);
|
|
5535
|
+
if (existsSync14(privPath)) return;
|
|
5536
|
+
const { publicKey, privateKey } = generateKeyPairSync("ed25519");
|
|
5537
|
+
const privPem = privateKey.export({ format: "pem", type: "pkcs8" });
|
|
5538
|
+
const pubPem = publicKey.export({ format: "pem", type: "spki" });
|
|
5539
|
+
mkdirSync11(localShareKeyDir(home), { recursive: true, mode: 448 });
|
|
5540
|
+
writeFileSync8(privPath, privPem, { mode: 384 });
|
|
5541
|
+
chmodSync6(privPath, 384);
|
|
5542
|
+
writeFileSync8(localSharePubKeyPath(home), pubPem, { mode: 420 });
|
|
5543
|
+
}
|
|
5544
|
+
function readLocalSharePublicKeyRaw(home = homedir8()) {
|
|
5545
|
+
const pubPem = readFileSync10(localSharePubKeyPath(home), "utf-8");
|
|
5546
|
+
const der = createPublicKey2(pubPem).export({ format: "der", type: "spki" });
|
|
5547
|
+
return Uint8Array.from(der.subarray(SPKI_ED25519_PREFIX_LEN));
|
|
5548
|
+
}
|
|
5549
|
+
function localSharePubkeyFingerprint(home = homedir8()) {
|
|
5550
|
+
return createHash3("sha256").update(Buffer.from(readLocalSharePublicKeyRaw(home))).digest("hex");
|
|
5551
|
+
}
|
|
5552
|
+
function signSharedMemoryEnvelope(body, home = homedir8()) {
|
|
5553
|
+
ensureLocalShareKeypair(home);
|
|
5554
|
+
const privPem = readFileSync10(localSharePrivKeyPath(home), "utf-8");
|
|
5555
|
+
const privKey = createPrivateKey(privPem);
|
|
5556
|
+
const canonical2 = canonicalizeSharedMemoryEnvelope(body);
|
|
5557
|
+
const signature = cryptoSign(null, Buffer.from(canonical2, "utf-8"), privKey).toString("base64");
|
|
5558
|
+
return {
|
|
5559
|
+
...body,
|
|
5560
|
+
_signature: signature,
|
|
5561
|
+
_signature_alg: "ed25519",
|
|
5562
|
+
_signature_payload_keys: SHARED_MEMORY_SIGNATURE_PAYLOAD_KEYS,
|
|
5563
|
+
_signature_pubkey_fingerprint: localSharePubkeyFingerprint(home)
|
|
5564
|
+
};
|
|
5565
|
+
}
|
|
5566
|
+
|
|
5567
|
+
// src/security/local-share-verifier.ts
|
|
5568
|
+
function verifyLocalShareEnvelope(envelope, pinnedFingerprint, home = homedir9()) {
|
|
5569
|
+
let key;
|
|
5570
|
+
try {
|
|
5571
|
+
const pubkeyBytes = readLocalSharePublicKeyRaw(home);
|
|
5572
|
+
key = {
|
|
5573
|
+
pubkeyBytes,
|
|
5574
|
+
// The fingerprint of the key we are ABOUT to verify with (the on-disk key).
|
|
5575
|
+
fingerprintHex: localSharePubkeyFingerprint(home),
|
|
5576
|
+
// The trusted set comes from the PIN — a different artifact than the key.
|
|
5577
|
+
// If the on-disk key was swapped, fingerprintHex ∉ this set → the core's
|
|
5578
|
+
// self-check fails → hard drop. This Set is deliberately NOT built from
|
|
5579
|
+
// `fingerprintHex`/`pubkeyBytes` (that would be vacuous — defect D4).
|
|
5580
|
+
knownFingerprints: /* @__PURE__ */ new Set([pinnedFingerprint]),
|
|
5581
|
+
keyLabel: "local-share"
|
|
5582
|
+
};
|
|
5583
|
+
} catch (err) {
|
|
5584
|
+
return {
|
|
5585
|
+
kind: "error",
|
|
5586
|
+
reason: `local-share pubkey unavailable: ${err instanceof Error ? err.message : String(err)}`
|
|
5587
|
+
};
|
|
5588
|
+
}
|
|
5589
|
+
return verifyEd25519SignedEnvelope(key, envelope);
|
|
5590
|
+
}
|
|
5591
|
+
|
|
5592
|
+
// src/shared-memory-sync.ts
|
|
5593
|
+
init_memory_db();
|
|
5594
|
+
|
|
5595
|
+
// src/shared-memory-audit.ts
|
|
5596
|
+
function latestSessionId(db) {
|
|
5597
|
+
try {
|
|
5598
|
+
const row = db.prepare(`SELECT session_id FROM sessions ORDER BY started_at_epoch DESC LIMIT 1`).get();
|
|
5599
|
+
return row?.session_id ?? null;
|
|
5600
|
+
} catch {
|
|
5601
|
+
return null;
|
|
5602
|
+
}
|
|
5603
|
+
}
|
|
5604
|
+
function writeSharedAudit(db, eventType, evidence, metadata, actor = "hook") {
|
|
5605
|
+
const sessionId = latestSessionId(db);
|
|
5606
|
+
if (!sessionId) return;
|
|
5607
|
+
try {
|
|
5608
|
+
db.prepare(
|
|
5609
|
+
`INSERT INTO audit_log (session_id, event_type, actor, evidence, metadata)
|
|
5610
|
+
VALUES (?, ?, ?, ?, ?)`
|
|
5611
|
+
).run(sessionId, eventType, actor, evidence, JSON.stringify(metadata));
|
|
5612
|
+
} catch {
|
|
5613
|
+
}
|
|
5614
|
+
}
|
|
5615
|
+
|
|
5616
|
+
// src/shared-memory-sync.ts
|
|
5617
|
+
var REPO_ID_RE = /^[0-9a-f-]{36}$/;
|
|
5618
|
+
var SHARED_CURSOR_PREFIX = "shared_cursor:";
|
|
5619
|
+
var ZERO_IMPORT = { imported: 0, dropped: 0, revoked: 0, skipped: 0 };
|
|
5620
|
+
async function importSharedMemories(db, transport, opts = {}) {
|
|
5621
|
+
const home = opts.home ?? homedir10();
|
|
5622
|
+
const nowEpoch = opts.nowEpoch ?? Math.floor(Date.now() / 1e3);
|
|
5623
|
+
const config = getConfig();
|
|
5624
|
+
const tier = opts.tier ?? getCachedTierReadOnly(db);
|
|
5625
|
+
if (!entitledForCrossRepoSurfacing(tier)) return { ...ZERO_IMPORT };
|
|
5626
|
+
const subscribe = opts.subscribe ?? config.memory?.share?.subscribe ?? [];
|
|
5627
|
+
if (!Array.isArray(subscribe) || subscribe.length === 0) return { ...ZERO_IMPORT };
|
|
5628
|
+
const ownRepoId = getRepoId(db);
|
|
5629
|
+
const result = { ...ZERO_IMPORT };
|
|
5630
|
+
for (const subscribedLabel of subscribe) {
|
|
5631
|
+
const entry = findRepoByLabel(home, subscribedLabel);
|
|
5632
|
+
if (!entry || !REPO_ID_RE.test(entry.repo_id)) continue;
|
|
5633
|
+
const originRepoId = entry.repo_id;
|
|
5634
|
+
if (ownRepoId && originRepoId === ownRepoId) continue;
|
|
5635
|
+
const pinned = getSharedPin(db, originRepoId) ?? tofuPinSharedFingerprint(db, originRepoId, entry.pubkey_fingerprint);
|
|
5636
|
+
const cursorKey = SHARED_CURSOR_PREFIX + originRepoId;
|
|
5637
|
+
const since = parseCursor2(getMemoryMeta(db, cursorKey));
|
|
5638
|
+
let envelopes;
|
|
5639
|
+
try {
|
|
5640
|
+
envelopes = await transport.fetchSince(originRepoId, since);
|
|
5641
|
+
} catch {
|
|
5642
|
+
continue;
|
|
5643
|
+
}
|
|
5644
|
+
let maxSeq = since;
|
|
5645
|
+
const advancePast = (env) => {
|
|
5646
|
+
if (typeof env.seq === "number" && env.seq > maxSeq) maxSeq = env.seq;
|
|
5647
|
+
};
|
|
5648
|
+
for (const env of envelopes) {
|
|
5649
|
+
const verdict = verifyLocalShareEnvelope(env, pinned, home);
|
|
5650
|
+
if (verdict.kind !== "valid") {
|
|
5651
|
+
result.dropped += 1;
|
|
5652
|
+
drop(db, "bad_signature", { kind: verdict.kind, origin: originRepoId });
|
|
5653
|
+
if (verdict.kind === "bad_signature") advancePast(env);
|
|
5654
|
+
continue;
|
|
5655
|
+
}
|
|
5656
|
+
if (!hasSignedLoadBearingKeys(env)) {
|
|
5657
|
+
result.dropped += 1;
|
|
5658
|
+
drop(db, "unsigned_field", { origin: originRepoId });
|
|
5659
|
+
advancePast(env);
|
|
5660
|
+
continue;
|
|
5661
|
+
}
|
|
5662
|
+
if (env.origin_repo_id !== originRepoId) {
|
|
5663
|
+
result.dropped += 1;
|
|
5664
|
+
drop(db, "origin_mismatch", { expected: originRepoId, got: env.origin_repo_id });
|
|
5665
|
+
advancePast(env);
|
|
5666
|
+
continue;
|
|
5667
|
+
}
|
|
5668
|
+
if (ownRepoId && env.origin_repo_id === ownRepoId) {
|
|
5669
|
+
result.dropped += 1;
|
|
5670
|
+
advancePast(env);
|
|
5671
|
+
continue;
|
|
5672
|
+
}
|
|
5673
|
+
advancePast(env);
|
|
5674
|
+
for (const revokedHash of parseStringArray(env.revokes_json)) {
|
|
5675
|
+
if (applyRevocation(db, revokedHash, nowEpoch)) result.revoked += 1;
|
|
5676
|
+
}
|
|
5677
|
+
const originLabel = deriveRepoLabel(entry.label);
|
|
5678
|
+
for (const rec of parseRecords(env.records_json)) {
|
|
5679
|
+
if (!isWellFormedRecord(rec)) {
|
|
5680
|
+
result.dropped += 1;
|
|
5681
|
+
drop(db, "malformed_record", { origin: originRepoId });
|
|
5682
|
+
continue;
|
|
5683
|
+
}
|
|
5684
|
+
const computed = hashSharedMemoryRecord(stripHash(rec));
|
|
5685
|
+
if (computed !== rec.record_hash) {
|
|
5686
|
+
result.dropped += 1;
|
|
5687
|
+
drop(db, "record_hash_mismatch", { origin: originRepoId });
|
|
5688
|
+
continue;
|
|
5689
|
+
}
|
|
5690
|
+
if (pendingExists(db, rec.record_hash)) {
|
|
5691
|
+
result.skipped += 1;
|
|
5692
|
+
continue;
|
|
5693
|
+
}
|
|
5694
|
+
insertPending(db, rec, env, originRepoId, originLabel, nowEpoch);
|
|
5695
|
+
result.imported += 1;
|
|
5696
|
+
writeSharedAudit(db, "shared_memory_imported", `pending from ${originLabel}`, {
|
|
5697
|
+
record_hash: rec.record_hash,
|
|
5698
|
+
origin_repo_id: originRepoId
|
|
5699
|
+
});
|
|
5700
|
+
}
|
|
5701
|
+
}
|
|
5702
|
+
if (maxSeq > since) setMemoryMeta(db, cursorKey, String(maxSeq));
|
|
5703
|
+
}
|
|
5704
|
+
return result;
|
|
5705
|
+
}
|
|
5706
|
+
var SIGNED_LOAD_BEARING_KEYS = ["origin_repo_id", "records_json", "revokes_json"];
|
|
5707
|
+
function hasSignedLoadBearingKeys(env) {
|
|
5708
|
+
const signed = Array.isArray(env._signature_payload_keys) ? env._signature_payload_keys : [];
|
|
5709
|
+
return SIGNED_LOAD_BEARING_KEYS.every((k) => signed.includes(k));
|
|
5710
|
+
}
|
|
5711
|
+
function parseCursor2(raw) {
|
|
5712
|
+
if (raw === null) return 0;
|
|
5713
|
+
const n = Number(raw);
|
|
5714
|
+
return Number.isInteger(n) && n >= 0 ? n : 0;
|
|
5715
|
+
}
|
|
5716
|
+
function parseStringArray(json) {
|
|
5717
|
+
try {
|
|
5718
|
+
const v = JSON.parse(json);
|
|
5719
|
+
return Array.isArray(v) ? v.filter((x) => typeof x === "string") : [];
|
|
5720
|
+
} catch {
|
|
5721
|
+
return [];
|
|
5722
|
+
}
|
|
5723
|
+
}
|
|
5724
|
+
function parseRecords(json) {
|
|
5725
|
+
try {
|
|
5726
|
+
const v = JSON.parse(json);
|
|
5727
|
+
return Array.isArray(v) ? v : [];
|
|
5728
|
+
} catch {
|
|
5729
|
+
return [];
|
|
5730
|
+
}
|
|
5731
|
+
}
|
|
5732
|
+
function isWellFormedRecord(r) {
|
|
5733
|
+
if (!r || typeof r !== "object") return false;
|
|
5734
|
+
const o = r;
|
|
5735
|
+
return typeof o.record_hash === "string" && typeof o.type === "string" && typeof o.title === "string" && typeof o.detail === "string" && typeof o.importance === "number" && typeof o.created_at_epoch === "number" && (o.superseded_by_hash === null || typeof o.superseded_by_hash === "string");
|
|
5736
|
+
}
|
|
5737
|
+
function stripHash(r) {
|
|
5738
|
+
return {
|
|
5739
|
+
type: r.type,
|
|
5740
|
+
title: r.title,
|
|
5741
|
+
detail: r.detail,
|
|
5742
|
+
importance: r.importance,
|
|
5743
|
+
created_at_epoch: r.created_at_epoch,
|
|
5744
|
+
superseded_by_hash: r.superseded_by_hash
|
|
5745
|
+
};
|
|
5746
|
+
}
|
|
5747
|
+
function pendingExists(db, recordHash) {
|
|
5748
|
+
return !!db.prepare(`SELECT 1 FROM shared_memory_pending WHERE record_hash = ? LIMIT 1`).get(recordHash);
|
|
5749
|
+
}
|
|
5750
|
+
function loadPending(db, recordHash) {
|
|
5751
|
+
return db.prepare(
|
|
5752
|
+
`SELECT id, record_hash, origin_repo_id, origin_repo_label, envelope_raw, record_json,
|
|
5753
|
+
accepted_at_epoch, refused_at_epoch, expired_at_epoch
|
|
5754
|
+
FROM shared_memory_pending WHERE record_hash = ? LIMIT 1`
|
|
5755
|
+
).get(recordHash) ?? null;
|
|
5756
|
+
}
|
|
5757
|
+
function insertPending(db, rec, env, originRepoId, originLabel, nowEpoch) {
|
|
5758
|
+
db.prepare(
|
|
5759
|
+
`INSERT OR IGNORE INTO shared_memory_pending
|
|
5760
|
+
(record_hash, origin_repo_id, origin_repo_label, envelope_raw, record_json, received_at_epoch)
|
|
5761
|
+
VALUES (?, ?, ?, ?, ?, ?)`
|
|
5762
|
+
).run(rec.record_hash, originRepoId, originLabel, JSON.stringify(env), JSON.stringify(rec), nowEpoch);
|
|
5763
|
+
}
|
|
5764
|
+
function applyRevocation(db, recordHash, nowEpoch) {
|
|
5765
|
+
const row = loadPending(db, recordHash);
|
|
5766
|
+
if (!row) return false;
|
|
5767
|
+
let changed = false;
|
|
5768
|
+
if (row.accepted_at_epoch !== null) {
|
|
5769
|
+
const iso = new Date(nowEpoch * 1e3).toISOString();
|
|
5770
|
+
const res = db.prepare(
|
|
5771
|
+
`UPDATE observations
|
|
5772
|
+
SET valid_to = ?, expired_at = ?, valid_to_epoch = ?, expired_at_epoch = ?
|
|
5773
|
+
WHERE origin = ?
|
|
5774
|
+
AND expired_at IS NULL
|
|
5775
|
+
AND json_extract(evidence, '$.record_hash') = ?`
|
|
5776
|
+
).run(iso, iso, nowEpoch, nowEpoch, `repo:${row.origin_repo_id}`, recordHash);
|
|
5777
|
+
changed = res.changes > 0;
|
|
5778
|
+
}
|
|
5779
|
+
const p = db.prepare(`UPDATE shared_memory_pending SET expired_at_epoch = ? WHERE record_hash = ? AND expired_at_epoch IS NULL`).run(nowEpoch, recordHash);
|
|
5780
|
+
changed = changed || p.changes > 0;
|
|
5781
|
+
if (changed) writeSharedAudit(db, "shared_memory_revoked", `revoked ${recordHash}`, { record_hash: recordHash });
|
|
5782
|
+
return changed;
|
|
5783
|
+
}
|
|
5784
|
+
function drop(db, reason, meta) {
|
|
5785
|
+
recordTelemetry(db, "shared_memory_dropped", { reason, ...meta });
|
|
5786
|
+
writeSharedAudit(db, "shared_memory_dropped", `dropped: ${reason}`, { reason, ...meta });
|
|
5787
|
+
}
|
|
5788
|
+
|
|
5789
|
+
// src/shared-memory-export.ts
|
|
5790
|
+
init_config();
|
|
5791
|
+
import { homedir as homedir11 } from "os";
|
|
5792
|
+
import { join as join9, dirname as dirname12 } from "path";
|
|
5793
|
+
import { mkdirSync as mkdirSync12, chmodSync as chmodSync7, writeFileSync as writeFileSync9, renameSync as renameSync3, openSync as openSync3, fsyncSync as fsyncSync3, closeSync as closeSync3 } from "fs";
|
|
5794
|
+
|
|
5795
|
+
// src/consolidation-config.ts
|
|
5796
|
+
init_config();
|
|
5797
|
+
var DEFAULT_CONSOLIDATION_CONFIG = {
|
|
5798
|
+
enabled: true,
|
|
5799
|
+
sessionSweepEnabled: true,
|
|
5800
|
+
// llmEndpoint / llmModel deliberately UNSET: the shipped default is
|
|
5801
|
+
// zero-LLM, zero-network, works offline on any machine.
|
|
5802
|
+
summarizeAfterDays: 5,
|
|
5803
|
+
// inside the 7-day conversation_turns prune window
|
|
5804
|
+
retentionDays: 90,
|
|
5805
|
+
importanceFloor: 2,
|
|
5806
|
+
protectedTypes: ["decision", "cr_violation", "incident_near_miss"],
|
|
5807
|
+
usageWarmupDays: 30,
|
|
5808
|
+
usageDecay: 0.9,
|
|
5809
|
+
reweightIntervalDays: 1,
|
|
5810
|
+
promoteMinOccurrences: 3,
|
|
5811
|
+
budgetMs: 3e3,
|
|
5812
|
+
suggestUpgrades: true,
|
|
5813
|
+
suggestIntervalDays: 30
|
|
5814
|
+
};
|
|
5815
|
+
function resolveConsolidationConfig() {
|
|
5816
|
+
try {
|
|
5817
|
+
const c = getConfig().memory?.consolidation;
|
|
5818
|
+
if (!c) return { ...DEFAULT_CONSOLIDATION_CONFIG };
|
|
5819
|
+
return { ...DEFAULT_CONSOLIDATION_CONFIG, ...c };
|
|
5820
|
+
} catch {
|
|
5821
|
+
return { ...DEFAULT_CONSOLIDATION_CONFIG };
|
|
5822
|
+
}
|
|
5823
|
+
}
|
|
5824
|
+
|
|
5825
|
+
// src/memory-llm.ts
|
|
5826
|
+
var LLM_API_KEY_ENV = "MASSU_MEMORY_LLM_API_KEY";
|
|
5827
|
+
var DEFAULT_BUDGET_MS = 2e4;
|
|
5828
|
+
var DEFAULT_MAX_CHARS = 900;
|
|
5829
|
+
var NOISE_PATTERNS = [
|
|
5830
|
+
/<command-(name|message|args)>/i,
|
|
5831
|
+
/<local-command-[^>]*>/i,
|
|
5832
|
+
/^\s*<[a-z-]+>\s*$/i,
|
|
5833
|
+
/^\s*(ok|okay|thanks|thank you|yes|no|yep|nope|sure|continue|proceed|go ahead)\s*[.!]?\s*$/i,
|
|
5834
|
+
/^\s*\/[a-z-]+\s*$/i,
|
|
5835
|
+
// a bare slash-command invocation
|
|
5836
|
+
/system-reminder/i
|
|
5837
|
+
];
|
|
5838
|
+
function isSummarizableSignal(text) {
|
|
5839
|
+
const t = text.trim();
|
|
5840
|
+
if (t.length < 25) return false;
|
|
5841
|
+
return !NOISE_PATTERNS.some((re) => re.test(t));
|
|
5842
|
+
}
|
|
5843
|
+
var CREDENTIAL_LABELS = ["api[_-]?key", "secret", "password", "token"];
|
|
5844
|
+
var LABELLED_CREDENTIAL = new RegExp(
|
|
5845
|
+
`\\b[A-Za-z0-9_-]*(?:${CREDENTIAL_LABELS.join("|")})["'\\s:=]+[A-Za-z0-9_\\-/+]{16,}`,
|
|
5846
|
+
"gi"
|
|
5847
|
+
);
|
|
5848
|
+
var SECRET_PATTERNS = [
|
|
5849
|
+
[/\bms_live_[A-Za-z0-9_-]{6,}/g, "ms_live_[REDACTED]", "MASSU_LIVE_KEY"],
|
|
5850
|
+
[/\bsk-[A-Za-z0-9_-]{16,}/g, "sk-[REDACTED]", "OPENAI_STYLE_KEY"],
|
|
5851
|
+
[/\b(gh[pousr]|github_pat)_[A-Za-z0-9_]{16,}/g, "[REDACTED_TOKEN]", "GITHUB_TOKEN"],
|
|
5852
|
+
[/\bsbp_[A-Za-z0-9]{16,}/g, "sbp_[REDACTED]", "SUPABASE_TOKEN"],
|
|
5853
|
+
[/\bAKIA[0-9A-Z]{12,}/g, "[REDACTED_AWS_KEY]", "AWS_ACCESS_KEY"],
|
|
5854
|
+
[
|
|
5855
|
+
/\bey[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{6,}/g,
|
|
5856
|
+
"[REDACTED_JWT]",
|
|
5857
|
+
"JWT"
|
|
5858
|
+
],
|
|
5859
|
+
[LABELLED_CREDENTIAL, "[REDACTED_CREDENTIAL]", "LABELLED_CREDENTIAL"]
|
|
5860
|
+
];
|
|
5861
|
+
function redactSecrets(text) {
|
|
5862
|
+
let out = text;
|
|
5863
|
+
for (const [re, replacement] of SECRET_PATTERNS) out = out.replace(re, replacement);
|
|
5864
|
+
return out;
|
|
5865
|
+
}
|
|
5866
|
+
function containsSecret(text) {
|
|
5867
|
+
for (const [re, , name] of SECRET_PATTERNS) {
|
|
5868
|
+
re.lastIndex = 0;
|
|
5869
|
+
const hit = re.test(text);
|
|
5870
|
+
re.lastIndex = 0;
|
|
5871
|
+
if (hit) return { matched: true, patternName: name };
|
|
5872
|
+
}
|
|
5873
|
+
return { matched: false };
|
|
5874
|
+
}
|
|
5875
|
+
function extractiveSummary(sources, maxChars = DEFAULT_MAX_CHARS) {
|
|
5876
|
+
const cleaned = sources.map((s, i) => ({ ...s, i, text: s.text.replace(/\s+/g, " ").trim() })).filter((s) => isSummarizableSignal(s.text));
|
|
5877
|
+
if (cleaned.length === 0) return "";
|
|
5878
|
+
const ranked = [...cleaned].sort((a, b) => b.weight - a.weight || a.i - b.i);
|
|
5879
|
+
const picked = [];
|
|
5880
|
+
let used = 0;
|
|
5881
|
+
for (const s of ranked) {
|
|
5882
|
+
const cost = s.text.length + 1;
|
|
5883
|
+
if (used + cost > maxChars) continue;
|
|
5884
|
+
picked.push(s);
|
|
5885
|
+
used += cost;
|
|
5886
|
+
}
|
|
5887
|
+
if (picked.length === 0) {
|
|
5888
|
+
return ranked[0].text.slice(0, maxChars);
|
|
5889
|
+
}
|
|
5890
|
+
picked.sort((a, b) => a.i - b.i);
|
|
5891
|
+
return picked.map((s) => s.text).join(" ");
|
|
5892
|
+
}
|
|
5893
|
+
async function summarizeViaEndpoint(endpoint, model, material, budgetMs, maxChars) {
|
|
5894
|
+
try {
|
|
5895
|
+
const controller = new AbortController();
|
|
5896
|
+
const timer = setTimeout(() => controller.abort(), budgetMs);
|
|
5897
|
+
const apiKey = process.env[LLM_API_KEY_ENV];
|
|
5898
|
+
const headers = { "content-type": "application/json" };
|
|
5899
|
+
if (apiKey) headers["authorization"] = `Bearer ${apiKey}`;
|
|
5900
|
+
let resp;
|
|
5901
|
+
try {
|
|
5902
|
+
resp = await fetch(`${endpoint.replace(/\/+$/, "")}/v1/chat/completions`, {
|
|
5903
|
+
method: "POST",
|
|
5904
|
+
headers,
|
|
5905
|
+
body: JSON.stringify({
|
|
5906
|
+
model,
|
|
5907
|
+
// a NAME/ALIAS the user configured — never a physical model id
|
|
5908
|
+
messages: [
|
|
5909
|
+
{
|
|
5910
|
+
role: "system",
|
|
5911
|
+
content: "You distill a software engineering session into ONE durable lesson a developer will read months later. Be concrete and factual. State what broke, what was tried and rejected, and what actually worked. Invent nothing that is not in the input."
|
|
5912
|
+
},
|
|
5913
|
+
{ role: "user", content: material }
|
|
5914
|
+
],
|
|
5915
|
+
max_tokens: 400,
|
|
5916
|
+
temperature: 0.2
|
|
5917
|
+
}),
|
|
5918
|
+
signal: controller.signal
|
|
5919
|
+
});
|
|
5920
|
+
} finally {
|
|
5921
|
+
clearTimeout(timer);
|
|
5922
|
+
}
|
|
5923
|
+
if (!resp.ok) return null;
|
|
5924
|
+
const data = await resp.json();
|
|
5925
|
+
const text = data.choices?.[0]?.message?.content;
|
|
5926
|
+
if (typeof text !== "string" || !text.trim()) return null;
|
|
5927
|
+
return text.trim().slice(0, maxChars);
|
|
5928
|
+
} catch {
|
|
5929
|
+
return null;
|
|
5930
|
+
}
|
|
5931
|
+
}
|
|
5932
|
+
async function summarizeText(sources, opts = {}) {
|
|
5933
|
+
const cfg = opts.config ?? resolveConsolidationConfig();
|
|
5934
|
+
const maxChars = opts.maxChars ?? DEFAULT_MAX_CHARS;
|
|
5935
|
+
const budgetMs = opts.budgetMs ?? DEFAULT_BUDGET_MS;
|
|
5936
|
+
const extractive = extractiveSummary(sources, maxChars);
|
|
5937
|
+
if (!cfg.llmEndpoint || !cfg.llmModel || !extractive) {
|
|
5938
|
+
return { text: extractive, tier: "extractive" };
|
|
5939
|
+
}
|
|
5940
|
+
const material = redactSecrets(
|
|
5941
|
+
sources.map((s) => s.text.replace(/\s+/g, " ").trim()).filter(Boolean).join("\n")
|
|
5942
|
+
);
|
|
5943
|
+
const viaModel = await summarizeViaEndpoint(
|
|
5944
|
+
cfg.llmEndpoint,
|
|
5945
|
+
cfg.llmModel,
|
|
5946
|
+
material,
|
|
5947
|
+
budgetMs,
|
|
5948
|
+
maxChars
|
|
5949
|
+
);
|
|
5950
|
+
return viaModel ? { text: viaModel, tier: "model" } : { text: extractive, tier: "extractive" };
|
|
5951
|
+
}
|
|
5952
|
+
|
|
5953
|
+
// src/shared-memory-export.ts
|
|
5954
|
+
init_memory_db();
|
|
5955
|
+
var DEFAULT_EXPORT_CAPS = {
|
|
5956
|
+
maxRecords: 200,
|
|
5957
|
+
maxBytesPerRecord: 16384,
|
|
5958
|
+
maxTotalBytes: 512e3
|
|
5959
|
+
};
|
|
5960
|
+
var EXPORT_SEQ_KEY = "shared_export_seq";
|
|
5961
|
+
var BACKUP_DONE_KEY = "shared_backup_at";
|
|
5962
|
+
function zero(enabled) {
|
|
5963
|
+
return { enabled, published: false, exported: 0, refused: 0, refusals: [], seq: null };
|
|
5964
|
+
}
|
|
5965
|
+
function ensureSharingBackup(db, repoId, home, nowEpoch, backupDirOverride) {
|
|
5966
|
+
if (getMemoryMeta(db, BACKUP_DONE_KEY)) return;
|
|
5967
|
+
const backupDir = backupDirOverride ?? join9(home, ".massu", "shared", "backups", repoId);
|
|
5968
|
+
const dest = join9(backupDir, `memory-${nowEpoch}.db`);
|
|
5969
|
+
const snapshot = db.serialize();
|
|
5970
|
+
mkdirSync12(dirname12(dest), { recursive: true, mode: 448 });
|
|
5971
|
+
const tmp = `${dest}.tmp-${process.pid}`;
|
|
5972
|
+
writeFileSync9(tmp, snapshot, { mode: 384 });
|
|
5973
|
+
const fd = openSync3(tmp, "r+");
|
|
5974
|
+
fsyncSync3(fd);
|
|
5975
|
+
closeSync3(fd);
|
|
5976
|
+
renameSync3(tmp, dest);
|
|
5977
|
+
chmodSync7(dest, 384);
|
|
5978
|
+
setMemoryMeta(db, BACKUP_DONE_KEY, String(nowEpoch));
|
|
5979
|
+
}
|
|
5980
|
+
async function exportSharedMemories(db, transport, opts = {}) {
|
|
5981
|
+
const home = opts.home ?? homedir11();
|
|
5982
|
+
const nowEpoch = opts.nowEpoch ?? Math.floor(Date.now() / 1e3);
|
|
5983
|
+
const caps = opts.caps ?? DEFAULT_EXPORT_CAPS;
|
|
5984
|
+
const config = getConfig();
|
|
5985
|
+
const shareEnabled = opts.shareEnabled ?? config.memory?.share?.enabled ?? false;
|
|
5986
|
+
if (!shareEnabled) return zero(false);
|
|
5987
|
+
const tier = opts.tier ?? getCachedTierReadOnly(db);
|
|
5988
|
+
if (!entitledForCrossRepoSurfacing(tier)) return zero(true);
|
|
5989
|
+
const repoId = getRepoId(db) ?? mintRepoId(db);
|
|
5990
|
+
const label = getRepoLabel();
|
|
5991
|
+
ensureLocalShareKeypair(home);
|
|
5992
|
+
const fingerprint = localSharePubkeyFingerprint(home);
|
|
5993
|
+
upsertRepoRegistration(
|
|
5994
|
+
{
|
|
5995
|
+
repo_id: repoId,
|
|
5996
|
+
label,
|
|
5997
|
+
pubkey_fingerprint: fingerprint,
|
|
5998
|
+
last_seen_path: getProjectRoot(),
|
|
5999
|
+
share_enabled: true
|
|
6000
|
+
},
|
|
6001
|
+
home
|
|
6002
|
+
);
|
|
6003
|
+
try {
|
|
6004
|
+
ensureSharingBackup(db, repoId, home, nowEpoch, opts.backupDir);
|
|
6005
|
+
} catch (err) {
|
|
6006
|
+
recordTelemetry(db, "shared_memory_export_refused", {
|
|
6007
|
+
reason: "backup_failed",
|
|
6008
|
+
detail: err instanceof Error ? err.message : String(err)
|
|
6009
|
+
});
|
|
6010
|
+
auditExportRefused(db, null, "backup_failed");
|
|
6011
|
+
return { ...zero(true), refused: 1, refusals: [{ observation_id: null, reason: "backup_failed" }] };
|
|
6012
|
+
}
|
|
6013
|
+
const rows = db.prepare(
|
|
6014
|
+
`SELECT id, type, title, detail, importance, created_at_epoch, origin
|
|
6015
|
+
FROM observations
|
|
6016
|
+
WHERE shareable = 1
|
|
6017
|
+
ORDER BY id
|
|
6018
|
+
LIMIT ?`
|
|
6019
|
+
).all(caps.maxRecords + 1);
|
|
6020
|
+
if (rows.length > caps.maxRecords) {
|
|
6021
|
+
recordTelemetry(db, "shared_memory_export_refused", { reason: "too_many_records", count: rows.length });
|
|
6022
|
+
auditExportRefused(db, null, "too_many_records");
|
|
6023
|
+
return { ...zero(true), refused: 1, refusals: [{ observation_id: null, reason: "too_many_records" }] };
|
|
6024
|
+
}
|
|
6025
|
+
const wasExported = db.prepare(`SELECT 1 FROM shared_memory_outbound WHERE record_hash = ? LIMIT 1`);
|
|
6026
|
+
const refusals = [];
|
|
6027
|
+
const records = [];
|
|
6028
|
+
const outboundInserts = [];
|
|
6029
|
+
for (const r of rows) {
|
|
6030
|
+
if (!isLocalOrigin(r.origin)) {
|
|
6031
|
+
refusals.push({ observation_id: r.id, reason: "non_local_origin" });
|
|
6032
|
+
auditExportRefused(db, r.id, "non_local_origin");
|
|
6033
|
+
continue;
|
|
6034
|
+
}
|
|
6035
|
+
const title = r.title ?? "";
|
|
6036
|
+
const detail = r.detail ?? "";
|
|
6037
|
+
const sTitle = containsSecret(title);
|
|
6038
|
+
const sDetail = containsSecret(detail);
|
|
6039
|
+
if (sTitle.matched || sDetail.matched) {
|
|
6040
|
+
const reason = `secret:${sTitle.patternName ?? sDetail.patternName}`;
|
|
6041
|
+
refusals.push({ observation_id: r.id, reason });
|
|
6042
|
+
auditExportRefused(db, r.id, reason);
|
|
6043
|
+
continue;
|
|
6044
|
+
}
|
|
6045
|
+
if (home && (title.includes(home) || detail.includes(home))) {
|
|
6046
|
+
refusals.push({ observation_id: r.id, reason: "home_path" });
|
|
6047
|
+
auditExportRefused(db, r.id, "home_path");
|
|
6048
|
+
continue;
|
|
6049
|
+
}
|
|
6050
|
+
const base = {
|
|
6051
|
+
type: r.type,
|
|
6052
|
+
title,
|
|
6053
|
+
detail,
|
|
6054
|
+
importance: r.importance,
|
|
6055
|
+
created_at_epoch: r.created_at_epoch,
|
|
6056
|
+
superseded_by_hash: null
|
|
6057
|
+
};
|
|
6058
|
+
const record_hash = hashSharedMemoryRecord(base);
|
|
6059
|
+
if (wasExported.get(record_hash)) continue;
|
|
6060
|
+
const recordBytes = Buffer.byteLength(JSON.stringify({ ...base, record_hash }), "utf-8");
|
|
6061
|
+
if (recordBytes > caps.maxBytesPerRecord) {
|
|
6062
|
+
refusals.push({ observation_id: r.id, reason: "record_too_large" });
|
|
6063
|
+
auditExportRefused(db, r.id, "record_too_large");
|
|
6064
|
+
continue;
|
|
6065
|
+
}
|
|
6066
|
+
records.push({ ...base, record_hash });
|
|
6067
|
+
outboundInserts.push({ hash: record_hash, id: r.id });
|
|
6068
|
+
}
|
|
6069
|
+
const revokeRows = db.prepare(
|
|
6070
|
+
`SELECT o.record_hash AS h
|
|
6071
|
+
FROM shared_memory_outbound o
|
|
6072
|
+
JOIN observations obs ON obs.id = o.observation_id
|
|
6073
|
+
WHERE o.revoked_at_epoch IS NULL
|
|
6074
|
+
AND obs.expired_at IS NOT NULL
|
|
6075
|
+
LIMIT ?`
|
|
6076
|
+
).all(caps.maxRecords);
|
|
6077
|
+
const revokedHashes = revokeRows.map((r) => r.h);
|
|
6078
|
+
if (records.length === 0 && revokedHashes.length === 0) {
|
|
6079
|
+
return { ...zero(true), refused: refusals.length, refusals };
|
|
6080
|
+
}
|
|
6081
|
+
const recordsJson = JSON.stringify(records);
|
|
6082
|
+
if (Buffer.byteLength(recordsJson, "utf-8") > caps.maxTotalBytes) {
|
|
6083
|
+
recordTelemetry(db, "shared_memory_export_refused", { reason: "envelope_too_large" });
|
|
6084
|
+
auditExportRefused(db, null, "envelope_too_large");
|
|
6085
|
+
return { ...zero(true), refused: refusals.length + 1, refusals: [...refusals, { observation_id: null, reason: "envelope_too_large" }] };
|
|
6086
|
+
}
|
|
6087
|
+
const seq = nextSeq(db);
|
|
6088
|
+
const body = {
|
|
6089
|
+
kind: SHARED_MEMORY_KIND,
|
|
6090
|
+
origin_repo_id: repoId,
|
|
6091
|
+
origin_repo_label: label,
|
|
6092
|
+
seq,
|
|
6093
|
+
issued_at: new Date(nowEpoch * 1e3).toISOString(),
|
|
6094
|
+
records_json: recordsJson,
|
|
6095
|
+
revokes_json: JSON.stringify(revokedHashes)
|
|
6096
|
+
};
|
|
6097
|
+
const envelope = signSharedMemoryEnvelope(body, home);
|
|
6098
|
+
await transport.publish(envelope);
|
|
6099
|
+
const insertOutbound = db.prepare(
|
|
6100
|
+
`INSERT OR IGNORE INTO shared_memory_outbound (record_hash, observation_id, origin_repo_id, seq, exported_at_epoch)
|
|
6101
|
+
VALUES (?, ?, ?, ?, ?)`
|
|
6102
|
+
);
|
|
6103
|
+
const markRevoked = db.prepare(
|
|
6104
|
+
`UPDATE shared_memory_outbound SET revoked_at_epoch = ? WHERE record_hash = ? AND revoked_at_epoch IS NULL`
|
|
6105
|
+
);
|
|
6106
|
+
const commit = db.transaction(() => {
|
|
6107
|
+
for (const o of outboundInserts) insertOutbound.run(o.hash, o.id, repoId, seq, nowEpoch);
|
|
6108
|
+
for (const h of revokedHashes) markRevoked.run(nowEpoch, h);
|
|
6109
|
+
setMemoryMeta(db, EXPORT_SEQ_KEY, String(seq));
|
|
6110
|
+
});
|
|
6111
|
+
commit();
|
|
6112
|
+
recordTelemetry(db, "shared_memory_exported", { count: records.length, revoked: revokedHashes.length, seq });
|
|
6113
|
+
auditExported(db, records.length, seq);
|
|
6114
|
+
return { enabled: true, published: true, exported: records.length, refused: refusals.length, refusals, seq };
|
|
6115
|
+
}
|
|
6116
|
+
function nextSeq(db) {
|
|
6117
|
+
const raw = getMemoryMeta(db, EXPORT_SEQ_KEY);
|
|
6118
|
+
const last = raw !== null && Number.isInteger(Number(raw)) ? Number(raw) : 0;
|
|
6119
|
+
return last + 1;
|
|
6120
|
+
}
|
|
6121
|
+
function auditExported(db, count, seq) {
|
|
6122
|
+
writeSharedAudit(db, "shared_memory_exported", `exported ${count} record(s) at seq ${seq}`, { count, seq });
|
|
6123
|
+
}
|
|
6124
|
+
function auditExportRefused(db, obsId, reason) {
|
|
6125
|
+
writeSharedAudit(db, "shared_memory_export_refused", `export refused: ${reason}`, { observation_id: obsId, reason });
|
|
6126
|
+
}
|
|
6127
|
+
|
|
6128
|
+
// src/shared-memory-transport.ts
|
|
6129
|
+
import {
|
|
6130
|
+
chmodSync as chmodSync8,
|
|
6131
|
+
existsSync as existsSync15,
|
|
6132
|
+
mkdirSync as mkdirSync13,
|
|
6133
|
+
readdirSync as readdirSync2,
|
|
6134
|
+
readFileSync as readFileSync11
|
|
6135
|
+
} from "fs";
|
|
6136
|
+
import { homedir as homedir12 } from "os";
|
|
6137
|
+
import { join as join10 } from "path";
|
|
6138
|
+
import { createHash as createHash4 } from "crypto";
|
|
6139
|
+
var REPO_ID_PATH_RE = /^[0-9a-f-]{36}$/;
|
|
6140
|
+
var CONTENT_HASH_RE = /^[0-9a-f]{64}$/;
|
|
6141
|
+
function sharedRootDir(home = homedir12()) {
|
|
6142
|
+
return join10(home, ".massu", "shared");
|
|
6143
|
+
}
|
|
6144
|
+
function outboxDirFor(originRepoId, home = homedir12()) {
|
|
6145
|
+
if (!REPO_ID_PATH_RE.test(originRepoId)) {
|
|
6146
|
+
throw new Error(`refusing to use an unvalidated repo_id as a path component: ${originRepoId}`);
|
|
6147
|
+
}
|
|
6148
|
+
const rel = join10("outbox", originRepoId);
|
|
6149
|
+
return assertContainedIn(sharedRootDir(home), rel, { allowNested: true });
|
|
6150
|
+
}
|
|
6151
|
+
function envelopeContentHash(env) {
|
|
6152
|
+
return createHash4("sha256").update(JSON.stringify(env), "utf-8").digest("hex");
|
|
6153
|
+
}
|
|
6154
|
+
var LocalFsTransport = class {
|
|
6155
|
+
constructor(home = homedir12()) {
|
|
6156
|
+
this.home = home;
|
|
6157
|
+
}
|
|
6158
|
+
home;
|
|
6159
|
+
async publish(env) {
|
|
6160
|
+
const dir = outboxDirFor(env.origin_repo_id, this.home);
|
|
6161
|
+
mkdirSync13(dir, { recursive: true, mode: 448 });
|
|
6162
|
+
const hash = envelopeContentHash(env);
|
|
6163
|
+
if (!CONTENT_HASH_RE.test(hash)) {
|
|
6164
|
+
throw new Error(`refusing to write an envelope with a malformed content hash: ${hash}`);
|
|
6165
|
+
}
|
|
6166
|
+
const dest = assertContainedIn(dir, `${seqPrefix(env.seq)}-${hash}.json`, { allowNested: false });
|
|
6167
|
+
atomicWriteFileSync(dest, JSON.stringify(env, null, 2));
|
|
6168
|
+
chmodSync8(dest, 384);
|
|
6169
|
+
}
|
|
6170
|
+
async fetchSince(originRepoId, sinceSeq) {
|
|
6171
|
+
if (!REPO_ID_PATH_RE.test(originRepoId)) return [];
|
|
6172
|
+
let dir;
|
|
6173
|
+
try {
|
|
6174
|
+
dir = outboxDirFor(originRepoId, this.home);
|
|
6175
|
+
} catch {
|
|
6176
|
+
return [];
|
|
6177
|
+
}
|
|
6178
|
+
if (!existsSync15(dir)) return [];
|
|
6179
|
+
const out = [];
|
|
6180
|
+
for (const name of readdirSync2(dir)) {
|
|
6181
|
+
if (!name.endsWith(".json")) continue;
|
|
6182
|
+
const seqFromName = parseSeqPrefix(name);
|
|
6183
|
+
if (seqFromName !== null && seqFromName <= sinceSeq) continue;
|
|
6184
|
+
let env;
|
|
6185
|
+
try {
|
|
6186
|
+
const p = assertContainedIn(dir, name, { allowNested: false });
|
|
6187
|
+
env = JSON.parse(readFileSync11(p, "utf-8"));
|
|
6188
|
+
} catch {
|
|
6189
|
+
continue;
|
|
6190
|
+
}
|
|
6191
|
+
if (typeof env?.seq === "number" && env.seq > sinceSeq) out.push(env);
|
|
6192
|
+
}
|
|
6193
|
+
out.sort((a, b) => a.seq - b.seq);
|
|
6194
|
+
return out;
|
|
6195
|
+
}
|
|
6196
|
+
};
|
|
6197
|
+
function seqPrefix(seq) {
|
|
6198
|
+
const s = Number.isInteger(seq) && seq >= 0 ? seq : 0;
|
|
6199
|
+
return String(s).padStart(16, "0");
|
|
6200
|
+
}
|
|
6201
|
+
function parseSeqPrefix(name) {
|
|
6202
|
+
const m = name.match(/^(\d{1,16})-/);
|
|
6203
|
+
return m ? Number(m[1]) : null;
|
|
6204
|
+
}
|
|
6205
|
+
|
|
6206
|
+
// src/hooks/session-end.ts
|
|
6207
|
+
init_config();
|
|
6208
|
+
|
|
6209
|
+
// src/analytics.ts
|
|
6210
|
+
init_config();
|
|
6211
|
+
var DEFAULT_WEIGHTS = {
|
|
6212
|
+
bug_found: -5,
|
|
6213
|
+
vr_failure: -10,
|
|
6214
|
+
incident: -20,
|
|
6215
|
+
cr_violation: -3,
|
|
6216
|
+
vr_pass: 2,
|
|
6217
|
+
clean_commit: 5,
|
|
6218
|
+
successful_verification: 3
|
|
6219
|
+
};
|
|
6220
|
+
var DEFAULT_CATEGORIES = ["security", "architecture", "coupling", "tests", "rule_compliance"];
|
|
4628
6221
|
function getWeights() {
|
|
4629
6222
|
return getConfig().analytics?.quality?.weights ?? DEFAULT_WEIGHTS;
|
|
4630
6223
|
}
|
|
@@ -4761,7 +6354,7 @@ function storeSessionCost(db, sessionId, usage, cost) {
|
|
|
4761
6354
|
|
|
4762
6355
|
// src/prompt-analyzer.ts
|
|
4763
6356
|
init_config();
|
|
4764
|
-
import { createHash as
|
|
6357
|
+
import { createHash as createHash5 } from "crypto";
|
|
4765
6358
|
|
|
4766
6359
|
// src/security-utils.ts
|
|
4767
6360
|
function escapeRegex(str) {
|
|
@@ -4785,7 +6378,7 @@ function categorizePrompt(promptText) {
|
|
|
4785
6378
|
}
|
|
4786
6379
|
function hashPrompt(promptText) {
|
|
4787
6380
|
const normalized = promptText.toLowerCase().replace(/\s+/g, " ").trim();
|
|
4788
|
-
return
|
|
6381
|
+
return createHash5("sha256").update(normalized).digest("hex").slice(0, 16);
|
|
4789
6382
|
}
|
|
4790
6383
|
function detectOutcome(followUpPrompts, assistantResponses) {
|
|
4791
6384
|
let correctionsNeeded = 0;
|
|
@@ -5398,160 +6991,9 @@ ${d.decision}`.trim(),
|
|
|
5398
6991
|
|
|
5399
6992
|
// src/memory-consolidate.ts
|
|
5400
6993
|
init_memory_db();
|
|
5401
|
-
import { createHash as
|
|
5402
|
-
import { existsSync as
|
|
5403
|
-
import { join as
|
|
5404
|
-
|
|
5405
|
-
// src/consolidation-config.ts
|
|
5406
|
-
init_config();
|
|
5407
|
-
var DEFAULT_CONSOLIDATION_CONFIG = {
|
|
5408
|
-
enabled: true,
|
|
5409
|
-
sessionSweepEnabled: true,
|
|
5410
|
-
// llmEndpoint / llmModel deliberately UNSET: the shipped default is
|
|
5411
|
-
// zero-LLM, zero-network, works offline on any machine.
|
|
5412
|
-
summarizeAfterDays: 5,
|
|
5413
|
-
// inside the 7-day conversation_turns prune window
|
|
5414
|
-
retentionDays: 90,
|
|
5415
|
-
importanceFloor: 2,
|
|
5416
|
-
protectedTypes: ["decision", "cr_violation", "incident_near_miss"],
|
|
5417
|
-
usageWarmupDays: 30,
|
|
5418
|
-
usageDecay: 0.9,
|
|
5419
|
-
reweightIntervalDays: 1,
|
|
5420
|
-
promoteMinOccurrences: 3,
|
|
5421
|
-
budgetMs: 3e3,
|
|
5422
|
-
suggestUpgrades: true,
|
|
5423
|
-
suggestIntervalDays: 30
|
|
5424
|
-
};
|
|
5425
|
-
function resolveConsolidationConfig() {
|
|
5426
|
-
try {
|
|
5427
|
-
const c = getConfig().memory?.consolidation;
|
|
5428
|
-
if (!c) return { ...DEFAULT_CONSOLIDATION_CONFIG };
|
|
5429
|
-
return { ...DEFAULT_CONSOLIDATION_CONFIG, ...c };
|
|
5430
|
-
} catch {
|
|
5431
|
-
return { ...DEFAULT_CONSOLIDATION_CONFIG };
|
|
5432
|
-
}
|
|
5433
|
-
}
|
|
5434
|
-
|
|
5435
|
-
// src/memory-llm.ts
|
|
5436
|
-
var LLM_API_KEY_ENV = "MASSU_MEMORY_LLM_API_KEY";
|
|
5437
|
-
var DEFAULT_BUDGET_MS = 2e4;
|
|
5438
|
-
var DEFAULT_MAX_CHARS = 900;
|
|
5439
|
-
var NOISE_PATTERNS = [
|
|
5440
|
-
/<command-(name|message|args)>/i,
|
|
5441
|
-
/<local-command-[^>]*>/i,
|
|
5442
|
-
/^\s*<[a-z-]+>\s*$/i,
|
|
5443
|
-
/^\s*(ok|okay|thanks|thank you|yes|no|yep|nope|sure|continue|proceed|go ahead)\s*[.!]?\s*$/i,
|
|
5444
|
-
/^\s*\/[a-z-]+\s*$/i,
|
|
5445
|
-
// a bare slash-command invocation
|
|
5446
|
-
/system-reminder/i
|
|
5447
|
-
];
|
|
5448
|
-
function isSummarizableSignal(text) {
|
|
5449
|
-
const t = text.trim();
|
|
5450
|
-
if (t.length < 25) return false;
|
|
5451
|
-
return !NOISE_PATTERNS.some((re) => re.test(t));
|
|
5452
|
-
}
|
|
5453
|
-
var CREDENTIAL_LABELS = ["api[_-]?key", "secret", "password", "token"];
|
|
5454
|
-
var LABELLED_CREDENTIAL = new RegExp(
|
|
5455
|
-
`\\b[A-Za-z0-9_-]*(?:${CREDENTIAL_LABELS.join("|")})["'\\s:=]+[A-Za-z0-9_\\-/+]{16,}`,
|
|
5456
|
-
"gi"
|
|
5457
|
-
);
|
|
5458
|
-
var SECRET_PATTERNS = [
|
|
5459
|
-
[/\bms_live_[A-Za-z0-9_-]{6,}/g, "ms_live_[REDACTED]", "MASSU_LIVE_KEY"],
|
|
5460
|
-
[/\bsk-[A-Za-z0-9_-]{16,}/g, "sk-[REDACTED]", "OPENAI_STYLE_KEY"],
|
|
5461
|
-
[/\b(gh[pousr]|github_pat)_[A-Za-z0-9_]{16,}/g, "[REDACTED_TOKEN]", "GITHUB_TOKEN"],
|
|
5462
|
-
[/\bsbp_[A-Za-z0-9]{16,}/g, "sbp_[REDACTED]", "SUPABASE_TOKEN"],
|
|
5463
|
-
[/\bAKIA[0-9A-Z]{12,}/g, "[REDACTED_AWS_KEY]", "AWS_ACCESS_KEY"],
|
|
5464
|
-
[
|
|
5465
|
-
/\bey[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{10,}\.[A-Za-z0-9_-]{6,}/g,
|
|
5466
|
-
"[REDACTED_JWT]",
|
|
5467
|
-
"JWT"
|
|
5468
|
-
],
|
|
5469
|
-
[LABELLED_CREDENTIAL, "[REDACTED_CREDENTIAL]", "LABELLED_CREDENTIAL"]
|
|
5470
|
-
];
|
|
5471
|
-
function redactSecrets(text) {
|
|
5472
|
-
let out = text;
|
|
5473
|
-
for (const [re, replacement] of SECRET_PATTERNS) out = out.replace(re, replacement);
|
|
5474
|
-
return out;
|
|
5475
|
-
}
|
|
5476
|
-
function extractiveSummary(sources, maxChars = DEFAULT_MAX_CHARS) {
|
|
5477
|
-
const cleaned = sources.map((s, i) => ({ ...s, i, text: s.text.replace(/\s+/g, " ").trim() })).filter((s) => isSummarizableSignal(s.text));
|
|
5478
|
-
if (cleaned.length === 0) return "";
|
|
5479
|
-
const ranked = [...cleaned].sort((a, b) => b.weight - a.weight || a.i - b.i);
|
|
5480
|
-
const picked = [];
|
|
5481
|
-
let used = 0;
|
|
5482
|
-
for (const s of ranked) {
|
|
5483
|
-
const cost = s.text.length + 1;
|
|
5484
|
-
if (used + cost > maxChars) continue;
|
|
5485
|
-
picked.push(s);
|
|
5486
|
-
used += cost;
|
|
5487
|
-
}
|
|
5488
|
-
if (picked.length === 0) {
|
|
5489
|
-
return ranked[0].text.slice(0, maxChars);
|
|
5490
|
-
}
|
|
5491
|
-
picked.sort((a, b) => a.i - b.i);
|
|
5492
|
-
return picked.map((s) => s.text).join(" ");
|
|
5493
|
-
}
|
|
5494
|
-
async function summarizeViaEndpoint(endpoint, model, material, budgetMs, maxChars) {
|
|
5495
|
-
try {
|
|
5496
|
-
const controller = new AbortController();
|
|
5497
|
-
const timer = setTimeout(() => controller.abort(), budgetMs);
|
|
5498
|
-
const apiKey = process.env[LLM_API_KEY_ENV];
|
|
5499
|
-
const headers = { "content-type": "application/json" };
|
|
5500
|
-
if (apiKey) headers["authorization"] = `Bearer ${apiKey}`;
|
|
5501
|
-
let resp;
|
|
5502
|
-
try {
|
|
5503
|
-
resp = await fetch(`${endpoint.replace(/\/+$/, "")}/v1/chat/completions`, {
|
|
5504
|
-
method: "POST",
|
|
5505
|
-
headers,
|
|
5506
|
-
body: JSON.stringify({
|
|
5507
|
-
model,
|
|
5508
|
-
// a NAME/ALIAS the user configured — never a physical model id
|
|
5509
|
-
messages: [
|
|
5510
|
-
{
|
|
5511
|
-
role: "system",
|
|
5512
|
-
content: "You distill a software engineering session into ONE durable lesson a developer will read months later. Be concrete and factual. State what broke, what was tried and rejected, and what actually worked. Invent nothing that is not in the input."
|
|
5513
|
-
},
|
|
5514
|
-
{ role: "user", content: material }
|
|
5515
|
-
],
|
|
5516
|
-
max_tokens: 400,
|
|
5517
|
-
temperature: 0.2
|
|
5518
|
-
}),
|
|
5519
|
-
signal: controller.signal
|
|
5520
|
-
});
|
|
5521
|
-
} finally {
|
|
5522
|
-
clearTimeout(timer);
|
|
5523
|
-
}
|
|
5524
|
-
if (!resp.ok) return null;
|
|
5525
|
-
const data = await resp.json();
|
|
5526
|
-
const text = data.choices?.[0]?.message?.content;
|
|
5527
|
-
if (typeof text !== "string" || !text.trim()) return null;
|
|
5528
|
-
return text.trim().slice(0, maxChars);
|
|
5529
|
-
} catch {
|
|
5530
|
-
return null;
|
|
5531
|
-
}
|
|
5532
|
-
}
|
|
5533
|
-
async function summarizeText(sources, opts = {}) {
|
|
5534
|
-
const cfg = opts.config ?? resolveConsolidationConfig();
|
|
5535
|
-
const maxChars = opts.maxChars ?? DEFAULT_MAX_CHARS;
|
|
5536
|
-
const budgetMs = opts.budgetMs ?? DEFAULT_BUDGET_MS;
|
|
5537
|
-
const extractive = extractiveSummary(sources, maxChars);
|
|
5538
|
-
if (!cfg.llmEndpoint || !cfg.llmModel || !extractive) {
|
|
5539
|
-
return { text: extractive, tier: "extractive" };
|
|
5540
|
-
}
|
|
5541
|
-
const material = redactSecrets(
|
|
5542
|
-
sources.map((s) => s.text.replace(/\s+/g, " ").trim()).filter(Boolean).join("\n")
|
|
5543
|
-
);
|
|
5544
|
-
const viaModel = await summarizeViaEndpoint(
|
|
5545
|
-
cfg.llmEndpoint,
|
|
5546
|
-
cfg.llmModel,
|
|
5547
|
-
material,
|
|
5548
|
-
budgetMs,
|
|
5549
|
-
maxChars
|
|
5550
|
-
);
|
|
5551
|
-
return viaModel ? { text: viaModel, tier: "model" } : { text: extractive, tier: "extractive" };
|
|
5552
|
-
}
|
|
5553
|
-
|
|
5554
|
-
// src/memory-consolidate.ts
|
|
6994
|
+
import { createHash as createHash6 } from "crypto";
|
|
6995
|
+
import { existsSync as existsSync16, mkdirSync as mkdirSync14, writeFileSync as writeFileSync10 } from "fs";
|
|
6996
|
+
import { join as join11 } from "path";
|
|
5555
6997
|
init_memory_embedder();
|
|
5556
6998
|
init_memory_vector();
|
|
5557
6999
|
var LEASE_KEY = "consolidate_lease";
|
|
@@ -5718,7 +7160,7 @@ ${r.detail ?? ""}`.trim());
|
|
|
5718
7160
|
}
|
|
5719
7161
|
clusters.push(cluster);
|
|
5720
7162
|
}
|
|
5721
|
-
const candidateDir =
|
|
7163
|
+
const candidateDir = join11(projectRoot, ".massu", "rule-candidates");
|
|
5722
7164
|
let promoted = 0;
|
|
5723
7165
|
for (const cluster of clusters) {
|
|
5724
7166
|
const occurrences = cluster.reduce((n, c) => n + (c.recurrence_count || 1), 0);
|
|
@@ -5730,15 +7172,15 @@ ${r.detail ?? ""}`.trim());
|
|
|
5730
7172
|
const promptText = `${representative.title}${representative.detail ? `
|
|
5731
7173
|
${representative.detail}` : ""}`;
|
|
5732
7174
|
const clusterKey = cluster.map((c) => c.title.toLowerCase().replace(/\s+/g, " ").trim()).sort().join("|");
|
|
5733
|
-
const promptHash =
|
|
5734
|
-
const candidatePath =
|
|
5735
|
-
if (
|
|
7175
|
+
const promptHash = createHash6("sha256").update(clusterKey).digest("hex").slice(0, 16);
|
|
7176
|
+
const candidatePath = join11(candidateDir, `${promptHash}.json`);
|
|
7177
|
+
if (existsSync16(candidatePath)) continue;
|
|
5736
7178
|
if (dryRun) {
|
|
5737
7179
|
promoted++;
|
|
5738
7180
|
continue;
|
|
5739
7181
|
}
|
|
5740
|
-
|
|
5741
|
-
|
|
7182
|
+
mkdirSync14(candidateDir, { recursive: true });
|
|
7183
|
+
writeFileSync10(
|
|
5742
7184
|
candidatePath,
|
|
5743
7185
|
JSON.stringify(
|
|
5744
7186
|
{
|
|
@@ -5803,7 +7245,7 @@ function stageReweight(db, cfg, now) {
|
|
|
5803
7245
|
MAX_ROWS_PER_STAGE
|
|
5804
7246
|
);
|
|
5805
7247
|
const bump = db.prepare(`UPDATE observations SET importance = importance + 1 WHERE id = ?`);
|
|
5806
|
-
const
|
|
7248
|
+
const drop2 = db.prepare(`UPDATE observations SET importance = importance - 1 WHERE id = ?`);
|
|
5807
7249
|
const mark = db.prepare(
|
|
5808
7250
|
`INSERT INTO memory_usage (source, record_id, hit_count, hits_windowed, last_reweight_epoch)
|
|
5809
7251
|
VALUES ('observation', ?, 0, 0, ?)
|
|
@@ -5816,7 +7258,7 @@ function stageReweight(db, cfg, now) {
|
|
|
5816
7258
|
changed++;
|
|
5817
7259
|
}
|
|
5818
7260
|
for (const r of demote) {
|
|
5819
|
-
|
|
7261
|
+
drop2.run(r.id);
|
|
5820
7262
|
mark.run(r.id, now);
|
|
5821
7263
|
changed++;
|
|
5822
7264
|
}
|
|
@@ -5975,6 +7417,12 @@ async function main() {
|
|
|
5975
7417
|
await pullTeamPromotions(db);
|
|
5976
7418
|
} catch (_pullErr) {
|
|
5977
7419
|
}
|
|
7420
|
+
try {
|
|
7421
|
+
const shareTransport = new LocalFsTransport();
|
|
7422
|
+
await exportSharedMemories(db, shareTransport);
|
|
7423
|
+
await importSharedMemories(db, shareTransport);
|
|
7424
|
+
} catch (_shareErr) {
|
|
7425
|
+
}
|
|
5978
7426
|
} catch (syncErr) {
|
|
5979
7427
|
const msg = syncErr instanceof Error ? syncErr.message : String(syncErr);
|
|
5980
7428
|
if (openLease) nackLearning(db, openLease, msg);
|
|
@@ -6237,14 +7685,14 @@ function extractFilesFromToolCall(toolName, input) {
|
|
|
6237
7685
|
return [];
|
|
6238
7686
|
}
|
|
6239
7687
|
function readStdin() {
|
|
6240
|
-
return new Promise((
|
|
7688
|
+
return new Promise((resolve8) => {
|
|
6241
7689
|
let data = "";
|
|
6242
7690
|
process.stdin.setEncoding("utf-8");
|
|
6243
7691
|
process.stdin.on("data", (chunk) => {
|
|
6244
7692
|
data += chunk;
|
|
6245
7693
|
});
|
|
6246
|
-
process.stdin.on("end", () =>
|
|
6247
|
-
setTimeout(() =>
|
|
7694
|
+
process.stdin.on("end", () => resolve8(data));
|
|
7695
|
+
setTimeout(() => resolve8(data), 5e3);
|
|
6248
7696
|
});
|
|
6249
7697
|
}
|
|
6250
7698
|
main();
|