@litmers/cursorflow-orchestrator 0.1.39 → 0.2.2
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/CHANGELOG.md +0 -2
- package/README.md +20 -16
- package/commands/cursorflow-init.md +0 -4
- package/dist/cli/logs.js +108 -9
- package/dist/cli/logs.js.map +1 -1
- package/dist/cli/models.js +20 -3
- package/dist/cli/models.js.map +1 -1
- package/dist/cli/monitor.d.ts +7 -10
- package/dist/cli/monitor.js +1088 -1240
- package/dist/cli/monitor.js.map +1 -1
- package/dist/cli/prepare.js +0 -1
- package/dist/cli/prepare.js.map +1 -1
- package/dist/cli/resume.js +23 -5
- package/dist/cli/resume.js.map +1 -1
- package/dist/cli/run.js +28 -9
- package/dist/cli/run.js.map +1 -1
- package/dist/cli/signal.d.ts +6 -1
- package/dist/cli/signal.js +94 -12
- package/dist/cli/signal.js.map +1 -1
- package/dist/cli/tasks.js +3 -46
- package/dist/cli/tasks.js.map +1 -1
- package/dist/core/agent-supervisor.d.ts +23 -0
- package/dist/core/agent-supervisor.js +42 -0
- package/dist/core/agent-supervisor.js.map +1 -0
- package/dist/core/auto-recovery.d.ts +2 -1
- package/dist/core/auto-recovery.js +6 -1
- package/dist/core/auto-recovery.js.map +1 -1
- package/dist/core/failure-policy.d.ts +0 -1
- package/dist/core/failure-policy.js +0 -1
- package/dist/core/failure-policy.js.map +1 -1
- package/dist/core/git-lifecycle-manager.d.ts +284 -0
- package/dist/core/git-lifecycle-manager.js +778 -0
- package/dist/core/git-lifecycle-manager.js.map +1 -0
- package/dist/core/git-pipeline-coordinator.d.ts +21 -0
- package/dist/core/git-pipeline-coordinator.js +205 -0
- package/dist/core/git-pipeline-coordinator.js.map +1 -0
- package/dist/core/intervention.d.ts +176 -0
- package/dist/core/intervention.js +424 -0
- package/dist/core/intervention.js.map +1 -0
- package/dist/core/lane-state-machine.d.ts +423 -0
- package/dist/core/lane-state-machine.js +890 -0
- package/dist/core/lane-state-machine.js.map +1 -0
- package/dist/core/orchestrator.d.ts +4 -1
- package/dist/core/orchestrator.js +38 -63
- package/dist/core/orchestrator.js.map +1 -1
- package/dist/core/runner/agent.d.ts +7 -1
- package/dist/core/runner/agent.js +45 -30
- package/dist/core/runner/agent.js.map +1 -1
- package/dist/core/runner/pipeline.js +283 -109
- package/dist/core/runner/pipeline.js.map +1 -1
- package/dist/core/runner/task.d.ts +4 -5
- package/dist/core/runner/task.js +6 -77
- package/dist/core/runner/task.js.map +1 -1
- package/dist/core/runner.js +11 -2
- package/dist/core/runner.js.map +1 -1
- package/dist/core/stall-detection.d.ts +27 -4
- package/dist/core/stall-detection.js +116 -28
- package/dist/core/stall-detection.js.map +1 -1
- package/dist/hooks/contexts/index.d.ts +104 -0
- package/dist/hooks/contexts/index.js +134 -0
- package/dist/hooks/contexts/index.js.map +1 -0
- package/dist/hooks/data-accessor.d.ts +86 -0
- package/dist/hooks/data-accessor.js +410 -0
- package/dist/hooks/data-accessor.js.map +1 -0
- package/dist/hooks/flow-controller.d.ts +136 -0
- package/dist/hooks/flow-controller.js +351 -0
- package/dist/hooks/flow-controller.js.map +1 -0
- package/dist/hooks/index.d.ts +68 -0
- package/dist/hooks/index.js +105 -0
- package/dist/hooks/index.js.map +1 -0
- package/dist/hooks/manager.d.ts +129 -0
- package/dist/hooks/manager.js +389 -0
- package/dist/hooks/manager.js.map +1 -0
- package/dist/hooks/types.d.ts +463 -0
- package/dist/hooks/types.js +45 -0
- package/dist/hooks/types.js.map +1 -0
- package/dist/services/logging/buffer.d.ts +2 -2
- package/dist/services/logging/buffer.js +95 -42
- package/dist/services/logging/buffer.js.map +1 -1
- package/dist/services/logging/console.js +8 -5
- package/dist/services/logging/console.js.map +1 -1
- package/dist/services/logging/formatter.d.ts +9 -3
- package/dist/services/logging/formatter.js +64 -17
- package/dist/services/logging/formatter.js.map +1 -1
- package/dist/services/logging/index.d.ts +0 -1
- package/dist/services/logging/index.js +0 -1
- package/dist/services/logging/index.js.map +1 -1
- package/dist/services/logging/paths.d.ts +8 -0
- package/dist/services/logging/paths.js +48 -0
- package/dist/services/logging/paths.js.map +1 -0
- package/dist/services/logging/raw-log.d.ts +6 -0
- package/dist/services/logging/raw-log.js +37 -0
- package/dist/services/logging/raw-log.js.map +1 -0
- package/dist/services/process/index.js +1 -1
- package/dist/services/process/index.js.map +1 -1
- package/dist/types/agent.d.ts +15 -0
- package/dist/types/config.d.ts +24 -1
- package/dist/types/event-categories.d.ts +601 -0
- package/dist/types/event-categories.js +233 -0
- package/dist/types/event-categories.js.map +1 -0
- package/dist/types/events.d.ts +0 -20
- package/dist/types/flow.d.ts +10 -6
- package/dist/types/index.d.ts +1 -1
- package/dist/types/index.js +17 -3
- package/dist/types/index.js.map +1 -1
- package/dist/types/lane.d.ts +1 -1
- package/dist/types/logging.d.ts +1 -1
- package/dist/types/task.d.ts +13 -2
- package/dist/ui/log-viewer.d.ts +3 -0
- package/dist/ui/log-viewer.js +3 -0
- package/dist/ui/log-viewer.js.map +1 -1
- package/dist/utils/config.js +15 -1
- package/dist/utils/config.js.map +1 -1
- package/dist/utils/cursor-agent.d.ts +11 -1
- package/dist/utils/cursor-agent.js +63 -16
- package/dist/utils/cursor-agent.js.map +1 -1
- package/dist/utils/enhanced-logger.d.ts +5 -1
- package/dist/utils/enhanced-logger.js +99 -20
- package/dist/utils/enhanced-logger.js.map +1 -1
- package/dist/utils/event-registry.d.ts +222 -0
- package/dist/utils/event-registry.js +463 -0
- package/dist/utils/event-registry.js.map +1 -0
- package/dist/utils/events.d.ts +1 -13
- package/dist/utils/events.js.map +1 -1
- package/dist/utils/flow.d.ts +10 -0
- package/dist/utils/flow.js +75 -0
- package/dist/utils/flow.js.map +1 -1
- package/dist/utils/git.d.ts +12 -1
- package/dist/utils/git.js +54 -1
- package/dist/utils/git.js.map +1 -1
- package/dist/utils/log-constants.d.ts +1 -0
- package/dist/utils/log-constants.js +2 -1
- package/dist/utils/log-constants.js.map +1 -1
- package/dist/utils/log-formatter.d.ts +3 -2
- package/dist/utils/log-formatter.js +11 -11
- package/dist/utils/log-formatter.js.map +1 -1
- package/dist/utils/logger.d.ts +11 -0
- package/dist/utils/logger.js +82 -3
- package/dist/utils/logger.js.map +1 -1
- package/dist/utils/repro-thinking-logs.js +0 -13
- package/dist/utils/repro-thinking-logs.js.map +1 -1
- package/dist/utils/run-service.js +1 -1
- package/dist/utils/run-service.js.map +1 -1
- package/examples/README.md +0 -2
- package/examples/demo-project/README.md +1 -2
- package/package.json +18 -28
- package/scripts/setup-security.sh +0 -1
- package/scripts/test-log-parser.ts +171 -0
- package/scripts/verify-change.sh +272 -0
- package/src/cli/logs.ts +121 -10
- package/src/cli/models.ts +20 -3
- package/src/cli/monitor.ts +1257 -1342
- package/src/cli/prepare.ts +0 -1
- package/src/cli/resume.ts +29 -5
- package/src/cli/run.ts +29 -11
- package/src/cli/signal.ts +115 -17
- package/src/cli/tasks.ts +2 -59
- package/src/core/agent-supervisor.ts +64 -0
- package/src/core/auto-recovery.ts +7 -1
- package/src/core/failure-policy.ts +0 -1
- package/src/core/git-lifecycle-manager.ts +1011 -0
- package/src/core/git-pipeline-coordinator.ts +221 -0
- package/src/core/intervention.ts +481 -0
- package/src/core/lane-state-machine.ts +1097 -0
- package/src/core/orchestrator.ts +45 -62
- package/src/core/runner/agent.ts +66 -33
- package/src/core/runner/pipeline.ts +318 -122
- package/src/core/runner/task.ts +12 -93
- package/src/core/runner.ts +12 -2
- package/src/core/stall-detection.ts +145 -28
- package/src/hooks/contexts/index.ts +256 -0
- package/src/hooks/data-accessor.ts +488 -0
- package/src/hooks/flow-controller.ts +425 -0
- package/src/hooks/index.ts +154 -0
- package/src/hooks/manager.ts +434 -0
- package/src/hooks/types.ts +544 -0
- package/src/services/logging/buffer.ts +104 -43
- package/src/services/logging/console.ts +9 -5
- package/src/services/logging/formatter.ts +74 -17
- package/src/services/logging/index.ts +0 -2
- package/src/services/logging/paths.ts +14 -0
- package/src/services/logging/raw-log.ts +43 -0
- package/src/services/process/index.ts +1 -1
- package/src/types/agent.ts +15 -0
- package/src/types/config.ts +25 -1
- package/src/types/event-categories.ts +663 -0
- package/src/types/events.ts +0 -25
- package/src/types/flow.ts +10 -6
- package/src/types/index.ts +50 -4
- package/src/types/lane.ts +1 -2
- package/src/types/logging.ts +2 -1
- package/src/types/task.ts +13 -2
- package/src/ui/log-viewer.ts +3 -0
- package/src/utils/config.ts +17 -1
- package/src/utils/cursor-agent.ts +68 -16
- package/src/utils/enhanced-logger.ts +106 -20
- package/src/utils/event-registry.ts +595 -0
- package/src/utils/events.ts +0 -16
- package/src/utils/flow.ts +84 -0
- package/src/utils/git.ts +59 -1
- package/src/utils/log-constants.ts +2 -1
- package/src/utils/log-formatter.ts +11 -12
- package/src/utils/logger.ts +49 -3
- package/src/utils/repro-thinking-logs.ts +0 -15
- package/src/utils/run-service.ts +1 -1
- package/dist/services/logging/file-writer.d.ts +0 -71
- package/dist/services/logging/file-writer.js +0 -516
- package/dist/services/logging/file-writer.js.map +0 -1
- package/dist/types/review.d.ts +0 -17
- package/dist/types/review.js +0 -6
- package/dist/types/review.js.map +0 -1
- package/scripts/ai-security-check.js +0 -233
- package/src/services/logging/file-writer.ts +0 -526
- package/src/types/review.ts +0 -20
|
@@ -13,14 +13,30 @@ import {
|
|
|
13
13
|
TaskExecutionResult,
|
|
14
14
|
LaneState
|
|
15
15
|
} from '../../types';
|
|
16
|
-
import {
|
|
17
|
-
cursorAgentCreateChat
|
|
18
|
-
} from './agent';
|
|
16
|
+
import { AgentSupervisor } from '../agent-supervisor';
|
|
19
17
|
import {
|
|
20
18
|
runTask,
|
|
21
|
-
waitForTaskDependencies
|
|
22
|
-
mergeDependencyBranches
|
|
19
|
+
waitForTaskDependencies
|
|
23
20
|
} from './task';
|
|
21
|
+
import { GitPipelineCoordinator } from '../git-pipeline-coordinator';
|
|
22
|
+
import {
|
|
23
|
+
readPendingIntervention,
|
|
24
|
+
clearPendingIntervention,
|
|
25
|
+
InterventionRequest,
|
|
26
|
+
} from '../intervention';
|
|
27
|
+
import {
|
|
28
|
+
getHookManager,
|
|
29
|
+
HookPoint,
|
|
30
|
+
createBeforeTaskContext,
|
|
31
|
+
createAfterTaskContext,
|
|
32
|
+
createOnErrorContext,
|
|
33
|
+
createOnLaneEndContext,
|
|
34
|
+
FlowAbortError,
|
|
35
|
+
FlowRetryError,
|
|
36
|
+
TaskDefinition,
|
|
37
|
+
TaskResult as HookTaskResult,
|
|
38
|
+
DependencyResult,
|
|
39
|
+
} from '../../hooks';
|
|
24
40
|
|
|
25
41
|
/**
|
|
26
42
|
* Validate task configuration
|
|
@@ -37,6 +53,26 @@ function validateTaskConfig(config: RunnerConfig): void {
|
|
|
37
53
|
}
|
|
38
54
|
}
|
|
39
55
|
|
|
56
|
+
/**
|
|
57
|
+
* Check for pending intervention and return intervention message if present
|
|
58
|
+
* This is called at the start of each task to inject intervention messages
|
|
59
|
+
*/
|
|
60
|
+
function checkAndConsumePendingIntervention(runDir: string): InterventionRequest | null {
|
|
61
|
+
const intervention = readPendingIntervention(runDir);
|
|
62
|
+
|
|
63
|
+
if (intervention) {
|
|
64
|
+
logger.info(`📨 Pending intervention found (type: ${intervention.type})`);
|
|
65
|
+
logger.info(` Message: "${intervention.message.substring(0, 80)}${intervention.message.length > 80 ? '...' : ''}"`);
|
|
66
|
+
|
|
67
|
+
// Clear the intervention file so it's not picked up again
|
|
68
|
+
clearPendingIntervention(runDir);
|
|
69
|
+
|
|
70
|
+
return intervention;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return null;
|
|
74
|
+
}
|
|
75
|
+
|
|
40
76
|
/**
|
|
41
77
|
* Run all tasks in sequence
|
|
42
78
|
*/
|
|
@@ -175,51 +211,13 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
|
|
|
175
211
|
logger.info(`Worktree: ${worktreeDir}`);
|
|
176
212
|
logger.info(`Tasks: ${config.tasks.length}`);
|
|
177
213
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
try {
|
|
186
|
-
// Ensure parent directory exists before calling git worktree
|
|
187
|
-
const worktreeParent = path.dirname(worktreeDir);
|
|
188
|
-
if (!fs.existsSync(worktreeParent)) {
|
|
189
|
-
fs.mkdirSync(worktreeParent, { recursive: true });
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
// Always use the current branch (already captured at start) as the base branch
|
|
193
|
-
await git.createWorktreeAsync(worktreeDir, pipelineBranch, {
|
|
194
|
-
baseBranch: currentBranch,
|
|
195
|
-
cwd: repoRoot,
|
|
196
|
-
});
|
|
197
|
-
break; // Success
|
|
198
|
-
} catch (e: any) {
|
|
199
|
-
lastError = e;
|
|
200
|
-
retries--;
|
|
201
|
-
if (retries > 0) {
|
|
202
|
-
const delay = Math.floor(Math.random() * 1000) + 500;
|
|
203
|
-
logger.warn(`Worktree creation failed, retrying in ${delay}ms... (${retries} retries left)`);
|
|
204
|
-
await new Promise(resolve => setTimeout(resolve, delay));
|
|
205
|
-
}
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
if (retries === 0 && lastError) {
|
|
210
|
-
throw new Error(`Failed to create Git worktree after retries: ${lastError.message}`);
|
|
211
|
-
}
|
|
212
|
-
} else {
|
|
213
|
-
// If it exists, ensure it's actually a worktree and on the right branch
|
|
214
|
-
logger.info(`Reusing existing worktree: ${worktreeDir}`);
|
|
215
|
-
try {
|
|
216
|
-
git.runGit(['checkout', pipelineBranch], { cwd: worktreeDir });
|
|
217
|
-
} catch (e) {
|
|
218
|
-
// If checkout fails, maybe the worktree is in a weird state.
|
|
219
|
-
// For now, just log it. In a more robust impl, we might want to repair it.
|
|
220
|
-
logger.warn(`Failed to checkout branch ${pipelineBranch} in existing worktree: ${e}`);
|
|
221
|
-
}
|
|
222
|
-
}
|
|
214
|
+
const gitCoordinator = new GitPipelineCoordinator();
|
|
215
|
+
await gitCoordinator.ensureWorktree({
|
|
216
|
+
worktreeDir,
|
|
217
|
+
pipelineBranch,
|
|
218
|
+
repoRoot,
|
|
219
|
+
baseBranch: currentBranch,
|
|
220
|
+
});
|
|
223
221
|
|
|
224
222
|
// Change current directory to worktree for all subsequent operations
|
|
225
223
|
// This ensures that all spawned processes (like git or npm) inherit the correct CWD
|
|
@@ -228,7 +226,8 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
|
|
|
228
226
|
|
|
229
227
|
// Create chat
|
|
230
228
|
logger.info('Creating chat session...');
|
|
231
|
-
const
|
|
229
|
+
const agentSupervisor = new AgentSupervisor();
|
|
230
|
+
const chatId = agentSupervisor.createChat(worktreeDir);
|
|
232
231
|
|
|
233
232
|
// Initialize state if not loaded
|
|
234
233
|
if (!state) {
|
|
@@ -264,6 +263,10 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
|
|
|
264
263
|
let previousTaskBranch: string | null = null;
|
|
265
264
|
|
|
266
265
|
for (let i = startIndex; i < config.tasks.length; i++) {
|
|
266
|
+
// Check for pending intervention at the start of each task
|
|
267
|
+
// This handles both resume cases and mid-run interventions
|
|
268
|
+
const intervention = checkAndConsumePendingIntervention(runDir);
|
|
269
|
+
|
|
267
270
|
// Re-read tasks file to allow dynamic updates to future tasks
|
|
268
271
|
try {
|
|
269
272
|
const currentConfig = JSON.parse(fs.readFileSync(tasksFile, 'utf8')) as RunnerConfig;
|
|
@@ -291,7 +294,15 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
|
|
|
291
294
|
logger.warn(`⚠️ Could not reload tasks from ${tasksFile}. Using existing configuration. (${e instanceof Error ? e.message : String(e)})`);
|
|
292
295
|
}
|
|
293
296
|
|
|
294
|
-
|
|
297
|
+
// Clone the task to avoid mutating the original config
|
|
298
|
+
let task = { ...config.tasks[i]! };
|
|
299
|
+
|
|
300
|
+
// If there's a pending intervention, prepend it to the task prompt
|
|
301
|
+
if (intervention) {
|
|
302
|
+
const originalPrompt = task.prompt;
|
|
303
|
+
task.prompt = `${intervention.message}\n\n---\n\nContinue with the following task:\n${originalPrompt}`;
|
|
304
|
+
logger.info(`🔀 Intervention message injected into task prompt`);
|
|
305
|
+
}
|
|
295
306
|
const taskBranch = `${pipelineBranch}--${String(i + 1).padStart(2, '0')}-${task.name}`;
|
|
296
307
|
|
|
297
308
|
// Delete previous task branch if it exists (Task 1 deleted when Task 2 starts, etc.)
|
|
@@ -331,7 +342,7 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
|
|
|
331
342
|
onTimeout: 'fail',
|
|
332
343
|
});
|
|
333
344
|
|
|
334
|
-
await mergeDependencyBranches(task.dependsOn, runDir, worktreeDir, pipelineBranch);
|
|
345
|
+
await gitCoordinator.mergeDependencyBranches(task.dependsOn, runDir, worktreeDir, pipelineBranch);
|
|
335
346
|
|
|
336
347
|
state.status = 'running';
|
|
337
348
|
state.waitingFor = [];
|
|
@@ -354,8 +365,103 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
|
|
|
354
365
|
}
|
|
355
366
|
}
|
|
356
367
|
|
|
368
|
+
// =========================================================================
|
|
369
|
+
// Hook System: Prepare context options
|
|
370
|
+
// =========================================================================
|
|
371
|
+
const hookManager = getHookManager();
|
|
372
|
+
const taskStartTime = Date.now();
|
|
373
|
+
|
|
374
|
+
// Build completed tasks list for hooks
|
|
375
|
+
const completedTasksForHooks: HookTaskResult[] = (state.completedTasks || []).map((name, idx) => ({
|
|
376
|
+
name,
|
|
377
|
+
status: 'success' as const,
|
|
378
|
+
duration: 0, // Not tracked historically
|
|
379
|
+
}));
|
|
380
|
+
|
|
381
|
+
// Convert config.tasks to TaskDefinition format
|
|
382
|
+
const tasksAsDefinitions: TaskDefinition[] = config.tasks.map(t => ({
|
|
383
|
+
name: t.name,
|
|
384
|
+
prompt: t.prompt,
|
|
385
|
+
model: t.model,
|
|
386
|
+
timeout: t.timeout,
|
|
387
|
+
dependsOn: t.dependsOn,
|
|
388
|
+
}));
|
|
389
|
+
|
|
390
|
+
const hookContextOptions = {
|
|
391
|
+
laneName,
|
|
392
|
+
runId: path.basename(path.dirname(runDir)),
|
|
393
|
+
taskIndex: i,
|
|
394
|
+
totalTasks: config.tasks.length,
|
|
395
|
+
task: {
|
|
396
|
+
name: task.name,
|
|
397
|
+
prompt: task.prompt,
|
|
398
|
+
model: task.model || config.model || 'sonnet-4.5',
|
|
399
|
+
dependsOn: task.dependsOn,
|
|
400
|
+
},
|
|
401
|
+
worktreeDir,
|
|
402
|
+
runDir,
|
|
403
|
+
taskBranch,
|
|
404
|
+
pipelineBranch,
|
|
405
|
+
tasksFile,
|
|
406
|
+
chatId,
|
|
407
|
+
tasks: tasksAsDefinitions,
|
|
408
|
+
completedTasks: completedTasksForHooks,
|
|
409
|
+
dependencyResults: [] as DependencyResult[], // TODO: populate from actual deps
|
|
410
|
+
taskStartTime,
|
|
411
|
+
laneStartTime: state.startTime || Date.now(),
|
|
412
|
+
agentSupervisor,
|
|
413
|
+
};
|
|
414
|
+
|
|
415
|
+
// =========================================================================
|
|
416
|
+
// Hook: beforeTask
|
|
417
|
+
// =========================================================================
|
|
418
|
+
let currentTask = task;
|
|
419
|
+
|
|
420
|
+
if (hookManager.hasHooks(HookPoint.BEFORE_TASK)) {
|
|
421
|
+
try {
|
|
422
|
+
const { context, flowController } = createBeforeTaskContext(hookContextOptions);
|
|
423
|
+
await hookManager.executeBeforeTask(context);
|
|
424
|
+
|
|
425
|
+
// Check if prompt was modified
|
|
426
|
+
const modifiedPrompt = flowController.getModifiedPrompt();
|
|
427
|
+
if (modifiedPrompt) {
|
|
428
|
+
currentTask = { ...task, prompt: modifiedPrompt };
|
|
429
|
+
logger.info(`🔧 [Hook] Task prompt modified by beforeTask hook`);
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
// Re-read tasks in case hooks modified them
|
|
433
|
+
hookContextOptions.tasks = config.tasks.map(t => ({
|
|
434
|
+
name: t.name,
|
|
435
|
+
prompt: t.prompt,
|
|
436
|
+
model: t.model,
|
|
437
|
+
timeout: t.timeout,
|
|
438
|
+
dependsOn: t.dependsOn,
|
|
439
|
+
}));
|
|
440
|
+
} catch (hookError: any) {
|
|
441
|
+
if (hookError instanceof FlowAbortError) {
|
|
442
|
+
state.status = 'failed';
|
|
443
|
+
state.error = hookError.message;
|
|
444
|
+
saveState(statePath, state);
|
|
445
|
+
logger.error(`[Hook] Flow aborted: ${hookError.message}`);
|
|
446
|
+
process.exit(1);
|
|
447
|
+
}
|
|
448
|
+
if (hookError instanceof FlowRetryError) {
|
|
449
|
+
// Retry with modified prompt if provided
|
|
450
|
+
if (hookError.modifiedPrompt) {
|
|
451
|
+
currentTask = { ...task, prompt: hookError.modifiedPrompt };
|
|
452
|
+
}
|
|
453
|
+
logger.info(`[Hook] Retry requested, continuing with ${hookError.modifiedPrompt ? 'modified' : 'original'} prompt`);
|
|
454
|
+
} else {
|
|
455
|
+
logger.warn(`[Hook] beforeTask hook error: ${hookError.message}`);
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
// =========================================================================
|
|
461
|
+
// Execute Task
|
|
462
|
+
// =========================================================================
|
|
357
463
|
const result = await runTask({
|
|
358
|
-
task,
|
|
464
|
+
task: currentTask,
|
|
359
465
|
config,
|
|
360
466
|
index: i,
|
|
361
467
|
worktreeDir,
|
|
@@ -363,6 +469,8 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
|
|
|
363
469
|
taskBranch,
|
|
364
470
|
chatId,
|
|
365
471
|
runDir,
|
|
472
|
+
agentSupervisor,
|
|
473
|
+
laneName,
|
|
366
474
|
});
|
|
367
475
|
|
|
368
476
|
results.push(result);
|
|
@@ -375,8 +483,74 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
|
|
|
375
483
|
}
|
|
376
484
|
saveState(statePath, state);
|
|
377
485
|
|
|
486
|
+
// =========================================================================
|
|
487
|
+
// Hook: afterTask
|
|
488
|
+
// =========================================================================
|
|
489
|
+
if (hookManager.hasHooks(HookPoint.AFTER_TASK)) {
|
|
490
|
+
try {
|
|
491
|
+
// Update completed tasks for the context
|
|
492
|
+
hookContextOptions.completedTasks = (state.completedTasks || []).map((name) => ({
|
|
493
|
+
name,
|
|
494
|
+
status: 'success' as const,
|
|
495
|
+
duration: Date.now() - taskStartTime,
|
|
496
|
+
}));
|
|
497
|
+
|
|
498
|
+
const afterTaskResult = {
|
|
499
|
+
status: (result.status === 'FINISHED' ? 'success' :
|
|
500
|
+
result.status === 'BLOCKED_DEPENDENCY' ? 'blocked' : 'error') as 'success' | 'error' | 'blocked',
|
|
501
|
+
exitCode: result.status === 'FINISHED' ? 0 : 1,
|
|
502
|
+
error: result.error,
|
|
503
|
+
};
|
|
504
|
+
|
|
505
|
+
const { context } = createAfterTaskContext(hookContextOptions, afterTaskResult);
|
|
506
|
+
await hookManager.executeAfterTask(context);
|
|
507
|
+
|
|
508
|
+
// Re-read tasks in case hooks modified them (injected new tasks)
|
|
509
|
+
try {
|
|
510
|
+
const updatedConfig = JSON.parse(fs.readFileSync(tasksFile, 'utf8')) as RunnerConfig;
|
|
511
|
+
if (updatedConfig.tasks && updatedConfig.tasks.length !== config.tasks.length) {
|
|
512
|
+
config.tasks = updatedConfig.tasks;
|
|
513
|
+
state.totalTasks = config.tasks.length;
|
|
514
|
+
saveState(statePath, state);
|
|
515
|
+
logger.info(`📋 [Hook] Task list updated by afterTask hook. New total: ${state.totalTasks}`);
|
|
516
|
+
}
|
|
517
|
+
} catch {
|
|
518
|
+
// Ignore file read errors
|
|
519
|
+
}
|
|
520
|
+
} catch (hookError: any) {
|
|
521
|
+
if (hookError instanceof FlowAbortError) {
|
|
522
|
+
state.status = 'failed';
|
|
523
|
+
state.error = hookError.message;
|
|
524
|
+
saveState(statePath, state);
|
|
525
|
+
logger.error(`[Hook] Flow aborted: ${hookError.message}`);
|
|
526
|
+
process.exit(1);
|
|
527
|
+
}
|
|
528
|
+
if (hookError instanceof FlowRetryError) {
|
|
529
|
+
// Decrement index to retry this task
|
|
530
|
+
i--;
|
|
531
|
+
logger.info(`[Hook] Retry requested for task "${task.name}"`);
|
|
532
|
+
continue;
|
|
533
|
+
}
|
|
534
|
+
logger.warn(`[Hook] afterTask hook error: ${hookError.message}`);
|
|
535
|
+
}
|
|
536
|
+
}
|
|
537
|
+
|
|
378
538
|
// Handle blocked or error
|
|
379
539
|
if (result.status === 'BLOCKED_DEPENDENCY') {
|
|
540
|
+
// Execute onError hook for blocked state
|
|
541
|
+
if (hookManager.hasHooks(HookPoint.ON_ERROR)) {
|
|
542
|
+
try {
|
|
543
|
+
const { context } = createOnErrorContext(hookContextOptions, {
|
|
544
|
+
type: 'unknown',
|
|
545
|
+
message: 'Task blocked on dependency change',
|
|
546
|
+
retryable: false,
|
|
547
|
+
});
|
|
548
|
+
await hookManager.executeOnError(context);
|
|
549
|
+
} catch {
|
|
550
|
+
// Continue with normal error handling
|
|
551
|
+
}
|
|
552
|
+
}
|
|
553
|
+
|
|
380
554
|
state.status = 'failed';
|
|
381
555
|
state.dependencyRequest = result.dependencyRequest || null;
|
|
382
556
|
saveState(statePath, state);
|
|
@@ -393,6 +567,24 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
|
|
|
393
567
|
}
|
|
394
568
|
|
|
395
569
|
if (result.status !== 'FINISHED') {
|
|
570
|
+
// Execute onError hook
|
|
571
|
+
if (hookManager.hasHooks(HookPoint.ON_ERROR)) {
|
|
572
|
+
try {
|
|
573
|
+
const { context } = createOnErrorContext(hookContextOptions, {
|
|
574
|
+
type: 'agent_error',
|
|
575
|
+
message: result.error || 'Unknown error',
|
|
576
|
+
retryable: true,
|
|
577
|
+
});
|
|
578
|
+
await hookManager.executeOnError(context);
|
|
579
|
+
} catch (hookError: any) {
|
|
580
|
+
if (hookError instanceof FlowRetryError) {
|
|
581
|
+
i--;
|
|
582
|
+
logger.info(`[Hook] Retry requested after error`);
|
|
583
|
+
continue;
|
|
584
|
+
}
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
|
|
396
588
|
state.status = 'failed';
|
|
397
589
|
state.error = result.error || 'Unknown error';
|
|
398
590
|
saveState(statePath, state);
|
|
@@ -401,52 +593,18 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
|
|
|
401
593
|
}
|
|
402
594
|
|
|
403
595
|
// Merge into pipeline
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
// Ensure we are on the pipeline branch before merging the task branch
|
|
407
|
-
logger.info(`🔄 Switching to pipeline branch ${pipelineBranch} to integrate changes`);
|
|
408
|
-
git.runGit(['checkout', pipelineBranch], { cwd: worktreeDir });
|
|
409
|
-
|
|
410
|
-
// Pre-check for conflicts (should be rare since task branch was created from pipeline)
|
|
411
|
-
const conflictCheck = git.checkMergeConflict(taskBranch, { cwd: worktreeDir });
|
|
412
|
-
if (conflictCheck.willConflict) {
|
|
413
|
-
logger.warn(`⚠️ Unexpected conflict detected when merging ${taskBranch}`);
|
|
414
|
-
logger.warn(` Conflicting files: ${conflictCheck.conflictingFiles.join(', ')}`);
|
|
415
|
-
logger.warn(` This may indicate concurrent modifications to ${pipelineBranch}`);
|
|
416
|
-
|
|
417
|
-
events.emit('merge.conflict_detected', {
|
|
596
|
+
try {
|
|
597
|
+
gitCoordinator.mergeTaskIntoPipeline({
|
|
418
598
|
taskName: task.name,
|
|
419
599
|
taskBranch,
|
|
420
600
|
pipelineBranch,
|
|
421
|
-
|
|
422
|
-
preCheck: true,
|
|
601
|
+
worktreeDir,
|
|
423
602
|
});
|
|
424
|
-
}
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
cwd: worktreeDir,
|
|
430
|
-
noFf: true,
|
|
431
|
-
message: `chore: merge task ${task.name} into pipeline`,
|
|
432
|
-
abortOnConflict: true,
|
|
433
|
-
});
|
|
434
|
-
|
|
435
|
-
if (!mergeResult.success) {
|
|
436
|
-
if (mergeResult.conflict) {
|
|
437
|
-
logger.error(`❌ Merge conflict: ${mergeResult.conflictingFiles.join(', ')}`);
|
|
438
|
-
state.status = 'failed';
|
|
439
|
-
state.error = `Merge conflict when integrating task ${task.name}: ${mergeResult.conflictingFiles.join(', ')}`;
|
|
440
|
-
saveState(statePath, state);
|
|
441
|
-
process.exit(1);
|
|
442
|
-
}
|
|
443
|
-
throw new Error(mergeResult.error || 'Merge failed');
|
|
444
|
-
}
|
|
445
|
-
|
|
446
|
-
// Log changed files
|
|
447
|
-
const stats = git.getLastOperationStats(worktreeDir);
|
|
448
|
-
if (stats) {
|
|
449
|
-
logger.info('Changed files:\n' + stats);
|
|
603
|
+
} catch (e: any) {
|
|
604
|
+
state.status = 'failed';
|
|
605
|
+
state.error = e.message;
|
|
606
|
+
saveState(statePath, state);
|
|
607
|
+
process.exit(1);
|
|
450
608
|
}
|
|
451
609
|
|
|
452
610
|
git.push(pipelineBranch, { cwd: worktreeDir });
|
|
@@ -470,31 +628,11 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
|
|
|
470
628
|
}
|
|
471
629
|
|
|
472
630
|
// 2. Create flow branch from pipelineBranch and cleanup
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
git.push(flowBranch, { cwd: worktreeDir, setUpstream: true });
|
|
479
|
-
|
|
480
|
-
// 3. Delete temporary pipeline branch
|
|
481
|
-
logger.info(`🗑️ Deleting temporary pipeline branch: ${pipelineBranch}`);
|
|
482
|
-
// Must be on another branch to delete pipelineBranch
|
|
483
|
-
git.runGit(['checkout', flowBranch], { cwd: worktreeDir });
|
|
484
|
-
git.deleteBranch(pipelineBranch, { cwd: worktreeDir, force: true });
|
|
485
|
-
|
|
486
|
-
try {
|
|
487
|
-
git.deleteBranch(pipelineBranch, { cwd: worktreeDir, force: true, remote: true });
|
|
488
|
-
logger.info(` Deleted remote branch: origin/${pipelineBranch}`);
|
|
489
|
-
} catch {
|
|
490
|
-
// May not exist on remote or delete failed
|
|
491
|
-
}
|
|
492
|
-
|
|
493
|
-
logger.success(`✓ Flow branch '${flowBranch}' is now the only remaining branch.`);
|
|
494
|
-
} catch (e) {
|
|
495
|
-
logger.error(`❌ Failed during final consolidation: ${e}`);
|
|
496
|
-
}
|
|
497
|
-
}
|
|
631
|
+
gitCoordinator.finalizeFlowBranch({
|
|
632
|
+
flowBranch,
|
|
633
|
+
pipelineBranch,
|
|
634
|
+
worktreeDir,
|
|
635
|
+
});
|
|
498
636
|
|
|
499
637
|
// Complete
|
|
500
638
|
state.status = 'completed';
|
|
@@ -512,7 +650,65 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
|
|
|
512
650
|
// Ignore
|
|
513
651
|
}
|
|
514
652
|
|
|
653
|
+
// =========================================================================
|
|
654
|
+
// Hook: onLaneEnd
|
|
655
|
+
// =========================================================================
|
|
656
|
+
const hookManager = getHookManager();
|
|
657
|
+
if (hookManager.hasHooks(HookPoint.ON_LANE_END)) {
|
|
658
|
+
try {
|
|
659
|
+
const totalDuration = (state.endTime || Date.now()) - (state.startTime || Date.now());
|
|
660
|
+
const failedCount = results.filter(r => r.status !== 'FINISHED').length;
|
|
661
|
+
|
|
662
|
+
const laneEndContextOptions = {
|
|
663
|
+
laneName,
|
|
664
|
+
runId: path.basename(path.dirname(runDir)),
|
|
665
|
+
taskIndex: config.tasks.length - 1,
|
|
666
|
+
totalTasks: config.tasks.length,
|
|
667
|
+
task: {
|
|
668
|
+
name: config.tasks[config.tasks.length - 1]?.name || 'unknown',
|
|
669
|
+
prompt: '',
|
|
670
|
+
model: config.model || 'sonnet-4.5',
|
|
671
|
+
},
|
|
672
|
+
worktreeDir,
|
|
673
|
+
runDir,
|
|
674
|
+
taskBranch: pipelineBranch,
|
|
675
|
+
pipelineBranch,
|
|
676
|
+
tasksFile,
|
|
677
|
+
chatId,
|
|
678
|
+
tasks: config.tasks.map(t => ({
|
|
679
|
+
name: t.name,
|
|
680
|
+
prompt: t.prompt,
|
|
681
|
+
model: t.model,
|
|
682
|
+
timeout: t.timeout,
|
|
683
|
+
dependsOn: t.dependsOn,
|
|
684
|
+
})),
|
|
685
|
+
completedTasks: (state.completedTasks || []).map(name => ({
|
|
686
|
+
name,
|
|
687
|
+
status: 'success' as const,
|
|
688
|
+
duration: 0,
|
|
689
|
+
})),
|
|
690
|
+
dependencyResults: [] as DependencyResult[],
|
|
691
|
+
taskStartTime: state.startTime || Date.now(),
|
|
692
|
+
laneStartTime: state.startTime || Date.now(),
|
|
693
|
+
agentSupervisor,
|
|
694
|
+
};
|
|
695
|
+
|
|
696
|
+
const { context } = createOnLaneEndContext(laneEndContextOptions, {
|
|
697
|
+
status: 'completed',
|
|
698
|
+
completedTasks: results.length - failedCount,
|
|
699
|
+
failedTasks: failedCount,
|
|
700
|
+
totalDuration,
|
|
701
|
+
});
|
|
702
|
+
|
|
703
|
+
// Execute async (don't block lane completion)
|
|
704
|
+
hookManager.executeOnLaneEnd(context).catch(err => {
|
|
705
|
+
logger.warn(`[Hook] onLaneEnd error: ${err.message}`);
|
|
706
|
+
});
|
|
707
|
+
} catch (hookError: any) {
|
|
708
|
+
logger.warn(`[Hook] onLaneEnd setup error: ${hookError.message}`);
|
|
709
|
+
}
|
|
710
|
+
}
|
|
711
|
+
|
|
515
712
|
logger.success('All tasks completed!');
|
|
516
713
|
return results;
|
|
517
714
|
}
|
|
518
|
-
|