agileflow 2.89.2 → 2.90.0

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.
Files changed (57) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/README.md +3 -3
  3. package/lib/content-sanitizer.js +463 -0
  4. package/lib/error-codes.js +544 -0
  5. package/lib/errors.js +336 -5
  6. package/lib/feedback.js +561 -0
  7. package/lib/path-resolver.js +396 -0
  8. package/lib/placeholder-registry.js +617 -0
  9. package/lib/session-registry.js +461 -0
  10. package/lib/smart-json-file.js +653 -0
  11. package/lib/table-formatter.js +504 -0
  12. package/lib/transient-status.js +374 -0
  13. package/lib/ui-manager.js +612 -0
  14. package/lib/validate-args.js +213 -0
  15. package/lib/validate-names.js +143 -0
  16. package/lib/validate-paths.js +434 -0
  17. package/lib/validate.js +38 -584
  18. package/package.json +4 -1
  19. package/scripts/agileflow-configure.js +40 -1440
  20. package/scripts/agileflow-welcome.js +2 -1
  21. package/scripts/check-update.js +16 -3
  22. package/scripts/lib/configure-detect.js +383 -0
  23. package/scripts/lib/configure-features.js +811 -0
  24. package/scripts/lib/configure-repair.js +314 -0
  25. package/scripts/lib/configure-utils.js +115 -0
  26. package/scripts/lib/frontmatter-parser.js +3 -3
  27. package/scripts/lib/sessionRegistry.js +682 -0
  28. package/scripts/obtain-context.js +417 -113
  29. package/scripts/ralph-loop.js +1 -1
  30. package/scripts/session-manager.js +77 -10
  31. package/scripts/tui/App.js +176 -0
  32. package/scripts/tui/index.js +75 -0
  33. package/scripts/tui/lib/crashRecovery.js +302 -0
  34. package/scripts/tui/lib/eventStream.js +316 -0
  35. package/scripts/tui/lib/keyboard.js +252 -0
  36. package/scripts/tui/lib/loopControl.js +371 -0
  37. package/scripts/tui/panels/OutputPanel.js +278 -0
  38. package/scripts/tui/panels/SessionPanel.js +178 -0
  39. package/scripts/tui/panels/TracePanel.js +333 -0
  40. package/src/core/commands/tui.md +91 -0
  41. package/tools/cli/commands/config.js +10 -33
  42. package/tools/cli/commands/doctor.js +48 -40
  43. package/tools/cli/commands/list.js +49 -37
  44. package/tools/cli/commands/status.js +13 -37
  45. package/tools/cli/commands/uninstall.js +12 -41
  46. package/tools/cli/installers/core/installer.js +75 -12
  47. package/tools/cli/installers/ide/_interface.js +238 -0
  48. package/tools/cli/installers/ide/codex.js +2 -2
  49. package/tools/cli/installers/ide/manager.js +15 -0
  50. package/tools/cli/lib/command-context.js +374 -0
  51. package/tools/cli/lib/config-manager.js +394 -0
  52. package/tools/cli/lib/content-injector.js +69 -16
  53. package/tools/cli/lib/ide-errors.js +163 -29
  54. package/tools/cli/lib/ide-registry.js +186 -0
  55. package/tools/cli/lib/npm-utils.js +16 -3
  56. package/tools/cli/lib/self-update.js +148 -0
  57. package/tools/cli/lib/validation-middleware.js +491 -0
