Skip to content

Commit 3b525f9

Browse files
author
StackMemory Bot (CLI)
committed
feat(gepa): delta output, multi-target config, apply command, auto-hook
- Replace full-file variant output with condensed delta summaries (added/changed/removed sections + score delta) - Add multi-target config (targets[] array) with per-target eval sets for global, provenantai, dispatch, and stackmemory CLAUDE.md files - Add `apply` command to patch best variant into target with backup - Add `run-all` command to optimize all targets sequentially - Add `targets` command to list configured targets - Add `--target <name>` flag for single-target optimization - Add `--full` flag to diff for unified diff (delta is default) - Add gepa-session-hook.js for Claude Code Stop event auto-wiring: tracks sessions, auto-triggers reflect+optimize after N sessions
1 parent c769e90 commit 3b525f9

3 files changed

Lines changed: 508 additions & 13 deletions

File tree

scripts/gepa/config.json

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,33 @@
88
"backup": true
99
},
1010

11+
"targets": [
12+
{
13+
"name": "global",
14+
"file": "~/.claude/CLAUDE.md",
15+
"evals": ["coding-tasks.jsonl"],
16+
"description": "Global rules affecting every session"
17+
},
18+
{
19+
"name": "provenantai",
20+
"file": "~/Dev/provenantai/.claude/CLAUDE.md",
21+
"evals": ["coding-tasks.jsonl"],
22+
"description": "Project conventions (Express, Jest, migrations)"
23+
},
24+
{
25+
"name": "dispatch",
26+
"file": "~/Dev/provenantai/.claude/commands/dispatch.md",
27+
"evals": ["conductor-tasks.jsonl"],
28+
"description": "Agent orchestration prompt"
29+
},
30+
{
31+
"name": "stackmemory",
32+
"file": "CLAUDE.md",
33+
"evals": ["stackmemory-tasks.jsonl"],
34+
"description": "StackMemory project prompt"
35+
}
36+
],
37+
1138
"evolution": {
1239
"populationSize": 4,
1340
"generations": 10,
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
#!/usr/bin/env node
2+
/**
3+
* GEPA Session Hook — Auto-wires into Claude Code Stop event.
4+
*
5+
* Pipeline:
6+
* 1. Save session metrics (eval-tracker)
7+
* 2. Count accumulated sessions since last optimization
8+
* 3. If threshold reached → reflect → optimize → show delta
9+
* 4. Never blocks — optimization runs async in background
10+
*
11+
* Install: Add to ~/.claude/settings.json hooks.Stop
12+
*
13+
* Env:
14+
* GEPA_DIR Override GEPA directory (default: parent of this script)
15+
* GEPA_AUTO_THRESHOLD Sessions before auto-optimize (default: 10)
16+
* GEPA_AUTO_DISABLE Set to "1" to disable auto-optimization
17+
*/
18+
19+
import fs from 'fs';
20+
import path from 'path';
21+
import { fileURLToPath } from 'url';
22+
import { spawn } from 'child_process';
23+
24+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
25+
const GEPA_DIR = process.env.GEPA_DIR || path.resolve(__dirname, '..');
26+
const RESULTS_DIR = path.join(GEPA_DIR, 'results');
27+
const SESSIONS_DIR = path.join(RESULTS_DIR, 'sessions');
28+
const STATE_PATH = path.join(GEPA_DIR, 'state.json');
29+
const HOOK_STATE_PATH = path.join(GEPA_DIR, '.hook-state.json');
30+
31+
const THRESHOLD = parseInt(process.env.GEPA_AUTO_THRESHOLD || '10');
32+
const DISABLED = process.env.GEPA_AUTO_DISABLE === '1';
33+
34+
// Ensure directories
35+
[RESULTS_DIR, SESSIONS_DIR].forEach((dir) => {
36+
if (!fs.existsSync(dir)) fs.mkdirSync(dir, { recursive: true });
37+
});
38+
39+
/**
40+
* Read hook state (tracks sessions since last optimization)
41+
*/
42+
function getHookState() {
43+
if (fs.existsSync(HOOK_STATE_PATH)) {
44+
return JSON.parse(fs.readFileSync(HOOK_STATE_PATH, 'utf8'));
45+
}
46+
return {
47+
sessionsSinceLastOptimize: 0,
48+
lastOptimizeTime: null,
49+
lastSessionTime: null,
50+
totalSessions: 0,
51+
};
52+
}
53+
54+
function saveHookState(state) {
55+
fs.writeFileSync(HOOK_STATE_PATH, JSON.stringify(state, null, 2));
56+
}
57+
58+
/**
59+
* Step 1: Save session metrics
60+
*/
61+
function saveSessionMetrics() {
62+
const sessionId = process.env.CLAUDE_SESSION_ID || `session-${Date.now()}`;
63+
const variant = process.env.GEPA_VARIANT || 'current';
64+
65+
// Minimal session record — eval-tracker captures detailed data via its own hooks
66+
const record = {
67+
sessionId,
68+
variant,
69+
timestamp: new Date().toISOString(),
70+
cwd: process.env.CLAUDE_CWD || process.cwd(),
71+
};
72+
73+
// Append to scores.jsonl
74+
fs.appendFileSync(
75+
path.join(RESULTS_DIR, 'scores.jsonl'),
76+
JSON.stringify(record) + '\n'
77+
);
78+
79+
return sessionId;
80+
}
81+
82+
/**
83+
* Step 2: Check if optimization should trigger
84+
*/
85+
function shouldOptimize(hookState) {
86+
if (DISABLED) return false;
87+
if (hookState.sessionsSinceLastOptimize < THRESHOLD) return false;
88+
89+
// Don't optimize more than once per hour
90+
if (hookState.lastOptimizeTime) {
91+
const elapsed = Date.now() - new Date(hookState.lastOptimizeTime).getTime();
92+
if (elapsed < 3600000) return false;
93+
}
94+
95+
// Need GEPA state initialized
96+
if (!fs.existsSync(STATE_PATH)) return false;
97+
98+
return true;
99+
}
100+
101+
/**
102+
* Step 3: Run optimization in background (non-blocking)
103+
*/
104+
function triggerOptimization(hookState) {
105+
const optimizePath = path.join(GEPA_DIR, 'optimize.js');
106+
const reflectPath = path.join(GEPA_DIR, 'hooks', 'reflect.js');
107+
108+
// Run reflect → optimize as a background pipeline
109+
const script = `
110+
// Reflect first (generates insights for mutation context)
111+
try {
112+
const { generateReflection } = await import('${reflectPath}');
113+
await generateReflection();
114+
} catch {}
115+
116+
// Then optimize (1 generation, quick)
117+
const { execSync } = await import('child_process');
118+
try {
119+
execSync('node ${optimizePath} mutate', { stdio: 'pipe', timeout: 300000 });
120+
execSync('node ${optimizePath} score', { stdio: 'pipe', timeout: 300000 });
121+
122+
// Read result and notify
123+
const fs = await import('fs');
124+
const state = JSON.parse(fs.readFileSync('${STATE_PATH}', 'utf8'));
125+
const msg = \`[GEPA] Auto-optimized: gen \${state.currentGeneration}, best=\${state.bestVariant} (\${(state.bestScore * 100).toFixed(1)}%). Run 'node ${optimizePath} apply' to apply.\`;
126+
process.stderr.write(msg + '\\n');
127+
} catch (e) {
128+
process.stderr.write('[GEPA] Auto-optimize failed: ' + e.message + '\\n');
129+
}
130+
`;
131+
132+
// Fire and forget — don't block the session end
133+
const child = spawn('node', ['--input-type=module', '-e', script], {
134+
detached: true,
135+
stdio: ['pipe', 'ignore', 'inherit'],
136+
env: { ...process.env, GEPA_DIR },
137+
});
138+
139+
child.unref();
140+
141+
// Update hook state
142+
hookState.sessionsSinceLastOptimize = 0;
143+
hookState.lastOptimizeTime = new Date().toISOString();
144+
saveHookState(hookState);
145+
146+
process.stderr.write(
147+
`[GEPA] Auto-optimization triggered (${THRESHOLD} sessions accumulated)\n`
148+
);
149+
}
150+
151+
// Main
152+
try {
153+
const hookState = getHookState();
154+
155+
// Step 1: Save metrics
156+
saveSessionMetrics();
157+
158+
// Step 2: Update counter
159+
hookState.sessionsSinceLastOptimize++;
160+
hookState.totalSessions++;
161+
hookState.lastSessionTime = new Date().toISOString();
162+
163+
// Step 3: Check and trigger
164+
if (shouldOptimize(hookState)) {
165+
triggerOptimization(hookState);
166+
} else {
167+
saveHookState(hookState);
168+
}
169+
} catch (e) {
170+
// Never fail the session end — GEPA is advisory
171+
process.stderr.write(`[GEPA] Hook error (non-fatal): ${e.message}\n`);
172+
}

0 commit comments

Comments
 (0)