@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
@@ -971,6 +971,253 @@ var init_db_backup = __esm({
971
971
  }
972
972
  });
973
973
 
974
+ // src/rule-delivery.ts
975
+ import { randomUUID } from "node:crypto";
976
+ function tableExists(db, table) {
977
+ const row = db.prepare("SELECT 1 AS ok FROM sqlite_master WHERE type='table' AND name=?").get(table);
978
+ return row !== void 0;
979
+ }
980
+ function claimPage(db, table, pk, orderBy, limit, token, nowMs) {
981
+ if (!tableExists(db, table)) return;
982
+ const cutoff = nowMs - LEASE_TTL_MS;
983
+ const ids = db.prepare(
984
+ `SELECT ${pk} AS k FROM ${table}
985
+ WHERE lease_token IS NULL OR leased_at_ms IS NULL OR leased_at_ms < ?
986
+ ORDER BY ${orderBy} ASC LIMIT ?`
987
+ ).all(cutoff, limit);
988
+ if (ids.length === 0) return;
989
+ const placeholders = ids.map(() => "?").join(",");
990
+ db.prepare(
991
+ `UPDATE ${table} SET lease_token = ?, leased_at_ms = ? WHERE ${pk} IN (${placeholders})`
992
+ ).run(token, nowMs, ...ids.map((r) => r.k));
993
+ }
994
+ function columns(db, table) {
995
+ const rows = db.prepare(`PRAGMA table_info(${table})`).all();
996
+ return new Set(rows.map((r) => r.name));
997
+ }
998
+ function migrateRuleDelivery(db) {
999
+ const ADDITIONS = [
1000
+ { name: "attempts", decl: "INTEGER NOT NULL DEFAULT 0" },
1001
+ { name: "lease_token", decl: "TEXT" },
1002
+ { name: "leased_at_ms", decl: "INTEGER" },
1003
+ { name: "last_error", decl: "TEXT" }
1004
+ ];
1005
+ for (const table of ALL_OUTBOUND_TABLES) {
1006
+ if (!tableExists(db, table)) continue;
1007
+ const cols = columns(db, table);
1008
+ for (const add of ADDITIONS) {
1009
+ if (cols.has(add.name)) continue;
1010
+ db.exec(`ALTER TABLE ${table} ADD COLUMN ${add.name} ${add.decl}`);
1011
+ }
1012
+ }
1013
+ }
1014
+ function leaseLearning(db, nowMs = Date.now()) {
1015
+ migrateRuleDelivery(db);
1016
+ const token = randomUUID();
1017
+ claimPage(db, "team_promotion_outbound", "prompt_hash", "created_at", PROMOTION_LEASE_LIMIT, token, nowMs);
1018
+ claimPage(db, "team_revocation_outbound", "prompt_hash", "created_at", PROMOTION_LEASE_LIMIT, token, nowMs);
1019
+ claimPage(db, TELEMETRY_OUTBOUND_TABLE, "id", "id", TELEMETRY_LEASE_LIMIT, token, nowMs);
1020
+ const promotions = tableExists(db, "team_promotion_outbound") ? db.prepare(
1021
+ `SELECT prompt_hash, destination, draft_text, score, signals_json,
1022
+ content_hash, hardened, review_attestation_json
1023
+ FROM team_promotion_outbound WHERE lease_token = ?
1024
+ ORDER BY created_at ASC LIMIT ?`
1025
+ ).all(token, PROMOTION_LEASE_LIMIT).map((r) => ({
1026
+ prompt_hash: r.prompt_hash,
1027
+ destination: r.destination,
1028
+ draft_text: r.draft_text,
1029
+ ...r.score !== null ? { score: r.score } : {},
1030
+ signals: safeParseArray(r.signals_json),
1031
+ content_hash: r.content_hash,
1032
+ ...r.hardened ? { hardened: true } : {},
1033
+ ...r.review_attestation_json != null ? { review_attestation: safeParseUnknown(r.review_attestation_json) } : {}
1034
+ })) : [];
1035
+ const revocations = tableExists(db, "team_revocation_outbound") ? db.prepare(
1036
+ `SELECT prompt_hash FROM team_revocation_outbound
1037
+ WHERE lease_token = ? ORDER BY created_at ASC LIMIT ?`
1038
+ ).all(token, PROMOTION_LEASE_LIMIT).map((r) => r.prompt_hash) : [];
1039
+ const events = tableExists(db, TELEMETRY_OUTBOUND_TABLE) ? db.prepare(
1040
+ `SELECT prompt_hash, event_type, metadata_json, created_at
1041
+ FROM ${TELEMETRY_OUTBOUND_TABLE} WHERE lease_token = ?
1042
+ ORDER BY id ASC LIMIT ?`
1043
+ ).all(token, TELEMETRY_LEASE_LIMIT).map((r) => ({
1044
+ prompt_hash: r.prompt_hash,
1045
+ event_type: r.event_type,
1046
+ created_at: r.created_at,
1047
+ metadata: safeParseUnknown(r.metadata_json) ?? {}
1048
+ })) : [];
1049
+ return { token, promotions, revocations, events };
1050
+ }
1051
+ function ackLearning(db, lease) {
1052
+ const delivered = lease.promotions.length + lease.revocations.length + lease.events.length;
1053
+ for (const table of ALL_OUTBOUND_TABLES) {
1054
+ if (!tableExists(db, table)) continue;
1055
+ db.prepare(`DELETE FROM ${table} WHERE lease_token = ?`).run(lease.token);
1056
+ }
1057
+ if (delivered > 0) {
1058
+ recordAnalytics(db, EVENT_DELIVERY_CONFIRMED, {
1059
+ promotions: lease.promotions.length,
1060
+ revocations: lease.revocations.length,
1061
+ events: lease.events.length
1062
+ });
1063
+ }
1064
+ }
1065
+ function nackLearning(db, lease, error) {
1066
+ for (const table of ALL_OUTBOUND_TABLES) {
1067
+ if (!tableExists(db, table)) continue;
1068
+ db.prepare(
1069
+ `UPDATE ${table}
1070
+ SET lease_token = NULL,
1071
+ leased_at_ms = NULL,
1072
+ attempts = attempts + 1,
1073
+ last_error = ?
1074
+ WHERE lease_token = ?`
1075
+ ).run(error.slice(0, 500), lease.token);
1076
+ }
1077
+ const snap = undeliveredSnapshot(db);
1078
+ if (snap.stalled) alarmStalled(db, snap, error);
1079
+ return snap;
1080
+ }
1081
+ function undeliveredSnapshot(db) {
1082
+ const count = (t) => {
1083
+ if (!tableExists(db, t)) return 0;
1084
+ const r = db.prepare(`SELECT COUNT(*) AS n FROM ${t}`).get();
1085
+ return r.n;
1086
+ };
1087
+ const promotions = count("team_promotion_outbound");
1088
+ const revocations = count("team_revocation_outbound");
1089
+ const events = count(TELEMETRY_OUTBOUND_TABLE);
1090
+ let maxAttempts = 0;
1091
+ let oldest = null;
1092
+ for (const t of RULE_OUTBOUND_TABLES) {
1093
+ if (!tableExists(db, t)) continue;
1094
+ if (!columns(db, t).has("attempts")) continue;
1095
+ const r = db.prepare(`SELECT MAX(attempts) AS a, MIN(created_at) AS c FROM ${t}`).get();
1096
+ if (r.a !== null && r.a > maxAttempts) maxAttempts = r.a;
1097
+ if (r.c !== null && (oldest === null || r.c < oldest)) oldest = r.c;
1098
+ }
1099
+ return {
1100
+ promotions,
1101
+ revocations,
1102
+ events,
1103
+ max_rule_attempts: maxAttempts,
1104
+ oldest_rule_created_at: oldest,
1105
+ stalled: promotions + revocations > 0 && maxAttempts >= STALL_ATTEMPTS
1106
+ };
1107
+ }
1108
+ function alarmStalled(db, snap, error) {
1109
+ const rules = snap.promotions + snap.revocations;
1110
+ process.stderr.write(
1111
+ `[massu] WARNING: ${rules} learned rule(s) have NOT reached your team after ${snap.max_rule_attempts} delivery attempts. They are SAFE (nothing was deleted) and will retry. Last error: ${error}. Run \`massu rule delivery-status\` for detail.
1112
+ `
1113
+ );
1114
+ recordAnalytics(db, EVENT_DELIVERY_STALLED, {
1115
+ undelivered_promotions: snap.promotions,
1116
+ undelivered_revocations: snap.revocations,
1117
+ undelivered_events: snap.events,
1118
+ max_attempts: snap.max_rule_attempts,
1119
+ oldest_created_at: snap.oldest_rule_created_at,
1120
+ last_error: error.slice(0, 300)
1121
+ });
1122
+ }
1123
+ function capTelemetry(db, cap = TELEMETRY_OUTBOX_CAP) {
1124
+ if (!tableExists(db, TELEMETRY_OUTBOUND_TABLE)) return 0;
1125
+ const before = db.prepare(`SELECT COUNT(*) AS n FROM ${TELEMETRY_OUTBOUND_TABLE}`).get();
1126
+ if (before.n <= cap) return 0;
1127
+ const info = db.prepare(
1128
+ `DELETE FROM ${TELEMETRY_OUTBOUND_TABLE}
1129
+ WHERE id NOT IN (SELECT id FROM ${TELEMETRY_OUTBOUND_TABLE} ORDER BY id DESC LIMIT ?)`
1130
+ ).run(cap);
1131
+ const dropped = info.changes;
1132
+ if (dropped > 0) {
1133
+ process.stderr.write(
1134
+ `[massu] NOTE: dropped ${dropped} oldest promotion-funnel telemetry row(s) at the ${cap}-row cap. No learned rules were affected (rules are never dropped).
1135
+ `
1136
+ );
1137
+ recordAnalytics(db, EVENT_TELEMETRY_CAPPED, { dropped, cap });
1138
+ }
1139
+ return dropped;
1140
+ }
1141
+ function stripLearningFromPayload(payload) {
1142
+ const clone = { ...payload };
1143
+ delete clone.rule_promotions;
1144
+ delete clone.rule_revocations;
1145
+ delete clone.rule_promotion_events;
1146
+ return clone;
1147
+ }
1148
+ function safeParseArray(raw) {
1149
+ const v = safeParseUnknown(raw);
1150
+ return Array.isArray(v) ? v : [];
1151
+ }
1152
+ function safeParseUnknown(raw) {
1153
+ try {
1154
+ return JSON.parse(raw);
1155
+ } catch {
1156
+ return null;
1157
+ }
1158
+ }
1159
+ function recordAnalytics(db, eventType, data) {
1160
+ try {
1161
+ db.prepare(
1162
+ `INSERT INTO analytics_events (event_type, event_data, created_at)
1163
+ VALUES (?, ?, datetime('now'))`
1164
+ ).run(eventType, JSON.stringify(data));
1165
+ } catch (err) {
1166
+ process.stderr.write(
1167
+ `[massu] WARNING: could not record '${eventType}' telemetry: ${err instanceof Error ? err.message : String(err)}
1168
+ `
1169
+ );
1170
+ }
1171
+ }
1172
+ var RULE_OUTBOUND_TABLES, TELEMETRY_OUTBOUND_TABLE, ALL_OUTBOUND_TABLES, TELEMETRY_OUTBOX_CAP, LEASE_TTL_MS, PROMOTION_LEASE_LIMIT, TELEMETRY_LEASE_LIMIT, STALL_ATTEMPTS, EVENT_DELIVERY_STALLED, EVENT_DELIVERY_CONFIRMED, EVENT_TELEMETRY_CAPPED;
1173
+ var init_rule_delivery = __esm({
1174
+ "src/rule-delivery.ts"() {
1175
+ "use strict";
1176
+ RULE_OUTBOUND_TABLES = [
1177
+ "team_promotion_outbound",
1178
+ "team_revocation_outbound"
1179
+ ];
1180
+ TELEMETRY_OUTBOUND_TABLE = "rule_promotion_events_outbound";
1181
+ ALL_OUTBOUND_TABLES = [
1182
+ ...RULE_OUTBOUND_TABLES,
1183
+ TELEMETRY_OUTBOUND_TABLE
1184
+ ];
1185
+ TELEMETRY_OUTBOX_CAP = 2e4;
1186
+ LEASE_TTL_MS = 15 * 60 * 1e3;
1187
+ PROMOTION_LEASE_LIMIT = 1e3;
1188
+ TELEMETRY_LEASE_LIMIT = 5e3;
1189
+ STALL_ATTEMPTS = 5;
1190
+ EVENT_DELIVERY_STALLED = "rule_delivery_stalled";
1191
+ EVENT_DELIVERY_CONFIRMED = "rule_delivery_confirmed";
1192
+ EVENT_TELEMETRY_CAPPED = "rule_telemetry_capped";
1193
+ }
1194
+ });
1195
+
1196
+ // src/rule-candidate-store.ts
1197
+ function ensureRuleCandidatesTable(db) {
1198
+ db.exec(`
1199
+ CREATE TABLE IF NOT EXISTS rule_candidates (
1200
+ prompt_hash TEXT PRIMARY KEY,
1201
+ status TEXT NOT NULL DEFAULT 'proposed'
1202
+ CHECK (status IN ('proposed','shown','promoted','dismissed')),
1203
+ origin TEXT NOT NULL DEFAULT 'local'
1204
+ CHECK (origin IN ('local','team','pack')),
1205
+ score REAL,
1206
+ destination TEXT,
1207
+ payload_json TEXT NOT NULL DEFAULT '{}',
1208
+ created_at TEXT NOT NULL DEFAULT (datetime('now')),
1209
+ updated_at TEXT NOT NULL DEFAULT (datetime('now'))
1210
+ );
1211
+ CREATE INDEX IF NOT EXISTS idx_rule_candidates_status ON rule_candidates(status);
1212
+ CREATE INDEX IF NOT EXISTS idx_rule_candidates_created ON rule_candidates(created_at);
1213
+ `);
1214
+ }
1215
+ var init_rule_candidate_store = __esm({
1216
+ "src/rule-candidate-store.ts"() {
1217
+ "use strict";
1218
+ }
1219
+ });
1220
+
974
1221
  // src/hooks/lib/hook-failure-signal.ts
