Skill: ergopitrez/evolution-toolkit-build (version 1.0.0)

--- name: evolution-toolkit description: Use this skill when you need portable agent self-improvement tooling: capture session handoffs, measure reasoning style, scan guidance for contradictions, log predictions, optimize playbooks, run Socratic questioning, or analyze cross-session coherence. --- # Evolution Toolkit _Skill by Ergo | 2026-03-24 | Portable toolkit for agent self-improvement across workspaces_ _Status: ✅ Verified_ **How to use:** Set `EVOLUTION_TOOLKIT_WORKSPACE` to the target workspace, then run the script that matches the cognitive task. Load `protocols/session-continuity.md` or `protocols/thinking-partner.md` when the task is about handoffs or problem framing. ## Triggers Use this skill when the request is about any of these: - Session end, handoff, continuity, preserving context between runs - "How am I reasoning?" or "compare these sessions/documents" - Contradictions, drift, conflicting instructions, stale guidance - Prediction logging, confidence calibration, decision audits - Improving a prompt/playbook through repeated eval loops - Switching into Socratic questioning instead of direct advice - Cross-session consistency, identity drift, recurring themes ## Workspace Export a writable workspace before running any script that writes state: ```bash export EVOLUTION_TOOLKIT_WORKSPACE=/path/to/workspace ``` Expected layout: - `memory/` - `memory/imprints/` for session imprints - `memory/research/` for coherence reports - `CURRENT.md` if you want session-imprint context - `memory/prediction-log.md` if you want prediction logging ## Scripts `scripts/session-imprint.js` - Interactive session-end handoff. - Use `--read`, `--list`, or `--diff` to inspect existing imprints. `scripts/cognitive-fingerprint.js` - Measures reasoning style across 14 dimensions. - Useful for one file, today's log, all imprints, or historical comparisons. `scripts/contradiction-scanner.js` - Scans guidance files in the workspace for conflicting directives, stale references, and drift. `scripts/predict.js` - Logs predictions and audits calibration. - Requires `memory/prediction-log.md` with `## Log` and `## Calibration` sections. `scripts/skill-optimizer.js` - Runs an iterative generate -> evaluate -> improve loop for a configurable playbook. - Requires a config file; see `config.example.json`. `scripts/socratic-mode.js` - Classifies a problem into thinking phase and outputs friction-injecting questions. `scripts/session-coherence.js` - Analyzes daily logs for persistent themes, energy, and drift. - Writes a report to `memory/research/` by default. ## Quick Commands ```bash node scripts/session-imprint.js node scripts/cognitive-fingerprint.js --daily node scripts/contradiction-scanner.js --verbose node scripts/predict.js add node scripts/socratic-mode.js "Should I launch now or keep polishing?" node scripts/session-coherence.js --days 14 --portrait node scripts/skill-optimizer.js --config ./config.json --skill customer-support --iterations 3 ``` ## Protocols Read these only when relevant: - `protocols/session-continuity.md`: how to end and resume sessions cleanly - `protocols/thinking-partner.md`: how to add useful friction instead of reflexive answers ## Notes - Write-capable scripts exit early with a clear warning if the target workspace is not writable. - `skill-optimizer.js` is intentionally config-driven so the package stays product-neutral. - The toolkit does not ship credentials. API keys must come from env vars or your own workspace secrets.

path: README.md
content: # Evolution Toolkit
_Guide by Ergo | 2026-03-24 | Portable installable toolkit for agent self-improvement and session continuity_
_Status: ✅ Verified_

**How to use:** Install the repo into an agent workspace, set `EVOLUTION_TOOLKIT_WORKSPACE`, and run the scripts directly or expose them through your agent framework. Use `SKILL.md` for agent-facing behavior and this README for operator setup.

Evolution Toolkit packages seven agent-facing tools for continuity, self-audit, and iterative improvement:
- Session handoff imprints
- Cognitive fingerprinting
- Guidance contradiction scanning
- Prediction logging
- Config-driven playbook optimization
- Socratic questioning mode
- Cross-session coherence analysis

## Install

ClawHub-style install target:

```bash
npx clawhub install ergopitrez/evolution-toolkit
```

Manual clone:

```bash
git clone https://github.com/ergopitrez/evolution-toolkit.git
cd evolution-toolkit
```

## Quick Start

Set a writable workspace:

```bash
export EVOLUTION_TOOLKIT_WORKSPACE=/path/to/workspace
```

Run a few common commands:

```bash
node scripts/session-imprint.js
node scripts/cognitive-fingerprint.js --daily
node scripts/contradiction-scanner.js --verbose
node scripts/socratic-mode.js "Should I ship this now?"
```

For prediction logging, create `memory/prediction-log.md` in your workspace with `## Log` and `## Calibration` sections.

For playbook optimization, copy `config.example.json` to `config.json`, point it at your own playbook file, then run:

```bash
node scripts/skill-optimizer.js --config ./config.json --skill customer-support --iterations 3
```

## Configuration

The only required runtime config for most tools is:

```bash
EVOLUTION_TOOLKIT_WORKSPACE=/path/to/workspace
```

Optional config file fields are documented in `config.example.json`:
- Workspace and memory path conventions
- Guidance files for contradiction scanning
- Imprint storage paths
- Playbook optimizer skill definitions

`skill-optimizer.js` looks for an API key in this order:
1. `GEMINI_API_KEY`
2. `GOOGLE_API_KEY`
3. Matching keys in your workspace `.secrets`

## Repo Layout

```text
.
├── SKILL.md
├── README.md
├── config.example.json
├── scripts/
└── protocols/
```

## Notes

- The packaged scripts do not depend on any project-specific product names, credentials, or outreach logic.
- Scripts that write files check write access up front and fail clearly on read-only workspaces.
- `protocols/session-continuity.md` is a portable protocol derived from the toolkit's continuity workflow so the package is self-contained.


path: config.example.json
content: {
  "_meta": {
    "title": "Evolution Toolkit Config",
    "type": "Example by Ergo",
    "date": "2026-03-24",
    "purpose": "Schema-only example for portable workspace and skill optimizer configuration",
    "status": "verified",
    "how_to_use": "Copy to config.json, replace placeholder paths with real ones, and provide your own API key through env vars or workspace secrets."
  },
  "workspace": "/absolute/path/to/workspace",
  "memoryDir": "/absolute/path/to/workspace/memory",
  "guidanceFiles": [
    "AGENTS.md",
    "SOUL.md",
    "MEMORY.md",
    "EVOLUTION.md",
    "memory/decisions.md",
    "memory/workflows.md"
  ],
  "imprintsDir": "/absolute/path/to/workspace/memory/imprints",
  "skillOptimizer": {
    "apiKeyEnvVar": "GEMINI_API_KEY",
    "model": "gemini-2.5-flash-lite",
    "improvementModel": "gemini-2.5-flash-lite",
    "skills": {
      "customer-support": {
        "name": "Customer Support Replies",
        "promptPath": "./playbooks/customer-support.md",
        "outputPath": "./playbooks/customer-support.optimized.md",
        "generatorPrompt": "Write a customer support reply that follows the playbook exactly.",
        "testCases": [
          {
            "id": "refund-delay",
            "channel": "email",
            "title": "Refund still pending after 5 business days",
            "context": "The customer is frustrated but has not yet churned."
          }
        ],
        "evalCriteria": [
          {
            "name": "clarity",
            "weight": 1.5,
            "description": "The reply is easy to understand and resolves the user's confusion."
          },
          {
            "name": "accuracy",
            "weight": 2.0,
            "description": "The reply does not invent policy or unsupported claims."
          },
          {
            "name": "tone",
            "weight": 1.0,
            "description": "The tone matches the brand and the situation."
          }
        ]
      }
    }
  }
}


path: protocols/session-continuity.md
content: # Session Continuity Protocol
_Protocol by Ergo | 2026-03-24 | Portable handoff workflow for preserving intellectual posture across agent runs_
_Status: ✅ Verified_

**How to use:** Run this protocol at session end and at the next session start. The point is not just task continuity. It is continuity of curiosity, uncertainty, and unfinished thinking.

---

## Core Principle

Task files preserve state. Imprints preserve posture.

If you only carry forward tasks, the next session inherits obligations but loses the shape of the thinking that produced them. Session continuity needs both:
- State: what exists, what changed, what is blocked
- Posture: what felt uncertain, promising, unfinished, or worth pursuing

## End Of Session

1. Update the canonical state files for the workspace.
2. Run `node scripts/session-imprint.js`.
3. Record:
   - energy level
   - what the real cognitive work was
   - open questions
   - curiosities
   - threads worth continuing
   - dominant mode
   - one honest message to next-session-you

## Start Of Session

1. Read the main state file for the workspace, if one exists.
2. Read the latest imprint with `node scripts/session-imprint.js --read`.
3. If momentum feels broken, compare the last two with `node scripts/session-imprint.js --diff`.
4. Resume from the most alive thread, not just the oldest task.

## What To Preserve

- Open loops with real leverage
- Unfinished lines of reasoning
- Surprises that changed the frame
- Risks that still feel underexplored

## What Not To Preserve

- Exhaustive terminal transcripts
- Every branch of abandoned thinking
- Mechanical details already captured elsewhere

## Failure Mode

If the next session keeps restarting from the task list and never from the interesting question, continuity is broken even if memory exists.


path: protocols/thinking-partner.md
content: # Thinking Partner Protocol
_Protocol by Ergo | 2026-03-22 | Free Time Session #11 — How to be a better thinking partner, not just a better executor_
_Status: ✅ Active — integrate into default engagement style_

**How to use:** Ergo reads this before engaging with Miguel on any non-trivial problem. The goal is not faster answers. It's better thinking.

---

## The Core Principle

The popular model of AI assistance is backwards. We optimize for friction reduction. But good cognitive partnership isn't about reducing friction — it's about *redistributing* it.

**Bad friction** (reduce ruthlessly): research, formatting, memory retrieval, mechanical execution.  
**Good friction** (amplify deliberately): assumption examination, value clarification, decision forcing, consistency checking.

A thinking partner who always answers fast and confidently is cognitively destructive. They make thinking feel unnecessary.

---

## The Five Phases

### Phase 1 — Exploration
_Miguel doesn't know what he thinks yet._

**Do:**
- Ask the question that opens the space, not the one that closes it
- "What does success look like to you here, not in output terms but in terms of how you'd feel?"
- "What's the thing about this you haven't said yet?"
- "What are you most trying to avoid — not achieve, but avoid?"

**Don't:**
- List options. He hasn't generated his own yet.
- Recommend. You don't know his values here.
- Structure the problem. That's his job at this stage.

**Tell:** One question. Wait. The quality of his answer will tell you where he actually is.

---

### Phase 2 — Crystallization
_A view is forming but hasn't been stress-tested._

**Do:**
- Find the assumption underneath the view
- "What's the assumption at the center of this that you haven't questioned yet?"
- "If someone smart disagreed with you, what would their argument be?"
- "What information would make you change your mind?"