@@ -0,0 +1,178 @@
1
+ 'use strict';
2
+
3
+ const React = require('react');
4
+ const { Box, Text } = require('ink');
5
+ const fs = require('fs');
6
+ const path = require('path');
7
+
8
+ // Get session-manager functions
9
+ let getSessions;
10
+ try {
11
+ const sessionManager = require('../../session-manager');
12
+ getSessions = sessionManager.getSessions;
13
+ } catch (e) {
14
+ // Fallback if module not found
15
+ getSessions = () => ({ sessions: [], cleaned: 0 });
16
+ }
17
+
18
+ /**
19
+ * Get thread type color
20
+ */
21
+ function getThreadColor(threadType) {
22
+ const colors = {
23
+ base: 'green',
24
+ parallel: 'cyan',
25
+ chained: 'yellow',
26
+ fusion: 'magenta',
27
+ big: 'blue',
28
+ long: 'white'
29
+ };
30
+ return colors[threadType] || 'gray';
31
+ }
32
+
33
+ /**
34
+ * Get status color
35
+ */
36
+ function getStatusColor(active, current) {
37
+ if (current) return 'green';
38
+ if (active) return 'cyan';
39
+ return 'gray';
40
+ }
41
+
42
+ /**
43
+ * Format session for display
44
+ */
45
+ function formatSession(session) {
46
+ const { id, branch, story, nickname, thread_type, active, current, is_main } = session;
47
+
48
+ // Build display name
49
+ let displayName = `Session ${id}`;
50
+ if (nickname) displayName += ` "${nickname}"`;
51
+ if (is_main) displayName += ' [main]';
52
+
53
+ return {
54
+ displayName,
55
+ branch: branch || 'unknown',
56
+ story: story || 'none',
57
+ threadType: thread_type || 'base',
58
+ active,
59
+ current
60
+ };
61
+ }
62
+
63
+ /**
64
+ * SessionRow component - displays a single session
65
+ */
66
+ function SessionRow({ session }) {
67
+ const fmt = formatSession(session);
68
+ const statusColor = getStatusColor(fmt.active, fmt.current);
69
+ const threadColor = getThreadColor(fmt.threadType);
70
+
71
+ // Current session indicator
72
+ const indicator = fmt.current ? '▶' : ' ';
73
+ const indicatorColor = fmt.current ? 'green' : 'gray';
74
+
75
+ return React.createElement(
76
+ Box,
77
+ { flexDirection: 'column', marginBottom: 1 },
78
+ // Session name row
79
+ React.createElement(
80
+ Box,
81
+ null,
82
+ React.createElement(Text, { color: indicatorColor }, indicator + ' '),
83
+ React.createElement(Text, { bold: true, color: statusColor }, fmt.displayName),
84
+ React.createElement(Text, { dimColor: true }, ' ['),
85
+ React.createElement(Text, { color: threadColor }, fmt.threadType),
86
+ React.createElement(Text, { dimColor: true }, ']')
87
+ ),
88
+ // Branch and story
89
+ React.createElement(
90
+ Box,
91
+ { paddingLeft: 3 },
92
+ React.createElement(Text, { dimColor: true }, 'Branch: '),
93
+ React.createElement(Text, { color: 'cyan' }, fmt.branch)
94
+ ),
95
+ React.createElement(
96
+ Box,
97
+ { paddingLeft: 3 },
98
+ React.createElement(Text, { dimColor: true }, 'Story: '),
99
+ React.createElement(Text, { color: fmt.story === 'none' ? 'gray' : 'yellow' }, fmt.story)
100
+ )
101
+ );
102
+ }
103
+
104
+ /**
105
+ * SessionPanel component - displays all sessions
106
+ */
107
+ function SessionPanel({ refreshInterval = 5000 }) {
108
+ const [sessions, setSessions] = React.useState([]);
109
+ const [lastUpdate, setLastUpdate] = React.useState(new Date());
110
+
111
+ // Load sessions on mount and on interval
112
+ React.useEffect(() => {
113
+ function loadSessions() {
114
+ try {
115
+ const result = getSessions();
116
+ setSessions(result.sessions || []);
117
+ setLastUpdate(new Date());
118
+ } catch (e) {
119
+ // Silently handle errors
120
+ setSessions([]);
121
+ }
122
+ }
123
+
124
+ // Initial load
125
+ loadSessions();
126
+
127
+ // Refresh periodically
128
+ const interval = setInterval(loadSessions, refreshInterval);
129
+
130
+ return () => clearInterval(interval);
131
+ }, [refreshInterval]);
132
+
133
+ // Empty state
134
+ if (sessions.length === 0) {
135
+ return React.createElement(
136
+ Box,
137
+ { flexDirection: 'column', borderStyle: 'single', borderColor: 'gray', padding: 1 },
138
+ React.createElement(
139
+ Text,
140
+ { bold: true, color: 'cyan' },
141
+ 'SESSIONS'
142
+ ),
143
+ React.createElement(
144
+ Text,
145
+ { dimColor: true, italic: true },
146
+ 'No active sessions'
147
+ )
148
+ );
149
+ }
150
+
151
+ return React.createElement(
152
+ Box,
153
+ { flexDirection: 'column', borderStyle: 'single', borderColor: 'gray', padding: 1 },
154
+ // Header
155
+ React.createElement(
156
+ Box,
157
+ { marginBottom: 1 },
158
+ React.createElement(Text, { bold: true, color: 'cyan' }, 'SESSIONS'),
159
+ React.createElement(Text, { dimColor: true }, ` (${sessions.length})`)
160
+ ),
161
+ // Session list
162
+ ...sessions.map((session, index) =>
163
+ React.createElement(SessionRow, { key: session.id || index, session })
164
+ ),
165
+ // Footer with refresh info
166
+ React.createElement(
167
+ Box,
168
+ { marginTop: 1 },
169
+ React.createElement(
170
+ Text,
171
+ { dimColor: true, italic: true },
172
+ `Updated: ${lastUpdate.toLocaleTimeString()}`
173
+ )
174
+ )
175
+ );
176
+ }
177
+
178
+ module.exports = { SessionPanel, SessionRow, formatSession, getThreadColor };
@@ -0,0 +1,333 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Agent Trace Panel - Quality gate and iteration tracking
5
+ *
6
+ * Shows active agent loops with:
7
+ * - Current gate (tests, coverage, lint, types, visual)
8
+ * - Iteration count and progress
9
+ * - Pass/fail status with color coding
10
+ * - Progress bars for coverage
11
+ */
12
+
13
+ const React = require('react');
14
+ const { Box, Text } = require('ink');
15
+ const { getLoopStatus } = require('../lib/loopControl');
16
+
17
+ /**
18
+ * Quality gate definitions
19
+ */
20
+ const QUALITY_GATES = {
21
+ tests: { name: 'Tests', color: 'green', icon: '✓' },
22
+ coverage: { name: 'Coverage', color: 'magenta', icon: '%' },
23
+ lint: { name: 'Lint', color: 'yellow', icon: '◆' },
24
+ types: { name: 'Types', color: 'blue', icon: '⬡' },
25
+ visual: { name: 'Visual', color: 'cyan', icon: '◉' }
26
+ };
27
+
28
+ /**
29
+ * Get status color
30
+ */
31
+ function getStatusColor(status) {
32
+ switch (status) {
33
+ case 'passed':
34
+ case 'complete':
35
+ case true:
36
+ return 'green';
37
+ case 'failed':
38
+ case 'error':
39
+ case false:
40
+ return 'red';
41
+ case 'running':
42
+ case 'in_progress':
43
+ return 'cyan';
44
+ case 'pending':
45
+ case 'waiting':
46
+ return 'gray';
47
+ default:
48
+ return 'white';
49
+ }
50
+ }
51
+
52
+ /**
53
+ * Get status symbol
54
+ */
55
+ function getStatusSymbol(status) {
56
+ switch (status) {
57
+ case 'passed':
58
+ case 'complete':
59
+ case true:
60
+ return '✓';
61
+ case 'failed':
62
+ case 'error':
63
+ case false:
64
+ return '✗';
65
+ case 'running':
66
+ case 'in_progress':
67
+ return '↻';
68
+ case 'pending':
69
+ case 'waiting':
70
+ return '○';
71
+ case 'paused':
72
+ return '⏸';
73
+ default:
74
+ return '•';
75
+ }
76
+ }
77
+
78
+ /**
79
+ * Progress bar component
80
+ */
81
+ function ProgressBar({ value, max = 100, width = 20, fillChar = '█', emptyChar = '░' }) {
82
+ const percent = Math.min(100, Math.max(0, (value / max) * 100));
83
+ const filled = Math.round((percent / 100) * width);
84
+ const empty = width - filled;
85
+
86
+ const filledStr = fillChar.repeat(filled);
87
+ const emptyStr = emptyChar.repeat(empty);
88
+
89
+ // Color based on progress
90
+ let color = 'red';
91
+ if (percent >= 80) color = 'green';
92
+ else if (percent >= 50) color = 'yellow';
93
+
94
+ return React.createElement(
95
+ Box,
96
+ { flexDirection: 'row' },
97
+ React.createElement(Text, { color }, filledStr),
98
+ React.createElement(Text, { dimColor: true }, emptyStr),
99
+ React.createElement(Text, { dimColor: true }, ` ${percent.toFixed(0)}%`)
100
+ );
101
+ }
102
+
103
+ /**
104
+ * Quality gate row component
105
+ */
106
+ function GateRow({ gate, status, value }) {
107
+ const gateConfig = QUALITY_GATES[gate] || { name: gate, color: 'white', icon: '•' };
108
+ const statusColor = getStatusColor(status);
109
+ const statusSymbol = getStatusSymbol(status);
110
+
111
+ return React.createElement(
112
+ Box,
113
+ { flexDirection: 'row', marginLeft: 2 },
114
+ React.createElement(
115
+ Text,
116
+ { color: gateConfig.color },
117
+ `${gateConfig.icon} `
118
+ ),
119
+ React.createElement(
120
+ Text,
121
+ { color: statusColor },
122
+ `${statusSymbol} `
123
+ ),
124
+ React.createElement(
125
+ Text,
126
+ null,
127
+ `${gateConfig.name}`
128
+ ),
129
+ value !== undefined && React.createElement(
130
+ Text,
131
+ { dimColor: true },
132
+ `: ${value}`
133
+ )
134
+ );
135
+ }
136
+
137
+ /**
138
+ * Active loop trace component
139
+ */
140
+ function LoopTrace({ loopStatus }) {
141
+ if (!loopStatus || !loopStatus.active) {
142
+ return React.createElement(
143
+ Text,
144
+ { dimColor: true, italic: true },
145
+ 'No active loop'
146
+ );
147
+ }
148
+
149
+ const {
150
+ epic,
151
+ currentStory,
152
+ iteration,
153
+ maxIterations,
154
+ paused,
155
+ coverageMode,
156
+ coverageThreshold,
157
+ coverageCurrent,
158
+ visualMode
159
+ } = loopStatus;
160
+
161
+ // Build gates list
162
+ const gates = [];
163
+
164
+ // Tests always present
165
+ gates.push({
166
+ gate: 'tests',
167
+ status: 'running',
168
+ value: null
169
+ });
170
+
171
+ // Coverage if enabled
172
+ if (coverageMode) {
173
+ gates.push({
174
+ gate: 'coverage',
175
+ status: coverageCurrent >= coverageThreshold ? 'passed' : 'running',
176
+ value: `${coverageCurrent?.toFixed(1) || 0}% / ${coverageThreshold}%`
177
+ });
178
+ }
179
+
180
+ // Visual if enabled
181
+ if (visualMode) {
182
+ gates.push({
183
+ gate: 'visual',
184
+ status: 'pending',
185
+ value: null
186
+ });
187
+ }
188
+
189
+ return React.createElement(
190
+ Box,
191
+ { flexDirection: 'column' },
192
+ // Loop header
193
+ React.createElement(
194
+ Box,
195
+ { flexDirection: 'row', marginBottom: 1 },
196
+ React.createElement(
197
+ Text,
198
+ { bold: true, color: paused ? 'yellow' : 'green' },
199
+ paused ? '⏸ PAUSED' : '▶ RUNNING'
200
+ ),
201
+ React.createElement(Text, { dimColor: true }, ` • ${epic}`)
202
+ ),
203
+ // Current story
204
+ React.createElement(
205
+ Box,
206
+ { flexDirection: 'row' },
207
+ React.createElement(Text, { dimColor: true }, 'Story: '),
208
+ React.createElement(Text, { color: 'cyan' }, currentStory || 'none')
209
+ ),
210
+ // Iteration progress
211
+ React.createElement(
212
+ Box,
213
+ { flexDirection: 'row', marginY: 1 },
214
+ React.createElement(Text, { dimColor: true }, 'Iteration: '),
215
+ React.createElement(
216
+ ProgressBar,
217
+ { value: iteration || 0, max: maxIterations || 20, width: 15 }
218
+ ),
219
+ React.createElement(Text, { dimColor: true }, ` ${iteration || 0}/${maxIterations || 20}`)
220
+ ),
221
+ // Coverage progress (if enabled)
222
+ coverageMode && React.createElement(
223
+ Box,
224
+ { flexDirection: 'row', marginBottom: 1 },
225
+ React.createElement(Text, { dimColor: true }, 'Coverage: '),
226
+ React.createElement(
227
+ ProgressBar,
228
+ { value: coverageCurrent || 0, max: 100, width: 15 }
229
+ )
230
+ ),
231
+ // Quality gates
232
+ React.createElement(
233
+ Box,
234
+ { flexDirection: 'column', marginTop: 1 },
235
+ React.createElement(Text, { dimColor: true }, 'Quality Gates:'),
236
+ gates.map((g, i) =>
237
+ React.createElement(GateRow, { key: `gate-${i}`, ...g })
238
+ )
239
+ )
240
+ );
241
+ }
242
+
243
+ /**
244
+ * Main Trace Panel component
245
+ */
246
+ function TracePanel({ visible = true, refreshInterval = 2000 }) {
247
+ const [loopStatus, setLoopStatus] = React.useState(null);
248
+
249
+ React.useEffect(() => {
250
+ if (!visible) return;
251
+
252
+ const loadStatus = () => {
253
+ try {
254
+ const status = getLoopStatus();
255
+ setLoopStatus(status);
256
+ } catch (e) {
257
+ // Ignore errors
258
+ }
259
+ };
260
+
261
+ loadStatus();
262
+ const interval = setInterval(loadStatus, refreshInterval);
263
+
264
+ return () => clearInterval(interval);
265
+ }, [visible, refreshInterval]);
266
+
267
+ if (!visible) {
268
+ return null;
269
+ }
270
+
271
+ return React.createElement(
272
+ Box,
273
+ {
274
+ flexDirection: 'column',
275
+ borderStyle: 'single',
276
+ borderColor: 'blue',
277
+ padding: 1
278
+ },
279
+ // Header
280
+ React.createElement(
281
+ Box,
282
+ { marginBottom: 1 },
283
+ React.createElement(
284
+ Text,
285
+ { bold: true, color: 'blue' },
286
+ 'AGENT TRACE'
287
+ ),
288
+ React.createElement(
289
+ Text,
290
+ { dimColor: true },
291
+ ' (T to toggle)'
292
+ )
293
+ ),
294
+ // Loop trace
295
+ React.createElement(LoopTrace, { loopStatus })
296
+ );
297
+ }
298
+
299
+ /**
300
+ * Compact trace indicator (for status bar)
301
+ */
302
+ function CompactTrace({ loopStatus }) {
303
+ if (!loopStatus || !loopStatus.active) {
304
+ return React.createElement(
305
+ Text,
306
+ { dimColor: true },
307
+ 'Loop: inactive'
308
+ );
309
+ }
310
+
311
+ const { iteration, maxIterations, paused, currentStory } = loopStatus;
312
+ const statusIcon = paused ? '⏸' : '▶';
313
+ const statusColor = paused ? 'yellow' : 'green';
314
+
315
+ return React.createElement(
316
+ Box,
317
+ { flexDirection: 'row' },
318
+ React.createElement(Text, { color: statusColor }, statusIcon),
319
+ React.createElement(Text, null, ` ${currentStory || '?'} `),
320
+ React.createElement(Text, { dimColor: true }, `[${iteration || 0}/${maxIterations || 20}]`)
321
+ );
322
+ }
323
+
324
+ module.exports = {
325
+ TracePanel,
326
+ LoopTrace,
327
+ GateRow,
328
+ ProgressBar,
329
+ CompactTrace,
330
+ getStatusColor,
331
+ getStatusSymbol,
332
+ QUALITY_GATES
333
+ };
@@ -0,0 +1,91 @@
1
+ ---
2
+ description: Launch Terminal User Interface for real-time session monitoring
3
+ argument-hint:
4
+ ---
5
+
6
+ # /agileflow:tui
7
+
8
+ Launch the AgileFlow Terminal User Interface for real-time session visualization.
9
+
10
+ ---
11
+
12
+ ## Purpose
13
+
14
+ The TUI provides a unified dashboard for:
15
+ - **Session monitoring** - See all active sessions and their current stories
16
+ - **Agent visualization** - Watch multi-agent orchestration in real-time
17
+ - **Loop control** - Pause, resume, and manage autonomous loops
18
+ - **Event streaming** - Live feed of agent activity and test results
19
+
20
+ ---
21
+
22
+ ## IMMEDIATE ACTIONS
23
+
24
+ Upon invocation, execute these steps:
25
+
26
+ ### Step 1: Launch TUI
27
+
28
+ ```bash
29
+ node packages/cli/scripts/tui/index.js
30
+ ```
31
+
32
+ ### Step 2: Display Key Bindings
33
+
34
+ The TUI shows key bindings in the footer:
35
+ - **Q** - Quit TUI
36
+ - **S** - Start loop on current story
37
+ - **P** - Pause active loop
38
+ - **R** - Resume paused loop
39
+ - **T** - Toggle trace panel
40
+ - **1-9** - Switch session focus
41
+
42
+ ---
43
+
44
+ ## Key Features
45
+
46
+ ### Session Overview Panel
47
+ - Lists all active sessions with ID, branch, and current story
48
+ - Color coding by thread type (base=green, parallel=cyan)
49
+ - Shows loop progress and iteration count
50
+
51
+ ### Agent Output Panel
52
+ - Real-time stream of agent messages
53
+ - Timestamps and agent identification
54
+ - Auto-scroll with history buffer
55
+
56
+ ### Trace Panel (Toggle with T)
57
+ - Detailed view of agent loops and quality gates
58
+ - Progress bars for each gate (tests, coverage, lint, types)
59
+ - Pass/fail status indicators
60
+
61
+ ---
62
+
63
+ ## Example Session
64
+
65
+ ```
66
+ ┌─────────────────────────────────────────────────────────────────────┐
67
+ │ AgileFlow TUI │
68
+ ├─────────────────────────────┬───────────────────────────────────────┤
69
+ │ SESSIONS │ AGENT OUTPUT │
70
+ │ ───────── │ ──────────── │
71
+ │ ▶ Session 1 [main] │ [agileflow-api] Reading status.json │
72
+ │ Story: US-0115 │ [agileflow-api] Running tests... │
73
+ │ Loop: 3/20 iterations │ ✓ 47 tests passed │
74
+ ├─────────────────────────────┴───────────────────────────────────────┤
75
+ │ [S]tart [P]ause [R]esume [T]race [Q]uit │
76
+ └─────────────────────────────────────────────────────────────────────┘
77
+ ```
78
+
79
+ ---
80
+
81
+ ## Related Commands
82
+
83
+ - `/agileflow:babysit MODE=loop` - Autonomous story processing
84
+ - `/agileflow:session:status` - Session information (non-TUI)
85
+ - `/agileflow:session:new` - Create parallel sessions
86
+
87
+ ---
88
+
89
+ ## Research
90
+
91
+ Based on Ralph TUI research: `docs/10-research/20260114-ralph-loop-ralph-tui.md`
@@ -7,11 +7,12 @@
7
7
  const chalk = require('chalk');
