@monoes/monomindcli 1.10.29 → 1.10.31

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 (111) hide show
  1. package/.claude/helpers/auto-memory-hook.mjs +39 -4
  2. package/.claude/helpers/handlers/adr-draft-handler.cjs +64 -0
  3. package/.claude/helpers/handlers/agent-start-handler.cjs +99 -0
  4. package/.claude/helpers/handlers/edit-handler.cjs +145 -0
  5. package/.claude/helpers/handlers/graph-status-handler.cjs +38 -0
  6. package/.claude/helpers/handlers/route-handler.cjs +393 -0
  7. package/.claude/helpers/handlers/session-handler.cjs +167 -0
  8. package/.claude/helpers/handlers/session-restore-handler.cjs +348 -0
  9. package/.claude/helpers/handlers/task-handler.cjs +329 -0
  10. package/.claude/helpers/hook-handler.cjs +120 -2431
  11. package/.claude/helpers/intelligence.cjs +21 -2
  12. package/.claude/helpers/learning-service.mjs +166 -8
  13. package/.claude/helpers/memory-palace.cjs +72 -12
  14. package/.claude/helpers/router.cjs +79 -5
  15. package/.claude/helpers/statusline.cjs +193 -399
  16. package/.claude/helpers/utils/micro-agents.cjs +338 -0
  17. package/.claude/helpers/utils/monograph.cjs +349 -0
  18. package/.claude/helpers/utils/telemetry.cjs +144 -0
  19. package/.claude/skills/agent-browser-testing/SKILL.md +3 -2
  20. package/.claude/skills/monomind/browse-agentcore.md +116 -0
  21. package/.claude/skills/monomind/browse-electron.md +189 -0
  22. package/.claude/skills/monomind/browse-qa.md +229 -0
  23. package/.claude/skills/monomind/browse-references/authentication.md +162 -0
  24. package/.claude/skills/monomind/browse-references/trust-boundaries.md +41 -0
  25. package/.claude/skills/monomind/browse-references/video-recording.md +84 -0
  26. package/.claude/skills/monomind/browse-slack.md +189 -0
  27. package/.claude/skills/monomind/browse-vercel.md +240 -0
  28. package/.claude/skills/monomind/browse.md +724 -0
  29. package/dist/src/browser/actions.d.ts +28 -0
  30. package/dist/src/browser/actions.d.ts.map +1 -0
  31. package/dist/src/browser/actions.js +292 -0
  32. package/dist/src/browser/actions.js.map +1 -0
  33. package/dist/src/browser/batch.d.ts +13 -0
  34. package/dist/src/browser/batch.d.ts.map +1 -0
  35. package/dist/src/browser/batch.js +11 -0
  36. package/dist/src/browser/batch.js.map +1 -0
  37. package/dist/src/browser/browser.d.ts +14 -0
  38. package/dist/src/browser/browser.d.ts.map +1 -0
  39. package/dist/src/browser/browser.js +198 -0
  40. package/dist/src/browser/browser.js.map +1 -0
  41. package/dist/src/browser/cdp.d.ts +17 -0
  42. package/dist/src/browser/cdp.d.ts.map +1 -0
  43. package/dist/src/browser/cdp.js +106 -0
  44. package/dist/src/browser/cdp.js.map +1 -0
  45. package/dist/src/browser/console-log.d.ts +22 -0
  46. package/dist/src/browser/console-log.d.ts.map +1 -0
  47. package/dist/src/browser/console-log.js +55 -0
  48. package/dist/src/browser/console-log.js.map +1 -0
  49. package/dist/src/browser/dialog.d.ts +11 -0
  50. package/dist/src/browser/dialog.d.ts.map +1 -0
  51. package/dist/src/browser/dialog.js +36 -0
  52. package/dist/src/browser/dialog.js.map +1 -0
  53. package/dist/src/browser/emulation.d.ts +15 -0
  54. package/dist/src/browser/emulation.d.ts.map +1 -0
  55. package/dist/src/browser/emulation.js +62 -0
  56. package/dist/src/browser/emulation.js.map +1 -0
  57. package/dist/src/browser/find.d.ts +21 -0
  58. package/dist/src/browser/find.d.ts.map +1 -0
  59. package/dist/src/browser/find.js +118 -0
  60. package/dist/src/browser/find.js.map +1 -0
  61. package/dist/src/browser/index.d.ts +18 -0
  62. package/dist/src/browser/index.d.ts.map +1 -0
  63. package/dist/src/browser/index.js +18 -0
  64. package/dist/src/browser/index.js.map +1 -0
  65. package/dist/src/browser/network.d.ts +11 -0
  66. package/dist/src/browser/network.d.ts.map +1 -0
  67. package/dist/src/browser/network.js +81 -0
  68. package/dist/src/browser/network.js.map +1 -0
  69. package/dist/src/browser/pdf.d.ts +15 -0
  70. package/dist/src/browser/pdf.d.ts.map +1 -0
  71. package/dist/src/browser/pdf.js +27 -0
  72. package/dist/src/browser/pdf.js.map +1 -0
  73. package/dist/src/browser/screenshot.d.ts +15 -0
  74. package/dist/src/browser/screenshot.d.ts.map +1 -0
  75. package/dist/src/browser/screenshot.js +36 -0
  76. package/dist/src/browser/screenshot.js.map +1 -0
  77. package/dist/src/browser/session.d.ts +8 -0
  78. package/dist/src/browser/session.d.ts.map +1 -0
  79. package/dist/src/browser/session.js +50 -0
  80. package/dist/src/browser/session.js.map +1 -0
  81. package/dist/src/browser/snapshot.d.ts +12 -0
  82. package/dist/src/browser/snapshot.d.ts.map +1 -0
  83. package/dist/src/browser/snapshot.js +147 -0
  84. package/dist/src/browser/snapshot.js.map +1 -0
  85. package/dist/src/browser/storage.d.ts +11 -0
  86. package/dist/src/browser/storage.d.ts.map +1 -0
  87. package/dist/src/browser/storage.js +43 -0
  88. package/dist/src/browser/storage.js.map +1 -0
  89. package/dist/src/browser/tabs.d.ts +8 -0
  90. package/dist/src/browser/tabs.d.ts.map +1 -0
  91. package/dist/src/browser/tabs.js +25 -0
  92. package/dist/src/browser/tabs.js.map +1 -0
  93. package/dist/src/browser/types.d.ts +109 -0
  94. package/dist/src/browser/types.d.ts.map +1 -0
  95. package/dist/src/browser/types.js +16 -0
  96. package/dist/src/browser/types.js.map +1 -0
  97. package/dist/src/browser/wait.d.ts +4 -0
  98. package/dist/src/browser/wait.d.ts.map +1 -0
  99. package/dist/src/browser/wait.js +122 -0
  100. package/dist/src/browser/wait.js.map +1 -0
  101. package/dist/src/commands/browse.d.ts +8 -0
  102. package/dist/src/commands/browse.d.ts.map +1 -0
  103. package/dist/src/commands/browse.js +1494 -0
  104. package/dist/src/commands/browse.js.map +1 -0
  105. package/dist/src/commands/index.d.ts.map +1 -1
  106. package/dist/src/commands/index.js +2 -0
  107. package/dist/src/commands/index.js.map +1 -1
  108. package/dist/src/ui/dashboard-v2.html +1857 -0
  109. package/dist/src/ui/server.mjs +71 -1
  110. package/dist/tsconfig.tsbuildinfo +1 -1
  111. package/package.json +2 -1
