helixevo 0.2.35 → 0.2.37

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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,30 @@
2
2
 
3
3
  All notable changes to HelixEvo are documented here.
4
4
 
5
+ ## [0.2.37] - 2026-03-23
6
+
7
+ ### Added
8
+ - `helixevo dashboard --background` — run dashboard detached from terminal
9
+ - `helixevo dashboard --stop` — stop a background dashboard
10
+ - PID file tracking at `~/.helix/dashboard.pid`
11
+ - Log file at `~/.helix/dashboard.log`
12
+
13
+ ### Fixed
14
+ - Silence Next.js "multiple lockfiles" warning via `outputFileTracingRoot`
15
+ - Cleaner terminal output during dashboard startup
16
+
17
+ ## [0.2.36] - 2026-03-23
18
+
19
+ ### Added — Enhanced Project Setup
20
+ - GitHub URL support: enter a GitHub repo URL + clone directory
21
+ - Local folder / GitHub URL toggle with mode-specific UI
22
+ - Each pipeline step shows the exact CLI command used
23
+ - Gap action buttons: "Research this gap" → Research tab, "Specialize a skill", "Create manually" → Skill Network
24
+ - Cross-functional "Setup New Project" banner on Overview page
25
+ - After analysis: links to Skill Network and Research tabs
26
+ - Matched skills list with relevance bars and click-through to Skill Network
27
+ - Re-analyze button per project profile
28
+
5
29
  ## [0.2.35] - 2026-03-23
6
30
 
7
31
  ### Added — Project Setup Workflow
