@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.
Files changed (106) hide show
  1. package/.claude/commands/agents/architect.md +1162 -0
  2. package/.claude/commands/agents/architect.meta.yaml +124 -0
  3. package/.claude/commands/agents/builder.md +1432 -0
  4. package/.claude/commands/agents/builder.meta.yaml +117 -0
  5. package/.claude/commands/agents/chronicler.md +633 -0
  6. package/.claude/commands/agents/chronicler.meta.yaml +217 -0
  7. package/.claude/commands/agents/guardian.md +456 -0
  8. package/.claude/commands/agents/guardian.meta.yaml +127 -0
  9. package/.claude/commands/agents/strategist.md +483 -0
  10. package/.claude/commands/agents/strategist.meta.yaml +158 -0
  11. package/.claude/commands/agents/system-designer.md +1137 -0
  12. package/.claude/commands/agents/system-designer.meta.yaml +156 -0
  13. package/.claude/commands/devflow-help.md +93 -0
  14. package/.claude/commands/devflow-status.md +60 -0
  15. package/.claude/commands/quick/create-adr.md +82 -0
  16. package/.claude/commands/quick/new-feature.md +57 -0
  17. package/.claude/commands/quick/security-check.md +54 -0
  18. package/.claude/commands/quick/system-design.md +58 -0
  19. package/.claude_project +52 -0
  20. package/.devflow/agents/architect.meta.yaml +122 -0
  21. package/.devflow/agents/builder.meta.yaml +116 -0
  22. package/.devflow/agents/chronicler.meta.yaml +222 -0
  23. package/.devflow/agents/guardian.meta.yaml +127 -0
  24. package/.devflow/agents/strategist.meta.yaml +158 -0
  25. package/.devflow/agents/system-designer.meta.yaml +265 -0
  26. package/.devflow/project.yaml +242 -0
  27. package/.gitignore-template +84 -0
  28. package/LICENSE +21 -0
  29. package/README.md +249 -0
  30. package/bin/devflow.js +54 -0
  31. package/lib/autopilot.js +235 -0
  32. package/lib/autopilotConstants.js +213 -0
  33. package/lib/constants.js +95 -0
  34. package/lib/init.js +200 -0
  35. package/lib/update.js +181 -0
  36. package/lib/utils.js +157 -0
  37. package/lib/web.js +119 -0
  38. package/package.json +57 -0
  39. package/web/CHANGELOG.md +192 -0
  40. package/web/README.md +156 -0
  41. package/web/app/api/autopilot/execute/route.ts +102 -0
  42. package/web/app/api/autopilot/terminal-execute/route.ts +124 -0
  43. package/web/app/api/files/route.ts +280 -0
  44. package/web/app/api/files/tree/route.ts +160 -0
  45. package/web/app/api/git/route.ts +201 -0
  46. package/web/app/api/health/route.ts +94 -0
  47. package/web/app/api/project/open/route.ts +134 -0
  48. package/web/app/api/search/route.ts +247 -0
  49. package/web/app/api/specs/route.ts +405 -0
  50. package/web/app/api/terminal/route.ts +222 -0
  51. package/web/app/globals.css +160 -0
  52. package/web/app/ide/layout.tsx +43 -0
  53. package/web/app/ide/page.tsx +216 -0
  54. package/web/app/layout.tsx +34 -0
  55. package/web/app/page.tsx +303 -0
  56. package/web/components/agents/AgentIcons.tsx +281 -0
  57. package/web/components/autopilot/AutopilotConfigModal.tsx +245 -0
  58. package/web/components/autopilot/AutopilotPanel.tsx +299 -0
  59. package/web/components/dashboard/DashboardPanel.tsx +393 -0
  60. package/web/components/editor/Breadcrumbs.tsx +134 -0
  61. package/web/components/editor/EditorPanel.tsx +120 -0
  62. package/web/components/editor/EditorTabs.tsx +229 -0
  63. package/web/components/editor/MarkdownPreview.tsx +154 -0
  64. package/web/components/editor/MermaidDiagram.tsx +113 -0
  65. package/web/components/editor/MonacoEditor.tsx +177 -0
  66. package/web/components/editor/TabContextMenu.tsx +207 -0
  67. package/web/components/git/GitPanel.tsx +534 -0
  68. package/web/components/layout/Shell.tsx +15 -0
  69. package/web/components/layout/StatusBar.tsx +100 -0
  70. package/web/components/modals/CommandPalette.tsx +393 -0
  71. package/web/components/modals/GlobalSearch.tsx +348 -0
  72. package/web/components/modals/QuickOpen.tsx +241 -0
  73. package/web/components/modals/RecentFiles.tsx +208 -0
  74. package/web/components/projects/ProjectSelector.tsx +147 -0
  75. package/web/components/settings/SettingItem.tsx +150 -0
  76. package/web/components/settings/SettingsPanel.tsx +323 -0
  77. package/web/components/specs/SpecsPanel.tsx +1091 -0
  78. package/web/components/terminal/TerminalPanel.tsx +683 -0
  79. package/web/components/ui/ContextMenu.tsx +182 -0
  80. package/web/components/ui/LoadingSpinner.tsx +66 -0
  81. package/web/components/ui/ResizeHandle.tsx +110 -0
  82. package/web/components/ui/Skeleton.tsx +108 -0
  83. package/web/components/ui/SkipLinks.tsx +37 -0
  84. package/web/components/ui/Toaster.tsx +57 -0
  85. package/web/hooks/useFocusTrap.ts +141 -0
  86. package/web/hooks/useKeyboardShortcuts.ts +169 -0
  87. package/web/hooks/useListNavigation.ts +237 -0
  88. package/web/lib/autopilotConstants.ts +213 -0
  89. package/web/lib/constants/agents.ts +67 -0
  90. package/web/lib/git.ts +339 -0
  91. package/web/lib/ptyManager.ts +191 -0
  92. package/web/lib/specsParser.ts +299 -0
  93. package/web/lib/stores/autopilotStore.ts +288 -0
  94. package/web/lib/stores/fileStore.ts +550 -0
  95. package/web/lib/stores/gitStore.ts +386 -0
  96. package/web/lib/stores/projectStore.ts +196 -0
  97. package/web/lib/stores/settingsStore.ts +126 -0
  98. package/web/lib/stores/specsStore.ts +297 -0
  99. package/web/lib/stores/uiStore.ts +175 -0
  100. package/web/lib/types/index.ts +177 -0
  101. package/web/lib/utils.ts +98 -0
  102. package/web/next.config.js +50 -0
  103. package/web/package.json +54 -0
  104. package/web/postcss.config.js +6 -0
  105. package/web/tailwind.config.ts +68 -0
  106. 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
+ );