975
1222
  import { appendFileSync, mkdirSync as mkdirSync3, existsSync as existsSync4 } from "fs";
976
1223
  import { join as join2, dirname as dirname3 } from "path";
@@ -1539,9 +1786,6 @@ __export(memory_db_exports, {
1539
1786
  createSession: () => createSession,
1540
1787
  deduplicateFailedAttempt: () => deduplicateFailedAttempt,
1541
1788
  dequeuePendingSync: () => dequeuePendingSync,
1542
- drainRulePromotionEvents: () => drainRulePromotionEvents,
1543
- drainTeamPromotions: () => drainTeamPromotions,
1544
- drainTeamRevocations: () => drainTeamRevocations,
1545
1789
  embedMissingObservations: () => embedMissingObservations,
1546
1790
  endSession: () => endSession,
1547
1791
  enqueueRulePromotionEvent: () => enqueueRulePromotionEvent,
@@ -2448,6 +2692,8 @@ function initMemorySchema(db) {
2448
2692
  `);
2449
2693
  migrateObservationEmbeddingChunks(db);
2450
2694
  migrateMemoryFilesFor4B(db);
2695
+ ensureRuleCandidatesTable(db);
2696
+ migrateRuleDelivery(db);
2451
2697
  }
2452
2698
  function enqueueSyncPayload(db, payload) {
2453
2699
  db.prepare("INSERT INTO pending_sync (payload) VALUES (?)").run(payload);
@@ -2522,38 +2768,12 @@ function enqueueTeamPromotion(db, promo) {
2522
2768
  promo.review_attestation !== void 0 ? JSON.stringify(promo.review_attestation) : null
2523
2769
  );
2524
2770
  }
2525
- function drainTeamPromotions(db) {
2526
- const rows = db.prepare(`
2527
- SELECT prompt_hash, destination, draft_text, score, signals_json, content_hash, hardened, review_attestation_json
2528
- FROM team_promotion_outbound ORDER BY created_at ASC LIMIT 1000
2529
- `).all();
2530
- if (rows.length === 0) return [];
2531
- db.prepare("DELETE FROM team_promotion_outbound").run();
2532
- return rows.map((r) => ({
2533
- prompt_hash: r.prompt_hash,
2534
- destination: r.destination,
2535
- draft_text: r.draft_text,
2536
- score: r.score ?? void 0,
2537
- signals: safeJsonArray(r.signals_json),
2538
- content_hash: r.content_hash,
2539
- hardened: r.hardened === 1,
2540
- review_attestation: r.review_attestation_json ? safeJsonParse(r.review_attestation_json) : void 0
2541
- }));
2542
- }
2543
2771
  function enqueueTeamRevocation(db, promptHash) {
2544
2772
  db.prepare(`
2545
2773
  INSERT OR REPLACE INTO team_revocation_outbound (prompt_hash, created_at)
2546
2774
  VALUES (?, datetime('now'))
2547
2775
  `).run(promptHash);
2548
2776
  }
2549
- function drainTeamRevocations(db) {
2550
- const rows = db.prepare(
2551
- `SELECT prompt_hash FROM team_revocation_outbound ORDER BY created_at ASC LIMIT 1000`
2552
- ).all();
2553
- if (rows.length === 0) return [];
2554
- db.prepare("DELETE FROM team_revocation_outbound").run();
2555
- return rows.map((r) => r.prompt_hash);
2556
- }
2557
2777
  function getRecurrenceCountForPromptHash(db, promptHash) {
2558
2778
  try {
2559
2779
  const row = db.prepare(`
@@ -2581,30 +2801,14 @@ function enqueueRulePromotionEvent(db, ev) {
2581
2801
  JSON.stringify(ev.metadata ?? {}),
2582
2802
  ev.created_at
2583
2803
  );
2584
- db.prepare(`
2585
- DELETE FROM rule_promotion_events_outbound
2586
- WHERE id NOT IN (
2587
- SELECT id FROM rule_promotion_events_outbound ORDER BY id DESC LIMIT ?
2588
- )
2589
- `).run(FUNNEL_EVENT_OUTBOX_CAP);
2590
- } catch {
2804
+ capTelemetry(db, FUNNEL_EVENT_OUTBOX_CAP);
2805
+ } catch (err) {
2806
+ process.stderr.write(
2807
+ `[massu] WARNING: failed to record promotion-funnel event (${ev.event_type} / ${ev.prompt_hash}): ${err instanceof Error ? err.message : String(err)}
2808
+ `
2809
+ );
2591
2810
  }
2592
2811
  }
2593
- function drainRulePromotionEvents(db) {
2594
- const rows = db.prepare(`
2595
- SELECT id, prompt_hash, event_type, metadata_json, created_at
2596
- FROM rule_promotion_events_outbound ORDER BY id ASC LIMIT ?
2597
- `).all(FUNNEL_EVENT_DRAIN_LIMIT);
2598
- if (rows.length === 0) return [];
2599
- const maxId = rows[rows.length - 1].id;
2600
- db.prepare("DELETE FROM rule_promotion_events_outbound WHERE id <= ?").run(maxId);
2601
- return rows.map((r) => ({
2602
- prompt_hash: r.prompt_hash,
2603
- event_type: r.event_type,
2604
- created_at: r.created_at,
2605
- metadata: safeJsonParse(r.metadata_json) ?? {}
2606
- }));
2607
- }
2608
2812
  function recordTelemetry(db, eventType, data) {
2609
2813
  try {
2610
2814
  db.prepare(`
@@ -2614,28 +2818,55 @@ function recordTelemetry(db, eventType, data) {
2614
2818
  } catch {
2615
2819
  }
2616
2820
  }
2617
- function safeJsonArray(json) {
2618
- try {
2619
- const v = JSON.parse(json);
2620
- return Array.isArray(v) ? v : [];
2621
- } catch {
2622
- return [];
2623
- }
2624
- }
2625
- function safeJsonParse(json) {
2626
- try {
2627
- return JSON.parse(json);
2628
- } catch {
2629
- return void 0;
2821
+ function rescueLearningFromStalePayloads(db, stale) {
2822
+ let rescued = 0;
2823
+ for (const item of stale) {
2824
+ let payload;
2825
+ try {
2826
+ payload = JSON.parse(item.payload);
2827
+ } catch {
2828
+ continue;
2829
+ }
2830
+ try {
2831
+ const promotions = Array.isArray(payload.rule_promotions) ? payload.rule_promotions : [];
2832
+ for (const p of promotions) {
2833
+ enqueueTeamPromotion(db, p);
2834
+ rescued++;
2835
+ }
2836
+ const revocations = Array.isArray(payload.rule_revocations) ? payload.rule_revocations : [];
2837
+ for (const r of revocations) {
2838
+ if (typeof r?.prompt_hash === "string") {
2839
+ enqueueTeamRevocation(db, r.prompt_hash);
2840
+ rescued++;
2841
+ }
2842
+ }
2843
+ const events = Array.isArray(payload.rule_promotion_events) ? payload.rule_promotion_events : [];
2844
+ for (const e of events) {
2845
+ enqueueRulePromotionEvent(db, e);
2846
+ }
2847
+ } catch (err) {
2848
+ process.stderr.write(
2849
+ `[massu] WARNING: failed to rescue learned rules from give-up payload ${item.id}: ${err instanceof Error ? err.message : String(err)}
2850
+ `
2851
+ );
2852
+ }
2630
2853
  }
2854
+ return rescued;
2631
2855
  }
2632
2856
  function dequeuePendingSync(db, limit = 10) {
2633
2857
  const stale = db.prepare(
2634
- "SELECT id, retry_count, last_error FROM pending_sync WHERE retry_count >= 10 LIMIT 10000"
2858
+ "SELECT id, retry_count, last_error, payload FROM pending_sync WHERE retry_count >= 10 LIMIT 10000"
2635
2859
  ).all();
2636
2860
  if (stale.length > 0) {
2861
+ const rescued = rescueLearningFromStalePayloads(db, stale);
2637
2862
  const ids = stale.map((s) => s.id);
2638
2863
  db.prepare(`DELETE FROM pending_sync WHERE id IN (${ids.map(() => "?").join(",")})`).run(...ids);
2864
+ if (rescued > 0) {
2865
+ process.stderr.write(
2866
+ `[massu] RESCUED ${rescued} learned rule(s) from ${stale.length} give-up payload(s) \u2014 they were re-queued for delivery, NOT discarded.
2867
+ `
2868
+ );
2869
+ }
2639
2870
  const lastErrors = [...new Set(stale.map((s) => s.last_error).filter(Boolean))];
2640
2871
  process.stderr.write(
2641
2872
  `[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)"}
@@ -3327,19 +3558,20 @@ function scoreFailureClasses(db, matchText, filePath, promptContext, weights) {
3327
3558
  }
3328
3559
  return bestMatch;
3329
3560
  }
3330
- 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;
3561
+ 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;
3331
3562
  var init_memory_db = __esm({
3332
3563
  "src/memory-db.ts"() {
3333
3564
  "use strict";
3334
3565
  init_config();
3335
3566
  init_memory_vector();
3336
3567
  init_db_backup();
3568
+ init_rule_delivery();
3569
+ init_rule_candidate_store();
3337
3570
  init_hook_failure_signal();
3338
3571
  init_memory_embed_sweep();
3339
3572
  MEMORY_SCHEMA_VERSION = 1;
3340
3573
  TOOL_COST_EVENTS_RETENTION_DAYS = 90;
3341
3574
  MAX_DRAFT_TEXT_LEN = 16384;
3342
- FUNNEL_EVENT_DRAIN_LIMIT = 5e3;
3343
3575
  FUNNEL_EVENT_OUTBOX_CAP = 2e4;
3344
3576
  _temporalColCache = /* @__PURE__ */ new WeakMap();
3345
3577
  CONSOLIDATION_LESSON_EVIDENCE = "consolidation:session-summary";
@@ -3351,6 +3583,7 @@ var init_memory_db = __esm({
3351
3583
 
3352
3584
  // src/hooks/session-end.ts
3353
3585
  init_memory_db();
3586
+ init_rule_delivery();
3354
3587
 
3355
3588
  // src/session-archiver.ts
3356
3589
  import { existsSync as existsSync7, readFileSync as readFileSync4, writeFileSync as writeFileSync2, mkdirSync as mkdirSync5, renameSync } from "fs";
@@ -3631,6 +3864,7 @@ function estimateTokens(text) {
3631
3864
  // src/cloud-sync.ts
3632
3865
  init_config();
3633
3866
  init_memory_db();
3867
+ init_rule_delivery();
3634
3868
 
3635
3869
  // src/observation-extractor.ts
3636
3870
  init_memory_db();
@@ -3669,7 +3903,9 @@ function classifyVisibility(title, detail) {
3669
3903
  // src/cloud-sync.ts
3670
3904
  var MAX_RETRIES = 3;
3671
3905
  var RETRY_DELAYS = [1e3, 2e3, 4e3];
3672
- var DEFAULT_CLOUD_REQUEST_TIMEOUT_MS = 2e3;
3906
+ var DEFAULT_CLOUD_REQUEST_TIMEOUT_MS = 8e3;
3907
+ var SYNC_DEADLINE_MS = 2e4;
3908
+ var MIN_ATTEMPT_BUDGET_MS = 1e3;
3673
3909
  async function syncToCloud(db, payload) {
3674
3910
  const config = getConfig();
3675
3911
  const cloud = config.cloud;
@@ -3752,8 +3988,15 @@ async function syncToCloud(db, payload) {
3752
3988
  }
3753
3989
  }
3754
3990
  let lastError = "";
3755
- const requestTimeoutMs = cloud.requestTimeoutMs ?? DEFAULT_CLOUD_REQUEST_TIMEOUT_MS;
3991
+ const configuredTimeoutMs = cloud.requestTimeoutMs ?? DEFAULT_CLOUD_REQUEST_TIMEOUT_MS;
3992
+ const deadlineAt = Date.now() + SYNC_DEADLINE_MS;
3756
3993
  for (let attempt = 0; attempt < MAX_RETRIES; attempt++) {
3994
+ const remainingMs = deadlineAt - Date.now();
3995
+ if (remainingMs < MIN_ATTEMPT_BUDGET_MS) {
3996
+ lastError = lastError || `sync deadline exceeded (${SYNC_DEADLINE_MS}ms)`;
3997
+ break;
3998
+ }
3999
+ const requestTimeoutMs = Math.min(configuredTimeoutMs, remainingMs);
3757
4000
  try {
3758
4001
  const response = await fetch(`${endpoint}/sync`, {
3759
4002
  method: "POST",
@@ -3781,6 +4024,10 @@ async function syncToCloud(db, payload) {
3781
4024
  const result = await response.json();
3782
4025
  return {
3783
4026
  success: true,
4027
+ // THE ONLY place this is ever set. The server received the payload and
4028
+ // answered 2xx. This — and nothing else — is a receipt, and a receipt is
4029
+ // the sole authority to delete a learned rule.
4030
+ transmitted: true,
3784
4031
  synced: {
3785
4032
  sessions: result.synced?.sessions ?? 0,
3786
4033
  observations: result.synced?.observations ?? 0,
@@ -3800,8 +4047,12 @@ async function syncToCloud(db, payload) {
3800
4047
  }
3801
4048
  }
3802
4049
  try {
3803
- enqueueSyncPayload(db, JSON.stringify(payload));
3804
- } catch (_e) {
4050
+ enqueueSyncPayload(db, JSON.stringify(stripLearningFromPayload(payload)));
4051
+ } catch (err) {
4052
+ process.stderr.write(
4053
+ `[massu] WARNING: could not queue sync payload for retry: ${err instanceof Error ? err.message : String(err)}
4054
+ `
4055
+ );
3805
4056
  }
3806
4057
  return {
3807
4058
  success: false,
@@ -4085,6 +4336,9 @@ init_memory_db();
4085
4336
  // src/rule-candidate-funnel.ts
4086
4337
  init_memory_db();
4087
4338
 
4339
+ // src/rule-candidate-applier.ts
4340
+ init_rule_candidate_store();
4341
+
4088
4342
  // src/rule-candidate-hardened.ts
4089
4343
  var TEAM_HARDENED_SHAREABLE_DESTINATIONS = [
4090
4344
  "pattern-scanner",
@@ -5690,17 +5944,31 @@ async function main() {
5690
5944
  }
5691
5945
  endSession(db, session_id, "completed");
5692
5946
  archiveAndRegenerate(db, session_id);
5947
+ let openLease = null;
5693
5948
  try {
5694
5949
  await drainSyncQueue(db);
5695
- const syncPayload = buildSyncPayload(db, session_id, observations, summary);
5950
+ const cloudCfg = getConfig().cloud;
5951
+ const willTransmit = !!cloudCfg?.enabled && !!cloudCfg?.apiKey && !!cloudCfg?.endpoint && cloudCfg?.sync?.memory !== false;
5952
+ const lease = willTransmit ? leaseLearning(db) : null;
5953
+ openLease = lease;
5954
+ const syncPayload = buildSyncPayload(db, session_id, observations, summary, lease);
5696
5955
  const result = await syncToCloud(db, syncPayload);
5697
- if (!result.success && result.error) {
5956
+ if (lease) {
5957
+ if (result.transmitted === true) {
5958
+ ackLearning(db, lease);
5959
+ } else {
5960
+ nackLearning(db, lease, result.error ?? "not transmitted (sync disabled or unreachable)");
5961
+ }
5698
5962
  }
5963
+ openLease = null;
5699
5964
  try {
5700
5965
  await pullTeamPromotions(db);
5701
5966
  } catch (_pullErr) {
5702
5967
  }
5703
- } catch (_syncErr) {
5968
+ } catch (syncErr) {
5969
+ const msg = syncErr instanceof Error ? syncErr.message : String(syncErr);
5970
+ if (openLease) nackLearning(db, openLease, msg);
5971
+ recordHookFailure("session-end:cloud-sync", syncErr);
5704
5972
  }
5705
5973
  } finally {
5706
5974
  db.close();
@@ -5710,12 +5978,10 @@ async function main() {
5710
5978
  }
5711
5979
  process.exit(0);
5712
5980
  }
5713
- function buildSyncPayload(db, sessionId, observations, summary) {
5714
- const cfg = getConfig().cloud;
5715
- const willTransmit = !!cfg?.enabled && !!cfg?.apiKey && cfg?.sync?.memory !== false;
5716
- const promotions = willTransmit ? drainTeamPromotions(db) : [];
5717
- const revocations = willTransmit ? drainTeamRevocations(db) : [];
5718
- const funnelEvents = willTransmit ? drainRulePromotionEvents(db) : [];
5981
+ function buildSyncPayload(db, sessionId, observations, summary, lease) {
5982
+ const promotions = lease?.promotions ?? [];
5983
+ const revocations = lease?.revocations ?? [];
5984
+ const funnelEvents = lease?.events ?? [];
5719
5985
  return {
5720
5986
  sessions: [{
5721
5987
  local_session_id: sessionId,