helixevo 0.2.24 → 0.2.26

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,7 +3,6 @@ import { spawn, type ChildProcess } from 'child_process'
3
3
 
4
4
  export const dynamic = 'force-dynamic'
5
5
 
6
- // Allowed commands — whitelist to prevent arbitrary execution
7
6
  const ALLOWED_COMMANDS: Record<string, { cmd: string; args: string[]; timeout: number }> = {
8
7
  'status': { cmd: 'helixevo', args: ['status'], timeout: 15000 },
9
8
  'health': { cmd: 'helixevo', args: ['health', '--verbose'], timeout: 120000 },
@@ -19,93 +18,103 @@ const ALLOWED_COMMANDS: Record<string, { cmd: string; args: string[]; timeout: n
19
18
  'report': { cmd: 'helixevo', args: ['report', '--days', '7'], timeout: 30000 },
20
19
  }
21
20
 
22
- // Track the currently running process so it can be stopped
23
21
  let activeProcess: ChildProcess | null = null
24
22
  let activeCommand: string | null = null
25
23
 
24
+ // POST — stream output via SSE
26
25
  export async function POST(request: Request) {
27
26
  const body = await request.json()
28
27
  const { command } = body as { command: string }
29
28
 
30
29
  const entry = ALLOWED_COMMANDS[command]
31
30
  if (!entry) {
32
- return NextResponse.json({
33
- success: false,
34
- error: `Unknown command: ${command}`,
35
- }, { status: 400 })
31
+ return NextResponse.json({ success: false, error: `Unknown command: ${command}` }, { status: 400 })
36
32
  }
37
33
 
38
- // Kill any existing process before starting a new one
34
+ // Kill any existing process
39
35
  if (activeProcess && !activeProcess.killed) {
40
36
  activeProcess.kill('SIGTERM')
41
37
  activeProcess = null
42
38
  activeCommand = null
43
39
  }
44
40
 
45
- return new Promise<Response>((resolve) => {
46
- let output = ''
47
- let timedOut = false
48
-
49
- const child = spawn(entry.cmd, entry.args, {
50
- env: { ...process.env },
51
- stdio: ['ignore', 'pipe', 'pipe'],
52
- })
53
-
54
- activeProcess = child
55
- activeCommand = command
56
-
57
- const timeout = setTimeout(() => {
58
- timedOut = true
59
- child.kill('SIGTERM')
60
- }, entry.timeout)
61
-
62
- child.stdout?.on('data', (data: Buffer) => {
63
- output += data.toString()
64
- })
65
-
66
- child.stderr?.on('data', (data: Buffer) => {
67
- output += data.toString()
68
- })
69
-
70
- child.on('close', (code, signal) => {
71
- clearTimeout(timeout)
72
- activeProcess = null
73
- activeCommand = null
74
-
75
- if (signal === 'SIGTERM' && !timedOut) {
76
- // Stopped by user
77
- resolve(NextResponse.json({
78
- success: false,
79
- stopped: true,
80
- output: output + '\n\n[Stopped by user]',
81
- }))
82
- } else if (timedOut) {
83
- resolve(NextResponse.json({
84
- success: false,
85
- output: output + '\n\n[Timed out]',
86
- }, { status: 500 }))
87
- } else {
88
- resolve(NextResponse.json({
89
- success: code === 0,
90
- command: `${entry.cmd} ${entry.args.join(' ')}`,
91
- output: output || 'No output',
92
- }, code === 0 ? {} : { status: 500 }))
41
+ const encoder = new TextEncoder()
42
+ let timedOut = false
43
+
44
+ const stream = new ReadableStream({
45
+ start(controller) {
46
+ const child = spawn(entry.cmd, entry.args, {
47
+ env: { ...process.env },
48
+ stdio: ['ignore', 'pipe', 'pipe'],
49
+ })
50
+
51
+ activeProcess = child
52
+ activeCommand = command
53
+
54
+ const timeout = setTimeout(() => {
55
+ timedOut = true
56
+ child.kill('SIGTERM')
57
+ }, entry.timeout)
58
+
59
+ const send = (event: string, data: string) => {
60
+ try {
61
+ controller.enqueue(encoder.encode(`event: ${event}\ndata: ${JSON.stringify(data)}\n\n`))
62
+ } catch {}
93
63
  }
94
- })
95
-
96
- child.on('error', (err) => {
97
- clearTimeout(timeout)
98
- activeProcess = null
99
- activeCommand = null
100
- resolve(NextResponse.json({
101
- success: false,
102
- output: err.message,
103
- }, { status: 500 }))
104
- })
64
+
65
+ child.stdout?.on('data', (chunk: Buffer) => {
66
+ send('output', chunk.toString())
67
+ })
68
+
69
+ child.stderr?.on('data', (chunk: Buffer) => {
70
+ send('output', chunk.toString())
71
+ })
72
+
73
+ child.on('close', (code, signal) => {
74
+ clearTimeout(timeout)
75
+ activeProcess = null
76
+ activeCommand = null
77
+
78
+ if (signal === 'SIGTERM' && !timedOut) {
79
+ send('done', JSON.stringify({ success: false, stopped: true }))
80
+ } else if (timedOut) {
81
+ send('done', JSON.stringify({ success: false, timedOut: true }))
82
+ } else {
83
+ send('done', JSON.stringify({ success: code === 0 }))
84
+ }
85
+
86
+ try { controller.close() } catch {}
87
+ })
88
+
89
+ child.on('error', (err) => {
90
+ clearTimeout(timeout)
91
+ activeProcess = null
92
+ activeCommand = null
93
+ send('output', `Error: ${err.message}`)
94
+ send('done', JSON.stringify({ success: false }))
95
+ try { controller.close() } catch {}
96
+ })
97
+ },
98
+
99
+ cancel() {
100
+ if (activeProcess && !activeProcess.killed) {
101
+ activeProcess.kill('SIGTERM')
102
+ activeProcess = null
103
+ activeCommand = null
104
+ }
105
+ },
106
+ })
107
+
108
+ return new Response(stream, {
109
+ headers: {
110
+ 'Content-Type': 'text/event-stream',
111
+ 'Cache-Control': 'no-cache',
112
+ 'Connection': 'keep-alive',
113
+ },
105
114
  })
106
115
  }
107
116
 
108
- // DELETE = stop the running command
117
+ // DELETE = stop
109
118
  export async function DELETE() {
110
119
  if (activeProcess && !activeProcess.killed) {
111
120
  const cmd = activeCommand
@@ -117,7 +126,7 @@ export async function DELETE() {
117
126
  return NextResponse.json({ stopped: false, message: 'No running command' })
118
127
  }
119
128
 
120
- // GET = check if something is running
129
+ // GET = check status
121
130
  export async function GET() {
122
131
  return NextResponse.json({
123
132
  running: activeProcess !== null && !activeProcess.killed,
@@ -59,12 +59,13 @@ const PIPELINE_STEPS = [
59
59
 
60
60
  export default function ResearchClient({ buffer, projects }: Props) {
61
61
  const [runState, setRunState] = useState<RunState>('idle')
62
- const [output, setOutput] = useState<string | null>(null)
62
+ const [output, setOutput] = useState<string>('')
63
63
  const abortRef = useRef<AbortController | null>(null)
64
+ const outputRef = useRef<HTMLPreElement | null>(null)
64
65
 
65
66
  const handleRun = async () => {
66
67
  setRunState('running')
67
- setOutput(null)
68
+ setOutput('')
68
69
  const controller = new AbortController()
69
70
  abortRef.current = controller
70
71
 
@@ -75,21 +76,58 @@ export default function ResearchClient({ buffer, projects }: Props) {
75
76
  body: JSON.stringify({ command: 'research' }),
76
77
  signal: controller.signal,
77
78
  })
78
- const data = await res.json()
79
- if (data.stopped) {
80
- setOutput(data.output ?? 'Stopped')
81
- setRunState('stopped')
82
- } else {
83
- setOutput(data.output ?? data.error ?? 'No output')
84
- setRunState(data.success ? 'success' : 'error')
79
+
80
+ if (!res.body) {
81
+ setOutput('No response body')
82
+ setRunState('error')
83
+ return
84
+ }
85
+
86
+ const reader = res.body.getReader()
87
+ const decoder = new TextDecoder()
88
+ let sseBuffer = ''
89
+
90
+ while (true) {
91
+ const { done, value } = await reader.read()
92
+ if (done) break
93
+
94
+ sseBuffer += decoder.decode(value, { stream: true })
95
+ const events = sseBuffer.split('\n\n')
96
+ sseBuffer = events.pop() ?? ''
97
+
98
+ for (const event of events) {
99
+ const lines = event.split('\n')
100
+ let eventType = ''
101
+ let eventData = ''
102
+ for (const line of lines) {
103
+ if (line.startsWith('event: ')) eventType = line.slice(7)
104
+ if (line.startsWith('data: ')) eventData = line.slice(6)
105
+ }
106
+ if (eventType === 'output' && eventData) {
107
+ try {
108
+ const text = JSON.parse(eventData) as string
109
+ setOutput(prev => prev + text)
110
+ setTimeout(() => {
111
+ if (outputRef.current) outputRef.current.scrollTop = outputRef.current.scrollHeight
112
+ }, 10)
113
+ } catch {}
114
+ }
115
+ if (eventType === 'done' && eventData) {
116
+ try {
117
+ const result = JSON.parse(JSON.parse(eventData) as string) as { success: boolean; stopped?: boolean }
118
+ setRunState(result.stopped ? 'stopped' : result.success ? 'success' : 'error')
119
+ } catch {}
120
+ }
121
+ }
85
122
  }
123
+ if (runState === 'running') setRunState('success')
86
124
  } catch (err: unknown) {
87
125
  if (err instanceof Error && err.name === 'AbortError') {
88
126
  try { await fetch('/api/run', { method: 'DELETE' }) } catch {}
89
- setOutput('Stopped by user')
127
+ setOutput(prev => prev + '\n\n[Stopped by user]')
90
128
  setRunState('stopped')
91
129
  } else {
92
- setOutput('Network error')
130
+ setOutput(prev => prev || 'Network error')
93
131
  setRunState('error')
94
132
  }
95
133
  } finally {
@@ -220,29 +258,29 @@ export default function ResearchClient({ buffer, projects }: Props) {
220
258
  </div>
221
259
 
222
260
  {/* Output panel */}
223
- {runState !== 'idle' && output && (
261
+ {runState !== 'idle' && (
224
262
  <div className="card" style={{ marginBottom: 24, overflow: 'hidden' }}>
225
263
  <div style={{
226
264
  padding: '10px 16px',
227
- background: runState === 'success' ? 'var(--green-light)' : runState === 'error' ? 'var(--red-light)' : 'var(--bg-section)',
265
+ background: runState === 'success' ? 'var(--green-light)' : runState === 'error' ? 'var(--red-light)' : runState === 'stopped' ? 'var(--yellow-light)' : 'var(--bg-section)',
228
266
  borderBottom: '1px solid var(--border)',
229
267
  fontSize: 12, fontWeight: 600,
230
- color: runState === 'success' ? 'var(--green)' : runState === 'error' ? 'var(--red)' : 'var(--text-secondary)',
268
+ color: runState === 'success' ? 'var(--green)' : runState === 'error' ? 'var(--red)' : runState === 'stopped' ? 'var(--yellow)' : 'var(--text-secondary)',
231
269
  }}>
232
270
  {runState === 'running' && '● Running research pipeline...'}
233
271
  {runState === 'success' && '✓ Research completed'}
234
272
  {runState === 'error' && '✗ Research failed'}
235
273
  {runState === 'stopped' && '■ Research stopped'}
236
274
  </div>
237
- <pre style={{
275
+ <pre ref={outputRef} style={{
238
276
  padding: '14px 16px', margin: 0,
239
277
  fontSize: 11, lineHeight: 1.55,
240
278
  fontFamily: 'var(--font-mono)',
241
279
  color: 'var(--text-secondary)',
242
- maxHeight: 300, overflow: 'auto',
280
+ maxHeight: 400, overflow: 'auto',
243
281
  whiteSpace: 'pre-wrap', wordBreak: 'break-word',
244
282
  }}>
245
- {output}
283
+ {output || 'Starting research pipeline...'}
246
284
  </pre>
247
285
  </div>
248
286
  )}
@@ -25,14 +25,15 @@ type RunState = 'idle' | 'running' | 'success' | 'error' | 'stopped'
25
25
  export function QuickActions({ actions, flowDiagram: FlowDiagram }: QuickActionsProps) {
26
26
  const [running, setRunning] = useState<string | null>(null)
27
27
  const [state, setState] = useState<RunState>('idle')
28
- const [output, setOutput] = useState<string | null>(null)
28
+ const [output, setOutput] = useState<string>('')
29
29
  const abortRef = useRef<AbortController | null>(null)
30
+ const outputRef = useRef<HTMLPreElement | null>(null)
30
31
 
31
32
  const handleRun = async (action: Action) => {
32
33
  if (action.disabled) return
33
34
  setRunning(action.id)
34
35
  setState('running')
35
- setOutput(null)
36
+ setOutput('')
36
37
 
37
38
  const controller = new AbortController()
38
39
  abortRef.current = controller
@@ -44,21 +45,76 @@ export function QuickActions({ actions, flowDiagram: FlowDiagram }: QuickActions
44
45
  body: JSON.stringify({ command: action.command }),
45
46
  signal: controller.signal,
46
47
  })
47
- const data = await res.json()
48
- if (data.stopped) {
49
- setOutput(data.output ?? 'Stopped')
50
- setState('stopped')
51
- } else {
52
- setOutput(data.output ?? data.error ?? 'No output')
53
- setState(data.success ? 'success' : 'error')
48
+
49
+ if (!res.body) {
50
+ setOutput('No response body')
51
+ setState('error')
52
+ return
53
+ }
54
+
55
+ const reader = res.body.getReader()
56
+ const decoder = new TextDecoder()
57
+ let buffer = ''
58
+
59
+ while (true) {
60
+ const { done, value } = await reader.read()
61
+ if (done) break
62
+
63
+ buffer += decoder.decode(value, { stream: true })
64
+
65
+ // Parse SSE events from buffer
66
+ const events = buffer.split('\n\n')
67
+ buffer = events.pop() ?? '' // keep incomplete event
68
+
69
+ for (const event of events) {
70
+ const lines = event.split('\n')
71
+ let eventType = ''
72
+ let eventData = ''
73
+
74
+ for (const line of lines) {
75
+ if (line.startsWith('event: ')) eventType = line.slice(7)
76
+ if (line.startsWith('data: ')) eventData = line.slice(6)
77
+ }
78
+
79
+ if (eventType === 'output' && eventData) {
80
+ try {
81
+ const text = JSON.parse(eventData) as string
82
+ setOutput(prev => prev + text)
83
+ // Auto-scroll to bottom
84
+ setTimeout(() => {
85
+ if (outputRef.current) {
86
+ outputRef.current.scrollTop = outputRef.current.scrollHeight
87
+ }
88
+ }, 10)
89
+ } catch {}
90
+ }
91
+
92
+ if (eventType === 'done' && eventData) {
93
+ try {
94
+ const result = JSON.parse(JSON.parse(eventData) as string) as { success: boolean; stopped?: boolean; timedOut?: boolean }
95
+ if (result.stopped) {
96
+ setOutput(prev => prev + '\n\n[Stopped by user]')
97
+ setState('stopped')
98
+ } else if (result.timedOut) {
99
+ setOutput(prev => prev + '\n\n[Timed out]')
100
+ setState('error')
101
+ } else {
102
+ setState(result.success ? 'success' : 'error')
103
+ }
104
+ } catch {}
105
+ }
106
+ }
54
107
  }
108
+
109
+ // If we reach here without a 'done' event, mark as complete
110
+ if (state === 'running') setState('success')
55
111
  } catch (err: unknown) {
56
112
  if (err instanceof Error && err.name === 'AbortError') {
57
113
  try { await fetch('/api/run', { method: 'DELETE' }) } catch {}
58
- setOutput('Stopped by user')
114
+ setOutput(prev => prev + '\n\n[Stopped by user]')
59
115
  setState('stopped')
60
116
  } else {
61
- setOutput('Network error')
117
+ setOutput(prev => prev || 'Network error')
62
118
  setState('error')
63
119
  }
64
120
  } finally {
@@ -227,7 +283,7 @@ export function QuickActions({ actions, flowDiagram: FlowDiagram }: QuickActions
227
283
  </div>
228
284
  </div>
229
285
 
230
- <pre style={{
286
+ <pre ref={outputRef} style={{
231
287
  padding: '12px 14px', margin: 0,
232
288
  fontSize: 11, lineHeight: 1.5,
233
289
  fontFamily: 'var(--font-mono)',
@@ -235,7 +291,7 @@ export function QuickActions({ actions, flowDiagram: FlowDiagram }: QuickActions
235
291
  maxHeight: 400, overflow: 'auto',
236
292
  whiteSpace: 'pre-wrap', wordBreak: 'break-word',
237
293
  }}>
238
- {output ?? 'Waiting for output...'}
294
+ {output || (state === 'running' ? 'Starting...' : 'No output')}
239
295
  </pre>
240
296
  </div>
241
297
  )}
package/dist/cli.js CHANGED
@@ -9808,7 +9808,7 @@ init_llm();
9808
9808
  import { join as join7 } from "node:path";
9809
9809
 
9810
9810
  // src/prompts/cluster.ts
9811
- function buildClusterPrompt(failures, skills) {
9811
+ function buildClusterPrompt(failures, skills, graph) {
9812
9812
  const failuresJson = failures.map((f) => ({
9813
9813
  id: f.id,
9814
9814
  project: f.project,
@@ -9819,6 +9819,29 @@ function buildClusterPrompt(failures, skills) {
9819
9819
  }));
9820
9820
  const skillsList = skills.map((s) => `- ${s.slug}: ${s.meta.description}`).join(`
9821
9821
  `);
9822
+ let graphSection = "";
9823
+ if (graph && graph.edges.length > 0) {
9824
+ const edgesList = graph.edges.map((e) => `- ${e.from} --[${e.type}]--> ${e.to} (strength: ${e.strength})`).join(`
9825
+ `);
9826
+ const clustersList = graph.clusters.map((c) => `- ${c.name}: [${c.skills.join(", ")}] (cohesion: ${c.cohesion})`).join(`
9827
+ `);
9828
+ graphSection = `
9829
+ ## Skill Network Graph
9830
+ The following relationships exist between skills. Use this to identify when failures might affect multiple related skills.
9831
+
9832
+ ### Relationships
9833
+ ${edgesList}
9834
+
9835
+ ### Clusters
9836
+ ${clustersList}
9837
+
9838
+ When clustering failures, consider:
9839
+ - Failures in skills that are in the same cluster likely share root causes
9840
+ - "inherits" edges mean a child skill should follow parent skill rules
9841
+ - "enhances" edges mean skills complement each other
9842
+ - "conflicts" edges mean skills may have contradictory rules
9843
+ `;
9844
+ }
9822
9845
  return `You are a failure pattern analyst.
9823
9846
 
9824
9847
  ## Task
@@ -9829,7 +9852,7 @@ ${JSON.stringify(failuresJson, null, 2)}
9829
9852
 
9830
9853
  ## Current Skills
9831
9854
  ${skillsList}
9832
-
9855
+ ${graphSection}
9833
9856
  ## Output Requirements
9834
9857
  Return a JSON object with this exact structure:
9835
9858
  {
@@ -9846,7 +9869,8 @@ Return a JSON object with this exact structure:
9846
9869
  - Do not create clusters with only 1 record (unless truly unique)
9847
9870
  - Maximum 8 clusters
9848
9871
  - Each record belongs to exactly one cluster
9849
- - Cluster names should be specific (e.g., "WCAG color contrast violations", not "UI issues")`;
9872
+ - Cluster names should be specific (e.g., "WCAG color contrast violations", not "UI issues")
9873
+ - If skill graph is available, use relationships to identify which skills should be targeted together`;
9850
9874
  }
9851
9875
  function parseClusterOutput(raw) {
9852
9876
  const data = raw;
@@ -9857,7 +9881,7 @@ function parseClusterOutput(raw) {
9857
9881
  }
9858
9882
 
9859
9883
  // src/prompts/proposer.ts
9860
- function buildProposerPrompt(clusterType, failures, relevantSkill, relatedProposals) {
9884
+ function buildProposerPrompt(clusterType, failures, relevantSkill, relatedProposals, graphContext) {
9861
9885
  const failureDetails = failures.map((f) => `- [${f.id}] User asked: "${f.userRequest}" → Agent did: "${f.agentAction}" → User corrected: "${f.correction}"`).join(`
9862
9886
  `);
9863
9887
  const skillContent = relevantSkill ? `### ${relevantSkill.slug}
@@ -9866,6 +9890,37 @@ ${relevantSkill.content}
9866
9890
  \`\`\`` : "No existing skill covers this area.";
9867
9891
  const historySection = relatedProposals.length > 0 ? relatedProposals.map((p) => `- [${p.id}] ${p.description} → ${p.outcome} (${p.outcomeReason})`).join(`
9868
9892
  `) : "No prior proposals for this area.";
9893
+ let graphSection = "";
9894
+ if (graphContext && graphContext.relatedSkills.length > 0) {
9895
+ const relatedEdges = relevantSkill ? graphContext.edges.filter((e) => e.from === relevantSkill.slug || e.to === relevantSkill.slug).map((e) => {
9896
+ const other = e.from === relevantSkill.slug ? e.to : e.from;
9897
+ const direction = e.from === relevantSkill.slug ? "outgoing" : "incoming";
9898
+ return ` - ${e.type} (${direction}): ${other} (strength: ${e.strength})`;
9899
+ }).join(`
9900
+ `) : "";
9901
+ const relatedSkillSummaries = graphContext.relatedSkills.map((s) => {
9902
+ const preview = s.content.slice(0, 200).replace(/\n/g, " ");
9903
+ return ` - ${s.slug} (score: ${s.meta.score ?? "N/A"}, gen: ${s.meta.generation ?? 0}): ${preview}...`;
9904
+ }).join(`
9905
+ `);
9906
+ const clusterInfo = graphContext.cluster ? `This skill belongs to the "${graphContext.cluster.name}" cluster (cohesion: ${graphContext.cluster.cohesion}) with: ${graphContext.cluster.skills.join(", ")}` : "";
9907
+ graphSection = `
9908
+ ## Skill Network Context
9909
+ ${relevantSkill?.slug ?? "New skill"} has the following relationships in the skill graph:
9910
+ ${relatedEdges || " No edges yet"}
9911
+
9912
+ ${clusterInfo}
9913
+
9914
+ ### Related Skills (summaries)
9915
+ ${relatedSkillSummaries}
9916
+
9917
+ ## Graph-Aware Guidelines
9918
+ - If this skill "inherits" from a parent, ensure your changes are consistent with the parent skill's rules
9919
+ - If this skill "enhances" another, check that your changes don't conflict with the enhanced skill
9920
+ - If related skills in the same cluster share similar rules, consider whether the fix should be applied at a more abstract level
9921
+ - If the correction addresses a pattern common across multiple related skills, note this in your justification — it may warrant propagation
9922
+ `;
9923
+ }
9869
9924
  return `You are a skill evolution expert. Analyze failures and propose a concrete skill improvement.
9870
9925
 
9871
9926
  ## Failure Cluster: "${clusterType}"
@@ -9876,12 +9931,13 @@ ${skillContent}
9876
9931
 
9877
9932
  ## Prior Proposals (avoid repeating rejected approaches)
9878
9933
  ${historySection}
9879
-
9934
+ ${graphSection}
9880
9935
  ## Instructions
9881
9936
  1. Analyze the root cause shared by these failures
9882
9937
  2. Determine: edit existing skill or create new one?
9883
- 3. Generate the exact SKILL.md content (complete file if create, or a unified diff if edit)
9938
+ 3. Generate the exact SKILL.md content (complete file with frontmatter)
9884
9939
  4. Explain why this is better than any rejected prior proposals
9940
+ 5. If graph context is available, consider whether the improvement should be structured hierarchically (core principles at top, specific rules below)
9885
9941
 
9886
9942
  ## Output
9887
9943
  Return JSON:
@@ -9891,14 +9947,19 @@ Return JSON:
9891
9947
  "proposedSkillMd": "full SKILL.md content with frontmatter",
9892
9948
  "diff": "human-readable description of changes",
9893
9949
  "justification": "why this addresses the root cause",
9894
- "relatedIterations": ["prop_id"] // related prior proposal IDs
9950
+ "relatedIterations": ["prop_id"],
9951
+ "propagationSuggestions": ["skill-slug"] // skills that might benefit from similar changes (optional)
9895
9952
  }
9896
9953
 
9897
9954
  ## Constraints
9898
9955
  - YAGNI: every rule must map to a specific failure
9899
9956
  - One skill at a time
9900
9957
  - Preserve all existing effective rules when editing
9901
- - Be specific — no vague "consider doing X" rules`;
9958
+ - Be specific — no vague "consider doing X" rules
9959
+ - When graph context shows skill relationships, organize content hierarchically:
9960
+ 1. Inherited/shared principles (from parent skills)
9961
+ 2. Core rules (this skill's domain)
9962
+ 3. Specific patterns (from failures)`;
9902
9963
  }
9903
9964
 
9904
9965
  // src/prompts/replay.ts
@@ -10398,9 +10459,17 @@ async function evolveCommand(options) {
10398
10459
  console.log(` Processing ${batch.length} failures (${failures.length} total unresolved)
10399
10460
  `);
10400
10461
  const skills = loadAllGeneralSkills();
10401
- if (verbose)
10402
- console.log(` Skills loaded: ${skills.map((s) => s.slug).join(", ")}
10403
- `);
10462
+ const graph = loadSkillGraph();
10463
+ const hasGraph = graph.nodes.length > 0 && graph.edges.length > 0;
10464
+ if (verbose) {
10465
+ console.log(` Skills loaded: ${skills.map((s) => s.slug).join(", ")}`);
10466
+ if (hasGraph) {
10467
+ console.log(` Graph: ${graph.nodes.length} nodes, ${graph.edges.length} edges, ${graph.clusters.length} clusters`);
10468
+ } else {
10469
+ console.log(` Graph: not built yet (run "helixevo graph --rebuild" for graph-aware evolution)`);
10470
+ }
10471
+ console.log();
10472
+ }
10404
10473
  const stagnation = getStagnationCount();
10405
10474
  if (stagnation >= config.evolution.stopAfterNoImprovement) {
10406
10475
  console.log(` ⚠ Stagnation detected (${stagnation} rounds with no improvement).`);
@@ -10408,7 +10477,7 @@ async function evolveCommand(options) {
10408
10477
  `);
10409
10478
  }
10410
10479
  console.log(" Step 1: Clustering failures...");
10411
- const clusterPrompt = buildClusterPrompt(batch, skills);
10480
+ const clusterPrompt = buildClusterPrompt(batch, skills, hasGraph ? graph : undefined);
10412
10481
  const clusterResult = await chatJson({ prompt: clusterPrompt });
10413
10482
  const clusters = parseClusterOutput(clusterResult);
10414
10483
  console.log(` → ${clusters.clusters.length} cluster(s) found
@@ -10436,7 +10505,10 @@ async function evolveCommand(options) {
10436
10505
  });
10437
10506
  const related = findRelatedProposals(relevantSkill?.slug ?? "", cluster.type);
10438
10507
  console.log(" Step 3: Generating proposal...");
10439
- const proposerPrompt = buildProposerPrompt(cluster.type, clusterFailures, relevantSkill, related);
10508
+ const relatedSkillSlugs = hasGraph && relevantSkill ? graph.edges.filter((e) => e.from === relevantSkill.slug || e.to === relevantSkill.slug).map((e) => e.from === relevantSkill.slug ? e.to : e.from) : [];
10509
+ const relatedSkillContents = relatedSkillSlugs.map((slug) => skills.find((s) => s.slug === slug)).filter((s) => s !== null);
10510
+ const skillCluster = hasGraph ? graph.clusters.find((c) => c.skills.includes(relevantSkill?.slug ?? "")) : undefined;
10511
+ const proposerPrompt = buildProposerPrompt(cluster.type, clusterFailures, relevantSkill, related, hasGraph ? { relatedSkills: relatedSkillContents, edges: graph.edges, cluster: skillCluster } : undefined);
10440
10512
  const proposalOutput = await chatJson({ prompt: proposerPrompt });
10441
10513
  if (verbose) {
10442
10514
  console.log(` Action: ${proposalOutput.action}`);
@@ -10554,8 +10626,15 @@ async function evolveCommand(options) {
10554
10626
  if (evicted)
10555
10627
  console.log(` Evicted: ${evicted.id} (score: ${evicted.score.toFixed(2)})`);
10556
10628
  }
10629
+ if (proposalOutput.propagationSuggestions?.length) {
10630
+ console.log(` ↗ Propagation suggested to: ${proposalOutput.propagationSuggestions.join(", ")}`);
10631
+ console.log(" (Run evolve again or generalize to apply these)");
10632
+ }
10557
10633
  } else if (finalAccepted && dryRun) {
10558
10634
  console.log(` [DRY RUN] Would apply: ${proposal.targetSkill}`);
10635
+ if (proposalOutput.propagationSuggestions?.length) {
10636
+ console.log(` ↗ Would propagate to: ${proposalOutput.propagationSuggestions.join(", ")}`);
10637
+ }
10559
10638
  }
10560
10639
  proposals.push(proposal);
10561
10640
  proposalCount++;
@@ -10569,7 +10648,25 @@ async function evolveCommand(options) {
10569
10648
  proposals
10570
10649
  };
10571
10650
  addIteration(iteration);
10572
- console.log(" Step 7: Network health assessment...");
10651
+ if (hasGraph) {
10652
+ const accepted2 = proposals.filter((p) => p.outcome === "accepted");
10653
+ if (accepted2.length > 0) {
10654
+ console.log(" Step 7: Updating skill graph...");
10655
+ for (const p of accepted2) {
10656
+ const node = graph.nodes.find((n) => n.id === p.targetSkill);
10657
+ if (node) {
10658
+ node.generation = generation;
10659
+ node.score = (p.judges.taskCompletion.score + p.judges.correctionAlignment.score + p.judges.sideEffectCheck.score) / 30;
10660
+ node.lastEvolved = new Date().toISOString();
10661
+ }
10662
+ }
10663
+ graph.updated = new Date().toISOString();
10664
+ saveSkillGraph(graph);
10665
+ console.log(` ✓ Graph updated (${accepted2.length} node(s) evolved)
10666
+ `);
10667
+ }
10668
+ }
10669
+ console.log(" Step 8: Network health assessment...");
10573
10670
  const health = await assessNetworkHealth(verbose);
10574
10671
  if (verbose)
10575
10672
  printHealthReport(health);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "helixevo",
3
- "version": "0.2.24",
3
+ "version": "0.2.26",
4
4
  "description": "Self-evolving skill ecosystem for AI agents. Skills and projects co-evolve through multi-judge evaluation and a Pareto frontier.",
5
5
  "type": "module",
6
6
  "bin": {