@massu/core 1.15.7 → 1.16.1

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.
@@ -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 mkdirSync3, existsSync as existsSync4 } from "fs";
955
- import { join as join2, dirname as dirname3 } from "path";
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 (existsSync4(join2(dir, ".massu")) || existsSync4(join2(dir, "massu.config.yaml"))) {
962
- return join2(dir, ".massu", "hook-failures.jsonl");
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 join2(process.cwd(), ".massu", "hook-failures.jsonl");
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
- mkdirSync3(dirname3(path), { recursive: true });
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 readFileSync3 } from "fs";
1150
+ import { readFileSync as readFileSync4 } from "fs";
1026
1151
  function loadVocab(vocabPath) {
1027
- const lines = readFileSync3(vocabPath, "utf-8").split("\n");
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 join3 } from "path";
1148
- import { existsSync as existsSync5 } from "fs";
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 (existsSync5(join3(dir, "package.json"))) {
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 = join3(root, rel);
1184
- if (existsSync5(join3(candidate, "model_quantized.onnx"))) return candidate;
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 = join3(modelDir, "model_quantized.onnx");
1216
- const vocabPath = join3(modelDir, "vocab.txt");
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 existsSync6, mkdirSync as mkdirSync4 } from "fs";
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 (!existsSync6(dir)) {
1576
- mkdirSync4(dir, { recursive: true });
1697
+ if (!existsSync7(dir)) {
1698
+ mkdirSync5(dir, { recursive: true });
1577
1699
  }
1578
- const preExisting = existsSync6(dbPath);
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.prepare(`
2549
- DELETE FROM rule_promotion_events_outbound
2550
- WHERE id NOT IN (
2551
- SELECT id FROM rule_promotion_events_outbound ORDER BY id DESC LIMIT ?
2552
- )
2553
- `).run(FUNNEL_EVENT_OUTBOX_CAP);
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 safeJsonArray(json) {
2582
- try {
2583
- const v = JSON.parse(json);
2584
- return Array.isArray(v) ? v : [];
2585
- } catch {
2586
- return [];
2587
- }
2588
- }
2589
- function safeJsonParse(json) {
2590
- try {
2591
- return JSON.parse(json);
2592
- } catch {
2593
- return void 0;
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, FUNNEL_EVENT_DRAIN_LIMIT, FUNNEL_EVENT_OUTBOX_CAP, _temporalColCache, CONSOLIDATION_LESSON_EVIDENCE, MEMORY_FILE_TITLE_PREFIX, MEMORY_FILE_TITLE_LIKE, USAGE_COUNTER_ARMED_KEY;
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 existsSync7, mkdirSync as mkdirSync5, writeFileSync as writeFileSync2, readFileSync as readFileSync4, readdirSync as readdirSync2, openSync, fstatSync, readSync, closeSync } from "fs";
3320
- import { join as join4 } from "path";
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;
@@ -3505,10 +3615,11 @@ function verifyEd25519SignedEnvelope(key, payload) {
3505
3615
  }
3506
3616
 
3507
3617
  // src/security/license-pubkey.generated.ts
3508
- var LICENSE_PUBKEY_ED25519 = new Uint8Array([39, 136, 108, 146, 85, 233, 119, 252, 223, 226, 123, 155, 234, 168, 200, 150, 36, 249, 174, 6, 130, 146, 125, 196, 136, 224, 202, 150, 53, 228, 114, 15]);
3509
- var LICENSE_PUBKEY_FINGERPRINT_HEX = "18a63d64fdec9e5a368fc45feaa49bed6ced815967e582bc7b8af534f22a9475";
3618
+ var LICENSE_PUBKEY_ED25519 = new Uint8Array([254, 188, 221, 150, 111, 252, 154, 100, 253, 247, 113, 192, 144, 224, 127, 160, 202, 111, 75, 40, 35, 169, 108, 89, 252, 250, 255, 233, 250, 120, 47, 160]);
3619
+ var LICENSE_PUBKEY_FINGERPRINT_HEX = "18c0456789a70eeee28012719f04725f43f59e693f735227ff73f0475f2290e3";
3510
3620
  var KNOWN_LICENSE_PUBKEY_FINGERPRINTS = /* @__PURE__ */ new Set([
3511
- "18a63d64fdec9e5a368fc45feaa49bed6ced815967e582bc7b8af534f22a9475"
3621
+ "18a63d64fdec9e5a368fc45feaa49bed6ced815967e582bc7b8af534f22a9475",
3622
+ "18c0456789a70eeee28012719f04725f43f59e693f735227ff73f0475f2290e3"
3512
3623
  ]);
3513
3624
 
3514
3625
  // src/security/license-response-verifier.ts
@@ -3624,6 +3735,7 @@ function entitledForTeamSharedPromotion(tier) {
3624
3735
  }
3625
3736
 
3626
3737
  // src/hooks/user-prompt.ts
3738
+ init_rule_candidate_store();
3627
3739
  init_hook_failure_signal();
3628
3740
  async function main() {
3629
3741
  try {
@@ -3656,7 +3768,7 @@ async function main() {
3656
3768
  const fileRefs = extractFileReferences(prompt);
3657
3769
  if (fileRefs.length > 0) {
3658
3770
  const knowledgeDbPath = getResolvedPaths().knowledgeDbPath;
3659
- if (knowledgeDbPath && existsSync7(knowledgeDbPath)) {
3771
+ if (knowledgeDbPath && existsSync8(knowledgeDbPath)) {
3660
3772
  const BetterSqlite3Ctor = (await import("better-sqlite3")).default;
3661
3773
  const kdb = new BetterSqlite3Ctor(knowledgeDbPath, { readonly: true });
3662
3774
  try {
@@ -3710,15 +3822,15 @@ async function main() {
3710
3822
  category: categorizePrompt(prompt),
3711
3823
  blacklist
3712
3824
  });
3713
- const candidateDir = join4(hookInput.cwd, ".massu", "rule-candidates");
3825
+ const candidateDir = join5(hookInput.cwd, ".massu", "rule-candidates");
3714
3826
  if (scoreResult.emitCandidate) {
3715
3827
  const cachedTier = getCachedTierReadOnly(db);
3716
3828
  if (entitledForAutoLearning(cachedTier)) {
3717
- mkdirSync5(candidateDir, { recursive: true });
3829
+ mkdirSync6(candidateDir, { recursive: true });
3718
3830
  const promptHash = hashPrompt(prompt);
3719
- const candidatePath = join4(candidateDir, `${promptHash}.json`);
3720
- if (!existsSync7(candidatePath)) {
3721
- writeFileSync2(candidatePath, JSON.stringify({
3831
+ const candidatePath = join5(candidateDir, `${promptHash}.json`);
3832
+ if (!existsSync8(candidatePath)) {
3833
+ upsertCandidate(db, hookInput.cwd, {
3722
3834
  prompt,
3723
3835
  prompt_hash: promptHash,
3724
3836
  score: scoreResult.score,
@@ -3726,7 +3838,7 @@ async function main() {
3726
3838
  prior_turn_files: priorTurn.files,
3727
3839
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
3728
3840
  session_id
3729
- }, null, 2));
3841
+ }, { origin: "local", score: scoreResult.score });
3730
3842
  if (entitledForTeamSharedPromotion(cachedTier)) {
3731
3843
  enqueueRulePromotionEvent(db, {
3732
3844
  prompt_hash: promptHash,
@@ -3741,28 +3853,28 @@ async function main() {
3741
3853
  }
3742
3854
  }
3743
3855
  } else {
3744
- const nudgePath = join4(candidateDir, ".last-tier-nudge");
3745
- if (!existsSync7(nudgePath)) {
3746
- mkdirSync5(candidateDir, { recursive: true });
3856
+ const nudgePath = join5(candidateDir, ".last-tier-nudge");
3857
+ if (!existsSync8(nudgePath)) {
3858
+ mkdirSync6(candidateDir, { recursive: true });
3747
3859
  process.stderr.write(
3748
3860
  `
3749
3861
  [RULE CANDIDATE] ${autoLearningUpgradeMessage(cachedTier)}
3750
3862
 
3751
3863
  `
3752
3864
  );
3753
- writeFileSync2(nudgePath, (/* @__PURE__ */ new Date()).toISOString());
3865
+ writeFileSync3(nudgePath, (/* @__PURE__ */ new Date()).toISOString());
3754
3866
  }
3755
3867
  }
3756
3868
  }
3757
- if (existsSync7(candidateDir)) {
3758
- const candidates = readdirSync2(candidateDir).filter(
3869
+ if (existsSync8(candidateDir)) {
3870
+ const candidates = readdirSync3(candidateDir).filter(
3759
3871
  (f) => f.endsWith(".json") && !f.startsWith(".")
3760
3872
  );
3761
3873
  const candidateCount = candidates.length;
3762
- const surfacedPath = join4(candidateDir, ".last-surfaced");
3874
+ const surfacedPath = join5(candidateDir, ".last-surfaced");
3763
3875
  let lastSurfaced = 0;
3764
- if (existsSync7(surfacedPath)) {
3765
- const raw = readFileSync4(surfacedPath, "utf-8").trim();
3876
+ if (existsSync8(surfacedPath)) {
3877
+ const raw = readFileSync5(surfacedPath, "utf-8").trim();
3766
3878
  const parsed = parseInt(raw, 10);
3767
3879
  if (!Number.isNaN(parsed)) lastSurfaced = parsed;
3768
3880
  }
@@ -3773,17 +3885,17 @@ async function main() {
3773
3885
 
3774
3886
  `
3775
3887
  );
3776
- writeFileSync2(surfacedPath, String(candidateCount));
3888
+ writeFileSync3(surfacedPath, String(candidateCount));
3777
3889
  }
3778
3890
  }
3779
3891
  } catch (candidateErr) {
3780
3892
  try {
3781
- const dir = join4(hookInput.cwd, ".massu", "rule-candidates");
3782
- if (!existsSync7(dir)) mkdirSync5(dir, { recursive: true });
3783
- const logPath = join4(dir, ".detector-failures.jsonl");
3784
- const pre = existsSync7(logPath) ? readFileSync4(logPath, "utf-8") : "";
3893
+ const dir = join5(hookInput.cwd, ".massu", "rule-candidates");
3894
+ if (!existsSync8(dir)) mkdirSync6(dir, { recursive: true });
3895
+ const logPath = join5(dir, ".detector-failures.jsonl");
3896
+ const pre = existsSync8(logPath) ? readFileSync5(logPath, "utf-8") : "";
3785
3897
  const sep = pre && !pre.endsWith("\n") ? "\n" : "";
3786
- writeFileSync2(logPath, pre + sep + JSON.stringify({
3898
+ writeFileSync3(logPath, pre + sep + JSON.stringify({
3787
3899
  session_id,
3788
3900
  // Security review (plan-2026-05-27): log the prompt HASH, never a
3789
3901
  // raw prompt excerpt — the failure log must not persist PII/secrets.
@@ -3804,7 +3916,7 @@ async function main() {
3804
3916
  }
3805
3917
  function detectPriorAssistantTurn(transcriptPath) {
3806
3918
  try {
3807
- if (!transcriptPath || !existsSync7(transcriptPath)) {
3919
+ if (!transcriptPath || !existsSync8(transcriptPath)) {
3808
3920
  return { hadEditOrWrite: false, files: [] };
3809
3921
  }
3810
3922
  if (!transcriptPath.includes("/.claude/projects/")) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@massu/core",
3
- "version": "1.15.7",
3
+ "version": "1.16.1",
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",