@massu/core 1.15.7 → 1.16.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 +63 -0
- package/dist/cli.js +1943 -774
- package/dist/hooks/auto-learning-pipeline.js +152 -67
- package/dist/hooks/classify-failure.js +152 -67
- package/dist/hooks/cost-tracker.js +152 -67
- package/dist/hooks/fix-detector.js +152 -67
- package/dist/hooks/incident-pipeline.js +152 -67
- package/dist/hooks/intent-suggester.js +152 -67
- package/dist/hooks/memory-recall.js +152 -67
- package/dist/hooks/post-edit-context.js +152 -67
- package/dist/hooks/post-tool-use.js +152 -67
- package/dist/hooks/pre-compact.js +152 -67
- package/dist/hooks/pre-delete-check.js +154 -69
- package/dist/hooks/pre-tool-use-gate.js +154 -69
- package/dist/hooks/quality-event.js +152 -67
- package/dist/hooks/rule-enforcement-pipeline.js +152 -67
- package/dist/hooks/security-gate.js +152 -67
- package/dist/hooks/session-end.js +346 -80
- package/dist/hooks/session-start.js +155 -67
- package/dist/hooks/user-prompt.js +222 -111
- package/package.json +1 -1
|
@@ -950,22 +950,147 @@ var init_db_backup = __esm({
|
|
|
950
950
|
}
|
|
951
951
|
});
|
|
952
952
|
|
|
953
|
+
// src/rule-delivery.ts
|
|
954
|
+
function tableExists(db, table) {
|
|
955
|
+
const row = db.prepare("SELECT 1 AS ok FROM sqlite_master WHERE type='table' AND name=?").get(table);
|
|
956
|
+
return row !== void 0;
|
|
957
|
+
}
|
|
958
|
+
function columns(db, table) {
|
|
959
|
+
const rows = db.prepare(`PRAGMA table_info(${table})`).all();
|
|
960
|
+
return new Set(rows.map((r) => r.name));
|
|
961
|
+
}
|
|
962
|
+
function migrateRuleDelivery(db) {
|
|
963
|
+
const ADDITIONS = [
|
|
964
|
+
{ name: "attempts", decl: "INTEGER NOT NULL DEFAULT 0" },
|
|
965
|
+
{ name: "lease_token", decl: "TEXT" },
|
|
966
|
+
{ name: "leased_at_ms", decl: "INTEGER" },
|
|
967
|
+
{ name: "last_error", decl: "TEXT" }
|
|
968
|
+
];
|
|
969
|
+
for (const table of ALL_OUTBOUND_TABLES) {
|
|
970
|
+
if (!tableExists(db, table)) continue;
|
|
971
|
+
const cols = columns(db, table);
|
|
972
|
+
for (const add of ADDITIONS) {
|
|
973
|
+
if (cols.has(add.name)) continue;
|
|
974
|
+
db.exec(`ALTER TABLE ${table} ADD COLUMN ${add.name} ${add.decl}`);
|
|
975
|
+
}
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
function capTelemetry(db, cap = TELEMETRY_OUTBOX_CAP) {
|
|
979
|
+
if (!tableExists(db, TELEMETRY_OUTBOUND_TABLE)) return 0;
|
|
980
|
+
const before = db.prepare(`SELECT COUNT(*) AS n FROM ${TELEMETRY_OUTBOUND_TABLE}`).get();
|
|
981
|
+
if (before.n <= cap) return 0;
|
|
982
|
+
const info = db.prepare(
|
|
983
|
+
`DELETE FROM ${TELEMETRY_OUTBOUND_TABLE}
|
|
984
|
+
WHERE id NOT IN (SELECT id FROM ${TELEMETRY_OUTBOUND_TABLE} ORDER BY id DESC LIMIT ?)`
|
|
985
|
+
).run(cap);
|
|
986
|
+
const dropped = info.changes;
|
|
987
|
+
if (dropped > 0) {
|
|
988
|
+
process.stderr.write(
|
|
989
|
+
`[massu] NOTE: dropped ${dropped} oldest promotion-funnel telemetry row(s) at the ${cap}-row cap. No learned rules were affected (rules are never dropped).
|
|
990
|
+
`
|
|
991
|
+
);
|
|
992
|
+
recordAnalytics(db, EVENT_TELEMETRY_CAPPED, { dropped, cap });
|
|
993
|
+
}
|
|
994
|
+
return dropped;
|
|
995
|
+
}
|
|
996
|
+
function recordAnalytics(db, eventType, data) {
|
|
997
|
+
try {
|
|
998
|
+
db.prepare(
|
|
999
|
+
`INSERT INTO analytics_events (event_type, event_data, created_at)
|
|
1000
|
+
VALUES (?, ?, datetime('now'))`
|
|
1001
|
+
).run(eventType, JSON.stringify(data));
|
|
1002
|
+
} catch (err) {
|
|
1003
|
+
process.stderr.write(
|
|
1004
|
+
`[massu] WARNING: could not record '${eventType}' telemetry: ${err instanceof Error ? err.message : String(err)}
|
|
1005
|
+
`
|
|
1006
|
+
);
|
|
1007
|
+
}
|
|
1008
|
+
}
|
|
1009
|
+
var RULE_OUTBOUND_TABLES, TELEMETRY_OUTBOUND_TABLE, ALL_OUTBOUND_TABLES, TELEMETRY_OUTBOX_CAP, LEASE_TTL_MS, EVENT_TELEMETRY_CAPPED;
|
|
1010
|
+
var init_rule_delivery = __esm({
|
|
1011
|
+
"src/rule-delivery.ts"() {
|
|
1012
|
+
"use strict";
|
|
1013
|
+
RULE_OUTBOUND_TABLES = [
|
|
1014
|
+
"team_promotion_outbound",
|
|
1015
|
+
"team_revocation_outbound"
|
|
1016
|
+
];
|
|
1017
|
+
TELEMETRY_OUTBOUND_TABLE = "rule_promotion_events_outbound";
|
|
1018
|
+
ALL_OUTBOUND_TABLES = [
|
|
1019
|
+
...RULE_OUTBOUND_TABLES,
|
|
1020
|
+
TELEMETRY_OUTBOUND_TABLE
|
|
1021
|
+
];
|
|
1022
|
+
TELEMETRY_OUTBOX_CAP = 2e4;
|
|
1023
|
+
LEASE_TTL_MS = 15 * 60 * 1e3;
|
|
1024
|
+
EVENT_TELEMETRY_CAPPED = "rule_telemetry_capped";
|
|
1025
|
+
}
|
|
1026
|
+
});
|
|
1027
|
+
|
|
1028
|
+
// src/rule-candidate-store.ts
|
|
1029
|
+
import { existsSync as existsSync4, mkdirSync as mkdirSync3, writeFileSync as writeFileSync2, readdirSync as readdirSync2, readFileSync as readFileSync3 } from "node:fs";
|
|
1030
|
+
import { join as join2 } from "node:path";
|
|
1031
|
+
function ensureRuleCandidatesTable(db) {
|
|
1032
|
+
db.exec(`
|
|
1033
|
+
CREATE TABLE IF NOT EXISTS rule_candidates (
|
|
1034
|
+
prompt_hash TEXT PRIMARY KEY,
|
|
1035
|
+
status TEXT NOT NULL DEFAULT 'proposed'
|
|
1036
|
+
CHECK (status IN ('proposed','shown','promoted','dismissed')),
|
|
1037
|
+
origin TEXT NOT NULL DEFAULT 'local'
|
|
1038
|
+
CHECK (origin IN ('local','team','pack')),
|
|
1039
|
+
score REAL,
|
|
1040
|
+
destination TEXT,
|
|
1041
|
+
payload_json TEXT NOT NULL DEFAULT '{}',
|
|
1042
|
+
created_at TEXT NOT NULL DEFAULT (datetime('now')),
|
|
1043
|
+
updated_at TEXT NOT NULL DEFAULT (datetime('now'))
|
|
1044
|
+
);
|
|
1045
|
+
CREATE INDEX IF NOT EXISTS idx_rule_candidates_status ON rule_candidates(status);
|
|
1046
|
+
CREATE INDEX IF NOT EXISTS idx_rule_candidates_created ON rule_candidates(created_at);
|
|
1047
|
+
`);
|
|
1048
|
+
}
|
|
1049
|
+
function sidecarPath(projectRoot, promptHash) {
|
|
1050
|
+
return join2(projectRoot, ".massu", "rule-candidates", `${promptHash}.json`);
|
|
1051
|
+
}
|
|
1052
|
+
function upsertCandidate(db, projectRoot, candidate, opts = {}) {
|
|
1053
|
+
ensureRuleCandidatesTable(db);
|
|
1054
|
+
const origin = opts.origin ?? "local";
|
|
1055
|
+
const score = opts.score ?? (typeof candidate.score === "number" ? candidate.score : null);
|
|
1056
|
+
db.prepare(`
|
|
1057
|
+
INSERT INTO rule_candidates (prompt_hash, status, origin, score, payload_json, created_at, updated_at)
|
|
1058
|
+
VALUES (?, 'proposed', ?, ?, ?, datetime('now'), datetime('now'))
|
|
1059
|
+
ON CONFLICT(prompt_hash) DO UPDATE SET
|
|
1060
|
+
payload_json = excluded.payload_json,
|
|
1061
|
+
score = excluded.score,
|
|
1062
|
+
origin = excluded.origin,
|
|
1063
|
+
updated_at = datetime('now')
|
|
1064
|
+
`).run(candidate.prompt_hash, origin, score, JSON.stringify(candidate));
|
|
1065
|
+
const dir = join2(projectRoot, ".massu", "rule-candidates");
|
|
1066
|
+
mkdirSync3(dir, { recursive: true });
|
|
1067
|
+
const path = sidecarPath(projectRoot, candidate.prompt_hash);
|
|
1068
|
+
if (!existsSync4(path)) {
|
|
1069
|
+
writeFileSync2(path, JSON.stringify(candidate, null, 2), "utf-8");
|
|
1070
|
+
}
|
|
1071
|
+
}
|
|
1072
|
+
var init_rule_candidate_store = __esm({
|
|
1073
|
+
"src/rule-candidate-store.ts"() {
|
|
1074
|
+
"use strict";
|
|
1075
|
+
}
|
|
1076
|
+
});
|
|
1077
|
+
|
|
953
1078
|
// src/hooks/lib/hook-failure-signal.ts
|
|
954
|
-
import { appendFileSync, mkdirSync as
|
|
955
|
-
import { join as
|
|
1079
|
+
import { appendFileSync, mkdirSync as mkdirSync4, existsSync as existsSync5 } from "fs";
|
|
1080
|
+
import { join as join3, dirname as dirname3 } from "path";
|
|
956
1081
|
function resolveFailureLogPath() {
|
|
957
1082
|
const explicit = process.env.MASSU_HOOK_FAILURE_LOG;
|
|
958
1083
|
if (explicit) return explicit;
|
|
959
1084
|
let dir = process.cwd();
|
|
960
1085
|
for (let i = 0; i < 12; i++) {
|
|
961
|
-
if (
|
|
962
|
-
return
|
|
1086
|
+
if (existsSync5(join3(dir, ".massu")) || existsSync5(join3(dir, "massu.config.yaml"))) {
|
|
1087
|
+
return join3(dir, ".massu", "hook-failures.jsonl");
|
|
963
1088
|
}
|
|
964
1089
|
const parent = dirname3(dir);
|
|
965
1090
|
if (parent === dir) break;
|
|
966
1091
|
dir = parent;
|
|
967
1092
|
}
|
|
968
|
-
return
|
|
1093
|
+
return join3(process.cwd(), ".massu", "hook-failures.jsonl");
|
|
969
1094
|
}
|
|
970
1095
|
function recordHookFailure(hook, error, context) {
|
|
971
1096
|
const record = {
|
|
@@ -978,7 +1103,7 @@ function recordHookFailure(hook, error, context) {
|
|
|
978
1103
|
let wroteSomething = false;
|
|
979
1104
|
try {
|
|
980
1105
|
const path = resolveFailureLogPath();
|
|
981
|
-
|
|
1106
|
+
mkdirSync4(dirname3(path), { recursive: true });
|
|
982
1107
|
appendFileSync(path, JSON.stringify(record) + "\n", "utf-8");
|
|
983
1108
|
wroteSomething = true;
|
|
984
1109
|
} catch {
|
|
@@ -1022,9 +1147,9 @@ var init_hook_failure_signal = __esm({
|
|
|
1022
1147
|
});
|
|
1023
1148
|
|
|
1024
1149
|
// src/memory-embedder-tokenizer.ts
|
|
1025
|
-
import { readFileSync as
|
|
1150
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
1026
1151
|
function loadVocab(vocabPath) {
|
|
1027
|
-
const lines =
|
|
1152
|
+
const lines = readFileSync4(vocabPath, "utf-8").split("\n");
|
|
1028
1153
|
const vocab = /* @__PURE__ */ new Map();
|
|
1029
1154
|
for (let i = 0; i < lines.length; i++) {
|
|
1030
1155
|
const tok = lines[i].replace(/\r$/, "");
|
|
@@ -1144,8 +1269,8 @@ var init_memory_embedder_tokenizer = __esm({
|
|
|
1144
1269
|
|
|
1145
1270
|
// src/memory-embedder.ts
|
|
1146
1271
|
import { fileURLToPath } from "url";
|
|
1147
|
-
import { dirname as dirname4, join as
|
|
1148
|
-
import { existsSync as
|
|
1272
|
+
import { dirname as dirname4, join as join4 } from "path";
|
|
1273
|
+
import { existsSync as existsSync6 } from "fs";
|
|
1149
1274
|
import { createRequire } from "module";
|
|
1150
1275
|
function getActiveEmbedModel() {
|
|
1151
1276
|
return _activeModel;
|
|
@@ -1170,7 +1295,7 @@ function resolveModelDir() {
|
|
|
1170
1295
|
let dir = dirname4(fileURLToPath(import.meta.url));
|
|
1171
1296
|
let root = null;
|
|
1172
1297
|
for (let i = 0; i < 8; i++) {
|
|
1173
|
-
if (
|
|
1298
|
+
if (existsSync6(join4(dir, "package.json"))) {
|
|
1174
1299
|
root = dir;
|
|
1175
1300
|
break;
|
|
1176
1301
|
}
|
|
@@ -1180,8 +1305,8 @@ function resolveModelDir() {
|
|
|
1180
1305
|
}
|
|
1181
1306
|
if (!root) return null;
|
|
1182
1307
|
for (const rel of ["dist/embedder", "assets/embedder"]) {
|
|
1183
|
-
const candidate =
|
|
1184
|
-
if (
|
|
1308
|
+
const candidate = join4(root, rel);
|
|
1309
|
+
if (existsSync6(join4(candidate, "model_quantized.onnx"))) return candidate;
|
|
1185
1310
|
}
|
|
1186
1311
|
return null;
|
|
1187
1312
|
} catch {
|
|
@@ -1212,8 +1337,8 @@ async function getSession() {
|
|
|
1212
1337
|
ort.env.wasm.proxy = false;
|
|
1213
1338
|
ort.env.wasm.wasmPaths = wasmDir.endsWith("/") ? wasmDir : wasmDir + "/";
|
|
1214
1339
|
_ortTensor = ort.Tensor;
|
|
1215
|
-
const modelPath =
|
|
1216
|
-
const vocabPath =
|
|
1340
|
+
const modelPath = join4(modelDir, "model_quantized.onnx");
|
|
1341
|
+
const vocabPath = join4(modelDir, "vocab.txt");
|
|
1217
1342
|
_vocab = loadVocab(vocabPath);
|
|
1218
1343
|
const session = await ort.InferenceSession.create(modelPath, {
|
|
1219
1344
|
executionProviders: ["wasm"],
|
|
@@ -1503,9 +1628,6 @@ __export(memory_db_exports, {
|
|
|
1503
1628
|
createSession: () => createSession,
|
|
1504
1629
|
deduplicateFailedAttempt: () => deduplicateFailedAttempt,
|
|
1505
1630
|
dequeuePendingSync: () => dequeuePendingSync,
|
|
1506
|
-
drainRulePromotionEvents: () => drainRulePromotionEvents,
|
|
1507
|
-
drainTeamPromotions: () => drainTeamPromotions,
|
|
1508
|
-
drainTeamRevocations: () => drainTeamRevocations,
|
|
1509
1631
|
embedMissingObservations: () => embedMissingObservations,
|
|
1510
1632
|
endSession: () => endSession,
|
|
1511
1633
|
enqueueRulePromotionEvent: () => enqueueRulePromotionEvent,
|
|
@@ -1555,7 +1677,7 @@ __export(memory_db_exports, {
|
|
|
1555
1677
|
});
|
|
1556
1678
|
import Database2 from "better-sqlite3";
|
|
1557
1679
|
import { dirname as dirname5, basename as basename2 } from "path";
|
|
1558
|
-
import { existsSync as
|
|
1680
|
+
import { existsSync as existsSync7, mkdirSync as mkdirSync5 } from "fs";
|
|
1559
1681
|
function sanitizeFts5Query(raw) {
|
|
1560
1682
|
const trimmed = raw.trim();
|
|
1561
1683
|
if (!trimmed) return '""';
|
|
@@ -1572,10 +1694,10 @@ function sanitizeFts5QueryOr(raw) {
|
|
|
1572
1694
|
function getMemoryDb() {
|
|
1573
1695
|
const dbPath = getResolvedPaths().memoryDbPath;
|
|
1574
1696
|
const dir = dirname5(dbPath);
|
|
1575
|
-
if (!
|
|
1576
|
-
|
|
1697
|
+
if (!existsSync7(dir)) {
|
|
1698
|
+
mkdirSync5(dir, { recursive: true });
|
|
1577
1699
|
}
|
|
1578
|
-
const preExisting =
|
|
1700
|
+
const preExisting = existsSync7(dbPath);
|
|
1579
1701
|
const db = new Database2(dbPath);
|
|
1580
1702
|
db.pragma("journal_mode = WAL");
|
|
1581
1703
|
db.pragma("foreign_keys = ON");
|
|
@@ -2412,6 +2534,8 @@ function initMemorySchema(db) {
|
|
|
2412
2534
|
`);
|
|
2413
2535
|
migrateObservationEmbeddingChunks(db);
|
|
2414
2536
|
migrateMemoryFilesFor4B(db);
|
|
2537
|
+
ensureRuleCandidatesTable(db);
|
|
2538
|
+
migrateRuleDelivery(db);
|
|
2415
2539
|
}
|
|
2416
2540
|
function enqueueSyncPayload(db, payload) {
|
|
2417
2541
|
db.prepare("INSERT INTO pending_sync (payload) VALUES (?)").run(payload);
|
|
@@ -2486,38 +2610,12 @@ function enqueueTeamPromotion(db, promo) {
|
|
|
2486
2610
|
promo.review_attestation !== void 0 ? JSON.stringify(promo.review_attestation) : null
|
|
2487
2611
|
);
|
|
2488
2612
|
}
|
|
2489
|
-
function drainTeamPromotions(db) {
|
|
2490
|
-
const rows = db.prepare(`
|
|
2491
|
-
SELECT prompt_hash, destination, draft_text, score, signals_json, content_hash, hardened, review_attestation_json
|
|
2492
|
-
FROM team_promotion_outbound ORDER BY created_at ASC LIMIT 1000
|
|
2493
|
-
`).all();
|
|
2494
|
-
if (rows.length === 0) return [];
|
|
2495
|
-
db.prepare("DELETE FROM team_promotion_outbound").run();
|
|
2496
|
-
return rows.map((r) => ({
|
|
2497
|
-
prompt_hash: r.prompt_hash,
|
|
2498
|
-
destination: r.destination,
|
|
2499
|
-
draft_text: r.draft_text,
|
|
2500
|
-
score: r.score ?? void 0,
|
|
2501
|
-
signals: safeJsonArray(r.signals_json),
|
|
2502
|
-
content_hash: r.content_hash,
|
|
2503
|
-
hardened: r.hardened === 1,
|
|
2504
|
-
review_attestation: r.review_attestation_json ? safeJsonParse(r.review_attestation_json) : void 0
|
|
2505
|
-
}));
|
|
2506
|
-
}
|
|
2507
2613
|
function enqueueTeamRevocation(db, promptHash) {
|
|
2508
2614
|
db.prepare(`
|
|
2509
2615
|
INSERT OR REPLACE INTO team_revocation_outbound (prompt_hash, created_at)
|
|
2510
2616
|
VALUES (?, datetime('now'))
|
|
2511
2617
|
`).run(promptHash);
|
|
2512
2618
|
}
|
|
2513
|
-
function drainTeamRevocations(db) {
|
|
2514
|
-
const rows = db.prepare(
|
|
2515
|
-
`SELECT prompt_hash FROM team_revocation_outbound ORDER BY created_at ASC LIMIT 1000`
|
|
2516
|
-
).all();
|
|
2517
|
-
if (rows.length === 0) return [];
|
|
2518
|
-
db.prepare("DELETE FROM team_revocation_outbound").run();
|
|
2519
|
-
return rows.map((r) => r.prompt_hash);
|
|
2520
|
-
}
|
|
2521
2619
|
function getRecurrenceCountForPromptHash(db, promptHash) {
|
|
2522
2620
|
try {
|
|
2523
2621
|
const row = db.prepare(`
|
|
@@ -2545,30 +2643,14 @@ function enqueueRulePromotionEvent(db, ev) {
|
|
|
2545
2643
|
JSON.stringify(ev.metadata ?? {}),
|
|
2546
2644
|
ev.created_at
|
|
2547
2645
|
);
|
|
2548
|
-
db
|
|
2549
|
-
|
|
2550
|
-
|
|
2551
|
-
|
|
2552
|
-
|
|
2553
|
-
|
|
2554
|
-
} catch {
|
|
2646
|
+
capTelemetry(db, FUNNEL_EVENT_OUTBOX_CAP);
|
|
2647
|
+
} catch (err) {
|
|
2648
|
+
process.stderr.write(
|
|
2649
|
+
`[massu] WARNING: failed to record promotion-funnel event (${ev.event_type} / ${ev.prompt_hash}): ${err instanceof Error ? err.message : String(err)}
|
|
2650
|
+
`
|
|
2651
|
+
);
|
|
2555
2652
|
}
|
|
2556
2653
|
}
|
|
2557
|
-
function drainRulePromotionEvents(db) {
|
|
2558
|
-
const rows = db.prepare(`
|
|
2559
|
-
SELECT id, prompt_hash, event_type, metadata_json, created_at
|
|
2560
|
-
FROM rule_promotion_events_outbound ORDER BY id ASC LIMIT ?
|
|
2561
|
-
`).all(FUNNEL_EVENT_DRAIN_LIMIT);
|
|
2562
|
-
if (rows.length === 0) return [];
|
|
2563
|
-
const maxId = rows[rows.length - 1].id;
|
|
2564
|
-
db.prepare("DELETE FROM rule_promotion_events_outbound WHERE id <= ?").run(maxId);
|
|
2565
|
-
return rows.map((r) => ({
|
|
2566
|
-
prompt_hash: r.prompt_hash,
|
|
2567
|
-
event_type: r.event_type,
|
|
2568
|
-
created_at: r.created_at,
|
|
2569
|
-
metadata: safeJsonParse(r.metadata_json) ?? {}
|
|
2570
|
-
}));
|
|
2571
|
-
}
|
|
2572
2654
|
function recordTelemetry(db, eventType, data) {
|
|
2573
2655
|
try {
|
|
2574
2656
|
db.prepare(`
|
|
@@ -2578,28 +2660,55 @@ function recordTelemetry(db, eventType, data) {
|
|
|
2578
2660
|
} catch {
|
|
2579
2661
|
}
|
|
2580
2662
|
}
|
|
2581
|
-
function
|
|
2582
|
-
|
|
2583
|
-
|
|
2584
|
-
|
|
2585
|
-
|
|
2586
|
-
|
|
2587
|
-
|
|
2588
|
-
|
|
2589
|
-
|
|
2590
|
-
|
|
2591
|
-
|
|
2592
|
-
|
|
2593
|
-
|
|
2663
|
+
function rescueLearningFromStalePayloads(db, stale) {
|
|
2664
|
+
let rescued = 0;
|
|
2665
|
+
for (const item of stale) {
|
|
2666
|
+
let payload;
|
|
2667
|
+
try {
|
|
2668
|
+
payload = JSON.parse(item.payload);
|
|
2669
|
+
} catch {
|
|
2670
|
+
continue;
|
|
2671
|
+
}
|
|
2672
|
+
try {
|
|
2673
|
+
const promotions = Array.isArray(payload.rule_promotions) ? payload.rule_promotions : [];
|
|
2674
|
+
for (const p of promotions) {
|
|
2675
|
+
enqueueTeamPromotion(db, p);
|
|
2676
|
+
rescued++;
|
|
2677
|
+
}
|
|
2678
|
+
const revocations = Array.isArray(payload.rule_revocations) ? payload.rule_revocations : [];
|
|
2679
|
+
for (const r of revocations) {
|
|
2680
|
+
if (typeof r?.prompt_hash === "string") {
|
|
2681
|
+
enqueueTeamRevocation(db, r.prompt_hash);
|
|
2682
|
+
rescued++;
|
|
2683
|
+
}
|
|
2684
|
+
}
|
|
2685
|
+
const events = Array.isArray(payload.rule_promotion_events) ? payload.rule_promotion_events : [];
|
|
2686
|
+
for (const e of events) {
|
|
2687
|
+
enqueueRulePromotionEvent(db, e);
|
|
2688
|
+
}
|
|
2689
|
+
} catch (err) {
|
|
2690
|
+
process.stderr.write(
|
|
2691
|
+
`[massu] WARNING: failed to rescue learned rules from give-up payload ${item.id}: ${err instanceof Error ? err.message : String(err)}
|
|
2692
|
+
`
|
|
2693
|
+
);
|
|
2694
|
+
}
|
|
2594
2695
|
}
|
|
2696
|
+
return rescued;
|
|
2595
2697
|
}
|
|
2596
2698
|
function dequeuePendingSync(db, limit = 10) {
|
|
2597
2699
|
const stale = db.prepare(
|
|
2598
|
-
"SELECT id, retry_count, last_error FROM pending_sync WHERE retry_count >= 10 LIMIT 10000"
|
|
2700
|
+
"SELECT id, retry_count, last_error, payload FROM pending_sync WHERE retry_count >= 10 LIMIT 10000"
|
|
2599
2701
|
).all();
|
|
2600
2702
|
if (stale.length > 0) {
|
|
2703
|
+
const rescued = rescueLearningFromStalePayloads(db, stale);
|
|
2601
2704
|
const ids = stale.map((s) => s.id);
|
|
2602
2705
|
db.prepare(`DELETE FROM pending_sync WHERE id IN (${ids.map(() => "?").join(",")})`).run(...ids);
|
|
2706
|
+
if (rescued > 0) {
|
|
2707
|
+
process.stderr.write(
|
|
2708
|
+
`[massu] RESCUED ${rescued} learned rule(s) from ${stale.length} give-up payload(s) \u2014 they were re-queued for delivery, NOT discarded.
|
|
2709
|
+
`
|
|
2710
|
+
);
|
|
2711
|
+
}
|
|
2603
2712
|
const lastErrors = [...new Set(stale.map((s) => s.last_error).filter(Boolean))];
|
|
2604
2713
|
process.stderr.write(
|
|
2605
2714
|
`[massu] WARNING: ${stale.length} cloud-sync queue item(s) discarded after 10+ retries. Likely cause: invalid API key or unreachable endpoint. Recent errors: ${lastErrors.slice(0, 3).join("; ") || "(none recorded)"}
|
|
@@ -3291,19 +3400,20 @@ function scoreFailureClasses(db, matchText, filePath, promptContext, weights) {
|
|
|
3291
3400
|
}
|
|
3292
3401
|
return bestMatch;
|
|
3293
3402
|
}
|
|
3294
|
-
var MEMORY_SCHEMA_VERSION, TOOL_COST_EVENTS_RETENTION_DAYS, MAX_DRAFT_TEXT_LEN,
|
|
3403
|
+
var MEMORY_SCHEMA_VERSION, TOOL_COST_EVENTS_RETENTION_DAYS, MAX_DRAFT_TEXT_LEN, FUNNEL_EVENT_OUTBOX_CAP, _temporalColCache, CONSOLIDATION_LESSON_EVIDENCE, MEMORY_FILE_TITLE_PREFIX, MEMORY_FILE_TITLE_LIKE, USAGE_COUNTER_ARMED_KEY;
|
|
3295
3404
|
var init_memory_db = __esm({
|
|
3296
3405
|
"src/memory-db.ts"() {
|
|
3297
3406
|
"use strict";
|
|
3298
3407
|
init_config();
|
|
3299
3408
|
init_memory_vector();
|
|
3300
3409
|
init_db_backup();
|
|
3410
|
+
init_rule_delivery();
|
|
3411
|
+
init_rule_candidate_store();
|
|
3301
3412
|
init_hook_failure_signal();
|
|
3302
3413
|
init_memory_embed_sweep();
|
|
3303
3414
|
MEMORY_SCHEMA_VERSION = 1;
|
|
3304
3415
|
TOOL_COST_EVENTS_RETENTION_DAYS = 90;
|
|
3305
3416
|
MAX_DRAFT_TEXT_LEN = 16384;
|
|
3306
|
-
FUNNEL_EVENT_DRAIN_LIMIT = 5e3;
|
|
3307
3417
|
FUNNEL_EVENT_OUTBOX_CAP = 2e4;
|
|
3308
3418
|
_temporalColCache = /* @__PURE__ */ new WeakMap();
|
|
3309
3419
|
CONSOLIDATION_LESSON_EVIDENCE = "consolidation:session-summary";
|
|
@@ -3316,8 +3426,8 @@ var init_memory_db = __esm({
|
|
|
3316
3426
|
// src/hooks/user-prompt.ts
|
|
3317
3427
|
init_memory_db();
|
|
3318
3428
|
init_config();
|
|
3319
|
-
import { existsSync as
|
|
3320
|
-
import { join as
|
|
3429
|
+
import { existsSync as existsSync8, mkdirSync as mkdirSync6, writeFileSync as writeFileSync3, readFileSync as readFileSync5, readdirSync as readdirSync3, openSync, fstatSync, readSync, closeSync } from "fs";
|
|
3430
|
+
import { join as join5 } from "path";
|
|
3321
3431
|
|
|
3322
3432
|
// src/rule-candidate-detector.ts
|
|
3323
3433
|
var CORRECTION_DISMISSAL_PATTERN = /\b(nevermind|never\s+mind|forget\s+it|ignore\s+(that|it)|actually\s+you('?re|\s+were)\s+right|on\s+second\s+thought|no\s+wait|scratch\s+that|disregard|abandon\s+(that|it))\b/i;
|
|
@@ -3624,6 +3734,7 @@ function entitledForTeamSharedPromotion(tier) {
|
|
|
3624
3734
|
}
|
|
3625
3735
|
|
|
3626
3736
|
// src/hooks/user-prompt.ts
|
|
3737
|
+
init_rule_candidate_store();
|
|
3627
3738
|
init_hook_failure_signal();
|
|
3628
3739
|
async function main() {
|
|
3629
3740
|
try {
|
|
@@ -3656,7 +3767,7 @@ async function main() {
|
|
|
3656
3767
|
const fileRefs = extractFileReferences(prompt);
|
|
3657
3768
|
if (fileRefs.length > 0) {
|
|
3658
3769
|
const knowledgeDbPath = getResolvedPaths().knowledgeDbPath;
|
|
3659
|
-
if (knowledgeDbPath &&
|
|
3770
|
+
if (knowledgeDbPath && existsSync8(knowledgeDbPath)) {
|
|
3660
3771
|
const BetterSqlite3Ctor = (await import("better-sqlite3")).default;
|
|
3661
3772
|
const kdb = new BetterSqlite3Ctor(knowledgeDbPath, { readonly: true });
|
|
3662
3773
|
try {
|
|
@@ -3710,15 +3821,15 @@ async function main() {
|
|
|
3710
3821
|
category: categorizePrompt(prompt),
|
|
3711
3822
|
blacklist
|
|
3712
3823
|
});
|
|
3713
|
-
const candidateDir =
|
|
3824
|
+
const candidateDir = join5(hookInput.cwd, ".massu", "rule-candidates");
|
|
3714
3825
|
if (scoreResult.emitCandidate) {
|
|
3715
3826
|
const cachedTier = getCachedTierReadOnly(db);
|
|
3716
3827
|
if (entitledForAutoLearning(cachedTier)) {
|
|
3717
|
-
|
|
3828
|
+
mkdirSync6(candidateDir, { recursive: true });
|
|
3718
3829
|
const promptHash = hashPrompt(prompt);
|
|
3719
|
-
const candidatePath =
|
|
3720
|
-
if (!
|
|
3721
|
-
|
|
3830
|
+
const candidatePath = join5(candidateDir, `${promptHash}.json`);
|
|
3831
|
+
if (!existsSync8(candidatePath)) {
|
|
3832
|
+
upsertCandidate(db, hookInput.cwd, {
|
|
3722
3833
|
prompt,
|
|
3723
3834
|
prompt_hash: promptHash,
|
|
3724
3835
|
score: scoreResult.score,
|
|
@@ -3726,7 +3837,7 @@ async function main() {
|
|
|
3726
3837
|
prior_turn_files: priorTurn.files,
|
|
3727
3838
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3728
3839
|
session_id
|
|
3729
|
-
},
|
|
3840
|
+
}, { origin: "local", score: scoreResult.score });
|
|
3730
3841
|
if (entitledForTeamSharedPromotion(cachedTier)) {
|
|
3731
3842
|
enqueueRulePromotionEvent(db, {
|
|
3732
3843
|
prompt_hash: promptHash,
|
|
@@ -3741,28 +3852,28 @@ async function main() {
|
|
|
3741
3852
|
}
|
|
3742
3853
|
}
|
|
3743
3854
|
} else {
|
|
3744
|
-
const nudgePath =
|
|
3745
|
-
if (!
|
|
3746
|
-
|
|
3855
|
+
const nudgePath = join5(candidateDir, ".last-tier-nudge");
|
|
3856
|
+
if (!existsSync8(nudgePath)) {
|
|
3857
|
+
mkdirSync6(candidateDir, { recursive: true });
|
|
3747
3858
|
process.stderr.write(
|
|
3748
3859
|
`
|
|
3749
3860
|
[RULE CANDIDATE] ${autoLearningUpgradeMessage(cachedTier)}
|
|
3750
3861
|
|
|
3751
3862
|
`
|
|
3752
3863
|
);
|
|
3753
|
-
|
|
3864
|
+
writeFileSync3(nudgePath, (/* @__PURE__ */ new Date()).toISOString());
|
|
3754
3865
|
}
|
|
3755
3866
|
}
|
|
3756
3867
|
}
|
|
3757
|
-
if (
|
|
3758
|
-
const candidates =
|
|
3868
|
+
if (existsSync8(candidateDir)) {
|
|
3869
|
+
const candidates = readdirSync3(candidateDir).filter(
|
|
3759
3870
|
(f) => f.endsWith(".json") && !f.startsWith(".")
|
|
3760
3871
|
);
|
|
3761
3872
|
const candidateCount = candidates.length;
|
|
3762
|
-
const surfacedPath =
|
|
3873
|
+
const surfacedPath = join5(candidateDir, ".last-surfaced");
|
|
3763
3874
|
let lastSurfaced = 0;
|
|
3764
|
-
if (
|
|
3765
|
-
const raw =
|
|
3875
|
+
if (existsSync8(surfacedPath)) {
|
|
3876
|
+
const raw = readFileSync5(surfacedPath, "utf-8").trim();
|
|
3766
3877
|
const parsed = parseInt(raw, 10);
|
|
3767
3878
|
if (!Number.isNaN(parsed)) lastSurfaced = parsed;
|
|
3768
3879
|
}
|
|
@@ -3773,17 +3884,17 @@ async function main() {
|
|
|
3773
3884
|
|
|
3774
3885
|
`
|
|
3775
3886
|
);
|
|
3776
|
-
|
|
3887
|
+
writeFileSync3(surfacedPath, String(candidateCount));
|
|
3777
3888
|
}
|
|
3778
3889
|
}
|
|
3779
3890
|
} catch (candidateErr) {
|
|
3780
3891
|
try {
|
|
3781
|
-
const dir =
|
|
3782
|
-
if (!
|
|
3783
|
-
const logPath =
|
|
3784
|
-
const pre =
|
|
3892
|
+
const dir = join5(hookInput.cwd, ".massu", "rule-candidates");
|
|
3893
|
+
if (!existsSync8(dir)) mkdirSync6(dir, { recursive: true });
|
|
3894
|
+
const logPath = join5(dir, ".detector-failures.jsonl");
|
|
3895
|
+
const pre = existsSync8(logPath) ? readFileSync5(logPath, "utf-8") : "";
|
|
3785
3896
|
const sep = pre && !pre.endsWith("\n") ? "\n" : "";
|
|
3786
|
-
|
|
3897
|
+
writeFileSync3(logPath, pre + sep + JSON.stringify({
|
|
3787
3898
|
session_id,
|
|
3788
3899
|
// Security review (plan-2026-05-27): log the prompt HASH, never a
|
|
3789
3900
|
// raw prompt excerpt — the failure log must not persist PII/secrets.
|
|
@@ -3804,7 +3915,7 @@ async function main() {
|
|
|
3804
3915
|
}
|
|
3805
3916
|
function detectPriorAssistantTurn(transcriptPath) {
|
|
3806
3917
|
try {
|
|
3807
|
-
if (!transcriptPath || !
|
|
3918
|
+
if (!transcriptPath || !existsSync8(transcriptPath)) {
|
|
3808
3919
|
return { hadEditOrWrite: false, files: [] };
|
|
3809
3920
|
}
|
|
3810
3921
|
if (!transcriptPath.includes("/.claude/projects/")) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@massu/core",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.16.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "AI Engineering Governance MCP Server - Session memory, knowledge system, feature registry, code intelligence, rule enforcement, tiered tooling (12 free / 73 total), 56 workflow commands, 11 agents, 20+ patterns",
|
|
6
6
|
"main": "./dist/cli.js",
|