@@ -158,6 +158,9 @@ function matchScore(promptWords, entryWords) {
158
158
  }
159
159
 
160
160
  var cachedEntries = null;
161
+ // Module-level state for feedback correlation
162
+ var _lastContext = null; // stores last prompt string from getContext() calls
163
+ var _recentEdits = []; // ring buffer of recent file edits (max 50)
161
164
 
162
165
  module.exports = {
163
166
  init: function() {
@@ -192,15 +195,31 @@ module.exports = {
192
195
  var summary = (e.entry.summary || e.entry.content || "").substring(0, 80);
193
196
  lines.push(" * (" + conf.toFixed(2) + ") " + summary);
194
197
  }
198
+ _lastContext = prompt; // capture for feedback() correlation
195
199
  return lines.join("\n");
196
200
  },
197
201
 
198
202
  recordEdit: function(file) {
199
- // pending-insights staging removed consolidate() was a no-op that just cleared this file
203
+ // Track recent edits for context relevance adjustment (ring buffer, max 50)
204
+ _recentEdits.push({ filePath: file, ts: Date.now() });
205
+ if (_recentEdits.length > 50) _recentEdits.shift();
200
206
  },
201
207
 
202
208
  feedback: function(success) {
203
- // Stub: no-op in minimal version
209
+ // Append outcome to intelligence-outcomes.jsonl. At session-end,
210
+ // session-handler.cjs reads this file (30-minute window, majority-vote)
211
+ // to derive intelligenceFeedback for routing-feedback.jsonl.
212
+ try {
213
+ var outPath = path.join(DATA_DIR, 'intelligence-outcomes.jsonl');
214
+ ensureDir(DATA_DIR);
215
+ var entry = JSON.stringify({
216
+ ts: Date.now(),
217
+ success: success,
218
+ context: _lastContext ? _lastContext.substring(0, 200) : null,
219
+ recentEdits: _recentEdits.slice(-5).map(function(e) { return e.filePath; }),
220
+ });
221
+ fs.appendFileSync(outPath, entry + '\n', 'utf-8');
222
+ } catch (e) { /* non-critical — must not break session-end hook */ }
204
223
  },
205
224
 
206
225
  consolidate: function() {
@@ -113,7 +113,9 @@ function initializeDatabase(db) {
113
113
  metadata TEXT
114
114
  );
115
115
 
116
- -- HNSW index metadata
116
+ -- HNSW index per-pattern mapping (unused: full index snapshots are stored in
117
+ -- session_state under keys hnsw_short_term/hnsw_long_term as JSON blobs.
118
+ -- This table exists for future per-vector indexing if needed.)
117
119
  CREATE TABLE IF NOT EXISTS hnsw_index (
118
120
  id INTEGER PRIMARY KEY,
119
121
  pattern_type TEXT NOT NULL, -- 'short_term' or 'long_term'
@@ -168,6 +170,20 @@ function initializeDatabase(db) {
168
170
  // HNSW Index (In-Memory with SQLite persistence)
169
171
  // =============================================================================
170
172
 
173
+ // TODO: Replace HNSWIndex with shared HnswLite from @monomind/memory once
174
+ // API differences are resolved. Migration notes:
175
+ // - HnswLite constructor: (dimensions: number, m: number, efConstruction: number, metric: string)
176
+ // vs HNSWIndex constructor: (config: { embedding: { dimension }, hnsw: { m, efConstruction } })
177
+ // - HnswLite.add(id: string, vector: Float32Array): void
178
+ // vs HNSWIndex.add(patternId, embedding): vectorId (numeric)
179
+ // - HnswLite.search(query: Float32Array, k: number, threshold?): { id, score }[]
180
+ // vs HNSWIndex.search(queryEmbedding, k): { results: { patternId, similarity, vectorId }[], searchTimeMs }
181
+ // - HnswLite.remove(id: string): void (no return)
182
+ // vs HNSWIndex.remove(patternId): boolean
183
+ // - dist/hnsw-lite.js exists at packages/@monomind/memory/dist/hnsw-lite.js
184
+ // - Import: import { HnswLite } from '../../packages/@monomind/memory/dist/hnsw-lite.js'
185
+ // Distance metric: HNSWIndex uses cosine similarity (1 - distance) in _searchGraph;
186
+ // HnswLite defaults to cosineSimilarity — compatible. See D3 comment in hnsw-lite.ts.
171
187
  class HNSWIndex {
172
188
  constructor(config) {
173
189
  this.config = config;
@@ -565,6 +581,15 @@ class EmbeddingService {
565
581
 
566
582
  // =============================================================================
567
583
  // Learning Service
584
+ //
585
+ // Singleton contract (enforced by hook-handler.cjs::getLearningService()):
586
+ // - One LearningService instance is created per hook-handler process.
587
+ // - initialize() opens the SQLite DB at DB_PATH and loads HNSW indexes;
588
+ // it must be called exactly once after construction.
589
+ // - consolidate() is called at session-end to prune + promote patterns;
590
+ // it operates on the already-open DB — no re-initialization needed.
591
+ // - The constructor intentionally leaves all fields null until initialize()
592
+ // is called; callers must not invoke other methods before initialize().
568
593
  // =============================================================================
569
594
 
570
595
  class LearningService {
@@ -658,6 +683,9 @@ class LearningService {
658
683
  // Check if we need to prune
659
684
  this._pruneShortTerm();
660
685
 
686
+ // Persist index snapshot non-blocking
687
+ this._saveIndexes().catch(e => console.error('[LearningService] saveIndexes error:', e));
688
+
661
689
  return { id, action: 'created', embedding: Array.from(embedding).slice(0, 5) };
662
690
  }
663
691
 
@@ -817,21 +845,32 @@ class LearningService {
817
845
  // 2. Rebuild indexes
818
846
  await this._loadIndexes();
819
847
 
820
- // 3. Remove duplicates in long-term
848
+ // 3. Remove duplicates in long-term (simhash-bucketed to avoid O(n²) cosine)
821
849
  const longTermPatterns = this.db.prepare('SELECT * FROM long_term_patterns').all();
850
+
851
+ // Build simhash fingerprints for all patterns
852
+ const fingerprints = new Map();
853
+ for (const p of longTermPatterns) {
854
+ fingerprints.set(p.id, this._simhash(p.strategy || ''));
855
+ }
856
+
857
+ // Near-dup detection: only compare pairs with Hamming distance < 4
822
858
  for (let i = 0; i < longTermPatterns.length; i++) {
823
859
  for (let j = i + 1; j < longTermPatterns.length; j++) {
860
+ const hammingDist = this._hammingDistance(
861
+ fingerprints.get(longTermPatterns[i].id),
862
+ fingerprints.get(longTermPatterns[j].id)
863
+ );
864
+ if (hammingDist >= 4) continue; // Skip dissimilar pairs (fast path)
824
865
  const sim = this._cosineSimilarity(
825
866
  this._bufferToFloat32Array(longTermPatterns[i].embedding),
826
867
  this._bufferToFloat32Array(longTermPatterns[j].embedding)
827
868
  );
828
-
829
869
  if (sim > CONFIG.patterns.dedupThreshold) {
830
870
  // Keep the higher quality one
831
871
  const toRemove = longTermPatterns[i].quality >= longTermPatterns[j].quality
832
872
  ? longTermPatterns[j].id
833
873
  : longTermPatterns[i].id;
834
-
835
874
  this.db.prepare('DELETE FROM long_term_patterns WHERE id = ?').run(toRemove);
836
875
  stats.duplicatesRemoved++;
837
876
  }
@@ -851,10 +890,74 @@ class LearningService {
851
890
 
852
891
  this.metrics.consolidations++;
853
892
 
893
+ // 5. Promote eligible episodic patterns to semantic long-term memory
894
+ const promResult = await this.promoteEpisodic();
895
+ if (promResult.promoted > 0) {
896
+ console.log(`[Learning] Promoted ${promResult.promoted} episodic pattern(s) to semantic memory`);
897
+ }
898
+
899
+ // Persist updated index snapshot non-blocking
900
+ this._saveIndexes().catch(e => console.error('[LearningService] saveIndexes error:', e));
901
+
854
902
  const duration = Date.now() - startTime;
855
903
  console.log(`[Learning] Consolidation complete in ${duration}ms:`, stats);
856
904
 
857
- return { ...stats, durationMs: duration };
905
+ return { ...stats, promoted: promResult.promoted, durationMs: duration };
906
+ }
907
+
908
+ // Promote short-term episodic patterns to long-term semantic memory
909
+ async promoteEpisodic() {
910
+ const threshold = CONFIG.patterns.promotionThreshold;
911
+ const candidates = this.db.prepare(`
912
+ SELECT * FROM short_term_patterns
913
+ WHERE usage_count >= ?
914
+ ORDER BY quality DESC
915
+ `).all(threshold);
916
+
917
+ let promoted = 0;
918
+ for (const pattern of candidates) {
919
+ try {
920
+ // Check if already in long-term (using source_pattern_id link)
921
+ const exists = this.db.prepare('SELECT id FROM long_term_patterns WHERE source_pattern_id = ? OR id = ?').get(pattern.id, `lt_${pattern.id}`);
922
+ if (exists) {
923
+ // Remove from short-term since it was already promoted
924
+ this.db.prepare('DELETE FROM short_term_patterns WHERE id = ?').run(pattern.id);
925
+ this.shortTermIndex.remove(pattern.id);
926
+ continue;
927
+ }
928
+
929
+ const now = Date.now();
930
+ const ltId = `lt_${pattern.id}`;
931
+
932
+ // Promote to long-term
933
+ this.db.prepare(`
934
+ INSERT OR IGNORE INTO long_term_patterns
935
+ (id, strategy, domain, embedding, quality, usage_count, success_count,
936
+ created_at, updated_at, promoted_at, source_pattern_id, metadata)
937
+ VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
938
+ `).run(
939
+ ltId, pattern.strategy, pattern.domain, pattern.embedding,
940
+ pattern.quality, pattern.usage_count, pattern.success_count || 0,
941
+ pattern.created_at, now, now, pattern.id, pattern.metadata
942
+ );
943
+
944
+ // Add to long-term HNSW index
945
+ const embedding = this._bufferToFloat32Array(pattern.embedding);
946
+ if (embedding) this.longTermIndex.add(ltId, embedding);
947
+
948
+ // Remove from short-term
949
+ this.db.prepare('DELETE FROM short_term_patterns WHERE id = ?').run(pattern.id);
950
+ this.shortTermIndex.remove(pattern.id);
951
+ promoted++;
952
+ } catch (e) { /* skip individual failures */ }
953
+ }
954
+
955
+ if (promoted > 0) {
956
+ this.metrics.promotions += promoted;
957
+ this._saveIndexes().catch(e => console.error('[LearningService] saveIndexes error:', e));
958
+ }
959
+
960
+ return { promoted };
858
961
  }
859
962
 
860
963
  // Export learning data for session end
@@ -903,9 +1006,41 @@ class LearningService {
903
1006
  };
904
1007
  }
905
1008
 
906
- // Load indexes from database
1009
+ // Save full HNSW index snapshots to session_state (key-value TEXT store).
1010
+ // Using session_state (not hnsw_index) because session_state stores arbitrary blobs;
1011
+ // hnsw_index stores per-pattern mappings and is not designed for full snapshots.
1012
+ async _saveIndexes() {
1013
+ try {
1014
+ const shortTermData = JSON.stringify(this.shortTermIndex.serialize());
1015
+ const longTermData = JSON.stringify(this.longTermIndex.serialize());
1016
+ this.db.prepare(`
1017
+ INSERT OR REPLACE INTO session_state (key, value, updated_at)
1018
+ VALUES ('hnsw_short_term', ?, ?)
1019
+ `).run(shortTermData, Date.now());
1020
+ this.db.prepare(`
1021
+ INSERT OR REPLACE INTO session_state (key, value, updated_at)
1022
+ VALUES ('hnsw_long_term', ?, ?)
1023
+ `).run(longTermData, Date.now());
1024
+ } catch (e) {
1025
+ console.error('[LearningService] Failed to save HNSW indexes:', e.message);
1026
+ }
1027
+ }
1028
+
1029
+ // Load indexes from database — try serialized snapshot first, fall back to rebuild
907
1030
  async _loadIndexes() {
908
- // Load short-term patterns
1031
+ try {
1032
+ const shortRow = this.db.prepare("SELECT value FROM session_state WHERE key = 'hnsw_short_term'").get();
1033
+ const longRow = this.db.prepare("SELECT value FROM session_state WHERE key = 'hnsw_long_term'").get();
1034
+ if (shortRow && longRow) {
1035
+ this.shortTermIndex = HNSWIndex.deserialize(JSON.parse(shortRow.value), CONFIG);
1036
+ this.longTermIndex = HNSWIndex.deserialize(JSON.parse(longRow.value), CONFIG);
1037
+ return;
1038
+ }
1039
+ } catch (e) {
1040
+ console.error('[LearningService] Failed to restore HNSW snapshots, rebuilding:', e.message);
1041
+ }
1042
+
1043
+ // Rebuild from patterns tables (fallback)
909
1044
  this.shortTermIndex = new HNSWIndex(CONFIG);
910
1045
  const shortTermPatterns = this.db.prepare('SELECT id, embedding FROM short_term_patterns').all();
911
1046
  for (const row of shortTermPatterns) {
@@ -915,7 +1050,6 @@ class LearningService {
915
1050
  }
916
1051
  }
917
1052
 
918
- // Load long-term patterns
919
1053
  this.longTermIndex = new HNSWIndex(CONFIG);
920
1054
  const longTermPatterns = this.db.prepare('SELECT id, embedding FROM long_term_patterns').all();
921
1055
  for (const row of longTermPatterns) {
@@ -959,6 +1093,30 @@ class LearningService {
959
1093
  `).run(key, value, Date.now());
960
1094
  }
961
1095
 
1096
+ // 32-bit simhash via bit-weighting of character bigrams
1097
+ _simhash(text) {
1098
+ const bits = new Int32Array(32);
1099
+ const chars = text.toLowerCase().replace(/\s+/g, ' ');
1100
+ for (let i = 0; i < chars.length - 1; i++) {
1101
+ const bigram = chars.charCodeAt(i) * 31 + chars.charCodeAt(i + 1);
1102
+ for (let b = 0; b < 32; b++) {
1103
+ bits[b] += (bigram >> b) & 1 ? 1 : -1;
1104
+ }
1105
+ }
1106
+ let hash = 0;
1107
+ for (let b = 0; b < 32; b++) {
1108
+ if (bits[b] > 0) hash |= (1 << b);
1109
+ }
1110
+ return hash >>> 0; // unsigned
1111
+ }
1112
+
1113
+ _hammingDistance(a, b) {
1114
+ let x = a ^ b;
1115
+ let count = 0;
1116
+ while (x) { count += x & 1; x >>>= 1; }
1117
+ return count;
1118
+ }
1119
+
962
1120
  // Cosine similarity helper
963
1121
  _cosineSimilarity(a, b) {
964
1122
  let dot = 0, normA = 0, normB = 0;
@@ -199,23 +199,83 @@ function storeVerbatim(cwd, content, meta) {
199
199
  // ── score persistence ─────────────────────────────────────────────────────────
200
200
  /**
201
201
  * _bumpScores(drawersFile, ids)
202
- * Rewrite drawers.jsonl with score += 1 for the given drawer ids.
202
+ * Appends score bumps to a sidecar .score-diffs.jsonl instead of rewriting
203
+ * the full drawers.jsonl on every search. Compacts when sidecar reaches 100 entries.
203
204
  * Called after search/recall so frequently-retrieved drawers rise to L1.
204
205
  */
205
206
  function _bumpScores(drawersFile, ids) {
206
207
  if (!ids || ids.length === 0) return;
207
- var idSet = {};
208
- ids.forEach(function(id) { idSet[id] = true; });
208
+ var diffPath = drawersFile.replace('.jsonl', '-score-diffs.jsonl');
209
+ try {
210
+ var entry = JSON.stringify({ ts: Date.now(), bumpIds: ids, delta: 1 });
211
+ fs.appendFileSync(diffPath, entry + '\n', 'utf-8');
212
+ _maybeCompactScoreDiffs(drawersFile, diffPath);
213
+ } catch (e) { /* non-fatal */ }
214
+ }
215
+
216
+ /**
217
+ * _maybeCompactScoreDiffs(drawersFile, diffPath)
218
+ * When the sidecar accumulates >= 100 entries, apply all diffs to the drawers
219
+ * file atomically and delete the sidecar.
220
+ */
221
+ function _maybeCompactScoreDiffs(drawersFile, diffPath) {
222
+ var lines;
223
+ try {
224
+ lines = fs.readFileSync(diffPath, 'utf-8').split('\n').filter(Boolean);
225
+ } catch { return; }
226
+ if (lines.length < 100) return;
227
+ _applyScoreDiffs(drawersFile, diffPath, lines);
228
+ }
229
+
230
+ /**
231
+ * _applyScoreDiffs(drawersFile, diffPath, lines)
232
+ * Apply accumulated score diffs to drawers.jsonl atomically, then remove sidecar.
233
+ */
234
+ function _applyScoreDiffs(drawersFile, diffPath, lines) {
209
235
  try {
210
236
  var drawers = readJsonl(drawersFile);
211
- var changed = false;
237
+ var diffs = (lines || fs.readFileSync(diffPath, 'utf-8').split('\n').filter(Boolean))
238
+ .map(function(l) { try { return JSON.parse(l); } catch { return null; } })
239
+ .filter(Boolean);
240
+ for (var i = 0; i < diffs.length; i++) {
241
+ var diff = diffs[i];
242
+ var delta = diff.delta || 1;
243
+ (diff.bumpIds || []).forEach(function(id) {
244
+ var d = drawers.find(function(x) { return x.id === id; });
245
+ if (d) d.score = (d.score || 1.0) + delta;
246
+ });
247
+ }
248
+ var tmpPath = drawersFile + '.tmp';
249
+ fs.writeFileSync(tmpPath, drawers.map(function(d) { return JSON.stringify(d); }).join('\n') + '\n', 'utf-8');
250
+ fs.renameSync(tmpPath, drawersFile);
251
+ try { fs.unlinkSync(diffPath); } catch {}
252
+ } catch (e) { /* non-fatal — leave sidecar intact for next attempt */ }
253
+ }
254
+
255
+ /**
256
+ * _getEffectiveDrawers(drawersFile)
257
+ * Read drawers.jsonl and apply any pending score diffs from the sidecar,
258
+ * returning the merged result without compacting. Used by wakeUp/recall/search
259
+ * to get accurate scores without triggering a full compact.
260
+ */
261
+ function _getEffectiveDrawers(drawersFile) {
262
+ var drawers = readJsonl(drawersFile);
263
+ var diffPath = drawersFile.replace('.jsonl', '-score-diffs.jsonl');
264
+ try {
265
+ var lines = fs.readFileSync(diffPath, 'utf-8').split('\n').filter(Boolean);
266
+ var diffs = lines.map(function(l) { try { return JSON.parse(l); } catch { return null; } }).filter(Boolean);
267
+ // Build score bump totals per id
268
+ var bumps = {};
269
+ diffs.forEach(function(diff) {
270
+ (diff.bumpIds || []).forEach(function(id) {
271
+ bumps[id] = (bumps[id] || 0) + (diff.delta || 1);
272
+ });
273
+ });
212
274
  drawers.forEach(function(d) {
213
- if (idSet[d.id]) { d.score = (d.score || 1.0) + 1; changed = true; }
275
+ if (bumps[d.id]) d.score = (d.score || 1.0) + bumps[d.id];
214
276
  });
215
- if (changed) {
216
- fs.writeFileSync(drawersFile, drawers.map(function(d) { return JSON.stringify(d); }).join('\n') + '\n', 'utf-8');
217
- }
218
- } catch (e) { /* non-fatal */ }
277
+ } catch { /* sidecar absent — use raw drawers */ }
278
+ return drawers;
219
279
  }
220
280
 
221
281
  // ── search (L3 deep) ──────────────────────────────────────────────────────────
@@ -229,7 +289,7 @@ function search(cwd, query, opts) {
229
289
  var limit = opts.limit || 5;
230
290
  var pDir = palaceDir(cwd);
231
291
  var drawersFile = path.join(pDir, 'drawers.jsonl');
232
- var drawers = readJsonl(drawersFile);
292
+ var drawers = _getEffectiveDrawers(drawersFile);
233
293
 
234
294
  if (opts.wing) drawers = drawers.filter(function(d) { return d.wing === opts.wing; });
235
295
  if (opts.room) drawers = drawers.filter(function(d) { return d.room === opts.room; });
@@ -273,7 +333,7 @@ function recall(cwd, opts) {
273
333
  opts = opts || {};
274
334
  var limit = opts.limit || 10;
275
335
  var drawersFile = path.join(palaceDir(cwd), 'drawers.jsonl');
276
- var drawers = readJsonl(drawersFile);
336
+ var drawers = _getEffectiveDrawers(drawersFile);
277
337
  if (opts.wing) drawers = drawers.filter(function(d) { return d.wing === opts.wing; });
278
338
  if (opts.room) drawers = drawers.filter(function(d) { return d.room === opts.room; });
279
339
  var top = drawers
@@ -363,7 +423,7 @@ function wakeUp(cwd) {
363
423
  var drawersFile = path.join(pDir, 'drawers.jsonl');
364
424
  if (fs.existsSync(drawersFile)) {
365
425
  try {
366
- var drawers = readJsonl(drawersFile);
426
+ var drawers = _getEffectiveDrawers(drawersFile);
367
427
  var cutoff = Date.now() - L1_DAYS * 24 * 60 * 60 * 1000;
368
428
  var recent = drawers.filter(function(d) {
369
429
  return d.ts && new Date(d.ts).getTime() > cutoff;
@@ -395,6 +395,73 @@ async function routeTaskSemantic(task) {
395
395
  return keywordResult;
396
396
  }
397
397
 
398
+ // ─── Feedback weight loader ───────────────────────────────────────────────────
399
+ // Reads .monomind/routing-feedback.jsonl to compute per-agent success-rate
400
+ // weights. Weights are memoized with a 60-second TTL.
401
+ //
402
+ // Signal source: routing-feedback.jsonl entries with a non-null intelligenceFeedback
403
+ // boolean. Derived at session-end from intelligence-outcomes.jsonl (30-minute window,
404
+ // majority-vote). Requires MIN_FEEDBACK_SAMPLES entries per agent to activate.
405
+ //
406
+ // To verify: copy .monomind/test-fixtures/routing-feedback-seeded.jsonl to
407
+ // .monomind/routing-feedback.jsonl, then call loadFeedbackWeights() in a REPL.
408
+
409
+ var _feedbackWeightsCache = null;
410
+ var _feedbackWeightsCacheTime = 0;
411
+ var FEEDBACK_CACHE_TTL_MS = 60000;
412
+ var MIN_FEEDBACK_SAMPLES = 10;
413
+
414
+ function loadFeedbackWeights() {
415
+ var now = Date.now();
416
+ if (_feedbackWeightsCache && now - _feedbackWeightsCacheTime < FEEDBACK_CACHE_TTL_MS) {
417
+ return _feedbackWeightsCache;
418
+ }
419
+ var feedbackPath = path.join(process.cwd(), '.monomind', 'routing-feedback.jsonl');
420
+ if (!fs.existsSync(feedbackPath)) {
421
+ _feedbackWeightsCache = new Map();
422
+ _feedbackWeightsCacheTime = now;
423
+ return _feedbackWeightsCache;
424
+ }
425
+ var lines;
426
+ try {
427
+ lines = fs.readFileSync(feedbackPath, 'utf8').trim().split('\n').filter(Boolean);
428
+ } catch {
429
+ _feedbackWeightsCache = new Map();
430
+ _feedbackWeightsCacheTime = now;
431
+ return _feedbackWeightsCache;
432
+ }
433
+ var agentStats = {};
434
+ for (var i = 0; i < lines.length; i++) {
435
+ try {
436
+ var entry = JSON.parse(lines[i]);
437
+ var agent = entry.suggestedAgent || entry.agent;
438
+ if (!agent) continue;
439
+ if (!agentStats[agent]) agentStats[agent] = { total: 0, successTotal: 0, withSignal: 0 };
440
+ agentStats[agent].total++;
441
+ // Only count entries where intelligenceFeedback is non-null as having signal
442
+ var fb = entry.intelligenceFeedback;
443
+ if (fb !== null && fb !== undefined) {
444
+ agentStats[agent].withSignal++;
445
+ var success = fb === true || (fb && fb.success === true);
446
+ if (success) agentStats[agent].successTotal++;
447
+ }
448
+ } catch { /* skip malformed lines */ }
449
+ }
450
+ var weights = new Map();
451
+ for (var agentName in agentStats) {
452
+ var stats = agentStats[agentName];
453
+ // Only activate weight when we have enough non-null signal entries
454
+ if (stats.withSignal >= MIN_FEEDBACK_SAMPLES) {
455
+ var rate = stats.successTotal / stats.withSignal;
456
+ // Clamp to [0.5, 1.5]: bad agents get penalized, good agents boosted slightly
457
+ weights.set(agentName, Math.max(0.5, Math.min(1.5, rate * 2)));
458
+ }
459
+ }
460
+ _feedbackWeightsCache = weights;
461
+ _feedbackWeightsCacheTime = now;
462
+ return weights;
463
+ }
464
+
398
465
  // ─── Main routing ─────────────────────────────────────────────────────────────
399
466
  function routeTask(task) {
400
467
  if (typeof task !== 'string' || !task) {
@@ -419,14 +486,19 @@ function routeTask(task) {
419
486
  };
420
487
  }
421
488
 
489
+ var feedbackWeights = loadFeedbackWeights();
490
+
422
491
  // Dev task pattern matching
423
492
  for (const [pattern, agent] of Object.entries(TASK_PATTERNS)) {
424
493
  const regex = new RegExp(pattern, 'i');
425
494
  if (regex.test(taskLower)) {
495
+ // Apply feedback weight to confidence (defaults to 1.0 = no change)
496
+ var baseConfidence = 0.8;
497
+ var weight = feedbackWeights.get(agent) || 1.0;
426
498
  return {
427
499
  agent,
428
500
  agentSlug: agent,
429
- confidence: 0.8,
501
+ confidence: Math.min(1.0, baseConfidence * weight),
430
502
  reason: `Matched pattern: ${pattern}`,
431
503
  skillMatches: matchSkills(task),
432
504
  extrasMatches: [],
@@ -436,14 +508,16 @@ function routeTask(task) {
436
508
  }
437
509
 
438
510
  // Default — low confidence, show both skill and extras suggestions
511
+ var defaultAgent = 'coder';
512
+ var defaultWeight = feedbackWeights.get(defaultAgent) || 1.0;
439
513
  return {
440
- agent: 'coder',
441
- agentSlug: 'coder',
442
- confidence: 0.5,
514
+ agent: defaultAgent,
515
+ agentSlug: defaultAgent,
516
+ confidence: Math.min(1.0, 0.5 * defaultWeight),
443
517
  reason: 'Default routing - no specific pattern matched',
444
518
  skillMatches: matchSkills(task),
445
519
  extrasMatches: matchExtras(task),
446
- specificAgents: SPECIFIC_AGENTS_MAP['coder'] || [],
520
+ specificAgents: SPECIFIC_AGENTS_MAP[defaultAgent] || [],
447
521
  };
448
522
  }
449
523