maistro 1.0.390

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 (111) hide show
  1. package/LICENSE +15 -0
  2. package/README.md +107 -0
  3. package/dist/app.d.ts +247 -0
  4. package/dist/app.d.ts.map +1 -0
  5. package/dist/app.js +4971 -0
  6. package/dist/app.js.map +1 -0
  7. package/dist/buildInfo.d.ts +5 -0
  8. package/dist/buildInfo.d.ts.map +1 -0
  9. package/dist/buildInfo.js +2 -0
  10. package/dist/buildInfo.js.map +1 -0
  11. package/dist/caffeinate.d.ts +72 -0
  12. package/dist/caffeinate.d.ts.map +1 -0
  13. package/dist/caffeinate.js +258 -0
  14. package/dist/caffeinate.js.map +1 -0
  15. package/dist/claudePath.d.ts +10 -0
  16. package/dist/claudePath.d.ts.map +1 -0
  17. package/dist/claudePath.js +34 -0
  18. package/dist/claudePath.js.map +1 -0
  19. package/dist/clipboard.d.ts +44 -0
  20. package/dist/clipboard.d.ts.map +1 -0
  21. package/dist/clipboard.js +442 -0
  22. package/dist/clipboard.js.map +1 -0
  23. package/dist/config.d.ts +211 -0
  24. package/dist/config.d.ts.map +1 -0
  25. package/dist/config.js +933 -0
  26. package/dist/config.js.map +1 -0
  27. package/dist/constants.d.ts +50 -0
  28. package/dist/constants.d.ts.map +1 -0
  29. package/dist/constants.js +81 -0
  30. package/dist/constants.js.map +1 -0
  31. package/dist/contextBuilder.d.ts +38 -0
  32. package/dist/contextBuilder.d.ts.map +1 -0
  33. package/dist/contextBuilder.js +113 -0
  34. package/dist/contextBuilder.js.map +1 -0
  35. package/dist/dependencyDetector.d.ts +57 -0
  36. package/dist/dependencyDetector.d.ts.map +1 -0
  37. package/dist/dependencyDetector.js +505 -0
  38. package/dist/dependencyDetector.js.map +1 -0
  39. package/dist/executor.d.ts +83 -0
  40. package/dist/executor.d.ts.map +1 -0
  41. package/dist/executor.js +583 -0
  42. package/dist/executor.js.map +1 -0
  43. package/dist/git.d.ts +85 -0
  44. package/dist/git.d.ts.map +1 -0
  45. package/dist/git.js +283 -0
  46. package/dist/git.js.map +1 -0
  47. package/dist/imageManager.d.ts +161 -0
  48. package/dist/imageManager.d.ts.map +1 -0
  49. package/dist/imageManager.js +674 -0
  50. package/dist/imageManager.js.map +1 -0
  51. package/dist/index.d.ts +3 -0
  52. package/dist/index.d.ts.map +1 -0
  53. package/dist/index.js +437 -0
  54. package/dist/index.js.map +1 -0
  55. package/dist/input-visual-test.d.ts +9 -0
  56. package/dist/input-visual-test.d.ts.map +1 -0
  57. package/dist/input-visual-test.js +108 -0
  58. package/dist/input-visual-test.js.map +1 -0
  59. package/dist/inputBox.d.ts +228 -0
  60. package/dist/inputBox.d.ts.map +1 -0
  61. package/dist/inputBox.js +966 -0
  62. package/dist/inputBox.js.map +1 -0
  63. package/dist/logger.d.ts +136 -0
  64. package/dist/logger.d.ts.map +1 -0
  65. package/dist/logger.js +347 -0
  66. package/dist/logger.js.map +1 -0
  67. package/dist/orchestrator.d.ts +149 -0
  68. package/dist/orchestrator.d.ts.map +1 -0
  69. package/dist/orchestrator.js +821 -0
  70. package/dist/orchestrator.js.map +1 -0
  71. package/dist/planner.d.ts +86 -0
  72. package/dist/planner.d.ts.map +1 -0
  73. package/dist/planner.js +830 -0
  74. package/dist/planner.js.map +1 -0
  75. package/dist/pty-test-runner.d.ts +87 -0
  76. package/dist/pty-test-runner.d.ts.map +1 -0
  77. package/dist/pty-test-runner.js +721 -0
  78. package/dist/pty-test-runner.js.map +1 -0
  79. package/dist/screen.d.ts +44 -0
  80. package/dist/screen.d.ts.map +1 -0
  81. package/dist/screen.js +152 -0
  82. package/dist/screen.js.map +1 -0
  83. package/dist/taskQueue.d.ts +70 -0
  84. package/dist/taskQueue.d.ts.map +1 -0
  85. package/dist/taskQueue.js +282 -0
  86. package/dist/taskQueue.js.map +1 -0
  87. package/dist/tui-test-harness.d.ts +216 -0
  88. package/dist/tui-test-harness.d.ts.map +1 -0
  89. package/dist/tui-test-harness.js +527 -0
  90. package/dist/tui-test-harness.js.map +1 -0
  91. package/dist/types.d.ts +257 -0
  92. package/dist/types.d.ts.map +1 -0
  93. package/dist/types.js +46 -0
  94. package/dist/types.js.map +1 -0
  95. package/dist/ui-visual-test.d.ts +15 -0
  96. package/dist/ui-visual-test.d.ts.map +1 -0
  97. package/dist/ui-visual-test.js +141 -0
  98. package/dist/ui-visual-test.js.map +1 -0
  99. package/dist/ui.d.ts +272 -0
  100. package/dist/ui.d.ts.map +1 -0
  101. package/dist/ui.js +1531 -0
  102. package/dist/ui.js.map +1 -0
  103. package/dist/validator.d.ts +53 -0
  104. package/dist/validator.d.ts.map +1 -0
  105. package/dist/validator.js +491 -0
  106. package/dist/validator.js.map +1 -0
  107. package/dist/versionCheck.d.ts +63 -0
  108. package/dist/versionCheck.d.ts.map +1 -0
  109. package/dist/versionCheck.js +261 -0
  110. package/dist/versionCheck.js.map +1 -0
  111. package/package.json +62 -0