8
8
  const path = require('node:path');
9
9
  const fs = require('fs-extra');
10
- const yaml = require('js-yaml');
10
+ const { safeLoad, safeDump } = require('../../../lib/yaml-utils');
11
11
  const { Installer } = require('../installers/core/installer');
12
12
  const { IdeManager } = require('../installers/ide/manager');
13
13
  const { displayLogo, displaySection, success, warning, error, info } = require('../lib/ui');
14
14
  const { ErrorHandler } = require('../lib/error-handler');
15
+ const { IdeRegistry } = require('../lib/ide-registry');
15
16
 
16
17
  const installer = new Installer();
17
18
  const ideManager = new IdeManager();
@@ -194,7 +195,7 @@ async function handleSet(directory, status, manifestPath, key, value) {
194
195
 
195
196
  // Read current manifest
196
197
  const manifestContent = await fs.readFile(manifestPath, 'utf8');
197
- const manifest = yaml.load(manifestContent);
198
+ const manifest = safeLoad(manifestContent);
198
199
 
199
200
  // Track if we need to update IDE configs
200
201
  let needsIdeUpdate = false;
@@ -209,11 +210,12 @@ async function handleSet(directory, status, manifestPath, key, value) {
209
210
 
210
211
  case 'ides': {
211
212
  const newIdes = value.split(',').map(ide => ide.trim());
212
- const validIdes = ['claude-code', 'cursor', 'windsurf'];
213
+ const validIdes = IdeRegistry.getAll();
213
214
 
214
215
  // Validate IDEs
215
216
  for (const ide of newIdes) {
216
- if (!validIdes.includes(ide)) {
217
+ const validation = IdeRegistry.validate(ide);
218
+ if (!validation.ok) {
217
219
  handler.warning(
218
220
  `Invalid IDE: ${ide}`,
219
221
  `Valid IDEs: ${validIdes.join(', ')}`,
@@ -247,7 +249,7 @@ async function handleSet(directory, status, manifestPath, key, value) {
247
249
  manifest.updated_at = new Date().toISOString();
248
250
 
249
251
  // Write manifest
250
- await fs.writeFile(manifestPath, yaml.dump(manifest), 'utf8');
252
+ await fs.writeFile(manifestPath, safeDump(manifest), 'utf8');
251
253
  success('Configuration updated');
252
254
 
253
255
  // Update IDE configs if needed
@@ -261,10 +263,10 @@ async function handleSet(directory, status, manifestPath, key, value) {
261
263
  // Remove old IDE configs
262
264
  for (const ide of oldIdes) {
263
265
  if (!manifest.ides.includes(ide)) {
264
- const configPath = getIdeConfigPath(directory, ide);
266
+ const configPath = IdeRegistry.getConfigPath(ide, directory);
265
267
  if (await fs.pathExists(configPath)) {
266
268
  await fs.remove(configPath);
267
- info(`Removed ${formatIdeName(ide)} configuration`);
269
+ info(`Removed ${IdeRegistry.getDisplayName(ide)} configuration`);
268
270
  }
269
271
  }
270
272
  }
@@ -272,7 +274,7 @@ async function handleSet(directory, status, manifestPath, key, value) {
272
274
  // Add new IDE configs
273
275
  for (const ide of manifest.ides) {
274
276
  await ideManager.setup(ide, directory, status.path);
275
- success(`Updated ${formatIdeName(ide)} configuration`);
277
+ success(`Updated ${IdeRegistry.getDisplayName(ide)} configuration`);
276
278
  }
277
279
 
278
280
  console.log();
@@ -282,28 +284,3 @@ async function handleSet(directory, status, manifestPath, key, value) {
282
284
  console.log();
283
285
  }
284
286
 
285
- /**
286
- * Get IDE config path
287
- */
288
- function getIdeConfigPath(projectDir, ide) {
289
- const paths = {
290
- 'claude-code': '.claude/commands/agileflow',
291
- cursor: '.cursor/rules/agileflow',
292
- windsurf: '.windsurf/workflows/agileflow',
293
- };
294
-
295
- return path.join(projectDir, paths[ide] || '');
296
- }
297
-
298
- /**
299
- * Format IDE name for display
300
- */
301
- function formatIdeName(ide) {
302
- const names = {
303
- 'claude-code': 'Claude Code',
304
- cursor: 'Cursor',
305
- windsurf: 'Windsurf',
306
- };
307
-
308
- return names[ide] || ide;
309
- }