@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
|
@@ -237,6 +237,32 @@ var init_config_memory_schema = __esm({
|
|
|
237
237
|
// turn of EVERY session, so an unbounded index is a permanent context tax — and the
|
|
238
238
|
// per-session cap bounds only the RATE, never the total.
|
|
239
239
|
indexMaxLines: z.number().int().min(1).default(50)
|
|
240
|
+
}).default({}),
|
|
241
|
+
// --- Cross-repo memory surfacing (plan-living-memory-slice-5-cross-repo-surfacing) ---
|
|
242
|
+
// A decision made in one of your repos can surface in another — opt-in per
|
|
243
|
+
// decision AND opt-in per repo, signed, verified, and materialized ONLY on
|
|
244
|
+
// explicit human acceptance. Local transport is FREE and zero-network.
|
|
245
|
+
//
|
|
246
|
+
// ⛔ BOTH switches default OFF, and OFF means NOTHING EXISTS — no registry, no
|
|
247
|
+
// keys, no inbox, no behavioural difference. Two INDEPENDENT opt-ins:
|
|
248
|
+
// • enabled — may this repo EXPORT its shareable decisions? (opt-in #1)
|
|
249
|
+
// • subscribe — which repo LABELS may this repo IMPORT from? (opt-in #2)
|
|
250
|
+
// `subscribe: []` means import NOTHING. There is deliberately NO `subscribe: all`
|
|
251
|
+
// — a repo you never named is a repo Massu never reads from.
|
|
252
|
+
share: z.object({
|
|
253
|
+
enabled: z.boolean().default(false),
|
|
254
|
+
subscribe: z.array(z.string()).default([]),
|
|
255
|
+
// C-04 — recall surfacing of cross-repo memories. `enabled` defaults true but is
|
|
256
|
+
// CONDITIONAL on `subscribe` being non-empty (empty by default), so the effective
|
|
257
|
+
// default is DORMANT: with `subscribe: []` the recall hook output is byte-identical
|
|
258
|
+
// to today's. `maxCrossRepoItems` caps how many accepted cross-repo items may appear
|
|
259
|
+
// per recall block (default 1); `minScore` is an OPTIONAL strictly-higher floor for
|
|
260
|
+
// cross-repo items (they are, by construction, less relevant than local ones).
|
|
261
|
+
recall: z.object({
|
|
262
|
+
enabled: z.boolean().default(true),
|
|
263
|
+
maxCrossRepoItems: z.number().int().min(0).default(1),
|
|
264
|
+
minScore: z.number().optional()
|
|
265
|
+
}).default({})
|
|
240
266
|
}).default({})
|
|
241
267
|
}).optional();
|
|
242
268
|
}
|
|
@@ -490,7 +516,6 @@ var init_config = __esm({
|
|
|
490
516
|
custom_patterns: z2.array(CustomPatternSchema).default([])
|
|
491
517
|
}).optional(),
|
|
492
518
|
adr: z2.object({
|
|
493
|
-
detection_phrases: z2.array(z2.string()).default(["chose", "decided", "switching to", "moving from", "going with"]),
|
|
494
519
|
template: z2.string().default("default"),
|
|
495
520
|
storage: z2.string().default("database"),
|
|
496
521
|
output_dir: z2.string().default("docs/adr")
|
|
@@ -1153,6 +1178,136 @@ var init_sqlite_loader = __esm({
|
|
|
1153
1178
|
}
|
|
1154
1179
|
});
|
|
1155
1180
|
|
|
1181
|
+
// src/db-driver.ts
|
|
1182
|
+
import { createRequire as createRequire2 } from "module";
|
|
1183
|
+
import { closeSync, existsSync as existsSync4, mkdtempSync, openSync, readFileSync as readFileSync4, rmSync as rmSync3, fsyncSync } from "fs";
|
|
1184
|
+
import { tmpdir } from "os";
|
|
1185
|
+
import { join as join2 } from "path";
|
|
1186
|
+
function resolveDbEngine() {
|
|
1187
|
+
return process.env[DB_ENGINE_ENV] === "better-sqlite3" ? "better-sqlite3" : DEFAULT_DB_ENGINE;
|
|
1188
|
+
}
|
|
1189
|
+
function nodeSqliteCtor() {
|
|
1190
|
+
if (!_nodeCtor) _nodeCtor = req2("node:sqlite").DatabaseSync;
|
|
1191
|
+
return _nodeCtor;
|
|
1192
|
+
}
|
|
1193
|
+
function plainRow(row) {
|
|
1194
|
+
if (row == null || typeof row !== "object") return row;
|
|
1195
|
+
return { ...row };
|
|
1196
|
+
}
|
|
1197
|
+
function openNodeSqlite(dbPath, opts) {
|
|
1198
|
+
const Ctor = nodeSqliteCtor();
|
|
1199
|
+
const raw = new Ctor(dbPath, {
|
|
1200
|
+
open: true,
|
|
1201
|
+
readOnly: !!opts.readonly,
|
|
1202
|
+
// FAITHFULNESS: better-sqlite3's ACTUAL default is foreign_keys=ON (empirically
|
|
1203
|
+
// verified — SQLite's own default is OFF, but bs3 enables it). massu was written +
|
|
1204
|
+
// tested against that default, so the adapter must match it, or a store that relies
|
|
1205
|
+
// on FK-ON-by-default (without an explicit pragma) would silently lose enforcement.
|
|
1206
|
+
// Explicit `foreign_keys=OFF/ON` pragmas (e.g. memory-db bulk ops) still override.
|
|
1207
|
+
enableForeignKeyConstraints: true,
|
|
1208
|
+
allowExtension: false
|
|
1209
|
+
});
|
|
1210
|
+
let savepointSeq = 0;
|
|
1211
|
+
const wrapStmt = (sql) => {
|
|
1212
|
+
const st = raw.prepare(sql);
|
|
1213
|
+
return {
|
|
1214
|
+
run: (...p) => {
|
|
1215
|
+
const r = st.run(...p);
|
|
1216
|
+
return {
|
|
1217
|
+
changes: typeof r.changes === "bigint" ? Number(r.changes) : r.changes,
|
|
1218
|
+
lastInsertRowid: r.lastInsertRowid
|
|
1219
|
+
};
|
|
1220
|
+
},
|
|
1221
|
+
get: (...p) => plainRow(st.get(...p)),
|
|
1222
|
+
all: (...p) => st.all(...p).map(plainRow)
|
|
1223
|
+
};
|
|
1224
|
+
};
|
|
1225
|
+
const pragma = (source, options) => {
|
|
1226
|
+
const s = String(source).trim();
|
|
1227
|
+
if (s.includes("=")) {
|
|
1228
|
+
raw.exec(`PRAGMA ${s}`);
|
|
1229
|
+
return void 0;
|
|
1230
|
+
}
|
|
1231
|
+
const rows = raw.prepare(`PRAGMA ${s}`).all().map(plainRow);
|
|
1232
|
+
if (options?.simple) {
|
|
1233
|
+
const first = rows[0];
|
|
1234
|
+
return first ? Object.values(first)[0] : void 0;
|
|
1235
|
+
}
|
|
1236
|
+
return rows;
|
|
1237
|
+
};
|
|
1238
|
+
const transaction = (fn) => {
|
|
1239
|
+
const run = (...args) => {
|
|
1240
|
+
const nested = raw.isTransaction;
|
|
1241
|
+
const name = `msp_${savepointSeq++}`;
|
|
1242
|
+
if (nested) raw.exec(`SAVEPOINT ${name}`);
|
|
1243
|
+
else raw.exec("BEGIN");
|
|
1244
|
+
try {
|
|
1245
|
+
const out = fn(...args);
|
|
1246
|
+
if (nested) raw.exec(`RELEASE ${name}`);
|
|
1247
|
+
else raw.exec("COMMIT");
|
|
1248
|
+
return out;
|
|
1249
|
+
} catch (e) {
|
|
1250
|
+
if (nested) {
|
|
1251
|
+
raw.exec(`ROLLBACK TO ${name}`);
|
|
1252
|
+
raw.exec(`RELEASE ${name}`);
|
|
1253
|
+
} else {
|
|
1254
|
+
raw.exec("ROLLBACK");
|
|
1255
|
+
}
|
|
1256
|
+
throw e;
|
|
1257
|
+
}
|
|
1258
|
+
};
|
|
1259
|
+
return run;
|
|
1260
|
+
};
|
|
1261
|
+
const serialize = () => {
|
|
1262
|
+
if (raw.isTransaction) {
|
|
1263
|
+
throw new Error(
|
|
1264
|
+
"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."
|
|
1265
|
+
);
|
|
1266
|
+
}
|
|
1267
|
+
const dir = mkdtempSync(join2(tmpdir(), "massu-serialize-"));
|
|
1268
|
+
const out = join2(dir, "snapshot.db");
|
|
1269
|
+
try {
|
|
1270
|
+
raw.exec(`VACUUM INTO '${out.replace(/'/g, "''")}'`);
|
|
1271
|
+
const fd = openSync(out, "r");
|
|
1272
|
+
try {
|
|
1273
|
+
fsyncSync(fd);
|
|
1274
|
+
} finally {
|
|
1275
|
+
closeSync(fd);
|
|
1276
|
+
}
|
|
1277
|
+
return readFileSync4(out);
|
|
1278
|
+
} finally {
|
|
1279
|
+
rmSync3(dir, { recursive: true, force: true });
|
|
1280
|
+
}
|
|
1281
|
+
};
|
|
1282
|
+
const handle = {
|
|
1283
|
+
prepare: (sql) => wrapStmt(sql),
|
|
1284
|
+
exec: (sql) => raw.exec(sql),
|
|
1285
|
+
pragma,
|
|
1286
|
+
transaction,
|
|
1287
|
+
serialize,
|
|
1288
|
+
close: () => raw.close()
|
|
1289
|
+
};
|
|
1290
|
+
return handle;
|
|
1291
|
+
}
|
|
1292
|
+
function openBetterSqlite3(dbPath, opts) {
|
|
1293
|
+
return openDatabase(dbPath, opts);
|
|
1294
|
+
}
|
|
1295
|
+
function openDatabase2(dbPath, opts = {}) {
|
|
1296
|
+
return resolveDbEngine() === "better-sqlite3" ? openBetterSqlite3(dbPath, opts) : openNodeSqlite(dbPath, opts);
|
|
1297
|
+
}
|
|
1298
|
+
var req2, DEFAULT_DB_ENGINE, DB_ENGINE_ENV, _nodeCtor;
|
|
1299
|
+
var init_db_driver = __esm({
|
|
1300
|
+
"src/db-driver.ts"() {
|
|
1301
|
+
"use strict";
|
|
1302
|
+
init_sqlite_loader();
|
|
1303
|
+
init_sqlite_loader();
|
|
1304
|
+
req2 = createRequire2(import.meta.url);
|
|
1305
|
+
DEFAULT_DB_ENGINE = "node-sqlite";
|
|
1306
|
+
DB_ENGINE_ENV = "MASSU_DB_ENGINE";
|
|
1307
|
+
_nodeCtor = null;
|
|
1308
|
+
}
|
|
1309
|
+
});
|
|
1310
|
+
|
|
1156
1311
|
// src/memory-vector.ts
|
|
1157
1312
|
function float32ToBlob(vec) {
|
|
1158
1313
|
return Buffer.from(new Uint8Array(vec.buffer, vec.byteOffset, vec.byteLength));
|
|
@@ -1173,26 +1328,26 @@ var init_memory_vector = __esm({
|
|
|
1173
1328
|
});
|
|
1174
1329
|
|
|
1175
1330
|
// src/db-backup.ts
|
|
1176
|
-
import { existsSync as
|
|
1177
|
-
import { resolve as resolve3, join as
|
|
1331
|
+
import { existsSync as existsSync5, mkdirSync as mkdirSync4, readdirSync, statSync, unlinkSync, copyFileSync, rmSync as rmSync4 } from "fs";
|
|
1332
|
+
import { resolve as resolve3, join as join3, basename, dirname as dirname4 } from "path";
|
|
1178
1333
|
import { homedir as homedir3 } from "os";
|
|
1179
1334
|
function dbBackupsRoot(home = homedir3()) {
|
|
1180
1335
|
return resolve3(home, ".massu", "db-backups");
|
|
1181
1336
|
}
|
|
1182
1337
|
function projectBackupDir(projectRoot, home = homedir3()) {
|
|
1183
1338
|
const slug = basename(projectRoot).replace(/[^A-Za-z0-9._-]/g, "_") || "project";
|
|
1184
|
-
return
|
|
1339
|
+
return join3(dbBackupsRoot(home), slug);
|
|
1185
1340
|
}
|
|
1186
1341
|
function backupStamp(nowMs) {
|
|
1187
1342
|
return new Date(nowMs).toISOString().replace(/[:.]/g, "-");
|
|
1188
1343
|
}
|
|
1189
1344
|
function listDbBackups(projectRoot, home = homedir3()) {
|
|
1190
1345
|
const dir = projectBackupDir(projectRoot, home);
|
|
1191
|
-
if (!
|
|
1346
|
+
if (!existsSync5(dir)) return [];
|
|
1192
1347
|
const out = [];
|
|
1193
1348
|
for (const f of readdirSync(dir)) {
|
|
1194
1349
|
if (!f.endsWith(".db")) continue;
|
|
1195
|
-
const p =
|
|
1350
|
+
const p = join3(dir, f);
|
|
1196
1351
|
const st = statSync(p);
|
|
1197
1352
|
const db = f.replace(/-\d{4}-\d{2}-\d{2}T.*$/, "");
|
|
1198
1353
|
out.push({ db, path: p, bytes: st.size, mtimeMs: st.mtimeMs });
|
|
@@ -1205,13 +1360,13 @@ function hasFreshDbBackup(projectRoot, dbPath, nowMs = Date.now(), home = homedi
|
|
|
1205
1360
|
if (backups.length === 0) return false;
|
|
1206
1361
|
const newest = backups[0];
|
|
1207
1362
|
if (nowMs - newest.mtimeMs > FRESH_WINDOW_MS) return false;
|
|
1208
|
-
if (
|
|
1363
|
+
if (existsSync5(dbPath) && statSync(dbPath).mtimeMs > newest.mtimeMs) return false;
|
|
1209
1364
|
return true;
|
|
1210
1365
|
}
|
|
1211
1366
|
function integrityOk(dbPath) {
|
|
1212
1367
|
let db = null;
|
|
1213
1368
|
try {
|
|
1214
|
-
db =
|
|
1369
|
+
db = openDatabase2(dbPath, { readonly: true });
|
|
1215
1370
|
const rows = db.pragma("integrity_check");
|
|
1216
1371
|
return rows.length === 1 && rows[0].integrity_check === "ok";
|
|
1217
1372
|
} catch {
|
|
@@ -1224,16 +1379,16 @@ function integrityOk(dbPath) {
|
|
|
1224
1379
|
}
|
|
1225
1380
|
}
|
|
1226
1381
|
function backupDb(projectRoot, dbPath, nowMs = Date.now(), home = homedir3()) {
|
|
1227
|
-
if (!
|
|
1382
|
+
if (!existsSync5(dbPath)) {
|
|
1228
1383
|
throw new DbBackupError(`cannot back up a database that does not exist: ${dbPath}`);
|
|
1229
1384
|
}
|
|
1230
1385
|
const dir = projectBackupDir(projectRoot, home);
|
|
1231
1386
|
mkdirSync4(dir, { recursive: true });
|
|
1232
1387
|
const name = basename(dbPath, ".db");
|
|
1233
|
-
const dest =
|
|
1388
|
+
const dest = join3(dir, `${name}-${backupStamp(nowMs)}.db`);
|
|
1234
1389
|
let src = null;
|
|
1235
1390
|
try {
|
|
1236
|
-
src =
|
|
1391
|
+
src = openDatabase2(dbPath, { readonly: true });
|
|
1237
1392
|
src.exec(`VACUUM INTO '${dest.replace(/'/g, "''")}'`);
|
|
1238
1393
|
} catch (err) {
|
|
1239
1394
|
throw new DbBackupError(
|
|
@@ -1272,7 +1427,7 @@ function pruneDbBackups(projectRoot, dbName, keep = DEFAULT_RETENTION, home = ho
|
|
|
1272
1427
|
}
|
|
1273
1428
|
function backupBeforeSchemaChange(projectRoot, dbPath, onError, nowMs = Date.now(), home = homedir3()) {
|
|
1274
1429
|
try {
|
|
1275
|
-
if (!
|
|
1430
|
+
if (!existsSync5(dbPath)) return null;
|
|
1276
1431
|
if (hasFreshDbBackup(projectRoot, dbPath, nowMs, home)) return null;
|
|
1277
1432
|
return backupDb(projectRoot, dbPath, nowMs, home);
|
|
1278
1433
|
} catch (err) {
|
|
@@ -1284,7 +1439,7 @@ var DEFAULT_RETENTION, FRESH_WINDOW_MS, DbBackupError;
|
|
|
1284
1439
|
var init_db_backup = __esm({
|
|
1285
1440
|
"src/db-backup.ts"() {
|
|
1286
1441
|
"use strict";
|
|
1287
|
-
|
|
1442
|
+
init_db_driver();
|
|
1288
1443
|
DEFAULT_RETENTION = 5;
|
|
1289
1444
|
FRESH_WINDOW_MS = 24 * 60 * 60 * 1e3;
|
|
1290
1445
|
DbBackupError = class extends Error {
|
|
@@ -1397,9 +1552,9 @@ var init_rule_candidate_store = __esm({
|
|
|
1397
1552
|
});
|
|
1398
1553
|
|
|
1399
1554
|
// src/memory-embedder-tokenizer.ts
|
|
1400
|
-
import { readFileSync as
|
|
1555
|
+
import { readFileSync as readFileSync5 } from "fs";
|
|
1401
1556
|
function loadVocab(vocabPath) {
|
|
1402
|
-
const lines =
|
|
1557
|
+
const lines = readFileSync5(vocabPath, "utf-8").split("\n");
|
|
1403
1558
|
const vocab = /* @__PURE__ */ new Map();
|
|
1404
1559
|
for (let i = 0; i < lines.length; i++) {
|
|
1405
1560
|
const tok = lines[i].replace(/\r$/, "");
|
|
@@ -1519,9 +1674,9 @@ var init_memory_embedder_tokenizer = __esm({
|
|
|
1519
1674
|
|
|
1520
1675
|
// src/memory-embedder.ts
|
|
1521
1676
|
import { fileURLToPath } from "url";
|
|
1522
|
-
import { dirname as dirname5, join as
|
|
1523
|
-
import { existsSync as
|
|
1524
|
-
import { createRequire as
|
|
1677
|
+
import { dirname as dirname5, join as join4 } from "path";
|
|
1678
|
+
import { existsSync as existsSync6 } from "fs";
|
|
1679
|
+
import { createRequire as createRequire3 } from "module";
|
|
1525
1680
|
function getActiveEmbedModel() {
|
|
1526
1681
|
return _activeModel;
|
|
1527
1682
|
}
|
|
@@ -1545,7 +1700,7 @@ function resolveModelDir() {
|
|
|
1545
1700
|
let dir = dirname5(fileURLToPath(import.meta.url));
|
|
1546
1701
|
let root = null;
|
|
1547
1702
|
for (let i = 0; i < 8; i++) {
|
|
1548
|
-
if (
|
|
1703
|
+
if (existsSync6(join4(dir, "package.json"))) {
|
|
1549
1704
|
root = dir;
|
|
1550
1705
|
break;
|
|
1551
1706
|
}
|
|
@@ -1555,8 +1710,8 @@ function resolveModelDir() {
|
|
|
1555
1710
|
}
|
|
1556
1711
|
if (!root) return null;
|
|
1557
1712
|
for (const rel of ["dist/embedder", "assets/embedder"]) {
|
|
1558
|
-
const candidate =
|
|
1559
|
-
if (
|
|
1713
|
+
const candidate = join4(root, rel);
|
|
1714
|
+
if (existsSync6(join4(candidate, "model_quantized.onnx"))) return candidate;
|
|
1560
1715
|
}
|
|
1561
1716
|
return null;
|
|
1562
1717
|
} catch {
|
|
@@ -1565,8 +1720,8 @@ function resolveModelDir() {
|
|
|
1565
1720
|
}
|
|
1566
1721
|
function resolveWasmDir() {
|
|
1567
1722
|
try {
|
|
1568
|
-
const
|
|
1569
|
-
const main2 =
|
|
1723
|
+
const req3 = createRequire3(import.meta.url);
|
|
1724
|
+
const main2 = req3.resolve(ORT_PKG);
|
|
1570
1725
|
return dirname5(main2);
|
|
1571
1726
|
} catch {
|
|
1572
1727
|
return null;
|
|
@@ -1587,8 +1742,8 @@ async function getSession() {
|
|
|
1587
1742
|
ort.env.wasm.proxy = false;
|
|
1588
1743
|
ort.env.wasm.wasmPaths = wasmDir.endsWith("/") ? wasmDir : wasmDir + "/";
|
|
1589
1744
|
_ortTensor = ort.Tensor;
|
|
1590
|
-
const modelPath =
|
|
1591
|
-
const vocabPath =
|
|
1745
|
+
const modelPath = join4(modelDir, "model_quantized.onnx");
|
|
1746
|
+
const vocabPath = join4(modelDir, "vocab.txt");
|
|
1592
1747
|
_vocab = loadVocab(vocabPath);
|
|
1593
1748
|
const session = await ort.InferenceSession.create(modelPath, {
|
|
1594
1749
|
executionProviders: ["wasm"],
|
|
@@ -1909,6 +2064,7 @@ __export(memory_db_exports, {
|
|
|
1909
2064
|
migrateAuditLogCheckExtension: () => migrateAuditLogCheckExtension,
|
|
1910
2065
|
migrateMemoryFilesFor4B: () => migrateMemoryFilesFor4B,
|
|
1911
2066
|
migrateObservationEmbeddingChunks: () => migrateObservationEmbeddingChunks,
|
|
2067
|
+
migrateSharedMemoryFor5B: () => migrateSharedMemoryFor5B,
|
|
1912
2068
|
pruneOldConversationTurns: () => pruneOldConversationTurns,
|
|
1913
2069
|
pruneOldObservations: () => pruneOldObservations,
|
|
1914
2070
|
pruneToolCostEvents: () => pruneToolCostEvents,
|
|
@@ -1926,7 +2082,7 @@ __export(memory_db_exports, {
|
|
|
1926
2082
|
usageWarmupElapsed: () => usageWarmupElapsed
|
|
1927
2083
|
});
|
|
1928
2084
|
import { dirname as dirname6, basename as basename2 } from "path";
|
|
1929
|
-
import { existsSync as
|
|
2085
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync5 } from "fs";
|
|
1930
2086
|
function sanitizeFts5Query(raw) {
|
|
1931
2087
|
const trimmed = raw.trim();
|
|
1932
2088
|
if (!trimmed) return '""';
|
|
@@ -1943,11 +2099,11 @@ function sanitizeFts5QueryOr(raw) {
|
|
|
1943
2099
|
function getMemoryDb() {
|
|
1944
2100
|
const dbPath = getResolvedPaths().memoryDbPath;
|
|
1945
2101
|
const dir = dirname6(dbPath);
|
|
1946
|
-
if (!
|
|
2102
|
+
if (!existsSync7(dir)) {
|
|
1947
2103
|
mkdirSync5(dir, { recursive: true });
|
|
1948
2104
|
}
|
|
1949
|
-
const preExisting =
|
|
1950
|
-
const db =
|
|
2105
|
+
const preExisting = existsSync7(dbPath);
|
|
2106
|
+
const db = openDatabase2(dbPath);
|
|
1951
2107
|
db.pragma("journal_mode = WAL");
|
|
1952
2108
|
db.pragma("foreign_keys = ON");
|
|
1953
2109
|
const onDisk = db.pragma("user_version", { simple: true });
|
|
@@ -2023,6 +2179,39 @@ function migrateMemoryFilesFor4B(db) {
|
|
|
2023
2179
|
db.exec(`ALTER TABLE ${add.table} ADD COLUMN ${add.name} ${add.decl}`);
|
|
2024
2180
|
}
|
|
2025
2181
|
}
|
|
2182
|
+
function migrateSharedMemoryFor5B(db) {
|
|
2183
|
+
const cols = db.prepare(`PRAGMA table_info(observations)`).all();
|
|
2184
|
+
if (cols.length > 0 && !cols.some((c) => c.name === "shareable")) {
|
|
2185
|
+
db.exec(`ALTER TABLE observations ADD COLUMN shareable INTEGER NOT NULL DEFAULT 0`);
|
|
2186
|
+
}
|
|
2187
|
+
db.exec(`
|
|
2188
|
+
CREATE TABLE IF NOT EXISTS shared_memory_pending (
|
|
2189
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
2190
|
+
record_hash TEXT NOT NULL UNIQUE, -- identity + idempotency key (hex sha256)
|
|
2191
|
+
origin_repo_id TEXT NOT NULL, -- the signed origin repo_id (v4 UUID)
|
|
2192
|
+
origin_repo_label TEXT NOT NULL, -- re-slugged on read; NEVER trusted from the wire
|
|
2193
|
+
envelope_raw TEXT NOT NULL, -- the VERBATIM signed bytes; B-05 re-verifies these
|
|
2194
|
+
record_json TEXT NOT NULL, -- the single record's canonical JSON
|
|
2195
|
+
received_at_epoch INTEGER NOT NULL, -- epoch SECONDS (Slice-2 convention)
|
|
2196
|
+
accepted_at_epoch INTEGER, -- set on accept (B-05); NULL while pending
|
|
2197
|
+
refused_at_epoch INTEGER, -- set on refuse (B-06)
|
|
2198
|
+
expired_at_epoch INTEGER -- revocation \u21D2 EXPIRE, never DELETE (B-07 / S-3)
|
|
2199
|
+
);
|
|
2200
|
+
CREATE INDEX IF NOT EXISTS idx_smp_origin ON shared_memory_pending(origin_repo_id);
|
|
2201
|
+
CREATE INDEX IF NOT EXISTS idx_smp_received ON shared_memory_pending(received_at_epoch DESC);
|
|
2202
|
+
`);
|
|
2203
|
+
db.exec(`
|
|
2204
|
+
CREATE TABLE IF NOT EXISTS shared_memory_outbound (
|
|
2205
|
+
record_hash TEXT PRIMARY KEY, -- the exported record's canonical hash
|
|
2206
|
+
observation_id INTEGER NOT NULL, -- the source observations.id (for B-07 revocation)
|
|
2207
|
+
origin_repo_id TEXT NOT NULL, -- this repo's own repo_id at export time
|
|
2208
|
+
seq INTEGER NOT NULL, -- the envelope seq this record first went out in
|
|
2209
|
+
exported_at_epoch INTEGER NOT NULL, -- epoch SECONDS
|
|
2210
|
+
revoked_at_epoch INTEGER -- set when the source row is superseded/expired (B-07)
|
|
2211
|
+
);
|
|
2212
|
+
CREATE INDEX IF NOT EXISTS idx_smo_obs ON shared_memory_outbound(observation_id);
|
|
2213
|
+
`);
|
|
2214
|
+
}
|
|
2026
2215
|
function migrateAuditLogCheckExtension(db) {
|
|
2027
2216
|
const row = db.prepare("SELECT sql FROM sqlite_master WHERE type='table' AND name='audit_log'").get();
|
|
2028
2217
|
if (!row) return;
|
|
@@ -2045,7 +2234,16 @@ function migrateAuditLogCheckExtension(db) {
|
|
|
2045
2234
|
"memory_file_adopted_human",
|
|
2046
2235
|
"memory_file_rendered",
|
|
2047
2236
|
"memory_file_render_refused",
|
|
2048
|
-
"memory_file_tombstoned"
|
|
2237
|
+
"memory_file_tombstoned",
|
|
2238
|
+
// B-10 (Slice 5) — the seven cross-repo shared-memory events. An existing DB whose
|
|
2239
|
+
// CHECK lacks any of these triggers the table rebuild below.
|
|
2240
|
+
"shared_memory_exported",
|
|
2241
|
+
"shared_memory_export_refused",
|
|
2242
|
+
"shared_memory_imported",
|
|
2243
|
+
"shared_memory_dropped",
|
|
2244
|
+
"shared_memory_accepted",
|
|
2245
|
+
"shared_memory_refused",
|
|
2246
|
+
"shared_memory_revoked"
|
|
2049
2247
|
];
|
|
2050
2248
|
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);
|
|
2051
2249
|
if (checkClauseMatch) {
|
|
@@ -2064,7 +2262,12 @@ function migrateAuditLogCheckExtension(db) {
|
|
|
2064
2262
|
'code_change', 'rule_enforced', 'approval', 'review', 'commit', 'compaction',
|
|
2065
2263
|
'rule_candidate_emitted', 'rule_promoted', 'rule_dismissed',
|
|
2066
2264
|
'memory_file_ingested', 'memory_file_expired', 'memory_file_adopted_human',
|
|
2067
|
-
'memory_file_rendered', 'memory_file_render_refused', 'memory_file_tombstoned'
|
|
2265
|
+
'memory_file_rendered', 'memory_file_render_refused', 'memory_file_tombstoned',
|
|
2266
|
+
-- B-10 (Slice 5) \u2014 cross-repo shared-memory events (must mirror the CREATE +
|
|
2267
|
+
-- the expected[] list above, or the migration would loop rebuilding forever).
|
|
2268
|
+
'shared_memory_exported', 'shared_memory_export_refused', 'shared_memory_imported',
|
|
2269
|
+
'shared_memory_dropped', 'shared_memory_accepted', 'shared_memory_refused',
|
|
2270
|
+
'shared_memory_revoked'
|
|
2068
2271
|
)),
|
|
2069
2272
|
actor TEXT NOT NULL DEFAULT 'ai' CHECK(actor IN ('ai', 'human', 'hook', 'agent')),
|
|
2070
2273
|
model_id TEXT,
|
|
@@ -2141,6 +2344,12 @@ function initMemorySchema(db) {
|
|
|
2141
2344
|
-- credential, or takes a snapshot. A Slice-5 synced memory arrives here with
|
|
2142
2345
|
-- origin='team' and must never reach disk without CR-55's gates.
|
|
2143
2346
|
origin TEXT NOT NULL DEFAULT 'local',
|
|
2347
|
+
-- B-01 (Slice 5, cross-repo surfacing): the per-decision SHARE opt-in. Set to 1
|
|
2348
|
+
-- ONLY by an explicit human act (massu memory share <id>); NEVER by a heuristic,
|
|
2349
|
+
-- score, LLM, hook, or consolidation pass. Export (B-02) reads WHERE shareable=1
|
|
2350
|
+
-- AND origin='local'. Defaults 0 so every pre-existing and machine-written row is
|
|
2351
|
+
-- un-shareable until a human says otherwise (fail-closed).
|
|
2352
|
+
shareable INTEGER NOT NULL DEFAULT 0,
|
|
2144
2353
|
created_at TEXT NOT NULL,
|
|
2145
2354
|
created_at_epoch INTEGER NOT NULL,
|
|
2146
2355
|
FOREIGN KEY(session_id) REFERENCES sessions(session_id) ON DELETE CASCADE
|
|
@@ -2414,7 +2623,13 @@ function initMemorySchema(db) {
|
|
|
2414
2623
|
'code_change', 'rule_enforced', 'approval', 'review', 'commit', 'compaction',
|
|
2415
2624
|
'rule_candidate_emitted', 'rule_promoted', 'rule_dismissed',
|
|
2416
2625
|
'memory_file_ingested', 'memory_file_expired', 'memory_file_adopted_human',
|
|
2417
|
-
'memory_file_rendered', 'memory_file_render_refused', 'memory_file_tombstoned'
|
|
2626
|
+
'memory_file_rendered', 'memory_file_render_refused', 'memory_file_tombstoned',
|
|
2627
|
+
-- B-10 (Slice 5): cross-repo shared-memory observability. Without these seven,
|
|
2628
|
+
-- nobody could ever answer "what has crossed between my repos?" (and an ingest
|
|
2629
|
+
-- catch would swallow the CHECK violation, silently producing NOTHING).
|
|
2630
|
+
'shared_memory_exported', 'shared_memory_export_refused', 'shared_memory_imported',
|
|
2631
|
+
'shared_memory_dropped', 'shared_memory_accepted', 'shared_memory_refused',
|
|
2632
|
+
'shared_memory_revoked'
|
|
2418
2633
|
)),
|
|
2419
2634
|
actor TEXT NOT NULL DEFAULT 'ai' CHECK(actor IN ('ai', 'human', 'hook', 'agent')),
|
|
2420
2635
|
model_id TEXT,
|
|
@@ -2783,6 +2998,7 @@ function initMemorySchema(db) {
|
|
|
2783
2998
|
`);
|
|
2784
2999
|
migrateObservationEmbeddingChunks(db);
|
|
2785
3000
|
migrateMemoryFilesFor4B(db);
|
|
3001
|
+
migrateSharedMemoryFor5B(db);
|
|
2786
3002
|
ensureRuleCandidatesTable(db);
|
|
2787
3003
|
migrateRuleDelivery(db);
|
|
2788
3004
|
}
|
|
@@ -3653,7 +3869,7 @@ var MEMORY_SCHEMA_VERSION, TOOL_COST_EVENTS_RETENTION_DAYS, MAX_DRAFT_TEXT_LEN,
|
|
|
3653
3869
|
var init_memory_db = __esm({
|
|
3654
3870
|
"src/memory-db.ts"() {
|
|
3655
3871
|
"use strict";
|
|
3656
|
-
|
|
3872
|
+
init_db_driver();
|
|
3657
3873
|
init_config();
|
|
3658
3874
|
init_memory_vector();
|
|
3659
3875
|
init_db_backup();
|
|
@@ -3674,21 +3890,21 @@ var init_memory_db = __esm({
|
|
|
3674
3890
|
});
|
|
3675
3891
|
|
|
3676
3892
|
// src/hooks/lib/hook-failure-signal.ts
|
|
3677
|
-
import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync6, existsSync as
|
|
3678
|
-
import { join as
|
|
3893
|
+
import { appendFileSync as appendFileSync2, mkdirSync as mkdirSync6, existsSync as existsSync8 } from "fs";
|
|
3894
|
+
import { join as join5, dirname as dirname7 } from "path";
|
|
3679
3895
|
function resolveFailureLogPath() {
|
|
3680
3896
|
const explicit = process.env.MASSU_HOOK_FAILURE_LOG;
|
|
3681
3897
|
if (explicit) return explicit;
|
|
3682
3898
|
let dir = process.cwd();
|
|
3683
3899
|
for (let i = 0; i < 12; i++) {
|
|
3684
|
-
if (
|
|
3685
|
-
return
|
|
3900
|
+
if (existsSync8(join5(dir, ".massu")) || existsSync8(join5(dir, "massu.config.yaml"))) {
|
|
3901
|
+
return join5(dir, ".massu", "hook-failures.jsonl");
|
|
3686
3902
|
}
|
|
3687
3903
|
const parent = dirname7(dir);
|
|
3688
3904
|
if (parent === dir) break;
|
|
3689
3905
|
dir = parent;
|
|
3690
3906
|
}
|
|
3691
|
-
return
|
|
3907
|
+
return join5(process.cwd(), ".massu", "hook-failures.jsonl");
|
|
3692
3908
|
}
|
|
3693
3909
|
function recordHookFailure(hook, error, context) {
|
|
3694
3910
|
const record = {
|
|
@@ -3747,9 +3963,9 @@ var init_hook_failure_signal = __esm({
|
|
|
3747
3963
|
// src/hooks/auto-learning-pipeline.ts
|
|
3748
3964
|
init_config();
|
|
3749
3965
|
import { execFileSync } from "child_process";
|
|
3750
|
-
import { existsSync as
|
|
3751
|
-
import { tmpdir } from "os";
|
|
3752
|
-
import { join as
|
|
3966
|
+
import { existsSync as existsSync9, readFileSync as readFileSync6, unlinkSync as unlinkSync2, readdirSync as readdirSync2, statSync as statSync2 } from "fs";
|
|
3967
|
+
import { tmpdir as tmpdir2 } from "os";
|
|
3968
|
+
import { join as join6 } from "path";
|
|
3753
3969
|
|
|
3754
3970
|
// src/hooks/lib/write-hook-message.ts
|
|
3755
3971
|
function writeHookMessage(message) {
|
|
@@ -3760,7 +3976,7 @@ function writeHookMessage(message) {
|
|
|
3760
3976
|
init_hook_failure_signal();
|
|
3761
3977
|
var MAX_FULL_DIFF_BYTES = 2 * 1024 * 1024;
|
|
3762
3978
|
function getSessionFlagPath(sessionId) {
|
|
3763
|
-
return
|
|
3979
|
+
return join6(tmpdir2(), "massu-auto-learning", `fixes-${sessionId.slice(0, 12)}.jsonl`);
|
|
3764
3980
|
}
|
|
3765
3981
|
async function main() {
|
|
3766
3982
|
try {
|
|
@@ -3777,9 +3993,9 @@ async function main() {
|
|
|
3777
3993
|
const autoLearn = config.autoLearning;
|
|
3778
3994
|
const flagPath = getSessionFlagPath(hookInput.session_id);
|
|
3779
3995
|
let sessionFixes = [];
|
|
3780
|
-
if (
|
|
3996
|
+
if (existsSync9(flagPath)) {
|
|
3781
3997
|
try {
|
|
3782
|
-
sessionFixes =
|
|
3998
|
+
sessionFixes = readFileSync6(flagPath, "utf-8").split("\n").filter(Boolean).map((line) => JSON.parse(line));
|
|
3783
3999
|
} catch {
|
|
3784
4000
|
}
|
|
3785
4001
|
}
|
|
@@ -3885,12 +4101,12 @@ async function main() {
|
|
|
3885
4101
|
}
|
|
3886
4102
|
function cleanup(flagPath) {
|
|
3887
4103
|
try {
|
|
3888
|
-
if (
|
|
3889
|
-
const dir =
|
|
3890
|
-
if (
|
|
4104
|
+
if (existsSync9(flagPath)) unlinkSync2(flagPath);
|
|
4105
|
+
const dir = join6(tmpdir2(), "massu-auto-learning");
|
|
4106
|
+
if (existsSync9(dir)) {
|
|
3891
4107
|
const now = Date.now();
|
|
3892
4108
|
for (const file of readdirSync2(dir)) {
|
|
3893
|
-
const fullPath =
|
|
4109
|
+
const fullPath = join6(dir, file);
|
|
3894
4110
|
try {
|
|
3895
4111
|
const stat = statSync2(fullPath);
|
|
3896
4112
|
if (now - stat.mtimeMs > 864e5) {
|