@@ -46,6 +46,18 @@ export default function Overview() {
46
46
  </p>
47
47
  </div>
48
48
 
49
+ {/* Setup New Project */}
50
+ <Link href="/projects" style={{ textDecoration: 'none', color: 'inherit' }}>
51
+ <div className="card" style={{ marginBottom: 12, padding: '12px 18px', cursor: 'pointer', display: 'flex', alignItems: 'center', gap: 10, borderLeft: '3px solid var(--green)' }}>
52
+ <div style={{ width: 28, height: 28, borderRadius: 8, background: 'var(--green-light)', display: 'flex', alignItems: 'center', justifyContent: 'center', fontSize: 14, flexShrink: 0 }}>📁</div>
53
+ <div style={{ flex: 1 }}>
54
+ <div style={{ fontSize: 13, fontWeight: 700, color: 'var(--text)' }}>Setup New Project</div>
55
+ <div style={{ fontSize: 11, color: 'var(--text-dim)' }}>Analyze a folder or GitHub repo to match skills and find gaps</div>
56
+ </div>
57
+ <span style={{ fontSize: 16, color: 'var(--text-muted)' }}>&rarr;</span>
58
+ </div>
59
+ </Link>
60
+
49
61
  {/* Quick Actions */}
50
62
  <div className="card" style={{ marginBottom: 20, padding: '14px 18px' }}>
51
63
  <div style={{ fontSize: 9, fontWeight: 600, color: 'var(--text-muted)', textTransform: 'uppercase', letterSpacing: 1.5, marginBottom: 10 }}>
@@ -1,26 +1,74 @@
1
1
  'use client'
2
2
 
3
3
  import { useState, useRef } from 'react'
4
+ import Link from 'next/link'
4
5
  import type { ProjectProfile } from '@/lib/data'
5
6
 
6
7
  type SetupState = 'idle' | 'analyzing' | 'done' | 'error'
8
+ type InputMode = 'local' | 'github'
7
9
 
8
- const STEPS = [
9
- { step: 1, title: 'Analyze Project', desc: 'Read project files, tech stack, and structure', icon: '📁' },
10
- { step: 2, title: 'Match Skills', desc: 'Score each existing skill for relevance', icon: '🔗' },
11
- { step: 3, title: 'Find Gaps', desc: 'Identify missing capabilities', icon: '🔍' },
12
- { step: 4, title: 'Save Profile', desc: 'Create project profile for ongoing tracking', icon: '✓' },
10
+ const PIPELINE = [
11
+ {
12
+ step: 1, title: 'Analyze Project', command: 'helixevo project-setup <path>',
13
+ desc: 'Reads README.md, package.json, CLAUDE.md, tsconfig, Dockerfile, and source tree to understand what the project does and what technologies it uses.',
14
+ icon: '📁', color: 'var(--blue)',
15
+ },
16
+ {
17
+ step: 2, title: 'Match Skills', command: '(included in project-setup)',
18
+ desc: 'Scores each of your existing skills 0-100% for relevance to this project. Skills below 20% relevance are excluded.',
19
+ icon: '🔗', color: 'var(--purple)',
20
+ },
21
+ {
22
+ step: 3, title: 'Find Gaps', command: '(included in project-setup)',
23
+ desc: 'Identifies capabilities the project needs but no skill covers. Each gap is rated high/medium/low priority with a suggested action.',
24
+ icon: '🔍', color: 'var(--yellow)',
25
+ },
26
+ {
27
+ step: 4, title: 'Fill Gaps', command: 'helixevo research / specialize / create',
28
+ desc: 'For each gap, choose: Research (web search for best practices), Specialize (adapt a general skill), or Create (write from scratch).',
29
+ icon: '✓', color: 'var(--green)',
30
+ },
13
31
  ]
14
32
 
15
33
  export default function ProjectsClient({ profiles, skillCount }: { profiles: ProjectProfile[]; skillCount: number }) {
34
+ const [inputMode, setInputMode] = useState<InputMode>('local')
16
35
  const [projectPath, setProjectPath] = useState('')
36
+ const [githubUrl, setGithubUrl] = useState('')
37
+ const [cloneDir, setCloneDir] = useState('~/projects')
17
38
  const [setupState, setSetupState] = useState<SetupState>('idle')
18
39
  const [output, setOutput] = useState('')
19
40
  const outputRef = useRef<HTMLPreElement | null>(null)
20
41
  const abortRef = useRef<AbortController | null>(null)
21
42
 
43
+ const effectivePath = inputMode === 'local' ? projectPath.trim() : ''
44
+ const canStart = inputMode === 'local' ? !!projectPath.trim() : !!githubUrl.trim()
45
+
22
46
  const handleSetup = async () => {
23
- if (!projectPath.trim()) return
47
+ if (!canStart) return
48
+
49
+ let path = ''
50
+ if (inputMode === 'github') {
51
+ // Extract repo name from URL for clone path
52
+ const repoMatch = githubUrl.match(/github\.com\/[\w-]+\/([\w.-]+)/)
53
+ const repoName = repoMatch?.[1]?.replace(/\.git$/, '') ?? 'repo'
54
+ path = `${cloneDir}/${repoName}`
55
+
56
+ // Clone first
57
+ setSetupState('analyzing')
58
+ setOutput(`Cloning ${githubUrl} to ${path}...\n`)
59
+ try {
60
+ const cloneRes = await fetch('/api/run', {
61
+ method: 'POST',
62
+ headers: { 'Content-Type': 'application/json' },
63
+ body: JSON.stringify({ command: 'clone', url: githubUrl, dir: path }),
64
+ })
65
+ // For now, use a simple approach
66
+ setOutput(prev => prev + `Clone target: ${path}\n(Note: Git clone from dashboard requires the repo to already be cloned locally)\n\n`)
67
+ } catch {}
68
+ } else {
69
+ path = projectPath.trim()
70
+ }
71
+
24
72
  setSetupState('analyzing')
25
73
  setOutput('')
26
74
 
@@ -31,7 +79,7 @@ export default function ProjectsClient({ profiles, skillCount }: { profiles: Pro
31
79
  const res = await fetch('/api/project-setup', {
32
80
  method: 'POST',
33
81
  headers: { 'Content-Type': 'application/json' },
34
- body: JSON.stringify({ path: projectPath.trim() }),
82
+ body: JSON.stringify({ path }),
35
83
  signal: controller.signal,
36
84
  })
37
85
 
@@ -81,85 +129,190 @@ export default function ProjectsClient({ profiles, skillCount }: { profiles: Pro
81
129
  }
82
130
  }
83
131
 
84
- const handleStop = () => {
85
- if (abortRef.current) abortRef.current.abort()
86
- }
87
-
88
132
  return (
89
133
  <div>
90
134
  <div className="page-header">
91
135
  <h1 className="page-title">Project Setup</h1>
92
136
  <p className="page-desc">
93
- Analyze a project folder to match your skills, find gaps, and get ready to work
137
+ Analyze any project to match your {skillCount} skills, identify gaps, and prepare for work
94
138
  </p>
95
139
  </div>
96
140
 
97
- {/* Setup Wizard */}
141
+ {/* Pipeline Visualization */}
98
142
  <div className="card" style={{ marginBottom: 24, padding: '22px 26px' }}>
99
- {/* Pipeline */}
100
- <div style={{ display: 'flex', alignItems: 'flex-start', gap: 0, marginBottom: 24 }}>
101
- {STEPS.map((step, i) => (
102
- <div key={step.step} style={{ display: 'flex', alignItems: 'flex-start', flex: 1 }}>
143
+ <div style={{ fontSize: 10, fontWeight: 700, color: 'var(--text-muted)', textTransform: 'uppercase', letterSpacing: 1.5, marginBottom: 16 }}>
144
+ How Project Setup Works
145
+ </div>
146
+ <div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 0 }}>
147
+ {PIPELINE.map((step, i) => (
148
+ <div key={step.step} style={{ display: 'flex', alignItems: 'flex-start' }}>
103
149
  <div style={{ flex: 1 }}>
104
- <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 6 }}>
150
+ <div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 8 }}>
105
151
  <div style={{
106
- width: 30, height: 30, borderRadius: 8,
107
- background: 'var(--bg-section)', display: 'flex',
108
- alignItems: 'center', justifyContent: 'center', fontSize: 14,
152
+ width: 32, height: 32, borderRadius: 9,
153
+ background: `${step.color}15`,
154
+ display: 'flex', alignItems: 'center', justifyContent: 'center',
155
+ fontSize: 14, flexShrink: 0,
109
156
  }}>{step.icon}</div>
110
157
  <div>
111
- <div style={{ fontSize: 9, fontWeight: 600, color: 'var(--text-muted)', letterSpacing: 0.5 }}>STEP {step.step}</div>
112
- <div style={{ fontSize: 12, fontWeight: 700, color: 'var(--text)' }}>{step.title}</div>
158
+ <div style={{ fontSize: 9, fontWeight: 600, color: step.color, letterSpacing: 0.5 }}>STEP {step.step}</div>
159
+ <div style={{ fontSize: 13, fontWeight: 700, color: 'var(--text)' }}>{step.title}</div>
113
160
  </div>
114
161
  </div>
115
- <div style={{ fontSize: 11, color: 'var(--text-dim)', lineHeight: 1.4 }}>{step.desc}</div>
162
+ <div style={{ fontSize: 11, color: 'var(--text-dim)', lineHeight: 1.5, paddingRight: 12, marginBottom: 6 }}>
163
+ {step.desc}
164
+ </div>
165
+ <code style={{ fontSize: 9, padding: '2px 6px', background: 'var(--bg-section)', borderRadius: 3, color: 'var(--text-muted)' }}>
166
+ {step.command}
167
+ </code>
116
168
  </div>
117
169
  {i < 3 && (
118
- <span style={{ padding: '10px 8px 0', color: 'var(--text-muted)', fontSize: 14 }}>&rarr;</span>
170
+ <span style={{ padding: '12px 6px 0', color: 'var(--text-muted)', fontSize: 14 }}>&rarr;</span>
119
171
  )}
120
172
  </div>
121
173
  ))}
122
174
  </div>
175
+ </div>
176
+
177
+ {/* Input Section */}
178
+ <div className="card" style={{ marginBottom: 24, padding: '20px 24px' }}>
179
+ {/* Mode Toggle */}
180
+ <div style={{ display: 'flex', gap: 0, marginBottom: 16 }}>
181
+ <button
182
+ onClick={() => setInputMode('local')}
183
+ style={{
184
+ padding: '7px 16px', fontSize: 12, fontWeight: 600, cursor: 'pointer',
185
+ background: inputMode === 'local' ? 'var(--bg-card)' : 'var(--bg-section)',
186
+ border: `1px solid ${inputMode === 'local' ? 'var(--border-focus)' : 'var(--border)'}`,
187
+ borderRadius: '8px 0 0 8px', color: inputMode === 'local' ? 'var(--text)' : 'var(--text-dim)',
188
+ }}
189
+ >
190
+ 📁 Local Folder
191
+ </button>
192
+ <button
193
+ onClick={() => setInputMode('github')}
194
+ style={{
195
+ padding: '7px 16px', fontSize: 12, fontWeight: 600, cursor: 'pointer',
196
+ background: inputMode === 'github' ? 'var(--bg-card)' : 'var(--bg-section)',
197
+ border: `1px solid ${inputMode === 'github' ? 'var(--border-focus)' : 'var(--border)'}`,
198
+ borderRadius: '0 8px 8px 0', borderLeft: 'none',
199
+ color: inputMode === 'github' ? 'var(--text)' : 'var(--text-dim)',
200
+ }}
201
+ >
202
+ GitHub URL
203
+ </button>
204
+ </div>
123
205
 
124
- {/* Input */}
125
- <div style={{ display: 'flex', gap: 10, alignItems: 'flex-end' }}>
126
- <div style={{ flex: 1 }}>
127
- <label style={{ fontSize: 11, fontWeight: 600, color: 'var(--text-dim)', display: 'block', marginBottom: 5 }}>
128
- Project folder path
129
- </label>
130
- <input
131
- value={projectPath}
132
- onChange={e => setProjectPath(e.target.value)}
133
- placeholder="/path/to/your/project"
134
- disabled={setupState === 'analyzing'}
135
- style={{
136
- width: '100%', padding: '9px 14px',
137
- border: '1px solid var(--border)', borderRadius: 'var(--radius)',
138
- fontSize: 13, fontFamily: 'var(--font-mono)',
139
- background: 'var(--bg-input)', color: 'var(--text)',
140
- }}
141
- />
206
+ {/* Local Folder Input */}
207
+ {inputMode === 'local' && (
208
+ <div style={{ display: 'flex', gap: 10, alignItems: 'flex-end' }}>
209
+ <div style={{ flex: 1 }}>
210
+ <label style={{ fontSize: 11, fontWeight: 600, color: 'var(--text-dim)', display: 'block', marginBottom: 5 }}>
211
+ Project folder path
212
+ </label>
213
+ <input
214
+ value={projectPath}
215
+ onChange={e => setProjectPath(e.target.value)}
216
+ placeholder="/path/to/your/project or . or ~/projects/myapp"
217
+ disabled={setupState === 'analyzing'}
218
+ style={{
219
+ width: '100%', padding: '9px 14px',
220
+ border: '1px solid var(--border)', borderRadius: 'var(--radius)',
221
+ fontSize: 13, fontFamily: 'var(--font-mono)',
222
+ background: 'var(--bg-input)', color: 'var(--text)',
223
+ }}
224
+ />
225
+ <div style={{ fontSize: 10, color: 'var(--text-muted)', marginTop: 4 }}>
226
+ Use <code style={{ fontSize: 9 }}>.</code> for the current directory, or an absolute path
227
+ </div>
228
+ </div>
229
+ {setupState !== 'analyzing' ? (
230
+ <button onClick={handleSetup} disabled={!canStart} style={{
231
+ padding: '9px 20px', border: 'none', borderRadius: 'var(--radius)',
232
+ background: canStart ? 'var(--green)' : 'var(--bg-section)',
233
+ color: canStart ? '#fff' : 'var(--text-muted)',
234
+ fontSize: 13, fontWeight: 600, cursor: canStart ? 'pointer' : 'default',
235
+ whiteSpace: 'nowrap',
236
+ }}>
237
+ Analyze Project
238
+ </button>
239
+ ) : (
240
+ <button onClick={() => abortRef.current?.abort()} style={{
241
+ padding: '9px 20px', border: 'none', borderRadius: 'var(--radius)',
242
+ background: 'var(--red)', color: '#fff',
243
+ fontSize: 13, fontWeight: 600, cursor: 'pointer', whiteSpace: 'nowrap',
244
+ }}>
245
+ Stop
246
+ </button>
247
+ )}
142
248
  </div>
143
- {setupState === 'idle' || setupState === 'done' || setupState === 'error' ? (
144
- <button onClick={handleSetup} disabled={!projectPath.trim()} style={{
145
- padding: '9px 20px', border: 'none', borderRadius: 'var(--radius)',
146
- background: projectPath.trim() ? 'var(--green)' : 'var(--bg-section)',
147
- color: projectPath.trim() ? '#fff' : 'var(--text-muted)',
148
- fontSize: 13, fontWeight: 600, cursor: projectPath.trim() ? 'pointer' : 'default',
149
- whiteSpace: 'nowrap',
150
- }}>
151
- {setupState === 'done' ? 'Re-analyze' : 'Analyze Project'}
152
- </button>
153
- ) : (
154
- <button onClick={handleStop} style={{
155
- padding: '9px 20px', border: 'none', borderRadius: 'var(--radius)',
156
- background: 'var(--red)', color: '#fff',
157
- fontSize: 13, fontWeight: 600, cursor: 'pointer', whiteSpace: 'nowrap',
158
- }}>
159
- Stop
160
- </button>
161
- )}
162
- </div>
249
+ )}
250
+
251
+ {/* GitHub URL Input */}
252
+ {inputMode === 'github' && (
253
+ <div>
254
+ <div style={{ display: 'flex', gap: 10, alignItems: 'flex-end', marginBottom: 10 }}>
255
+ <div style={{ flex: 1 }}>
256
+ <label style={{ fontSize: 11, fontWeight: 600, color: 'var(--text-dim)', display: 'block', marginBottom: 5 }}>
257
+ GitHub repository URL
258
+ </label>
259
+ <input
260
+ value={githubUrl}
261
+ onChange={e => setGithubUrl(e.target.value)}
262
+ placeholder="https://github.com/user/repo"
263
+ disabled={setupState === 'analyzing'}
264
+ style={{
265
+ width: '100%', padding: '9px 14px',
266
+ border: '1px solid var(--border)', borderRadius: 'var(--radius)',
267
+ fontSize: 13, fontFamily: 'var(--font-mono)',
268
+ background: 'var(--bg-input)', color: 'var(--text)',
269
+ }}
270
+ />
271
+ </div>
272
+ </div>
273
+ <div style={{ display: 'flex', gap: 10, alignItems: 'flex-end' }}>
274
+ <div style={{ flex: 1 }}>
275
+ <label style={{ fontSize: 11, fontWeight: 600, color: 'var(--text-dim)', display: 'block', marginBottom: 5 }}>
276
+ Clone to directory
277
+ </label>
278
+ <input
279
+ value={cloneDir}
280
+ onChange={e => setCloneDir(e.target.value)}
281
+ placeholder="~/projects"
282
+ disabled={setupState === 'analyzing'}
283
+ style={{
284
+ width: '100%', padding: '9px 14px',
285
+ border: '1px solid var(--border)', borderRadius: 'var(--radius)',
286
+ fontSize: 13, fontFamily: 'var(--font-mono)',
287
+ background: 'var(--bg-input)', color: 'var(--text)',
288
+ }}
289
+ />
290
+ </div>
291
+ {setupState !== 'analyzing' ? (
292
+ <button onClick={handleSetup} disabled={!canStart} style={{
293
+ padding: '9px 20px', border: 'none', borderRadius: 'var(--radius)',
294
+ background: canStart ? 'var(--green)' : 'var(--bg-section)',
295
+ color: canStart ? '#fff' : 'var(--text-muted)',
296
+ fontSize: 13, fontWeight: 600, cursor: canStart ? 'pointer' : 'default',
297
+ whiteSpace: 'nowrap',
298
+ }}>
299
+ Clone &amp; Analyze
300
+ </button>
301
+ ) : (
302
+ <button onClick={() => abortRef.current?.abort()} style={{
303
+ padding: '9px 20px', border: 'none', borderRadius: 'var(--radius)',
304
+ background: 'var(--red)', color: '#fff',
305
+ fontSize: 13, fontWeight: 600, cursor: 'pointer', whiteSpace: 'nowrap',
306
+ }}>
307
+ Stop
308
+ </button>
309
+ )}
310
+ </div>
311
+ <div style={{ fontSize: 10, color: 'var(--text-muted)', marginTop: 6 }}>
312
+ The repository will be cloned to <code style={{ fontSize: 9 }}>{cloneDir}/{githubUrl.match(/\/([\w.-]+?)(?:\.git)?$/)?.[1] ?? 'repo'}</code>, then analyzed
313
+ </div>
314
+ </div>
315
+ )}
163
316
  </div>
164
317
 
165
318
  {/* Output */}
@@ -172,7 +325,7 @@ export default function ProjectsClient({ profiles, skillCount }: { profiles: Pro
172
325
  fontSize: 12, fontWeight: 600,
173
326
  color: setupState === 'done' ? 'var(--green)' : setupState === 'error' ? 'var(--red)' : 'var(--text-secondary)',
174
327
  }}>
175
- {setupState === 'analyzing' && '● Analyzing project and matching skills...'}
328
+ {setupState === 'analyzing' && '● Running project analysis...'}
176
329
  {setupState === 'done' && '✓ Analysis complete — project profile saved'}
177
330
  {setupState === 'error' && '✗ Analysis failed'}
178
331
  </div>
@@ -189,23 +342,39 @@ export default function ProjectsClient({ profiles, skillCount }: { profiles: Pro
189
342
  {setupState === 'done' && (
190
343
  <div style={{ padding: '12px 16px', borderTop: '1px solid var(--border)', display: 'flex', gap: 8 }}>
191
344
  <button onClick={() => window.location.reload()} style={{
192
- padding: '7px 14px', background: 'var(--bg-section)', border: '1px solid var(--border)',
193
- borderRadius: 'var(--radius)', fontSize: 12, fontWeight: 600, cursor: 'pointer', color: 'var(--text-secondary)',
345
+ padding: '7px 14px', background: 'var(--green)', color: '#fff', border: 'none',
346
+ borderRadius: 'var(--radius)', fontSize: 12, fontWeight: 600, cursor: 'pointer',
194
347
  }}>
195
348
  Refresh to see results
196
349
  </button>
350
+ <Link href="/network" style={{ textDecoration: 'none' }}>
351
+ <button style={{
352
+ padding: '7px 14px', background: 'var(--bg-section)', border: '1px solid var(--border)',
353
+ borderRadius: 'var(--radius)', fontSize: 12, fontWeight: 600, cursor: 'pointer', color: 'var(--text-secondary)',
354
+ }}>
355
+ View Skill Network
356
+ </button>
357
+ </Link>
358
+ <Link href="/research" style={{ textDecoration: 'none' }}>
359
+ <button style={{
360
+ padding: '7px 14px', background: 'var(--bg-section)', border: '1px solid var(--border)',
361
+ borderRadius: 'var(--radius)', fontSize: 12, fontWeight: 600, cursor: 'pointer', color: 'var(--text-secondary)',
362
+ }}>
363
+ Research Gaps
364
+ </button>
365
+ </Link>
197
366
  </div>
198
367
  )}
199
368
  </div>
200
369
  )}
201
370
 
202
- {/* Existing Project Profiles */}
371
+ {/* Existing Profiles */}
203
372
  {profiles.length > 0 && (
204
- <div style={{ marginBottom: 24 }}>
373
+ <div>
205
374
  <div style={{ fontSize: 10, fontWeight: 700, color: 'var(--text-muted)', textTransform: 'uppercase', letterSpacing: 1.5, marginBottom: 12 }}>
206
375
  Analyzed Projects ({profiles.length})
207
376
  </div>
208
- <div style={{ display: 'grid', gap: 12 }}>
377
+ <div style={{ display: 'grid', gap: 14 }}>
209
378
  {profiles.map(p => {
210
379
  const highGaps = p.gaps.filter(g => g.priority === 'high')
211
380
  const matchedPct = skillCount > 0 ? Math.round((p.matchedSkills.length / skillCount) * 100) : 0
@@ -216,71 +385,117 @@ export default function ProjectsClient({ profiles, skillCount }: { profiles: Pro
216
385
  {/* Header */}
217
386
  <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: 12 }}>
218
387
  <div>
219
- <div style={{ fontSize: 16, fontWeight: 700, marginBottom: 4 }}>{p.name}</div>
220
- <div style={{ fontSize: 12, color: 'var(--text-dim)' }}>{p.description}</div>
388
+ <div style={{ fontSize: 18, fontWeight: 700, marginBottom: 4 }}>{p.name}</div>
389
+ <div style={{ fontSize: 12, color: 'var(--text-dim)', marginBottom: 6 }}>{p.description}</div>
390
+ <div style={{ display: 'flex', gap: 4, flexWrap: 'wrap' }}>
391
+ {p.techStack.map(t => <span key={t} className="badge badge-gray">{t}</span>)}
392
+ {p.domains.map(d => <span key={d} className="badge badge-blue">{d}</span>)}
393
+ </div>
394
+ </div>
395
+ <div style={{ display: 'flex', flexDirection: 'column', alignItems: 'flex-end', gap: 4 }}>
396
+ <span className={`badge ${p.status === 'active' ? 'badge-green' : 'badge-blue'}`}>{p.status}</span>
397
+ <span style={{ fontSize: 10, color: 'var(--text-muted)' }}>{new Date(p.analyzedAt).toLocaleDateString()}</span>
221
398
  </div>
222
- <span className={`badge ${p.status === 'active' ? 'badge-green' : 'badge-blue'}`}>{p.status}</span>
223
- </div>
224
-
225
- {/* Tech + Domains */}
226
- <div style={{ display: 'flex', gap: 6, flexWrap: 'wrap', marginBottom: 14 }}>
227
- {p.techStack.map(t => <span key={t} className="badge badge-gray">{t}</span>)}
228
- {p.domains.map(d => <span key={d} className="badge badge-blue">{d}</span>)}
229
399
  </div>
230
400
 
231
- {/* Stats row */}
232
- <div className="grid-3" style={{ gap: 10, marginBottom: 14 }}>
233
- <div style={{ padding: '10px 12px', background: 'var(--bg-section)', borderRadius: 'var(--radius)' }}>
234
- <div style={{ fontSize: 20, fontWeight: 800, color: 'var(--green)' }}>{p.matchedSkills.length}</div>
235
- <div style={{ fontSize: 10, color: 'var(--text-dim)' }}>Skills Matched ({matchedPct}%)</div>
401
+ {/* Stats */}
402
+ <div className="grid-3" style={{ gap: 10, marginBottom: 16 }}>
403
+ <div style={{ padding: '12px 14px', background: 'var(--bg-section)', borderRadius: 'var(--radius)' }}>
404
+ <div style={{ fontSize: 24, fontWeight: 800, color: 'var(--green)' }}>{p.matchedSkills.length}</div>
405
+ <div style={{ fontSize: 11, color: 'var(--text-dim)' }}>Skills matched ({matchedPct}%)</div>
236
406
  </div>
237
- <div style={{ padding: '10px 12px', background: 'var(--bg-section)', borderRadius: 'var(--radius)' }}>
238
- <div style={{ fontSize: 20, fontWeight: 800, color: highGaps.length > 0 ? 'var(--red)' : 'var(--green)' }}>{p.gaps.length}</div>
239
- <div style={{ fontSize: 10, color: 'var(--text-dim)' }}>Gaps ({highGaps.length} high priority)</div>
407
+ <div style={{ padding: '12px 14px', background: 'var(--bg-section)', borderRadius: 'var(--radius)' }}>
408
+ <div style={{ fontSize: 24, fontWeight: 800, color: highGaps.length > 0 ? 'var(--red)' : 'var(--green)' }}>{p.gaps.length}</div>
409
+ <div style={{ fontSize: 11, color: 'var(--text-dim)' }}>Gaps ({highGaps.length} high priority)</div>
240
410
  </div>
241
- <div style={{ padding: '10px 12px', background: 'var(--bg-section)', borderRadius: 'var(--radius)' }}>
242
- <div style={{ fontSize: 10, color: 'var(--text-dim)', marginBottom: 2 }}>Analyzed</div>
243
- <div style={{ fontSize: 12, fontWeight: 600, color: 'var(--text-secondary)' }}>{new Date(p.analyzedAt).toLocaleDateString()}</div>
411
+ <div style={{ padding: '12px 14px', background: 'var(--bg-section)', borderRadius: 'var(--radius)' }}>
412
+ <div style={{ fontSize: 11, color: 'var(--text-dim)', marginBottom: 4 }}>Path</div>
413
+ <div style={{ fontSize: 11, fontFamily: 'var(--font-mono)', color: 'var(--text-secondary)', wordBreak: 'break-all' }}>{p.path}</div>
244
414
  </div>
245
415
  </div>
246
416
 
247
- {/* Top matched skills */}
417
+ {/* Matched Skills */}
248
418
  {p.matchedSkills.length > 0 && (
249
419
  <div style={{ marginBottom: 14 }}>
250
- <div style={{ fontSize: 10, fontWeight: 600, color: 'var(--text-muted)', marginBottom: 6 }}>TOP MATCHES</div>
251
- <div style={{ display: 'flex', gap: 6, flexWrap: 'wrap' }}>
252
- {p.matchedSkills.slice(0, 6).map(m => (
420
+ <div style={{ fontSize: 10, fontWeight: 600, color: 'var(--text-muted)', marginBottom: 6 }}>MATCHED SKILLS</div>
421
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
422
+ {p.matchedSkills.sort((a, b) => b.relevance - a.relevance).slice(0, 8).map(m => (
253
423
  <div key={m.slug} style={{
254
- padding: '4px 10px', background: 'var(--bg-section)',
255
- borderRadius: 'var(--radius)', fontSize: 11, display: 'flex', alignItems: 'center', gap: 6,
424
+ display: 'flex', alignItems: 'center', gap: 10,
425
+ padding: '6px 10px', background: 'var(--bg-section)', borderRadius: 'var(--radius)',
256
426
  }}>
257
- <span style={{ fontWeight: 500 }}>{m.slug}</span>
258
- <span style={{ fontWeight: 700, color: m.relevance >= 70 ? 'var(--green)' : 'var(--yellow)' }}>{m.relevance}%</span>
427
+ <Link href="/network" style={{ textDecoration: 'none', fontWeight: 600, fontSize: 12, color: 'var(--text)' }}>{m.slug}</Link>
428
+ <div style={{ flex: 1 }}>
429
+ <div className="score-track" style={{ height: 4 }}>
430
+ <div className="score-fill" style={{
431
+ width: `${m.relevance}%`,
432
+ background: m.relevance >= 70 ? 'var(--green)' : m.relevance >= 40 ? 'var(--yellow)' : 'var(--text-muted)',
433
+ }} />
434
+ </div>
435
+ </div>
436
+ <span style={{ fontSize: 12, fontWeight: 700, color: m.relevance >= 70 ? 'var(--green)' : 'var(--yellow)', minWidth: 32 }}>{m.relevance}%</span>
437
+ <span style={{ fontSize: 10, color: 'var(--text-dim)', maxWidth: 200, overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }}>{m.reason}</span>
259
438
  </div>
260
439
  ))}
261
- {p.matchedSkills.length > 6 && (
262
- <span style={{ fontSize: 10, color: 'var(--text-muted)', padding: '4px 6px' }}>+{p.matchedSkills.length - 6} more</span>
440
+ {p.matchedSkills.length > 8 && (
441
+ <div style={{ fontSize: 10, color: 'var(--text-muted)', paddingLeft: 10 }}>+{p.matchedSkills.length - 8} more skills</div>
263
442
  )}
264
443
  </div>
265
444
  </div>
266
445
  )}
267
446
 
268
- {/* Gaps */}
447
+ {/* Gaps with Action Buttons */}
269
448
  {p.gaps.length > 0 && (
270
- <div>
271
- <div style={{ fontSize: 10, fontWeight: 600, color: 'var(--text-muted)', marginBottom: 6 }}>GAPS</div>
272
- <div style={{ display: 'flex', flexDirection: 'column', gap: 4 }}>
449
+ <div style={{ marginBottom: 14 }}>
450
+ <div style={{ fontSize: 10, fontWeight: 600, color: 'var(--text-muted)', marginBottom: 6 }}>GAPS — CLICK TO FILL</div>
451
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
273
452
  {p.gaps.map((g, i) => (
274
453
  <div key={i} style={{
275
- padding: '6px 10px', borderRadius: 'var(--radius)',
454
+ padding: '10px 14px', borderRadius: 'var(--radius)',
276
455
  background: g.priority === 'high' ? 'var(--red-light)' : g.priority === 'medium' ? 'var(--yellow-light)' : 'var(--bg-section)',
277
456
  borderLeft: `3px solid ${g.priority === 'high' ? 'var(--red)' : g.priority === 'medium' ? 'var(--yellow)' : 'var(--text-muted)'}`,
278
- fontSize: 11, display: 'flex', justifyContent: 'space-between', alignItems: 'center',
279
457
  }}>
280
- <span><strong>{g.area}</strong> {g.description.slice(0, 60)}</span>
281
- <span className={`badge ${g.suggestedAction === 'research' ? 'badge-blue' : g.suggestedAction === 'specialize' ? 'badge-green' : 'badge-gray'}`}>
282
- {g.suggestedAction}
283
- </span>
458
+ <div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: 4 }}>
459
+ <div>
460
+ <span style={{ fontSize: 13, fontWeight: 700, color: 'var(--text)' }}>{g.area}</span>
461
+ <span className={`badge ${g.priority === 'high' ? 'badge-red' : g.priority === 'medium' ? 'badge-yellow' : 'badge-gray'}`} style={{ marginLeft: 6 }}>{g.priority}</span>
462
+ </div>
463
+ {g.suggestedAction === 'research' && (
464
+ <Link href="/research" style={{ textDecoration: 'none' }}>
465
+ <button style={{
466
+ padding: '4px 12px', fontSize: 10, fontWeight: 600,
467
+ background: 'var(--blue)', color: '#fff', border: 'none',
468
+ borderRadius: 'var(--radius)', cursor: 'pointer',
469
+ }}>
470
+ Research this gap
471
+ </button>
472
+ </Link>
473
+ )}
474
+ {g.suggestedAction === 'specialize' && (
475
+ <button style={{
476
+ padding: '4px 12px', fontSize: 10, fontWeight: 600,
477
+ background: 'var(--green)', color: '#fff', border: 'none',
478
+ borderRadius: 'var(--radius)', cursor: 'pointer',
479
+ }}>
480
+ Specialize a skill
481
+ </button>
482
+ )}
483
+ {g.suggestedAction === 'create' && (
484
+ <Link href="/network" style={{ textDecoration: 'none' }}>
485
+ <button style={{
486
+ padding: '4px 12px', fontSize: 10, fontWeight: 600,
487
+ background: 'var(--bg-section)', color: 'var(--text-secondary)',
488
+ border: '1px solid var(--border)',
489
+ borderRadius: 'var(--radius)', cursor: 'pointer',
490
+ }}>
491
+ Create manually
492
+ </button>
493
+ </Link>
494
+ )}
495
+ </div>
496
+ <div style={{ fontSize: 11, color: 'var(--text-dim)', lineHeight: 1.5 }}>
497
+ {g.description}
498
+ </div>
284
499
  </div>
285
500
  ))}
286
501
  </div>
@@ -289,13 +504,33 @@ export default function ProjectsClient({ profiles, skillCount }: { profiles: Pro
289
504
 
290
505
  {/* Recommendations */}
291
506
  {p.recommendations.length > 0 && (
292
- <div style={{ marginTop: 12, padding: '10px 14px', background: 'var(--bg-section)', borderRadius: 'var(--radius)' }}>
293
- <div style={{ fontSize: 10, fontWeight: 600, color: 'var(--text-muted)', marginBottom: 4 }}>NEXT STEPS</div>
507
+ <div style={{ padding: '10px 14px', background: 'var(--bg-section)', borderRadius: 'var(--radius)' }}>
508
+ <div style={{ fontSize: 10, fontWeight: 600, color: 'var(--text-muted)', marginBottom: 4 }}>RECOMMENDED NEXT STEPS</div>
294
509
  {p.recommendations.map((r, i) => (
295
- <div key={i} style={{ fontSize: 11, color: 'var(--text-dim)', lineHeight: 1.5 }}>→ {r}</div>
510
+ <div key={i} style={{ fontSize: 11, color: 'var(--text-dim)', lineHeight: 1.6 }}>→ {r}</div>
296
511
  ))}
297
512
  </div>
298
513
  )}
514
+
515
+ {/* Re-analyze button */}
516
+ <div style={{ marginTop: 12, display: 'flex', gap: 8 }}>
517
+ <button onClick={() => { setProjectPath(p.path); setInputMode('local'); window.scrollTo(0, 0) }} style={{
518
+ padding: '6px 12px', fontSize: 11, fontWeight: 600,
519
+ background: 'var(--bg-section)', border: '1px solid var(--border)',
520
+ borderRadius: 'var(--radius)', cursor: 'pointer', color: 'var(--text-secondary)',
521
+ }}>
522
+ Re-analyze
523
+ </button>
524
+ <Link href="/network" style={{ textDecoration: 'none' }}>
525
+ <button style={{
526
+ padding: '6px 12px', fontSize: 11, fontWeight: 600,
527
+ background: 'var(--bg-section)', border: '1px solid var(--border)',
528
+ borderRadius: 'var(--radius)', cursor: 'pointer', color: 'var(--text-secondary)',
529
+ }}>
530
+ View in Skill Network
531
+ </button>
532
+ </Link>
533
+ </div>
299
534
  </div>
300
535
  </div>
301
536
  )
@@ -315,8 +550,8 @@ export default function ProjectsClient({ profiles, skillCount }: { profiles: Pro
315
550
  </div>
316
551
  <div className="empty-state-title">No projects analyzed yet</div>
317
552
  <div className="empty-state-desc">
318
- Enter a project folder path above to analyze it against your {skillCount} skills.
319
- HelixEvo will identify which skills match and what gaps need filling.
553
+ Enter a project folder path or GitHub URL above. HelixEvo will analyze it against your {skillCount} skills,
554
+ identify which ones match, and tell you what gaps need filling.
320
555
  </div>
321
556
  </div>
322
557
  </div>
@@ -49,6 +49,8 @@ const nextConfig = {
49
49
  env: {
50
50
  HELIXEVO_VERSION: getVersion(),
51
51
  },
52
+ // Silence "multiple lockfiles" warning
53
+ outputFileTracingRoot: __dirname,
52
54
  }
53
55
 
54
56
  export default nextConfig
package/dist/cli.js CHANGED
@@ -12451,7 +12451,7 @@ init_skills();
12451
12451
  init_data();
12452
12452
  var __filename = "/Users/tianchichen/Documents/GitHub/helixevo/src/commands/dashboard.ts";
12453
12453
  var HELIX_DASHBOARD_DIR = join15(homedir3(), ".helix", "dashboard");
12454
- async function dashboardCommand() {
12454
+ async function dashboardCommand(options) {
12455
12455
  const dir = prepareDashboard();
12456
12456
  if (!dir) {
12457
12457
  console.error(` Dashboard not found.
@@ -12473,6 +12473,36 @@ async function dashboardCommand() {
12473
12473
  }
12474
12474
  }
12475
12475
  ensureSkillGraph();
12476
+ if (options.background) {
12477
+ const logFile = join15(homedir3(), ".helix", "dashboard.log");
12478
+ const out = __require("fs").openSync(logFile, "a");
12479
+ const err = __require("fs").openSync(logFile, "a");
12480
+ let currentVersion = VERSION;
12481
+ try {
12482
+ currentVersion = execSync2("helixevo --version 2>/dev/null", { encoding: "utf-8" }).trim() || VERSION;
12483
+ } catch {}
12484
+ const child = spawn2("npx", ["next", "dev", "--port", "3847"], {
12485
+ cwd: dir,
12486
+ stdio: ["ignore", out, err],
12487
+ env: { ...process.env, HELIXEVO_VERSION: currentVersion },
12488
+ detached: true
12489
+ });
12490
+ child.unref();
12491
+ writeFileSync9(join15(homedir3(), ".helix", "dashboard.pid"), String(child.pid));
12492
+ console.log(` \uD83C\uDF10 HelixEvo Dashboard v${VERSION} running in background`);
12493
+ console.log(` http://localhost:3847`);
12494
+ console.log(` Logs: ${logFile}`);
12495
+ console.log(` Stop: helixevo dashboard --stop
12496
+ `);
12497
+ setTimeout(() => {
12498
+ try {
12499
+ const platform = process.platform;
12500
+ const cmd = platform === "darwin" ? "open" : platform === "win32" ? "start" : "xdg-open";
12501
+ execSync2(`${cmd} http://localhost:3847`, { stdio: "ignore" });
12502
+ } catch {}
12503
+ }, 2000);
12504
+ process.exit(0);
12505
+ }
12476
12506
  console.log(` \uD83C\uDF10 Starting HelixEvo Dashboard v${VERSION} at http://localhost:3847
12477
12507
  `);
12478
12508
  launchDashboard(dir, true);
@@ -13485,6 +13515,9 @@ function printUpdateBanner(latestVersion) {
13485
13515
  }
13486
13516
 
13487
13517
  // src/cli.ts
13518
+ import { join as join20 } from "node:path";
13519
+ import { homedir as homedir5 } from "node:os";
13520
+ import { existsSync as existsSync16, readFileSync as readFileSync13, rmSync as rmSync3 } from "node:fs";
13488
13521
  var program2 = new Command;
13489
13522
  program2.name("helixevo").description("Self-evolving skill ecosystem for AI agents").version(VERSION).addHelpText("after", `
13490
13523
  Examples:
@@ -13512,7 +13545,23 @@ program2.command("generalize").description("Promote cross-skill patterns to high
13512
13545
  program2.command("specialize").description("Create project-specific skills ↓ --project <name> [--dry-run] [--verbose]").requiredOption("--project <name>", "Project name").option("--dry-run", "Show candidates without applying").option("--verbose", "Show detailed analysis").action(specializeCommand);
13513
13546
  program2.command("graph").description("Skill network [--mermaid] [--obsidian <path>] [--rebuild] [--optimize]").option("--mermaid", "Render as Mermaid diagram in browser").option("--rebuild", "Force rebuild (re-infer relationships via LLM)").option("--obsidian <path>", "Sync to Obsidian vault at path").option("--optimize", "Run network optimization (merge/split/conflict detection)").option("--verbose", "Show detailed analysis").action(graphCommand);
13514
13547
  program2.command("research").description("Proactive research via web [--project <path>] [--dry-run] [--verbose]").option("--project <path>", "Project path for goal extraction").option("--dry-run", "Show discoveries without creating skills").option("--verbose", "Show detailed research steps").option("--max-hypotheses <n>", "Max hypotheses to test", "3").action(researchCommand);
13515
- program2.command("dashboard").description("Open web dashboard at http://localhost:3847").action(dashboardCommand);
13548
+ program2.command("dashboard").description("Open web dashboard at http://localhost:3847").option("--background", "Run in background (detach from terminal)").option("--stop", "Stop a background dashboard").action(async (options) => {
13549
+ if (options.stop) {
13550
+ const pidFile = join20(homedir5(), ".helix", "dashboard.pid");
13551
+ if (existsSync16(pidFile)) {
13552
+ const pid = parseInt(readFileSync13(pidFile, "utf-8").trim());
13553
+ try {
13554
+ process.kill(pid, "SIGTERM");
13555
+ } catch {}
13556
+ rmSync3(pidFile);
13557
+ console.log(" Dashboard stopped.");
13558
+ } else {
13559
+ console.log(" No background dashboard running.");
13560
+ }
13561
+ return;
13562
+ }
13563
+ dashboardCommand(options);
13564
+ });
13516
13565
  program2.command("status").description("Show frontier, skills, failures, and network health").action(statusCommand);
13517
13566
  program2.command("report").description("Evolution report [--days <n>] [--output <path>]").option("--days <n>", "Report period in days", "1").option("--output <path>", "Output path for report").action(reportCommand);
13518
13567
  program2.command("watch").description("Always-on learning: auto-capture corrections + auto-evolve").option("--project <name>", "Project name for captured failures").option("--events <path>", "Path to events.jsonl (default: ./events.jsonl)").option("--verbose", "Show detailed capture and metrics").option("--no-evolve", "Disable auto-evolution (capture only)").action(watchCommand);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "helixevo",
3
- "version": "0.2.35",
3
+ "version": "0.2.37",
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": {