clementine-agent 1.18.132 → 1.18.134

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.
@@ -98,7 +98,7 @@ export interface SelfImproveLoopOptions {
98
98
  allowAutoApplySafeFixes?: boolean;
99
99
  }
100
100
  export declare function classifyFailure(recentErrors: string[]): FixRecipe;
101
- export declare class SelfImproveLoop {
101
+ export declare class FailureFixConsumer {
102
102
  private readonly tickMs;
103
103
  private readonly triggersDir;
104
104
  private readonly pendingDir;
@@ -133,4 +133,4 @@ export declare class SelfImproveLoop {
133
133
  private processOne;
134
134
  private notifyAgent;
135
135
  }
136
- //# sourceMappingURL=self-improve-loop.d.ts.map
136
+ //# sourceMappingURL=failure-fix-consumer.d.ts.map
@@ -29,7 +29,7 @@ import path from 'node:path';
29
29
  import matter from 'gray-matter';
30
30
  import pino from 'pino';
31
31
  import { AGENTS_DIR, BASE_DIR, SYSTEM_DIR } from '../config.js';
32
- const logger = pino({ name: 'clementine.self-improve-loop' });
32
+ const logger = pino({ name: 'clementine.failure-fix-consumer' });
33
33
  /**
34
34
  * Fallback tick interval. The loop is primarily event-driven via fs.watch
35
35
  * on the triggers directory — this is just a slow safety net for cases
@@ -231,7 +231,7 @@ function writePendingChange(record, dir) {
231
231
  return file;
232
232
  }
233
233
  // ── Main loop ────────────────────────────────────────────────────────
234
- export class SelfImproveLoop {
234
+ export class FailureFixConsumer {
235
235
  tickMs;
236
236
  triggersDir;
237
237
  pendingDir;
@@ -503,4 +503,4 @@ export class SelfImproveLoop {
503
503
  }
504
504
  }
505
505
  }
506
- //# sourceMappingURL=self-improve-loop.js.map
506
+ //# sourceMappingURL=failure-fix-consumer.js.map
@@ -411,12 +411,18 @@ export class SelfImproveLoop {
411
411
  consecutiveLow++;
412
412
  continue;
413
413
  }
414
- // Diversity safety net: skip if hypothesis targets an over-represented area:target
414
+ // Diversity safety net: skip if hypothesis targets an over-represented area:target.
415
+ // 1.18.134 — loosened cap from 3 to 5. The old cap caused the
416
+ // loop to plateau immediately whenever it had ~3 ideas about
417
+ // SOUL.md (which is a frequent attractor). At 5 the
418
+ // hypothesizer gets a few more swings at the same area before
419
+ // diversity kicks in — but still avoids monomania.
420
+ const DIVERSITY_CAP = 5;
415
421
  const proposalKey = `${proposal.area}:${proposal.target}`;
416
422
  const proposalCount = history.filter(e => `${e.area}:${e.target}` === proposalKey).length
417
423
  + this.getPendingChanges().filter(p => `${p.area}:${p.target}` === proposalKey).length;
418
- if (proposalCount >= 3) {
419
- logger.warn({ area: proposal.area, target: proposal.target, count: proposalCount }, 'Hypothesis over-targeted — skipping');
424
+ if (proposalCount >= DIVERSITY_CAP) {
425
+ logger.warn({ area: proposal.area, target: proposal.target, count: proposalCount, cap: DIVERSITY_CAP }, 'Hypothesis over-targeted — skipping');
420
426
  consecutiveLow++;
421
427
  continue;
422
428
  }
@@ -440,8 +446,37 @@ export class SelfImproveLoop {
440
446
  const before = await this.readCurrentState(proposal.area, proposal.target);
441
447
  // Step 5: Evaluate
442
448
  const evaluation = await this.withTimeout(this.evaluate(before, proposal.proposedChange, proposal.hypothesis), 60_000);
443
- const score = evaluation?.score ?? 0;
444
- const normalizedScore = score / 10; // Convert 0-10 to 0-1
449
+ const llmScore = evaluation?.score ?? 0;
450
+ const normalizedLlmScore = llmScore / 10; // Convert 0-10 to 0-1
451
+ // 1.18.134 — blend the LLM evaluator score with an objective
452
+ // signal pulled from real metrics. Karpathy's autoresearch uses
453
+ // ONE objective metric (val_bpb); Clementine's LLM-only score
454
+ // can drift, especially when proposals affect SOUL.md or
455
+ // cosmetic prompt fields. The objective floor is computed from
456
+ // the baseline metrics gathered at the start of THIS run:
457
+ //
458
+ // objective = cronSuccessRate * feedbackPositiveRatio
459
+ //
460
+ // Both 0..1; product preserves the multiplicative penalty of
461
+ // either metric being weak. We weight 70% LLM / 30% objective
462
+ // — the LLM still drives most decisions, but a proposal can't
463
+ // sail through on a 9/10 evaluator score when feedback is at
464
+ // 35% positive (which is the current reality). Set the env
465
+ // var SELF_IMPROVE_OBJECTIVE_WEIGHT to override (0..1).
466
+ const objectiveWeightRaw = parseFloat(process.env.SELF_IMPROVE_OBJECTIVE_WEIGHT ?? '0.3');
467
+ const objectiveWeight = Number.isFinite(objectiveWeightRaw) && objectiveWeightRaw >= 0 && objectiveWeightRaw <= 1
468
+ ? objectiveWeightRaw : 0.3;
469
+ const llmWeight = 1 - objectiveWeight;
470
+ const objectiveScore = (state.baselineMetrics.cronSuccessRate || 0)
471
+ * (state.baselineMetrics.feedbackPositiveRatio || 0);
472
+ const normalizedScore = (normalizedLlmScore * llmWeight) + (objectiveScore * objectiveWeight);
473
+ const score = normalizedScore * 10; // For display + reason text
474
+ logger.debug({
475
+ llm: normalizedLlmScore.toFixed(3),
476
+ objective: objectiveScore.toFixed(3),
477
+ blended: normalizedScore.toFixed(3),
478
+ weight: objectiveWeight,
479
+ }, 'Score blend');
445
480
  const accepted = normalizedScore >= this.config.acceptThreshold;
446
481
  // Surface gate: even when accepted, only score >= surfaceThreshold
447
482
  // reaches the user's pending-changes inbox. Below that floor we
@@ -7047,6 +7047,57 @@ If the tool returns nothing or errors, return an empty array \`[]\`.`,
7047
7047
  res.status(500).json({ error: String(err) });
7048
7048
  }
7049
7049
  });
7050
+ // 1.18.132 — list top-level files in a registered project. Used by
7051
+ // the Skill Builder's Files sidebar tab to surface clickable file
7052
+ // paths that the user can insert into their skill body.
7053
+ // Hardened: the path query param MUST match a registered project's
7054
+ // path exactly (no path traversal, no scanning arbitrary directories).
7055
+ app.get('/api/projects/files', (req, res) => {
7056
+ try {
7057
+ const projPath = String(req.query.path ?? '');
7058
+ if (!projPath)
7059
+ return res.status(400).json({ ok: false, error: 'path query param required' });
7060
+ const projects = loadProjectsMeta();
7061
+ const found = projects.find((p) => p.path === projPath);
7062
+ if (!found)
7063
+ return res.status(404).json({ ok: false, error: 'path is not a registered project' });
7064
+ if (!existsSync(projPath))
7065
+ return res.json({ ok: true, files: [], note: 'project path does not exist on disk' });
7066
+ let entries;
7067
+ try {
7068
+ entries = readdirSync(projPath);
7069
+ }
7070
+ catch (err) {
7071
+ return res.status(500).json({ ok: false, error: 'failed to list directory: ' + String(err) });
7072
+ }
7073
+ // Skip hidden + node_modules + standard noise. Up to 50 entries
7074
+ // (the sidebar caps at 50 anyway). One level deep — the panel is
7075
+ // an entry point; users can dig further from their editor.
7076
+ const NOISE = new Set(['node_modules', '.git', '.DS_Store', 'dist', 'build', '.next', '.cache']);
7077
+ const out = [];
7078
+ for (const entry of entries.sort()) {
7079
+ if (entry.startsWith('.'))
7080
+ continue;
7081
+ if (NOISE.has(entry))
7082
+ continue;
7083
+ const abs = path.join(projPath, entry);
7084
+ let st;
7085
+ try {
7086
+ st = statSync(abs);
7087
+ }
7088
+ catch {
7089
+ continue;
7090
+ }
7091
+ out.push({ relPath: entry, isDir: st.isDirectory(), sizeBytes: st.isFile() ? st.size : 0 });
7092
+ if (out.length >= 50)
7093
+ break;
7094
+ }
7095
+ res.json({ ok: true, files: out });
7096
+ }
7097
+ catch (err) {
7098
+ res.status(500).json({ ok: false, error: String(err) });
7099
+ }
7100
+ });
7050
7101
  // ── Available Tools ──────────────────────────────────────────
7051
7102
  app.get('/api/available-tools', async (_req, res) => {
7052
7103
  try {
@@ -28178,6 +28229,7 @@ async function openSkillBuilder(skillName) {
28178
28229
  + '<span id="sb-save-status" style="font-size:11px;color:var(--text-muted)"></span>'
28179
28230
  + '</div>'
28180
28231
  + '<div style="display:flex;align-items:center;gap:6px">'
28232
+ + '<button onclick="sbRunSkillTest()" id="sb-test-btn" style="font-size:12px;padding:7px 12px;border:1px solid var(--green);border-radius:6px;background:transparent;color:var(--green);cursor:pointer;font-weight:500" title="Fire this skill once and stream the result inline (toast on completion)">▶ Test run</button>'
28181
28233
  + '<button onclick="sbSaveCurrent()" id="sb-save-btn" class="btn-primary" style="font-size:12px;padding:7px 14px;border:none;border-radius:6px;background:var(--accent);color:#fff;font-weight:500;cursor:pointer" disabled>Save (⌘S)</button>'
28182
28234
  + '<button onclick="closeSkillBuilder()" style="font-size:12px;padding:7px 12px;border:1px solid var(--border);border-radius:6px;background:transparent;color:var(--text-primary);cursor:pointer">Close</button>'
28183
28235
  + '</div>'
@@ -28203,6 +28255,8 @@ async function openSkillBuilder(skillName) {
28203
28255
  + '<div style="border-left:1px solid var(--border);background:var(--bg-secondary);display:flex;flex-direction:column;min-height:0">'
28204
28256
  + '<div style="padding:8px 6px 0;display:flex;gap:2px;border-bottom:1px solid var(--border)">'
28205
28257
  + '<button onclick="sbSwitchTab(\\x27tools\\x27)" id="sb-tab-tools" class="sb-tab" style="flex:1;font-size:11px;padding:8px 4px;border:none;background:transparent;color:var(--text-muted);cursor:pointer;border-bottom:2px solid transparent;font-weight:500">Tools</button>'
28258
+ + '<button onclick="sbSwitchTab(\\x27files\\x27)" id="sb-tab-files" class="sb-tab" style="flex:1;font-size:11px;padding:8px 4px;border:none;background:transparent;color:var(--text-muted);cursor:pointer;border-bottom:2px solid transparent;font-weight:500">Files</button>'
28259
+ + '<button onclick="sbSwitchTab(\\x27memory\\x27)" id="sb-tab-memory" class="sb-tab" style="flex:1;font-size:11px;padding:8px 4px;border:none;background:transparent;color:var(--text-muted);cursor:pointer;border-bottom:2px solid transparent;font-weight:500">Memory</button>'
28206
28260
  + '<button onclick="sbSwitchTab(\\x27skills\\x27)" id="sb-tab-skills" class="sb-tab" style="flex:1;font-size:11px;padding:8px 4px;border:none;background:transparent;color:var(--text-muted);cursor:pointer;border-bottom:2px solid transparent;font-weight:500">Skills</button>'
28207
28261
  + '</div>'
28208
28262
  + '<div style="padding:6px 10px;border-bottom:1px solid var(--border)">'
@@ -28389,10 +28443,10 @@ async function sbDeleteFile(relPath) {
28389
28443
  }
28390
28444
  }
28391
28445
 
28392
- // ── Sidebar tabs (Tools / Skills) ────────────────────────────────────
28446
+ // ── Sidebar tabs (Tools / Files / Memory / Skills) ──────────────────
28393
28447
  function sbSwitchTab(tab) {
28394
28448
  window._sbActiveTab = tab;
28395
- ['tools', 'skills'].forEach(function(t) {
28449
+ ['tools', 'files', 'memory', 'skills'].forEach(function(t) {
28396
28450
  var el = document.getElementById('sb-tab-' + t);
28397
28451
  if (el) {
28398
28452
  el.style.color = (t === tab) ? 'var(--accent)' : 'var(--text-muted)';
@@ -28408,6 +28462,10 @@ async function sbRenderSidebar() {
28408
28462
  var q = (document.getElementById('sb-sidebar-search')?.value || '').toLowerCase().trim();
28409
28463
  if (window._sbActiveTab === 'tools') {
28410
28464
  await sbRenderToolsTab(listEl, q);
28465
+ } else if (window._sbActiveTab === 'files') {
28466
+ await sbRenderFilesTab(listEl, q);
28467
+ } else if (window._sbActiveTab === 'memory') {
28468
+ await sbRenderMemoryTab(listEl, q);
28411
28469
  } else if (window._sbActiveTab === 'skills') {
28412
28470
  await sbRenderSkillsTab(listEl, q);
28413
28471
  }
@@ -28505,6 +28563,163 @@ async function sbRenderSkillsTab(listEl, q) {
28505
28563
  listEl.innerHTML = html;
28506
28564
  }
28507
28565
 
28566
+ // 1.18.132 — Files tab (Phase 2.5). Browses the user's projects.
28567
+ // Click a project to expand its file tree; click a file to insert
28568
+ // its absolute path at the cursor (so the skill body can reference
28569
+ // it via Read or Bash). Scoped to projectsData (already populated by
28570
+ // /api/projects on page load); no separate filesystem walk endpoint
28571
+ // needed in this iteration.
28572
+ async function sbRenderFilesTab(listEl, q) {
28573
+ var projects = (typeof projectsData !== 'undefined' && Array.isArray(projectsData)) ? projectsData : [];
28574
+ if (projects.length === 0) {
28575
+ try {
28576
+ var pr = await window.apiFetch('/api/projects');
28577
+ var pd = await pr.json();
28578
+ if (pd && Array.isArray(pd.projects)) projects = pd.projects;
28579
+ } catch (_) { /* fall through to empty */ }
28580
+ }
28581
+ if (q) {
28582
+ projects = projects.filter(function(p) {
28583
+ return ((p.name || '') + ' ' + (p.path || '')).toLowerCase().indexOf(q) > -1;
28584
+ });
28585
+ }
28586
+ if (projects.length === 0) {
28587
+ listEl.innerHTML = '<div style="padding:18px;color:var(--text-muted);font-size:11px;text-align:center">' + (q ? 'No matches.' : 'No projects yet. Add one in Settings → Projects.') + '</div>';
28588
+ return;
28589
+ }
28590
+ var html = '<div style="padding:6px 12px;font-size:10px;font-weight:600;color:var(--text-muted);text-transform:uppercase;letter-spacing:0.04em;background:var(--bg-tertiary)">Projects (' + projects.length + ')</div>';
28591
+ for (var i = 0; i < projects.length; i++) {
28592
+ var p = projects[i];
28593
+ // Click on row → insert the project path. Click on icon to load
28594
+ // a child file list inline (lazy expand).
28595
+ html += '<div style="border-bottom:1px solid var(--border-light)">'
28596
+ + '<div onclick="sbInsertAtCursor(\\x27' + jsStr(p.path) + '\\x27)" style="padding:7px 12px;cursor:pointer;font-size:12px;display:flex;align-items:center;gap:6px" onmouseover="this.style.background=\\x27var(--bg-tertiary)\\x27" onmouseout="this.style.background=\\x27transparent\\x27" title="Insert this absolute path at the cursor">'
28597
+ + '<span>📁</span>'
28598
+ + '<span style="font-weight:500;color:var(--text-primary);flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap">' + esc(p.name || p.path.split(\'/\').pop()) + '</span>'
28599
+ + '<button onclick="event.stopPropagation();sbToggleProjectFiles(\\x27' + jsStr(p.path) + '\\x27, this)" title="Browse files inside this project" style="background:none;border:1px solid var(--border);color:var(--text-muted);font-size:10px;padding:1px 6px;border-radius:3px;cursor:pointer">▸</button>'
28600
+ + '</div>'
28601
+ + '<div id="sb-files-children-' + i + '" data-project-path="' + esc(p.path) + '" style="display:none;font-size:11px;background:var(--bg-secondary);padding:4px 0"></div>'
28602
+ + '</div>';
28603
+ }
28604
+ listEl.innerHTML = html;
28605
+ }
28606
+
28607
+ // Lazy-load the child file list for a project. Hits a tiny endpoint
28608
+ // that returns the top-level files (max 50, no recursion) so we don't
28609
+ // blow the panel on huge projects.
28610
+ async function sbToggleProjectFiles(projectPath, btn) {
28611
+ // Find the children container next to this button
28612
+ var children = btn.parentElement.parentElement.querySelector('[data-project-path]');
28613
+ if (!children) return;
28614
+ if (children.style.display !== 'none') {
28615
+ children.style.display = 'none';
28616
+ btn.textContent = '▸';
28617
+ return;
28618
+ }
28619
+ children.style.display = '';
28620
+ btn.textContent = '▾';
28621
+ if (!children.dataset.loaded) {
28622
+ children.innerHTML = '<div style="padding:8px 24px;color:var(--text-muted);font-size:10px">Loading…</div>';
28623
+ try {
28624
+ var r = await window.apiFetch('/api/projects/files?path=' + encodeURIComponent(projectPath));
28625
+ var d = await r.json();
28626
+ if (!r.ok || !Array.isArray(d.files)) {
28627
+ children.innerHTML = '<div style="padding:8px 24px;color:var(--text-muted);font-size:10px">No files surfaced (or endpoint unavailable).</div>';
28628
+ return;
28629
+ }
28630
+ var html = '';
28631
+ for (var i = 0; i < Math.min(d.files.length, 50); i++) {
28632
+ var f = d.files[i];
28633
+ var icon = f.isDir ? '📁' : '📄';
28634
+ var fullPath = projectPath + '/' + f.relPath;
28635
+ html += '<div onclick="sbInsertAtCursor(\\x27' + jsStr(fullPath) + '\\x27)" style="padding:4px 24px;cursor:pointer;display:flex;align-items:center;gap:6px" onmouseover="this.style.background=\\x27var(--bg-tertiary)\\x27" onmouseout="this.style.background=\\x27transparent\\x27">'
28636
+ + '<span>' + icon + '</span>'
28637
+ + '<span style="flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;color:var(--text-secondary)">' + esc(f.relPath) + '</span>'
28638
+ + '</div>';
28639
+ }
28640
+ if (d.files.length > 50) {
28641
+ html += '<div style="padding:4px 24px;color:var(--text-muted);font-size:10px;font-style:italic">+ ' + (d.files.length - 50) + ' more (open the project in your editor for the rest)</div>';
28642
+ }
28643
+ children.innerHTML = html;
28644
+ children.dataset.loaded = '1';
28645
+ } catch (err) {
28646
+ children.innerHTML = '<div style="padding:8px 24px;color:var(--red);font-size:10px">' + esc(String(err)) + '</div>';
28647
+ }
28648
+ }
28649
+ }
28650
+
28651
+ // 1.18.132 — Memory tab (Phase 2.5). Surfaces three things the agent
28652
+ // can reach when the skill runs:
28653
+ // 1. Recent extractions (last facts the auto-extractor saved)
28654
+ // 2. Top-level MEMORY.md sections (h2 headers)
28655
+ // 3. A button to insert a memory_search call template
28656
+ // Click any item → inserts a contextual reference at the cursor so
28657
+ // the skill body can document or trigger a recall.
28658
+ async function sbRenderMemoryTab(listEl, q) {
28659
+ var html = '';
28660
+ // 1. Recent extractions
28661
+ try {
28662
+ var r = await window.apiFetch('/api/memory/writes/recent?limit=12');
28663
+ var d = await r.json();
28664
+ var writes = (d && Array.isArray(d.writes)) ? d.writes : (d && Array.isArray(d.entries) ? d.entries : []);
28665
+ if (q) {
28666
+ writes = writes.filter(function(w) {
28667
+ return JSON.stringify(w).toLowerCase().indexOf(q) > -1;
28668
+ });
28669
+ }
28670
+ if (writes.length > 0) {
28671
+ html += '<div style="padding:6px 12px;font-size:10px;font-weight:600;color:var(--text-muted);text-transform:uppercase;letter-spacing:0.04em;background:var(--bg-tertiary)">Recent extractions</div>';
28672
+ for (var i = 0; i < Math.min(writes.length, 8); i++) {
28673
+ var w = writes[i];
28674
+ var preview = (w.content_preview || w.content || w.preview || w.text || '').slice(0, 100);
28675
+ if (!preview) continue;
28676
+ var insertion = 'Reference recent fact: "' + preview.replace(/"/g, '\\\\\\\\"').slice(0, 80) + '"';
28677
+ html += '<div onclick="sbInsertAtCursor(\\x27' + jsStr(insertion) + '\\x27)" style="padding:7px 12px;cursor:pointer;font-size:12px;border-bottom:1px solid var(--border-light)" onmouseover="this.style.background=\\x27var(--bg-tertiary)\\x27" onmouseout="this.style.background=\\x27transparent\\x27">'
28678
+ + '<div style="color:var(--text-secondary);line-height:1.4;font-size:11px">' + esc(preview) + (preview.length >= 100 ? '…' : '') + '</div>'
28679
+ + '</div>';
28680
+ }
28681
+ }
28682
+ } catch (_) { /* memory writes endpoint may not be live in early daemons */ }
28683
+ // 2. MEMORY.md slot headers
28684
+ try {
28685
+ var r2 = await window.apiFetch('/api/memory/md');
28686
+ var d2 = await r2.json();
28687
+ var content = (d2 && d2.content) || '';
28688
+ var lines = content.split('\\n');
28689
+ var sections = [];
28690
+ for (var li = 0; li < lines.length; li++) {
28691
+ var match = lines[li].match(/^##\\s+(.+)$/);
28692
+ if (match) sections.push(match[1].trim());
28693
+ }
28694
+ if (q) {
28695
+ sections = sections.filter(function(s) { return s.toLowerCase().indexOf(q) > -1; });
28696
+ }
28697
+ if (sections.length > 0) {
28698
+ html += '<div style="padding:6px 12px;font-size:10px;font-weight:600;color:var(--text-muted);text-transform:uppercase;letter-spacing:0.04em;background:var(--bg-tertiary);margin-top:4px">MEMORY.md sections</div>';
28699
+ for (var si = 0; si < sections.length; si++) {
28700
+ var s = sections[si];
28701
+ var ins = 'Refer to MEMORY.md > "' + s + '" section.';
28702
+ html += '<div onclick="sbInsertAtCursor(\\x27' + jsStr(ins) + '\\x27)" style="padding:6px 12px;cursor:pointer;font-size:12px;display:flex;align-items:center;gap:6px;border-bottom:1px solid var(--border-light)" onmouseover="this.style.background=\\x27var(--bg-tertiary)\\x27" onmouseout="this.style.background=\\x27transparent\\x27">'
28703
+ + '<span>📄</span>'
28704
+ + '<span style="color:var(--text-primary)">' + esc(s) + '</span>'
28705
+ + '</div>';
28706
+ }
28707
+ }
28708
+ } catch (_) { /* defensive */ }
28709
+ // 3. Always-available memory_search insert
28710
+ html += '<div style="padding:6px 12px;font-size:10px;font-weight:600;color:var(--text-muted);text-transform:uppercase;letter-spacing:0.04em;background:var(--bg-tertiary);margin-top:4px">Patterns</div>';
28711
+ html += '<div onclick="sbInsertAtCursor(\\x27Use memory_search with query: \\\\\\\"YOUR_QUERY\\\\\\\".\\x27)" style="padding:7px 12px;cursor:pointer;font-size:12px;border-bottom:1px solid var(--border-light)" onmouseover="this.style.background=\\x27var(--bg-tertiary)\\x27" onmouseout="this.style.background=\\x27transparent\\x27">'
28712
+ + '<span style="font-family:ui-monospace,SFMono-Regular,Menlo,monospace;color:var(--text-primary)">memory_search</span>'
28713
+ + '<span style="color:var(--text-muted);font-size:10px;margin-left:8px">Recall facts/transcripts at runtime</span>'
28714
+ + '</div>';
28715
+ html += '<div onclick="sbInsertAtCursor(\\x27Use memory_write to save this fact: \\\\\\\"FACT\\\\\\\" with reason \\\\\\\"WHY_IT_MATTERS\\\\\\\".\\x27)" style="padding:7px 12px;cursor:pointer;font-size:12px;border-bottom:1px solid var(--border-light)" onmouseover="this.style.background=\\x27var(--bg-tertiary)\\x27" onmouseout="this.style.background=\\x27transparent\\x27">'
28716
+ + '<span style="font-family:ui-monospace,SFMono-Regular,Menlo,monospace;color:var(--text-primary)">memory_write</span>'
28717
+ + '<span style="color:var(--text-muted);font-size:10px;margin-left:8px">Persist a new fact</span>'
28718
+ + '</div>';
28719
+ if (!html.trim()) html = '<div style="padding:18px;color:var(--text-muted);font-size:11px;text-align:center">No memory items surfaced.</div>';
28720
+ listEl.innerHTML = html;
28721
+ }
28722
+
28508
28723
  function sbInsertAtCursor(text) {
28509
28724
  var ed = document.getElementById('sb-editor');
28510
28725
  if (!ed) return;
@@ -28518,6 +28733,38 @@ function sbInsertAtCursor(text) {
28518
28733
  sbOnEdit();
28519
28734
  }
28520
28735
 
28736
+ // 1.18.132 — Test runner. Fires the open skill once via the same
28737
+ // /api/cron/run/:name path that the Skills detail Run-now button
28738
+ // uses (which itself works for unscheduled skills via cmdCronRun's
28739
+ // catalog fallback). Refuses to run if the file has unsaved changes
28740
+ // (otherwise the user would be testing the saved version, not what
28741
+ // they see in the editor — confusing).
28742
+ async function sbRunSkillTest() {
28743
+ if (window._sbState.dirty) {
28744
+ if (!confirm('Save current changes before testing? Unsaved edits won\\x27t be in the run.')) return;
28745
+ await sbSaveCurrent();
28746
+ if (window._sbState.dirty) return; // save failed
28747
+ }
28748
+ var name = window._sbState.skillName;
28749
+ if (!name) return;
28750
+ var btn = document.getElementById('sb-test-btn');
28751
+ if (btn) { btn.disabled = true; btn.textContent = '⏳ Running…'; btn.style.color = 'var(--text-muted)'; btn.style.borderColor = 'var(--border)'; }
28752
+ try {
28753
+ var r = await window.apiFetch('/api/cron/run/' + encodeURIComponent(name), { method: 'POST' });
28754
+ if (r.status === 409) {
28755
+ toast('Already running. Wait for the in-flight run to finish.', 'warn');
28756
+ return;
28757
+ }
28758
+ var d = await r.json();
28759
+ if (!r.ok) { toast(d.error || 'Run failed', 'error'); return; }
28760
+ toast('Started "' + name + '" — output streams to chat. Close the builder to see it.', 'success');
28761
+ } catch (err) {
28762
+ toast('Failed: ' + err, 'error');
28763
+ } finally {
28764
+ if (btn) { btn.disabled = false; btn.textContent = '▶ Test run'; btn.style.color = 'var(--green)'; btn.style.borderColor = 'var(--green)'; }
28765
+ }
28766
+ }
28767
+
28521
28768
  async function _openSkillModal(opts) {
28522
28769
  opts = opts || {};
28523
28770
  var existing = null;
package/dist/index.js CHANGED
@@ -754,11 +754,14 @@ async function asyncMain() {
754
754
  // output to their Discord channel.
755
755
  const { AgentHeartbeatManager } = await import('./gateway/agent-heartbeat-manager.js');
756
756
  const agentHeartbeats = new AgentHeartbeatManager(gateway.getAgentManager(), gateway);
757
- // Self-improve loopcloses the gap between "trigger written" and
758
- // "fix applied." Every 10 min, scans self-improve/triggers/, classifies
759
- // failures, auto-applies safe cron-config fixes, escalates risky ones.
760
- const { SelfImproveLoop } = await import('./agent/self-improve-loop.js');
761
- const selfImproveLoop = new SelfImproveLoop(dispatcher);
757
+ // Failure-fix consumer (1.18.134 renamed from "self-improve-loop"
758
+ // to disambiguate from the Karpathy autoresearch SelfImproveLoop in
759
+ // src/agent/self-improve.ts). Every 10 min, scans
760
+ // self-improve/triggers/, classifies failures, auto-applies safe
761
+ // cron-config fixes, escalates risky ones. Different concern from
762
+ // the autoresearch hypothesize/evaluate loop.
763
+ const { FailureFixConsumer } = await import('./agent/failure-fix-consumer.js');
764
+ const failureFixConsumer = new FailureFixConsumer(dispatcher);
762
765
  // ── Build channel tasks ──────────────────────────────────────────
763
766
  const channelTasks = [];
764
767
  const activeChannels = [];
@@ -856,7 +859,38 @@ async function asyncMain() {
856
859
  heartbeat.start();
857
860
  cronScheduler.start();
858
861
  agentHeartbeats.start();
859
- selfImproveLoop.start();
862
+ failureFixConsumer.start();
863
+ // 1.18.134 — nightly Karpathy-autoresearch self-improve trigger.
864
+ // The Karpathy SelfImproveLoop (src/agent/self-improve.ts) was
865
+ // previously only triggered by /self-improve run or CLI. With no
866
+ // automatic schedule the loop ran ~3 times in the prior 4 days and
867
+ // sat plateaued. This wires a daily 3am trigger so it iterates on
868
+ // its own — matching Karpathy's continuous-iteration model.
869
+ // SELF_IMPROVE_HOUR env var overrides (0–23, default 3).
870
+ const selfImproveHour = (() => {
871
+ const raw = parseInt(process.env.SELF_IMPROVE_HOUR ?? '3', 10);
872
+ if (Number.isFinite(raw) && raw >= 0 && raw <= 23)
873
+ return raw;
874
+ return 3;
875
+ })();
876
+ // node-cron is already a dependency (used by cron-scheduler). Schedule
877
+ // a single daily tick — the SelfImproveLoop's own time/iteration caps
878
+ // and plateau detection bound the work; we don't need finer granularity.
879
+ try {
880
+ const nodeCron = (await import('node-cron')).default;
881
+ nodeCron.schedule(`0 ${selfImproveHour} * * *`, () => {
882
+ logger.info({ hour: selfImproveHour }, 'Nightly self-improve trigger firing');
883
+ gateway.handleSelfImprove('run').then((summary) => {
884
+ logger.info({ summary }, 'Nightly self-improve trigger complete');
885
+ }).catch((err) => {
886
+ logger.error({ err }, 'Nightly self-improve trigger failed');
887
+ });
888
+ }, { timezone: process.env.TZ || 'America/Los_Angeles' });
889
+ logger.info({ hour: selfImproveHour }, `Self-improve nightly trigger scheduled (${selfImproveHour}:00 daily)`);
890
+ }
891
+ catch (err) {
892
+ logger.warn({ err }, 'Failed to schedule nightly self-improve trigger');
893
+ }
860
894
  // Background-task hygiene: any task left in 'running' is from a prior
861
895
  // process. Mark them aborted so the lifecycle is honest. (P6b will add
862
896
  // resumability; for now fail-fast is clearer than silently re-running.)
@@ -1086,7 +1120,7 @@ async function asyncMain() {
1086
1120
  heartbeat.stop();
1087
1121
  cronScheduler.stop();
1088
1122
  agentHeartbeats.stop();
1089
- selfImproveLoop.stop();
1123
+ failureFixConsumer.stop();
1090
1124
  // ── Self-restart (enhanced with health check + rollback) ────────
1091
1125
  if (restartRequested) {
1092
1126
  // Clear our PID file BEFORE spawning the child, so ensureSingleton()
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "clementine-agent",
3
- "version": "1.18.132",
3
+ "version": "1.18.134",
4
4
  "description": "Clementine — Personal AI Assistant (TypeScript)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",