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.
- package/LICENSE +15 -0
- package/README.md +107 -0
- package/dist/app.d.ts +247 -0
- package/dist/app.d.ts.map +1 -0
- package/dist/app.js +4971 -0
- package/dist/app.js.map +1 -0
- package/dist/buildInfo.d.ts +5 -0
- package/dist/buildInfo.d.ts.map +1 -0
- package/dist/buildInfo.js +2 -0
- package/dist/buildInfo.js.map +1 -0
- package/dist/caffeinate.d.ts +72 -0
- package/dist/caffeinate.d.ts.map +1 -0
- package/dist/caffeinate.js +258 -0
- package/dist/caffeinate.js.map +1 -0
- package/dist/claudePath.d.ts +10 -0
- package/dist/claudePath.d.ts.map +1 -0
- package/dist/claudePath.js +34 -0
- package/dist/claudePath.js.map +1 -0
- package/dist/clipboard.d.ts +44 -0
- package/dist/clipboard.d.ts.map +1 -0
- package/dist/clipboard.js +442 -0
- package/dist/clipboard.js.map +1 -0
- package/dist/config.d.ts +211 -0
- package/dist/config.d.ts.map +1 -0
- package/dist/config.js +933 -0
- package/dist/config.js.map +1 -0
- package/dist/constants.d.ts +50 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +81 -0
- package/dist/constants.js.map +1 -0
- package/dist/contextBuilder.d.ts +38 -0
- package/dist/contextBuilder.d.ts.map +1 -0
- package/dist/contextBuilder.js +113 -0
- package/dist/contextBuilder.js.map +1 -0
- package/dist/dependencyDetector.d.ts +57 -0
- package/dist/dependencyDetector.d.ts.map +1 -0
- package/dist/dependencyDetector.js +505 -0
- package/dist/dependencyDetector.js.map +1 -0
- package/dist/executor.d.ts +83 -0
- package/dist/executor.d.ts.map +1 -0
- package/dist/executor.js +583 -0
- package/dist/executor.js.map +1 -0
- package/dist/git.d.ts +85 -0
- package/dist/git.d.ts.map +1 -0
- package/dist/git.js +283 -0
- package/dist/git.js.map +1 -0
- package/dist/imageManager.d.ts +161 -0
- package/dist/imageManager.d.ts.map +1 -0
- package/dist/imageManager.js +674 -0
- package/dist/imageManager.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +437 -0
- package/dist/index.js.map +1 -0
- package/dist/input-visual-test.d.ts +9 -0
- package/dist/input-visual-test.d.ts.map +1 -0
- package/dist/input-visual-test.js +108 -0
- package/dist/input-visual-test.js.map +1 -0
- package/dist/inputBox.d.ts +228 -0
- package/dist/inputBox.d.ts.map +1 -0
- package/dist/inputBox.js +966 -0
- package/dist/inputBox.js.map +1 -0
- package/dist/logger.d.ts +136 -0
- package/dist/logger.d.ts.map +1 -0
- package/dist/logger.js +347 -0
- package/dist/logger.js.map +1 -0
- package/dist/orchestrator.d.ts +149 -0
- package/dist/orchestrator.d.ts.map +1 -0
- package/dist/orchestrator.js +821 -0
- package/dist/orchestrator.js.map +1 -0
- package/dist/planner.d.ts +86 -0
- package/dist/planner.d.ts.map +1 -0
- package/dist/planner.js +830 -0
- package/dist/planner.js.map +1 -0
- package/dist/pty-test-runner.d.ts +87 -0
- package/dist/pty-test-runner.d.ts.map +1 -0
- package/dist/pty-test-runner.js +721 -0
- package/dist/pty-test-runner.js.map +1 -0
- package/dist/screen.d.ts +44 -0
- package/dist/screen.d.ts.map +1 -0
- package/dist/screen.js +152 -0
- package/dist/screen.js.map +1 -0
- package/dist/taskQueue.d.ts +70 -0
- package/dist/taskQueue.d.ts.map +1 -0
- package/dist/taskQueue.js +282 -0
- package/dist/taskQueue.js.map +1 -0
- package/dist/tui-test-harness.d.ts +216 -0
- package/dist/tui-test-harness.d.ts.map +1 -0
- package/dist/tui-test-harness.js +527 -0
- package/dist/tui-test-harness.js.map +1 -0
- package/dist/types.d.ts +257 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +46 -0
- package/dist/types.js.map +1 -0
- package/dist/ui-visual-test.d.ts +15 -0
- package/dist/ui-visual-test.d.ts.map +1 -0
- package/dist/ui-visual-test.js +141 -0
- package/dist/ui-visual-test.js.map +1 -0
- package/dist/ui.d.ts +272 -0
- package/dist/ui.d.ts.map +1 -0
- package/dist/ui.js +1531 -0
- package/dist/ui.js.map +1 -0
- package/dist/validator.d.ts +53 -0
- package/dist/validator.d.ts.map +1 -0
- package/dist/validator.js +491 -0
- package/dist/validator.js.map +1 -0
- package/dist/versionCheck.d.ts +63 -0
- package/dist/versionCheck.d.ts.map +1 -0
- package/dist/versionCheck.js +261 -0
- package/dist/versionCheck.js.map +1 -0
- 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
|