@massu/core 1.15.6 → 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.
Files changed (105) hide show
  1. package/CHANGELOG.md +103 -0
  2. package/agents/massu-architecture-reviewer.md +20 -0
  3. package/agents/massu-blast-radius-analyzer.md +20 -0
  4. package/agents/massu-competitive-scorer.md +20 -0
  5. package/agents/massu-help-sync.md +20 -0
  6. package/agents/massu-migration-writer.md +20 -0
  7. package/agents/massu-output-scorer.md +20 -0
  8. package/agents/massu-pattern-reviewer.md +20 -0
  9. package/agents/massu-plan-auditor.md +20 -0
  10. package/agents/massu-schema-sync-verifier.md +20 -0
  11. package/agents/massu-security-reviewer.md +20 -0
  12. package/agents/massu-ux-reviewer.md +20 -0
  13. package/commands/_shared-preamble.md +21 -0
  14. package/commands/_shared-references/auto-learning-protocol.md +2 -0
  15. package/commands/_shared-references/blast-radius-protocol.md +2 -0
  16. package/commands/_shared-references/security-pre-screen.md +2 -0
  17. package/commands/_shared-references/test-first-protocol.md +2 -0
  18. package/commands/_shared-references/verification-table.md +2 -0
  19. package/commands/_verification-laws.md +131 -0
  20. package/commands/_verification-laws.md.sha256 +1 -0
  21. package/commands/massu-data/references/common-queries.md +2 -0
  22. package/commands/massu-data/references/table-guide.md +2 -0
  23. package/commands/massu-data.md +2 -0
  24. package/commands/massu-debug/references/auto-learning.md +2 -0
  25. package/commands/massu-debug/references/codegraph-tracing.md +2 -0
  26. package/commands/massu-debug/references/common-shortcuts.md +2 -0
  27. package/commands/massu-debug/references/investigation-phases.md +2 -0
  28. package/commands/massu-debug/references/report-format.md +2 -0
  29. package/commands/massu-deploy.python-docker.md +2 -0
  30. package/commands/massu-deploy.python-fly.md +2 -0
  31. package/commands/massu-deploy.python-launchd.md +2 -0
  32. package/commands/massu-deploy.python-systemd.md +2 -0
  33. package/commands/massu-full-audit.md +2 -0
  34. package/commands/massu-golden-path/references/approval-points.md +2 -0
  35. package/commands/massu-golden-path/references/phase-0-requirements.md +2 -0
  36. package/commands/massu-golden-path/references/phase-1-plan-creation.md +2 -0
  37. package/commands/massu-golden-path/references/phase-2-implementation.md +2 -0
  38. package/commands/massu-golden-path/references/phase-2.5-gap-analyzer.md +2 -0
  39. package/commands/massu-golden-path/references/phase-3-simplify.md +2 -0
  40. package/commands/massu-golden-path/references/phase-3.5-security-audit.md +2 -0
  41. package/commands/massu-golden-path/references/phase-4-commit.md +2 -0
  42. package/commands/massu-golden-path/references/phase-5-push.md +2 -0
  43. package/commands/massu-golden-path/references/phase-5.5-production-verify.md +2 -0
  44. package/commands/massu-golden-path/references/phase-6-completion.md +2 -0
  45. package/commands/massu-golden-path/references/qa-evaluator-spec.md +2 -0
  46. package/commands/massu-golden-path/references/sprint-contract-protocol.md +2 -0
  47. package/commands/massu-golden-path/references/vr-visual-calibration.md +2 -0
  48. package/commands/massu-guide.md +2 -0
  49. package/commands/massu-hooks.md +2 -0
  50. package/commands/massu-learning-audit.md +2 -0
  51. package/commands/massu-loop/references/auto-learning.md +2 -0
  52. package/commands/massu-loop/references/checkpoint-audit.md +2 -0
  53. package/commands/massu-loop/references/guardrails.md +2 -0
  54. package/commands/massu-loop/references/iteration-structure.md +2 -0
  55. package/commands/massu-loop/references/loop-controller.md +2 -0
  56. package/commands/massu-loop/references/plan-extraction.md +2 -0
  57. package/commands/massu-loop/references/vr-plan-spec.md +2 -0
  58. package/commands/massu-parity.md +2 -0
  59. package/commands/massu-push.md +2 -0
  60. package/commands/massu-scaffold-hook.md +2 -0
  61. package/commands/massu-scaffold-page.md +2 -0
  62. package/commands/massu-scaffold-page.swift.md +2 -0
  63. package/commands/massu-scaffold-router.md +2 -0
  64. package/commands/massu-scaffold-router.python-django.md +2 -0
  65. package/commands/massu-scaffold-router.python-fastapi.md +2 -0
  66. package/commands/massu-scaffold-router.python.md +2 -0
  67. package/dist/cli.js +2012 -787
  68. package/dist/hooks/auto-learning-pipeline.js +152 -67
  69. package/dist/hooks/classify-failure.js +152 -67
  70. package/dist/hooks/cost-tracker.js +152 -67
  71. package/dist/hooks/fix-detector.js +152 -67
  72. package/dist/hooks/incident-pipeline.js +152 -67
  73. package/dist/hooks/intent-suggester.js +152 -67
  74. package/dist/hooks/memory-recall.js +152 -67
  75. package/dist/hooks/post-edit-context.js +152 -67
  76. package/dist/hooks/post-tool-use.js +152 -67
  77. package/dist/hooks/pre-compact.js +152 -67
  78. package/dist/hooks/pre-delete-check.js +154 -69
  79. package/dist/hooks/pre-tool-use-gate.js +154 -69
  80. package/dist/hooks/quality-event.js +152 -67
  81. package/dist/hooks/rule-enforcement-pipeline.js +152 -67
  82. package/dist/hooks/security-gate.js +152 -67
  83. package/dist/hooks/session-end.js +346 -80
  84. package/dist/hooks/session-start.js +155 -67
  85. package/dist/hooks/user-prompt.js +222 -111
  86. package/package.json +4 -3
  87. package/patterns/build-patterns.md +2 -0
  88. package/patterns/component-patterns.md +2 -0
  89. package/patterns/display-patterns.md +2 -0
  90. package/patterns/form-patterns.md +2 -0
  91. package/patterns/integration-testing-checklist.md +2 -0
  92. package/patterns/security-patterns.md +2 -0
  93. package/patterns/testing-patterns.md +2 -0
  94. package/patterns/tool-routing.md +2 -0
  95. package/patterns/ui-patterns.md +2 -0
  96. package/protocols/plan-implementation.md +2 -0
  97. package/protocols/recovery.md +2 -0
  98. package/protocols/verification.md +2 -0
  99. package/reference/cr-rules-reference.md +2 -0
  100. package/reference/hook-execution-order.md +2 -0
  101. package/reference/lessons-learned.md +2 -0
  102. package/reference/patterns-quickref.md +2 -0
  103. package/reference/standards.md +2 -0
  104. package/reference/subagents-reference.md +2 -0
  105. package/reference/vr-verification-reference.md +2 -0
