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