@@ -0,0 +1,821 @@
1
+ import { DEFAULT_CONFIG, createProjectState } from './types.js';
2
+ import { loadState, saveState, getNextTask, updateTaskStatus, allTasksCompleted, getTaskSummary, resetInProgressTasks, } from './taskQueue.js';
3
+ import { decompose, validateTaskDependencies, sortTasksByDependency } from './planner.js';
4
+ import { buildContext } from './contextBuilder.js';
5
+ import { executeTask, executeTaskWithFeedback, isClaudeCodeAvailable, runClaudeInit } from './executor.js';
6
+ import { validate } from './validator.js';
7
+ import { commitAll, hasChanges, isGitRepo, initRepo, ensureMaistroInGitignore, stageAll, unstageAll } from './git.js';
8
+ import { extractImagePlaceholders } from './imageManager.js';
9
+ import { join } from 'node:path';
10
+ import { Logger } from './logger.js';
11
+ /**
12
+ * Extract a brief failure reason from a full error message
13
+ * Looks for common patterns like test failures, build errors, etc.
14
+ */
15
+ function extractFailureReason(error, failureType) {
16
+ if (!error) {
17
+ return failureType === 'execution' ? 'Execution failed' : 'Validation failed';
18
+ }
19
+ const errorLower = error.toLowerCase();
20
+ // API errors - check these first as they're from Claude Code/API issues
21
+ // These should be prioritized over other patterns
22
+ if (errorLower.includes('api error')) {
23
+ // Extract specific API error details
24
+ const apiMatch = error.match(/API Error:\s*(\d+)\s*(?:due to\s*)?([^\n.]+)?/i);
25
+ if (apiMatch) {
26
+ const code = apiMatch[1];
27
+ const reason = apiMatch[2]?.trim();
28
+ if (reason) {
29
+ return `API error ${code}: ${reason.slice(0, 30)}${reason.length > 30 ? '...' : ''}`;
30
+ }
31
+ return `API error ${code}`;
32
+ }
33
+ return 'API error';
34
+ }
35
+ // Rate limiting
36
+ if (errorLower.includes('rate limit') || errorLower.includes('too many requests') || errorLower.includes('concurrency')) {
37
+ return 'Rate limited';
38
+ }
39
+ // Authentication errors
40
+ if (errorLower.includes('unauthorized') || errorLower.includes('authentication') || errorLower.includes('auth error')) {
41
+ return 'Authentication error';
42
+ }
43
+ // Test failure patterns
44
+ if (errorLower.includes('test') && (errorLower.includes('fail') || errorLower.includes('error'))) {
45
+ // Try to extract test count
46
+ const testMatch = error.match(/(\d+)\s*(test|spec)s?\s*(fail|error)/i);
47
+ if (testMatch) {
48
+ return `${testMatch[1]} test${parseInt(testMatch[1]) > 1 ? 's' : ''} failed`;
49
+ }
50
+ return 'Test failures';
51
+ }
52
+ // Build/compile errors
53
+ if (errorLower.includes('build') && errorLower.includes('fail')) {
54
+ return 'Build failed';
55
+ }
56
+ if (errorLower.includes('compile') || errorLower.includes('compilation')) {
57
+ return 'Compilation error';
58
+ }
59
+ // TypeScript errors - be more specific to avoid matching prompt content
60
+ // Look for actual TypeScript compiler error patterns, not just the word "typescript"
61
+ const tsErrorPatterns = [
62
+ /ts\d{4,5}:/i, // TS error codes like TS2304:
63
+ /error ts\d+/i, // "error TS2304"
64
+ /\.tsx?:\d+:\d+.*error/i, // file.ts:10:5 - error
65
+ /type '.*' is not assignable/i, // common TS error message
66
+ /property '.*' does not exist/i, // common TS error message
67
+ /cannot find (?:module|name)/i, // common TS error message
68
+ /typescript.*error/i, // "TypeScript error" but as actual error
69
+ /tsc.*error/i, // tsc compiler error
70
+ ];
71
+ if (tsErrorPatterns.some(pattern => pattern.test(error))) {
72
+ return 'TypeScript error';
73
+ }
74
+ // Syntax errors
75
+ if (errorLower.includes('syntax error') || errorLower.includes('syntaxerror')) {
76
+ return 'Syntax error';
77
+ }
78
+ // Timeout
79
+ if (errorLower.includes('timeout') || errorLower.includes('timed out')) {
80
+ return 'Execution timeout';
81
+ }
82
+ // Exit code
83
+ const exitMatch = error.match(/exit(?:ed)?\s*(?:with)?\s*(?:code)?\s*(\d+)/i);
84
+ if (exitMatch && exitMatch[1] !== '0') {
85
+ return `Exit code ${exitMatch[1]}`;
86
+ }
87
+ // Crash
88
+ if (error.startsWith('Crash:')) {
89
+ return 'Process crashed';
90
+ }
91
+ // Default based on failure type
92
+ if (failureType === 'acceptance') {
93
+ return 'Failed validation';
94
+ }
95
+ // Truncate first line if nothing else matches
96
+ const firstLine = error.split('\n')[0].trim();
97
+ if (firstLine.length > 50) {
98
+ return firstLine.slice(0, 47) + '...';
99
+ }
100
+ return firstLine || 'Unknown error';
101
+ }
102
+ /**
103
+ * Add a retry attempt to the task's retry history
104
+ */
105
+ function addRetryAttempt(task, failureType, error, durationMs) {
106
+ const history = task.retryHistory || [];
107
+ const attempt = {
108
+ attempt: task.retryCount + 1,
109
+ timestamp: new Date().toISOString(),
110
+ failureType,
111
+ reason: extractFailureReason(error, failureType),
112
+ fullError: error,
113
+ durationMs,
114
+ };
115
+ return [...history, attempt];
116
+ }
117
+ /**
118
+ * Main orchestrator class
119
+ */
120
+ export class Orchestrator {
121
+ state = null;
122
+ config;
123
+ options;
124
+ logger;
125
+ constructor(options) {
126
+ this.options = options;
127
+ this.config = { ...DEFAULT_CONFIG, ...options.config };
128
+ this.logger = new Logger(options.projectPath);
129
+ }
130
+ log(message) {
131
+ this.options.onLog?.(message);
132
+ }
133
+ get statePath() {
134
+ return join(this.options.projectPath, this.config.statePath);
135
+ }
136
+ /**
137
+ * Decompose a goal into tasks (first phase of init)
138
+ * @param goal The project goal
139
+ * @param projectContext Optional context from discovery phase to inform decomposition
140
+ */
141
+ async decompose(goal, projectContext = '') {
142
+ // Check if Claude Code is available
143
+ const claudeAvailable = await isClaudeCodeAvailable();
144
+ if (!claudeAvailable) {
145
+ return { success: false, tasks: [], error: 'Claude Code CLI not found. Please install it first.' };
146
+ }
147
+ // Ensure git is initialized
148
+ const isRepo = await isGitRepo(this.options.projectPath);
149
+ if (!isRepo) {
150
+ this.log('Initializing git repository...');
151
+ const initResult = await initRepo(this.options.projectPath);
152
+ if (!initResult.success) {
153
+ return { success: false, tasks: [], error: `Failed to init git: ${initResult.error}` };
154
+ }
155
+ }
156
+ // Ensure .maistro is in .gitignore
157
+ this.log('Configuring .gitignore...');
158
+ const gitignoreResult = await ensureMaistroInGitignore(this.options.projectPath);
159
+ if (!gitignoreResult.success) {
160
+ // Log warning but don't fail - this is not critical
161
+ this.log(`Warning: Could not update .gitignore: ${gitignoreResult.error}`);
162
+ }
163
+ // Run Claude's /init command to create CLAUDE.md
164
+ this.log('Creating project context (CLAUDE.md)...');
165
+ const claudeInitResult = await runClaudeInit({ cwd: this.options.projectPath });
166
+ if (!claudeInitResult.success) {
167
+ // Log note but don't fail - decomposition can proceed without CLAUDE.md
168
+ this.log(`Note: Could not create CLAUDE.md: ${claudeInitResult.error}`);
169
+ }
170
+ // Decompose goal into tasks
171
+ this.log('Decomposing goal into tasks...');
172
+ const decomposeResult = await decompose(goal, projectContext, {
173
+ cwd: this.options.projectPath,
174
+ });
175
+ if (!decomposeResult.success) {
176
+ return { success: false, tasks: [], error: `Failed to decompose goal: ${decomposeResult.error}` };
177
+ }
178
+ return { success: true, tasks: decomposeResult.tasks };
179
+ }
180
+ /**
181
+ * Finalize project initialization with approved tasks
182
+ */
183
+ async finalize(goal, projectName, tasks) {
184
+ // Validate dependencies
185
+ const depValidation = validateTaskDependencies(tasks);
186
+ if (!depValidation.valid) {
187
+ return { success: false, error: `Invalid task dependencies: ${depValidation.error}` };
188
+ }
189
+ // Sort tasks by dependency
190
+ const sortedTasks = sortTasksByDependency(tasks);
191
+ // Create project state
192
+ this.state = createProjectState(goal, projectName, this.options.projectPath);
193
+ this.state.tasks = sortedTasks;
194
+ this.state.status = 'ready';
195
+ // Save state
196
+ await saveState(this.statePath, this.state);
197
+ this.log(`Created ${sortedTasks.length} tasks`);
198
+ return { success: true };
199
+ }
200
+ /**
201
+ * Initialize a new project with a goal (legacy method for backward compatibility)
202
+ */
203
+ async init(goal, projectName) {
204
+ this.log(`Initializing project: ${projectName}`);
205
+ const decomposeResult = await this.decompose(goal);
206
+ if (!decomposeResult.success) {
207
+ return { success: false, error: decomposeResult.error };
208
+ }
209
+ return this.finalize(goal, projectName, decomposeResult.tasks);
210
+ }
211
+ /**
212
+ * Update the task list for an existing project
213
+ */
214
+ async updatePlan(tasks) {
215
+ if (!this.state) {
216
+ const loadResult = await this.load();
217
+ if (!loadResult.success) {
218
+ return { success: false, error: loadResult.error };
219
+ }
220
+ }
221
+ const state = this.state;
222
+ // Validate dependencies
223
+ const depValidation = validateTaskDependencies(tasks);
224
+ if (!depValidation.valid) {
225
+ return { success: false, error: `Invalid task dependencies: ${depValidation.error}` };
226
+ }
227
+ // Sort tasks by dependency
228
+ const sortedTasks = sortTasksByDependency(tasks);
229
+ // Update state
230
+ state.tasks = sortedTasks;
231
+ state.status = 'ready';
232
+ state.updatedAt = new Date().toISOString();
233
+ // Save state
234
+ await saveState(this.statePath, state);
235
+ this.log(`Updated plan with ${sortedTasks.length} tasks`);
236
+ return { success: true };
237
+ }
238
+ /**
239
+ * Get the current goal
240
+ */
241
+ async getGoal() {
242
+ if (!this.state) {
243
+ await this.load();
244
+ }
245
+ return this.state?.goal ?? null;
246
+ }
247
+ /**
248
+ * Load existing project state
249
+ */
250
+ async load() {
251
+ this.state = await loadState(this.statePath);
252
+ if (!this.state) {
253
+ return { success: false, error: 'No project found. Run init first.' };
254
+ }
255
+ return { success: true };
256
+ }
257
+ /**
258
+ * Sleep helper for retry delays
259
+ */
260
+ async sleep(ms) {
261
+ return new Promise(resolve => setTimeout(resolve, ms));
262
+ }
263
+ /**
264
+ * Check if a task should block execution based on its ifFailed setting
265
+ */
266
+ shouldBlockExecution(task) {
267
+ // Only block if task failed, exceeded retries, and has 'stop' ifFailed setting
268
+ return task.status === 'failed' &&
269
+ task.retryCount >= task.maxRetries &&
270
+ task.ifFailed === 'stop';
271
+ }
272
+ /**
273
+ * Run the orchestration loop
274
+ */
275
+ async run() {
276
+ this.logger.session('start');
277
+ this.logger.info('Starting orchestration run', { projectPath: this.options.projectPath });
278
+ if (!this.state) {
279
+ const loadResult = await this.load();
280
+ if (!loadResult.success) {
281
+ this.logger.error('Failed to load project state', { error: loadResult.error });
282
+ return {
283
+ success: false,
284
+ completedTasks: 0,
285
+ totalTasks: 0,
286
+ error: loadResult.error,
287
+ };
288
+ }
289
+ }
290
+ const state = this.state;
291
+ const totalTasks = state.tasks.length;
292
+ this.logger.info('Project loaded', { goal: state.goal, totalTasks });
293
+ // Reset any in_progress tasks to pending (from interrupted sessions)
294
+ const inProgressCount = state.tasks.filter(t => t.status === 'in_progress').length;
295
+ if (inProgressCount > 0) {
296
+ this.log(`Resetting ${inProgressCount} interrupted task(s) to pending`);
297
+ this.logger.info('Resetting interrupted tasks', { count: inProgressCount });
298
+ state.tasks = resetInProgressTasks(state.tasks);
299
+ await saveState(this.statePath, state);
300
+ }
301
+ // Check for tasks that should block execution (failed with ifFailed='stop')
302
+ const blockingTask = state.tasks.find(t => this.shouldBlockExecution(t));
303
+ if (blockingTask) {
304
+ return {
305
+ success: false,
306
+ completedTasks: getTaskSummary(state.tasks).completed,
307
+ totalTasks,
308
+ error: `Task "${blockingTask.title}" has failed and is marked as critical (ifFailed=stop)`,
309
+ blockedTask: blockingTask,
310
+ };
311
+ }
312
+ // Check if already complete
313
+ if (allTasksCompleted(state.tasks)) {
314
+ return {
315
+ success: true,
316
+ completedTasks: totalTasks,
317
+ totalTasks,
318
+ };
319
+ }
320
+ state.status = 'executing';
321
+ await saveState(this.statePath, state);
322
+ // Main execution loop
323
+ let task;
324
+ const firstTask = getNextTask(state.tasks);
325
+ this.logger.debug('Orchestrator.run() first task check', {
326
+ firstTaskId: firstTask?.id,
327
+ firstTaskTitle: firstTask?.title,
328
+ taskStatuses: state.tasks.map(t => ({ id: t.id, status: t.status, deps: t.dependencies })),
329
+ });
330
+ while ((task = getNextTask(state.tasks)) !== null) {
331
+ // Check if user requested stop (e.g., pressed Escape)
332
+ if (this.options.shouldStop?.()) {
333
+ this.log('Execution paused by user');
334
+ state.status = 'paused';
335
+ await saveState(this.statePath, state);
336
+ return {
337
+ success: false,
338
+ completedTasks: getTaskSummary(state.tasks).completed,
339
+ totalTasks,
340
+ error: 'Execution paused by user',
341
+ };
342
+ }
343
+ const result = await this.executeOneTask(task);
344
+ if (!result.success) {
345
+ // Check if this was a user-requested abort
346
+ if (result.error === 'Execution aborted by user') {
347
+ this.log('Execution paused by user');
348
+ state.status = 'paused';
349
+ await saveState(this.statePath, state);
350
+ return {
351
+ success: false,
352
+ completedTasks: getTaskSummary(state.tasks).completed,
353
+ totalTasks,
354
+ error: 'Execution paused by user',
355
+ };
356
+ }
357
+ // Get the updated task from state
358
+ const updatedTask = state.tasks.find(t => t.id === task.id);
359
+ if (!updatedTask)
360
+ continue;
361
+ // Handle based on ifFailed setting and retry count
362
+ if (updatedTask.retryCount >= updatedTask.maxRetries) {
363
+ // Exhausted all retries
364
+ switch (updatedTask.ifFailed) {
365
+ case 'stop':
366
+ // Critical task - stop execution
367
+ this.log(`Critical task "${task.title}" failed after ${updatedTask.retryCount} retries - stopping execution`);
368
+ state.status = 'paused';
369
+ await saveState(this.statePath, state);
370
+ return {
371
+ success: false,
372
+ completedTasks: getTaskSummary(state.tasks).completed,
373
+ totalTasks,
374
+ error: `Critical task "${task.title}" failed after ${updatedTask.retryCount} retries`,
375
+ blockedTask: updatedTask,
376
+ };
377
+ case 'skip':
378
+ // Non-critical task - skip and continue (preserve failure info)
379
+ this.log(`Task "${task.title}" failed after ${updatedTask.retryCount} retries - skipping (ifFailed=skip)`);
380
+ state.tasks = updateTaskStatus(state.tasks, task.id, 'skipped', updatedTask.error);
381
+ // Preserve failure reason and retry history when skipping
382
+ state.tasks = state.tasks.map(t => t.id === task.id ? {
383
+ ...t,
384
+ failureReason: updatedTask.failureReason,
385
+ failureType: updatedTask.failureType,
386
+ retryHistory: updatedTask.retryHistory,
387
+ } : t);
388
+ await saveState(this.statePath, state);
389
+ break;
390
+ case 'retry':
391
+ // Keep retrying indefinitely (reset retry count)
392
+ this.log(`Task "${task.title}" failed - will keep retrying (ifFailed=retry)`);
393
+ state.tasks = state.tasks.map(t => t.id === task.id
394
+ ? { ...t, status: 'pending', retryCount: 0 }
395
+ : t);
396
+ await saveState(this.statePath, state);
397
+ break;
398
+ }
399
+ }
400
+ else {
401
+ // Still have retries left - schedule retry with delay
402
+ const retryDelayMs = this.config.retryDelayMs;
403
+ const nextAttempt = updatedTask.retryCount + 1;
404
+ this.log(`Task "${task.title}" failed (attempt ${updatedTask.retryCount}/${updatedTask.maxRetries}) - retrying in ${retryDelayMs / 1000}s`);
405
+ this.options.onRetry?.(updatedTask, nextAttempt, updatedTask.maxRetries, retryDelayMs);
406
+ // Reset task to pending for retry, preserving error and failureType for feedback in next attempt
407
+ state.tasks = state.tasks.map(t => t.id === task.id
408
+ ? { ...t, status: 'pending', error: updatedTask.error, failureType: updatedTask.failureType }
409
+ : t);
410
+ await saveState(this.statePath, state);
411
+ // Wait before retry
412
+ await this.sleep(retryDelayMs);
413
+ }
414
+ }
415
+ // Report progress
416
+ const summary = getTaskSummary(state.tasks);
417
+ this.options.onProgress?.(summary.completed, totalTasks);
418
+ }
419
+ // Check if all tasks actually completed
420
+ const finalSummary = getTaskSummary(state.tasks);
421
+ this.logger.debug('Orchestrator.run() after main loop', {
422
+ completed: finalSummary.completed,
423
+ failed: finalSummary.failed,
424
+ pending: finalSummary.pending,
425
+ taskStatuses: state.tasks.map(t => ({ id: t.id, status: t.status })),
426
+ });
427
+ if (allTasksCompleted(state.tasks)) {
428
+ state.status = 'completed';
429
+ await saveState(this.statePath, state);
430
+ return {
431
+ success: true,
432
+ completedTasks: finalSummary.completed,
433
+ totalTasks,
434
+ };
435
+ }
436
+ // Some tasks failed - find if any are blocking (ifFailed=stop)
437
+ const stoppedTask = state.tasks.find(t => this.shouldBlockExecution(t));
438
+ if (stoppedTask) {
439
+ state.status = 'paused';
440
+ await saveState(this.statePath, state);
441
+ return {
442
+ success: false,
443
+ completedTasks: finalSummary.completed,
444
+ totalTasks,
445
+ error: `Critical task "${stoppedTask.title}" failed: ${stoppedTask.error || 'Unknown error'}`,
446
+ blockedTask: stoppedTask,
447
+ };
448
+ }
449
+ // No blocking tasks - consider it complete (skipped tasks are OK)
450
+ state.status = 'completed';
451
+ await saveState(this.statePath, state);
452
+ return {
453
+ success: true,
454
+ completedTasks: finalSummary.completed,
455
+ totalTasks,
456
+ };
457
+ }
458
+ /**
459
+ * Build project context for task execution
460
+ */
461
+ buildProjectContext(task) {
462
+ const state = this.state;
463
+ const taskIndex = state.tasks.findIndex(t => t.id === task.id);
464
+ const completedTasks = state.tasks
465
+ .filter(t => t.status === 'completed')
466
+ .map(t => ({ title: t.title, description: t.description }));
467
+ const pendingTasks = state.tasks
468
+ .filter(t => t.status === 'pending' && t.id !== task.id)
469
+ .map(t => ({ title: t.title, description: t.description }));
470
+ return {
471
+ goal: state.goal,
472
+ projectName: state.projectName,
473
+ completedTasks,
474
+ pendingTasks,
475
+ currentTaskIndex: taskIndex,
476
+ totalTasks: state.tasks.length,
477
+ };
478
+ }
479
+ /**
480
+ * Execute a single task with robust error handling
481
+ */
482
+ async executeOneTask(task) {
483
+ const state = this.state;
484
+ const taskIndex = state.tasks.findIndex(t => t.id === task.id);
485
+ const totalTasks = state.tasks.length;
486
+ this.log(`Starting task: ${task.title} (attempt ${task.retryCount + 1}/${task.maxRetries})`);
487
+ this.logger.taskStart(task.id, task.title, taskIndex, totalTasks);
488
+ this.logger.taskPhase(task.id, 'executing');
489
+ this.options.onTaskStart?.(task);
490
+ const taskStartTime = Date.now();
491
+ // Wrap onOutput to also log to task-specific log
492
+ const wrappedOnOutput = (line) => {
493
+ this.logger.taskOutput(task.id, line);
494
+ this.options.onOutput?.(line);
495
+ };
496
+ // Extract image references from task description
497
+ const imageRefs = extractImagePlaceholders(task.description);
498
+ if (imageRefs.length > 0) {
499
+ // Update task with extracted image references
500
+ task.imageRefs = imageRefs;
501
+ state.tasks = state.tasks.map(t => t.id === task.id ? { ...t, imageRefs } : t);
502
+ this.logger.info('Extracted image references', { taskId: task.id, imageRefs });
503
+ }
504
+ // Mark task as in progress
505
+ state.tasks = updateTaskStatus(state.tasks, task.id, 'in_progress');
506
+ await saveState(this.statePath, state);
507
+ // Use per-task timeout, falling back to config default
508
+ const taskTimeout = task.timeout || this.config.executionTimeout;
509
+ // Capture result summary for successful tasks (set inside try block)
510
+ let resultSummary;
511
+ try {
512
+ // Build context (file paths only - Claude Code reads content itself)
513
+ const context = await buildContext(task, this.options.projectPath);
514
+ this.log(`Context: ${context.fileCount} files`);
515
+ this.logger.info('Context built', { taskId: task.id, fileCount: context.fileCount });
516
+ // Build project context for full awareness
517
+ const projectContext = this.buildProjectContext(task);
518
+ // Execute task with per-task timeout
519
+ let execResult;
520
+ // Get image metadata for this task
521
+ const imageMetadata = state.images;
522
+ if (task.retryCount > 0 && task.error) {
523
+ // Retry with feedback about previous error
524
+ execResult = await executeTaskWithFeedback(task, context, task.error, {
525
+ cwd: this.options.projectPath,
526
+ timeout: taskTimeout,
527
+ onOutput: wrappedOnOutput,
528
+ shouldAbort: this.options.shouldStop,
529
+ }, projectContext, imageMetadata);
530
+ }
531
+ else {
532
+ execResult = await executeTask(task, context, {
533
+ cwd: this.options.projectPath,
534
+ timeout: taskTimeout,
535
+ onOutput: wrappedOnOutput,
536
+ shouldAbort: this.options.shouldStop,
537
+ }, projectContext, imageMetadata);
538
+ }
539
+ if (!execResult.success) {
540
+ // Check if this was a user-requested abort (exitCode -2)
541
+ if (execResult.exitCode === -2) {
542
+ this.log(`Task execution aborted by user`);
543
+ this.logger.info('Task execution aborted by user', { taskId: task.id });
544
+ // Reset task to pending so it can be resumed later
545
+ state.tasks = updateTaskStatus(state.tasks, task.id, 'pending');
546
+ await saveState(this.statePath, state);
547
+ // Return special error to indicate abort (not a real failure)
548
+ return { success: false, error: 'Execution aborted by user' };
549
+ }
550
+ this.log(`Task execution failed: ${execResult.error}`);
551
+ this.logger.error('Task execution failed', { taskId: task.id, error: execResult.error, exitCode: execResult.exitCode });
552
+ state.tasks = updateTaskStatus(state.tasks, task.id, 'failed', execResult.error);
553
+ // Save duration, failure type, reason, and retry history
554
+ const taskDuration = Date.now() - taskStartTime;
555
+ const failureType = 'execution';
556
+ const failureReason = extractFailureReason(execResult.error, failureType);
557
+ const currentTask = state.tasks.find(t => t.id === task.id);
558
+ const retryHistory = addRetryAttempt(currentTask, failureType, execResult.error, taskDuration);
559
+ state.tasks = state.tasks.map(t => t.id === task.id ? {
560
+ ...t,
561
+ durationMs: taskDuration,
562
+ failureType,
563
+ failureReason,
564
+ retryHistory,
565
+ } : t);
566
+ await saveState(this.statePath, state);
567
+ this.logger.taskComplete(task.id, task.title, false, taskDuration, execResult.error);
568
+ this.options.onTaskComplete?.(task, false);
569
+ return { success: false, error: execResult.error };
570
+ }
571
+ this.logger.info('Task execution succeeded', { taskId: task.id, exitCode: execResult.exitCode });
572
+ // Stage all changes so git diff includes new files
573
+ const stageResult = await stageAll(this.options.projectPath);
574
+ if (!stageResult.success) {
575
+ this.logger.warn('Failed to stage changes', { taskId: task.id, error: stageResult.error });
576
+ }
577
+ // Validate using staged diff (includes new files)
578
+ this.log('Validating changes...');
579
+ this.logger.taskPhase(task.id, 'validating');
580
+ const validationResult = await validate(task, {
581
+ cwd: this.options.projectPath,
582
+ useAIReview: this.config.useAIReview,
583
+ useStagedDiff: true,
584
+ });
585
+ this.logger.validation(task.id, validationResult.passed, validationResult.details);
586
+ // Store acceptance criteria results regardless of pass/fail (for visibility)
587
+ if (validationResult.acceptanceCriteriaResults) {
588
+ state.tasks = state.tasks.map(t => t.id === task.id ? {
589
+ ...t,
590
+ acceptanceCriteriaResults: validationResult.acceptanceCriteriaResults,
591
+ } : t);
592
+ // Log each criterion result to task log
593
+ for (const result of validationResult.acceptanceCriteriaResults) {
594
+ this.logger.taskAcceptanceCriteria(task.id, result.criterion, result.passed, result.explanation);
595
+ }
596
+ // Log which criteria failed for debugging
597
+ const failedCriteria = validationResult.acceptanceCriteriaResults.filter(r => !r.passed);
598
+ if (failedCriteria.length > 0) {
599
+ this.logger.info('Acceptance criteria failures', {
600
+ taskId: task.id,
601
+ failedCount: failedCriteria.length,
602
+ totalCount: validationResult.acceptanceCriteriaResults.length,
603
+ failed: failedCriteria.map(r => r.criterion),
604
+ });
605
+ }
606
+ }
607
+ if (!validationResult.passed) {
608
+ // Unstage changes so Claude Code can retry with clean state
609
+ await unstageAll(this.options.projectPath);
610
+ this.log(`Validation failed: ${validationResult.details}`);
611
+ state.tasks = updateTaskStatus(state.tasks, task.id, 'failed', validationResult.details);
612
+ // Save duration, failure type, reason, and retry history
613
+ const taskDuration = Date.now() - taskStartTime;
614
+ const failureType = 'acceptance';
615
+ const failureReason = extractFailureReason(validationResult.details, failureType);
616
+ const currentTask = state.tasks.find(t => t.id === task.id);
617
+ const retryHistory = addRetryAttempt(currentTask, failureType, validationResult.details, taskDuration);
618
+ state.tasks = state.tasks.map(t => t.id === task.id ? {
619
+ ...t,
620
+ durationMs: taskDuration,
621
+ failureType,
622
+ failureReason,
623
+ retryHistory,
624
+ } : t);
625
+ await saveState(this.statePath, state);
626
+ this.logger.taskComplete(task.id, task.title, false, taskDuration, validationResult.details);
627
+ this.options.onTaskComplete?.(task, false);
628
+ return { success: false, error: validationResult.details };
629
+ }
630
+ // Capture result summary for use after try/catch
631
+ resultSummary = validationResult.resultSummary;
632
+ }
633
+ catch (error) {
634
+ // Catch any unexpected errors (crashes, exceptions) to prevent maistro from crashing
635
+ const errorMessage = error instanceof Error ? error.message : String(error);
636
+ this.log(`Task crashed with error: ${errorMessage}`);
637
+ this.logger.error('Task crashed with exception', { taskId: task.id, error: errorMessage });
638
+ const crashError = `Crash: ${errorMessage}`;
639
+ state.tasks = updateTaskStatus(state.tasks, task.id, 'failed', crashError);
640
+ const taskDuration = Date.now() - taskStartTime;
641
+ const failureType = 'execution';
642
+ const failureReason = 'Process crashed';
643
+ const currentTask = state.tasks.find(t => t.id === task.id);
644
+ const retryHistory = addRetryAttempt(currentTask, failureType, crashError, taskDuration);
645
+ state.tasks = state.tasks.map(t => t.id === task.id ? {
646
+ ...t,
647
+ durationMs: taskDuration,
648
+ failureType,
649
+ failureReason,
650
+ retryHistory,
651
+ } : t);
652
+ await saveState(this.statePath, state);
653
+ this.logger.taskComplete(task.id, task.title, false, taskDuration, crashError);
654
+ this.options.onTaskComplete?.(task, false);
655
+ return { success: false, error: crashError };
656
+ }
657
+ // Commit changes
658
+ this.logger.taskPhase(task.id, 'committing');
659
+ if (await hasChanges(this.options.projectPath)) {
660
+ const commitResult = await commitAll(this.options.projectPath, `[maistro] ${task.title}`);
661
+ this.logger.git('commit', commitResult.success, commitResult.error);
662
+ if (!commitResult.success) {
663
+ this.log(`Git commit failed: ${commitResult.error}`);
664
+ // Don't fail the task for git errors
665
+ }
666
+ else {
667
+ this.log('Changes committed');
668
+ }
669
+ }
670
+ // Mark complete with duration
671
+ // Preserve retryHistory and retryCount so users can see if there was friction
672
+ const taskDuration = Date.now() - taskStartTime;
673
+ const currentTask = state.tasks.find(t => t.id === task.id);
674
+ state.tasks = updateTaskStatus(state.tasks, task.id, 'completed');
675
+ state.tasks = state.tasks.map(t => t.id === task.id ? {
676
+ ...t,
677
+ durationMs: taskDuration,
678
+ // Store result summary from validation
679
+ resultSummary,
680
+ // Preserve retry info to show friction history
681
+ retryHistory: currentTask.retryHistory,
682
+ retryCount: currentTask.retryCount,
683
+ // Clear transient error fields since task succeeded
684
+ error: undefined,
685
+ failureType: undefined,
686
+ failureReason: undefined,
687
+ } : t);
688
+ await saveState(this.statePath, state);
689
+ this.log(`Task completed: ${task.title}`);
690
+ this.logger.taskComplete(task.id, task.title, true, taskDuration);
691
+ this.options.onTaskComplete?.(task, true);
692
+ return { success: true };
693
+ }
694
+ /**
695
+ * Get current project status
696
+ */
697
+ async getStatus() {
698
+ if (!this.state) {
699
+ const loadResult = await this.load();
700
+ if (!loadResult.success) {
701
+ return null;
702
+ }
703
+ }
704
+ const state = this.state;
705
+ const summary = getTaskSummary(state.tasks);
706
+ const currentTask = state.tasks.find(t => t.status === 'in_progress');
707
+ return {
708
+ status: state.status,
709
+ goal: state.goal,
710
+ tasks: {
711
+ total: state.tasks.length,
712
+ completed: summary.completed,
713
+ pending: summary.pending,
714
+ failed: summary.failed,
715
+ inProgress: summary.in_progress,
716
+ skipped: summary.skipped,
717
+ },
718
+ currentTask,
719
+ };
720
+ }
721
+ /**
722
+ * Get all tasks
723
+ */
724
+ async getTasks() {
725
+ if (!this.state) {
726
+ await this.load();
727
+ }
728
+ return this.state?.tasks ?? [];
729
+ }
730
+ /**
731
+ * Skip a failed task
732
+ */
733
+ async skipTask(taskId) {
734
+ if (!this.state) {
735
+ const loadResult = await this.load();
736
+ if (!loadResult.success) {
737
+ return { success: false, error: loadResult.error };
738
+ }
739
+ }
740
+ const state = this.state;
741
+ const task = state.tasks.find(t => t.id === taskId);
742
+ if (!task) {
743
+ return { success: false, error: `Task ${taskId} not found` };
744
+ }
745
+ if (task.status !== 'failed') {
746
+ return { success: false, error: `Task ${taskId} is not failed` };
747
+ }
748
+ state.tasks = updateTaskStatus(state.tasks, taskId, 'skipped');
749
+ await saveState(this.statePath, state);
750
+ this.log(`Skipped task: ${task.title}`);
751
+ return { success: true };
752
+ }
753
+ /**
754
+ * Retry a failed task
755
+ */
756
+ async retryTask(taskId) {
757
+ if (!this.state) {
758
+ const loadResult = await this.load();
759
+ if (!loadResult.success) {
760
+ return { success: false, error: loadResult.error };
761
+ }
762
+ }
763
+ const state = this.state;
764
+ const task = state.tasks.find(t => t.id === taskId);
765
+ if (!task) {
766
+ return { success: false, error: `Task ${taskId} not found` };
767
+ }
768
+ // Reset task for retry
769
+ state.tasks = state.tasks.map(t => {
770
+ if (t.id !== taskId)
771
+ return t;
772
+ return {
773
+ ...t,
774
+ status: 'pending',
775
+ retryCount: 0,
776
+ error: undefined,
777
+ updatedAt: new Date().toISOString(),
778
+ };
779
+ });
780
+ await saveState(this.statePath, state);
781
+ this.log(`Reset task for retry: ${task.title}`);
782
+ return { success: true };
783
+ }
784
+ /**
785
+ * Clear the current plan (goal and all tasks) while preserving logs
786
+ * Resets the project to a clean state ready for a new goal
787
+ */
788
+ async clearPlan() {
789
+ if (!this.state) {
790
+ const loadResult = await this.load();
791
+ if (!loadResult.success) {
792
+ return { success: false, error: loadResult.error };
793
+ }
794
+ }
795
+ const state = this.state;
796
+ // Log the clear action
797
+ this.logger.info('Clearing plan', {
798
+ previousGoal: state.goal,
799
+ taskCount: state.tasks.length,
800
+ });
801
+ // Reset state while preserving project path and timestamps
802
+ state.goal = '';
803
+ state.projectName = '';
804
+ state.tasks = [];
805
+ state.currentTaskIndex = 0;
806
+ state.status = 'idle';
807
+ state.updatedAt = new Date().toISOString();
808
+ // Keep images - user may want to reuse them
809
+ // Keep inputHistory - preserves user's command history
810
+ await saveState(this.statePath, state);
811
+ this.log('Plan cleared');
812
+ return { success: true };
813
+ }
814
+ }
815
+ /**
816
+ * Create and initialize an orchestrator
817
+ */
818
+ export async function createOrchestrator(options) {
819
+ return new Orchestrator(options);
820
+ }
821
+ //# sourceMappingURL=orchestrator.js.map