@evolve.labs/devflow 0.8.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.
- package/.claude/commands/agents/architect.md +1162 -0
- package/.claude/commands/agents/architect.meta.yaml +124 -0
- package/.claude/commands/agents/builder.md +1432 -0
- package/.claude/commands/agents/builder.meta.yaml +117 -0
- package/.claude/commands/agents/chronicler.md +633 -0
- package/.claude/commands/agents/chronicler.meta.yaml +217 -0
- package/.claude/commands/agents/guardian.md +456 -0
- package/.claude/commands/agents/guardian.meta.yaml +127 -0
- package/.claude/commands/agents/strategist.md +483 -0
- package/.claude/commands/agents/strategist.meta.yaml +158 -0
- package/.claude/commands/agents/system-designer.md +1137 -0
- package/.claude/commands/agents/system-designer.meta.yaml +156 -0
- package/.claude/commands/devflow-help.md +93 -0
- package/.claude/commands/devflow-status.md +60 -0
- package/.claude/commands/quick/create-adr.md +82 -0
- package/.claude/commands/quick/new-feature.md +57 -0
- package/.claude/commands/quick/security-check.md +54 -0
- package/.claude/commands/quick/system-design.md +58 -0
- package/.claude_project +52 -0
- package/.devflow/agents/architect.meta.yaml +122 -0
- package/.devflow/agents/builder.meta.yaml +116 -0
- package/.devflow/agents/chronicler.meta.yaml +222 -0
- package/.devflow/agents/guardian.meta.yaml +127 -0
- package/.devflow/agents/strategist.meta.yaml +158 -0
- package/.devflow/agents/system-designer.meta.yaml +265 -0
- package/.devflow/project.yaml +242 -0
- package/.gitignore-template +84 -0
- package/LICENSE +21 -0
- package/README.md +249 -0
- package/bin/devflow.js +54 -0
- package/lib/autopilot.js +235 -0
- package/lib/autopilotConstants.js +213 -0
- package/lib/constants.js +95 -0
- package/lib/init.js +200 -0
- package/lib/update.js +181 -0
- package/lib/utils.js +157 -0
- package/lib/web.js +119 -0
- package/package.json +57 -0
- package/web/CHANGELOG.md +192 -0
- package/web/README.md +156 -0
- package/web/app/api/autopilot/execute/route.ts +102 -0
- package/web/app/api/autopilot/terminal-execute/route.ts +124 -0
- package/web/app/api/files/route.ts +280 -0
- package/web/app/api/files/tree/route.ts +160 -0
- package/web/app/api/git/route.ts +201 -0
- package/web/app/api/health/route.ts +94 -0
- package/web/app/api/project/open/route.ts +134 -0
- package/web/app/api/search/route.ts +247 -0
- package/web/app/api/specs/route.ts +405 -0
- package/web/app/api/terminal/route.ts +222 -0
- package/web/app/globals.css +160 -0
- package/web/app/ide/layout.tsx +43 -0
- package/web/app/ide/page.tsx +216 -0
- package/web/app/layout.tsx +34 -0
- package/web/app/page.tsx +303 -0
- package/web/components/agents/AgentIcons.tsx +281 -0
- package/web/components/autopilot/AutopilotConfigModal.tsx +245 -0
- package/web/components/autopilot/AutopilotPanel.tsx +299 -0
- package/web/components/dashboard/DashboardPanel.tsx +393 -0
- package/web/components/editor/Breadcrumbs.tsx +134 -0
- package/web/components/editor/EditorPanel.tsx +120 -0
- package/web/components/editor/EditorTabs.tsx +229 -0
- package/web/components/editor/MarkdownPreview.tsx +154 -0
- package/web/components/editor/MermaidDiagram.tsx +113 -0
- package/web/components/editor/MonacoEditor.tsx +177 -0
- package/web/components/editor/TabContextMenu.tsx +207 -0
- package/web/components/git/GitPanel.tsx +534 -0
- package/web/components/layout/Shell.tsx +15 -0
- package/web/components/layout/StatusBar.tsx +100 -0
- package/web/components/modals/CommandPalette.tsx +393 -0
- package/web/components/modals/GlobalSearch.tsx +348 -0
- package/web/components/modals/QuickOpen.tsx +241 -0
- package/web/components/modals/RecentFiles.tsx +208 -0
- package/web/components/projects/ProjectSelector.tsx +147 -0
- package/web/components/settings/SettingItem.tsx +150 -0
- package/web/components/settings/SettingsPanel.tsx +323 -0
- package/web/components/specs/SpecsPanel.tsx +1091 -0
- package/web/components/terminal/TerminalPanel.tsx +683 -0
- package/web/components/ui/ContextMenu.tsx +182 -0
- package/web/components/ui/LoadingSpinner.tsx +66 -0
- package/web/components/ui/ResizeHandle.tsx +110 -0
- package/web/components/ui/Skeleton.tsx +108 -0
- package/web/components/ui/SkipLinks.tsx +37 -0
- package/web/components/ui/Toaster.tsx +57 -0
- package/web/hooks/useFocusTrap.ts +141 -0
- package/web/hooks/useKeyboardShortcuts.ts +169 -0
- package/web/hooks/useListNavigation.ts +237 -0
- package/web/lib/autopilotConstants.ts +213 -0
- package/web/lib/constants/agents.ts +67 -0
- package/web/lib/git.ts +339 -0
- package/web/lib/ptyManager.ts +191 -0
- package/web/lib/specsParser.ts +299 -0
- package/web/lib/stores/autopilotStore.ts +288 -0
- package/web/lib/stores/fileStore.ts +550 -0
- package/web/lib/stores/gitStore.ts +386 -0
- package/web/lib/stores/projectStore.ts +196 -0
- package/web/lib/stores/settingsStore.ts +126 -0
- package/web/lib/stores/specsStore.ts +297 -0
- package/web/lib/stores/uiStore.ts +175 -0
- package/web/lib/types/index.ts +177 -0
- package/web/lib/utils.ts +98 -0
- package/web/next.config.js +50 -0
- package/web/package.json +54 -0
- package/web/postcss.config.js +6 -0
- package/web/tailwind.config.ts +68 -0
- package/web/tsconfig.json +41 -0
|
@@ -0,0 +1,299 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Specs Parser - Extract structured data from markdown files
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { Spec, Requirement, DesignDecision, Task, SpecPhase } from './types';
|
|
6
|
+
|
|
7
|
+
// Frontmatter regex
|
|
8
|
+
const FRONTMATTER_REGEX = /^---\n([\s\S]*?)\n---/;
|
|
9
|
+
|
|
10
|
+
// Task checkbox regex
|
|
11
|
+
const TASK_REGEX = /^[\s]*[-*]\s*\[([ xX])\]\s*(.+)$/gm;
|
|
12
|
+
|
|
13
|
+
// Section regex
|
|
14
|
+
const SECTION_REGEX = /^##\s+(.+)$/gm;
|
|
15
|
+
|
|
16
|
+
interface ParsedFrontmatter {
|
|
17
|
+
title?: string;
|
|
18
|
+
status?: string;
|
|
19
|
+
priority?: string;
|
|
20
|
+
type?: string;
|
|
21
|
+
agent?: string;
|
|
22
|
+
created?: string;
|
|
23
|
+
updated?: string;
|
|
24
|
+
[key: string]: string | undefined;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Parse frontmatter from markdown content
|
|
29
|
+
*/
|
|
30
|
+
export function parseFrontmatter(content: string): { frontmatter: ParsedFrontmatter; body: string } {
|
|
31
|
+
const match = content.match(FRONTMATTER_REGEX);
|
|
32
|
+
|
|
33
|
+
if (!match) {
|
|
34
|
+
return { frontmatter: {}, body: content };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const frontmatterStr = match[1];
|
|
38
|
+
const body = content.slice(match[0].length).trim();
|
|
39
|
+
|
|
40
|
+
const frontmatter: ParsedFrontmatter = {};
|
|
41
|
+
|
|
42
|
+
for (const line of frontmatterStr.split('\n')) {
|
|
43
|
+
const [key, ...valueParts] = line.split(':');
|
|
44
|
+
if (key && valueParts.length > 0) {
|
|
45
|
+
frontmatter[key.trim()] = valueParts.join(':').trim().replace(/^["']|["']$/g, '');
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
return { frontmatter, body };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Extract tasks from markdown content (checkbox items)
|
|
54
|
+
*/
|
|
55
|
+
export function extractTasks(content: string, specId: string, filePath?: string): Task[] {
|
|
56
|
+
const tasks: Task[] = [];
|
|
57
|
+
let match;
|
|
58
|
+
let index = 0;
|
|
59
|
+
|
|
60
|
+
const taskRegex = /^[\s]*[-*]\s*\[([ xX])\]\s*(.+)$/gm;
|
|
61
|
+
|
|
62
|
+
while ((match = taskRegex.exec(content)) !== null) {
|
|
63
|
+
const isCompleted = match[1].toLowerCase() === 'x';
|
|
64
|
+
const title = match[2].trim();
|
|
65
|
+
|
|
66
|
+
// Try to extract priority from title (e.g., [HIGH] or [P1])
|
|
67
|
+
const priorityMatch = title.match(/\[(HIGH|MEDIUM|LOW|CRITICAL|P[0-3])\]/i);
|
|
68
|
+
let priority: Task['priority'] = 'medium';
|
|
69
|
+
let cleanTitle = title;
|
|
70
|
+
|
|
71
|
+
if (priorityMatch) {
|
|
72
|
+
const p = priorityMatch[1].toUpperCase();
|
|
73
|
+
if (p === 'HIGH' || p === 'P1') priority = 'high';
|
|
74
|
+
else if (p === 'CRITICAL' || p === 'P0') priority = 'critical';
|
|
75
|
+
else if (p === 'LOW' || p === 'P3') priority = 'low';
|
|
76
|
+
cleanTitle = title.replace(priorityMatch[0], '').trim();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Try to extract assigned agent from title (e.g., @Builder or @architect)
|
|
80
|
+
const agentMatch = cleanTitle.match(/@(strategist|architect|system-designer|builder|guardian|chronicler)/i);
|
|
81
|
+
let assignedAgent: string | undefined;
|
|
82
|
+
|
|
83
|
+
if (agentMatch) {
|
|
84
|
+
assignedAgent = agentMatch[1].toLowerCase();
|
|
85
|
+
cleanTitle = cleanTitle.replace(agentMatch[0], '').trim();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
tasks.push({
|
|
89
|
+
id: `${specId}-task-${index++}`,
|
|
90
|
+
specId,
|
|
91
|
+
filePath,
|
|
92
|
+
title: cleanTitle,
|
|
93
|
+
description: '',
|
|
94
|
+
status: isCompleted ? 'completed' : 'pending',
|
|
95
|
+
priority,
|
|
96
|
+
dependencies: [],
|
|
97
|
+
assignedAgent,
|
|
98
|
+
createdAt: new Date(),
|
|
99
|
+
completedAt: isCompleted ? new Date() : undefined,
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return tasks;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Extract sections from markdown
|
|
108
|
+
*/
|
|
109
|
+
export function extractSections(content: string): Map<string, string> {
|
|
110
|
+
const sections = new Map<string, string>();
|
|
111
|
+
const lines = content.split('\n');
|
|
112
|
+
let currentSection = 'intro';
|
|
113
|
+
let currentContent: string[] = [];
|
|
114
|
+
|
|
115
|
+
for (const line of lines) {
|
|
116
|
+
const sectionMatch = line.match(/^##\s+(.+)$/);
|
|
117
|
+
if (sectionMatch) {
|
|
118
|
+
// Save previous section
|
|
119
|
+
if (currentContent.length > 0) {
|
|
120
|
+
sections.set(currentSection, currentContent.join('\n').trim());
|
|
121
|
+
}
|
|
122
|
+
currentSection = sectionMatch[1].toLowerCase().replace(/\s+/g, '-');
|
|
123
|
+
currentContent = [];
|
|
124
|
+
} else {
|
|
125
|
+
currentContent.push(line);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Save last section
|
|
130
|
+
if (currentContent.length > 0) {
|
|
131
|
+
sections.set(currentSection, currentContent.join('\n').trim());
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return sections;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Parse a user story markdown file
|
|
139
|
+
*/
|
|
140
|
+
export function parseUserStory(
|
|
141
|
+
content: string,
|
|
142
|
+
filePath: string
|
|
143
|
+
): { spec: Spec; requirement: Requirement; tasks: Task[] } {
|
|
144
|
+
const { frontmatter, body } = parseFrontmatter(content);
|
|
145
|
+
const sections = extractSections(body);
|
|
146
|
+
|
|
147
|
+
const id = filePath.split('/').pop()?.replace('.md', '') || `story-${Date.now()}`;
|
|
148
|
+
|
|
149
|
+
// Extract title from first H1 or frontmatter
|
|
150
|
+
const titleMatch = body.match(/^#\s+(.+)$/m);
|
|
151
|
+
const title = frontmatter.title || titleMatch?.[1] || id;
|
|
152
|
+
|
|
153
|
+
// Extract description (first paragraph after title)
|
|
154
|
+
const descMatch = body.match(/^#\s+.+\n\n([\s\S]*?)(?=\n##|\n\n##|$)/);
|
|
155
|
+
const description = descMatch?.[1]?.trim() || '';
|
|
156
|
+
|
|
157
|
+
// Extract acceptance criteria
|
|
158
|
+
const acSection = sections.get('acceptance-criteria') || sections.get('criterios-de-aceitacao') || '';
|
|
159
|
+
const acceptanceCriteria = acSection
|
|
160
|
+
.split('\n')
|
|
161
|
+
.filter(line => line.match(/^[-*]\s+/))
|
|
162
|
+
.map(line => line.replace(/^[-*]\s+/, '').trim());
|
|
163
|
+
|
|
164
|
+
// Map priority
|
|
165
|
+
const priorityMap: Record<string, Requirement['priority']> = {
|
|
166
|
+
'must': 'must',
|
|
167
|
+
'should': 'should',
|
|
168
|
+
'could': 'could',
|
|
169
|
+
'wont': 'wont',
|
|
170
|
+
'alta': 'must',
|
|
171
|
+
'media': 'should',
|
|
172
|
+
'baixa': 'could',
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
// Map status
|
|
176
|
+
const statusMap: Record<string, Requirement['status']> = {
|
|
177
|
+
'draft': 'draft',
|
|
178
|
+
'rascunho': 'draft',
|
|
179
|
+
'approved': 'approved',
|
|
180
|
+
'aprovado': 'approved',
|
|
181
|
+
'implemented': 'implemented',
|
|
182
|
+
'implementado': 'implemented',
|
|
183
|
+
'done': 'implemented',
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
const spec: Spec = {
|
|
187
|
+
id,
|
|
188
|
+
name: title,
|
|
189
|
+
description,
|
|
190
|
+
phase: 'requirements',
|
|
191
|
+
status: statusMap[frontmatter.status?.toLowerCase() || ''] || 'draft',
|
|
192
|
+
createdAt: frontmatter.created ? new Date(frontmatter.created) : new Date(),
|
|
193
|
+
updatedAt: frontmatter.updated ? new Date(frontmatter.updated) : new Date(),
|
|
194
|
+
filePath,
|
|
195
|
+
};
|
|
196
|
+
|
|
197
|
+
const requirement: Requirement = {
|
|
198
|
+
id: `req-${id}`,
|
|
199
|
+
specId: id,
|
|
200
|
+
title,
|
|
201
|
+
description,
|
|
202
|
+
type: 'functional',
|
|
203
|
+
priority: priorityMap[frontmatter.priority?.toLowerCase() || ''] || 'should',
|
|
204
|
+
acceptanceCriteria,
|
|
205
|
+
status: spec.status,
|
|
206
|
+
filePath,
|
|
207
|
+
};
|
|
208
|
+
|
|
209
|
+
const tasks = extractTasks(body, id, filePath);
|
|
210
|
+
|
|
211
|
+
return { spec, requirement, tasks };
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Parse an ADR (Architecture Decision Record) markdown file
|
|
216
|
+
*/
|
|
217
|
+
export function parseADR(
|
|
218
|
+
content: string,
|
|
219
|
+
filePath: string
|
|
220
|
+
): { spec: Spec; decision: DesignDecision } {
|
|
221
|
+
const { frontmatter, body } = parseFrontmatter(content);
|
|
222
|
+
const sections = extractSections(body);
|
|
223
|
+
|
|
224
|
+
const id = filePath.split('/').pop()?.replace('.md', '') || `adr-${Date.now()}`;
|
|
225
|
+
|
|
226
|
+
// Extract title
|
|
227
|
+
const titleMatch = body.match(/^#\s+(.+)$/m);
|
|
228
|
+
const title = frontmatter.title || titleMatch?.[1] || id;
|
|
229
|
+
|
|
230
|
+
// Extract sections
|
|
231
|
+
const context = sections.get('context') || sections.get('contexto') || '';
|
|
232
|
+
const decision = sections.get('decision') || sections.get('decisao') || sections.get('decisão') || '';
|
|
233
|
+
const consequencesSection = sections.get('consequences') || sections.get('consequencias') || sections.get('consequências') || '';
|
|
234
|
+
const alternativesSection = sections.get('alternatives') || sections.get('alternativas') || '';
|
|
235
|
+
|
|
236
|
+
const consequences = consequencesSection
|
|
237
|
+
.split('\n')
|
|
238
|
+
.filter(line => line.match(/^[-*]\s+/))
|
|
239
|
+
.map(line => line.replace(/^[-*]\s+/, '').trim());
|
|
240
|
+
|
|
241
|
+
const alternatives = alternativesSection
|
|
242
|
+
.split('\n')
|
|
243
|
+
.filter(line => line.match(/^[-*]\s+/))
|
|
244
|
+
.map(line => line.replace(/^[-*]\s+/, '').trim());
|
|
245
|
+
|
|
246
|
+
// Map status
|
|
247
|
+
const statusMap: Record<string, DesignDecision['status']> = {
|
|
248
|
+
'proposed': 'proposed',
|
|
249
|
+
'proposto': 'proposed',
|
|
250
|
+
'accepted': 'accepted',
|
|
251
|
+
'aceito': 'accepted',
|
|
252
|
+
'deprecated': 'deprecated',
|
|
253
|
+
'deprecado': 'deprecated',
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
const spec: Spec = {
|
|
257
|
+
id,
|
|
258
|
+
name: title,
|
|
259
|
+
description: context.slice(0, 200),
|
|
260
|
+
phase: 'design',
|
|
261
|
+
status: statusMap[frontmatter.status?.toLowerCase() || ''] === 'accepted' ? 'approved' : 'draft',
|
|
262
|
+
createdAt: frontmatter.created ? new Date(frontmatter.created) : new Date(),
|
|
263
|
+
updatedAt: frontmatter.updated ? new Date(frontmatter.updated) : new Date(),
|
|
264
|
+
filePath,
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
const designDecision: DesignDecision = {
|
|
268
|
+
id: `design-${id}`,
|
|
269
|
+
specId: id,
|
|
270
|
+
title,
|
|
271
|
+
context,
|
|
272
|
+
decision,
|
|
273
|
+
consequences,
|
|
274
|
+
alternatives: alternatives.length > 0 ? alternatives : undefined,
|
|
275
|
+
status: statusMap[frontmatter.status?.toLowerCase() || ''] || 'proposed',
|
|
276
|
+
filePath,
|
|
277
|
+
};
|
|
278
|
+
|
|
279
|
+
return { spec, decision: designDecision };
|
|
280
|
+
}
|
|
281
|
+
|
|
282
|
+
/**
|
|
283
|
+
* Determine spec type from file path
|
|
284
|
+
*/
|
|
285
|
+
export function getSpecType(filePath: string): 'story' | 'adr' | 'spec' | 'unknown' {
|
|
286
|
+
const lowerPath = filePath.toLowerCase();
|
|
287
|
+
|
|
288
|
+
if (lowerPath.includes('/stories/') || lowerPath.includes('us-')) {
|
|
289
|
+
return 'story';
|
|
290
|
+
}
|
|
291
|
+
if (lowerPath.includes('/decisions/') || lowerPath.includes('adr-') || lowerPath.includes('/adr/')) {
|
|
292
|
+
return 'adr';
|
|
293
|
+
}
|
|
294
|
+
if (lowerPath.includes('/specs/') || lowerPath.includes('/planning/')) {
|
|
295
|
+
return 'spec';
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
return 'unknown';
|
|
299
|
+
}
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
import { create } from 'zustand';
|
|
2
|
+
import { persist } from 'zustand/middleware';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Autopilot Store - Terminal-based execution
|
|
6
|
+
* Agents run inside the terminal PTY with streaming output.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
export type AgentId = 'strategist' | 'architect' | 'system-designer' | 'builder' | 'guardian' | 'chronicler';
|
|
10
|
+
export type PhaseStatus = 'pending' | 'running' | 'completed' | 'failed' | 'skipped';
|
|
11
|
+
export type RunStatus = 'idle' | 'running' | 'completed' | 'failed';
|
|
12
|
+
|
|
13
|
+
export interface PhaseResult {
|
|
14
|
+
agent: AgentId;
|
|
15
|
+
name: string;
|
|
16
|
+
status: PhaseStatus;
|
|
17
|
+
output?: string;
|
|
18
|
+
error?: string;
|
|
19
|
+
duration?: number;
|
|
20
|
+
tasksCompleted?: string[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface AutopilotConfig {
|
|
24
|
+
phases: AgentId[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Default phases
|
|
28
|
+
export const DEFAULT_PHASES: { id: AgentId; name: string }[] = [
|
|
29
|
+
{ id: 'strategist', name: 'Planning' },
|
|
30
|
+
{ id: 'architect', name: 'Design' },
|
|
31
|
+
{ id: 'system-designer', name: 'System Design' },
|
|
32
|
+
{ id: 'builder', name: 'Implementation' },
|
|
33
|
+
{ id: 'guardian', name: 'Validation' },
|
|
34
|
+
{ id: 'chronicler', name: 'Documentation' },
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
export const DEFAULT_CONFIG: AutopilotConfig = {
|
|
38
|
+
phases: ['strategist', 'architect', 'system-designer', 'builder', 'guardian', 'chronicler'],
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
interface AutopilotState {
|
|
42
|
+
// State
|
|
43
|
+
status: RunStatus;
|
|
44
|
+
currentPhaseIndex: number;
|
|
45
|
+
phases: PhaseResult[];
|
|
46
|
+
error: string | null;
|
|
47
|
+
specId: string | null;
|
|
48
|
+
specTitle: string | null;
|
|
49
|
+
|
|
50
|
+
// Terminal session for autopilot execution
|
|
51
|
+
terminalSessionId: string | null;
|
|
52
|
+
|
|
53
|
+
// Config modal
|
|
54
|
+
isConfigModalOpen: boolean;
|
|
55
|
+
selectedSpecId: string | null;
|
|
56
|
+
selectedSpecTitle: string | null;
|
|
57
|
+
selectedSpecContent: string | null;
|
|
58
|
+
selectedSpecFilePath: string | null;
|
|
59
|
+
|
|
60
|
+
// Actions
|
|
61
|
+
openConfigModal: (specId: string, specTitle: string, specContent: string, specFilePath?: string) => void;
|
|
62
|
+
closeConfigModal: () => void;
|
|
63
|
+
setTerminalSessionId: (sessionId: string) => void;
|
|
64
|
+
startRun: (config: AutopilotConfig, projectPath: string) => Promise<void>;
|
|
65
|
+
abortRun: () => Promise<void>;
|
|
66
|
+
reset: () => void;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export const useAutopilotStore = create<AutopilotState>()(
|
|
70
|
+
persist(
|
|
71
|
+
(set, get) => ({
|
|
72
|
+
status: 'idle',
|
|
73
|
+
currentPhaseIndex: -1,
|
|
74
|
+
phases: [],
|
|
75
|
+
error: null,
|
|
76
|
+
specId: null,
|
|
77
|
+
specTitle: null,
|
|
78
|
+
terminalSessionId: null,
|
|
79
|
+
isConfigModalOpen: false,
|
|
80
|
+
selectedSpecId: null,
|
|
81
|
+
selectedSpecTitle: null,
|
|
82
|
+
selectedSpecContent: null,
|
|
83
|
+
selectedSpecFilePath: null,
|
|
84
|
+
|
|
85
|
+
openConfigModal: (specId, specTitle, specContent, specFilePath) => {
|
|
86
|
+
set({
|
|
87
|
+
isConfigModalOpen: true,
|
|
88
|
+
selectedSpecId: specId,
|
|
89
|
+
selectedSpecTitle: specTitle,
|
|
90
|
+
selectedSpecContent: specContent,
|
|
91
|
+
selectedSpecFilePath: specFilePath || null,
|
|
92
|
+
});
|
|
93
|
+
},
|
|
94
|
+
|
|
95
|
+
closeConfigModal: () => {
|
|
96
|
+
set({
|
|
97
|
+
isConfigModalOpen: false,
|
|
98
|
+
selectedSpecId: null,
|
|
99
|
+
selectedSpecTitle: null,
|
|
100
|
+
selectedSpecContent: null,
|
|
101
|
+
selectedSpecFilePath: null,
|
|
102
|
+
});
|
|
103
|
+
},
|
|
104
|
+
|
|
105
|
+
setTerminalSessionId: (sessionId) => {
|
|
106
|
+
set({ terminalSessionId: sessionId });
|
|
107
|
+
},
|
|
108
|
+
|
|
109
|
+
startRun: async (config, projectPath) => {
|
|
110
|
+
const { selectedSpecId, selectedSpecTitle, selectedSpecContent, selectedSpecFilePath, terminalSessionId } = get();
|
|
111
|
+
|
|
112
|
+
if (!selectedSpecId || !selectedSpecTitle || !selectedSpecContent) {
|
|
113
|
+
throw new Error('No spec selected');
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
if (!terminalSessionId) {
|
|
117
|
+
throw new Error('No terminal session available. Open the terminal first.');
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Initialize phases
|
|
121
|
+
const initialPhases: PhaseResult[] = config.phases.map((agentId) => {
|
|
122
|
+
const phaseInfo = DEFAULT_PHASES.find((p) => p.id === agentId);
|
|
123
|
+
return {
|
|
124
|
+
agent: agentId,
|
|
125
|
+
name: phaseInfo?.name || agentId,
|
|
126
|
+
status: 'pending',
|
|
127
|
+
};
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
set({
|
|
131
|
+
status: 'running',
|
|
132
|
+
currentPhaseIndex: 0,
|
|
133
|
+
phases: initialPhases,
|
|
134
|
+
error: null,
|
|
135
|
+
specId: selectedSpecId,
|
|
136
|
+
specTitle: selectedSpecTitle,
|
|
137
|
+
isConfigModalOpen: false,
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
// Execute phases sequentially via terminal
|
|
141
|
+
let previousOutputs: string[] = [];
|
|
142
|
+
|
|
143
|
+
for (let i = 0; i < config.phases.length; i++) {
|
|
144
|
+
// Check if aborted
|
|
145
|
+
if (get().status !== 'running') return;
|
|
146
|
+
|
|
147
|
+
const agentId = config.phases[i];
|
|
148
|
+
|
|
149
|
+
// Update current phase to running
|
|
150
|
+
set((state) => ({
|
|
151
|
+
currentPhaseIndex: i,
|
|
152
|
+
phases: state.phases.map((p, idx) =>
|
|
153
|
+
idx === i ? { ...p, status: 'running' } : p
|
|
154
|
+
),
|
|
155
|
+
}));
|
|
156
|
+
|
|
157
|
+
const startTime = Date.now();
|
|
158
|
+
|
|
159
|
+
try {
|
|
160
|
+
const response = await fetch('/api/autopilot/terminal-execute', {
|
|
161
|
+
method: 'POST',
|
|
162
|
+
headers: { 'Content-Type': 'application/json' },
|
|
163
|
+
body: JSON.stringify({
|
|
164
|
+
sessionId: terminalSessionId,
|
|
165
|
+
agent: agentId,
|
|
166
|
+
specContent: selectedSpecContent,
|
|
167
|
+
specFilePath: selectedSpecFilePath,
|
|
168
|
+
previousOutputs,
|
|
169
|
+
projectPath,
|
|
170
|
+
}),
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
if (!response.ok) {
|
|
174
|
+
const errorData = await response.json().catch(() => ({ error: 'Unknown error' }));
|
|
175
|
+
throw new Error(errorData.error || `Phase ${agentId} failed`);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const result = await response.json();
|
|
179
|
+
const duration = Date.now() - startTime;
|
|
180
|
+
|
|
181
|
+
if (!result.success) {
|
|
182
|
+
throw new Error(result.error || `Phase ${agentId} failed`);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
previousOutputs.push(result.output || '');
|
|
186
|
+
|
|
187
|
+
// Update phase as completed
|
|
188
|
+
set((state) => ({
|
|
189
|
+
phases: state.phases.map((p, idx) =>
|
|
190
|
+
idx === i
|
|
191
|
+
? {
|
|
192
|
+
...p,
|
|
193
|
+
status: 'completed',
|
|
194
|
+
output: result.output,
|
|
195
|
+
duration,
|
|
196
|
+
tasksCompleted: result.tasksCompleted || [],
|
|
197
|
+
}
|
|
198
|
+
: p
|
|
199
|
+
),
|
|
200
|
+
}));
|
|
201
|
+
|
|
202
|
+
} catch (error) {
|
|
203
|
+
const duration = Date.now() - startTime;
|
|
204
|
+
const errorMessage = error instanceof Error ? error.message : 'Unknown error';
|
|
205
|
+
|
|
206
|
+
// Update phase as failed
|
|
207
|
+
set((state) => ({
|
|
208
|
+
status: 'failed',
|
|
209
|
+
error: errorMessage,
|
|
210
|
+
phases: state.phases.map((p, idx) =>
|
|
211
|
+
idx === i
|
|
212
|
+
? { ...p, status: 'failed', error: errorMessage, duration }
|
|
213
|
+
: idx > i
|
|
214
|
+
? { ...p, status: 'skipped' }
|
|
215
|
+
: p
|
|
216
|
+
),
|
|
217
|
+
}));
|
|
218
|
+
|
|
219
|
+
return; // Stop execution
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// All phases completed
|
|
224
|
+
set({ status: 'completed' });
|
|
225
|
+
},
|
|
226
|
+
|
|
227
|
+
abortRun: async () => {
|
|
228
|
+
const { terminalSessionId, currentPhaseIndex } = get();
|
|
229
|
+
|
|
230
|
+
// Send Ctrl+C to the terminal to interrupt the current command
|
|
231
|
+
if (terminalSessionId) {
|
|
232
|
+
try {
|
|
233
|
+
await fetch('/api/terminal', {
|
|
234
|
+
method: 'POST',
|
|
235
|
+
headers: { 'Content-Type': 'application/json' },
|
|
236
|
+
body: JSON.stringify({
|
|
237
|
+
action: 'write',
|
|
238
|
+
sessionId: terminalSessionId,
|
|
239
|
+
data: '\x03', // Ctrl+C
|
|
240
|
+
}),
|
|
241
|
+
});
|
|
242
|
+
} catch {
|
|
243
|
+
// Ignore errors
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
// Mark current phase as failed, remaining as skipped
|
|
248
|
+
set((state) => ({
|
|
249
|
+
status: 'failed',
|
|
250
|
+
error: 'Aborted by user',
|
|
251
|
+
phases: state.phases.map((p, idx) =>
|
|
252
|
+
idx === currentPhaseIndex
|
|
253
|
+
? { ...p, status: 'failed', error: 'Aborted' }
|
|
254
|
+
: idx > currentPhaseIndex
|
|
255
|
+
? { ...p, status: 'skipped' }
|
|
256
|
+
: p
|
|
257
|
+
),
|
|
258
|
+
}));
|
|
259
|
+
},
|
|
260
|
+
|
|
261
|
+
reset: () => {
|
|
262
|
+
set({
|
|
263
|
+
status: 'idle',
|
|
264
|
+
currentPhaseIndex: -1,
|
|
265
|
+
phases: [],
|
|
266
|
+
error: null,
|
|
267
|
+
specId: null,
|
|
268
|
+
specTitle: null,
|
|
269
|
+
terminalSessionId: null,
|
|
270
|
+
isConfigModalOpen: false,
|
|
271
|
+
selectedSpecId: null,
|
|
272
|
+
selectedSpecTitle: null,
|
|
273
|
+
selectedSpecContent: null,
|
|
274
|
+
selectedSpecFilePath: null,
|
|
275
|
+
});
|
|
276
|
+
},
|
|
277
|
+
}),
|
|
278
|
+
{
|
|
279
|
+
name: 'autopilot-storage',
|
|
280
|
+
partialize: (state) => ({
|
|
281
|
+
status: state.status,
|
|
282
|
+
phases: state.phases,
|
|
283
|
+
specId: state.specId,
|
|
284
|
+
specTitle: state.specTitle,
|
|
285
|
+
}),
|
|
286
|
+
}
|
|
287
|
+
)
|
|
288
|
+
);
|