@litmers/cursorflow-orchestrator 0.1.40 → 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 +7 -3
- 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/resume.js +21 -1
- 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 +29 -62
- 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 -123
- 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 -80
- package/dist/core/runner/task.js.map +1 -1
- package/dist/core/runner.js +8 -2
- package/dist/core/runner.js.map +1 -1
- package/dist/core/stall-detection.d.ts +11 -4
- package/dist/core/stall-detection.js +62 -27
- 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 +6 -1
- package/dist/services/logging/console.js.map +1 -1
- package/dist/services/logging/formatter.d.ts +9 -4
- package/dist/services/logging/formatter.js +64 -18
- 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 +22 -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 +12 -1
- 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 +10 -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 +98 -19
- 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/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 +2 -1
- package/dist/utils/log-formatter.js +10 -10
- 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/resume.ts +27 -1
- 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 +35 -61
- package/src/core/runner/agent.ts +66 -33
- package/src/core/runner/pipeline.ts +318 -138
- package/src/core/runner/task.ts +12 -97
- package/src/core/runner.ts +8 -2
- package/src/core/stall-detection.ts +72 -27
- 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 +7 -1
- package/src/services/logging/formatter.ts +74 -18
- 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 +23 -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 +12 -1
- package/src/ui/log-viewer.ts +3 -0
- package/src/utils/config.ts +11 -1
- package/src/utils/cursor-agent.ts +68 -16
- package/src/utils/enhanced-logger.ts +105 -19
- package/src/utils/event-registry.ts +595 -0
- package/src/utils/events.ts +0 -16
- package/src/utils/flow.ts +83 -0
- package/src/utils/log-constants.ts +2 -1
- package/src/utils/log-formatter.ts +10 -11
- 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,68 +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
|
-
logger.warn(`⚠️ Directory exists but is not a valid worktree: ${worktreeDir}`);
|
|
186
|
-
logger.info(` Cleaning up invalid directory and recreating worktree...`);
|
|
187
|
-
try {
|
|
188
|
-
git.cleanupInvalidWorktreeDir(worktreeDir);
|
|
189
|
-
} catch (e: any) {
|
|
190
|
-
logger.error(`Failed to cleanup invalid worktree directory: ${e.message}`);
|
|
191
|
-
throw new Error(`Cannot proceed: worktree directory is invalid and cleanup failed`);
|
|
192
|
-
}
|
|
193
|
-
}
|
|
194
|
-
|
|
195
|
-
// Create worktree if it doesn't exist or was just cleaned up
|
|
196
|
-
if (worktreeNeedsCreation || worktreeIsInvalid) {
|
|
197
|
-
// Use a simple retry mechanism for Git worktree creation to handle potential race conditions
|
|
198
|
-
let retries = 3;
|
|
199
|
-
let lastError: Error | null = null;
|
|
200
|
-
|
|
201
|
-
while (retries > 0) {
|
|
202
|
-
try {
|
|
203
|
-
// Ensure parent directory exists before calling git worktree
|
|
204
|
-
const worktreeParent = path.dirname(worktreeDir);
|
|
205
|
-
if (!fs.existsSync(worktreeParent)) {
|
|
206
|
-
fs.mkdirSync(worktreeParent, { recursive: true });
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
// Always use the current branch (already captured at start) as the base branch
|
|
210
|
-
await git.createWorktreeAsync(worktreeDir, pipelineBranch, {
|
|
211
|
-
baseBranch: currentBranch,
|
|
212
|
-
cwd: repoRoot,
|
|
213
|
-
});
|
|
214
|
-
break; // Success
|
|
215
|
-
} catch (e: any) {
|
|
216
|
-
lastError = e;
|
|
217
|
-
retries--;
|
|
218
|
-
if (retries > 0) {
|
|
219
|
-
const delay = Math.floor(Math.random() * 1000) + 500;
|
|
220
|
-
logger.warn(`Worktree creation failed, retrying in ${delay}ms... (${retries} retries left)`);
|
|
221
|
-
await new Promise(resolve => setTimeout(resolve, delay));
|
|
222
|
-
}
|
|
223
|
-
}
|
|
224
|
-
}
|
|
225
|
-
|
|
226
|
-
if (retries === 0 && lastError) {
|
|
227
|
-
throw new Error(`Failed to create Git worktree after retries: ${lastError.message}`);
|
|
228
|
-
}
|
|
229
|
-
} else {
|
|
230
|
-
// Worktree exists and is valid - reuse it
|
|
231
|
-
logger.info(`Reusing existing worktree: ${worktreeDir}`);
|
|
232
|
-
try {
|
|
233
|
-
git.runGit(['checkout', pipelineBranch], { cwd: worktreeDir });
|
|
234
|
-
} catch (e) {
|
|
235
|
-
// If checkout fails in a valid worktree, log warning but continue
|
|
236
|
-
// The worktree might be on a different branch that will be handled later
|
|
237
|
-
logger.warn(`Failed to checkout branch ${pipelineBranch} in existing worktree: ${e}`);
|
|
238
|
-
}
|
|
239
|
-
}
|
|
214
|
+
const gitCoordinator = new GitPipelineCoordinator();
|
|
215
|
+
await gitCoordinator.ensureWorktree({
|
|
216
|
+
worktreeDir,
|
|
217
|
+
pipelineBranch,
|
|
218
|
+
repoRoot,
|
|
219
|
+
baseBranch: currentBranch,
|
|
220
|
+
});
|
|
240
221
|
|
|
241
222
|
// Change current directory to worktree for all subsequent operations
|
|
242
223
|
// This ensures that all spawned processes (like git or npm) inherit the correct CWD
|
|
@@ -245,7 +226,8 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
|
|
|
245
226
|
|
|
246
227
|
// Create chat
|
|
247
228
|
logger.info('Creating chat session...');
|
|
248
|
-
const
|
|
229
|
+
const agentSupervisor = new AgentSupervisor();
|
|
230
|
+
const chatId = agentSupervisor.createChat(worktreeDir);
|
|
249
231
|
|
|
250
232
|
// Initialize state if not loaded
|
|
251
233
|
if (!state) {
|
|
@@ -281,6 +263,10 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
|
|
|
281
263
|
let previousTaskBranch: string | null = null;
|
|
282
264
|
|
|
283
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
|
+
|
|
284
270
|
// Re-read tasks file to allow dynamic updates to future tasks
|
|
285
271
|
try {
|
|
286
272
|
const currentConfig = JSON.parse(fs.readFileSync(tasksFile, 'utf8')) as RunnerConfig;
|
|
@@ -308,7 +294,15 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
|
|
|
308
294
|
logger.warn(`⚠️ Could not reload tasks from ${tasksFile}. Using existing configuration. (${e instanceof Error ? e.message : String(e)})`);
|
|
309
295
|
}
|
|
310
296
|
|
|
311
|
-
|
|
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
|
+
}
|
|
312
306
|
const taskBranch = `${pipelineBranch}--${String(i + 1).padStart(2, '0')}-${task.name}`;
|
|
313
307
|
|
|
314
308
|
// Delete previous task branch if it exists (Task 1 deleted when Task 2 starts, etc.)
|
|
@@ -348,7 +342,7 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
|
|
|
348
342
|
onTimeout: 'fail',
|
|
349
343
|
});
|
|
350
344
|
|
|
351
|
-
await mergeDependencyBranches(task.dependsOn, runDir, worktreeDir, pipelineBranch);
|
|
345
|
+
await gitCoordinator.mergeDependencyBranches(task.dependsOn, runDir, worktreeDir, pipelineBranch);
|
|
352
346
|
|
|
353
347
|
state.status = 'running';
|
|
354
348
|
state.waitingFor = [];
|
|
@@ -371,8 +365,103 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
|
|
|
371
365
|
}
|
|
372
366
|
}
|
|
373
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
|
+
// =========================================================================
|
|
374
463
|
const result = await runTask({
|
|
375
|
-
task,
|
|
464
|
+
task: currentTask,
|
|
376
465
|
config,
|
|
377
466
|
index: i,
|
|
378
467
|
worktreeDir,
|
|
@@ -380,6 +469,8 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
|
|
|
380
469
|
taskBranch,
|
|
381
470
|
chatId,
|
|
382
471
|
runDir,
|
|
472
|
+
agentSupervisor,
|
|
473
|
+
laneName,
|
|
383
474
|
});
|
|
384
475
|
|
|
385
476
|
results.push(result);
|
|
@@ -392,8 +483,74 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
|
|
|
392
483
|
}
|
|
393
484
|
saveState(statePath, state);
|
|
394
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
|
+
|
|
395
538
|
// Handle blocked or error
|
|
396
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
|
+
|
|
397
554
|
state.status = 'failed';
|
|
398
555
|
state.dependencyRequest = result.dependencyRequest || null;
|
|
399
556
|
saveState(statePath, state);
|
|
@@ -410,6 +567,24 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
|
|
|
410
567
|
}
|
|
411
568
|
|
|
412
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
|
+
|
|
413
588
|
state.status = 'failed';
|
|
414
589
|
state.error = result.error || 'Unknown error';
|
|
415
590
|
saveState(statePath, state);
|
|
@@ -418,52 +593,18 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
|
|
|
418
593
|
}
|
|
419
594
|
|
|
420
595
|
// Merge into pipeline
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
// Ensure we are on the pipeline branch before merging the task branch
|
|
424
|
-
logger.info(`🔄 Switching to pipeline branch ${pipelineBranch} to integrate changes`);
|
|
425
|
-
git.runGit(['checkout', pipelineBranch], { cwd: worktreeDir });
|
|
426
|
-
|
|
427
|
-
// Pre-check for conflicts (should be rare since task branch was created from pipeline)
|
|
428
|
-
const conflictCheck = git.checkMergeConflict(taskBranch, { cwd: worktreeDir });
|
|
429
|
-
if (conflictCheck.willConflict) {
|
|
430
|
-
logger.warn(`⚠️ Unexpected conflict detected when merging ${taskBranch}`);
|
|
431
|
-
logger.warn(` Conflicting files: ${conflictCheck.conflictingFiles.join(', ')}`);
|
|
432
|
-
logger.warn(` This may indicate concurrent modifications to ${pipelineBranch}`);
|
|
433
|
-
|
|
434
|
-
events.emit('merge.conflict_detected', {
|
|
596
|
+
try {
|
|
597
|
+
gitCoordinator.mergeTaskIntoPipeline({
|
|
435
598
|
taskName: task.name,
|
|
436
599
|
taskBranch,
|
|
437
600
|
pipelineBranch,
|
|
438
|
-
|
|
439
|
-
preCheck: true,
|
|
601
|
+
worktreeDir,
|
|
440
602
|
});
|
|
441
|
-
}
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
cwd: worktreeDir,
|
|
447
|
-
noFf: true,
|
|
448
|
-
message: `chore: merge task ${task.name} into pipeline`,
|
|
449
|
-
abortOnConflict: true,
|
|
450
|
-
});
|
|
451
|
-
|
|
452
|
-
if (!mergeResult.success) {
|
|
453
|
-
if (mergeResult.conflict) {
|
|
454
|
-
logger.error(`❌ Merge conflict: ${mergeResult.conflictingFiles.join(', ')}`);
|
|
455
|
-
state.status = 'failed';
|
|
456
|
-
state.error = `Merge conflict when integrating task ${task.name}: ${mergeResult.conflictingFiles.join(', ')}`;
|
|
457
|
-
saveState(statePath, state);
|
|
458
|
-
process.exit(1);
|
|
459
|
-
}
|
|
460
|
-
throw new Error(mergeResult.error || 'Merge failed');
|
|
461
|
-
}
|
|
462
|
-
|
|
463
|
-
// Log changed files
|
|
464
|
-
const stats = git.getLastOperationStats(worktreeDir);
|
|
465
|
-
if (stats) {
|
|
466
|
-
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);
|
|
467
608
|
}
|
|
468
609
|
|
|
469
610
|
git.push(pipelineBranch, { cwd: worktreeDir });
|
|
@@ -487,30 +628,11 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
|
|
|
487
628
|
}
|
|
488
629
|
|
|
489
630
|
// 2. Create flow branch from pipelineBranch and cleanup
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
git.push(flowBranch, { cwd: worktreeDir, setUpstream: true });
|
|
496
|
-
|
|
497
|
-
// 3. Delete temporary pipeline branch (LOCAL ONLY)
|
|
498
|
-
// Keep remote branch for dependency lanes that may need to merge it!
|
|
499
|
-
logger.info(`🗑️ Deleting local pipeline branch: ${pipelineBranch}`);
|
|
500
|
-
// Must be on another branch to delete pipelineBranch
|
|
501
|
-
git.runGit(['checkout', flowBranch], { cwd: worktreeDir });
|
|
502
|
-
git.deleteBranch(pipelineBranch, { cwd: worktreeDir, force: true });
|
|
503
|
-
|
|
504
|
-
// NOTE: We intentionally keep the remote pipeline branch alive
|
|
505
|
-
// because other lanes with dependsOn may need to merge it.
|
|
506
|
-
// The pipeline branch on remote serves as the "official" completion branch
|
|
507
|
-
// that dependency-tracking code in task.ts uses (via state.pipelineBranch).
|
|
508
|
-
|
|
509
|
-
logger.success(`✓ Flow branch '${flowBranch}' created. Remote pipeline branch preserved for dependencies.`);
|
|
510
|
-
} catch (e) {
|
|
511
|
-
logger.error(`❌ Failed during final consolidation: ${e}`);
|
|
512
|
-
}
|
|
513
|
-
}
|
|
631
|
+
gitCoordinator.finalizeFlowBranch({
|
|
632
|
+
flowBranch,
|
|
633
|
+
pipelineBranch,
|
|
634
|
+
worktreeDir,
|
|
635
|
+
});
|
|
514
636
|
|
|
515
637
|
// Complete
|
|
516
638
|
state.status = 'completed';
|
|
@@ -528,7 +650,65 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
|
|
|
528
650
|
// Ignore
|
|
529
651
|
}
|
|
530
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
|
+
|
|
531
712
|
logger.success('All tasks completed!');
|
|
532
713
|
return results;
|
|
533
714
|
}
|
|
534
|
-
|