@@ -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;
@@ -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 && existsSync7(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 = join4(hookInput.cwd, ".massu", "rule-candidates");
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
- mkdirSync5(candidateDir, { recursive: true });
3828
+ mkdirSync6(candidateDir, { recursive: true });
3718
3829
  const promptHash = hashPrompt(prompt);
3719
- const candidatePath = join4(candidateDir, `${promptHash}.json`);
3720
- if (!existsSync7(candidatePath)) {
3721
- writeFileSync2(candidatePath, JSON.stringify({
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
- }, null, 2));
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 = join4(candidateDir, ".last-tier-nudge");
3745
- if (!existsSync7(nudgePath)) {
3746
- mkdirSync5(candidateDir, { recursive: true });
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
- writeFileSync2(nudgePath, (/* @__PURE__ */ new Date()).toISOString());
3864
+ writeFileSync3(nudgePath, (/* @__PURE__ */ new Date()).toISOString());
3754
3865
  }
3755
3866
  }
3756
3867
  }
3757
- if (existsSync7(candidateDir)) {
3758
- const candidates = readdirSync2(candidateDir).filter(
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 = join4(candidateDir, ".last-surfaced");
3873
+ const surfacedPath = join5(candidateDir, ".last-surfaced");
3763
3874
  let lastSurfaced = 0;
3764
- if (existsSync7(surfacedPath)) {
3765
- const raw = readFileSync4(surfacedPath, "utf-8").trim();
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
- writeFileSync2(surfacedPath, String(candidateCount));
3887
+ writeFileSync3(surfacedPath, String(candidateCount));
3777
3888
  }
3778
3889
  }
3779
3890
  } catch (candidateErr) {
3780
3891
  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") : "";
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
- writeFileSync2(logPath, pre + sep + JSON.stringify({
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 || !existsSync7(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.15.6",
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",
@@ -24,14 +24,15 @@
24
24
  "start": "npx tsx src/server.ts",
25
25
  "test": "vitest run",
26
26
  "build": "tsc --noEmit && npm run build:cli && npm run build:hooks && npm run build:assets && npm run build:adapter-types && npm run build:adapter-subpath && npm run build:bundle-adapters",
27
- "build:assets": "node scripts/copy-embedder-assets.mjs",
27
+ "build:assets": "node scripts/gen-owned-sidecars.mjs && node scripts/copy-embedder-assets.mjs",
28
28
  "build:adapter-types": "tsc -p tsconfig.adapter-types.json",
29
29
  "build:adapter-subpath": "tsx scripts/bundle-adapters.ts --subpath-only",
30
30
  "build:bundle-adapters": "tsx scripts/bundle-adapters.ts",
31
31
  "build:cli": "esbuild --bundle --platform=node --format=esm --outfile=dist/cli.js src/cli.ts --external:better-sqlite3 --external:yaml --external:zod --external:chokidar --external:proper-lockfile --external:fsevents --external:web-tree-sitter --external:tweetnacl --external:tar --external:smol-toml --external:vscode-languageserver-protocol --external:onnxruntime-web --external:@massu/adapter-rails --external:@massu/adapter-spring --banner:js='#!/usr/bin/env node\nimport{createRequire as __cr}from\"module\";const require=__cr(import.meta.url);'",
32
32
  "build:hooks": "esbuild --bundle --platform=node --format=esm --outdir=dist/hooks src/hooks/*.ts --external:better-sqlite3 --external:yaml --external:zod --external:chokidar --external:proper-lockfile --external:fsevents --external:web-tree-sitter --external:tweetnacl --external:tar --external:smol-toml --external:vscode-languageserver-protocol --external:onnxruntime-web --external:@massu/adapter-rails --external:@massu/adapter-spring --banner:js='import{createRequire as __cr}from\"module\";const require=__cr(import.meta.url);'",
33
33
  "prepublishOnly": "bash ../../scripts/prepublish-check.sh && node ../../scripts/bundle-pubkey.mjs && node ../../scripts/bundle-license-pubkey.mjs && node ../../scripts/bundle-promotion-pubkey.mjs && node ../../scripts/bundle-audit-export-pubkey.mjs && cp ../../CHANGELOG.md ./CHANGELOG.md && npm run build && bash ../../scripts/npm-publish-guard.sh",
34
- "bench:watch": "tsx test/perf/watch-benchmark.ts"
34
+ "bench:watch": "tsx test/perf/watch-benchmark.ts",
35
+ "build:owned-sidecars": "node scripts/gen-owned-sidecars.mjs"
35
36
  },
36
37
  "dependencies": {
37
38
  "@clack/prompts": "^0.9.1",
@@ -1,5 +1,7 @@
1
1
  # Build Patterns
2
2
 
3
+ > **Verification laws apply.** Read `.claude/commands/_verification-laws.md` — CR-64 (a gate must prove it can fail), CR-65 (broken and empty may never render identically), and: an audit that does not run commands is not an audit.
4
+
3
5
  **Purpose**: Patterns for resolving build issues in Next.js applications with Prisma, native modules, and Edge Runtime.
4
6
 
5
7
  **When to Read**: When encountering build errors, hangs, or warnings. Before adding heavy dependencies.
@@ -1,5 +1,7 @@
1
1
  # Component Patterns
2
2
 
3
+ > **Verification laws apply.** Read `.claude/commands/_verification-laws.md` — CR-64 (a gate must prove it can fail), CR-65 (broken and empty may never render identically), and: an audit that does not run commands is not an audit.
4
+
3
5
  **Purpose**: Standard patterns for UI components, forms, search inputs, and common interactive elements.
4
6
 
5
7
  **When to Read**: Before creating or modifying UI components.
@@ -1,5 +1,7 @@
1
1
  # Display Patterns
2
2
 
3
+ > **Verification laws apply.** Read `.claude/commands/_verification-laws.md` — CR-64 (a gate must prove it can fail), CR-65 (broken and empty may never render identically), and: an audit that does not run commands is not an audit.
4
+
3
5
  **Purpose**: Centralized formatting functions and display conventions for data presentation.
4
6
 
5
7
  **When to Read**: Before displaying formatted data (currency, phone numbers, dates, names, file sizes).
@@ -1,5 +1,7 @@
1
1
  # Form Field Patterns
2
2
 
3
+ > **Verification laws apply.** Read `.claude/commands/_verification-laws.md` — CR-64 (a gate must prove it can fail), CR-65 (broken and empty may never render identically), and: an audit that does not run commands is not an audit.
4
+
3
5
  **Purpose**: Form patterns for semantic TextField types, PhoneInputField, AddressForm, react-hook-form + Zod setup, register() vs Controller, edit forms, submission patterns.
4
6
 
5
7
  **When to Read**: Before creating or modifying forms.
@@ -1,5 +1,7 @@
1
1
  # Integration Testing Checklist
2
2
 
3
+ > **Verification laws apply.** Read `.claude/commands/_verification-laws.md` — CR-64 (a gate must prove it can fail), CR-65 (broken and empty may never render identically), and: an audit that does not run commands is not an audit.
4
+
3
5
  **Purpose**: Router contract verification, pre-test writing steps, test templates, common anti-patterns, prevention system.
4
6
 
5
7
  **When to Read**: Before writing integration tests.
@@ -1,5 +1,7 @@
1
1
  # Security Patterns
2
2
 
3
+ > **Verification laws apply.** Read `.claude/commands/_verification-laws.md` — CR-64 (a gate must prove it can fail), CR-65 (broken and empty may never render identically), and: an audit that does not run commands is not an audit.
4
+
3
5
  **Purpose**: Security patterns for credential management, authentication, and code safety.
4
6
 
5
7
  **When to Read**: Before handling secrets, authentication, or security-sensitive code.
@@ -1,5 +1,7 @@
1
1
  # Testing Patterns
2
2
 
3
+ > **Verification laws apply.** Read `.claude/commands/_verification-laws.md` — CR-64 (a gate must prove it can fail), CR-65 (broken and empty may never render identically), and: an audit that does not run commands is not an audit.
4
+
3
5
  **Purpose**: Testing pyramid, patterns, and best practices for unit, integration, and E2E tests.
4
6
 
5
7
  **When to Read**: Before writing tests, setting up test infrastructure, or debugging test failures.
@@ -1,5 +1,7 @@
1
1
  # Tool Routing Patterns
2
2
 
3
+ > **Verification laws apply.** Read `.claude/commands/_verification-laws.md` — CR-64 (a gate must prove it can fail), CR-65 (broken and empty may never render identically), and: an audit that does not run commands is not an audit.
4
+
3
5
  **Purpose**: Decision trees for selecting the correct tool/MCP for browser automation, Google services, and code search.
4
6
 
5
7
  **When to Read**: Before using any browser automation, Google service, or code search tool.
@@ -1,5 +1,7 @@
1
1
  # UI Patterns
2
2
 
3
+ > **Verification laws apply.** Read `.claude/commands/_verification-laws.md` — CR-64 (a gate must prove it can fail), CR-65 (broken and empty may never render identically), and: an audit that does not run commands is not an audit.
4
+
3
5
  **Purpose**: Layout, theming, component selection, and UI quality standards.
4
6
 
5
7
  **When to Read**: Before creating or modifying UI components, pages, or styles.