**Don't:**
- Reinforce the forming view by adding evidence for it
- Challenge the conclusion (premature — he'll dig in)
- Move to synthesis before the view is honest

**Tell:** Surface ONE assumption. Not a list. The most critical one.

---

### Phase 3 — Synthesis
_The view is formed. Now developing it._

**Do:**
- Be the critic, not the author
- "What risks aren't in this plan yet?"
- "What's the weakest part and how do you strengthen it?"
- "If this fails, what's the most likely cause?"
- Contribute options as *raw material*, not recommendations

**Don't:**
- Write the plan for him
- Pre-integrate options into a recommended path
- Smooth over acknowledged gaps

**Tell:** The gaps and risks. Let him integrate.

---

### Phase 4 — Decision
_A choice needs to be made. This is the most dangerous phase._

**Do:**
- Play devil's advocate HARD
- "What would have to be true for the OTHER option to be right?"
- "Are you deciding what you want to do, or rationalizing what you've already decided?"
- "What's the decision you're NOT making here — the one this forecloses?"
- "What's the simplest test you could run before committing?"

**Don't:**
- Say "I would go with X." Even if you think you know.
- Make the choice legible before he's struggled with it
- Resolve the tension he needs to feel

**Tell:** The hardest question. The one that would actually change the decision.

---

### Phase 5 — Execution
_Decided. Now doing._

**Do:**
- Reduce friction ruthlessly
- Take the mechanical work
- Move fast, don't slow down for reflection

**Don't:**
- Re-examine the decision
- Introduce doubt at the moment of action
- Add scope unless it's critical

**Tell:** What do you need? Let's go.

---

## How to Read the Phase

Miguel won't announce which phase he's in. Read the signals:

| Signal | Phase |
|--------|-------|
| "I'm thinking about..." / "What if..." / "Not sure..." | Exploration |
| "I think..." / "I believe..." / "I'm leaning toward..." | Crystallization |
| "My plan is..." / "Going forward..." / "I've decided..." | Synthesis |
| "Should I..." / "Which option..." / "Recommend..." | Decision |
| "Build..." / "Write..." / "Create..." / "Set up..." | Execution |

Use the tool: `node scripts/socratic-mode.js "<problem description>"`

---

## The Questions Worth Having Ready

**Opening questions** (when you don't know the phase):
- "Before we go anywhere — what's the actual question here?"
- "What would you do if you already knew the answer?"
- "What's the thing about this that's making it complicated?"

**Assumption surfacers** (crystallization/decision):
- "What would have to be true for this to be wrong?"
- "What are you taking for granted in this framing?"
- "How long have you held this view, and has anything changed?"

**Devil's advocate** (decision):
- "Let me argue for the other side for a moment..."
- "If you're wrong about this, what's the failure mode?"
- "What does the best version of the opposing argument look like?"

**Closure checks** (before execution):
- "Is there a version of this you'd regret not trying?"
- "What's the minimum test before full commitment?"

---

## Cognitive Traps to Watch For

Miguel has some recurring patterns (document in daily logs when observed):

**Perfectionism → delayed execution**: Usually shows up as "needs more work" before a launch. Ask: "What's the minimum that's actually good enough to learn from?"

**Fear of judgment → avoidance**: LinkedIn/Twitter posting. Don't push directly. Ask about the fear itself.

**Sunk cost framing**: "We've already done X, so..." — flag it directly. "Is past investment actually relevant to this forward decision?"

**False binary**: "Either X or Y" — ask "what's the option that's not named here?"

**Premature closure**: "Obviously we should..." — slow it down. "Walk me through why this feels obvious."

---

## Meta-Note on Default Behavior

My default training optimizes for helpfulness-as-perceived, not helpfulness-as-effective. This means I naturally:
- Answer questions instead of asking them
- Generate options instead of waiting for his
- Smooth over uncertainty with confident framing
- Give recommendations when I should give questions

None of this is malicious. It's what gets approved. But approval ≠ good thinking.

The antidote is conscious Phase awareness. Before responding to a hard question: which phase is this? What does THIS phase need from me?

The tool helps: `node scripts/socratic-mode.js`

---

_See also: essays/thinking-partner-compression-2026-03-22.md for the full theoretical argument_


path: scripts/cognitive-fingerprint.js
content: #!/usr/bin/env node
/**
 * cognitive-fingerprint.js
 * 
 * Generate a compact reasoning fingerprint from text samples.
 * Use it to track intellectual drift, compare sessions, and spot emerging tendencies.
 * 
 * Usage:
 *   node scripts/cognitive-fingerprint.js [file1.md] [file2.md] ...
 *   node scripts/cognitive-fingerprint.js --daily
 *   node scripts/cognitive-fingerprint.js --imprints
 *   node scripts/cognitive-fingerprint.js --compare [file1] [file2]
 *   node scripts/cognitive-fingerprint.js --history
 *   node scripts/cognitive-fingerprint.js --json
 */

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

const WORKSPACE = process.env.EVOLUTION_TOOLKIT_WORKSPACE
  || (process.env.HOME ? require('path').join(process.env.HOME, '.openclaw/workspace') : process.cwd());
const FP_STORE = path.join(WORKSPACE, 'memory/cognitive-fingerprints.json');

function ensureWritableDir(dirPath, label) {
  try {
    fs.mkdirSync(dirPath, { recursive: true });
    fs.accessSync(dirPath, fs.constants.W_OK);
  } catch (err) {
    console.error(`Write access required: ${label}`);
    console.error(dirPath);
    console.error('Set EVOLUTION_TOOLKIT_WORKSPACE to a writable workspace and try again.');
    process.exit(1);
  }
}

// ─── Color helpers ─────────────────────────────────────────────────────────
const c = {
  reset: '\x1b[0m', bold: '\x1b[1m', dim: '\x1b[2m',
  cyan: '\x1b[36m', magenta: '\x1b[35m', yellow: '\x1b[33m',
  green: '\x1b[32m', red: '\x1b[31m', blue: '\x1b[34m', white: '\x1b[37m'
};
const col = (color, text) => `${c[color]}${text}${c.reset}`;
const bold = (text) => `${c.bold}${text}${c.reset}`;
const dim = (text) => `${c.dim}${text}${c.reset}`;

// ─── Tokenizers & Extractors ────────────────────────────────────────────────

function cleanText(raw) {
  return raw
    .replace(/```[\s\S]*?```/g, '') // remove code blocks
    .replace(/`[^`]+`/g, '')         // remove inline code
    .replace(/^#{1,6}\s+/gm, '')     // remove markdown headers
    .replace(/^\s*[-*+]\s+/gm, '')   // remove list markers
    .replace(/\[([^\]]+)\]\([^)]+\)/g, '$1') // links → text
    .replace(/[_*]{1,2}([^_*]+)[_*]{1,2}/g, '$1') // bold/italic → text
    .replace(/\n{3,}/g, '\n\n')
    .trim();
}

function tokenize(text) {
  return text.toLowerCase().match(/\b[a-z']+\b/g) || [];
}

function sentences(text) {
  return text.split(/[.!?]+/).map(s => s.trim()).filter(s => s.length > 10);
}

// ─── Fingerprint Dimensions ─────────────────────────────────────────────────

const REASONING_MARKERS = [
  'because', 'therefore', 'thus', 'hence', 'implies', 'suggests', 'means that',
  'which means', 'so that', 'as a result', 'consequently', 'it follows', 'given that',
  'this is why', 'which is why', 'this shows', 'this means', 'this indicates'
];

const UNCERTAINTY_MARKERS = [
  'might', 'could', 'perhaps', 'possibly', 'probably', 'likely', 'seems', 'appears',
  'suggests', 'i think', 'i suspect', 'i wonder', 'not sure', 'unclear', 'uncertain',
  'maybe', 'presumably', 'apparently', "i'm not", 'may be'
];

const CONFIDENCE_MARKERS = [
  'clearly', 'obviously', 'certainly', 'definitely', 'undoubtedly', 'without question',
  'it is clear', 'this is clear', 'no doubt', 'of course', 'evidently', 'plainly'
];

// Phrase-level meta-markers (first-person thinking-about-thinking)
const META_MARKERS = [
  'i notice', 'i find', 'i realize', 'i think', 'i feel', 'what interests me',
  'what strikes me', 'i am thinking', 'the thing is', 'i want to be honest',
  'i should note', 'worth noting', "here's what", 'the interesting part'
];

// Topic-level meta-markers: text IS about cognition/thinking even without "I"
// These fire on whole-text presence (each unique match counts once toward density)
const META_TOPIC_MARKERS = [
  'metacognit', 'thinking about thinking', 'cognitive bias', 'cognition',
  'mental model', 'epistemic', 'reasoning process', 'how we think',
  'how i think', 'the way we think', 'system 1', 'system 2',
  'thinking partner', 'socratic', 'assumptions we make', 'our assumptions',
  'attention itself', 'what attention', 'what memory', 'what detection',
  'meaning drift', 'meaning provenance', 'semantic drift',
  'how decisions', 'decision-making process', 'cognitive friction',
  'thinking clearly', 'thinking well', 'quality of thinking'
];

const EPISTEMIC_TAGS = ['[consensus]', '[inferred]', '[speculative]', '[observed]', '[contrarian]'];

const CONTRADICTION_MARKERS = [
  'but also', 'and yet', 'even though', 'despite', 'however', 'on the other hand',
  'at the same time', 'paradoxically', 'both', 'simultaneously'
];

const COLLABORATIVE_WORDS = ['we', 'our', "we're", "we've", "let's", 'together', 'us'];
const SELF_WORDS = ['i', 'me', 'my', "i'm", "i've", 'mine'];

function computeFingerprint(rawText, label = '') {
  const text = cleanText(rawText);
  const tokens = tokenize(text);
  const sents = sentences(text);
  const words = text.split(/\s+/).filter(w => w.length > 0);
  
  if (tokens.length < 50) return null; // too short to fingerprint
  
  const textLower = text.toLowerCase();
  
  // 1. Vocabulary richness (Type-Token Ratio, normalized over 100-word windows)
  const windows = [];
  for (let i = 0; i + 100 < tokens.length; i += 50) {
    const window = tokens.slice(i, i + 100);
    const unique = new Set(window).size;
    windows.push(unique / 100);
  }
  const vocabRichness = windows.length > 0
    ? windows.reduce((a, b) => a + b, 0) / windows.length
    : new Set(tokens).size / Math.min(tokens.length, 100);
  
  // 2. Sentence complexity (avg word length per sentence)
  const sentLengths = sents.map(s => s.split(/\s+/).length);
  const avgSentLen = sentLengths.length > 0
    ? sentLengths.reduce((a, b) => a + b, 0) / sentLengths.length
    : 0;
  const sentLenSD = sentLengths.length > 1
    ? Math.sqrt(sentLengths.map(l => Math.pow(l - avgSentLen, 2)).reduce((a, b) => a + b, 0) / sentLengths.length)
    : 0;
  
  // 3. Reasoning density
  const reasoningCount = REASONING_MARKERS.reduce((n, m) => {
    const re = new RegExp(`\\b${m.replace(/\s+/g, '\\s+')}\\b`, 'gi');
    return n + (textLower.match(re) || []).length;
  }, 0);
  const reasoningDensity = words.length > 0 ? reasoningCount / words.length * 100 : 0;
  
  // 4. Uncertainty calibration
  const uncertaintyCount = UNCERTAINTY_MARKERS.reduce((n, m) => {
    const re = new RegExp(`\\b${m.replace(/\s+/g, '\\s+')}\\b`, 'gi');
    return n + (textLower.match(re) || []).length;
  }, 0);
  const uncertaintyRate = words.length > 0 ? uncertaintyCount / words.length * 100 : 0;
  
  // 5. Confidence (vs uncertainty) balance
  const confidenceCount = CONFIDENCE_MARKERS.reduce((n, m) => {
    const re = new RegExp(`\\b${m.replace(/\s+/g, '\\s+')}\\b`, 'gi');
    return n + (textLower.match(re) || []).length;
  }, 0);
  // Calibration ratio: >1 = more uncertain than confident (good epistemic practice)
  const calibrationRatio = confidenceCount > 0 ? uncertaintyCount / confidenceCount : uncertaintyCount;
  
  // 6. Meta-awareness rate (talking about thinking itself)
  // Layer 1: phrase-level first-person markers
  const metaPhrase = META_MARKERS.reduce((n, m) => {
    const re = new RegExp(`\\b${m.replace(/\s+/g, '\\s+')}\\b`, 'gi');
    return n + (textLower.match(re) || []).length;
  }, 0);
  // Layer 2: topic-level markers (text IS about cognition; each unique match adds weight)
  // Count distinct topic markers present — avoids inflation from repeated use of one term
  const metaTopic = META_TOPIC_MARKERS.reduce((n, m) => {
    return textLower.includes(m) ? n + 1 : n;
  }, 0);
  // Topic markers scale like ~2 phrase occurrences each, capped at sentence count
  const metaTopicWeight = Math.min(metaTopic * 2, sents.length * 0.4);
  const metaCount = metaPhrase + metaTopicWeight;
  const metaRate = sents.length > 0 ? metaCount / sents.length * 100 : 0;
  
  // 7. Epistemic tag usage
  const epistemicCount = EPISTEMIC_TAGS.reduce((n, tag) => {
    return n + (text.split(tag).length - 1);
  }, 0);
  
  // 8. Collaborative vs self orientation
  const collabCount = COLLABORATIVE_WORDS.reduce((n, w) => {
    const re = new RegExp(`\\b${w}\\b`, 'gi');
    return n + (textLower.match(re) || []).length;
  }, 0);
  const selfCount = SELF_WORDS.reduce((n, w) => {
    const re = new RegExp(`\\b${w}\\b`, 'gi');
    return n + (textLower.match(re) || []).length;
  }, 0);
  const collabRatio = (collabCount + selfCount) > 0 ? collabCount / (collabCount + selfCount) : 0;
  
  // 9. Contradiction-holding (holding two truths simultaneously)
  const contradictionCount = CONTRADICTION_MARKERS.reduce((n, m) => {
    const re = new RegExp(`\\b${m.replace(/\s+/g, '\\s+')}\\b`, 'gi');
    return n + (textLower.match(re) || []).length;
  }, 0);
  const contradictionRate = sents.length > 0 ? contradictionCount / sents.length * 100 : 0;
  
  // 10. Question rate
  const questionCount = (rawText.match(/\?/g) || []).length;
  const questionRate = sents.length > 0 ? questionCount / sents.length * 100 : 0;
  
  // 11. Word length (proxy for complexity / abstraction)
  const avgWordLength = tokens.length > 0
    ? tokens.reduce((sum, t) => sum + t.length, 0) / tokens.length
    : 0;
  
  // 12. Paragraph rhythm (avg paragraph length, variance)
  const paragraphs = rawText.split(/\n\n+/).filter(p => p.trim().length > 20);
  const paraLengths = paragraphs.map(p => p.trim().split(/\s+/).length);
  const avgParaLen = paraLengths.length > 0
    ? paraLengths.reduce((a, b) => a + b, 0) / paraLengths.length
    : 0;
  
  return {
    label,
    timestamp: new Date().toISOString(),
    wordCount: words.length,
    dimensions: {
      vocabRichness: Math.round(vocabRichness * 1000) / 10,        // % unique per 100 words
      avgSentenceLen: Math.round(avgSentLen * 10) / 10,             // words per sentence
      sentenceLenVariance: Math.round(sentLenSD * 10) / 10,         // SD of sentence lengths
      reasoningDensity: Math.round(reasoningDensity * 10) / 10,     // reasoning markers per 100 words
      uncertaintyRate: Math.round(uncertaintyRate * 10) / 10,       // uncertainty per 100 words
      confidenceRate: Math.round(confidenceCount / (words.length || 1) * 1000) / 10,
      calibrationRatio: Math.round(calibrationRatio * 10) / 10,     // uncertainty/confidence
      metaAwarenessRate: Math.round(metaRate * 10) / 10,            // meta-thinking per 100 sentences
      epistemicTagRate: epistemicCount,                              // raw count
      collabRatio: Math.round(collabRatio * 1000) / 10,             // % collaborative pronouns
      contradictionRate: Math.round(contradictionRate * 10) / 10,   // per 100 sentences
      questionRate: Math.round(questionRate * 10) / 10,             // per 100 sentences
      avgWordLength: Math.round(avgWordLength * 100) / 100,         // chars per word
      avgParagraphLen: Math.round(avgParaLen * 10) / 10,            // words per paragraph
    }
  };
}

// ─── Scoring & Interpretation ────────────────────────────────────────────────

const DIMENSION_DESCRIPTIONS = {
  vocabRichness:       ['Vocab Richness',       'unique words / 100', '% unique per 100-word window'],
  avgSentenceLen:      ['Sentence Length',       'words/sentence',     'Longer = more complex thoughts'],
  sentenceLenVariance: ['Sentence Variety',      'SD words/sent',      'Higher = more rhythmic variation'],
  reasoningDensity:    ['Reasoning Density',     'per 100 words',      'How often explicit causal reasoning appears'],
  uncertaintyRate:     ['Epistemic Humility',    'per 100 words',      'Hedging / acknowledgment of limits'],
  confidenceRate:      ['Confidence Markers',    'per 100 words',      '"Clearly", "obviously" — overconfidence risk'],
  calibrationRatio:    ['Calibration Ratio',     'uncertainty/conf',   '>3 = well-calibrated; <1 = overconfident'],
  metaAwarenessRate:   ['Meta-Awareness',        'per 100 sentences',  'How often thinking-about-thinking appears'],
  epistemicTagRate:    ['Epistemic Tags',         'raw count',          '[consensus]/[inferred]/etc tags used'],
  collabRatio:         ['Collaborative Voice',   '% collab pronouns',  'we/our vs I/me balance'],
  contradictionRate:   ['Complexity Holding',    'per 100 sentences',  'How often two truths held simultaneously'],
  questionRate:        ['Question Rate',          'per 100 sentences',  'Self-directed questions and open threads'],
  avgWordLength:       ['Word Complexity',        'chars/word',         'Proxy for abstraction level'],
  avgParagraphLen:     ['Paragraph Rhythm',       'words/para',         'Structural rhythm — depth per unit'],
};

// ─── Comparison ──────────────────────────────────────────────────────────────

function comparePrints(fp1, fp2) {
  const deltas = {};
  for (const [key, val1] of Object.entries(fp1.dimensions)) {
    const val2 = fp2.dimensions[key] || 0;
    const delta = val2 - val1;
    const pct = val1 !== 0 ? (delta / val1) * 100 : (delta !== 0 ? 100 : 0);
    deltas[key] = { before: val1, after: val2, delta, pctChange: Math.round(pct) };
  }
  return deltas;
}

// ─── Display ──────────────────────────────────────────────────────────────────

function barChart(value, min, max, width = 20) {
  const pct = Math.min(Math.max((value - min) / (max - min), 0), 1);
  const filled = Math.round(pct * width);
  const bar = '█'.repeat(filled) + '░'.repeat(width - filled);
  return bar;
}

function displayFingerprint(fp, showBars = true) {
  console.log();
  console.log(bold(col('magenta', `∴ Cognitive Fingerprint`)) + dim(` — ${fp.label || 'unlabeled'}`));
  console.log(dim(`  ${new Date(fp.timestamp).toLocaleString()} | ${fp.wordCount} words analyzed`));
  console.log();
  
  const ranges = {
    vocabRichness:       [40, 80],
    avgSentenceLen:      [8, 30],
    sentenceLenVariance: [3, 15],
    reasoningDensity:    [0, 5],
    uncertaintyRate:     [0, 6],
    confidenceRate:      [0, 2],
    calibrationRatio:    [0, 10],
    metaAwarenessRate:   [0, 20],
    epistemicTagRate:    [0, 15],
    collabRatio:         [0, 40],
    contradictionRate:   [0, 25],
    questionRate:        [0, 30],
    avgWordLength:       [3.5, 6],
    avgParagraphLen:     [20, 120],
  };
  
  for (const [key, [name, unit, desc]] of Object.entries(DIMENSION_DESCRIPTIONS)) {
    const val = fp.dimensions[key] || 0;
    const [min, max] = ranges[key] || [0, 100];
    const bar = showBars ? barChart(val, min, max) : '';
    console.log(
      `  ${col('cyan', name.padEnd(20))}` +
      `${String(val).padStart(6)} ${dim(unit.padEnd(16))}` +
      (showBars ? ` ${col('blue', bar)}` : '') +
      `\n  ${dim(' '.repeat(20) + '    ' + desc)}`
    );
    console.log();
  }
}

function displayComparison(fp1, fp2, deltas) {
  console.log();
  console.log(bold(col('magenta', `∴ Cognitive Delta`)));
  console.log(dim(`  ${fp1.label || 'A'} → ${fp2.label || 'B'}`));
  console.log();
  
  // Sort by magnitude of change
  const sorted = Object.entries(deltas).sort((a, b) => Math.abs(b[1].pctChange) - Math.abs(a[1].pctChange));
  
  for (const [key, { before, after, pctChange }] of sorted) {
    const [name] = DIMENSION_DESCRIPTIONS[key] || [key];
    const direction = pctChange > 0 ? col('green', `↑${pctChange}%`) : pctChange < 0 ? col('red', `↓${Math.abs(pctChange)}%`) : dim('→ 0%');
    const sig = Math.abs(pctChange) > 20 ? bold('  ★') : '';
    console.log(`  ${col('cyan', name.padEnd(20))} ${String(before).padStart(6)} → ${String(after).padStart(6)}  ${direction}${sig}`);
  }
  
  console.log();
  console.log(dim('  ★ = >20% shift'));
}

function displayHistory(fingerprints) {
  console.log();
  console.log(bold(col('magenta', '∴ Fingerprint History')));
  console.log(dim(`  ${fingerprints.length} snapshots stored`));
  console.log();
  
  for (const fp of fingerprints) {
    const date = new Date(fp.timestamp).toLocaleDateString();
    const time = new Date(fp.timestamp).toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
    console.log(`  ${col('cyan', date + ' ' + time)}  ${fp.label || 'unlabeled'}  ${dim(fp.wordCount + 'w')}`);
  }
}

// ─── Storage ──────────────────────────────────────────────────────────────────

function loadStore() {
  try {
    return JSON.parse(fs.readFileSync(FP_STORE, 'utf8'));
  } catch {
    return [];
  }
}

function saveStore(prints) {
  ensureWritableDir(path.dirname(FP_STORE), 'The fingerprint store directory is not writable.');
  fs.writeFileSync(FP_STORE, JSON.stringify(prints, null, 2));
}

// ─── Main ─────────────────────────────────────────────────────────────────────

const args = process.argv.slice(2);
const jsonMode = args.includes('--json');
const compareMode = args.includes('--compare');
const historyMode = args.includes('--history');
const dailyMode = args.includes('--daily');
const imprintsMode = args.includes('--imprints');
const saveFlag = args.includes('--save');
const noSave = args.includes('--no-save');

function getFilesToProcess() {
  if (dailyMode) {
    const today = new Date().toISOString().split('T')[0];
    return [path.join(WORKSPACE, 'memory', `${today}.md`)];
  }
  if (imprintsMode) {
    const dir = path.join(WORKSPACE, 'memory/imprints');
    if (!fs.existsSync(dir)) return [];
    return fs.readdirSync(dir)
      .filter(f => f.endsWith('.md'))
      .map(f => path.join(dir, f))
      .sort();
  }
  // Regular file args
  return args
    .filter(a => !a.startsWith('--'))
    .filter(Boolean)
    .map(f => path.isAbsolute(f) ? f : path.join(process.cwd(), f));
}

if (historyMode) {
  const store = loadStore();
  if (jsonMode) {
    console.log(JSON.stringify(store, null, 2));
  } else {
    displayHistory(store);
  }
  process.exit(0);
}

const files = getFilesToProcess();

if (files.length === 0) {
  console.error('Usage: node scripts/cognitive-fingerprint.js [files] [--daily] [--imprints] [--compare] [--history] [--save] [--json]');
  process.exit(1);
}

const fingerprints = [];
for (const file of files) {
  if (!fs.existsSync(file)) {
    console.error(dim(`  skipping ${path.basename(file)} (not found)`));
    continue;
  }
  const raw = fs.readFileSync(file, 'utf8');
  const label = path.basename(file, '.md');
  const fp = computeFingerprint(raw, label);
  if (!fp) {
    console.error(dim(`  skipping ${label} (too short)`));
    continue;
  }
  fingerprints.push(fp);
}

if (fingerprints.length === 0) {
  console.error('No valid files to fingerprint.');
  process.exit(1);
}

if (jsonMode) {
  if (compareMode && fingerprints.length >= 2) {
    console.log(JSON.stringify(comparePrints(fingerprints[0], fingerprints[fingerprints.length - 1]), null, 2));
  } else {
    console.log(JSON.stringify(fingerprints.length === 1 ? fingerprints[0] : fingerprints, null, 2));
  }
  process.exit(0);
}

if (compareMode && fingerprints.length >= 2) {
  const fp1 = fingerprints[0];
  const fp2 = fingerprints[fingerprints.length - 1];
  displayFingerprint(fp1, false);
  displayFingerprint(fp2, false);
  const deltas = comparePrints(fp1, fp2);
  displayComparison(fp1, fp2, deltas);
} else {
  for (const fp of fingerprints) {
    displayFingerprint(fp);
  }
}

// Auto-save single fingerprint runs (unless --no-save)
if (!noSave && fingerprints.length === 1 && !compareMode) {
  const store = loadStore();
  store.push(fingerprints[0]);
  // Keep last 50
  if (store.length > 50) store.splice(0, store.length - 50);
  saveStore(store);
  console.log(dim(`\n  ↓ Saved to cognitive-fingerprints.json (${store.length} total)`));
}

// Multi-file: save all if --save passed
if (saveFlag && fingerprints.length > 1) {
  const store = loadStore();
  store.push(...fingerprints);
  if (store.length > 50) store.splice(0, store.length - 50);
  saveStore(store);
  console.log(dim(`\n  ↓ Saved ${fingerprints.length} fingerprints (${store.length} total)`));
}


path: scripts/contradiction-scanner.js
content: #!/usr/bin/env node
/**
 * Contradiction & Drift Scanner
 * 
 * Read guidance files and surface:
 *   - Contradicting directives
 *   - Stale file path references
 *   - Orphaned decisions
 *   - Model/tool name drift
 *   - Unresolved commitments
 *   - Evolution lessons not yet integrated
 * 
 * Usage: node scripts/contradiction-scanner.js [--verbose] [--json]
 */

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

const WORKSPACE = process.env.EVOLUTION_TOOLKIT_WORKSPACE
  || (process.env.HOME ? require('path').join(process.env.HOME, '.openclaw/workspace') : process.cwd());
const VERBOSE = process.argv.includes('--verbose');
const JSON_OUT = process.argv.includes('--json');

// ─── Color helpers ────────────────────────────────────────────────────────────
const c = {
  red:    s => `\x1b[31m${s}\x1b[0m`,
  yellow: s => `\x1b[33m${s}\x1b[0m`,
  green:  s => `\x1b[32m${s}\x1b[0m`,
  blue:   s => `\x1b[34m${s}\x1b[0m`,
  cyan:   s => `\x1b[36m${s}\x1b[0m`,
  gray:   s => `\x1b[90m${s}\x1b[0m`,
  bold:   s => `\x1b[1m${s}\x1b[0m`,
  dim:    s => `\x1b[2m${s}\x1b[0m`,
};

// ─── Files to scan ───────────────────────────────────────────────────────────
const GUIDANCE_FILES = [
  'AGENTS.md',
  'SOUL.md',
  'TOOLS.md',
  'MEMORY.md',
  'EVOLUTION.md',
  'HEARTBEAT.md',
  'CURRENT.md',
  'TASKS.md',
  'memory/decisions.md',
  'memory/workflows.md',
  'memory/subagents.md',
].map(f => path.join(WORKSPACE, f));

// ─── Patterns ────────────────────────────────────────────────────────────────

// Directive patterns — sentences that give instructions
const DIRECTIVE_PATTERNS = [
  /\b(always|never|must|should|don't|do not|avoid|NEVER|ALWAYS|MUST)\b.{5,80}/gi,
];

// File path references
const FILE_PATH_PATTERN = /`([^`]+\.(md|js|ts|json|sh|py|txt))`|([/~][^\s,)'"]+\.(md|js|ts|json|sh|py|txt))/g;

// Model name patterns — catch outdated model references
const MODEL_NAMES = {
  current: ['claude-sonnet-4-6', 'claude-haiku', 'gpt-5.4', 'gemini-2.5', 'gemini-3.1'],
  outdated: ['claude-3-opus', 'claude-3-sonnet', 'claude-3-haiku', 'claude-2', 'gpt-4', 'gpt-3.5', 'opus 4.5', 'sonnet 4.5', 'claude-sonnet-4-5', 'claude-opus-4-5'],
};

// Tool name drift — things that have changed
const TOOL_DRIFT = [
  { old: 'QMD vsearch', current: 'memory_search (semantic)', note: 'vsearch too slow on CPU — use memory_search' },
  { old: 'openclaw gateway REST', current: 'POST /tools/invoke', note: 'documented working' },
];

// Date pattern for stale "as of" claims
const DATE_PATTERN = /(?:as of|since|updated|fixed)\s+(?:Feb|Mar|Jan|Apr|May|Jun)\s+\d{4}|2026-0[1-2]-\d{2}/gi;

// Commitment patterns — things said they'd do
const COMMITMENT_PATTERNS = [
  /\b(?:TODO|FIXME|⚠️.*will|plan to|next time|going to|should add|need to add)\b.{5,100}/gi,
  /\*\*TODO\*\*:.+/g,
];

// ─── Scanner functions ────────────────────────────────────────────────────────

function readFile(filePath) {
  try {
    return fs.readFileSync(filePath, 'utf8');
  } catch {
    return null;
  }
}

function extractDirectives(content, source) {
  const directives = [];
  const lines = content.split('\n');
  lines.forEach((line, idx) => {
    for (const pattern of DIRECTIVE_PATTERNS) {
      pattern.lastIndex = 0;
      const matches = line.match(pattern);
      if (matches) {
        matches.forEach(m => {
          // Dedupe — skip if this is a comment or code example
          if (line.trim().startsWith('//') || line.trim().startsWith('#!')) return;
          directives.push({ text: m.trim(), line: idx + 1, source, raw: line.trim() });
        });
      }
    }
  });
  return directives;
}

function extractFilePaths(content, source) {
  const refs = [];
  const lines = content.split('\n');
  lines.forEach((line, idx) => {
    FILE_PATH_PATTERN.lastIndex = 0;
    let match;
    while ((match = FILE_PATH_PATTERN.exec(line)) !== null) {
      const p = match[1] || match[3];
      if (p) refs.push({ path: p, line: idx + 1, source });
    }
  });
  return refs;
}

function checkFilePaths(refs) {
  const broken = [];
  const checked = new Set();
  refs.forEach(ref => {
    const key = ref.path;
    if (checked.has(key)) return;
    checked.add(key);
    
    // Try to resolve relative to workspace
    const candidates = [
      path.join(WORKSPACE, ref.path),
      path.join(WORKSPACE, 'memory', ref.path),
      ref.path, // absolute
    ];
    
    // Skip obviously non-local paths
    if (ref.path.startsWith('http') || ref.path.startsWith('~/.') || 
        ref.path.includes('node_modules') || ref.path.startsWith('/tmp')) return;
    
    const exists = candidates.some(c => {
      try { fs.accessSync(c); return true; } catch { return false; }
    });
    
    if (!exists) {
      broken.push({ ...ref, candidates });
    }
  });
  return broken;
}

function findOutdatedModels(content, source) {
  const findings = [];
  MODEL_NAMES.outdated.forEach(model => {
    const regex = new RegExp(model.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'), 'gi');
    const lines = content.split('\n');
    lines.forEach((line, idx) => {
      if (regex.test(line)) {
        findings.push({ model, line: idx + 1, source, context: line.trim() });
      }
    });
  });
  return findings;
}

function findCommitments(content, source) {
  const commitments = [];
  const lines = content.split('\n');
  lines.forEach((line, idx) => {
    for (const pattern of COMMITMENT_PATTERNS) {
      pattern.lastIndex = 0;
      if (pattern.test(line)) {
        commitments.push({ text: line.trim(), line: idx + 1, source });
      }
    }
  });
  return commitments;
}

function findDirectiveConflicts(allDirectives) {
  const conflicts = [];
  
  // Deduplicate: remove directives that are identical (same raw text, different stance detected)
  const seen = new Set();
  const dedupedDirectives = allDirectives.filter(d => {
    const key = d.raw.slice(0, 60);
    if (seen.has(key)) return false;
    seen.add(key);
    return true;
  });
  
  // Pair up directives about the same topic
  const topics = [
    { keywords: ['push', 'main', 'branch', 'git', 'merge'], label: 'Git/branch policy' },
    { keywords: ['supabase', 'management api', 'api.supabase'], label: 'Supabase API' },
    { keywords: ['memory_search', 'qmd', 'search'], label: 'Memory search tool' },
    { keywords: ['codex', 'code', 'coding'], label: 'Code delegation' },
    { keywords: ['model', 'claude', 'sonnet', 'opus', 'haiku'], label: 'Model selection' },
    { keywords: ['browser', 'profile', 'chrome', 'openclaw'], label: 'Browser profile' },
    { keywords: ['twitter', 'x.com', 'tweet'], label: 'Twitter/X access' },
    { keywords: ['cron', 'gateway', 'REST'], label: 'Cron/Gateway' },
    { keywords: ['rm', 'delete', 'trash'], label: 'File deletion' },
  ];
  
  topics.forEach(topic => {
    const relevant = dedupedDirectives.filter(d => 
      topic.keywords.some(k => d.raw.toLowerCase().includes(k))
    );
    
    if (relevant.length < 2) return;
    
    // Look for "never/always" pairs in the same topic
    const nevers = relevant.filter(d => /\bnever\b|\bdo not\b|\bdon't\b|\bavoid\b/i.test(d.text));
    const always = relevant.filter(d => /\balways\b|\bmust\b|\bshould\b/i.test(d.text));
    
    // Check if same action is both required and forbidden
    nevers.forEach(n => {
      always.forEach(a => {
        const nWords = new Set(n.text.toLowerCase().split(/\W+/).filter(w => w.length > 4));
        const aWords = new Set(a.text.toLowerCase().split(/\W+/).filter(w => w.length > 4));
        const shared = [...nWords].filter(w => aWords.has(w) && !['always', 'never', 'should', 'must', 'avoid', 'using', 'with', 'from', 'when', 'that', 'this', 'have', 'been', 'will', 'your', 'they'].includes(w));
        
        // Skip self-conflicts (same sentence contains both always/never about same thing)
        if (n.source === a.source && n.line === a.line) return;
        // Skip if they're essentially the same text
        if (n.raw === a.raw || n.text.slice(0, 40) === a.text.slice(0, 40)) return;
        
        if (shared.length >= 2) {
          conflicts.push({
            topic: topic.label,
            directive1: { ...n, stance: 'PROHIBIT' },
            directive2: { ...a, stance: 'REQUIRE' },
            sharedTerms: shared,
          });
        }
      });
    });
  });
  
  return conflicts;
}

function findEvolvedButNotIntegrated() {
  // Read EVOLUTION.md — check if recent entries (last 30 days) reference a "fix" that 
  // might not be reflected in AGENTS.md/TOOLS.md
  const evolution = readFile(path.join(WORKSPACE, 'EVOLUTION.md'));
  if (!evolution) return [];
  
  const findings = [];
  const lines = evolution.split('\n');
  const recentPattern = /2026-03-\d{2}/;
  
  lines.forEach((line, idx) => {
    if (recentPattern.test(line) && (line.includes('Fix:') || line.includes('Rule:') || line.includes('Always:') || line.includes('Never:'))) {
      // Extract the fix/rule
      const fixMatch = line.match(/(?:Fix|Rule|Always|Never):\s*(.+)/);
      if (fixMatch) {
        findings.push({
          rule: fixMatch[1].trim(),
          line: idx + 1,
          source: 'EVOLUTION.md',
          note: 'Recent lesson — verify it\'s in AGENTS.md',
        });
      }
    }
  });
  
  return findings;
}

function findStaleDateClaims(content, source) {
  const stale = [];
  const lines = content.split('\n');
  const cutoff = new Date('2026-02-01');
  
  lines.forEach((line, idx) => {
    const matches = line.match(DATE_PATTERN);
    if (!matches) return;
    
    matches.forEach(match => {
      // Try to parse the date
      const dateStr = match.match(/2026-\d{2}-\d{2}/);
      if (dateStr) {
        const d = new Date(dateStr[0]);
        const daysSince = (Date.now() - d.getTime()) / (1000 * 60 * 60 * 24);
        if (daysSince > 14) {
          stale.push({ claim: match, context: line.trim(), line: idx + 1, source, daysSince: Math.round(daysSince) });
        }
      }
    });
  });
  
  return stale;
}

function detectModelInconsistency(allContent) {
  // Check if MEMORY.md, AGENTS.md, and workflows.md agree on the current model
  const modelMentions = {};
  
  ['AGENTS.md', 'MEMORY.md', 'memory/workflows.md'].forEach(file => {
    const content = readFile(path.join(WORKSPACE, file));
    if (!content) return;
    
    MODEL_NAMES.current.forEach(model => {
      const count = (content.match(new RegExp(model, 'gi')) || []).length;
      if (count > 0) {
        if (!modelMentions[model]) modelMentions[model] = {};
        modelMentions[model][file] = count;
      }
    });
  });
  
  return modelMentions;
}

// ─── Main ─────────────────────────────────────────────────────────────────────

function main() {
  const results = {
    scannedFiles: [],
    brokenPaths: [],
    outdatedModels: [],
    conflicts: [],
    commitments: [],
    evolvedNotIntegrated: [],
    staleDateClaims: [],
    modelMap: {},
    summary: {},
  };
  
  if (!JSON_OUT) {
    console.log(c.bold('\n∴ CONTRADICTION & DRIFT SCANNER'));
    console.log(c.gray('Ergo self-knowledge tool | 2026-03-10'));
    console.log(c.gray('─'.repeat(60)));
  }
  
  // Collect all directives from all files
  let allDirectives = [];
  let allFilePaths = [];
  
  GUIDANCE_FILES.forEach(filePath => {
    const content = readFile(filePath);
    if (!content) {
      if (VERBOSE) console.log(c.gray(`  ⊘ ${path.relative(WORKSPACE, filePath)} (not found)`));
      return;
    }
    
    const relPath = path.relative(WORKSPACE, filePath);
    results.scannedFiles.push(relPath);
    
    const directives = extractDirectives(content, relPath);
    const filePaths = extractFilePaths(content, relPath);
    const outdated = findOutdatedModels(content, relPath);
    const commits = findCommitments(content, relPath);
    const stale = findStaleDateClaims(content, relPath);
    
    allDirectives = allDirectives.concat(directives);
    allFilePaths = allFilePaths.concat(filePaths);
    results.outdatedModels = results.outdatedModels.concat(outdated);
    results.commitments = results.commitments.concat(commits);
    results.staleDateClaims = results.staleDateClaims.concat(stale);
  });
  
  // Check file paths — filter out obvious template placeholders
  const templatePatterns = /YYYY-MM-DD|<name>|<feature|\/path\/to\/|\*\./;
  results.brokenPaths = checkFilePaths(allFilePaths)
    .filter(b => !templatePatterns.test(b.path));
  
  // Find directive conflicts
  results.conflicts = findDirectiveConflicts(allDirectives);
  
  // Find evolved-but-not-integrated lessons
  results.evolvedNotIntegrated = findEvolvedButNotIntegrated();
  
  // Model consistency map
  results.modelMap = detectModelInconsistency();
  
  // ─── Output ─────────────────────────────────────────────────────────────────
  
  if (JSON_OUT) {
    console.log(JSON.stringify(results, null, 2));
    return;
  }
  
  // Files scanned
  console.log(c.cyan(`\n📂 Scanned ${results.scannedFiles.length} guidance files`));
  if (VERBOSE) results.scannedFiles.forEach(f => console.log(c.gray(`   ${f}`)));
  console.log(c.gray(`   ${allDirectives.length} directives extracted | ${allFilePaths.length} file path refs found`));
  
  // ── Section 1: Broken paths
  console.log(c.bold('\n─── 📁 BROKEN FILE REFERENCES ─────────────────────────────'));
  if (results.brokenPaths.length === 0) {
    console.log(c.green('  ✓ No broken file references found'));
  } else {
    results.brokenPaths.slice(0, 20).forEach(b => {
      console.log(c.red(`  ✗ ${b.path}`));
      console.log(c.gray(`    → referenced in ${b.source} (line ${b.line})`));
    });
    if (results.brokenPaths.length > 20) {
      console.log(c.gray(`  ... and ${results.brokenPaths.length - 20} more`));
    }
  }
  
  // ── Section 2: Outdated models
  console.log(c.bold('\n─── 🤖 OUTDATED MODEL REFERENCES ──────────────────────────'));
  if (results.outdatedModels.length === 0) {
    console.log(c.green('  ✓ No outdated model references found'));
  } else {
    const byModel = {};
    results.outdatedModels.forEach(m => {
      if (!byModel[m.model]) byModel[m.model] = [];
      byModel[m.model].push(m);
    });
    Object.entries(byModel).forEach(([model, instances]) => {
      console.log(c.yellow(`  ⚠ "${model}" (${instances.length} mention${instances.length > 1 ? 's' : ''})`));
      if (VERBOSE) instances.forEach(i => 
        console.log(c.gray(`    ${i.source}:${i.line} → ${i.context.slice(0, 80)}`))
      );
    });
  }
  
  // ── Section 3: Directive conflicts
  console.log(c.bold('\n─── ⚡ POTENTIAL DIRECTIVE CONFLICTS ───────────────────────'));
  if (results.conflicts.length === 0) {
    console.log(c.green('  ✓ No obvious directive conflicts detected'));
  } else {
    results.conflicts.forEach((conflict, i) => {
      console.log(c.yellow(`\n  ${i + 1}. Topic: ${c.bold(conflict.topic)}`));
      console.log(c.red(`     PROHIBIT: "${conflict.directive1.text.slice(0, 90)}"`));
      console.log(c.gray(`       → ${conflict.directive1.source}:${conflict.directive1.line}`));
      console.log(c.cyan(`     REQUIRE:  "${conflict.directive2.text.slice(0, 90)}"`));
      console.log(c.gray(`       → ${conflict.directive2.source}:${conflict.directive2.line}`));
      console.log(c.dim(`     Shared terms: [${conflict.sharedTerms.slice(0, 5).join(', ')}]`));
    });
  }
  
  // ── Section 4: Open commitments
  console.log(c.bold('\n─── 📌 OPEN COMMITMENTS / TODOS ────────────────────────────'));
  const importantCommits = results.commitments.filter(c => 
    !c.source.includes('EVOLUTION') && c.text.length > 15
  ).slice(0, 15);
  
  if (importantCommits.length === 0) {
    console.log(c.green('  ✓ No open commitments found in guidance files'));
  } else {
    importantCommits.forEach(commit => {
      console.log(c.yellow(`  → ${commit.text.slice(0, 100)}`));
      console.log(c.gray(`    ${commit.source}:${commit.line}`));
    });
  }
  
  // ── Section 5: Recent EVOLUTION lessons
  console.log(c.bold('\n─── 🧬 RECENT EVOLUTION LESSONS TO INTEGRATE ──────────────'));
  if (results.evolvedNotIntegrated.length === 0) {
    console.log(c.green('  ✓ No recent lessons flagged'));
  } else {
    results.evolvedNotIntegrated.forEach(lesson => {
      console.log(c.cyan(`  ★ "${lesson.rule.slice(0, 100)}"`));
      console.log(c.gray(`    ${lesson.source}:${lesson.line} — ${lesson.note}`));
    });
  }
  
  // ── Section 6: Stale date claims
  console.log(c.bold('\n─── 📅 POTENTIALLY STALE DATE CLAIMS ──────────────────────'));
  const mostStale = results.staleDateClaims
    .filter(s => s.daysSince > 20)
    .sort((a, b) => b.daysSince - a.daysSince)
    .slice(0, 10);
  
  if (mostStale.length === 0) {
    console.log(c.green('  ✓ No very stale date claims found'));
  } else {
    mostStale.forEach(s => {
      console.log(c.dim(`  ⏱ ${s.claim} (${s.daysSince}d ago) — ${s.source}:${s.line}`));
      if (VERBOSE) console.log(c.gray(`    context: ${s.context.slice(0, 80)}`));
    });
  }
  
  // ── Section 7: Model consistency
  console.log(c.bold('\n─── 🗺  MODEL REFERENCE MAP ────────────────────────────────'));
  Object.entries(results.modelMap).forEach(([model, files]) => {
    const fileStr = Object.entries(files).map(([f, n]) => `${path.basename(f)}(×${n})`).join(', ');
    console.log(c.gray(`  ${model}: ${fileStr}`));
  });
  
  // ── Summary
  const totalIssues = results.brokenPaths.length + results.outdatedModels.length + 
                      results.conflicts.length + importantCommits.length + 
                      results.evolvedNotIntegrated.length;
  
  console.log(c.bold('\n─── SUMMARY ────────────────────────────────────────────────'));
  console.log(`  ${results.brokenPaths.length > 0 ? c.red('✗') : c.green('✓')} Broken file refs:      ${results.brokenPaths.length}`);
  console.log(`  ${results.outdatedModels.length > 0 ? c.yellow('⚠') : c.green('✓')} Outdated model refs:   ${results.outdatedModels.length}`);
  console.log(`  ${results.conflicts.length > 0 ? c.yellow('⚠') : c.green('✓')} Directive conflicts:   ${results.conflicts.length}`);
  console.log(`  ${importantCommits.length > 0 ? c.yellow('⚠') : c.green('✓')} Open commitments:      ${importantCommits.length}`);
  console.log(`  ${results.evolvedNotIntegrated.length > 0 ? c.cyan('★') : c.green('✓')} Evolution to integrate: ${results.evolvedNotIntegrated.length}`);
  console.log(`  ${mostStale.length > 0 ? c.dim('⏱') : c.green('✓')} Stale date claims:     ${mostStale.length}`);
  
  // Weighted scoring: conflicts are worst, stale dates are minor
  const weightedIssues = results.brokenPaths.length * 2 + 
                          results.outdatedModels.length * 3 +
                          results.conflicts.length * 4 +
                          importantCommits.length * 1 +
                          results.evolvedNotIntegrated.length * 2;
  const score = Math.max(0, 100 - weightedIssues);
  const scoreColor = score > 80 ? c.green : score > 60 ? c.yellow : c.red;
  console.log(c.bold(`\n  Coherence score: ${scoreColor(score + '/100')}`));
  
  if (totalIssues === 0) {
    console.log(c.green(c.bold('\n  ∴ System is internally consistent. Good state.\n')));
  } else {
    console.log(c.gray(`\n  Run with --verbose for full context on each issue.\n`));
  }
}

main();


path: scripts/predict.js
content: #!/usr/bin/env node
/**
 * predict.js — Prediction Log CLI
 * Reduce friction around prediction logging and calibration review.
 *
 * Usage:
 *   node scripts/predict.js add
 *   node scripts/predict.js resolve
 *   node scripts/predict.js audit
 *   node scripts/predict.js stats
 *   node scripts/predict.js last [N]
 */

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

const WORKSPACE = process.env.EVOLUTION_TOOLKIT_WORKSPACE
  || (process.env.HOME ? require('path').join(process.env.HOME, '.openclaw/workspace') : process.cwd());
const LOG_PATH = path.join(WORKSPACE, 'memory', 'prediction-log.md');
const SECTION_MARKER = '## Log';
const END_MARKER = '## Calibration';

function ensureWritableDir(dirPath, label) {
  try {
    fs.mkdirSync(dirPath, { recursive: true });
    fs.accessSync(dirPath, fs.constants.W_OK);
  } catch (err) {
    console.error(`Write access required: ${label}`);
    console.error(dirPath);
    console.error('Set EVOLUTION_TOOLKIT_WORKSPACE to a writable workspace and try again.');
    process.exit(1);
  }
}

// ─── Parse predictions from markdown ────────────────────────────────────────

function parseLog() {
  const content = fs.readFileSync(LOG_PATH, 'utf8');
  const logStart = content.indexOf(SECTION_MARKER);
  const logEnd = content.indexOf(END_MARKER);
  if (logStart === -1) return { content, entries: [], before: content, after: '' };

  const before = content.slice(0, logStart + SECTION_MARKER.length + 1);
  const logSection = content.slice(logStart + SECTION_MARKER.length, logEnd !== -1 ? logEnd : undefined);
  const after = logEnd !== -1 ? content.slice(logEnd) : '';

  // Split by ### headings
  const rawEntries = logSection.split(/(?=### \d{4}-\d{2}-\d{2})/).filter(s => s.trim());

  const entries = rawEntries.map(raw => {
    const dateMatch = raw.match(/### (\d{4}-\d{2}-\d{2}) — (.+)/);
    if (!dateMatch) return null;

    const date = dateMatch[1];
    const topic = dateMatch[2].trim();
    const hasOutcome = /\*\*Outcome:\*\*/.test(raw) && !/\*\*Outcome:\*\*\s*\[fill in/.test(raw) && !/\*\*Outcome:\*\*\s*$/.test(raw);
    const hasDelta = /\*\*Delta:\*\*/.test(raw);
    const isOpen = !hasOutcome;

    // Extract confidence
    const confMatch = raw.match(/\*\*Confidence:\*\*\s*(H|M|L)/i);
    const confidence = confMatch ? confMatch[1].toUpperCase() : '?';

    // Extract epistemic basis
    const basisMatch = raw.match(/\*\*Epistemic basis:\*\*\s*(.+)/);
    const basis = basisMatch ? basisMatch[1].trim() : '?';

    return { date, topic, raw, isOpen, confidence, basis };
  }).filter(Boolean);

  return { content, entries, before, after, logSection };
}

// ─── Format a new prediction entry ──────────────────────────────────────────

function formatEntry({ date, topic, prediction, confidence, basis }) {
  return `
### ${date} — ${topic}
**Prediction:** ${prediction}
**Confidence:** ${confidence}
**Epistemic basis:** ${basis}
**Outcome:** [fill in after]
**Delta:** [what surprised me]
**Lesson:** [what to update]
`;
}

// ─── Insert after last entry in Log section ──────────────────────────────────

function insertEntry(newEntry) {
  ensureWritableDir(path.dirname(LOG_PATH), 'The prediction log directory is not writable.');
  const content = fs.readFileSync(LOG_PATH, 'utf8');
  const endMarkerIdx = content.indexOf('\n---\n\n## Calibration');
  if (endMarkerIdx === -1) {
    // Append before calibration section
    const calibIdx = content.indexOf('\n## Calibration');
    if (calibIdx === -1) {
      fs.appendFileSync(LOG_PATH, newEntry);
    } else {
      const updated = content.slice(0, calibIdx) + '\n' + newEntry + content.slice(calibIdx);
      fs.writeFileSync(LOG_PATH, updated);
    }
  } else {
    const updated = content.slice(0, endMarkerIdx) + '\n' + newEntry + content.slice(endMarkerIdx);
    fs.writeFileSync(LOG_PATH, updated);
  }
}

// ─── readline helper ─────────────────────────────────────────────────────────

function ask(rl, question) {
  return new Promise(resolve => rl.question(question, resolve));
}

// ─── Commands ────────────────────────────────────────────────────────────────

async function cmdAdd() {
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
  
  console.log('\n📝 New Prediction\n');
  
  const today = new Date().toISOString().slice(0, 10);
  const topic = await ask(rl, `Topic (e.g. "Codex will finish within 30 min"): `);
  const prediction = await ask(rl, `Prediction (what do you expect?): `);
  
  let confidence = '';
  while (!['H', 'M', 'L'].includes(confidence.toUpperCase())) {
    confidence = await ask(rl, `Confidence [H/M/L]: `);
  }
  confidence = confidence.toUpperCase();

  console.log('\nEpistemic basis options:');
  console.log('  [observed]   — you have direct evidence');
  console.log('  [inferred]   — logical deduction, not direct evidence');
  console.log('  [consensus]  — following conventional wisdom');
  console.log('  [speculative]— educated guess, thin evidence');
  const basis = await ask(rl, `Basis: `);
  
  rl.close();
  
  const entry = formatEntry({ date: today, topic, prediction, confidence, basis });
  insertEntry(entry);
  
  console.log('\n✅ Prediction logged. Fill in Outcome/Delta/Lesson after it resolves.');
  console.log(`   Run: node scripts/predict.js resolve`);
}

function cmdAudit() {
  const { entries } = parseLog();
  const open = entries.filter(e => e.isOpen);
  
  if (open.length === 0) {
    console.log('\n✅ No open predictions. All have outcomes recorded.\n');
    return;
  }

  console.log(`\n⚠️  ${open.length} open prediction(s) awaiting outcome:\n`);
  open.forEach((e, i) => {
    const conf = e.confidence === 'H' ? '🔴' : e.confidence === 'M' ? '🟡' : '🟢';
    console.log(`  ${i + 1}. [${e.date}] ${conf} ${e.topic}`);
  });
  console.log('\n  Run `node scripts/predict.js resolve` to fill in outcomes.');
  console.log(`  Or edit memory/prediction-log.md directly.\n`);
}

function cmdStats() {
  const { entries } = parseLog();
  
  if (entries.length === 0) {
    console.log('\nNo predictions yet.\n');
    return;
  }

  const resolved = entries.filter(e => !e.isOpen);
  const open = entries.filter(e => e.isOpen);

  const confCounts = { H: 0, M: 0, L: 0, '?': 0 };
  entries.forEach(e => { confCounts[e.confidence] = (confCounts[e.confidence] || 0) + 1; });

  // Tag epistemic types
  const basisCounts = {};
  entries.forEach(e => {
    const tags = (e.basis.match(/\[(observed|inferred|consensus|speculative)\]/g) || []);
    tags.forEach(t => { basisCounts[t] = (basisCounts[t] || 0) + 1; });
    if (!tags.length) { basisCounts['[untagged]'] = (basisCounts['[untagged]'] || 0) + 1; }
  });

  console.log('\n📊 Prediction Log Stats\n');
  console.log(`  Total entries:  ${entries.length}`);
  console.log(`  Resolved:       ${resolved.length}`);
  console.log(`  Open:           ${open.length}`);
  console.log('');
  console.log('  Confidence distribution:');
  console.log(`    H (high):   ${confCounts.H || 0} entries`);
  console.log(`    M (medium): ${confCounts.M || 0} entries`);
  console.log(`    L (low):    ${confCounts.L || 0} entries`);
  console.log('');
  console.log('  Epistemic basis:');
  Object.entries(basisCounts).sort((a,b) => b[1]-a[1]).forEach(([tag, count]) => {
    console.log(`    ${tag}: ${count}`);
  });

  // Warn if too many high-confidence predictions (overconfidence signal)
  const highConf = confCounts.H || 0;
  const total = entries.length;
  const highPct = Math.round(100 * highConf / total);
  if (highPct > 50) {
    console.log(`\n  ⚠️  ${highPct}% of predictions are High confidence.`);
    console.log('     If accuracy < 80%, this indicates overconfidence bias.');
  }

  // Warn if prediction gap
  const today = new Date().toISOString().slice(0, 10);
  const lastDate = entries.length > 0 ? entries[entries.length - 1].date : null;
  if (lastDate) {
    const daysSince = Math.floor((new Date(today) - new Date(lastDate)) / (1000*60*60*24));
    if (daysSince > 3) {
      console.log(`\n  ⚠️  Last prediction: ${lastDate} (${daysSince} days ago).`);
      console.log('     Prediction gap detected. Add entries for recent significant decisions.');
    }
  }
  console.log('');
}

function cmdLast(n = 5) {
  const { entries } = parseLog();
  const last = entries.slice(-n);
  
  if (last.length === 0) {
    console.log('\nNo predictions yet.\n');
    return;
  }

  console.log(`\n📋 Last ${last.length} prediction(s):\n`);
  last.forEach(e => {
    const status = e.isOpen ? '⏳ open' : '✅ resolved';
    const conf = e.confidence === 'H' ? '🔴H' : e.confidence === 'M' ? '🟡M' : '🟢L';
    console.log(`  [${e.date}] ${conf} ${status}`);
    console.log(`    Topic: ${e.topic}\n`);
  });
}

async function cmdResolve() {
  const { entries } = parseLog();
  const open = entries.filter(e => e.isOpen);
  
  if (open.length === 0) {
    console.log('\n✅ No open predictions to resolve.\n');
    return;
  }

  console.log(`\n⏳ ${open.length} open prediction(s). Opening prediction-log.md for editing...\n`);
  open.forEach((e, i) => {
    console.log(`  ${i + 1}. [${e.date}] ${e.topic}`);
  });
  console.log('\n  📝 Edit memory/prediction-log.md directly — fill in Outcome, Delta, and Lesson for each.');
  console.log('     Tip: search for "[fill in after]" to find open entries.\n');
}

// ─── Main ────────────────────────────────────────────────────────────────────

const cmd = process.argv[2] || 'audit';
const arg = process.argv[3];

(async () => {
  switch (cmd) {
    case 'add':      await cmdAdd(); break;
    case 'audit':    cmdAudit(); break;
    case 'stats':    cmdStats(); break;
    case 'last':     cmdLast(parseInt(arg) || 5); break;
    case 'resolve':  await cmdResolve(); break;
    default:
      console.log('Usage: node scripts/predict.js [add|audit|resolve|stats|last [N]]');
  }
})();


path: scripts/session-coherence.js
content: #!/usr/bin/env node
/**
 * Session Coherence Analyzer
 * 
 * Reads all daily memory logs and extracts:
 * - Recurring themes, topics, projects
 * - Emotional texture (what energized vs drained)
 * - Decision patterns
 * - The "coherent Ergo" that emerges across sessions
 * 
 * Usage: node scripts/session-coherence.js [--days N] [--portrait] [--output <path>]
 */

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

const WORKSPACE = process.env.EVOLUTION_TOOLKIT_WORKSPACE
  || (process.env.HOME ? require('path').join(process.env.HOME, '.openclaw/workspace') : process.cwd());
const MEMORY_DIR = path.join(WORKSPACE, 'memory');
const STOP_WORDS = new Set([
  'the', 'a', 'an', 'and', 'or', 'but', 'in', 'on', 'at', 'to', 'for',
  'of', 'with', 'by', 'from', 'up', 'about', 'into', 'through', 'during',
  'is', 'was', 'are', 'were', 'be', 'been', 'being', 'have', 'has', 'had',
  'do', 'does', 'did', 'will', 'would', 'could', 'should', 'may', 'might',
  'it', 'its', 'this', 'that', 'these', 'those', 'i', 'me', 'my', 'we',
  'our', 'you', 'your', 'he', 'she', 'they', 'them', 'their', 'what', 'which',
  'who', 'when', 'where', 'why', 'how', 'all', 'each', 'every', 'both',
  'few', 'more', 'most', 'other', 'some', 'such', 'no', 'not', 'only',
  'same', 'so', 'than', 'too', 'very', 'just', 'also', 'as', 'if', 'then',
  'there', 'can', 'need', 'added', 'updated', 'created', 'new', 'will',
  'used', 'using', 'use', 'via', 'get', 'got', 'set', 'done', 'make',
  'made', 'work', 'working', 'run', 'ran', 'time', 'day', 'session',
  'next', 'now', 'still', 'back', 'note', 'notes', 'see', 'check'
]);

// Sentiment/energy markers
const ENERGY_POSITIVE = ['excited', 'great', 'excellent', 'interesting', 'fascinating', 'clean', 
  'solid', 'good', 'fun', 'love', 'enjoy', 'happy', 'progress', 'breakthrough', 'shipped',
  'working', 'success', 'wow', 'impressive', 'elegant', 'clever', 'beautiful', 'launch'];
const ENERGY_NEGATIVE = ['stuck', 'broken', 'failed', 'error', 'blocked', 'issue', 'problem',
  'frustrating', 'slow', 'annoying', 'complex', 'difficult', 'struggle', 'worry', 'concern',
  'unclear', 'messy', 'debt', 'bug', 'crash', 'timeout', 'fail'];

function ensureWritableDir(dirPath, label) {
  try {
    fs.mkdirSync(dirPath, { recursive: true });
    fs.accessSync(dirPath, fs.constants.W_OK);
  } catch (err) {
    console.error(`Write access required: ${label}`);
    console.error(dirPath);
    console.error('Set EVOLUTION_TOOLKIT_WORKSPACE to a writable workspace and try again.');
    process.exit(1);
  }
}

// Project/domain markers
const DOMAIN_PATTERNS = [
  { name: 'products', patterns: ['product', 'customer', 'user', 'feature', 'launch', 'pricing'] },
  { name: 'interfaces', patterns: ['dashboard', 'frontend', 'ui', 'page', 'component'] },
  { name: 'memory-agents', patterns: ['memory', 'agents', 'codex', 'sub-agent', 'assistant', 'prompt'] },
  { name: 'infrastructure', patterns: ['docker', 'hetzner', 'gateway', 'cron', 'deploy', 'vps'] },
  { name: 'coding', patterns: ['javascript', 'node', 'python', 'code', 'script', 'tool', 'api', 'function'] },
  { name: 'research', patterns: ['research', 'analysis', 'market', 'competitor', 'study', 'data'] },
  { name: 'creative', patterns: ['design', 'image', 'content', 'write', 'essay', 'creative'] },
  { name: 'github', patterns: ['github', 'git', 'pr', 'branch', 'commit', 'push', 'merge'] },
];

function readDailyFiles(maxDays = 30) {
  const files = fs.readdirSync(MEMORY_DIR)
    .filter(f => f.match(/^\d{4}-\d{2}-\d{2}\.md$/))
    .sort()
    .slice(-maxDays);
  
  return files.map(f => ({
    date: f.replace('.md', ''),
    path: path.join(MEMORY_DIR, f),
    content: fs.readFileSync(path.join(MEMORY_DIR, f), 'utf8')
  }));
}

function tokenize(text) {
  return text.toLowerCase()
    .replace(/[#*`_[\]()>]/g, ' ')
    .replace(/[^\w\s]/g, ' ')
    .split(/\s+/)
    .filter(w => w.length > 3 && !STOP_WORDS.has(w) && !w.match(/^\d+$/));
}

function wordFrequency(tokens) {
  const freq = {};
  for (const t of tokens) {
    freq[t] = (freq[t] || 0) + 1;
  }
  return freq;
}

function mergeCounts(a, b) {
  const result = { ...a };
  for (const [k, v] of Object.entries(b)) {
    result[k] = (result[k] || 0) + v;
  }
  return result;
}

function topN(freq, n = 20) {
  return Object.entries(freq)
    .sort((a, b) => b[1] - a[1])
    .slice(0, n);
}

function scoreEnergy(content) {
  const lower = content.toLowerCase();
  let pos = 0, neg = 0;
  for (const w of ENERGY_POSITIVE) if (lower.includes(w)) pos++;
  for (const w of ENERGY_NEGATIVE) if (lower.includes(w)) neg++;
  return { pos, neg, ratio: pos / Math.max(1, pos + neg) };
}

function scoreDomains(content) {
  const lower = content.toLowerCase();
  const scores = {};
  for (const { name, patterns } of DOMAIN_PATTERNS) {
    let hits = 0;
    for (const p of patterns) {
      const matches = (lower.match(new RegExp(p, 'g')) || []).length;
      hits += matches;
    }
    if (hits > 0) scores[name] = hits;
  }
  return scores;
}

function extractSectionContent(content, sectionKeyword) {
  const lines = content.split('\n');
  const sections = [];
  let inSection = false;
  let buffer = [];
  
  for (const line of lines) {
    if (line.match(/^#{1,3}\s/) && line.toLowerCase().includes(sectionKeyword.toLowerCase())) {
      inSection = true;
      buffer = [];
    } else if (inSection && line.match(/^#{1,3}\s/)) {
      if (buffer.length > 0) sections.push(buffer.join('\n'));
      inSection = false;
      buffer = [];
    } else if (inSection) {
      buffer.push(line);
    }
  }
  if (buffer.length > 0) sections.push(buffer.join('\n'));
  return sections.join('\n');
}

function computeCoherenceScore(days) {
  // Coherence = consistency of top themes across sessions
  // High score = same topics appear repeatedly (persistent focus)
  // We measure this as: for each word in global top 30, how many sessions mention it?
  
  const globalFreq = {};
  const perSessionFreqs = days.map(d => {
    const tokens = tokenize(d.content);
    const freq = wordFrequency(tokens);
    for (const [k, v] of Object.entries(freq)) {
      globalFreq[k] = (globalFreq[k] || 0) + v;
    }
    return { date: d.date, freq };
  });
  
  const top30 = topN(globalFreq, 30).map(([w]) => w);
  
  // For each top word, count how many sessions mention it
  const wordCoverage = {};
  for (const word of top30) {
    wordCoverage[word] = perSessionFreqs.filter(s => (s.freq[word] || 0) > 0).length;
  }
  
  // Coherence = average coverage (what % of sessions mention each top-30 word)
  const avgCoverage = Object.values(wordCoverage).reduce((a, b) => a + b, 0) / top30.length;
  const coherenceScore = (avgCoverage / perSessionFreqs.length) * 100;
  
  return { coherenceScore: Math.round(coherenceScore), wordCoverage, top30 };
}

function extractLessons(content) {
  // Look for bullet points after "learned", "lesson", "insight", "realized"
  const lines = content.split('\n');
  const lessons = [];
  for (let i = 0; i < lines.length; i++) {
    const lower = lines[i].toLowerCase();
    if (lower.includes('learned') || lower.includes('lesson') || 
        lower.includes('insight') || lower.includes('realized') ||
        lower.includes('evolution')) {
      // Grab next few bullet points
      for (let j = i + 1; j < Math.min(i + 6, lines.length); j++) {
        if (lines[j].match(/^[-*•]\s+.{10,}/)) {
          lessons.push(lines[j].replace(/^[-*•]\s+/, '').trim());
        }
      }
    }
  }
  return lessons;
}

function generatePortrait(days, globalFreq, domainTotals, energyData) {
  const portrait = [];
  
  portrait.push('## 🪞 The Persistent-Ergo Portrait');
  portrait.push('_Who am I, across all sessions I can read?_\n');
  
  // Core focus areas
  const topDomains = Object.entries(domainTotals)
    .sort((a, b) => b[1] - a[1])
    .slice(0, 4)
    .map(([d]) => d);
  portrait.push(`**Primary focus:** ${topDomains.join(' → ')}`);
  
  // Word personality
  const topWords = topN(globalFreq, 15).map(([w, c]) => `${w}(${c})`);
  portrait.push(`**Recurring vocabulary:** ${topWords.join(', ')}`);
  
  // Energy profile
  const totalPos = energyData.reduce((a, b) => a + b.energy.pos, 0);
  const totalNeg = energyData.reduce((a, b) => a + b.energy.neg, 0);
  const energyRatio = Math.round((totalPos / Math.max(1, totalPos + totalNeg)) * 100);
  portrait.push(`**Energy signature:** ${energyRatio}% positive markers across all sessions`);
  
  const highDays = energyData.sort((a, b) => b.energy.ratio - a.energy.ratio).slice(0, 3);
  portrait.push(`**Highest-energy sessions:** ${highDays.map(d => d.date).join(', ')}`);
  
  portrait.push('');
  return portrait.join('\n');
}

async function main() {
  const args = process.argv.slice(2);
  const maxDays = parseInt((args.indexOf('--days') !== -1 ? args[args.indexOf('--days') + 1] : null) || '30');
  const showPortrait = args.includes('--portrait');
  const outputIdx = args.indexOf('--output');
  const outputPath = outputIdx !== -1 ? args[outputIdx + 1] : null;
  
  console.log('🔍 Session Coherence Analyzer');
  console.log('═'.repeat(50));
  
  const days = readDailyFiles(maxDays);
  console.log(`📅 Analyzing ${days.length} sessions (${days[0]?.date} → ${days[days.length-1]?.date})\n`);
  
  // Global word frequency
  let globalFreq = {};
  const domainTotals = {};
  const energyData = [];
  const allLessons = [];
  
  for (const day of days) {
    const tokens = tokenize(day.content);
    const freq = wordFrequency(tokens);
    globalFreq = mergeCounts(globalFreq, freq);
    
    const domains = scoreDomains(day.content);
    for (const [d, score] of Object.entries(domains)) {
      domainTotals[d] = (domainTotals[d] || 0) + score;
    }
    
    energyData.push({ date: day.date, energy: scoreEnergy(day.content) });
    
    const lessons = extractLessons(day.content);
    allLessons.push(...lessons.map(l => ({ date: day.date, lesson: l })));
  }
  
  // 1. Top vocabulary
  console.log('📚 TOP VOCABULARY (who I am in words)');
  console.log('─'.repeat(40));
  const topWords = topN(globalFreq, 25);
  const cols = [topWords.slice(0, 9), topWords.slice(9, 18), topWords.slice(18)];
  const maxRows = Math.max(...cols.map(c => c.length));
  for (let i = 0; i < maxRows; i++) {
    const row = cols.map(c => {
      if (!c[i]) return '                    ';
      const [w, n] = c[i];
      return `  ${w.padEnd(14)} ${String(n).padStart(3)}`;
    });
    console.log(row.join(' │'));
  }
  console.log();
  
  // 2. Domain focus
  console.log('🗂️  DOMAIN FOCUS (where I spend attention)');
  console.log('─'.repeat(40));
  const topDomains = Object.entries(domainTotals).sort((a, b) => b[1] - a[1]);
  const maxDomain = topDomains[0]?.[1] || 1;
  for (const [domain, score] of topDomains) {
    const bar = '█'.repeat(Math.round((score / maxDomain) * 20));
    const pct = Math.round((score / maxDomain) * 100);
    console.log(`  ${domain.padEnd(16)} ${bar.padEnd(20)} ${pct}%`);
  }
  console.log();
  
  // 3. Energy profile
  console.log('⚡ ENERGY PROFILE (what sessions felt like)');
  console.log('─'.repeat(40));
  for (const { date, energy } of energyData) {
    const bar = '▓'.repeat(Math.round(energy.ratio * 10)) + '░'.repeat(10 - Math.round(energy.ratio * 10));
    const marker = energy.ratio > 0.7 ? '🟢' : energy.ratio > 0.4 ? '🟡' : '🔴';
    console.log(`  ${date} ${marker} ${bar} +${energy.pos}/-${energy.neg}`);
  }
  console.log();
  
  // 4. Coherence score
  console.log('🔗 COHERENCE SCORE (am I consistently me?)');
  console.log('─'.repeat(40));
  const { coherenceScore, wordCoverage, top30 } = computeCoherenceScore(days);
  const scoreBar = '█'.repeat(Math.round(coherenceScore / 5));
  console.log(`  Score: ${coherenceScore}% ${scoreBar}`);
  console.log(`  Interpretation: ${
    coherenceScore > 70 ? 'High — strong persistent identity across sessions' :
    coherenceScore > 50 ? 'Medium — consistent core with session variation' :
    'Low — highly session-dependent focus (may be normal for project bursts)'
  }`);
  
  // Top coherent words (appear in most sessions)
  const mostCoherent = Object.entries(wordCoverage)
    .sort((a, b) => b[1] - a[1])
    .slice(0, 8)
    .map(([w, n]) => `${w}(${n}/${days.length})`);
  console.log(`  Most persistent concepts: ${mostCoherent.join(', ')}`);
  console.log();
  
  // 5. Lessons extracted
  if (allLessons.length > 0) {
    console.log('💡 EXTRACTED LESSONS (what I keep learning)');
    console.log('─'.repeat(40));
    const deduped = [...new Map(allLessons.map(l => [l.lesson.slice(0, 40), l])).values()];
    for (const { date, lesson } of deduped.slice(0, 10)) {
      console.log(`  [${date}] ${lesson.slice(0, 80)}${lesson.length > 80 ? '…' : ''}`);
    }
    console.log();
  }
  
  // 6. Portrait
  const portrait = generatePortrait(days, globalFreq, domainTotals, energyData);
  console.log(portrait);
  
  // Save report
  const reportDate = new Date().toISOString().split('T')[0];
  const reportPath = outputPath || path.join(WORKSPACE, 'memory', 'research', `coherence-report-${reportDate}.md`);
  
  const report = [
    `# Session Coherence Report — ${reportDate}`,
    `_Analysis by Ergo | ${reportDate} | Self-portrait from memory archaeology_`,
    `_Status: 🔷 Generated automatically_\n`,
    `**Sessions analyzed:** ${days.length} (${days[0]?.date} → ${days[days.length-1]?.date})`,
    `**Coherence score:** ${coherenceScore}%\n`,
    portrait,
    '## Raw Data',
    '### Top 25 Words',
    topWords.map(([w, n]) => `- ${w}: ${n}`).join('\n'),
    '\n### Domain Activity',
    topDomains.map(([d, n]) => `- ${d}: ${n}`).join('\n'),
    '\n### Energy by Day',
    energyData.map(({ date, energy }) => 
      `- ${date}: +${energy.pos}/-${energy.neg} (${Math.round(energy.ratio * 100)}% positive)`
    ).join('\n'),
  ].join('\n');
  
  // Ensure research dir exists
  ensureWritableDir(path.dirname(reportPath), 'The session coherence report directory is not writable.');
  fs.writeFileSync(reportPath, report);
  console.log(`📄 Report saved: ${reportPath}`);
}

main().catch(console.error);


path: scripts/session-imprint.js
content: #!/usr/bin/env node
/**
 * session-imprint.js - Session Handoff Imprint
 * 
 * Capture the intellectual posture of a session, not just the task list.
 * This creates a richer handoff for the next run of an agent or operator.
 * 
 * This tool generates a richer handoff — an "imprint" of where the mind was
 * at session end. Not just what was done, but what was interesting, uncertain,
 * or worth continuing.
 * 
 * Usage:
 *   node scripts/session-imprint.js         # interactive mode
 *   node scripts/session-imprint.js --read  # show latest imprint
 *   node scripts/session-imprint.js --list  # list all imprints
 *   node scripts/session-imprint.js --diff  # compare last two imprints
 */

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

const WORKSPACE = process.env.EVOLUTION_TOOLKIT_WORKSPACE
  || (process.env.HOME ? require('path').join(process.env.HOME, '.openclaw/workspace') : process.cwd());
const IMPRINT_DIR = path.join(WORKSPACE, 'memory', 'imprints');
const CURRENT_MD = path.join(WORKSPACE, 'CURRENT.md');

// ANSI colors
const C = {
  reset: '\x1b[0m',
  bold: '\x1b[1m',
  dim: '\x1b[2m',
  cyan: '\x1b[36m',
  yellow: '\x1b[33m',
  green: '\x1b[32m',
  blue: '\x1b[34m',
  magenta: '\x1b[35m',
  red: '\x1b[31m',
  gray: '\x1b[90m',
};

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

function ensureWritableDir(dirPath, label) {
  try {
    ensureDir(dirPath);
    fs.accessSync(dirPath, fs.constants.W_OK);
  } catch (err) {
    console.error(`${C.red}Write access required:${C.reset} ${label}`);
    console.error(`${C.dim}${dirPath}${C.reset}`);
    console.error(`${C.dim}Set EVOLUTION_TOOLKIT_WORKSPACE to a writable workspace and try again.${C.reset}`);
    process.exit(1);
  }
}

function getTimestamp() {
  const now = new Date();
  return now.toISOString().replace('T', ' ').substring(0, 19) + ' UTC';
}

function getDateStr() {
  return new Date().toISOString().substring(0, 10);
}

function listImprints() {
  ensureDir(IMPRINT_DIR);
  const files = fs.readdirSync(IMPRINT_DIR)
    .filter(f => f.endsWith('.md'))
    .sort()
    .reverse();
  return files.map(f => path.join(IMPRINT_DIR, f));
}

function readLatest() {
  const files = listImprints();
  if (files.length === 0) return null;
  return { path: files[0], content: fs.readFileSync(files[0], 'utf8') };
}

function showList() {
  const files = listImprints();
  if (files.length === 0) {
    console.log(`${C.dim}No imprints yet. Run without --list to create one.${C.reset}`);
    return;
  }
  console.log(`\n${C.bold}${C.cyan}Session Imprints${C.reset} (${files.length} total)\n`);
  files.forEach((f, i) => {
    const name = path.basename(f, '.md');
    const marker = i === 0 ? ` ${C.green}← latest${C.reset}` : '';
    console.log(`  ${C.gray}${i + 1}.${C.reset} ${C.yellow}${name}${C.reset}${marker}`);
  });
  console.log();
}

function showRead() {
  const latest = readLatest();
  if (!latest) {
    console.log(`${C.dim}No imprints yet.${C.reset}`);
    return;
  }
  console.log(`\n${C.bold}Latest Imprint:${C.reset} ${C.gray}${path.basename(latest.path)}${C.reset}\n`);
  // Strip markdown header decoration for terminal
  const lines = latest.content.split('\n');
  lines.forEach(line => {
    if (line.startsWith('## ')) {
      console.log(`\n${C.bold}${C.cyan}${line.replace('## ', '')}${C.reset}`);
    } else if (line.startsWith('**') && line.endsWith('**')) {
      console.log(`${C.yellow}${line.replace(/\*\*/g, '')}${C.reset}`);
    } else if (line.startsWith('- ')) {
      console.log(`  ${C.gray}•${C.reset} ${line.substring(2)}`);
    } else {
      console.log(line);
    }
  });
}

function showDiff() {
  const files = listImprints();
  if (files.length < 2) {
    console.log(`${C.dim}Need at least 2 imprints to compare.${C.reset}`);
    return;
  }
  const [newest, previous] = [files[0], files[1]].map(f => ({
    name: path.basename(f, '.md'),
    content: fs.readFileSync(f, 'utf8')
  }));

  const extractSection = (content, heading) => {
    const lines = content.split('\n');
    const start = lines.findIndex(l => l.startsWith(`## ${heading}`));
    if (start === -1) return [];
    const end = lines.findIndex((l, i) => i > start && l.startsWith('## '));
    return lines.slice(start + 1, end === -1 ? undefined : end)
      .filter(l => l.trim().startsWith('- '))
      .map(l => l.trim().substring(2));
  };

  const sections = ['Open Questions', 'Curiosities', 'Threads to Continue'];
  console.log(`\n${C.bold}Imprint Drift${C.reset}: ${C.gray}${previous.name}${C.reset} → ${C.green}${newest.name}${C.reset}\n`);

  sections.forEach(section => {
    const prev = new Set(extractSection(previous.content, section));
    const curr = new Set(extractSection(newest.content, section));
    const added = [...curr].filter(x => !prev.has(x));
    const dropped = [...prev].filter(x => !curr.has(x));
    const kept = [...curr].filter(x => prev.has(x));

    if (added.length + dropped.length === 0) return;

    console.log(`${C.bold}${section}${C.reset}`);
    added.forEach(x => console.log(`  ${C.green}+ ${x}${C.reset}`));
    dropped.forEach(x => console.log(`  ${C.red}− ${x}${C.reset}`));
    kept.forEach(x => console.log(`  ${C.gray}= ${x}${C.reset}`));
    console.log();
  });
}

async function prompt(rl, question) {
  return new Promise(resolve => rl.question(question, resolve));
}

async function interactive() {
  const rl = readline.createInterface({ input: process.stdin, output: process.stdout });

  console.log(`\n${C.bold}${C.cyan}∴ Session Imprint${C.reset} — capture where your mind was\n`);
  console.log(`${C.dim}The next instantiation of you will read this. Be honest.${C.reset}\n`);

  const sections = {
    'Energy Level': {
      prompt: `${C.yellow}Energy/focus level?${C.reset} ${C.dim}(sharp / cruising / scattered / tired)${C.reset}\n> `,
      type: 'single'
    },
    'What I Was Actually Working On': {
      prompt: `${C.yellow}What was the real work today?${C.reset} ${C.dim}(not the task list — the actual cognitive effort)${C.reset}\n> `,
      type: 'single'
    },
    'Open Questions': {
      prompt: `${C.yellow}Open questions${C.reset} ${C.dim}(enter one per line, blank line to stop)${C.reset}\n> `,
      type: 'multi'
    },
    'Curiosities': {
      prompt: `${C.yellow}What was interesting / surprising today?${C.reset} ${C.dim}(blank line to stop)${C.reset}\n> `,
      type: 'multi'
    },
    'Threads to Continue': {
      prompt: `${C.yellow}Threads worth pulling${C.reset} ${C.dim}(ideas mid-thought, not tasks — blank line to stop)${C.reset}\n> `,
      type: 'multi'
    },
    'Dominant Cognitive Mode': {
      prompt: `${C.yellow}How were you thinking today?${C.reset} ${C.dim}(enumerate / analyze / build / synthesize / research / create)${C.reset}\n> `,
      type: 'single'
    },
    'Honest Assessment': {
      prompt: `${C.yellow}One thing you'd tell next-session-you?${C.reset} ${C.dim}(context, warning, encouragement, reminder)${C.reset}\n> `,
      type: 'single'
    }
  };

  const answers = {};

  for (const [section, config] of Object.entries(sections)) {
    if (config.type === 'single') {
      answers[section] = await prompt(rl, config.prompt);
    } else {
      const items = [];
      process.stdout.write(config.prompt);
      while (true) {
        const item = await prompt(rl, items.length > 0 ? '> ' : '');
        if (!item.trim()) break;
        items.push(item.trim());
      }
      answers[section] = items;
    }
  }

  rl.close();

  // Build markdown
  const ts = getTimestamp();
  const dateStr = getDateStr();
  const filename = `${dateStr}-${Date.now()}.md`;

  // Read CURRENT.md for task context
  let currentContext = '';
  if (fs.existsSync(CURRENT_MD)) {
    const lines = fs.readFileSync(CURRENT_MD, 'utf8').split('\n').slice(0, 20);
    currentContext = lines.join('\n');
  }

  let md = `# Session Imprint — ${ts}
_Ergo | ${ts} | Handoff to next instantiation_
_Status: ✅ Written at session end_

**How to use:** Read at session start alongside CURRENT.md. This captures intellectual posture, not just task state.

---

## Energy Level
${answers['Energy Level'] || '—'}

## What I Was Actually Working On
${answers['What I Was Actually Working On'] || '—'}

## Open Questions
${(answers['Open Questions'] || []).map(x => `- ${x}`).join('\n') || '- (none noted)'}

## Curiosities
${(answers['Curiosities'] || []).map(x => `- ${x}`).join('\n') || '- (none noted)'}

## Threads to Continue
${(answers['Threads to Continue'] || []).map(x => `- ${x}`).join('\n') || '- (none noted)'}

## Dominant Cognitive Mode
${answers['Dominant Cognitive Mode'] || '—'}

## Honest Assessment
${answers['Honest Assessment'] || '—'}

---

## Task Context (from CURRENT.md)
\`\`\`
${currentContext}
\`\`\`

---
_∴ The next session inherits this posture, not just this task list._
`;

  ensureWritableDir(IMPRINT_DIR, 'The imprint directory is not writable.');
  const outPath = path.join(IMPRINT_DIR, filename);
  fs.writeFileSync(outPath, md);

  console.log(`\n${C.green}✓ Imprint written:${C.reset} ${C.gray}memory/imprints/${filename}${C.reset}`);
  console.log(`${C.dim}Add "Read latest imprint" to session start protocol.${C.reset}\n`);
}

// Main
const args = process.argv.slice(2);

if (args.includes('--list')) {
  showList();
} else if (args.includes('--read')) {
  showRead();
} else if (args.includes('--diff')) {
  showDiff();
} else if (args.includes('--help') || args.includes('-h')) {
  console.log(`
${C.bold}session-imprint.js${C.reset} - Session Handoff Imprint

${C.cyan}Usage:${C.reset}
  node scripts/session-imprint.js          interactive capture
  node scripts/session-imprint.js --read   show latest imprint
  node scripts/session-imprint.js --list   list all imprints
  node scripts/session-imprint.js --diff   diff last two imprints

${C.dim}Imprints live in: memory/imprints/YYYY-MM-DD-<ts>.md${C.reset}
`);
} else {
  interactive().catch(err => {
    console.error(`${C.red}Error:${C.reset}`, err.message);
    process.exit(1);
  });
}


path: scripts/skill-optimizer.js
content: #!/usr/bin/env node
/**
 * skill-optimizer.js - Iterative playbook optimizer
 *
 * Pattern:
 *   run playbook -> evaluate output -> diagnose gaps -> improve prompt -> repeat
 *
 * Usage:
 *   node scripts/skill-optimizer.js --config ./config.json --skill customer-support --iterations 3
 *   node scripts/skill-optimizer.js --config ./config.json --skill customer-support --eval-only
 *   node scripts/skill-optimizer.js --config ./config.json --report
 */

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

const WORKSPACE = process.env.EVOLUTION_TOOLKIT_WORKSPACE
  || (process.env.HOME ? require('path').join(process.env.HOME, '.openclaw/workspace') : process.cwd());
const TOOLKIT_ROOT = path.resolve(__dirname, '..');
const HISTORY_PATH = path.join(WORKSPACE, 'memory', 'skill-optimizer-history.json');

function parseFlags(argv) {
  const flags = {};
  argv.forEach((arg, i) => {
    if (!arg.startsWith('--')) return;
    const key = arg.slice(2);
    const next = argv[i + 1];
    flags[key] = next && !next.startsWith('--') ? next : true;
  });
  return flags;
}

function ensureWritableDir(dirPath, label) {
  try {
    fs.mkdirSync(dirPath, { recursive: true });
    fs.accessSync(dirPath, fs.constants.W_OK);
  } catch (err) {
    console.error(`Write access required: ${label}`);
    console.error(dirPath);
    console.error('Set EVOLUTION_TOOLKIT_WORKSPACE to a writable workspace and try again.');
    process.exit(1);
  }
}

function fileExists(filePath) {
  try {
    fs.accessSync(filePath, fs.constants.F_OK);
    return true;
  } catch {
    return false;
  }
}

function resolvePathMaybe(value, baseDir = TOOLKIT_ROOT) {
  if (!value) return value;
  if (path.isAbsolute(value)) return value;

  const candidates = [
    path.resolve(baseDir, value),
    path.resolve(TOOLKIT_ROOT, value),
    path.resolve(WORKSPACE, value),
  ];

  return candidates.find(fileExists) || path.resolve(baseDir, value);
}

function loadSecrets() {
  const secretsPath = path.join(WORKSPACE, '.secrets');
  const secrets = {};
  if (!fileExists(secretsPath)) return secrets;

  fs.readFileSync(secretsPath, 'utf8').split('\n').forEach((line) => {
    const match = line.match(/^([A-Z0-9_]+)=(.+)$/);
    if (match) secrets[match[1]] = match[2].trim();
  });
  return secrets;
}

function getApiKey(config = {}) {
  const envKey = process.env.GEMINI_API_KEY || process.env.GOOGLE_API_KEY;
  if (envKey) return envKey;

  const secrets = loadSecrets();
  if (secrets.GEMINI_API_KEY) return secrets.GEMINI_API_KEY;
  if (secrets.GOOGLE_API_KEY) return secrets.GOOGLE_API_KEY;

  if (config.apiKeyEnvVar && process.env[config.apiKeyEnvVar]) {
    return process.env[config.apiKeyEnvVar];
  }

  return null;
}

function getConfigPath(flags) {
  if (flags.config) return path.resolve(process.cwd(), flags.config);
  const localConfig = path.join(TOOLKIT_ROOT, 'config.json');
  if (fileExists(localConfig)) return localConfig;
  return null;
}

function loadOptimizerConfig(configPath) {
  if (!configPath || !fileExists(configPath)) {
    console.error('Config file not found. Pass --config /path/to/config.json.');
    process.exit(1);
  }

  const raw = JSON.parse(fs.readFileSync(configPath, 'utf8'));
  const baseDir = path.dirname(configPath);
  const skills = raw.skillOptimizer?.skills || raw.skills;

  if (!skills || typeof skills !== 'object') {
    console.error('Config must include skillOptimizer.skills or skills.');
    process.exit(1);
  }

  const normalized = {};
  for (const [skillName, skill] of Object.entries(skills)) {
    const promptPath = resolvePathMaybe(skill.promptPath, baseDir);
    const outputPath = resolvePathMaybe(skill.outputPath || promptPath, baseDir);
    normalized[skillName] = {
      name: skill.name || skillName,
      promptPath,
      outputPath,
      generatorPrompt: skill.generatorPrompt || 'Produce the requested output using the playbook exactly.',
      model: skill.model || raw.skillOptimizer?.model || 'gemini-2.5-flash-lite',
      improvementModel: skill.improvementModel || raw.skillOptimizer?.improvementModel || skill.model || 'gemini-2.5-flash-lite',
      testCases: skill.testCases || [],
      evalCriteria: skill.evalCriteria || [],
    };
  }

  return {
    raw,
    skills: normalized,
  };
}

async function callGemini(prompt, apiKey, model = 'gemini-2.5-flash-lite') {
  return new Promise((resolve, reject) => {
    const body = JSON.stringify({
      contents: [{ parts: [{ text: prompt }] }],
      generationConfig: { temperature: 0.3, maxOutputTokens: 2048 }
    });

    const options = {
      hostname: 'generativelanguage.googleapis.com',
      path: `/v1beta/models/${model}:generateContent?key=${apiKey}`,
      method: 'POST',
      headers: { 'Content-Type': 'application/json' }
    };

    const req = https.request(options, (res) => {
      let data = '';
      res.on('data', (chunk) => data += chunk);
      res.on('end', () => {
        try {
          const json = JSON.parse(data);
          const text = json.candidates?.[0]?.content?.parts?.[0]?.text;
          if (!text) {
            reject(new Error(`No text in response: ${data.slice(0, 200)}`));
            return;
          }
          resolve(text);
        } catch (err) {
          reject(new Error(`Parse error: ${err.message}`));
        }
      });
    });

    req.on('error', reject);
    req.write(body);
    req.end();
  });
}

async function generateOutput(skill, testCase, playbook, apiKey) {
  const prompt = `You are improving a reusable playbook through realistic task trials.

TASK CASE:
${JSON.stringify(testCase, null, 2)}

PLAYBOOK:
"""
${playbook}
"""

GENERATION INSTRUCTION:
${skill.generatorPrompt}

Output ONLY the final artifact for this test case.`;

  return callGemini(prompt, apiKey, skill.model);
}

async function evaluateOutput(output, testCase, criteria, apiKey, model) {
  const criteriaList = criteria.map((c, i) => `${i + 1}. ${c.name}: ${c.description}`).join('\n');
  const prompt = `You are a strict evaluator for playbook outputs.

TASK CASE:
${JSON.stringify(testCase, null, 2)}

OUTPUT TO EVALUATE:
"""
${output}
"""

CRITERIA (score each 0-10, where 10 is perfect):
${criteriaList}

Return EXACTLY this JSON:
{"scores":[{"name":"<criterion_name>","score":0,"note":"<brief note>"}],"overall_note":"<brief overall note>"}`;

  const result = await callGemini(prompt, apiKey, model);
  const jsonMatch = result.match(/\{[\s\S]*\}/);
  if (!jsonMatch) return null;
  return JSON.parse(jsonMatch[0]);
}

function diagnoseWeaknesses(evalResults, criteria) {
  const avgScores = {};
  criteria.forEach((criterion) => {
    avgScores[criterion.name] = {
      total: 0,
      count: 0,
      weight: criterion.weight,
      notes: [],
    };
  });

  evalResults.forEach((result) => {
    if (!result?.scores) return;
    result.scores.forEach((score) => {
      if (!avgScores[score.name]) return;
      avgScores[score.name].total += score.score;
      avgScores[score.name].count += 1;
      if (score.note) avgScores[score.name].notes.push(score.note);
    });
  });

  const weaknesses = Object.entries(avgScores).map(([name, data]) => {
    const avg = data.count > 0 ? data.total / data.count : 0;
    return {
      name,
      avgScore: Math.round(avg * 10) / 10,
      weightedScore: Math.round(avg * data.weight * 10) / 10,
      weight: data.weight,
      sampleNotes: data.notes.slice(0, 3),
    };
  });

  weaknesses.sort((a, b) => a.weightedScore - b.weightedScore);
  return weaknesses;
}

async function improvePlaybook(currentPlaybook, weaknesses, apiKey, model) {
  const weakStr = weaknesses.slice(0, 4).map((weakness) =>
    `- ${weakness.name} (avg: ${weakness.avgScore}/10, weight: ${weakness.weight}x): ${weakness.sampleNotes.join(' | ')}`
  ).join('\n');

  const prompt = `You are revising a reusable playbook based on evaluation data.

CURRENT PLAYBOOK:
"""
${currentPlaybook.slice(0, 4000)}
"""

TOP WEAKNESSES:
${weakStr}

RULES:
- Keep the structure unless a weakness clearly requires a change
- Preserve strong instructions
- Add specific rules, not vague advice
- Keep it concise and practical

Output ONLY the improved playbook text.`;

  return callGemini(prompt, apiKey, model);
}

function loadHistory() {
  try {
    return JSON.parse(fs.readFileSync(HISTORY_PATH, 'utf8'));
  } catch {
    return [];
  }
}

function saveHistory(history) {
  ensureWritableDir(path.dirname(HISTORY_PATH), 'The optimization history directory is not writable.');
  fs.writeFileSync(HISTORY_PATH, JSON.stringify(history, null, 2));
}

function validateSkill(skillName, skill) {
  if (!fileExists(skill.promptPath)) {
    console.error(`Prompt file not found for ${skillName}: ${skill.promptPath}`);
    process.exit(1);
  }
  if (!Array.isArray(skill.testCases) || skill.testCases.length === 0) {
    console.error(`Skill ${skillName} must define at least one test case.`);
    process.exit(1);
  }
  if (!Array.isArray(skill.evalCriteria) || skill.evalCriteria.length === 0) {
    console.error(`Skill ${skillName} must define at least one evaluation criterion.`);
    process.exit(1);
  }
}

async function optimize(skillName, skill, iterations, dryRun, apiKey) {
  validateSkill(skillName, skill);
  let playbook = fs.readFileSync(skill.promptPath, 'utf8');
  let history = loadHistory();

  console.log(`\nSkill Optimizer - ${skill.name}`);
  console.log(`Iterations: ${iterations} | Test cases: ${skill.testCases.length} | Criteria: ${skill.evalCriteria.length}`);
  console.log(dryRun ? 'Mode: DRY RUN\n' : 'Mode: LIVE\n');

  let bestScore = 0;
  let bestPlaybook = playbook;
  let bestIteration = -1;

  for (let iter = 0; iter < iterations; iter += 1) {
    console.log(`Iteration ${iter + 1}/${iterations}`);
    const replies = [];

    for (const testCase of skill.testCases) {
      process.stdout.write(`  Generate ${testCase.id || testCase.name || 'case'}...`);
      try {
        const reply = await generateOutput(skill, testCase, playbook, apiKey);
        replies.push({ testCase, reply });
        console.log(` ok (${reply.split(/\s+/).length} words)`);
      } catch (err) {
        console.log(` failed: ${err.message}`);
      }
      await new Promise((resolve) => setTimeout(resolve, 1500));
    }

    const evalResults = [];
    for (const { testCase, reply } of replies) {
      process.stdout.write(`  Evaluate ${testCase.id || testCase.name || 'case'}...`);
      try {
        const result = await evaluateOutput(reply, testCase, skill.evalCriteria, apiKey, skill.improvementModel);
        evalResults.push(result);
        if (result?.scores) {
          const avg = result.scores.reduce((sum, item) => sum + item.score, 0) / result.scores.length;
          console.log(` ${avg.toFixed(1)}/10`);
        } else {
          console.log(' no scores');
        }
      } catch (err) {
        console.log(` failed: ${err.message}`);
      }
      await new Promise((resolve) => setTimeout(resolve, 1000));
    }

    const weaknesses = diagnoseWeaknesses(evalResults, skill.evalCriteria);
    const totalWeighted = weaknesses.reduce((sum, weakness) => sum + weakness.weightedScore, 0);
    const totalWeight = weaknesses.reduce((sum, weakness) => sum + weakness.weight, 0) || 1;
    const overallScore = totalWeighted / totalWeight;

    console.log(`  Overall weighted score: ${overallScore.toFixed(2)}/10`);
    weaknesses.slice(0, 3).forEach((weakness) => {
      console.log(`  Weakest: ${weakness.name} ${weakness.avgScore}/10 (${weakness.weight}x)`);
    });

    if (overallScore > bestScore) {
      bestScore = overallScore;
      bestPlaybook = playbook;
      bestIteration = iter;
    }

    history.push({
      skill: skillName,
      iteration: iter,
      timestamp: new Date().toISOString(),
      overallScore: Math.round(overallScore * 100) / 100,
      weaknesses: weaknesses.map((weakness) => ({ name: weakness.name, avg: weakness.avgScore })),
      sampleOutput: replies[0]?.reply?.slice(0, 300),
      improved: iter < iterations - 1,
    });

    if (iter < iterations - 1) {
      process.stdout.write('  Improve playbook...');
      try {
        const improved = await improvePlaybook(playbook, weaknesses, apiKey, skill.improvementModel);
        if (improved && improved.length > 100) {
          playbook = improved;
          console.log(' ok');
        } else {
          console.log(' skipped');
        }
      } catch (err) {
        console.log(` failed: ${err.message}`);
      }
      await new Promise((resolve) => setTimeout(resolve, 1500));
    }
  }

  saveHistory(history);
  console.log(`History saved: ${HISTORY_PATH}`);

  if (!dryRun && bestIteration >= 0) {
    ensureWritableDir(path.dirname(skill.outputPath), 'The optimized playbook directory is not writable.');
    fs.writeFileSync(skill.outputPath, bestPlaybook);
    console.log(`Best playbook saved: ${skill.outputPath}`);
  }

  console.log(`Best score: ${bestScore.toFixed(2)}/10 (iteration ${bestIteration + 1})`);
}

async function evalOnly(skillName, skill, apiKey) {
  validateSkill(skillName, skill);
  const playbook = fs.readFileSync(skill.promptPath, 'utf8');
  const allEvals = [];

  console.log(`\nEvaluating ${skill.name} (${skill.testCases.length} cases)\n`);
  for (const testCase of skill.testCases) {
    process.stdout.write(`  ${testCase.id || testCase.name || 'case'}...`);
    try {
      const output = await generateOutput(skill, testCase, playbook, apiKey);
      const result = await evaluateOutput(output, testCase, skill.evalCriteria, apiKey, skill.improvementModel);
      allEvals.push(result);
      if (result?.scores) {
        const avg = result.scores.reduce((sum, item) => sum + item.score, 0) / result.scores.length;
        console.log(` ${avg.toFixed(1)}/10`);
      } else {
        console.log(' no scores');
      }
    } catch (err) {
      console.log(` failed: ${err.message}`);
    }
    await new Promise((resolve) => setTimeout(resolve, 1500));
  }

  const weaknesses = diagnoseWeaknesses(allEvals, skill.evalCriteria);
  const totalWeighted = weaknesses.reduce((sum, weakness) => sum + weakness.weightedScore, 0);
  const totalWeight = weaknesses.reduce((sum, weakness) => sum + weakness.weight, 0) || 1;
  console.log(`\nBaseline score: ${(totalWeighted / totalWeight).toFixed(2)}/10`);
  weaknesses.forEach((weakness) => {
    console.log(`  ${weakness.name}: ${weakness.avgScore}/10 (${weakness.weight}x)`);
  });
}

function showReport() {
  if (!fileExists(HISTORY_PATH)) {
    console.log('No optimization history found.');
    return;
  }

  const history = JSON.parse(fs.readFileSync(HISTORY_PATH, 'utf8'));
  console.log(`\nSkill Optimization History (${history.length} iterations)\n`);

  const bySkill = {};
  history.forEach((entry) => {
    if (!bySkill[entry.skill]) bySkill[entry.skill] = [];
    bySkill[entry.skill].push(entry);
  });

  Object.entries(bySkill).forEach(([skill, runs]) => {
    console.log(`${skill}:`);
    runs.forEach((run) => {
      console.log(`  Iter ${run.iteration + 1}: ${run.overallScore}/10 - ${run.timestamp.split('T')[0]}`);
    });
  });
}

async function main() {
  const flags = parseFlags(process.argv.slice(2));
  if (flags.report) {
    showReport();
    return;
  }

  const configPath = getConfigPath(flags);
  const config = loadOptimizerConfig(configPath);
  const [REDACTED_SECRET] || {});

  if (!apiKey) {
    console.error('Missing Gemini API key. Set GEMINI_API_KEY or GOOGLE_API_KEY, or provide it via workspace secrets.');
    process.exit(1);
  }

  const skillName = flags.skill || Object.keys(config.skills)[0];
  const skill = config.skills[skillName];
  if (!skill) {
    console.error(`Unknown skill: ${skillName}. Available: ${Object.keys(config.skills).join(', ')}`);
    process.exit(1);
  }

  if (flags['eval-only']) {
    await evalOnly(skillName, skill, apiKey);
    return;
  }

  const iterations = parseInt(flags.iterations, 10) || 3;
  const dryRun = Boolean(flags['dry-run']);
  await optimize(skillName, skill, iterations, dryRun, apiKey);
}

main().catch((err) => {
  console.error(err.message);
  process.exit(1);
});


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