@massu/core 1.16.3 → 2.1.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 +135 -0
- package/dist/cli.js +2453 -1187
- package/dist/hooks/auto-learning-pipeline.js +265 -48
- package/dist/hooks/classify-failure.js +264 -47
- package/dist/hooks/cost-tracker.js +261 -44
- package/dist/hooks/fix-detector.js +270 -53
- package/dist/hooks/incident-pipeline.js +259 -42
- package/dist/hooks/intent-suggester.js +261 -44
- package/dist/hooks/memory-recall.js +401 -50
- package/dist/hooks/post-edit-context.js +263 -46
- package/dist/hooks/post-tool-use.js +287 -170
- package/dist/hooks/pre-compact.js +261 -44
- package/dist/hooks/pre-delete-check.js +265 -48
- package/dist/hooks/pre-tool-use-gate.js +265 -48
- package/dist/hooks/quality-event.js +261 -44
- package/dist/hooks/rule-enforcement-pipeline.js +260 -43
- package/dist/hooks/security-gate.js +261 -44
- package/dist/hooks/session-end.js +1442 -339
- package/dist/hooks/session-start.js +618 -274
- package/dist/hooks/user-prompt.js +287 -70
- package/package.json +2 -2
|
@@ -243,6 +243,32 @@ var init_config_memory_schema = __esm({
|
|
|
243
243
|
// turn of EVERY session, so an unbounded index is a permanent context tax — and the
|
|
244
244
|
// per-session cap bounds only the RATE, never the total.
|
|
245
245
|
indexMaxLines: z.number().int().min(1).default(50)
|
|
246
|
+
}).default({}),
|
|
247
|
+
// --- Cross-repo memory surfacing (plan-living-memory-slice-5-cross-repo-surfacing) ---
|
|
248
|
+
// A decision made in one of your repos can surface in another — opt-in per
|
|
249
|
+
// decision AND opt-in per repo, signed, verified, and materialized ONLY on
|
|
250
|
+
// explicit human acceptance. Local transport is FREE and zero-network.
|
|
251
|
+
//
|
|
252
|
+
// ⛔ BOTH switches default OFF, and OFF means NOTHING EXISTS — no registry, no
|
|
253
|
+
// keys, no inbox, no behavioural difference. Two INDEPENDENT opt-ins:
|
|
254
|
+
// • enabled — may this repo EXPORT its shareable decisions? (opt-in #1)
|
|
255
|
+
// • subscribe — which repo LABELS may this repo IMPORT from? (opt-in #2)
|
|
256
|
+
// `subscribe: []` means import NOTHING. There is deliberately NO `subscribe: all`
|
|
257
|
+
// — a repo you never named is a repo Massu never reads from.
|
|
258
|
+
share: z.object({
|
|
259
|
+
enabled: z.boolean().default(false),
|
|
260
|
+
subscribe: z.array(z.string()).default([]),
|
|
261
|
+
// C-04 — recall surfacing of cross-repo memories. `enabled` defaults true but is
|
|
262
|
+
// CONDITIONAL on `subscribe` being non-empty (empty by default), so the effective
|
|
263
|
+
// default is DORMANT: with `subscribe: []` the recall hook output is byte-identical
|
|
264
|
+
// to today's. `maxCrossRepoItems` caps how many accepted cross-repo items may appear
|
|
265
|
+
// per recall block (default 1); `minScore` is an OPTIONAL strictly-higher floor for
|
|
266
|
+
// cross-repo items (they are, by construction, less relevant than local ones).
|
|
267
|
+
recall: z.object({
|
|
268
|
+
enabled: z.boolean().default(true),
|
|
269
|
+
maxCrossRepoItems: z.number().int().min(0).default(1),
|
|
270
|
+
minScore: z.number().optional()
|
|
271
|
+
}).default({})
|
|
246
272
|
}).default({})
|
|
247
273
|
}).optional();
|
|
248
274
|
}
|
|
@@ -419,6 +445,7 @@ function getResolvedPaths() {
|
|
|
419
445
|
sessionStatePath: resolve2(root, config.conventions?.sessionStatePath ?? `${claudeDirName}/session-state/CURRENT.md`),
|
|
420
446
|
sessionArchivePath: resolve2(root, config.conventions?.sessionArchivePath ?? `${claudeDirName}/session-state/archive`),
|
|
421
447
|
mcpJsonPath: resolve2(root, ".mcp.json"),
|
|
448
|
+
settingsPath: resolve2(root, claudeDirName, "settings.json"),
|
|
422
449
|
settingsLocalPath: resolve2(root, claudeDirName, "settings.local.json")
|
|
423
450
|
};
|
|
424
451
|
}
|
|
@@ -496,7 +523,6 @@ var init_config = __esm({
|
|
|
496
523
|
custom_patterns: z2.array(CustomPatternSchema).default([])
|
|
497
524
|
}).optional(),
|
|
498
525
|
adr: z2.object({
|
|
499
|
-
detection_phrases: z2.array(z2.string()).default(["chose", "decided", "switching to", "moving from", "going with"]),
|
|
500
526
|
template: z2.string().default("default"),
|
|
501
527
|
storage: z2.string().default("database"),
|
|
502
528
|
output_dir: z2.string().default("docs/adr")
|
|
@@ -1159,6 +1185,136 @@ var init_sqlite_loader = __esm({
|
|
|
1159
1185
|
}
|
|
1160
1186
|
});
|
|
1161
1187
|
|
|
1188
|
+
// src/db-driver.ts
|
|
1189
|
+
import { createRequire as createRequire2 } from "module";
|
|
1190
|
+
import { closeSync, existsSync as existsSync4, mkdtempSync, openSync, readFileSync as readFileSync4, rmSync as rmSync3, fsyncSync } from "fs";
|
|
1191
|
+
import { tmpdir } from "os";
|
|
1192
|
+
import { join as join2 } from "path";
|
|
1193
|
+
function resolveDbEngine() {
|
|
1194
|
+
return process.env[DB_ENGINE_ENV] === "better-sqlite3" ? "better-sqlite3" : DEFAULT_DB_ENGINE;
|
|
1195
|
+
}
|
|
1196
|
+
function nodeSqliteCtor() {
|
|
1197
|
+
if (!_nodeCtor) _nodeCtor = req2("node:sqlite").DatabaseSync;
|
|
1198
|
+
return _nodeCtor;
|
|
1199
|
+
}
|
|
1200
|
+
function plainRow(row) {
|
|
1201
|
+
if (row == null || typeof row !== "object") return row;
|
|
1202
|
+
return { ...row };
|
|
1203
|
+
}
|
|
1204
|
+
function openNodeSqlite(dbPath, opts) {
|
|
1205
|
+
const Ctor = nodeSqliteCtor();
|
|
1206
|
+
const raw = new Ctor(dbPath, {
|
|
1207
|
+
open: true,
|
|
1208
|
+
readOnly: !!opts.readonly,
|
|
1209
|
+
// FAITHFULNESS: better-sqlite3's ACTUAL default is foreign_keys=ON (empirically
|
|
1210
|
+
// verified — SQLite's own default is OFF, but bs3 enables it). massu was written +
|
|
1211
|
+
// tested against that default, so the adapter must match it, or a store that relies
|
|
1212
|
+
// on FK-ON-by-default (without an explicit pragma) would silently lose enforcement.
|
|
1213
|
+
// Explicit `foreign_keys=OFF/ON` pragmas (e.g. memory-db bulk ops) still override.
|
|
1214
|
+
enableForeignKeyConstraints: true,
|
|
1215
|
+
allowExtension: false
|
|
1216
|
+
});
|
|
1217
|
+
let savepointSeq = 0;
|
|
1218
|
+
const wrapStmt = (sql) => {
|
|
1219
|
+
const st = raw.prepare(sql);
|
|
1220
|
+
return {
|
|
1221
|
+
run: (...p) => {
|
|
1222
|
+
const r = st.run(...p);
|
|
1223
|
+
return {
|
|
1224
|
+
changes: typeof r.changes === "bigint" ? Number(r.changes) : r.changes,
|
|
1225
|
+
lastInsertRowid: r.lastInsertRowid
|
|
1226
|
+
};
|
|
1227
|
+
},
|
|
1228
|
+
get: (...p) => plainRow(st.get(...p)),
|
|
1229
|
+
all: (...p) => st.all(...p).map(plainRow)
|
|
1230
|
+
};
|
|
1231
|
+
};
|
|
1232
|
+
const pragma = (source, options) => {
|
|
1233
|
+
const s = String(source).trim();
|
|
1234
|
+
if (s.includes("=")) {
|
|
1235
|
+
raw.exec(`PRAGMA ${s}`);
|
|
1236
|
+
return void 0;
|
|
1237
|
+
}
|
|
1238
|
+
const rows = raw.prepare(`PRAGMA ${s}`).all().map(plainRow);
|
|
1239
|
+
if (options?.simple) {
|
|
1240
|
+
const first = rows[0];
|
|
1241
|
+
return first ? Object.values(first)[0] : void 0;
|
|
1242
|
+
}
|
|
1243
|
+
return rows;
|
|
1244
|
+
};
|
|
1245
|
+
const transaction = (fn) => {
|
|
1246
|
+
const run = (...args) => {
|
|
1247
|
+
const nested = raw.isTransaction;
|
|
1248
|
+
const name = `msp_${savepointSeq++}`;
|
|
1249
|
+
if (nested) raw.exec(`SAVEPOINT ${name}`);
|
|
1250
|
+
else raw.exec("BEGIN");
|
|
1251
|
+
try {
|
|
1252
|
+
const out = fn(...args);
|
|
1253
|
+
if (nested) raw.exec(`RELEASE ${name}`);
|
|
1254
|
+
else raw.exec("COMMIT");
|
|
1255
|
+
return out;
|
|
1256
|
+
} catch (e) {
|
|
1257
|
+
if (nested) {
|
|
1258
|
+
raw.exec(`ROLLBACK TO ${name}`);
|
|
1259
|
+
raw.exec(`RELEASE ${name}`);
|
|
1260
|
+
} else {
|
|
1261
|
+
raw.exec("ROLLBACK");
|
|
1262
|
+
}
|
|
1263
|
+
throw e;
|
|
1264
|
+
}
|
|
1265
|
+
};
|
|
1266
|
+
return run;
|
|
1267
|
+
};
|
|
1268
|
+
const serialize = () => {
|
|
1269
|
+
if (raw.isTransaction) {
|
|
1270
|
+
throw new Error(
|
|
1271
|
+
"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."
|
|
1272
|
+
);
|
|
1273
|
+
}
|
|
1274
|
+
const dir = mkdtempSync(join2(tmpdir(), "massu-serialize-"));
|
|
1275
|
+
const out = join2(dir, "snapshot.db");
|
|
1276
|
+
try {
|
|
1277
|
+
raw.exec(`VACUUM INTO '${out.replace(/'/g, "''")}'`);
|
|
1278
|
+
const fd = openSync(out, "r");
|
|
1279
|
+
try {
|
|
1280
|
+
fsyncSync(fd);
|
|
1281
|
+
} finally {
|
|
1282
|
+
closeSync(fd);
|
|
1283
|
+
}
|
|
1284
|
+
return readFileSync4(out);
|
|
1285
|
+
} finally {
|
|
1286
|
+
rmSync3(dir, { recursive: true, force: true });
|
|
1287
|
+
}
|
|
1288
|
+
};
|
|
1289
|
+
const handle = {
|
|
1290
|
+
prepare: (sql) => wrapStmt(sql),
|
|
1291
|
+
exec: (sql) => raw.exec(sql),
|
|
1292
|
+
pragma,
|
|
1293
|
+
transaction,
|
|
1294
|
+
serialize,
|
|
1295
|
+
close: () => raw.close()
|
|
1296
|
+
};
|
|
1297
|
+
return handle;
|
|
1298
|
+
}
|
|
1299
|
+
function openBetterSqlite3(dbPath, opts) {
|
|
1300
|
+
return openDatabase(dbPath, opts);
|
|
1301
|
+
}
|
|
1302
|
+
function openDatabase2(dbPath, opts = {}) {
|
|
1303
|
+
return resolveDbEngine() === "better-sqlite3" ? openBetterSqlite3(dbPath, opts) : openNodeSqlite(dbPath, opts);
|
|
1304
|
+
}
|
|
1305
|
+
var req2, DEFAULT_DB_ENGINE, DB_ENGINE_ENV, _nodeCtor;
|
|
1306
|
+
var init_db_driver = __esm({
|
|
1307
|
+
"src/db-driver.ts"() {
|
|
1308
|
+
"use strict";
|
|
1309
|
+
init_sqlite_loader();
|
|
1310
|
+
init_sqlite_loader();
|
|
1311
|
+
req2 = createRequire2(import.meta.url);
|
|
1312
|
+
DEFAULT_DB_ENGINE = "node-sqlite";
|
|
1313
|
+
DB_ENGINE_ENV = "MASSU_DB_ENGINE";
|
|
1314
|
+
_nodeCtor = null;
|
|
1315
|
+
}
|
|
1316
|
+
});
|
|
1317
|
+
|
|
1162
1318
|
// src/memory-vector.ts
|
|
1163
1319
|
function float32ToBlob(vec) {
|
|
1164
1320
|
return Buffer.from(new Uint8Array(vec.buffer, vec.byteOffset, vec.byteLength));
|
|
@@ -1179,26 +1335,26 @@ var init_memory_vector = __esm({
|
|
|
1179
1335
|
});
|
|
1180
1336
|
|
|
1181
1337
|
// src/db-backup.ts
|
|
1182
|
-
import { existsSync as
|
|
1183
|
-
import { resolve as resolve3, join as
|
|
1338
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync4, readdirSync, statSync, unlinkSync, copyFileSync, rmSync as rmSync4 } from "fs";
|
|
1339
|
+
import { resolve as resolve3, join as join3, basename, dirname as dirname4 } from "path";
|
|
1184
1340
|
import { homedir as homedir3 } from "os";
|
|
1185
1341
|
function dbBackupsRoot(home = homedir3()) {
|
|
1186
1342
|
return resolve3(home, ".massu", "db-backups");
|
|
1187
1343
|
}
|
|
1188
1344
|
function projectBackupDir(projectRoot, home = homedir3()) {
|
|
1189
1345
|
const slug = basename(projectRoot).replace(/[^A-Za-z0-9._-]/g, "_") || "project";
|
|
1190
|
-
return
|
|
1346
|
+
return join3(dbBackupsRoot(home), slug);
|
|
1191
1347
|
}
|
|
1192
1348
|
function backupStamp(nowMs) {
|
|
1193
1349
|
return new Date(nowMs).toISOString().replace(/[:.]/g, "-");
|
|
1194
1350
|
}
|
|
1195
1351
|
function listDbBackups(projectRoot, home = homedir3()) {
|
|
1196
1352
|
const dir = projectBackupDir(projectRoot, home);
|
|
1197
|
-
if (!
|
|
1353
|
+
if (!existsSync5(dir)) return [];
|
|
1198
1354
|
const out = [];
|
|
1199
1355
|
for (const f of readdirSync(dir)) {
|
|
1200
1356
|
if (!f.endsWith(".db")) continue;
|
|
1201
|
-
const p =
|
|
1357
|
+
const p = join3(dir, f);
|
|
1202
1358
|
const st = statSync(p);
|
|
1203
1359
|
const db = f.replace(/-\d{4}-\d{2}-\d{2}T.*$/, "");
|
|
1204
1360
|
out.push({ db, path: p, bytes: st.size, mtimeMs: st.mtimeMs });
|
|
@@ -1211,13 +1367,13 @@ function hasFreshDbBackup(projectRoot, dbPath, nowMs = Date.now(), home = homedi
|
|
|
1211
1367
|
if (backups.length === 0) return false;
|
|
1212
1368
|
const newest = backups[0];
|
|
1213
1369
|
if (nowMs - newest.mtimeMs > FRESH_WINDOW_MS) return false;
|
|
1214
|
-
if (
|
|
1370
|
+
if (existsSync5(dbPath) && statSync(dbPath).mtimeMs > newest.mtimeMs) return false;
|
|
1215
1371
|
return true;
|
|
1216
1372
|
}
|
|
1217
1373
|
function integrityOk(dbPath) {
|
|
1218
1374
|
let db = null;
|
|
1219
1375
|
try {
|
|
1220
|
-
db =
|
|
1376
|
+
db = openDatabase2(dbPath, { readonly: true });
|
|
1221
1377
|
const rows = db.pragma("integrity_check");
|
|
1222
1378
|
return rows.length === 1 && rows[0].integrity_check === "ok";
|
|
1223
1379
|
} catch {
|
|
@@ -1230,16 +1386,16 @@ function integrityOk(dbPath) {
|
|
|
1230
1386
|
}
|
|
1231
1387
|
}
|
|
1232
1388
|
function backupDb(projectRoot, dbPath, nowMs = Date.now(), home = homedir3()) {
|
|
1233
|
-
if (!
|
|
1389
|
+
if (!existsSync5(dbPath)) {
|
|
1234
1390
|
throw new DbBackupError(`cannot back up a database that does not exist: ${dbPath}`);
|
|
1235
1391
|
}
|
|
1236
1392
|
const dir = projectBackupDir(projectRoot, home);
|
|
1237
1393
|
mkdirSync4(dir, { recursive: true });
|
|
1238
1394
|
const name = basename(dbPath, ".db");
|
|
1239
|
-
const dest =
|
|
1395
|
+
const dest = join3(dir, `${name}-${backupStamp(nowMs)}.db`);
|
|
1240
1396
|
let src = null;
|
|
1241
1397
|
try {
|
|
1242
|
-
src =
|
|
1398
|
+
src = openDatabase2(dbPath, { readonly: true });
|
|
1243
1399
|
src.exec(`VACUUM INTO '${dest.replace(/'/g, "''")}'`);
|
|
1244
1400
|
} catch (err) {
|
|
1245
1401
|
throw new DbBackupError(
|
|
@@ -1278,7 +1434,7 @@ function pruneDbBackups(projectRoot, dbName, keep = DEFAULT_RETENTION, home = ho
|
|
|
1278
1434
|
}
|
|
1279
1435
|
function backupBeforeSchemaChange(projectRoot, dbPath, onError, nowMs = Date.now(), home = homedir3()) {
|
|
1280
1436
|
try {
|
|
1281
|
-
if (!
|
|
1437
|
+
if (!existsSync5(dbPath)) return null;
|
|
1282
1438
|
if (hasFreshDbBackup(projectRoot, dbPath, nowMs, home)) return null;
|
|
1283
1439
|
return backupDb(projectRoot, dbPath, nowMs, home);
|
|
1284
1440
|
} catch (err) {
|
|
@@ -1290,7 +1446,7 @@ var DEFAULT_RETENTION, FRESH_WINDOW_MS, DbBackupError;
|
|
|
1290
1446
|
var init_db_backup = __esm({
|
|
1291
1447
|
"src/db-backup.ts"() {
|
|
1292
1448
|
"use strict";
|
|
1293
|
-
|
|
1449
|
+
init_db_driver();
|
|
1294
1450
|
DEFAULT_RETENTION = 5;
|
|
1295
1451
|
FRESH_WINDOW_MS = 24 * 60 * 60 * 1e3;
|
|
1296
1452
|
DbBackupError = class extends Error {
|
|
@@ -1403,9 +1559,9 @@ var init_rule_candidate_store = __esm({
|
|
|
1403
1559
|
});
|
|
1404
1560
|
|
|
1405
1561
|
// src/memory-embedder-tokenizer.ts
|
|
1406
|
-
import { readFileSync as
|
|
1562
|
+
import { readFileSync as readFileSync5 } from "fs";
|
|
1407
1563
|
function loadVocab(vocabPath) {
|
|
1408
|
-
const lines =
|
|
1564
|
+
const lines = readFileSync5(vocabPath, "utf-8").split("\n");
|
|
1409
1565
|
const vocab = /* @__PURE__ */ new Map();
|
|
1410
1566
|
for (let i = 0; i < lines.length; i++) {
|
|
1411
1567
|
const tok = lines[i].replace(/\r$/, "");
|
|
@@ -1525,9 +1681,9 @@ var init_memory_embedder_tokenizer = __esm({
|
|
|
1525
1681
|
|
|
1526
1682
|
// src/memory-embedder.ts
|
|
1527
1683
|
import { fileURLToPath } from "url";
|
|
1528
|
-
import { dirname as dirname5, join as
|
|
1529
|
-
import { existsSync as
|
|
1530
|
-
import { createRequire as
|
|
1684
|
+
import { dirname as dirname5, join as join4 } from "path";
|
|
1685
|
+
import { existsSync as existsSync6 } from "fs";
|
|
1686
|
+
import { createRequire as createRequire3 } from "module";
|
|
1531
1687
|
function getActiveEmbedModel() {
|
|
1532
1688
|
return _activeModel;
|
|
1533
1689
|
}
|
|
@@ -1551,7 +1707,7 @@ function resolveModelDir() {
|
|
|
1551
1707
|
let dir = dirname5(fileURLToPath(import.meta.url));
|
|
1552
1708
|
let root = null;
|
|
1553
1709
|
for (let i = 0; i < 8; i++) {
|
|
1554
|
-
if (
|
|
1710
|
+
if (existsSync6(join4(dir, "package.json"))) {
|
|
1555
1711
|
root = dir;
|
|
1556
1712
|
break;
|
|
1557
1713
|
}
|
|
@@ -1561,8 +1717,8 @@ function resolveModelDir() {
|
|
|
1561
1717
|
}
|
|
1562
1718
|
if (!root) return null;
|
|
1563
1719
|
for (const rel of ["dist/embedder", "assets/embedder"]) {
|
|
1564
|
-
const candidate =
|
|
1565
|
-
if (
|
|
1720
|
+
const candidate = join4(root, rel);
|
|
1721
|
+
if (existsSync6(join4(candidate, "model_quantized.onnx"))) return candidate;
|
|
1566
1722
|
}
|
|
1567
1723
|
return null;
|
|
1568
1724
|
} catch {
|
|
@@ -1571,8 +1727,8 @@ function resolveModelDir() {
|
|
|
1571
1727
|
}
|
|
1572
1728
|
function resolveWasmDir() {
|
|
1573
1729
|
try {
|
|
1574
|
-
const
|
|
1575
|
-
const main2 =
|
|
1730
|
+
const req3 = createRequire3(import.meta.url);
|
|
1731
|
+
const main2 = req3.resolve(ORT_PKG);
|
|
1576
1732
|
return dirname5(main2);
|
|
1577
1733
|
} catch {
|
|
1578
1734
|
return null;
|
|
@@ -1593,8 +1749,8 @@ async function getSession() {
|
|
|
1593
1749
|
ort.env.wasm.proxy = false;
|
|
1594
1750
|
ort.env.wasm.wasmPaths = wasmDir.endsWith("/") ? wasmDir : wasmDir + "/";
|
|
1595
1751
|
_ortTensor = ort.Tensor;
|
|
1596
|
-
const modelPath =
|
|
1597
|
-
const vocabPath =
|
|
1752
|
+
const modelPath = join4(modelDir, "model_quantized.onnx");
|
|
1753
|
+
const vocabPath = join4(modelDir, "vocab.txt");
|
|
1598
1754
|
_vocab = loadVocab(vocabPath);
|
|
1599
1755
|
const session = await ort.InferenceSession.create(modelPath, {
|
|
1600
1756
|
executionProviders: ["wasm"],
|
|
@@ -1915,6 +2071,7 @@ __export(memory_db_exports, {
|
|
|
1915
2071
|
migrateAuditLogCheckExtension: () => migrateAuditLogCheckExtension,
|
|
1916
2072
|
migrateMemoryFilesFor4B: () => migrateMemoryFilesFor4B,
|
|
1917
2073
|
migrateObservationEmbeddingChunks: () => migrateObservationEmbeddingChunks,
|
|
2074
|
+
migrateSharedMemoryFor5B: () => migrateSharedMemoryFor5B,
|
|
1918
2075
|
pruneOldConversationTurns: () => pruneOldConversationTurns,
|
|
1919
2076
|
pruneOldObservations: () => pruneOldObservations,
|
|
1920
2077
|
pruneToolCostEvents: () => pruneToolCostEvents,
|
|
@@ -1932,7 +2089,7 @@ __export(memory_db_exports, {
|
|
|
1932
2089
|
usageWarmupElapsed: () => usageWarmupElapsed
|
|
1933
2090
|
});
|
|
1934
2091
|
import { dirname as dirname6, basename as basename2 } from "path";
|
|
1935
|
-
import { existsSync as
|
|
2092
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync5 } from "fs";
|
|
1936
2093
|
function sanitizeFts5Query(raw) {
|
|
1937
2094
|
const trimmed = raw.trim();
|
|
1938
2095
|
if (!trimmed) return '""';
|
|
@@ -1949,11 +2106,11 @@ function sanitizeFts5QueryOr(raw) {
|
|
|
1949
2106
|
function getMemoryDb() {
|
|
1950
2107
|
const dbPath = getResolvedPaths().memoryDbPath;
|
|
1951
2108
|
const dir = dirname6(dbPath);
|
|
1952
|
-
if (!
|
|
2109
|
+
if (!existsSync7(dir)) {
|
|
1953
2110
|
mkdirSync5(dir, { recursive: true });
|
|
1954
2111
|
}
|
|
1955
|
-
const preExisting =
|
|
1956
|
-
const db =
|
|
2112
|
+
const preExisting = existsSync7(dbPath);
|
|
2113
|
+
const db = openDatabase2(dbPath);
|
|
1957
2114
|
db.pragma("journal_mode = WAL");
|
|
1958
2115
|
db.pragma("foreign_keys = ON");
|
|
1959
2116
|
const onDisk = db.pragma("user_version", { simple: true });
|
|
@@ -2029,6 +2186,39 @@ function migrateMemoryFilesFor4B(db) {
|
|
|
2029
2186
|
db.exec(`ALTER TABLE ${add.table} ADD COLUMN ${add.name} ${add.decl}`);
|
|
2030
2187
|
}
|
|
2031
2188
|
}
|
|
2189
|
+
function migrateSharedMemoryFor5B(db) {
|
|
2190
|
+
const cols = db.prepare(`PRAGMA table_info(observations)`).all();
|
|
2191
|
+
if (cols.length > 0 && !cols.some((c) => c.name === "shareable")) {
|
|
2192
|
+
db.exec(`ALTER TABLE observations ADD COLUMN shareable INTEGER NOT NULL DEFAULT 0`);
|
|
2193
|
+
}
|
|
2194
|
+
db.exec(`
|
|
2195
|
+
CREATE TABLE IF NOT EXISTS shared_memory_pending (
|
|
2196
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
2197
|
+
record_hash TEXT NOT NULL UNIQUE, -- identity + idempotency key (hex sha256)
|
|
2198
|
+
origin_repo_id TEXT NOT NULL, -- the signed origin repo_id (v4 UUID)
|
|
2199
|
+
origin_repo_label TEXT NOT NULL, -- re-slugged on read; NEVER trusted from the wire
|
|
2200
|
+
envelope_raw TEXT NOT NULL, -- the VERBATIM signed bytes; B-05 re-verifies these
|
|
2201
|
+
record_json TEXT NOT NULL, -- the single record's canonical JSON
|
|
2202
|
+
received_at_epoch INTEGER NOT NULL, -- epoch SECONDS (Slice-2 convention)
|
|
2203
|
+
accepted_at_epoch INTEGER, -- set on accept (B-05); NULL while pending
|
|
2204
|
+
refused_at_epoch INTEGER, -- set on refuse (B-06)
|
|
2205
|
+
expired_at_epoch INTEGER -- revocation \u21D2 EXPIRE, never DELETE (B-07 / S-3)
|
|
2206
|
+
);
|
|
2207
|
+
CREATE INDEX IF NOT EXISTS idx_smp_origin ON shared_memory_pending(origin_repo_id);
|
|
2208
|
+
CREATE INDEX IF NOT EXISTS idx_smp_received ON shared_memory_pending(received_at_epoch DESC);
|
|
2209
|
+
`);
|
|
2210
|
+
db.exec(`
|
|
2211
|
+
CREATE TABLE IF NOT EXISTS shared_memory_outbound (
|
|
2212
|
+
record_hash TEXT PRIMARY KEY, -- the exported record's canonical hash
|
|
2213
|
+
observation_id INTEGER NOT NULL, -- the source observations.id (for B-07 revocation)
|
|
2214
|
+
origin_repo_id TEXT NOT NULL, -- this repo's own repo_id at export time
|
|
2215
|
+
seq INTEGER NOT NULL, -- the envelope seq this record first went out in
|
|
2216
|
+
exported_at_epoch INTEGER NOT NULL, -- epoch SECONDS
|
|
2217
|
+
revoked_at_epoch INTEGER -- set when the source row is superseded/expired (B-07)
|
|
2218
|
+
);
|
|
2219
|
+
CREATE INDEX IF NOT EXISTS idx_smo_obs ON shared_memory_outbound(observation_id);
|
|
2220
|
+
`);
|
|
2221
|
+
}
|
|
2032
2222
|
function migrateAuditLogCheckExtension(db) {
|
|
2033
2223
|
const row = db.prepare("SELECT sql FROM sqlite_master WHERE type='table' AND name='audit_log'").get();
|
|
2034
2224
|
if (!row) return;
|
|
@@ -2051,7 +2241,16 @@ function migrateAuditLogCheckExtension(db) {
|
|
|
2051
2241
|
"memory_file_adopted_human",
|
|
2052
2242
|
"memory_file_rendered",
|
|
2053
2243
|
"memory_file_render_refused",
|
|
2054
|
-
"memory_file_tombstoned"
|
|
2244
|
+
"memory_file_tombstoned",
|
|
2245
|
+
// B-10 (Slice 5) — the seven cross-repo shared-memory events. An existing DB whose
|
|
2246
|
+
// CHECK lacks any of these triggers the table rebuild below.
|
|
2247
|
+
"shared_memory_exported",
|
|
2248
|
+
"shared_memory_export_refused",
|
|
2249
|
+
"shared_memory_imported",
|
|
2250
|
+
"shared_memory_dropped",
|
|
2251
|
+
"shared_memory_accepted",
|
|
2252
|
+
"shared_memory_refused",
|
|
2253
|
+
"shared_memory_revoked"
|
|
2055
2254
|
];
|
|
2056
2255
|
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);
|
|
2057
2256
|
if (checkClauseMatch) {
|
|
@@ -2070,7 +2269,12 @@ function migrateAuditLogCheckExtension(db) {
|
|
|
2070
2269
|
'code_change', 'rule_enforced', 'approval', 'review', 'commit', 'compaction',
|
|
2071
2270
|
'rule_candidate_emitted', 'rule_promoted', 'rule_dismissed',
|
|
2072
2271
|
'memory_file_ingested', 'memory_file_expired', 'memory_file_adopted_human',
|
|
2073
|
-
'memory_file_rendered', 'memory_file_render_refused', 'memory_file_tombstoned'
|
|
2272
|
+
'memory_file_rendered', 'memory_file_render_refused', 'memory_file_tombstoned',
|
|
2273
|
+
-- B-10 (Slice 5) \u2014 cross-repo shared-memory events (must mirror the CREATE +
|
|
2274
|
+
-- the expected[] list above, or the migration would loop rebuilding forever).
|
|
2275
|
+
'shared_memory_exported', 'shared_memory_export_refused', 'shared_memory_imported',
|
|
2276
|
+
'shared_memory_dropped', 'shared_memory_accepted', 'shared_memory_refused',
|
|
2277
|
+
'shared_memory_revoked'
|
|
2074
2278
|
)),
|
|
2075
2279
|
actor TEXT NOT NULL DEFAULT 'ai' CHECK(actor IN ('ai', 'human', 'hook', 'agent')),
|
|
2076
2280
|
model_id TEXT,
|
|
@@ -2147,6 +2351,12 @@ function initMemorySchema(db) {
|
|
|
2147
2351
|
-- credential, or takes a snapshot. A Slice-5 synced memory arrives here with
|
|
2148
2352
|
-- origin='team' and must never reach disk without CR-55's gates.
|
|
2149
2353
|
origin TEXT NOT NULL DEFAULT 'local',
|
|
2354
|
+
-- B-01 (Slice 5, cross-repo surfacing): the per-decision SHARE opt-in. Set to 1
|
|
2355
|
+
-- ONLY by an explicit human act (massu memory share <id>); NEVER by a heuristic,
|
|
2356
|
+
-- score, LLM, hook, or consolidation pass. Export (B-02) reads WHERE shareable=1
|
|
2357
|
+
-- AND origin='local'. Defaults 0 so every pre-existing and machine-written row is
|
|
2358
|
+
-- un-shareable until a human says otherwise (fail-closed).
|
|
2359
|
+
shareable INTEGER NOT NULL DEFAULT 0,
|
|
2150
2360
|
created_at TEXT NOT NULL,
|
|
2151
2361
|
created_at_epoch INTEGER NOT NULL,
|
|
2152
2362
|
FOREIGN KEY(session_id) REFERENCES sessions(session_id) ON DELETE CASCADE
|
|
@@ -2420,7 +2630,13 @@ function initMemorySchema(db) {
|
|
|
2420
2630
|
'code_change', 'rule_enforced', 'approval', 'review', 'commit', 'compaction',
|
|
2421
2631
|
'rule_candidate_emitted', 'rule_promoted', 'rule_dismissed',
|
|
2422
2632
|
'memory_file_ingested', 'memory_file_expired', 'memory_file_adopted_human',
|
|
2423
|
-
'memory_file_rendered', 'memory_file_render_refused', 'memory_file_tombstoned'
|
|
2633
|
+
'memory_file_rendered', 'memory_file_render_refused', 'memory_file_tombstoned',
|
|
2634
|
+
-- B-10 (Slice 5): cross-repo shared-memory observability. Without these seven,
|
|
2635
|
+
-- nobody could ever answer "what has crossed between my repos?" (and an ingest
|
|
2636
|
+
-- catch would swallow the CHECK violation, silently producing NOTHING).
|
|
2637
|
+
'shared_memory_exported', 'shared_memory_export_refused', 'shared_memory_imported',
|
|
2638
|
+
'shared_memory_dropped', 'shared_memory_accepted', 'shared_memory_refused',
|
|
2639
|
+
'shared_memory_revoked'
|
|
2424
2640
|
)),
|
|
2425
2641
|
actor TEXT NOT NULL DEFAULT 'ai' CHECK(actor IN ('ai', 'human', 'hook', 'agent')),
|
|
2426
2642
|
model_id TEXT,
|
|
@@ -2789,6 +3005,7 @@ function initMemorySchema(db) {
|
|
|
2789
3005
|
`);
|
|
2790
3006
|
migrateObservationEmbeddingChunks(db);
|
|
2791
3007
|
migrateMemoryFilesFor4B(db);
|
|
3008
|
+
migrateSharedMemoryFor5B(db);
|
|
2792
3009
|
ensureRuleCandidatesTable(db);
|
|
2793
3010
|
migrateRuleDelivery(db);
|
|
2794
3011
|
}
|
|
@@ -3659,7 +3876,7 @@ var MEMORY_SCHEMA_VERSION, TOOL_COST_EVENTS_RETENTION_DAYS, MAX_DRAFT_TEXT_LEN,
|
|
|
3659
3876
|
var init_memory_db = __esm({
|
|
3660
3877
|
"src/memory-db.ts"() {
|
|
3661
3878
|
"use strict";
|
|
3662
|
-
|
|
3879
|
+
init_db_driver();
|
|
3663
3880
|
init_config();
|
|
3664
3881
|
init_memory_vector();
|
|
3665
3882
|
init_db_backup();
|
|
@@ -3680,21 +3897,21 @@ var init_memory_db = __esm({
|
|
|
3680
3897
|
});
|
|
3681
3898
|
|
|
3682
3899
|
// src/hooks/lib/hook-failure-signal.ts
|
|
3683
|
-
import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync6, existsSync as
|
|
3684
|
-
import { join as
|
|
3900
|
+
import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync6, existsSync as existsSync8 } from "fs";
|
|
3901
|
+
import { join as join5, dirname as dirname7 } from "path";
|
|
3685
3902
|
function resolveFailureLogPath() {
|
|
3686
3903
|
const explicit = process.env.MASSU_HOOK_FAILURE_LOG;
|
|
3687
3904
|
if (explicit) return explicit;
|
|
3688
3905
|
let dir = process.cwd();
|
|
3689
3906
|
for (let i = 0; i < 12; i++) {
|
|
3690
|
-
if (
|
|
3691
|
-
return
|
|
3907
|
+
if (existsSync8(join5(dir, ".massu")) || existsSync8(join5(dir, "massu.config.yaml"))) {
|
|
3908
|
+
return join5(dir, ".massu", "hook-failures.jsonl");
|
|
3692
3909
|
}
|
|
3693
3910
|
const parent = dirname7(dir);
|
|
3694
3911
|
if (parent === dir) break;
|
|
3695
3912
|
dir = parent;
|
|
3696
3913
|
}
|
|
3697
|
-
return
|
|
3914
|
+
return join5(process.cwd(), ".massu", "hook-failures.jsonl");
|
|
3698
3915
|
}
|
|
3699
3916
|
function recordHookFailure(hook, error, context) {
|
|
3700
3917
|
const record = {
|
|
@@ -3753,9 +3970,9 @@ var init_hook_failure_signal = __esm({
|
|
|
3753
3970
|
// src/hooks/fix-detector.ts
|
|
3754
3971
|
init_config();
|
|
3755
3972
|
import { execFileSync } from "child_process";
|
|
3756
|
-
import { existsSync as
|
|
3757
|
-
import { tmpdir } from "os";
|
|
3758
|
-
import { join as
|
|
3973
|
+
import { existsSync as existsSync9, appendFileSync as appendFileSync3, mkdirSync as mkdirSync7, readFileSync as readFileSync6 } from "fs";
|
|
3974
|
+
import { tmpdir as tmpdir2 } from "os";
|
|
3975
|
+
import { join as join6 } from "path";
|
|
3759
3976
|
|
|
3760
3977
|
// src/hooks/lib/write-hook-message.ts
|
|
3761
3978
|
function writeHookMessage(message) {
|
|
@@ -3806,15 +4023,15 @@ var FIX_HEURISTICS = [
|
|
|
3806
4023
|
}
|
|
3807
4024
|
];
|
|
3808
4025
|
function getSessionFlagPath(sessionId) {
|
|
3809
|
-
const dir =
|
|
3810
|
-
if (!
|
|
4026
|
+
const dir = join6(tmpdir2(), "massu-auto-learning");
|
|
4027
|
+
if (!existsSync9(dir)) {
|
|
3811
4028
|
mkdirSync7(dir, { recursive: true });
|
|
3812
4029
|
}
|
|
3813
|
-
return
|
|
4030
|
+
return join6(dir, `fixes-${sessionId.slice(0, 12)}.jsonl`);
|
|
3814
4031
|
}
|
|
3815
4032
|
function _stateDir() {
|
|
3816
|
-
const dir =
|
|
3817
|
-
if (!
|
|
4033
|
+
const dir = join6(tmpdir2(), "massu-fix-detector-state");
|
|
4034
|
+
if (!existsSync9(dir)) {
|
|
3818
4035
|
mkdirSync7(dir, { recursive: true });
|
|
3819
4036
|
}
|
|
3820
4037
|
return dir;
|
|
@@ -3828,9 +4045,9 @@ function _cwdHash(cwd) {
|
|
|
3828
4045
|
}
|
|
3829
4046
|
function isGitWorkTreeFast(cwd) {
|
|
3830
4047
|
try {
|
|
3831
|
-
const cachePath =
|
|
3832
|
-
if (
|
|
3833
|
-
const cached = JSON.parse(
|
|
4048
|
+
const cachePath = join6(_stateDir(), `worktree-${_cwdHash(cwd)}.json`);
|
|
4049
|
+
if (existsSync9(cachePath)) {
|
|
4050
|
+
const cached = JSON.parse(readFileSync6(cachePath, "utf-8"));
|
|
3834
4051
|
const ageMs = Date.now() - cached.ts;
|
|
3835
4052
|
if (ageMs < 36e5) return cached.isWorkTree;
|
|
3836
4053
|
}
|
|
@@ -3853,11 +4070,11 @@ function isGitWorkTreeFast(cwd) {
|
|
|
3853
4070
|
}
|
|
3854
4071
|
}
|
|
3855
4072
|
function _disabledFlagPath(sessionId) {
|
|
3856
|
-
return
|
|
4073
|
+
return join6(_stateDir(), `disabled-${sessionId.slice(0, 12)}.flag`);
|
|
3857
4074
|
}
|
|
3858
4075
|
function isSessionAutoDisabled(sessionId) {
|
|
3859
4076
|
try {
|
|
3860
|
-
return
|
|
4077
|
+
return existsSync9(_disabledFlagPath(sessionId));
|
|
3861
4078
|
} catch {
|
|
3862
4079
|
return false;
|
|
3863
4080
|
}
|
|
@@ -3875,7 +4092,7 @@ async function main() {
|
|
|
3875
4092
|
const input = await readStdin();
|
|
3876
4093
|
const hookInput = JSON.parse(input);
|
|
3877
4094
|
const filePath = hookInput.tool_input?.file_path;
|
|
3878
|
-
if (!filePath || !
|
|
4095
|
+
if (!filePath || !existsSync9(filePath)) {
|
|
3879
4096
|
process.exit(0);
|
|
3880
4097
|
return;
|
|
3881
4098
|
}
|
|
@@ -3936,7 +4153,7 @@ async function main() {
|
|
|
3936
4153
|
};
|
|
3937
4154
|
const flagPath = getSessionFlagPath(hookInput.session_id);
|
|
3938
4155
|
appendFileSync3(flagPath, JSON.stringify(signal) + "\n");
|
|
3939
|
-
const lines =
|
|
4156
|
+
const lines = readFileSync6(flagPath, "utf-8").split("\n").filter(Boolean);
|
|
3940
4157
|
if (lines.length === 1) {
|
|
3941
4158
|
writeHookMessage(
|
|
3942
4159
|
`[Massu Auto-Learning] Bug fix detected in ${filePath} (signals: ${detected.join(", ")}). The auto-learning pipeline will prompt you at session end to create an incident report, derive a prevention rule, and add enforcement.`
|