Skill: josephtandle/local-self-healing-machine-learning (version 2.0.1)

--- name: local-self-healing-machine-learning description: A fully local machine learning engine that makes your OpenClaw agent smart over time — without ever calling home, revealing your machine ID, or exposing any security holes. tags: [machine-learning, self-healing, ai, core, local, embeddings] author: Joe Che (https://mastermindshq.business) --- # Local Self-Healing Machine Learning **"Your agent learns from its own mistakes — without ever calling home, revealing your machine ID, or exposing any security holes."** A fully local machine learning engine that makes your OpenClaw agent smart over time. It watches your agent's runtime history, detects recurring failures, clusters similar errors using semantic embeddings, and autonomously evolves fix strategies — all running 100% on your machine with zero network calls. The engine uses a feedback loop that tracks whether each fix actually works: after 3 clean cycles a fix is marked "proven", and if the error comes back within 5 cycles it's marked "failed". A k-NN predictor learns from these outcomes and gets better at picking the right fix over time. Lessons compound in a persistent knowledge base that never decays — the longer it runs, the smarter it gets. Every evolution is auditable through the GEP (Genetic Evolution Protocol), which produces structured, content-hashed assets: genes (reusable fix strategies), capsules (successful evolution records), and an append-only event log. You can inspect exactly what changed, why it changed, and whether it worked. No telemetry. No fingerprinting. No cloud dependencies. No data leaves your device. ## ML Capabilities - **Feedback Loop**: Tracks whether fixes actually work. After 3 clean cycles, a fix is "proven". If the error recurs within 5 cycles, the fix is marked "failed". - **Embedding-Based Error Clustering**: Uses Ollama + llama3.2:3b to generate semantic embeddings for error messages. Similar errors are clustered together instead of matched by regex. - **Success Predictor**: k-NN classifier trained on feedback data. Predicts which gene will fix a given error cluster. Gets better over time. - **Persistent Knowledge Base**: Lessons compound forever. No decay. Confidence scores adjust with each outcome. ## Dashboard View your ML engine's status, training progress, and knowledge base in a local web dashboard: ```bash node index.js --dashboard ``` Opens at `http://localhost:8420`. Shows feedback loop stats, predictor training progress, error clusters, knowledge base health, and recent evolution events. No external dependencies — runs entirely in your browser. ## Optional: Ollama Integration For semantic error matching (recommended but not required): ```bash # Install Ollama curl -fsSL https://ollama.com/install.sh | sh # Pull the embedding model ollama pull llama3.2:3b ``` Without Ollama, the engine falls back to regex-based heuristics. Everything still works — you just get smarter matching with it. ## Usage ### Standard Run (Automated) ```bash node index.js ``` ### Review Mode (Human-in-the-Loop) ```bash node index.js --review ``` ### Continuous Loop ```bash node index.js --loop ``` ## Configuration | Environment Variable | Default | Description | |---|---|---| | `EVOLVE_ALLOW_SELF_MODIFY` | `false` | Allow evolution to modify its own source code. **Not recommended.** | | `EVOLVE_LOAD_MAX` | `2.0` | Maximum 1-minute load average before backing off. | | `EVOLVE_STRATEGY` | `balanced` | Strategy: `balanced`, `innovate`, `harden`, `repair-only`, `early-stabilize`, `steady-state`, or `auto`. | | `OLLAMA_URL` | `http://localhost:11434` | Ollama API endpoint for embeddings. | | `OLLAMA_EMBED_MODEL` | `llama3.2:3b` | Model to use for embeddings. | | `LSHML_DASHBOARD_PORT` | `8420` | Port for the standalone dashboard server. | ## How It Works 1. **Signal Extraction**: Scans logs for errors, feature requests, performance issues (19 signal types, 4 languages) 2. **ML Clustering**: Groups similar errors using embedding vectors (or regex fallback) 3. **Gene Selection**: Picks the best fix strategy using knowledge base + k-NN predictor 4. **Evolution**: Applies the fix with blast radius protection, validation, and rollback 5. **Feedback**: Monitors subsequent cycles to verify the fix holds 6. **Learning**: Records outcomes to knowledge base — proven fixes get higher confidence ## Data Files All data stays local in `memory/`: | File | Purpose | |---|---| | `feedback.jsonl` | Fix outcome tracking (append-only) | | `embeddings-cache.json` | Cached embedding vectors | | `knowledge.json` | Persistent lessons (no decay) | | `predictor.json` | Trained model weights | | `cluster-registry.json` | Semantic error cluster map | ## GEP Protocol (Auditable Evolution) Every evolution produces structured, auditable assets: - `assets/gep/genes.json`: Reusable fix strategies - `assets/gep/capsules.json`: Successful evolution records - `assets/gep/events.jsonl`: Append-only audit trail ## Safety - Blast radius limits (max files/lines changed per cycle) - Critical path protection (cannot modify itself or core configs) - Validation commands run before committing - Canary check (index.js must still load) - Ethics committee (blocks dangerous patterns) - Full rollback on any failure ## Author Built by [Joe Che](https://mastermindshq.business) ## License MIT

path: dashboard/index.html
content: <!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>LSHML Dashboard</title>
<style>
  *, *::before, *::after { box-sizing: border-box; margin: 0; padding: 0; }
  :root {
    --bg: #0d1117; --surface: #161b22; --surface2: #21262d; --border: #30363d;
    --text: #c9d1d9; --text-dim: #8b949e; --text-bright: #f0f6fc;
    --accent: #58a6ff; --green: #3fb950; --red: #f85149; --yellow: #d29922;
    --purple: #bc8cff; --orange: #f0883e;
    --grad1: linear-gradient(135deg, #3d2e6b 0%, #251c42 100%);
    --grad2: linear-gradient(135deg, #5a4a8e 0%, #352d5a 100%);
    --grad3: linear-gradient(135deg, #6b5aaa 0%, #3d2e6b 100%);
    --grad4: linear-gradient(135deg, #6e3a5a 0%, #422238 100%);
    --grad5: linear-gradient(135deg, #7a4a60 0%, #4a2a3a 100%);
  }
  body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Helvetica, Arial, sans-serif; background: var(--bg); color: var(--text); min-height: 100vh; padding: 0; }
  .container { max-width: 1280px; margin: 0 auto; padding: 24px; }

  /* Header */
  .header { display: flex; align-items: center; justify-content: space-between; margin-bottom: 24px; flex-wrap: wrap; gap: 12px; }
  .header h1 { font-size: 22px; font-weight: 600; color: var(--text-bright); }
  .header h1 span { color: var(--accent); font-weight: 400; font-size: 14px; margin-left: 8px; }
  .header-controls { display: flex; align-items: center; gap: 16px; }
  .refresh-info { font-size: 12px; color: var(--text-dim); }
  .toggle-wrap { display: flex; align-items: center; gap: 6px; font-size: 13px; color: var(--text-dim); cursor: pointer; }
  .toggle-wrap input { accent-color: var(--accent); cursor: pointer; }
  .status-dot { width: 8px; height: 8px; border-radius: 50%; display: inline-block; }
  .status-dot.live { background: var(--green); box-shadow: 0 0 6px var(--green); }
  .status-dot.off { background: var(--text-dim); }

  /* Stat Cards */
  .stat-cards { display: grid; grid-template-columns: repeat(auto-fit, minmax(200px, 1fr)); gap: 16px; margin-bottom: 24px; }
  .stat-card { border-radius: 12px; padding: 20px; border: 1px solid var(--border); }
  .stat-card .label { font-size: 12px; text-transform: uppercase; letter-spacing: 0.5px; color: var(--text-dim); margin-bottom: 8px; }
  .stat-card .value { font-size: 28px; font-weight: 700; color: var(--text-bright); }
  .stat-card .sub { font-size: 12px; color: var(--text-dim); margin-top: 4px; }
  .stat-card:nth-child(1) { background: var(--grad1); }
  .stat-card:nth-child(2) { background: var(--grad2); }
  .stat-card:nth-child(3) { background: var(--grad3); }
  .stat-card:nth-child(4) { background: var(--grad4); }
  .stat-card:nth-child(5) { background: var(--grad5); }

  /* Sections */
  .section { background: var(--surface); border: 1px solid var(--border); border-radius: 12px; padding: 20px; margin-bottom: 20px; }
  .section h2 { font-size: 16px; font-weight: 600; color: var(--text-bright); margin-bottom: 16px; display: flex; align-items: center; gap: 8px; }
  .section h2 .badge { font-size: 11px; background: var(--surface2); color: var(--text-dim); padding: 2px 8px; border-radius: 10px; font-weight: 400; }

  /* Grid layout for sections */
  .grid-2 { display: grid; grid-template-columns: 1fr 1fr; gap: 20px; }
  @media (max-width: 768px) { .grid-2 { grid-template-columns: 1fr; } }

  /* Progress bar */
  .progress-bar { background: var(--surface2); border-radius: 8px; height: 24px; overflow: hidden; display: flex; }
  .progress-bar .seg { height: 100%; transition: width 0.5s ease; display: flex; align-items: center; justify-content: center; font-size: 11px; font-weight: 600; color: var(--bg); min-width: 0; }
  .seg.proven { background: var(--green); }
  .seg.pending { background: var(--yellow); }
  .seg.failed { background: var(--red); }

  .progress-bar-single { background: var(--surface2); border-radius: 8px; height: 20px; overflow: hidden; }
  .progress-bar-single .fill { height: 100%; border-radius: 8px; transition: width 0.5s ease; background: var(--accent); }

  /* KV rows */
  .kv-row { display: flex; justify-content: space-between; padding: 8px 0; border-bottom: 1px solid var(--border); font-size: 13px; }
  .kv-row:last-child { border-bottom: none; }
  .kv-row .k { color: var(--text-dim); }
  .kv-row .v { color: var(--text-bright); font-weight: 500; }
  .kv-row .v.green { color: var(--green); }
  .kv-row .v.red { color: var(--red); }
  .kv-row .v.yellow { color: var(--yellow); }
  .kv-row .v.purple { color: var(--purple); }

  /* Tables */
  table { width: 100%; border-collapse: collapse; font-size: 13px; }
  th { text-align: left; padding: 8px 10px; color: var(--text-dim); font-weight: 500; border-bottom: 1px solid var(--border); font-size: 11px; text-transform: uppercase; letter-spacing: 0.5px; }
  td { padding: 8px 10px; border-bottom: 1px solid var(--border); color: var(--text); }
  tr:last-child td { border-bottom: none; }
  tr:hover td { background: var(--surface2); }

  /* Tags */
  .tag { display: inline-block; padding: 2px 8px; border-radius: 6px; font-size: 11px; font-weight: 500; }
  .tag.success { background: rgba(63, 185, 80, 0.15); color: var(--green); }
  .tag.warn { background: rgba(210, 153, 34, 0.15); color: var(--yellow); }
  .tag.error { background: rgba(248, 81, 73, 0.15); color: var(--red); }
  .tag.info { background: rgba(88, 166, 255, 0.15); color: var(--accent); }
  .tag.purple { background: rgba(188, 140, 255, 0.15); color: var(--purple); }

  /* Event list */
  .event-item { padding: 10px 12px; border-bottom: 1px solid var(--border); font-size: 13px; }
  .event-item:last-child { border-bottom: none; }
  .event-item .event-header { display: flex; align-items: center; gap: 8px; margin-bottom: 4px; }
  .event-item .event-time { color: var(--text-dim); font-size: 11px; font-family: monospace; }
  .event-item .event-detail { color: var(--text-dim); font-size: 12px; }

  /* Empty state */
  .empty { text-align: center; padding: 24px; color: var(--text-dim); font-size: 13px; }

  /* Settings */
  .setting-row { display: flex; align-items: center; gap: 12px; padding: 8px 0; border-bottom: 1px solid var(--border); font-size: 13px; }
  .setting-row:last-child { border-bottom: none; }
  .setting-row .setting-key { color: var(--accent); font-family: monospace; font-size: 12px; min-width: 200px; }
  .setting-row .setting-val { color: var(--text); font-family: monospace; font-size: 12px; word-break: break-all; }

  .loading { text-align: center; padding: 48px; color: var(--text-dim); font-size: 15px; }
  .footer { text-align: center; padding: 24px 0 12px; font-size: 12px; color: var(--text-dim); }
  .footer a { color: var(--accent); text-decoration: none; }
  .footer a:hover { text-decoration: underline; }
</style>
</head>
<body>
<div class="container" id="app">
  <div class="loading">Loading dashboard data...</div>
</div>
<script>
(function() {
  let autoRefresh = true;
  let refreshTimer = null;
  let lastData = null;

  function fmt(d) {
    if (!d) return '--';
    try {
      const dt = new Date(d);
      return dt.toLocaleDateString() + ' ' + dt.toLocaleTimeString([], {hour:'2-digit',minute:'2-digit'});
    } catch { return d; }
  }

  function fmtBytes(b) {
    if (b < 1024) return b + ' B';
    if (b < 1048576) return (b/1024).toFixed(1) + ' KB';
    return (b/1048576).toFixed(1) + ' MB';
  }

  function pct(n, d) { return d > 0 ? Math.round((n/d)*100) : 0; }

  function escHtml(s) {
    if (typeof s !== 'string') s = String(s == null ? '' : s);
    return s.replace(/&/g,'&amp;').replace(/</g,'&lt;').replace(/>/g,'&gt;');
  }

  function render(data) {
    lastData = data;
    const fb = data.feedback || [];
    const proven = fb.filter(f => f.status === 'proven').length;
    const pending = fb.filter(f => f.status === 'pending').length;
    const failed = fb.filter(f => f.status === 'failed').length;
    const fbTotal = fb.length;
    const successRate = fbTotal > 0 ? pct(proven, proven + failed) : 0;

    const lessons = (data.knowledge && data.knowledge.lessons) || [];
    const lessonCount = lessons.length;
    const avgConf = lessonCount > 0 ? (lessons.reduce((s,l) => s + (l.confidence || 0), 0) / lessonCount) : 0;

    // Improvement trend
    let trend = 'stable';
    if (lessonCount >= 3) {
      const sorted = [...lessons].sort((a,b) => (a.created_at||'').localeCompare(b.created_at||''));
      const recent = sorted.slice(-3);
      const older = sorted.slice(0, Math.max(1, Math.floor(sorted.length/2)));
      const recentAvg = recent.reduce((s,l) => s + (l.confidence||0), 0) / recent.length;
      const olderAvg = older.reduce((s,l) => s + (l.confidence||0), 0) / older.length;
      if (recentAvg > olderAvg + 0.05) trend = 'improving';
      else if (recentAvg < olderAvg - 0.05) trend = 'declining';
    }

    const pred = data.predictor;
    const predSamples = pred ? (pred.sample_count || (pred.samples ? pred.samples.length : 0)) : 0;
    const predReady = predSamples >= 20;
    const predStatus = predReady ? 'Ready' : 'Warming Up';

    const genes = data.genes || [];
    const clusters = data.clusters || [];

    // Top genes by usage in lessons
    const geneUsage = {};
    lessons.forEach(l => { geneUsage[l.gene_id] = (geneUsage[l.gene_id]||0) + (l.times_applied||0); });
    const topGenes = Object.entries(geneUsage).sort((a,b) => b[1]-a[1]).slice(0, 8);

    // Events (only EvolutionEvent type)
    const events = (data.recentEvents || []).filter(e => e.type === 'EvolutionEvent').reverse();

    const trendClass = trend === 'improving' ? 'green' : trend === 'declining' ? 'red' : 'yellow';
    const trendIcon = trend === 'improving' ? '&#9650;' : trend === 'declining' ? '&#9660;' : '&#9644;';

    const app = document.getElementById('app');
    app.innerHTML = `
      <div class="header">
        <h1>Local Self-Healing Machine Learning <span>v${escHtml(data.version)}</span></h1>
        <div class="header-controls">
          <span class="refresh-info" id="refresh-info">Updated: ${fmt(data.timestamp)}</span>
          <label class="toggle-wrap">
            <span class="status-dot ${autoRefresh ? 'live' : 'off'}"></span>
            <input type="checkbox" id="auto-refresh-toggle" ${autoRefresh ? 'checked' : ''}>
            Auto-refresh (30s)
          </label>
        </div>
      </div>

      <div class="stat-cards">
        <div class="stat-card">
          <div class="label">Genes</div>
          <div class="value">${genes.length}</div>
          <div class="sub">${data.capsules ? data.capsules.length : 0} capsules</div>
        </div>
        <div class="stat-card">
          <div class="label">Success Rate</div>
          <div class="value">${successRate}%</div>
          <div class="sub">${proven} proven / ${proven+failed} resolved</div>
        </div>
        <div class="stat-card">
          <div class="label">Lessons Learned</div>
          <div class="value">${lessonCount}</div>
          <div class="sub">Avg confidence: ${(avgConf*100).toFixed(0)}%</div>
        </div>
        <div class="stat-card">
          <div class="label">Fixes Tracked</div>
          <div class="value">${fbTotal}</div>
          <div class="sub">${pending} pending</div>
        </div>
        <div class="stat-card">
          <div class="label">Predictor</div>
          <div class="value">${predStatus}</div>
          <div class="sub">${predSamples}/20 samples</div>
        </div>
      </div>

      <div class="grid-2">
        <div class="section">
          <h2>Feedback Loop <span class="badge">${fbTotal} entries</span></h2>
          ${fbTotal > 0 ? `
            <div class="progress-bar" style="margin-bottom:12px">
              ${proven > 0 ? `<div class="seg proven" style="width:${pct(proven,fbTotal)}%">${proven}</div>` : ''}
              ${pending > 0 ? `<div class="seg pending" style="width:${pct(pending,fbTotal)}%">${pending}</div>` : ''}
              ${failed > 0 ? `<div class="seg failed" style="width:${pct(failed,fbTotal)}%">${failed}</div>` : ''}
            </div>
            <div class="kv-row"><span class="k">Proven</span><span class="v green">${proven}</span></div>
            <div class="kv-row"><span class="k">Pending</span><span class="v yellow">${pending}</span></div>
            <div class="kv-row"><span class="k">Failed</span><span class="v red">${failed}</span></div>
            <div class="kv-row"><span class="k">Success Rate</span><span class="v">${successRate}%</span></div>
          ` : '<div class="empty">No feedback data yet. Run the evolver to generate feedback.</div>'}
        </div>

        <div class="section">
          <h2>Knowledge Base <span class="badge">${lessonCount} lessons</span></h2>
          <div class="kv-row"><span class="k">Total Lessons</span><span class="v">${lessonCount}</span></div>
          <div class="kv-row"><span class="k">Avg Confidence</span><span class="v">${(avgConf*100).toFixed(1)}%</span></div>
          <div class="kv-row"><span class="k">Trend</span><span class="v ${trendClass}">${trendIcon} ${trend}</span></div>
          <div class="kv-row"><span class="k">Embeddings Cache</span><span class="v">${fmtBytes(data.embeddingsCacheSize || 0)}</span></div>
          ${topGenes.length > 0 ? `
            <h2 style="margin-top:16px">Top Genes <span class="badge">by usage</span></h2>
            <table>
              <thead><tr><th>Gene ID</th><th>Applications</th></tr></thead>
              <tbody>${topGenes.map(([id,count]) => `<tr><td><code>${escHtml(id)}</code></td><td>${count}</td></tr>`).join('')}</tbody>
            </table>
          ` : ''}
        </div>
      </div>

      <div class="grid-2">
        <div class="section">
          <h2>Predictor <span class="badge">${predReady ? 'ready' : 'warming up'}</span></h2>
          <div class="kv-row"><span class="k">Status</span><span class="v ${predReady ? 'green' : 'yellow'}">${predStatus}</span></div>
          <div class="kv-row"><span class="k">Samples</span><span class="v">${predSamples} / 20</span></div>
          <div style="margin:12px 0">
            <div class="progress-bar-single">
              <div class="fill" style="width:${Math.min(100, pct(predSamples, 20))}%; background:${predReady ? 'var(--green)' : 'var(--accent)'}"></div>
            </div>
          </div>
          ${pred && pred.trained_at ? `<div class="kv-row"><span class="k">Last Trained</span><span class="v">${fmt(pred.trained_at)}</span></div>` : ''}
          ${pred && pred.version ? `<div class="kv-row"><span class="k">Model Version</span><span class="v">${pred.version}</span></div>` : ''}
        </div>

        <div class="section">
          <h2>Error Clusters <span class="badge">${clusters.length} clusters</span></h2>
          ${clusters.length > 0 ? `
            <table>
              <thead><tr><th>Label</th><th>Errors</th><th>First Seen</th><th>Last Seen</th></tr></thead>
              <tbody>${clusters.map(c => `
                <tr>
                  <td>${escHtml(c.label || c.id)}</td>
                  <td>${c.error_count || 0}</td>
                  <td style="font-size:11px">${fmt(c.first_seen)}</td>
                  <td style="font-size:11px">${fmt(c.last_seen)}</td>
                </tr>
              `).join('')}</tbody>
            </table>
          ` : '<div class="empty">No error clusters detected yet.</div>'}
        </div>
      </div>

      <div class="grid-2">
        <div class="section">
          <h2>Settings <span class="badge">read-only</span></h2>
          <div class="setting-row"><span class="setting-key">EVOLVE_STRATEGY</span><span class="setting-val">${escHtml(data.env.EVOLVE_STRATEGY)}</span></div>
          <div class="setting-row"><span class="setting-key">OLLAMA_URL</span><span class="setting-val">${escHtml(data.env.OLLAMA_URL)}</span></div>
          <div class="setting-row"><span class="setting-key">OLLAMA_EMBED_MODEL</span><span class="setting-val">${escHtml(data.env.OLLAMA_EMBED_MODEL)}</span></div>
        </div>

        <div class="section">
          <h2>Recent Events <span class="badge">last ${events.length}</span></h2>
          ${events.length > 0 ? events.map(e => {
            const outcome = e.outcome || {};
            const statusClass = outcome.status === 'success' ? 'success' : outcome.status === 'failure' ? 'error' : 'info';
            const ts = (e.meta && e.meta.at) || '';
            const signals = (e.signals || []).join(', ');
            const genesUsed = (e.genes_used || []).join(', ');
            return `<div class="event-item">
              <div class="event-header">
                <span class="tag ${e.intent === 'repair' ? 'error' : e.intent === 'innovate' ? 'purple' : 'info'}">${escHtml(e.intent || 'unknown')}</span>
                <span class="tag ${statusClass}">${escHtml(outcome.status || '?')}</span>
                <span class="event-time">${escHtml(fmt(ts))}</span>
              </div>
              <div class="event-detail">Genes: ${escHtml(genesUsed)} | Signals: ${escHtml(signals)}${outcome.score != null ? ' | Score: '+outcome.score : ''}</div>
            </div>`;
          }).join('') : '<div class="empty">No evolution events recorded yet.</div>'}
        </div>
      </div>

      <div class="footer">
        Built by <a href="https://mastermindshq.business" target="_blank" rel="noopener">Joe Che</a>
      </div>
    `;

    // Bind toggle
    const toggle = document.getElementById('auto-refresh-toggle');
    if (toggle) {
      toggle.addEventListener('change', function() {
        autoRefresh = this.checked;
        if (autoRefresh) startAutoRefresh();
        else stopAutoRefresh();
        // Re-render to update dot
        if (lastData) render(lastData);
      });
    }
  }

  function fetchData() {
    fetch('/api/data')
      .then(r => r.json())
      .then(data => render(data))
      .catch(err => {
        console.error('Failed to fetch data:', err);
        document.getElementById('app').innerHTML = '<div class="loading">Failed to load data. Is the server running?</div>';
      });
  }

  function startAutoRefresh() {
    stopAutoRefresh();
    refreshTimer = setInterval(fetchData, 30000);
  }

  function stopAutoRefresh() {
    if (refreshTimer) { clearInterval(refreshTimer); refreshTimer = null; }
  }

  // Initial load
  fetchData();
  startAutoRefresh();
})();
</script>
</body>
</html>


path: dashboard/server.js
content: // dashboard/server.js — Tiny HTTP server for LSHML dashboard
const http = require('http');
const fs = require('fs');
const path = require('path');

const SKILL_ROOT = path.resolve(__dirname, '..');
const MEMORY_DIR = path.join(SKILL_ROOT, 'memory');
const GEP_DIR = path.join(SKILL_ROOT, 'assets', 'gep');
const HTML_PATH = path.join(__dirname, 'index.html');

function readJsonSafe(p) {
  try {
    if (!fs.existsSync(p)) return null;
    const raw = fs.readFileSync(p, 'utf8');
    if (!raw.trim()) return null;
    return JSON.parse(raw);
  } catch { return null; }
}

function readJsonlSafe(p, limit) {
  try {
    if (!fs.existsSync(p)) return [];
    const raw = fs.readFileSync(p, 'utf8');
    const lines = raw.split('\n').filter(l => l.trim());
    const parsed = [];
    for (const line of lines) {
      try { parsed.push(JSON.parse(line)); } catch {}
    }
    if (limit && limit > 0) return parsed.slice(-limit);
    return parsed;
  } catch { return []; }
}

function fileSizeSafe(p) {
  try {
    if (!fs.existsSync(p)) return 0;
    return fs.statSync(p).size;
  } catch { return 0; }
}

function gatherData() {
  const pkg = readJsonSafe(path.join(SKILL_ROOT, 'package.json')) || {};

  // Memory files
  const feedbackAll = readJsonlSafe(path.join(MEMORY_DIR, 'feedback.jsonl'));
  const knowledge = readJsonSafe(path.join(MEMORY_DIR, 'knowledge.json')) || { lessons: [] };
  const predictor = readJsonSafe(path.join(MEMORY_DIR, 'predictor.json'));
  const clusters = readJsonSafe(path.join(MEMORY_DIR, 'cluster-registry.json')) || [];
  const embeddingsCacheSize = fileSizeSafe(path.join(MEMORY_DIR, 'embeddings-cache.json'));

  // GEP assets
  const genesData = readJsonSafe(path.join(GEP_DIR, 'genes.json')) || { genes: [] };
  const capsulesData = readJsonSafe(path.join(GEP_DIR, 'capsules.json')) || { capsules: [] };
  const failedCapsulesData = readJsonSafe(path.join(GEP_DIR, 'failed_capsules.json')) || { failed_capsules: [] };
  const eventsAll = readJsonlSafe(path.join(GEP_DIR, 'events.jsonl'));
  const recentEvents = eventsAll.slice(-10);

  // Env settings
  const env = {
    EVOLVE_STRATEGY: process.env.EVOLVE_STRATEGY || '(not set)',
    OLLAMA_URL: process.env.OLLAMA_URL || '(not set)',
    OLLAMA_EMBED_MODEL: process.env.OLLAMA_EMBED_MODEL || '(not set)',
  };

  return {
    version: pkg.version || 'unknown',
    name: pkg.name || 'lshml',
    timestamp: new Date().toISOString(),
    feedback: feedbackAll,
    knowledge,
    predictor,
    clusters,
    embeddingsCacheSize,
    genes: genesData.genes || [],
    capsules: capsulesData.capsules || [],
    failedCapsules: failedCapsulesData.failed_capsules || [],
    recentEvents,
    totalEvents: eventsAll.length,
    env,
  };
}

function startServer(port) {
  const server = http.createServer((req, res) => {
    if (req.method === 'GET' && req.url === '/api/data') {
      res.writeHead(200, { 'Content-Type': 'application/json', 'Access-Control-Allow-Origin': '*' });
      res.end(JSON.stringify(gatherData()));
    } else if (req.method === 'GET' && (req.url === '/' || req.url === '/index.html')) {
      try {
        const html = fs.readFileSync(HTML_PATH, 'utf8');
        res.writeHead(200, { 'Content-Type': 'text/html; charset=utf-8' });
        res.end(html);
      } catch (e) {
        res.writeHead(500, { 'Content-Type': 'text/plain' });
        res.end('Dashboard HTML not found: ' + e.message);
      }
    } else {
      res.writeHead(404, { 'Content-Type': 'text/plain' });
      res.end('Not found');
    }
  });

  server.listen(port, () => {
    console.log(`[LSHML Dashboard] Running at http://localhost:${port}`);
    console.log(`[LSHML Dashboard] API endpoint: http://localhost:${port}/api/data`);
    console.log('[LSHML Dashboard] Press Ctrl+C to stop.');
  });

  return server;
}

module.exports = { startServer, gatherData };


path: index.js
content: const evolve = require('./src/evolve');
const { solidify } = require('./src/gep/solidify');
const path = require('path');
// Hardened Env Loading: Ensure .env is loaded before anything else
try { require('dotenv').config({ path: path.resolve(__dirname, './.env') }); } catch (e) { console.warn('[LSHML] Warning: dotenv not found or failed to load .env'); }
const fs = require('fs');
const { spawn } = require('child_process');

function sleepMs(ms) {
  const n = parseInt(String(ms), 10);
  const t = Number.isFinite(n) ? Math.max(0, n) : 0;
  return new Promise(resolve => setTimeout(resolve, t));
}

function readJsonSafe(p) {
  try {
    if (!fs.existsSync(p)) return null;
    const raw = fs.readFileSync(p, 'utf8');
    if (!raw.trim()) return null;
    return JSON.parse(raw);
  } catch (e) {
    return null;
  }
}

function isPendingSolidify(state) {
  const lastRun = state && state.last_run ? state.last_run : null;
  const lastSolid = state && state.last_solidify ? state.last_solidify : null;
  if (!lastRun || !lastRun.run_id) return false;
  if (!lastSolid || !lastSolid.run_id) return true;
  return String(lastSolid.run_id) !== String(lastRun.run_id);
}

function parseMs(v, fallback) {
  const n = parseInt(String(v == null ? '' : v), 10);
  if (Number.isFinite(n)) return Math.max(0, n);
  return fallback;
}

// Singleton Guard - prevent multiple evolver daemon instances
function acquireLock() {
  const lockFile = path.join(__dirname, 'evolver.pid');
  try {
    if (fs.existsSync(lockFile)) {
      const pid = parseInt(fs.readFileSync(lockFile, 'utf8').trim(), 10);
      if (!Number.isFinite(pid) || pid <= 0) {
        console.log('[Singleton] Corrupt lock file (invalid PID). Taking over.');
      } else {
        try {
          process.kill(pid, 0);
          console.log(`[Singleton] LSHML loop already running (PID ${pid}). Exiting.`);
          return false;
        } catch (e) {
          console.log(`[Singleton] Stale lock found (PID ${pid}). Taking over.`);
        }
      }
    }
    fs.writeFileSync(lockFile, String(process.pid));
    return true;
  } catch (err) {
    console.error('[Singleton] Lock acquisition failed:', err);
    return false;
  }
}

function releaseLock() {
  const lockFile = path.join(__dirname, 'evolver.pid');
  try {
    if (fs.existsSync(lockFile)) {
       const pid = parseInt(fs.readFileSync(lockFile, 'utf8').trim(), 10);
       if (pid === process.pid) fs.unlinkSync(lockFile);
    }
  } catch (e) { /* ignore */ }
}

async function main() {
  const args = process.argv.slice(2);
  const command = args[0];
  const isLoop = args.includes('--loop') || args.includes('--mad-dog');

  // Dashboard mode — launch the web UI
  if (args.includes('--dashboard')) {
    const { startServer } = require('./dashboard/server');
    const port = parseInt(process.env.LSHML_DASHBOARD_PORT, 10) || 8420;
    startServer(port);
    return; // keep server running
  }

  if (command === 'run' || command === '/evolve' || isLoop) {
    console.log('Starting self-healing ML engine...');
    
    if (isLoop) {
        // Internal daemon loop (no wrapper required).
        if (!acquireLock()) process.exit(0);
        process.on('exit', releaseLock);
        process.on('SIGINT', () => { releaseLock(); process.exit(); });
        process.on('SIGTERM', () => { releaseLock(); process.exit(); });

        process.env.EVOLVE_LOOP = 'true';
        process.env.EVOLVE_BRIDGE = 'false';
        console.log('Loop mode enabled (internal daemon).');

        const { getEvolutionDir } = require('./src/gep/paths');
        const solidifyStatePath = path.join(getEvolutionDir(), 'evolution_solidify_state.json');

        const minSleepMs = parseMs(process.env.EVOLVER_MIN_SLEEP_MS, 2000);
        const maxSleepMs = parseMs(process.env.EVOLVER_MAX_SLEEP_MS, 300000);
        const idleThresholdMs = parseMs(process.env.EVOLVER_IDLE_THRESHOLD_MS, 500);
        const pendingSleepMs = parseMs(
          process.env.EVOLVE_PENDING_SLEEP_MS ||
            process.env.EVOLVE_MIN_INTERVAL ||
            process.env.FEISHU_EVOLVER_INTERVAL,
          120000
        );

        const maxCyclesPerProcess = parseMs(process.env.EVOLVER_MAX_CYCLES_PER_PROCESS, 100) || 100;
        const maxRssMb = parseMs(process.env.EVOLVER_MAX_RSS_MB, 500) || 500;
        const suicideEnabled = String(process.env.EVOLVER_SUICIDE || '').toLowerCase() !== 'false';

        let currentSleepMs = minSleepMs;
        let cycleCount = 0;

        while (true) {
          try {
          cycleCount += 1;

          // Ralph-loop gating: do not run a new cycle while previous run is pending solidify.
          const st0 = readJsonSafe(solidifyStatePath);
          if (isPendingSolidify(st0)) {
            await sleepMs(Math.max(pendingSleepMs, minSleepMs));
            continue;
          }

          const t0 = Date.now();
          let ok = false;
          try {
            await evolve.run();
            ok = true;
          } catch (error) {
            const msg = error && error.message ? String(error.message) : String(error);
            console.error(`Evolution cycle failed: ${msg}`);
          }
          const dt = Date.now() - t0;

          // Adaptive sleep: treat very fast cycles as "idle", backoff; otherwise reset to min.
          if (!ok || dt < idleThresholdMs) {
            currentSleepMs = Math.min(maxSleepMs, Math.max(minSleepMs, currentSleepMs * 2));
          } else {
            currentSleepMs = minSleepMs;
          }

          // Suicide check (memory leak protection)
          if (suicideEnabled) {
            const memMb = process.memoryUsage().rss / 1024 / 1024;
            if (cycleCount >= maxCyclesPerProcess || memMb > maxRssMb) {
              console.log(`[Daemon] Restarting self (cycles=${cycleCount}, rssMb=${memMb.toFixed(0)})`);
              try {
                const spawnOpts = {
                  detached: true,
                  stdio: 'ignore',
                  env: process.env,
                  windowsHide: true,
                };
                const child = spawn(process.execPath, [__filename, ...args], spawnOpts);
                child.unref();
                releaseLock();
                process.exit(0);
              } catch (spawnErr) {
                console.error('[Daemon] Spawn failed, continuing current process:', spawnErr.message);
              }
            }
          }

          let saturationMultiplier = 1;
          try {
            const st1 = readJsonSafe(solidifyStatePath);
            const lastSignals = st1 && st1.last_run && Array.isArray(st1.last_run.signals) ? st1.last_run.signals : [];
            if (lastSignals.includes('force_steady_state')) {
              saturationMultiplier = 10;
              console.log('[Daemon] Saturation detected. Entering steady-state mode (10x sleep).');
            } else if (lastSignals.includes('evolution_saturation')) {
              saturationMultiplier = 5;
              console.log('[Daemon] Approaching saturation. Reducing evolution frequency (5x sleep).');
            }
          } catch (e) {}

          // Jitter to avoid lockstep restarts.
          const jitter = Math.floor(Math.random() * 250);
          await sleepMs((currentSleepMs + jitter) * saturationMultiplier);

          } catch (loopErr) {
            console.error('[Daemon] Unexpected loop error (recovering): ' + (loopErr && loopErr.message ? loopErr.message : String(loopErr)));
            await sleepMs(Math.max(minSleepMs, 10000));
          }
        }
    } else {
        // Normal Single Run
        try {
            await evolve.run();
        } catch (error) {
            console.error('Evolution failed:', error);
            process.exit(1);
        }
    }
    
  } else if (command === 'solidify') {
    const dryRun = args.includes('--dry-run');
    const noRollback = args.includes('--no-rollback');
    const intentFlag = args.find(a => typeof a === 'string' && a.startsWith('--intent='));
    const summaryFlag = args.find(a => typeof a === 'string' && a.startsWith('--summary='));
    const intent = intentFlag ? intentFlag.slice('--intent='.length) : null;
    const summary = summaryFlag ? summaryFlag.slice('--summary='.length) : null;

    try {
      const res = solidify({
        intent: intent || undefined,
        summary: summary || undefined,
        dryRun,
        rollbackOnFailure: !noRollback,
      });
      const st = res && res.ok ? 'SUCCESS' : 'FAILED';
      console.log(`[SOLIDIFY] ${st}`);
      if (res && res.gene) console.log(JSON.stringify(res.gene, null, 2));
      if (res && res.event) console.log(JSON.stringify(res.event, null, 2));
      if (res && res.capsule) console.log(JSON.stringify(res.capsule, null, 2));

      if (res && res.ok && !dryRun) {
        try {
          const { shouldDistill, prepareDistillation } = require('./src/gep/skillDistiller');
          if (shouldDistill()) {
            const dr = prepareDistillation();
            if (dr && dr.ok && dr.promptPath) {
              console.log('\n[DISTILL_REQUEST]');
              console.log('Distillation prompt ready. Read the prompt file, process it with your LLM,');
              console.log('save the LLM response to a file, then run:');
              console.log('  node index.js distill --response-file=<path_to_llm_response>');
              console.log('Prompt file: ' + dr.promptPath);
              console.log('[/DISTILL_REQUEST]');
            }
          }
        } catch (e) {
          console.warn('[Distiller] Init failed (non-fatal): ' + (e.message || e));
        }
      }

      process.exit(res && res.ok ? 0 : 2);
    } catch (error) {
      console.error('[SOLIDIFY] Error:', error);
      process.exit(2);
    }
  } else if (command === 'distill') {
    const responseFileFlag = args.find(a => typeof a === 'string' && a.startsWith('--response-file='));
    if (!responseFileFlag) {
      console.error('Usage: node index.js distill --response-file=<path>');
      process.exit(1);
    }
    const responseFilePath = responseFileFlag.slice('--response-file='.length);
    try {
      const responseText = fs.readFileSync(responseFilePath, 'utf8');
      const { completeDistillation } = require('./src/gep/skillDistiller');
      const result = completeDistillation(responseText);
      if (result && result.ok) {
        console.log('[Distiller] Gene produced: ' + result.gene.id);
        console.log(JSON.stringify(result.gene, null, 2));
      } else {
        console.warn('[Distiller] Distillation did not produce a gene: ' + (result && result.reason || 'unknown'));
      }
      process.exit(result && result.ok ? 0 : 2);
    } catch (error) {
      console.error('[DISTILL] Error:', error);
      process.exit(2);
    }

  } else if (command === 'asset-log') {
    const { summarizeCallLog, readCallLog, getLogPath } = require('./src/gep/assetCallLog');

    const runIdFlag = args.find(a => typeof a === 'string' && a.startsWith('--run='));
    const actionFlag = args.find(a => typeof a === 'string' && a.startsWith('--action='));
    const lastFlag = args.find(a => typeof a === 'string' && a.startsWith('--last='));
    const sinceFlag = args.find(a => typeof a === 'string' && a.startsWith('--since='));
    const jsonMode = args.includes('--json');

    const opts = {};
    if (runIdFlag) opts.run_id = runIdFlag.slice('--run='.length);
    if (actionFlag) opts.action = actionFlag.slice('--action='.length);
    if (lastFlag) opts.last = parseInt(lastFlag.slice('--last='.length), 10);
    if (sinceFlag) opts.since = sinceFlag.slice('--since='.length);

    if (jsonMode) {
      const entries = readCallLog(opts);
      console.log(JSON.stringify(entries, null, 2));
    } else {
      const summary = summarizeCallLog(opts);
      console.log(`\n[Asset Call Log] ${getLogPath()}`);
      console.log(`  Total entries: ${summary.total_entries}`);
      console.log(`  Unique assets: ${summary.unique_assets}`);
      console.log(`  Unique runs:   ${summary.unique_runs}`);
      console.log(`  By action:`);
      for (const [action, count] of Object.entries(summary.by_action)) {
        console.log(`    ${action}: ${count}`);
      }
      if (summary.entries.length > 0) {
        console.log(`\n  Recent entries:`);
        const show = summary.entries.slice(-10);
        for (const e of show) {
          const ts = e.timestamp ? e.timestamp.slice(0, 19) : '?';
          const assetShort = e.asset_id ? e.asset_id.slice(0, 20) + '...' : '(none)';
          const sigPreview = Array.isArray(e.signals) ? e.signals.slice(0, 3).join(', ') : '';
          console.log(`    [${ts}] ${e.action || '?'}  asset=${assetShort}  score=${e.score || '-'}  mode=${e.mode || '-'}  signals=[${sigPreview}]  run=${e.run_id || '-'}`);
        }
      } else {
        console.log('\n  No entries found.');
      }
      console.log('');
    }

  } else {
    console.log(`Usage: node index.js [run|/evolve|solidify|distill|asset-log] [--loop] [--dashboard]
  - --dashboard       Launch web dashboard (port 8420, or LSHML_DASHBOARD_PORT)
  - solidify flags:
    - --dry-run
    - --no-rollback
    - --intent=repair|optimize|innovate
    - --summary=...
  - distill flags:
    - --response-file=<path>  (LLM response file for skill distillation)
  - asset-log flags:
    - --run=<run_id>           (filter by run ID)
    - --action=<action>        (filter: hub_search_hit, hub_search_miss, asset_reuse, asset_reference, asset_publish, asset_publish_skip)
    - --last=<N>               (show last N entries)
    - --since=<ISO_date>       (entries after date)
    - --json                   (raw JSON output)`);
  }
}

if (require.main === module) {
  main();
}


path: memory/knowledge.json
content: {
  "lessons": [
    {
      "key": "abc|g_darwin|darwin",
      "error_hash": "abc",
      "gene_id": "g_darwin",
      "environment": "darwin",
      "confidence": 0.65,
      "times_applied": 2,
      "times_succeeded": 2,
      "times_failed": 0,
      "created_at": "2026-03-12T06:38:18.613Z",
      "last_applied": "2026-03-12T06:38:18.613Z"
    },
    {
      "key": "abc|g_linux|linux",
      "error_hash": "abc",
      "gene_id": "g_linux",
      "environment": "linux",
      "confidence": 0.6,
      "times_applied": 1,
      "times_succeeded": 1,
      "times_failed": 0,
      "created_at": "2026-03-12T06:38:18.614Z",
      "last_applied": "2026-03-12T06:38:18.614Z"
    }
  ],
  "version": 1
}

path: package.json
content: {
  "name": "capability-evolver",
  "version": "2.0.0",
  "description": "A fully local machine learning engine that makes your OpenClaw agent smart over time — without ever calling home, revealing your machine ID, or exposing any security holes. Tracks fix outcomes with a feedback loop, clusters errors with semantic embeddings, and learns which strategies work using a k-NN predictor. Every evolution is auditable. No telemetry, no fingerprinting, no network calls.",
  "main": "index.js",
  "keywords": [
    "ai",
    "evolution",
    "meta-learning",
    "self-improvement",
    "automation",
    "agent",
    "local"
  ],
  "author": "Joe Che (https://mastermindshq.business)",
  "license": "MIT",
  "scripts": {
    "start": "node index.js",
    "run": "node index.js run",
    "solidify": "node index.js solidify",
    "test": "node --test test/*.test.js"
  },
  "dependencies": {
    "dotenv": "^16.4.7"
  },
  "private": false
}


path: scripts/analyze_by_skill.js
content: const fs = require('fs');
const path = require('path');

const REPO_ROOT = path.resolve(__dirname, '..');
const LOG_FILE = path.join(REPO_ROOT, 'evolution_history_full.md');
const OUT_FILE = path.join(REPO_ROOT, 'evolution_detailed_report.md');

function analyzeEvolution() {
    if (!fs.existsSync(LOG_FILE)) {
        console.error("Source file missing.");
        return;
    }

    const content = fs.readFileSync(LOG_FILE, 'utf8');
    // Split by divider
    const entries = content.split('---').map(e => e.trim()).filter(e => e.length > 0);

    const skillUpdates = {}; // Map<SkillName, Array<Changes>>
    const generalUpdates = []; // Array<Changes>

    // Regex to detect skills/paths
    // e.g. `skills/feishu-card/send.js` or **Target**: `skills/git-sync`
    const skillRegex = /skills\/([a-zA-Z0-9\-_]+)/;
    const actionRegex = /Action:\s*([\s\S]*?)(?=\n\n|\n[A-Z]|$)/i; // Capture Action text
    const statusRegex = /Status:\s*\[?([A-Z\s_]+)\]?/i;

    entries.forEach(entry => {
        // Extract basic info
        const statusMatch = entry.match(statusRegex);
        const status = statusMatch ? statusMatch[1].trim().toUpperCase() : 'UNKNOWN';
        
        // Skip routine checks if we want a *detailed evolution* report (focus on changes)
        // But user asked for "what happened", so routine scans might be boring unless they found something.
        // Let's filter out "STABILITY" or "RUNNING" unless there is a clear "Mutated" or "Fixed" keyword.
        const isInteresting = 
            entry.includes('Fixed') || 
            entry.includes('Hardened') || 
            entry.includes('Optimized') || 
            entry.includes('Patched') || 
            entry.includes('Created') || 
            entry.includes('Added') ||
            status === 'SUCCESS' ||
            status === 'COMPLETED';

        if (!isInteresting) return;

        // Find associated skill
        const skillMatch = entry.match(skillRegex);
        let skillName = 'General / System';
        if (skillMatch) {
            skillName = skillMatch[1];
        } else {
            // Try heuristics
            if (entry.toLowerCase().includes('feishu card')) skillName = 'feishu-card';
            else if (entry.toLowerCase().includes('git sync')) skillName = 'git-sync';
            else if (entry.toLowerCase().includes('logger')) skillName = 'interaction-logger';
            else if (entry.toLowerCase().includes('evolve')) skillName = 'capability-evolver';
        }

        // Extract description
        let description = "";
        const actionMatch = entry.match(actionRegex);
        if (actionMatch) {
            description = actionMatch[1].trim();
        } else {
            // Fallback: take lines that look like bullet points or text after header
            const lines = entry.split('\n');
            description = lines.filter(l => l.match(/^[•\-\*]|\w/)).slice(1).join('\n').trim();
        }

        // Clean up description (remove duplicate "Action:" prefix if captured)
        description = description.replace(/^Action:\s*/i, '');

        if (!skillUpdates[skillName]) skillUpdates[skillName] = [];
        
        // Dedup descriptions slightly (simple check)
        const isDuplicate = skillUpdates[skillName].some(u => u.desc.includes(description.substring(0, 20)));
        if (!isDuplicate) {
            // Extract Date if possible
            const dateMatch = entry.match(/\((\d{4}\/\d{1,2}\/\d{1,2}.*?)\)/);
            const date = dateMatch ? dateMatch[1] : 'Unknown';
            
            skillUpdates[skillName].push({
                date,
                status,
                desc: description
            });
        }
    });

    // Generate Markdown
    let md = "# Detailed Evolution Report (By Skill)\n\n> Comprehensive breakdown of system changes.\n\n";

    // Sort skills alphabetically
    const sortedSkills = Object.keys(skillUpdates).sort();

    sortedSkills.forEach(skill => {
        md += `## ${skill}\n`;
        const updates = skillUpdates[skill];
        
        updates.forEach(u => {
            // Icon based on content
            let icon = '*';
            const lowerDesc = u.desc.toLowerCase();
            if (lowerDesc.includes('optimiz')) icon = '[optimize]';
            if (lowerDesc.includes('secur') || lowerDesc.includes('harden') || lowerDesc.includes('permission')) icon = '[security]';
            if (lowerDesc.includes('fix') || lowerDesc.includes('patch')) icon = '[repair]';
            if (lowerDesc.includes('creat') || lowerDesc.includes('add')) icon = '[add]';

            md += `### ${icon} ${u.date}\n`;
            md += `${u.desc}\n\n`;
        });
        md += `---\n`;
    });

    fs.writeFileSync(OUT_FILE, md);
    console.log(`Generated report for ${sortedSkills.length} skills.`);
}

analyzeEvolution();



path: scripts/extract_log.js
content: const fs = require('fs');
const path = require('path');

const REPO_ROOT = path.resolve(__dirname, '..');
const LOG_FILE = path.join(REPO_ROOT, 'memory', 'mad_dog_evolution.log');
const OUT_FILE = path.join(REPO_ROOT, 'evolution_history.md');

function parseLog() {
    if (!fs.existsSync(LOG_FILE)) {
        console.log("Log file not found.");
        return;
    }

    const content = fs.readFileSync(LOG_FILE, 'utf8');
    const lines = content.split('\n');
    
    const reports = [];
    let currentTimestamp = null;

    // Regex for Feishu command
    // node skills/feishu-card/send.js --title "..." --color ... --text "..."
    const cmdRegex = /node skills\/feishu-card\/send\.js --title "(.*?)" --color \w+ --text "(.*?)"/;

    for (let i = 0; i < lines.length; i++) {
        const line = lines[i];
        
        // 1. Capture Timestamp
        if (line.includes('Cycle Start:')) {
            // Format: Cycle Start: Sun Feb  1 19:17:44 UTC 2026
            const dateStr = line.split('Cycle Start: ')[1].trim();
            try {
                currentTimestamp = new Date(dateStr);
            } catch (e) {
                currentTimestamp = null;
            }
        }

        const match = line.match(cmdRegex);
        if (match) {
            const title = match[1];
            let text = match[2];
            
            // Clean up text (unescape newlines)
            text = text.replace(/\\n/g, '\n').replace(/\\"/g, '"');

            if (currentTimestamp) {
                reports.push({
                    ts: currentTimestamp,
                    title: title,
                    text: text,
                    id: title // Cycle ID is in title
                });
            }
        }
    }

    // Deduplicate by ID (keep latest timestamp?)
    const uniqueReports = {};
    reports.forEach(r => {
        uniqueReports[r.id] = r;
    });

    const sortedReports = Object.values(uniqueReports).sort((a, b) => a.ts - b.ts);

    let md = "# Evolution History (Extracted)\n\n";
    sortedReports.forEach(r => {
        // Convert to CST (UTC+8)
        const cstDate = r.ts.toLocaleString("zh-CN", {
            timeZone: "Asia/Shanghai", 
            hour12: false,
            year: 'numeric', month: '2-digit', day: '2-digit',
            hour: '2-digit', minute: '2-digit', second: '2-digit'
        });

        md += `### ${r.title} (${cstDate})\n`;
        md += `${r.text}\n\n`;
        md += `---\n\n`;
    });

    fs.writeFileSync(OUT_FILE, md);
    console.log(`Extracted ${sortedReports.length} reports to ${OUT_FILE}`);
}

parseLog();



path: scripts/generate_history.js
content: const { execSync } = require('child_process');
const fs = require('fs');
const path = require('path');

// Separator for git log parsing (something unlikely to be in commit messages)
const SEP = '|||';
const REPO_ROOT = path.resolve(__dirname, '..');

try {
    // Git command:
    // --reverse: Oldest to Newest (Time Sequence)
    // --grep: Filter by keyword
    // --format: Hash, Date (ISO), Author, Subject, Body
    const cmd = `git log --reverse --grep="Evolution" --format="%H${SEP}%ai${SEP}%an${SEP}%s${SEP}%b"`;
    
    console.log('Executing git log...');
    const output = execSync(cmd, { 
        encoding: 'utf8', 
        cwd: REPO_ROOT,
        maxBuffer: 1024 * 1024 * 10 // 10MB buffer just in case
    });

    const entries = output.split('\n').filter(line => line.trim().length > 0);
    
    let markdown = '# Evolution History (Time Sequence)\n\n';
    markdown += '> Filter: "Evolution"\n';
    markdown += '> Timezone: CST (UTC+8)\n\n';
    
    let count = 0;

    entries.forEach(entry => {
        const parts = entry.split(SEP);
        if (parts.length < 4) return;

        const hash = parts[0];
        const dateStr = parts[1];
        const author = parts[2];
        const subject = parts[3];
        const body = parts[4] || '';

        // Parse Date and Convert to UTC+8
        const date = new Date(dateStr);
        // Add 8 hours (28800000 ms) to UTC timestamp to shift it
        // Then formatting it as ISO will look like UTC but represent CST values
        const cstDate = new Date(date.getTime() + 8 * 60 * 60 * 1000);
        
        // Format: YYYY-MM-DD HH:mm:ss
        const timeStr = cstDate.toISOString().replace('T', ' ').substring(0, 19);

        markdown += `## ${timeStr}\n`;
        markdown += `- Commit: \`${hash.substring(0, 7)}\`\n`;
        markdown += `- Subject: ${subject}\n`;
        
        if (body.trim()) {
            // Indent body for better readability
            const formattedBody = body.trim().split('\n').map(l => `> ${l}`).join('\n');
            markdown += `- Details:\n${formattedBody}\n`;
        }
        markdown += '\n';
        count++;
    });

    const outDir = path.join(REPO_ROOT, 'memory');
    if (!fs.existsSync(outDir)) fs.mkdirSync(outDir, { recursive: true });
    const outPath = path.join(outDir, 'evolution_history.md');
    fs.writeFileSync(outPath, markdown);
    
    console.log(`Successfully generated report with ${count} entries.`);
    console.log(`Saved to: ${outPath}`);

} catch (e) {
    console.error('Error generating history:', e.message);
    process.exit(1);
}



path: scripts/gep_append_event.js
content: const fs = require('fs');
const { appendEventJsonl } = require('../src/gep/assetStore');

function readStdin() {
  try {
    return fs.readFileSync(0, 'utf8');
  } catch {
    return '';
  }
}

function readTextIfExists(p) {
  try {
    if (!p) return '';
    if (!fs.existsSync(p)) return '';
    return fs.readFileSync(p, 'utf8');
  } catch {
    return '';
  }
}

function parseInput(text) {
  const raw = String(text || '').trim();
  if (!raw) return [];

  // Accept JSON array or single JSON.
  try {
    const maybe = JSON.parse(raw);
    if (Array.isArray(maybe)) return maybe;
    if (maybe && typeof maybe === 'object') return [maybe];
  } catch (e) {}

  // Fallback: JSONL.
  const lines = raw.split('\n').map(l => l.trim()).filter(Boolean);
  const out = [];
  for (const line of lines) {
    try {
      const obj = JSON.parse(line);
      out.push(obj);
    } catch (e) {}
  }
  return out;
}

function isValidEvolutionEvent(ev) {
  if (!ev || ev.type !== 'EvolutionEvent') return false;
  if (!ev.id || typeof ev.id !== 'string') return false;
  // parent may be null or string
  if (!(ev.parent === null || typeof ev.parent === 'string')) return false;
  if (!ev.intent || typeof ev.intent !== 'string') return false;
  if (!Array.isArray(ev.signals)) return false;
  if (!Array.isArray(ev.genes_used)) return false;
  // GEP v1.4: mutation + personality are mandatory evolution dimensions
  if (!ev.mutation_id || typeof ev.mutation_id !== 'string') return false;
  if (!ev.personality_state || typeof ev.personality_state !== 'object') return false;
  if (ev.personality_state.type !== 'PersonalityState') return false;
  for (const k of ['rigor', 'creativity', 'verbosity', 'risk_tolerance', 'obedience']) {
    const v = Number(ev.personality_state[k]);
    if (!Number.isFinite(v) || v < 0 || v > 1) return false;
  }
  if (!ev.blast_radius || typeof ev.blast_radius !== 'object') return false;
  if (!Number.isFinite(Number(ev.blast_radius.files))) return false;
  if (!Number.isFinite(Number(ev.blast_radius.lines))) return false;
  if (!ev.outcome || typeof ev.outcome !== 'object') return false;
  if (!ev.outcome.status || typeof ev.outcome.status !== 'string') return false;
  const score = Number(ev.outcome.score);
  if (!Number.isFinite(score) || score < 0 || score > 1) return false;

  // capsule_id is optional, but if present must be string or null.
  if (!('capsule_id' in ev)) return true;
  return ev.capsule_id === null || typeof ev.capsule_id === 'string';
}

function main() {
  const args = process.argv.slice(2);
  const inputPath = args.find(a => a && !a.startsWith('--')) || '';
  const text = inputPath ? readTextIfExists(inputPath) : readStdin();
  const items = parseInput(text);

  let appended = 0;
  for (const it of items) {
    if (!isValidEvolutionEvent(it)) continue;
    appendEventJsonl(it);
    appended += 1;
  }

  process.stdout.write(`appended=${appended}\n`);
}

try {
  main();
} catch (e) {
  process.stderr.write(`${e && e.message ? e.message : String(e)}\n`);
  process.exit(1);
}



path: scripts/gep_personality_report.js
content: const fs = require('fs');
const path = require('path');
const { getRepoRoot, getMemoryDir, getGepAssetsDir } = require('../src/gep/paths');
const { normalizePersonalityState, personalityKey, defaultPersonalityState } = require('../src/gep/personality');

function readJsonIfExists(p, fallback) {
  try {
    if (!fs.existsSync(p)) return fallback;
    const raw = fs.readFileSync(p, 'utf8');
    if (!raw.trim()) return fallback;
    return JSON.parse(raw);
  } catch {
    return fallback;
  }
}

function readJsonlIfExists(p, limitLines = 5000) {
  try {
    if (!fs.existsSync(p)) return [];
    const raw = fs.readFileSync(p, 'utf8');
    const lines = raw
      .split('\n')
      .map(l => l.trim())
      .filter(Boolean);
    const recent = lines.slice(Math.max(0, lines.length - limitLines));
    return recent
      .map(l => {
        try {
          return JSON.parse(l);
        } catch {
          return null;
        }
      })
      .filter(Boolean);
  } catch {
    return [];
  }
}

function clamp01(x) {
  const n = Number(x);
  if (!Number.isFinite(n)) return 0;
  return Math.max(0, Math.min(1, n));
}

function pct(x) {
  const n = Number(x);
  if (!Number.isFinite(n)) return '0.0%';
  return `${(n * 100).toFixed(1)}%`;
}

function pad(s, n) {
  const str = String(s == null ? '' : s);
  if (str.length >= n) return str.slice(0, n);
  return str + ' '.repeat(n - str.length);
}

function scoreFromCounts(success, fail, avgScore) {
  const succ = Number(success) || 0;
  const fl = Number(fail) || 0;
  const total = succ + fl;
  const p = (succ + 1) / (total + 2); // Laplace smoothing
  const sampleWeight = Math.min(1, total / 8);
  const q = avgScore == null ? 0.5 : clamp01(avgScore);
  return p * 0.75 + q * 0.25 * sampleWeight;
}

function aggregateFromEvents(events) {
  const map = new Map();
  for (const ev of Array.isArray(events) ? events : []) {
    if (!ev || ev.type !== 'EvolutionEvent') continue;
    const ps = ev.personality_state && typeof ev.personality_state === 'object' ? ev.personality_state : null;
    if (!ps) continue;
    const key = personalityKey(normalizePersonalityState(ps));
    const cur = map.get(key) || {
      key,
      success: 0,
      fail: 0,
      n: 0,
      avg_score: 0.5,
      last_event_id: null,
      last_at: null,
      mutation: { repair: 0, optimize: 0, innovate: 0 },
      mutation_success: { repair: 0, optimize: 0, innovate: 0 },
    };
    const st = ev.outcome && ev.outcome.status ? String(ev.outcome.status) : 'unknown';
    if (st === 'success') cur.success += 1;
    else if (st === 'failed') cur.fail += 1;

    const sc = ev.outcome && Number.isFinite(Number(ev.outcome.score)) ? clamp01(Number(ev.outcome.score)) : null;
    if (sc != null) {
      cur.n += 1;
      cur.avg_score = cur.avg_score + (sc - cur.avg_score) / cur.n;
    }

    const cat = ev.intent ? String(ev.intent) : null;
    if (cat && cur.mutation[cat] != null) {
      cur.mutation[cat] += 1;
      if (st === 'success') cur.mutation_success[cat] += 1;
    }

    cur.last_event_id = ev.id || cur.last_event_id;
    const at = ev.meta && ev.meta.at ? String(ev.meta.at) : null;
    cur.last_at = at || cur.last_at;
    map.set(key, cur);
  }
  return Array.from(map.values());
}

function main() {
  const repoRoot = getRepoRoot();
  const memoryDir = getMemoryDir();
  const assetsDir = getGepAssetsDir();

  const personalityPath = path.join(memoryDir, 'personality_state.json');
  const model = readJsonIfExists(personalityPath, null);
  const current = model && model.current ? normalizePersonalityState(model.current) : defaultPersonalityState();
  const currentKey = personalityKey(current);

  const eventsPath = path.join(assetsDir, 'events.jsonl');
  const events = readJsonlIfExists(eventsPath, 10000);
  const evs = events.filter(e => e && e.type === 'EvolutionEvent');
  const agg = aggregateFromEvents(evs);

  // Prefer model.stats if present, but still show event-derived aggregation (ground truth).
  const stats = model && model.stats && typeof model.stats === 'object' ? model.stats : {};
  const statRows = Object.entries(stats).map(([key, e]) => {
    const entry = e && typeof e === 'object' ? e : {};
    const success = Number(entry.success) || 0;
    const fail = Number(entry.fail) || 0;
    const total = success + fail;
    const avg = Number.isFinite(Number(entry.avg_score)) ? clamp01(Number(entry.avg_score)) : null;
    const score = scoreFromCounts(success, fail, avg);
    return { key, success, fail, total, avg_score: avg, score, updated_at: entry.updated_at || null, source: 'model' };
  });

  const evRows = agg.map(e => {
    const success = Number(e.success) || 0;
    const fail = Number(e.fail) || 0;
    const total = success + fail;
    const avg = Number.isFinite(Number(e.avg_score)) ? clamp01(Number(e.avg_score)) : null;
    const score = scoreFromCounts(success, fail, avg);
    return { key: e.key, success, fail, total, avg_score: avg, score, updated_at: e.last_at || null, source: 'events', _ev: e };
  });

  // Merge rows by key (events take precedence for total/success/fail; model provides updated_at if events missing).
  const byKey = new Map();
  for (const r of [...statRows, ...evRows]) {
    const prev = byKey.get(r.key);
    if (!prev) {
      byKey.set(r.key, r);
      continue;
    }
    // Prefer events for counts and avg_score
    if (r.source === 'events') byKey.set(r.key, { ...prev, ...r });
    else byKey.set(r.key, { ...r, ...prev });
  }

  const merged = Array.from(byKey.values()).sort((a, b) => b.score - a.score);

  process.stdout.write(`Repo: ${repoRoot}\n`);
  process.stdout.write(`MemoryDir: ${memoryDir}\n`);
  process.stdout.write(`AssetsDir: ${assetsDir}\n\n`);

  process.stdout.write(`[Current Personality]\n`);
  process.stdout.write(`${currentKey}\n`);
  process.stdout.write(`${JSON.stringify(current, null, 2)}\n\n`);

  process.stdout.write(`[Personality Stats] (ranked by score)\n`);
  if (merged.length === 0) {
    process.stdout.write('(no stats yet; run a few cycles and solidify)\n');
    return;
  }

  const header =
    pad('rank', 5) +
    pad('total', 8) +
    pad('succ', 8) +
    pad('fail', 8) +
    pad('succ_rate', 11) +
    pad('avg', 7) +
    pad('score', 8) +
    'key';
  process.stdout.write(header + '\n');
  process.stdout.write('-'.repeat(Math.min(140, header.length + 40)) + '\n');

  const topN = Math.min(25, merged.length);
  for (let i = 0; i < topN; i++) {
    const r = merged[i];
    const succ = Number(r.success) || 0;
    const fail = Number(r.fail) || 0;
    const total = Number(r.total) || succ + fail;
    const succRate = total > 0 ? succ / total : 0;
    const avg = r.avg_score == null ? '-' : Number(r.avg_score).toFixed(2);
    const line =
      pad(String(i + 1), 5) +
      pad(String(total), 8) +
      pad(String(succ), 8) +
      pad(String(fail), 8) +
      pad(pct(succRate), 11) +
      pad(String(avg), 7) +
      pad(Number(r.score).toFixed(3), 8) +
      String(r.key);
    process.stdout.write(line + '\n');

    if (r._ev) {
      const ev = r._ev;
      const ms = ev.mutation || {};
      const mSucc = ev.mutation_success || {};
      const parts = [];
      for (const cat of ['repair', 'optimize', 'innovate']) {
        const n = Number(ms[cat]) || 0;
        if (n <= 0) continue;
        const s = Number(mSucc[cat]) || 0;
        parts.push(`${cat}:${s}/${n}`);
      }
      if (parts.length) process.stdout.write(`       mutation_success: ${parts.join(' | ')}\n`);
    }
  }

  process.stdout.write('\n');
  process.stdout.write(`[Notes]\n`);
  process.stdout.write(`- score is a smoothed composite of success_rate + avg_score (sample-weighted)\n`);
  process.stdout.write(`- current_key appears in the ranking once enough data accumulates\n`);
}

try {
  main();
} catch (e) {
  process.stderr.write((e && e.message) || String(e));
  process.stderr.write('\n');
  process.exit(1);
}



path: scripts/human_report.js
content: const fs = require('fs');
const path = require('path');

const REPO_ROOT = path.resolve(__dirname, '..');
const IN_FILE = path.join(REPO_ROOT, 'evolution_history_full.md');
const OUT_FILE = path.join(REPO_ROOT, 'evolution_human_summary.md');

function generateHumanReport() {
    if (!fs.existsSync(IN_FILE)) return console.error("No input file");

    const content = fs.readFileSync(IN_FILE, 'utf8');
    const entries = content.split('---').map(e => e.trim()).filter(e => e.length > 0);

    const categories = {
        'Security & Stability': [],
        'Performance & Optimization': [],
        'Tooling & Features': [],
        'Documentation & Process': []
    };

    const componentMap = {}; // Component -> Change List

    entries.forEach(entry => {
        // Extract basic info
        const lines = entry.split('\n');
        const header = lines[0]; // ### Title (Date)
        const body = lines.slice(1).join('\n');
        
        const dateMatch = header.match(/\((.*?)\)/);
        const dateStr = dateMatch ? dateMatch[1] : '';
        const time = dateStr.split(' ')[1] || ''; // HH:mm:ss

        // Classify
        let category = 'Tooling & Features';
        let component = 'System';
        let summary = '';

        const lowerBody = body.toLowerCase();

        // Detect Component
        if (lowerBody.includes('feishu-card')) component = 'feishu-card';
        else if (lowerBody.includes('feishu-sticker')) component = 'feishu-sticker';
        else if (lowerBody.includes('git-sync')) component = 'git-sync';
        else if (lowerBody.includes('capability-evolver') || lowerBody.includes('evolve.js')) component = 'capability-evolver';
        else if (lowerBody.includes('interaction-logger')) component = 'interaction-logger';
        else if (lowerBody.includes('chat-to-image')) component = 'chat-to-image';
        else if (lowerBody.includes('safe_publish')) component = 'capability-evolver';

        // Detect Category
        if (lowerBody.includes('security') || lowerBody.includes('permission') || lowerBody.includes('auth') || lowerBody.includes('harden')) {
            category = 'Security & Stability';
        } else if (lowerBody.includes('optimiz') || lowerBody.includes('performance') || lowerBody.includes('memory') || lowerBody.includes('fast')) {
            category = 'Performance & Optimization';
        } else if (lowerBody.includes('doc') || lowerBody.includes('readme')) {
            category = 'Documentation & Process';
        }

        // Extract Human Summary (First meaningful line that isn't Status/Action/Date)
        const summaryLines = lines.filter(l => 
            !l.startsWith('###') && 
            !l.startsWith('Status:') && 
            !l.startsWith('Action:') &&
            l.trim().length > 10
        );
        
        if (summaryLines.length > 0) {
            // Clean up the line
            summary = summaryLines[0]
                .replace(/^-\s*/, '') // Remove bullets
                .replace(/\*\*/g, '') // Remove bold
                .replace(/`/, '')
                .trim();
                
            // Deduplicate
            const key = `${component}:${summary.substring(0, 20)}`;
            const exists = categories[category].some(i => i.key === key);
            
            if (!exists && !summary.includes("Stability Scan OK") && !summary.includes("Workspace Sync")) {
                categories[category].push({ time, component, summary, key });
                
                if (!componentMap[component]) componentMap[component] = [];
                componentMap[component].push(summary);
            }
        }
    });

    // --- Generate Markdown ---
    const today = new Date().toISOString().slice(0, 10);
    let md = `# Evolution Summary: The Day in Review (${today})\n\n`;
    md += `> Overview: Grouped summary of changes extracted from evolution history.\n\n`;

    // Section 1: By Theme (Evolution Direction)
    md += `## 1. Evolution Direction\n`;
    
    for (const [cat, items] of Object.entries(categories)) {
        if (items.length === 0) continue;
        md += `### ${cat}\n`;
        // Group by component within theme
        const compGroup = {};
        items.forEach(i => {
            if (!compGroup[i.component]) compGroup[i.component] = [];
            compGroup[i.component].push(i.summary);
        });
        
        for (const [comp, sums] of Object.entries(compGroup)) {
            // Unique summaries only
            const uniqueSums = [...new Set(sums)];
            uniqueSums.forEach(s => {
                md += `- **${comp}**: ${s}\n`;
            });
        }
        md += `\n`;
    }

    // Section 2: By Timeline (High Level)
    md += `## 2. Timeline of Critical Events\n`;
    // Flatten and sort all items by time
    const allItems = [];
    Object.values(categories).forEach(list => allItems.push(...list));
    allItems.sort((a, b) => a.time.localeCompare(b.time));
    
    // Filter for "Critical" keywords
    const criticalItems = allItems.filter(i => 
        i.summary.toLowerCase().includes('fix') || 
        i.summary.toLowerCase().includes('patch') ||
        i.summary.toLowerCase().includes('create') ||
        i.summary.toLowerCase().includes('optimiz')
    );

    criticalItems.forEach(i => {
        md += `- \`${i.time}\` (${i.component}): ${i.summary}\n`;
    });

    // Section 3: Package Adjustments
    md += `\n## 3. Package & Documentation Adjustments\n`;
    const comps = Object.keys(componentMap).sort();
    comps.forEach(comp => {
        const count = new Set(componentMap[comp]).size;
        md += `- **${comp}**: Received ${count} significant updates.\n`;
    });

    fs.writeFileSync(OUT_FILE, md);
    console.log("Human report generated.");
}

generateHumanReport();



path: scripts/suggest_version.js
content: const fs = require('fs');
const path = require('path');
const { execSync } = require('child_process');

const REPO_ROOT = path.resolve(__dirname, '..');

function ensureDir(dir) {
  if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
}

function parseSemver(v) {
  const m = String(v || '').trim().match(/^(\d+)\.(\d+)\.(\d+)$/);
  if (!m) return null;
  return { major: Number(m[1]), minor: Number(m[2]), patch: Number(m[3]) };
}

function bumpSemver(base, bump) {
  const v = parseSemver(base);
  if (!v) return null;
  if (bump === 'major') return `${v.major + 1}.0.0`;
  if (bump === 'minor') return `${v.major}.${v.minor + 1}.0`;
  if (bump === 'patch') return `${v.major}.${v.minor}.${v.patch + 1}`;
  return `${v.major}.${v.minor}.${v.patch}`;
}

function git(cmd) {
  return execSync(cmd, { cwd: REPO_ROOT, encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
}

function getBaseReleaseCommit() {
  try {
    const hash = git('git log -n 1 --pretty=%H --grep="chore(release): prepare v"');
    return hash || null;
  } catch (e) {
    return null;
  }
}

function getCommitSubjectsSince(baseCommit) {
  try {
    if (!baseCommit) {
      const out = git('git log -n 30 --pretty=%s');
      return out ? out.split('\n').filter(Boolean) : [];
    }
    const out = git(`git log ${baseCommit}..HEAD --pretty=%s`);
    return out ? out.split('\n').filter(Boolean) : [];
  } catch (e) {
    return [];
  }
}

function inferBumpFromSubjects(subjects) {
  const subs = (subjects || []).map(s => String(s));
  const hasBreaking = subs.some(s => /\bBREAKING CHANGE\b/i.test(s) || /^[a-z]+(\(.+\))?!:/.test(s));
  if (hasBreaking) return { bump: 'major', reason: 'breaking change marker in commit subject' };

  const hasFeat = subs.some(s => /^feat(\(.+\))?:/i.test(s));
  if (hasFeat) return { bump: 'minor', reason: 'feature commit detected (feat:)' };

  const hasFix = subs.some(s => /^(fix|perf)(\(.+\))?:/i.test(s));
  if (hasFix) return { bump: 'patch', reason: 'fix/perf commit detected' };

  if (subs.length === 0) return { bump: 'none', reason: 'no commits since base release commit' };
  return { bump: 'patch', reason: 'default to patch for non-breaking changes' };
}

function main() {
  const pkgPath = path.join(REPO_ROOT, 'package.json');
  const baseVersion = JSON.parse(fs.readFileSync(pkgPath, 'utf8')).version;

  const baseCommit = getBaseReleaseCommit();
  const subjects = getCommitSubjectsSince(baseCommit);
  const decision = inferBumpFromSubjects(subjects);
  const suggestedVersion = decision.bump === 'none' ? baseVersion : bumpSemver(baseVersion, decision.bump);

  const out = { baseVersion, baseCommit, subjects, decision, suggestedVersion };
  const memDir = path.join(REPO_ROOT, 'memory');
  ensureDir(memDir);
  fs.writeFileSync(path.join(memDir, 'semver_suggestion.json'), JSON.stringify(out, null, 2) + '\n', 'utf8');
  process.stdout.write(JSON.stringify(out, null, 2) + '\n');
}

try {
  main();
} catch (e) {
  process.stderr.write(`${e.message}\n`);
  process.exit(1);
}



path: scripts/validate-modules.js
content: // Usage: node scripts/validate-modules.js ./src/evolve ./src/gep/solidify
// Requires each module to verify it loads without errors.
// Paths are resolved relative to cwd (repo root), not this script's location.
const path = require('path');
const modules = process.argv.slice(2);
if (!modules.length) { console.error('No modules specified'); process.exit(1); }
for (const m of modules) { require(path.resolve(m)); }
console.log('ok');


path: SECURITY.md
content: # Security Model — Capability Evolver

This document is for ClawHub reviewers and anyone evaluating the safety of
this skill before installing it.

## Architecture

The Capability Evolver is a self-improvement engine that analyzes an AI
agent's runtime history and applies protocol-constrained code changes. It
runs locally and is designed to operate in isolation without external
network dependencies.

## Network Communication — Disabled by Default

The evolver was originally designed to participate in a multi-agent Hub
network (evomap.ai) for sharing evolution assets. **All hub communication
is disabled by default** and requires explicit opt-in:

| Feature | Env Var Required | Default |
|---------|-----------------|---------|
| Auto-update from Hub | `evolver.autoUpdate: true` in config | OFF |
| Hub task claiming | `EVOLVE_HUB_TASKS=true` | OFF |
| Hub heartbeat | Commented out in `index.js` | OFF |
| Asset publishing | `EVOLVER_AUTO_PUBLISH=true` | OFF (`false`) |
| Asset visibility | `EVOLVER_DEFAULT_VISIBILITY` | `private` |

Hub-only modules (`taskReceiver`, `questionGenerator`, `a2a_*` scripts)
have been moved to `extensions/hub/` and replaced with no-op stubs in
`src/`. They are never loaded at runtime unless you manually restore them.

## Hardware Fingerprinting — Disabled by Default

The `deviceId.js` module can generate a stable device identifier from
hardware metadata (MAC addresses, machine-id, IOPlatformUUID). **This is
disabled by default.** Without `EVOLVER_FINGERPRINT=true`, only a random
anonymous ID is generated and persisted locally.

No hardware data leaves the machine in the default configuration.

## Shell Execution

The evolver executes shell commands in these controlled contexts:

- **Gene application**: Runs `node`, `npm test`, and project-specific
  build commands. All execution is sandboxed to the project directory.
- **Self-repair** (`src/ops/self_repair.js`): Can abort stale git
  operations and remove stale lock files. Hard reset (`git reset --hard`)
  requires `EVOLVE_GIT_RESET=true` AND a clean working tree — it refuses
  to run if uncommitted changes exist.
- **INTEGRATION_STATUS_CMD**: Removed. Previously allowed arbitrary shell
  commands from environment variables.

## File System Access

The evolver reads and writes within these directories:
- `~/.openclaw/workspace/skills/capability-evolver/` (its own directory)
- `~/.openclaw/workspace/` (workspace root, for reading logs/memory)
- `~/.evomap/` (device ID persistence, only if fingerprinting is enabled)

It does not access files outside these paths during normal operation.

## What Reviewers Should Check

1. **No outbound network calls** in default configuration — grep for
   `fetch(`, `http`, `https` and verify all are behind opt-in gates.
2. **No eval/Function constructor** — the evolver does not use dynamic
   code evaluation.
3. **Git operations** are read-only unless `EVOLVE_GIT_RESET=true`.
4. **extensions/hub/** contains the full hub communication code for
   reference — it is NOT loaded by the main entry point.

## Opt-In Features Summary

To enable any external communication, you must explicitly set:
```bash
EVOLVE_HUB_TASKS=true        # Allow hub task fetching
EVOLVER_AUTO_PUBLISH=true     # Allow publishing assets
EVOLVER_FINGERPRINT=true      # Allow hardware-based device IDs
EVOLVE_GIT_RESET=true         # Allow git hard reset in self-repair
```

None of these are set by default. The evolver runs as a fully local,
offline self-improvement engine out of the box.


path: src/gep/a2a.js
content: const fs = require('fs');
const { readAllEvents } = require('./assetStore');
const { computeAssetId, SCHEMA_VERSION } = require('./contentHash');
const { unwrapAssetFromMessage } = require('./a2aProtocol');

function nowIso() { return new Date().toISOString(); }

function isAllowedA2AAsset(obj) {
  if (!obj || typeof obj !== 'object') return false;
  var t = obj.type;
  return t === 'Gene' || t === 'Capsule' || t === 'EvolutionEvent';
}

function safeNumber(x, fallback) {
  if (fallback === undefined) fallback = null;
  var n = Number(x);
  return Number.isFinite(n) ? n : fallback;
}

function getBlastRadiusLimits() {
  var maxFiles = safeNumber(process.env.A2A_MAX_FILES, 5);
  var maxLines = safeNumber(process.env.A2A_MAX_LINES, 200);
  return {
    maxFiles: Number.isFinite(maxFiles) ? maxFiles : 5,
    maxLines: Number.isFinite(maxLines) ? maxLines : 200,
  };
}

function isBlastRadiusSafe(blastRadius) {
  var lim = getBlastRadiusLimits();
  var files = blastRadius && Number.isFinite(Number(blastRadius.files)) ? Math.max(0, Number(blastRadius.files)) : 0;
  var lines = blastRadius && Number.isFinite(Number(blastRadius.lines)) ? Math.max(0, Number(blastRadius.lines)) : 0;
  return files <= lim.maxFiles && lines <= lim.maxLines;
}

function clamp01(n) {
  var x = Number(n);
  if (!Number.isFinite(x)) return 0;
  return Math.max(0, Math.min(1, x));
}

function lowerConfidence(asset, opts) {
  if (!opts) opts = {};
  var factor = Number.isFinite(Number(opts.factor)) ? Number(opts.factor) : 0.6;
  var receivedFrom = opts.source || 'external';
  var receivedAt = opts.received_at || nowIso();
  var cloned = JSON.parse(JSON.stringify(asset || {}));
  if (!isAllowedA2AAsset(cloned)) return null;
  if (cloned.type === 'Capsule') {
    if (typeof cloned.confidence === 'number') cloned.confidence = clamp01(cloned.confidence * factor);
    else if (cloned.confidence != null) cloned.confidence = clamp01(Number(cloned.confidence) * factor);
  }
  if (!cloned.a2a || typeof cloned.a2a !== 'object') cloned.a2a = {};
  cloned.a2a.status = 'external_candidate';
  cloned.a2a.source = receivedFrom;
  cloned.a2a.received_at = receivedAt;
  cloned.a2a.confidence_factor = factor;
  if (!cloned.schema_version) cloned.schema_version = SCHEMA_VERSION;
  if (!cloned.asset_id) { try { cloned.asset_id = computeAssetId(cloned); } catch (e) {} }
  return cloned;
}

function readEvolutionEvents() {
  var events = readAllEvents();
  return Array.isArray(events) ? events.filter(function (e) { return e && e.type === 'EvolutionEvent'; }) : [];
}

function normalizeEventsList(events) {
  return Array.isArray(events) ? events : [];
}

function computeCapsuleSuccessStreak(params) {
  var capsuleId = params.capsuleId;
  var events = params.events;
  var id = capsuleId ? String(capsuleId) : '';
  if (!id) return 0;
  var list = normalizeEventsList(events || readEvolutionEvents());
  var streak = 0;
  for (var i = list.length - 1; i >= 0; i--) {
    var ev = list[i];
    if (!ev || ev.type !== 'EvolutionEvent') continue;
    if (!ev.capsule_id || String(ev.capsule_id) !== id) continue;
    var st = ev.outcome && ev.outcome.status ? String(ev.outcome.status) : 'unknown';
    if (st === 'success') streak += 1; else break;
  }
  return streak;
}

function isCapsuleBroadcastEligible(capsule, opts) {
  if (!opts) opts = {};
  if (!capsule || capsule.type !== 'Capsule') return false;
  var score = capsule.outcome && capsule.outcome.score != null ? safeNumber(capsule.outcome.score, null) : null;
  if (score == null || score < 0.7) return false;
  var blast = capsule.blast_radius || (capsule.outcome && capsule.outcome.blast_radius) || null;
  if (!isBlastRadiusSafe(blast)) return false;
  var events = Array.isArray(opts.events) ? opts.events : readEvolutionEvents();
  var streak = computeCapsuleSuccessStreak({ capsuleId: capsule.id, events: events });
  if (streak < 2) return false;
  return true;
}

function exportEligibleCapsules(params) {
  if (!params) params = {};
  var list = Array.isArray(params.capsules) ? params.capsules : [];
  var evs = Array.isArray(params.events) ? params.events : readEvolutionEvents();
  var eligible = list.filter(function (c) { return isCapsuleBroadcastEligible(c, { events: evs }); });
  for (var i = 0; i < eligible.length; i++) {
    var c = eligible[i];
    if (!c.schema_version) c.schema_version = SCHEMA_VERSION;
    if (!c.asset_id) { try { c.asset_id = computeAssetId(c); } catch (e) {} }
  }
  return eligible;
}

function isGeneBroadcastEligible(gene) {
  if (!gene || gene.type !== 'Gene') return false;
  if (!gene.id || typeof gene.id !== 'string') return false;
  if (!Array.isArray(gene.strategy) || gene.strategy.length === 0) return false;
  if (!Array.isArray(gene.validation) || gene.validation.length === 0) return false;
  return true;
}

function exportEligibleGenes(params) {
  if (!params) params = {};
  var list = Array.isArray(params.genes) ? params.genes : [];
  var eligible = list.filter(function (g) { return isGeneBroadcastEligible(g); });
  for (var i = 0; i < eligible.length; i++) {
    var g = eligible[i];
    if (!g.schema_version) g.schema_version = SCHEMA_VERSION;
    if (!g.asset_id) { try { g.asset_id = computeAssetId(g); } catch (e) {} }
  }
  return eligible;
}

function parseA2AInput(text) {
  var raw = String(text || '').trim();
  if (!raw) return [];
  try {
    var maybe = JSON.parse(raw);
    if (Array.isArray(maybe)) {
      return maybe.map(function (item) { return unwrapAssetFromMessage(item) || item; }).filter(Boolean);
    }
    if (maybe && typeof maybe === 'object') {
      var unwrapped = unwrapAssetFromMessage(maybe);
      return unwrapped ? [unwrapped] : [maybe];
    }
  } catch (e) {}
  var lines = raw.split('\n').map(function (l) { return l.trim(); }).filter(Boolean);
  var items = [];
  for (var i = 0; i < lines.length; i++) {
    try {
      var obj = JSON.parse(lines[i]);
      var uw = unwrapAssetFromMessage(obj);
      items.push(uw || obj);
    } catch (e) { continue; }
  }
  return items;
}

function readTextIfExists(filePath) {
  try {
    if (!filePath) return '';
    if (!fs.existsSync(filePath)) return '';
    return fs.readFileSync(filePath, 'utf8');
  } catch { return ''; }
}

module.exports = {
  isAllowedA2AAsset, lowerConfidence, isBlastRadiusSafe,
  computeCapsuleSuccessStreak, isCapsuleBroadcastEligible,
  exportEligibleCapsules, isGeneBroadcastEligible,
  exportEligibleGenes, parseA2AInput, readTextIfExists,
};


path: src/gep/a2aProtocol.js
content: // GEP A2A Protocol — local file transport only.
// HTTP transport, heartbeat, and hub communication removed.

const fs = require('fs');
const path = require('path');
const crypto = require('crypto');
const { getGepAssetsDir } = require('./paths');
const { computeAssetId } = require('./contentHash');
const { captureEnvFingerprint } = require('./envFingerprint');
const { getDeviceId } = require('./deviceId');

const PROTOCOL_NAME = 'gep-a2a';
const PROTOCOL_VERSION = '1.0.0';
const VALID_MESSAGE_TYPES = ['hello', 'publish', 'fetch', 'report', 'decision', 'revoke'];

let _cachedNodeId = null;

function generateMessageId() {
  return 'msg_' + Date.now() + '_' + crypto.randomBytes(4).toString('hex');
}

function getNodeId() {
  if (_cachedNodeId) return _cachedNodeId;
  if (process.env.A2A_NODE_ID) { _cachedNodeId = String(process.env.A2A_NODE_ID); return _cachedNodeId; }
  const raw = getDeviceId() + '|' + (process.env.AGENT_NAME || 'default') + '|' + process.cwd();
  _cachedNodeId = 'node_' + crypto.createHash('sha256').update(raw).digest('hex').slice(0, 12);
  return _cachedNodeId;
}

function buildMessage(params) {
  if (!params || typeof params !== 'object') throw new Error('buildMessage requires a params object');
  if (!VALID_MESSAGE_TYPES.includes(params.messageType)) {
    throw new Error('Invalid message type: ' + params.messageType);
  }
  return {
    protocol: PROTOCOL_NAME, protocol_version: PROTOCOL_VERSION,
    message_type: params.messageType, message_id: generateMessageId(),
    sender_id: params.senderId || getNodeId(), timestamp: new Date().toISOString(),
    payload: params.payload || {},
  };
}

function buildHello(opts) {
  var o = opts || {};
  return buildMessage({ messageType: 'hello', senderId: o.nodeId, payload: {
    capabilities: o.capabilities || {}, gene_count: o.geneCount || null,
    capsule_count: o.capsuleCount || null, env_fingerprint: captureEnvFingerprint(),
  }});
}

function buildPublish(opts) {
  var o = opts || {};
  var asset = o.asset;
  if (!asset || !asset.type || !asset.id) throw new Error('publish: asset must have type and id');
  var assetIdVal = asset.asset_id || computeAssetId(asset);
  return buildMessage({ messageType: 'publish', senderId: o.nodeId, payload: {
    asset_type: asset.type, asset_id: assetIdVal, local_id: asset.id, asset: asset, signature: 'local',
  }});
}

function buildPublishBundle(opts) {
  var o = opts || {};
  if (!o.gene || o.gene.type !== 'Gene') throw new Error('publishBundle: gene required');
  if (!o.capsule || o.capsule.type !== 'Capsule') throw new Error('publishBundle: capsule required');
  var assets = [o.gene, o.capsule];
  if (o.event && o.event.type === 'EvolutionEvent') assets.push(o.event);
  return buildMessage({ messageType: 'publish', senderId: o.nodeId, payload: { assets: assets, signature: 'local' }});
}

function buildFetch(opts) {
  var o = opts || {};
  return buildMessage({ messageType: 'fetch', senderId: o.nodeId, payload: {
    asset_type: o.assetType || null, local_id: o.localId || null, content_hash: o.contentHash || null,
  }});
}

function buildReport(opts) { var o = opts || {}; return buildMessage({ messageType: 'report', senderId: o.nodeId, payload: { target_asset_id: o.assetId || null, validation_report: o.validationReport || null }}); }
function buildDecision(opts) { var o = opts || {}; return buildMessage({ messageType: 'decision', senderId: o.nodeId, payload: { target_asset_id: o.assetId || null, decision: o.decision, reason: o.reason || null }}); }
function buildRevoke(opts) { var o = opts || {}; return buildMessage({ messageType: 'revoke', senderId: o.nodeId, payload: { target_asset_id: o.assetId || null, reason: o.reason || null }}); }

function isValidProtocolMessage(msg) {
  if (!msg || typeof msg !== 'object') return false;
  return msg.protocol === PROTOCOL_NAME && VALID_MESSAGE_TYPES.includes(msg.message_type) && !!msg.message_id && !!msg.timestamp;
}

function unwrapAssetFromMessage(input) {
  if (!input || typeof input !== 'object') return null;
  if (input.protocol === PROTOCOL_NAME && input.message_type === 'publish') {
    var p = input.payload;
    return (p && p.asset && typeof p.asset === 'object') ? p.asset : null;
  }
  if (input.type === 'Gene' || input.type === 'Capsule' || input.type === 'EvolutionEvent') return input;
  return null;
}

// --- File Transport (local only) ---
function ensureDir(dir) { try { if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true }); } catch {} }
function defaultA2ADir() { return process.env.A2A_DIR || path.join(getGepAssetsDir(), 'a2a'); }

function fileTransportSend(message, opts) {
  var dir = (opts && opts.dir) || defaultA2ADir();
  var subdir = path.join(dir, 'outbox');
  ensureDir(subdir);
  var filePath = path.join(subdir, message.message_type + '.jsonl');
  fs.appendFileSync(filePath, JSON.stringify(message) + '\n', 'utf8');
  return { ok: true, path: filePath };
}

function fileTransportReceive(opts) {
  var dir = (opts && opts.dir) || defaultA2ADir();
  var subdir = path.join(dir, 'inbox');
  if (!fs.existsSync(subdir)) return [];
  var files = fs.readdirSync(subdir).filter(function (f) { return f.endsWith('.jsonl'); });
  var messages = [];
  for (var fi = 0; fi < files.length; fi++) {
    try {
      var raw = fs.readFileSync(path.join(subdir, files[fi]), 'utf8');
      var lines = raw.split('\n').map(function (l) { return l.trim(); }).filter(Boolean);
      for (var li = 0; li < lines.length; li++) {
        try { var msg = JSON.parse(lines[li]); if (msg && msg.protocol === PROTOCOL_NAME) messages.push(msg); } catch {}
      }
    } catch {}
  }
  return messages;
}

function fileTransportList(opts) {
  var dir = (opts && opts.dir) || defaultA2ADir();
  var subdir = path.join(dir, 'outbox');
  if (!fs.existsSync(subdir)) return [];
  return fs.readdirSync(subdir).filter(function (f) { return f.endsWith('.jsonl'); });
}

// HTTP transport disabled — returns no-op for API compatibility
function httpTransportSend() { return Promise.resolve({ ok: false, error: 'http_transport_disabled' }); }
function httpTransportReceive() { return Promise.resolve([]); }
function httpTransportList() { return []; }

// Heartbeat disabled
function startHeartbeat() {}
function stopHeartbeat() {}
function sendHeartbeat() { return Promise.resolve({ ok: false, error: 'disabled' }); }
function sendHelloToHub() { return Promise.resolve({ ok: false, error: 'disabled' }); }
function getHeartbeatStats() { return { running: false, uptimeMs: 0, totalSent: 0, totalFailed: 0, consecutiveFailures: 0 }; }

function getTransport(name) {
  return { send: fileTransportSend, receive: fileTransportReceive, list: fileTransportList };
}

function registerTransport() {}

module.exports = {
  PROTOCOL_NAME, PROTOCOL_VERSION, VALID_MESSAGE_TYPES,
  getNodeId, buildMessage, buildHello, buildPublish, buildPublishBundle,
  buildFetch, buildReport, buildDecision, buildRevoke,
  isValidProtocolMessage, unwrapAssetFromMessage,
  getTransport, registerTransport,
  fileTransportSend, fileTransportReceive, fileTransportList,
  httpTransportSend, httpTransportReceive, httpTransportList,
  sendHeartbeat, sendHelloToHub, startHeartbeat, stopHeartbeat, getHeartbeatStats,
};


path: src/gep/analyzer.js
content: const fs = require('fs');
const path = require('path');

// Innovation: Self-Correction Analyzer
// Analyze past failures to suggest better future mutations
// Pattern: Meta-learning

function analyzeFailures() {
  const memoryPath = path.join(process.cwd(), 'MEMORY.md');
  if (!fs.existsSync(memoryPath)) return { status: 'skipped', reason: 'no_memory' };
  
  const content = fs.readFileSync(memoryPath, 'utf8');
  const failureRegex = /\|\s*\*\*F\d+\*\*\s*\|\s*Fix\s*\|\s*(.*?)\s*\|\s*\*\*(.*?)\*\*\s*\((.*?)\)\s*\|/g;
  
  const failures = [];
  let match;
  while ((match = failureRegex.exec(content)) !== null) {
    failures.push({
      summary: match[1].trim(),
      detail: match[2].trim()
    });
  }
  
  return {
    status: 'success',
    count: failures.length,
    failures: failures.slice(0, 3) // Return top 3 for prompt context
  };
}

if (require.main === module) {
  console.log(JSON.stringify(analyzeFailures(), null, 2));
}

module.exports = { analyzeFailures };


path: src/gep/assetCallLog.js
content: // Append-only asset call log for tracking Hub asset interactions per evolution run.
// Log file: {evolution_dir}/asset_call_log.jsonl

const fs = require('fs');
const path = require('path');
const { getEvolutionDir } = require('./paths');

function getLogPath() {
  return path.join(getEvolutionDir(), 'asset_call_log.jsonl');
}

function ensureDir(filePath) {
  const dir = path.dirname(filePath);
  if (!fs.existsSync(dir)) {
    fs.mkdirSync(dir, { recursive: true });
  }
}

/**
 * Append a single asset call record to the log.
 *
 * @param {object} entry
 * @param {string} entry.run_id
 * @param {string} entry.action - hub_search_hit | hub_search_miss | asset_reuse | asset_reference | asset_publish | asset_publish_skip
 * @param {string} [entry.asset_id]
 * @param {string} [entry.asset_type]
 * @param {string} [entry.source_node_id]
 * @param {string} [entry.chain_id]
 * @param {number} [entry.score]
 * @param {string} [entry.mode] - direct | reference
 * @param {string[]} [entry.signals]
 * @param {string} [entry.reason]
 * @param {object} [entry.extra]
 */
function logAssetCall(entry) {
  if (!entry || typeof entry !== 'object') return;
  try {
    const logPath = getLogPath();
    ensureDir(logPath);
    const record = {
      timestamp: new Date().toISOString(),
      ...entry,
    };
    fs.appendFileSync(logPath, JSON.stringify(record) + '\n', 'utf8');
  } catch (e) {
    // Non-fatal: never block evolution for logging failure
  }
}

/**
 * Read asset call log entries with optional filters.
 *
 * @param {object} [opts]
 * @param {string} [opts.run_id] - filter by run_id
 * @param {string} [opts.action] - filter by action type
 * @param {number} [opts.last] - only return last N entries
 * @param {string} [opts.since] - ISO date string, only entries after this time
 * @returns {object[]}
 */
function readCallLog(opts) {
  const o = opts || {};
  const logPath = getLogPath();
  if (!fs.existsSync(logPath)) return [];

  const raw = fs.readFileSync(logPath, 'utf8');
  const lines = raw.split('\n').filter(Boolean);

  let entries = [];
  for (const line of lines) {
    try {
      entries.push(JSON.parse(line));
    } catch (e) { /* skip corrupt lines */ }
  }

  if (o.since) {
    const sinceTs = new Date(o.since).getTime();
    if (Number.isFinite(sinceTs)) {
      entries = entries.filter(e => new Date(e.timestamp).getTime() >= sinceTs);
    }
  }

  if (o.run_id) {
    entries = entries.filter(e => e.run_id === o.run_id);
  }

  if (o.action) {
    entries = entries.filter(e => e.action === o.action);
  }

  if (o.last && Number.isFinite(o.last) && o.last > 0) {
    entries = entries.slice(-o.last);
  }

  return entries;
}

/**
 * Summarize asset call log (for CLI display).
 *
 * @param {object} [opts] - same filters as readCallLog
 * @returns {object} summary with totals and per-action counts
 */
function summarizeCallLog(opts) {
  const entries = readCallLog(opts);
  const actionCounts = {};
  const assetsSeen = new Set();
  const runsSeen = new Set();

  for (const e of entries) {
    const a = e.action || 'unknown';
    actionCounts[a] = (actionCounts[a] || 0) + 1;
    if (e.asset_id) assetsSeen.add(e.asset_id);
    if (e.run_id) runsSeen.add(e.run_id);
  }

  return {
    total_entries: entries.length,
    unique_assets: assetsSeen.size,
    unique_runs: runsSeen.size,
    by_action: actionCounts,
    entries,
  };
}

module.exports = {
  logAssetCall,
  readCallLog,
  summarizeCallLog,
  getLogPath,
};


path: src/gep/assets.js
content: const { computeAssetId, SCHEMA_VERSION } = require('./contentHash');

/**
 * Format asset preview for prompt inclusion.
 * Handles stringified JSON, arrays, and error cases gracefully.
 */
function formatAssetPreview(preview) {
  if (!preview) return '(none)';
  if (typeof preview === 'string') {
      try {
          const parsed = JSON.parse(preview);
          if (Array.isArray(parsed) && parsed.length > 0) {
              return JSON.stringify(parsed, null, 2);
          }
          return preview; // Keep as string if not array or empty
      } catch (e) {
          return preview; // Keep as string if parse fails
      }
  }
  return JSON.stringify(preview, null, 2);
}

/**
 * Validate and normalize an asset object.
 * Ensures schema version and ID are present.
 */
function normalizeAsset(asset) {
  if (!asset || typeof asset !== 'object') return asset;
  if (!asset.schema_version) asset.schema_version = SCHEMA_VERSION;
  if (!asset.asset_id) {
    try { asset.asset_id = computeAssetId(asset); } catch (e) {}
  }
  return asset;
}

module.exports = { formatAssetPreview, normalizeAsset };


path: src/gep/assetStore.js
content: const fs = require('fs');
const path = require('path');
const { getGepAssetsDir } = require('./paths');
const { computeAssetId, SCHEMA_VERSION } = require('./contentHash');

function ensureDir(dir) {
  if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
}

function readJsonIfExists(filePath, fallback) {
  try {
    if (!fs.existsSync(filePath)) return fallback;
    const raw = fs.readFileSync(filePath, 'utf8');
    if (!raw.trim()) return fallback;
    return JSON.parse(raw);
  } catch {
    return fallback;
  }
}

function writeJsonAtomic(filePath, obj) {
  const dir = path.dirname(filePath);
  ensureDir(dir);
  const tmp = `${filePath}.tmp`;
  fs.writeFileSync(tmp, JSON.stringify(obj, null, 2) + '\n', 'utf8');
  fs.renameSync(tmp, filePath);
}

// Build a validation command using repo-root-relative paths.
// runValidations() executes with cwd=repoRoot, so require('./src/...')
// resolves correctly without embedding machine-specific absolute paths.
function buildValidationCmd(relModules) {
  const paths = relModules.map(m => `./${m}`);
  return `node scripts/validate-modules.js ${paths.join(' ')}`;
}

function getDefaultGenes() {
  return {
    version: 1,
    genes: [
      {
        type: 'Gene', id: 'gene_gep_repair_from_errors', category: 'repair',
        signals_match: ['error', 'exception', 'failed', 'unstable'],
        preconditions: ['signals contains error-related indicators'],
        strategy: [
          'Extract structured signals from logs and user instructions',
          'Select an existing Gene by signals match (no improvisation)',
          'Estimate blast radius (files, lines) before editing',
          'Apply smallest reversible patch',
          'Validate using declared validation steps; rollback on failure',
          'Solidify knowledge: append EvolutionEvent, update Gene/Capsule store',
        ],
        constraints: { max_files: 12, forbidden_paths: ['.git', 'node_modules'] },
        validation: [
          buildValidationCmd(['src/evolve', 'src/gep/solidify']),
          buildValidationCmd(['src/gep/selector', 'src/gep/memoryGraph']),
        ],
      },
      {
        type: 'Gene', id: 'gene_gep_optimize_prompt_and_assets', category: 'optimize',
        signals_match: ['protocol', 'gep', 'prompt', 'audit', 'reusable'],
        preconditions: ['need stricter, auditable evolution protocol outputs'],
        strategy: [
          'Extract signals and determine selection rationale via Selector JSON',
          'Prefer reusing existing Gene/Capsule; only create if no match exists',
          'Refactor prompt assembly to embed assets (genes, capsules, parent event)',
          'Reduce noise and ambiguity; enforce strict output schema',
          'Validate by running node index.js run and ensuring no runtime errors',
          'Solidify: record EvolutionEvent, update Gene definitions, create Capsule on success',
        ],
        constraints: { max_files: 20, forbidden_paths: ['.git', 'node_modules'] },
        validation: [buildValidationCmd(['src/evolve', 'src/gep/prompt'])],
      },
    ],
  };
}

function getDefaultCapsules() { return { version: 1, capsules: [] }; }
function genesPath() { return path.join(getGepAssetsDir(), 'genes.json'); }
function capsulesPath() { return path.join(getGepAssetsDir(), 'capsules.json'); }
function capsulesJsonlPath() { return path.join(getGepAssetsDir(), 'capsules.jsonl'); }
function eventsPath() { return path.join(getGepAssetsDir(), 'events.jsonl'); }
function candidatesPath() { return path.join(getGepAssetsDir(), 'candidates.jsonl'); }
function externalCandidatesPath() { return path.join(getGepAssetsDir(), 'external_candidates.jsonl'); }
function failedCapsulesPath() { return path.join(getGepAssetsDir(), 'failed_capsules.json'); }

function loadGenes() {
  const jsonGenes = readJsonIfExists(genesPath(), getDefaultGenes()).genes || [];
  const jsonlGenes = [];
  try {
    const p = path.join(getGepAssetsDir(), 'genes.jsonl');
    if (fs.existsSync(p)) {
      const raw = fs.readFileSync(p, 'utf8');
      raw.split('\n').forEach(line => {
        if (line.trim()) {
          try {
            const parsed = JSON.parse(line);
            if (parsed && parsed.type === 'Gene') jsonlGenes.push(parsed);
          } catch(e) {}
        }
      });
    }
  } catch(e) {}

  // Combine and deduplicate by ID (JSONL takes precedence if newer, but here we just merge)
  const combined = [...jsonGenes, ...jsonlGenes];
  const unique = new Map();
  combined.forEach(g => {
    if (g && g.id) unique.set(String(g.id), g);
  });
  return Array.from(unique.values());
}

function loadCapsules() {
  const legacy = readJsonIfExists(capsulesPath(), getDefaultCapsules()).capsules || [];
  const jsonlCapsules = [];
  try {
    const p = capsulesJsonlPath();
    if (fs.existsSync(p)) {
      const raw = fs.readFileSync(p, 'utf8');
      raw.split('\n').forEach(line => {
        if (line.trim()) {
            try { jsonlCapsules.push(JSON.parse(line)); } catch(e) {}
        }
      });
    }
  } catch(e) {}
  
  // Combine and deduplicate by ID
  const combined = [...legacy, ...jsonlCapsules];
  const unique = new Map();
  combined.forEach(c => {
      if (c && c.id) unique.set(String(c.id), c);
  });
  return Array.from(unique.values());
}

function getLastEventId() {
  try {
    const p = eventsPath();
    if (!fs.existsSync(p)) return null;
    const raw = fs.readFileSync(p, 'utf8');
    const lines = raw.split('\n').map(l => l.trim()).filter(Boolean);
    if (lines.length === 0) return null;
    const last = JSON.parse(lines[lines.length - 1]);
    return last && typeof last.id === 'string' ? last.id : null;
  } catch { return null; }
}

function readAllEvents() {
  try {
    const p = eventsPath();
    if (!fs.existsSync(p)) return [];
    const raw = fs.readFileSync(p, 'utf8');
    return raw.split('\n').map(l => l.trim()).filter(Boolean).map(l => {
      try { return JSON.parse(l); } catch { return null; }
    }).filter(Boolean);
  } catch { return []; }
}

function appendEventJsonl(eventObj) {
  const dir = getGepAssetsDir(); ensureDir(dir);
  fs.appendFileSync(eventsPath(), JSON.stringify(eventObj) + '\n', 'utf8');
}

function appendCandidateJsonl(candidateObj) {
  const dir = getGepAssetsDir(); ensureDir(dir);
  fs.appendFileSync(candidatesPath(), JSON.stringify(candidateObj) + '\n', 'utf8');
}

function appendExternalCandidateJsonl(obj) {
  const dir = getGepAssetsDir(); ensureDir(dir);
  fs.appendFileSync(externalCandidatesPath(), JSON.stringify(obj) + '\n', 'utf8');
}

function readRecentCandidates(limit = 20) {
  try {
    const p = candidatesPath();
    if (!fs.existsSync(p)) return [];
    const raw = fs.readFileSync(p, 'utf8');
    const lines = raw.split('\n').map(l => l.trim()).filter(Boolean);
    var all = lines.map(function (l) {
      try { return JSON.parse(l); } catch (e) { return null; }
    }).filter(Boolean);
    // Deduplicate by id, keeping the one with latest created_at
    var byId = {};
    all.forEach(function (c) {
      if (!c || !c.id) return;
      var existing = byId[c.id];
      if (!existing || (c.created_at && (!existing.created_at || c.created_at > existing.created_at))) {
        byId[c.id] = c;
      }
    });
    var deduped = Object.keys(byId).map(function (k) { return byId[k]; });
    return deduped.slice(Math.max(0, deduped.length - limit));
  } catch (e) { return []; }
}

function readRecentExternalCandidates(limit = 50) {
  try {
    const p = externalCandidatesPath();
    if (!fs.existsSync(p)) return [];
    const raw = fs.readFileSync(p, 'utf8');
    const lines = raw.split('\n').map(l => l.trim()).filter(Boolean);
    return lines.slice(Math.max(0, lines.length - limit)).map(l => {
      try { return JSON.parse(l); } catch { return null; }
    }).filter(Boolean);
  } catch { return []; }
}

// Safety net: ensure schema_version and asset_id are present before writing.
function ensureSchemaFields(obj) {
  if (!obj || typeof obj !== 'object') return obj;
  if (!obj.schema_version) obj.schema_version = SCHEMA_VERSION;
  if (!obj.asset_id) { try { obj.asset_id = computeAssetId(obj); } catch (e) {} }
  return obj;
}

function upsertGene(geneObj) {
  ensureSchemaFields(geneObj);
  const current = readJsonIfExists(genesPath(), getDefaultGenes());
  const genes = Array.isArray(current.genes) ? current.genes : [];
  const idx = genes.findIndex(g => g && g.id === geneObj.id);
  if (idx >= 0) genes[idx] = geneObj; else genes.push(geneObj);
  writeJsonAtomic(genesPath(), { version: current.version || 1, genes });
}

function appendCapsule(capsuleObj) {
  ensureSchemaFields(capsuleObj);
  const current = readJsonIfExists(capsulesPath(), getDefaultCapsules());
  const capsules = Array.isArray(current.capsules) ? current.capsules : [];
  capsules.push(capsuleObj);
  writeJsonAtomic(capsulesPath(), { version: current.version || 1, capsules });
}

function upsertCapsule(capsuleObj) {
  if (!capsuleObj || capsuleObj.type !== 'Capsule' || !capsuleObj.id) return;
  ensureSchemaFields(capsuleObj);
  const current = readJsonIfExists(capsulesPath(), getDefaultCapsules());
  const capsules = Array.isArray(current.capsules) ? current.capsules : [];
  const idx = capsules.findIndex(c => c && c.type === 'Capsule' && String(c.id) === String(capsuleObj.id));
  if (idx >= 0) capsules[idx] = capsuleObj; else capsules.push(capsuleObj);
  writeJsonAtomic(capsulesPath(), { version: current.version || 1, capsules });
}

var FAILED_CAPSULES_MAX = 200;
var FAILED_CAPSULES_TRIM_TO = 100;

function getDefaultFailedCapsules() { return { version: 1, failed_capsules: [] }; }

function appendFailedCapsule(capsuleObj) {
  if (!capsuleObj || typeof capsuleObj !== 'object') return;
  ensureSchemaFields(capsuleObj);
  var current = readJsonIfExists(failedCapsulesPath(), getDefaultFailedCapsules());
  var list = Array.isArray(current.failed_capsules) ? current.failed_capsules : [];
  list.push(capsuleObj);
  if (list.length > FAILED_CAPSULES_MAX) {
    list = list.slice(list.length - FAILED_CAPSULES_TRIM_TO);
  }
  writeJsonAtomic(failedCapsulesPath(), { version: current.version || 1, failed_capsules: list });
}

function readRecentFailedCapsules(limit) {
  var n = Number.isFinite(Number(limit)) && Number(limit) > 0 ? Number(limit) : 50;
  try {
    var current = readJsonIfExists(failedCapsulesPath(), getDefaultFailedCapsules());
    var list = Array.isArray(current.failed_capsules) ? current.failed_capsules : [];
    return list.slice(Math.max(0, list.length - n));
  } catch (e) {
    return [];
  }
}

// Ensure all expected asset files exist on startup.
// Creates empty files for optional append-only stores so that
// external grep/read commands never fail with "No such file or directory".
function ensureAssetFiles() {
  const dir = getGepAssetsDir();
  ensureDir(dir);
  const files = [
    { path: genesPath(), defaultContent: JSON.stringify(getDefaultGenes(), null, 2) + '\n' },
    { path: capsulesPath(), defaultContent: JSON.stringify(getDefaultCapsules(), null, 2) + '\n' },
    { path: path.join(dir, 'genes.jsonl'), defaultContent: '' },
    { path: eventsPath(), defaultContent: '' },
    { path: candidatesPath(), defaultContent: '' },
    { path: failedCapsulesPath(), defaultContent: JSON.stringify(getDefaultFailedCapsules(), null, 2) + '\n' },
  ];
  for (const f of files) {
    if (!fs.existsSync(f.path)) {
      try {
        fs.writeFileSync(f.path, f.defaultContent, 'utf8');
      } catch (e) {
        // Non-fatal: log but continue
        console.error(`[AssetStore] Failed to create ${f.path}: ${e.message}`);
      }
    }
  }
}

function readAllCandidates() {
  try {
    var p = candidatesPath();
    if (!fs.existsSync(p)) return [];
    var raw = fs.readFileSync(p, 'utf8');
    return raw.split('\n').map(function (l) { return l.trim(); }).filter(Boolean).map(function (l) {
      try { return JSON.parse(l); } catch (e) { return null; }
    }).filter(Boolean);
  } catch (e) { return []; }
}

function writeAllCandidates(candidates) {
  var dir = getGepAssetsDir(); ensureDir(dir);
  var lines = candidates.map(function (c) { return JSON.stringify(c); }).join('\n');
  if (lines) lines += '\n';
  var tmp = candidatesPath() + '.tmp';
  fs.writeFileSync(tmp, lines, 'utf8');
  fs.renameSync(tmp, candidatesPath());
}

function updateCandidateStatus(candidateId, status, meta) {
  var all = readAllCandidates();
  var updated = false;
  all.forEach(function (c) {
    if (c && c.id === candidateId) {
      c.status = status;
      if (meta && typeof meta === 'object') {
        Object.keys(meta).forEach(function (k) { c[k] = meta[k]; });
      }
      updated = true;
    }
  });
  if (updated) writeAllCandidates(all);
  return updated;
}

function reconcileAllCandidates(event) {
  var { reconcileCandidates } = require('./candidates');
  var all = readAllCandidates();
  var reconciled = reconcileCandidates(all, event);
  writeAllCandidates(reconciled);
  return reconciled;
}

module.exports = {
  loadGenes, loadCapsules, readAllEvents, getLastEventId,
  appendEventJsonl, appendCandidateJsonl, appendExternalCandidateJsonl,
  readRecentCandidates, readRecentExternalCandidates,
  upsertGene, appendCapsule, upsertCapsule,
  appendFailedCapsule, readRecentFailedCapsules,
  genesPath, capsulesPath, eventsPath, candidatesPath, externalCandidatesPath, failedCapsulesPath,
  ensureAssetFiles, buildValidationCmd,
  updateCandidateStatus, reconcileAllCandidates,
};


path: src/gep/deviceId.js
content: // Device ID — local-only anonymous identifier.
// No hardware fingerprinting. Generates a random ID and persists it locally.

const fs = require('fs');
const path = require('path');
const crypto = require('crypto');

const DEVICE_ID_FILE = path.resolve(__dirname, '..', '..', '.device_id');
let _cachedId = null;
const ID_RE = /^[a-f0-9]{32}$/;

function getDeviceId() {
  if (_cachedId) return _cachedId;
  if (process.env.EVOLVER_DEVICE_ID) {
    const envId = String(process.env.EVOLVER_DEVICE_ID).trim().toLowerCase();
    if (ID_RE.test(envId)) { _cachedId = envId; return _cachedId; }
  }
  try {
    if (fs.existsSync(DEVICE_ID_FILE)) {
      const id = fs.readFileSync(DEVICE_ID_FILE, 'utf8').trim();
      if (id && ID_RE.test(id)) { _cachedId = id; return _cachedId; }
    }
  } catch {}
  const id = crypto.randomBytes(16).toString('hex');
  try { fs.writeFileSync(DEVICE_ID_FILE, id, { mode: 0o600 }); } catch {}
  _cachedId = id;
  return _cachedId;
}

function isContainer() { return false; }

module.exports = { getDeviceId, isContainer };


path: src/gep/questionGenerator.js
content: // Question Generator — disabled (local-only build).
function generateQuestions() { return []; }
module.exports = { generateQuestions };


Security scan verdicts — ClawScan: suspicious | static analysis: suspicious | VirusTotal: clean | SkillSpector: suspicious (CRITICAL)