@litmers/cursorflow-orchestrator 0.1.31 → 0.1.36

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (150) hide show
  1. package/CHANGELOG.md +27 -0
  2. package/README.md +182 -59
  3. package/commands/cursorflow-add.md +159 -0
  4. package/commands/cursorflow-doctor.md +45 -23
  5. package/commands/cursorflow-monitor.md +23 -2
  6. package/commands/cursorflow-new.md +87 -0
  7. package/commands/cursorflow-run.md +60 -111
  8. package/dist/cli/add.d.ts +7 -0
  9. package/dist/cli/add.js +377 -0
  10. package/dist/cli/add.js.map +1 -0
  11. package/dist/cli/clean.js +1 -0
  12. package/dist/cli/clean.js.map +1 -1
  13. package/dist/cli/config.d.ts +7 -0
  14. package/dist/cli/config.js +181 -0
  15. package/dist/cli/config.js.map +1 -0
  16. package/dist/cli/doctor.js +47 -4
  17. package/dist/cli/doctor.js.map +1 -1
  18. package/dist/cli/index.js +34 -30
  19. package/dist/cli/index.js.map +1 -1
  20. package/dist/cli/logs.js +17 -34
  21. package/dist/cli/logs.js.map +1 -1
  22. package/dist/cli/monitor.js +62 -65
  23. package/dist/cli/monitor.js.map +1 -1
  24. package/dist/cli/new.d.ts +7 -0
  25. package/dist/cli/new.js +232 -0
  26. package/dist/cli/new.js.map +1 -0
  27. package/dist/cli/prepare.js +95 -193
  28. package/dist/cli/prepare.js.map +1 -1
  29. package/dist/cli/resume.js +57 -68
  30. package/dist/cli/resume.js.map +1 -1
  31. package/dist/cli/run.js +60 -30
  32. package/dist/cli/run.js.map +1 -1
  33. package/dist/cli/stop.js +6 -0
  34. package/dist/cli/stop.js.map +1 -1
  35. package/dist/cli/tasks.d.ts +5 -3
  36. package/dist/cli/tasks.js +181 -29
  37. package/dist/cli/tasks.js.map +1 -1
  38. package/dist/core/failure-policy.d.ts +9 -0
  39. package/dist/core/failure-policy.js +9 -0
  40. package/dist/core/failure-policy.js.map +1 -1
  41. package/dist/core/orchestrator.d.ts +20 -6
  42. package/dist/core/orchestrator.js +215 -334
  43. package/dist/core/orchestrator.js.map +1 -1
  44. package/dist/core/runner/agent.d.ts +27 -0
  45. package/dist/core/runner/agent.js +294 -0
  46. package/dist/core/runner/agent.js.map +1 -0
  47. package/dist/core/runner/index.d.ts +5 -0
  48. package/dist/core/runner/index.js +22 -0
  49. package/dist/core/runner/index.js.map +1 -0
  50. package/dist/core/runner/pipeline.d.ts +9 -0
  51. package/dist/core/runner/pipeline.js +539 -0
  52. package/dist/core/runner/pipeline.js.map +1 -0
  53. package/dist/core/runner/prompt.d.ts +25 -0
  54. package/dist/core/runner/prompt.js +175 -0
  55. package/dist/core/runner/prompt.js.map +1 -0
  56. package/dist/core/runner/task.d.ts +26 -0
  57. package/dist/core/runner/task.js +283 -0
  58. package/dist/core/runner/task.js.map +1 -0
  59. package/dist/core/runner/utils.d.ts +37 -0
  60. package/dist/core/runner/utils.js +161 -0
  61. package/dist/core/runner/utils.js.map +1 -0
  62. package/dist/core/runner.d.ts +2 -96
  63. package/dist/core/runner.js +11 -1136
  64. package/dist/core/runner.js.map +1 -1
  65. package/dist/core/stall-detection.d.ts +326 -0
  66. package/dist/core/stall-detection.js +781 -0
  67. package/dist/core/stall-detection.js.map +1 -0
  68. package/dist/services/logging/console.js +2 -1
  69. package/dist/services/logging/console.js.map +1 -1
  70. package/dist/types/config.d.ts +6 -6
  71. package/dist/types/flow.d.ts +84 -0
  72. package/dist/types/flow.js +10 -0
  73. package/dist/types/flow.js.map +1 -0
  74. package/dist/types/index.d.ts +1 -0
  75. package/dist/types/index.js +3 -3
  76. package/dist/types/index.js.map +1 -1
  77. package/dist/types/lane.d.ts +0 -2
  78. package/dist/types/logging.d.ts +5 -1
  79. package/dist/types/task.d.ts +7 -11
  80. package/dist/utils/config.d.ts +5 -1
  81. package/dist/utils/config.js +15 -16
  82. package/dist/utils/config.js.map +1 -1
  83. package/dist/utils/dependency.d.ts +36 -1
  84. package/dist/utils/dependency.js +256 -1
  85. package/dist/utils/dependency.js.map +1 -1
  86. package/dist/utils/doctor.js +40 -8
  87. package/dist/utils/doctor.js.map +1 -1
  88. package/dist/utils/enhanced-logger.d.ts +45 -82
  89. package/dist/utils/enhanced-logger.js +239 -844
  90. package/dist/utils/enhanced-logger.js.map +1 -1
  91. package/dist/utils/flow.d.ts +9 -0
  92. package/dist/utils/flow.js +73 -0
  93. package/dist/utils/flow.js.map +1 -0
  94. package/dist/utils/git.d.ts +29 -0
  95. package/dist/utils/git.js +115 -5
  96. package/dist/utils/git.js.map +1 -1
  97. package/dist/utils/state.js +0 -2
  98. package/dist/utils/state.js.map +1 -1
  99. package/dist/utils/task-service.d.ts +2 -2
  100. package/dist/utils/task-service.js +40 -31
  101. package/dist/utils/task-service.js.map +1 -1
  102. package/package.json +4 -3
  103. package/src/cli/add.ts +397 -0
  104. package/src/cli/clean.ts +1 -0
  105. package/src/cli/config.ts +177 -0
  106. package/src/cli/doctor.ts +48 -4
  107. package/src/cli/index.ts +36 -32
  108. package/src/cli/logs.ts +20 -33
  109. package/src/cli/monitor.ts +70 -75
  110. package/src/cli/new.ts +235 -0
  111. package/src/cli/prepare.ts +98 -205
  112. package/src/cli/resume.ts +61 -76
  113. package/src/cli/run.ts +333 -306
  114. package/src/cli/stop.ts +8 -0
  115. package/src/cli/tasks.ts +200 -21
  116. package/src/core/failure-policy.ts +9 -0
  117. package/src/core/orchestrator.ts +279 -379
  118. package/src/core/runner/agent.ts +314 -0
  119. package/src/core/runner/index.ts +6 -0
  120. package/src/core/runner/pipeline.ts +567 -0
  121. package/src/core/runner/prompt.ts +174 -0
  122. package/src/core/runner/task.ts +320 -0
  123. package/src/core/runner/utils.ts +142 -0
  124. package/src/core/runner.ts +8 -1347
  125. package/src/core/stall-detection.ts +936 -0
  126. package/src/services/logging/console.ts +2 -1
  127. package/src/types/config.ts +6 -6
  128. package/src/types/flow.ts +91 -0
  129. package/src/types/index.ts +15 -3
  130. package/src/types/lane.ts +0 -2
  131. package/src/types/logging.ts +5 -1
  132. package/src/types/task.ts +7 -11
  133. package/src/utils/config.ts +16 -17
  134. package/src/utils/dependency.ts +311 -2
  135. package/src/utils/doctor.ts +36 -8
  136. package/src/utils/enhanced-logger.ts +264 -927
  137. package/src/utils/flow.ts +42 -0
  138. package/src/utils/git.ts +145 -5
  139. package/src/utils/state.ts +0 -2
  140. package/src/utils/task-service.ts +48 -40
  141. package/commands/cursorflow-review.md +0 -56
  142. package/commands/cursorflow-runs.md +0 -59
  143. package/dist/cli/runs.d.ts +0 -5
  144. package/dist/cli/runs.js +0 -214
  145. package/dist/cli/runs.js.map +0 -1
  146. package/dist/core/reviewer.d.ts +0 -66
  147. package/dist/core/reviewer.js +0 -265
  148. package/dist/core/reviewer.js.map +0 -1
  149. package/src/cli/runs.ts +0 -212
  150. package/src/core/reviewer.ts +0 -285
@@ -2,11 +2,7 @@
2
2
  /**
3
3
  * Core Runner - Execute tasks sequentially in a lane
4
4
  *
5
- * Features:
6
- * - Enhanced retry with circuit breaker
7
- * - Checkpoint system for recovery
8
- * - State validation and repair
9
- * - Improved dependency management
5
+ * This file is now a wrapper around modular components in ./runner/
10
6
  */
11
7
  var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
12
8
  if (k2 === undefined) k2 = k;
@@ -41,1140 +37,19 @@ var __importStar = (this && this.__importStar) || (function () {
41
37
  return result;
42
38
  };
43
39
  })();
40
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
41
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
42
+ };
44
43
  Object.defineProperty(exports, "__esModule", { value: true });
45
- exports.cursorAgentCreateChat = cursorAgentCreateChat;
46
- exports.validateTaskConfig = validateTaskConfig;
47
- exports.cursorAgentSend = cursorAgentSend;
48
- exports.extractDependencyRequest = extractDependencyRequest;
49
- exports.readDependencyRequestFile = readDependencyRequestFile;
50
- exports.clearDependencyRequestFile = clearDependencyRequestFile;
51
- exports.wrapPromptForDependencyPolicy = wrapPromptForDependencyPolicy;
52
- exports.wrapPrompt = wrapPrompt;
53
- exports.applyDependencyFilePermissions = applyDependencyFilePermissions;
54
- exports.waitForTaskDependencies = waitForTaskDependencies;
55
- exports.mergeDependencyBranches = mergeDependencyBranches;
56
- exports.runTask = runTask;
57
- exports.runTasks = runTasks;
58
44
  const fs = __importStar(require("fs"));
59
45
  const path = __importStar(require("path"));
60
- const child_process_1 = require("child_process");
61
- const git = __importStar(require("../utils/git"));
62
- const logger = __importStar(require("../utils/logger"));
63
- const cursor_agent_1 = require("../utils/cursor-agent");
64
- const state_1 = require("../utils/state");
65
- const events_1 = require("../utils/events");
66
46
  const config_1 = require("../utils/config");
67
47
  const webhook_1 = require("../utils/webhook");
68
- const reviewer_1 = require("./reviewer");
69
- const path_1 = require("../utils/path");
70
- const failure_policy_1 = require("./failure-policy");
71
- const checkpoint_1 = require("../utils/checkpoint");
72
- const dependency_1 = require("../utils/dependency");
73
- const health_1 = require("../utils/health");
74
- /**
75
- * Execute cursor-agent command with timeout and better error handling
76
- */
77
- function cursorAgentCreateChat() {
78
- try {
79
- const res = (0, child_process_1.spawnSync)('cursor-agent', ['create-chat'], {
80
- encoding: 'utf8',
81
- stdio: 'pipe',
82
- timeout: 30000, // 30 second timeout
83
- });
84
- if (res.error || res.status !== 0) {
85
- throw res.error || new Error(res.stderr || 'Failed to create chat');
86
- }
87
- const out = res.stdout;
88
- const lines = out.split('\n').filter(Boolean);
89
- const chatId = lines[lines.length - 1] || null;
90
- if (!chatId) {
91
- throw new Error('Failed to get chat ID from cursor-agent');
92
- }
93
- logger.info(`Created chat session: ${chatId}`);
94
- return chatId;
95
- }
96
- catch (error) {
97
- // Check for common errors
98
- if (error.message.includes('ENOENT')) {
99
- throw new Error('cursor-agent CLI not found. Install with: npm install -g @cursor/agent');
100
- }
101
- if (error.message.includes('ETIMEDOUT') || error.killed) {
102
- throw new Error('cursor-agent timed out. Check your internet connection and Cursor authentication.');
103
- }
104
- if (error.stderr) {
105
- const stderr = error.stderr.toString();
106
- // Check for authentication errors
107
- if (stderr.includes('not authenticated') ||
108
- stderr.includes('login') ||
109
- stderr.includes('auth')) {
110
- throw new Error('Cursor authentication failed. Please:\n' +
111
- ' 1. Open Cursor IDE\n' +
112
- ' 2. Sign in to your account\n' +
113
- ' 3. Verify you can use AI features\n' +
114
- ' 4. Try running cursorflow again\n\n' +
115
- `Original error: ${stderr.trim()}`);
116
- }
117
- // Check for API key errors
118
- if (stderr.includes('api key') || stderr.includes('API_KEY')) {
119
- throw new Error('Cursor API key error. Please check your Cursor account and subscription.\n' +
120
- `Error: ${stderr.trim()}`);
121
- }
122
- throw new Error(`cursor-agent error: ${stderr.trim()}`);
123
- }
124
- throw new Error(`Failed to create chat: ${error.message}`);
125
- }
126
- }
127
- function parseJsonFromStdout(stdout) {
128
- const text = String(stdout || '').trim();
129
- if (!text)
130
- return null;
131
- const lines = text.split('\n').filter(Boolean);
132
- for (let i = lines.length - 1; i >= 0; i--) {
133
- const line = lines[i]?.trim();
134
- if (line?.startsWith('{') && line?.endsWith('}')) {
135
- try {
136
- return JSON.parse(line);
137
- }
138
- catch {
139
- continue;
140
- }
141
- }
142
- }
143
- return null;
144
- }
145
- /** Default timeout: 10 minutes */
146
- const DEFAULT_TIMEOUT_MS = 600000;
147
- /** Heartbeat interval: 30 seconds */
148
- const HEARTBEAT_INTERVAL_MS = 30000;
149
- /**
150
- * Validate task configuration
151
- * @throws Error if validation fails
152
- */
153
- function validateTaskConfig(config) {
154
- if (!config.tasks || !Array.isArray(config.tasks)) {
155
- throw new Error('Invalid config: "tasks" must be an array');
156
- }
157
- if (config.tasks.length === 0) {
158
- throw new Error('Invalid config: "tasks" array is empty');
159
- }
160
- for (let i = 0; i < config.tasks.length; i++) {
161
- const task = config.tasks[i];
162
- const taskNum = i + 1;
163
- if (!task) {
164
- throw new Error(`Invalid config: Task ${taskNum} is null or undefined`);
165
- }
166
- if (!task.name || typeof task.name !== 'string') {
167
- throw new Error(`Invalid config: Task ${taskNum} missing required "name" field.\n` +
168
- ` Found: ${JSON.stringify(task, null, 2).substring(0, 200)}...\n` +
169
- ` Expected: { "name": "task-name", "prompt": "..." }`);
170
- }
171
- if (!task.prompt || typeof task.prompt !== 'string') {
172
- throw new Error(`Invalid config: Task "${task.name}" (${taskNum}) missing required "prompt" field`);
173
- }
174
- // Validate task name format (no spaces, special chars that could break branch names)
175
- if (!/^[a-zA-Z0-9_-]+$/.test(task.name)) {
176
- throw new Error(`Invalid config: Task name "${task.name}" contains invalid characters.\n` +
177
- ` Task names must only contain: letters, numbers, underscore (_), hyphen (-)`);
178
- }
179
- }
180
- // Validate timeout if provided
181
- if (config.timeout !== undefined) {
182
- if (typeof config.timeout !== 'number' || config.timeout <= 0) {
183
- throw new Error(`Invalid config: "timeout" must be a positive number (milliseconds).\n` +
184
- ` Found: ${config.timeout}`);
185
- }
186
- }
187
- }
188
- /**
189
- * Internal: Execute cursor-agent command with streaming
190
- */
191
- async function cursorAgentSendRaw({ workspaceDir, chatId, prompt, model, signalDir, timeout, enableIntervention, outputFormat, taskName }) {
192
- // Use stream-json format for structured output with tool calls and results
193
- const format = outputFormat || 'stream-json';
194
- const args = [
195
- '--print',
196
- '--force',
197
- '--approve-mcps',
198
- '--output-format', format,
199
- '--workspace', workspaceDir,
200
- ...(model ? ['--model', model] : []),
201
- '--resume', chatId,
202
- prompt,
203
- ];
204
- const timeoutMs = timeout || DEFAULT_TIMEOUT_MS;
205
- // Determine stdio mode based on intervention setting
206
- const stdinMode = enableIntervention ? 'pipe' : 'ignore';
207
- return new Promise((resolve) => {
208
- // Build environment, preserving user's NODE_OPTIONS but disabling problematic flags
209
- const childEnv = { ...process.env };
210
- if (childEnv.NODE_OPTIONS) {
211
- const filtered = childEnv.NODE_OPTIONS
212
- .split(' ')
213
- .filter(opt => !opt.includes('--inspect') && !opt.includes('--debug'))
214
- .join(' ');
215
- childEnv.NODE_OPTIONS = filtered;
216
- }
217
- childEnv.PYTHONUNBUFFERED = '1';
218
- const child = (0, child_process_1.spawn)('cursor-agent', args, {
219
- stdio: [stdinMode, 'pipe', 'pipe'],
220
- env: childEnv,
221
- });
222
- // Save PID to state if possible
223
- if (child.pid && signalDir) {
224
- try {
225
- const statePath = (0, path_1.safeJoin)(signalDir, 'state.json');
226
- const state = JSON.parse(fs.readFileSync(statePath, 'utf8'));
227
- state.pid = child.pid;
228
- fs.writeFileSync(statePath, JSON.stringify(state, null, 2));
229
- }
230
- catch {
231
- // Best effort
232
- }
233
- }
234
- let fullStdout = '';
235
- let fullStderr = '';
236
- let timeoutHandle;
237
- // Heartbeat logging
238
- let lastHeartbeat = Date.now();
239
- let bytesReceived = 0;
240
- const startTime = Date.now();
241
- const heartbeatInterval = setInterval(() => {
242
- const totalElapsed = Math.round((Date.now() - startTime) / 1000);
243
- // Output without timestamp - orchestrator will add it
244
- console.log(`⏱ Heartbeat: ${totalElapsed}s elapsed, ${bytesReceived} bytes received`);
245
- }, HEARTBEAT_INTERVAL_MS);
246
- // Signal watchers (intervention, timeout)
247
- const interventionPath = signalDir ? path.join(signalDir, 'intervention.txt') : null;
248
- const timeoutPath = signalDir ? path.join(signalDir, 'timeout.txt') : null;
249
- let signalWatcher = null;
250
- if (signalDir && fs.existsSync(signalDir)) {
251
- signalWatcher = fs.watch(signalDir, (event, filename) => {
252
- if (filename === 'intervention.txt' && interventionPath && fs.existsSync(interventionPath)) {
253
- try {
254
- const message = fs.readFileSync(interventionPath, 'utf8').trim();
255
- if (message) {
256
- if (enableIntervention && child.stdin) {
257
- logger.info(`Injecting intervention: ${message}`);
258
- child.stdin.write(message + '\n');
259
- // Log to conversation history for visibility in monitor/logs
260
- if (signalDir) {
261
- const convoPath = path.join(signalDir, 'conversation.jsonl');
262
- (0, state_1.appendLog)(convoPath, (0, state_1.createConversationEntry)('intervention', `[HUMAN INTERVENTION]: ${message}`, {
263
- task: taskName || 'AGENT_TURN',
264
- model: 'manual'
265
- }));
266
- }
267
- }
268
- else {
269
- logger.warn(`Intervention requested but stdin not available: ${message}`);
270
- }
271
- fs.unlinkSync(interventionPath);
272
- }
273
- }
274
- catch { }
275
- }
276
- if (filename === 'timeout.txt' && timeoutPath && fs.existsSync(timeoutPath)) {
277
- try {
278
- const newTimeoutStr = fs.readFileSync(timeoutPath, 'utf8').trim();
279
- const newTimeoutMs = parseInt(newTimeoutStr);
280
- if (!isNaN(newTimeoutMs) && newTimeoutMs > 0) {
281
- logger.info(`⏱ Dynamic timeout update: ${Math.round(newTimeoutMs / 1000)}s`);
282
- if (timeoutHandle)
283
- clearTimeout(timeoutHandle);
284
- const elapsed = Date.now() - startTime;
285
- const remaining = Math.max(1000, newTimeoutMs - elapsed);
286
- timeoutHandle = setTimeout(() => {
287
- clearInterval(heartbeatInterval);
288
- child.kill();
289
- resolve({ ok: false, exitCode: -1, error: `cursor-agent timed out after updated limit.` });
290
- }, remaining);
291
- fs.unlinkSync(timeoutPath);
292
- }
293
- }
294
- catch { }
295
- }
296
- });
297
- }
298
- if (child.stdout) {
299
- child.stdout.on('data', (data) => {
300
- fullStdout += data.toString();
301
- bytesReceived += data.length;
302
- process.stdout.write(data);
303
- });
304
- }
305
- if (child.stderr) {
306
- child.stderr.on('data', (data) => {
307
- fullStderr += data.toString();
308
- process.stderr.write(data);
309
- });
310
- }
311
- timeoutHandle = setTimeout(() => {
312
- clearInterval(heartbeatInterval);
313
- child.kill();
314
- resolve({
315
- ok: false,
316
- exitCode: -1,
317
- error: `cursor-agent timed out after ${Math.round(timeoutMs / 1000)} seconds.`,
318
- });
319
- }, timeoutMs);
320
- child.on('close', (code) => {
321
- clearTimeout(timeoutHandle);
322
- clearInterval(heartbeatInterval);
323
- if (signalWatcher)
324
- signalWatcher.close();
325
- const json = parseJsonFromStdout(fullStdout);
326
- if (code !== 0 || !json || json.type !== 'result') {
327
- let errorMsg = fullStderr.trim() || fullStdout.trim() || `exit=${code}`;
328
- resolve({ ok: false, exitCode: code ?? -1, error: errorMsg });
329
- }
330
- else {
331
- resolve({
332
- ok: !json.is_error,
333
- exitCode: code ?? 0,
334
- sessionId: json.session_id || chatId,
335
- resultText: json.result || '',
336
- });
337
- }
338
- });
339
- child.on('error', (err) => {
340
- clearTimeout(timeoutHandle);
341
- clearInterval(heartbeatInterval);
342
- resolve({ ok: false, exitCode: -1, error: `Failed to start cursor-agent: ${err.message}` });
343
- });
344
- });
345
- }
346
- /**
347
- * Execute cursor-agent command with retries for transient errors
348
- */
349
- async function cursorAgentSend(options) {
350
- const laneName = options.signalDir ? path.basename(path.dirname(options.signalDir)) : 'agent';
351
- return (0, failure_policy_1.withRetry)(laneName, () => cursorAgentSendRaw(options), (res) => ({ ok: res.ok, error: res.error }), { maxRetries: 3 });
352
- }
353
- /**
354
- * Extract dependency change request from agent response
355
- */
356
- function extractDependencyRequest(text) {
357
- const t = String(text || '');
358
- const marker = 'DEPENDENCY_CHANGE_REQUIRED';
359
- if (!t.includes(marker)) {
360
- return { required: false, raw: t };
361
- }
362
- const after = t.split(marker).slice(1).join(marker);
363
- const match = after.match(/\{[\s\S]*?\}/);
364
- if (match) {
365
- try {
366
- return {
367
- required: true,
368
- plan: JSON.parse(match[0]),
369
- raw: t,
370
- };
371
- }
372
- catch {
373
- return { required: true, raw: t };
374
- }
375
- }
376
- return { required: true, raw: t };
377
- }
378
- /**
379
- * Inter-task state file name
380
- */
381
- const LANE_STATE_FILE = '_cursorflow/lane-state.json';
382
- /**
383
- * Dependency request file name - agent writes here when dependency changes are needed
384
- */
385
- const DEPENDENCY_REQUEST_FILE = '_cursorflow/dependency-request.json';
386
- /**
387
- * Read dependency request from file if it exists
388
- */
389
- function readDependencyRequestFile(worktreeDir) {
390
- const filePath = (0, path_1.safeJoin)(worktreeDir, DEPENDENCY_REQUEST_FILE);
391
- if (!fs.existsSync(filePath)) {
392
- return { required: false };
393
- }
394
- try {
395
- const content = fs.readFileSync(filePath, 'utf8');
396
- const plan = JSON.parse(content);
397
- // Validate required fields
398
- if (plan.reason && Array.isArray(plan.commands) && plan.commands.length > 0) {
399
- logger.info(`📦 Dependency request file detected: ${filePath}`);
400
- return { required: true, plan };
401
- }
402
- logger.warn(`Invalid dependency request file format: ${filePath}`);
403
- return { required: false };
404
- }
405
- catch (e) {
406
- logger.warn(`Failed to parse dependency request file: ${e}`);
407
- return { required: false };
408
- }
409
- }
410
- /**
411
- * Clear dependency request file after processing
412
- */
413
- function clearDependencyRequestFile(worktreeDir) {
414
- const filePath = (0, path_1.safeJoin)(worktreeDir, DEPENDENCY_REQUEST_FILE);
415
- if (fs.existsSync(filePath)) {
416
- try {
417
- fs.unlinkSync(filePath);
418
- logger.info(`🗑️ Cleared dependency request file: ${filePath}`);
419
- }
420
- catch (e) {
421
- logger.warn(`Failed to clear dependency request file: ${e}`);
422
- }
423
- }
424
- }
425
- /**
426
- * Wrap prompt with dependency policy instructions (legacy, used by tests)
427
- */
428
- function wrapPromptForDependencyPolicy(prompt, policy) {
429
- if (policy.allowDependencyChange && !policy.lockfileReadOnly) {
430
- return prompt;
431
- }
432
- let wrapped = `### 📦 Dependency Policy\n`;
433
- wrapped += `- allowDependencyChange: ${policy.allowDependencyChange}\n`;
434
- wrapped += `- lockfileReadOnly: ${policy.lockfileReadOnly}\n\n`;
435
- wrapped += prompt;
436
- return wrapped;
437
- }
438
- /**
439
- * Wrap prompt with global context, dependency policy, and worktree instructions
440
- */
441
- function wrapPrompt(prompt, config, options = {}) {
442
- const { noGit = false, isWorktree = true, previousState = null } = options;
443
- // 1. PREFIX: Environment & Worktree context
444
- let wrapped = `### 🛠 Environment & Context\n`;
445
- wrapped += `- **Workspace**: 당신은 독립된 **Git 워크트리** (프로젝트 루트)에서 작업 중입니다.\n`;
446
- wrapped += `- **Path Rule**: 모든 파일 참조 및 터미널 명령어는 **현재 디렉토리(./)**를 기준으로 하세요.\n`;
447
- if (isWorktree) {
448
- wrapped += `- **File Availability**: Git 추적 파일만 존재합니다. (node_modules, .env 등은 기본적으로 없음)\n`;
449
- }
450
- // 2. Previous Task State (if available)
451
- if (previousState) {
452
- wrapped += `\n### 💡 Previous Task State\n`;
453
- wrapped += `이전 태스크에서 전달된 상태 정보입니다:\n`;
454
- wrapped += `\`\`\`json\n${previousState}\n\`\`\`\n`;
455
- }
456
- // 3. Dependency Policy (Integrated)
457
- const policy = config.dependencyPolicy;
458
- wrapped += `\n### 📦 Dependency Policy\n`;
459
- wrapped += `- allowDependencyChange: ${policy.allowDependencyChange}\n`;
460
- wrapped += `- lockfileReadOnly: ${policy.lockfileReadOnly}\n`;
461
- if (noGit) {
462
- wrapped += `- NO_GIT_MODE: Git 명령어를 사용하지 마세요. 파일 수정만 가능합니다.\n`;
463
- }
464
- wrapped += `\n**📦 Dependency Change Rules:**\n`;
465
- wrapped += `1. 코드를 수정하기 전, 의존성 변경이 필요한지 **먼저** 판단하세요.\n`;
466
- wrapped += `2. 의존성 변경이 필요하다면:\n`;
467
- wrapped += ` - **다른 파일을 절대 수정하지 마세요.**\n`;
468
- wrapped += ` - 아래 JSON을 \`./${DEPENDENCY_REQUEST_FILE}\` 파일에 저장하세요:\n`;
469
- wrapped += ` \`\`\`json\n`;
470
- wrapped += ` {\n`;
471
- wrapped += ` "reason": "왜 이 의존성이 필요한지 설명",\n`;
472
- wrapped += ` "changes": ["add lodash@^4.17.21", "remove unused-pkg"],\n`;
473
- wrapped += ` "commands": ["pnpm add lodash@^4.17.21", "pnpm remove unused-pkg"],\n`;
474
- wrapped += ` "notes": "추가 참고사항 (선택)" \n`;
475
- wrapped += ` }\n`;
476
- wrapped += ` \`\`\`\n`;
477
- wrapped += ` - 파일 저장 후 **즉시 작업을 종료**하세요. 오케스트레이터가 처리합니다.\n`;
478
- wrapped += `3. 의존성 변경이 불필요하면 바로 본 작업을 진행하세요.\n`;
479
- wrapped += `\n---\n\n${prompt}\n\n---\n`;
480
- // 4. SUFFIX: Task Completion & Git Requirements
481
- wrapped += `\n### 📝 Task Completion Requirements\n`;
482
- wrapped += `**반드시 다음 순서로 작업을 마무리하세요:**\n\n`;
483
- if (!noGit) {
484
- wrapped += `1. **Git Commit & Push** (필수!):\n`;
485
- wrapped += ` \`\`\`bash\n`;
486
- wrapped += ` git add -A\n`;
487
- wrapped += ` git commit -m "feat: <작업 내용 요약>"\n`;
488
- wrapped += ` git push origin HEAD\n`;
489
- wrapped += ` \`\`\`\n`;
490
- wrapped += ` ⚠️ 커밋과 푸시 없이 작업을 종료하면 변경사항이 손실됩니다!\n\n`;
491
- }
492
- wrapped += `2. **State Passing**: 다음 태스크로 전달할 정보가 있다면 \`./${LANE_STATE_FILE}\`에 JSON으로 저장하세요.\n\n`;
493
- wrapped += `3. **Summary**: 작업 완료 후 다음을 요약해 주세요:\n`;
494
- wrapped += ` - 생성/수정된 파일 목록\n`;
495
- wrapped += ` - 주요 변경 사항\n`;
496
- wrapped += ` - 커밋 해시 (git log --oneline -1)\n\n`;
497
- wrapped += `4. 지시된 문서(docs/...)를 찾을 수 없다면 즉시 보고하세요.\n`;
498
- return wrapped;
499
- }
500
- /**
501
- * Apply file permissions based on dependency policy
502
- */
503
- function applyDependencyFilePermissions(worktreeDir, policy) {
504
- const targets = [];
505
- if (!policy.allowDependencyChange) {
506
- targets.push('package.json');
507
- }
508
- if (policy.lockfileReadOnly) {
509
- targets.push('pnpm-lock.yaml', 'package-lock.json', 'yarn.lock');
510
- }
511
- for (const file of targets) {
512
- const filePath = (0, path_1.safeJoin)(worktreeDir, file);
513
- if (!fs.existsSync(filePath))
514
- continue;
515
- try {
516
- const stats = fs.statSync(filePath);
517
- const mode = stats.mode & 0o777;
518
- fs.chmodSync(filePath, mode & ~0o222); // Remove write bits
519
- }
520
- catch {
521
- // Best effort
522
- }
523
- }
524
- }
525
- /**
526
- * Wait for task-level dependencies to be completed by other lanes
527
- * Now uses the enhanced dependency module with timeout support
528
- */
529
- async function waitForTaskDependencies(deps, runDir, options = {}) {
530
- if (!deps || deps.length === 0)
531
- return;
532
- const lanesRoot = path.dirname(runDir);
533
- const result = await (0, dependency_1.waitForTaskDependencies)(deps, lanesRoot, {
534
- timeoutMs: options.timeoutMs || 30 * 60 * 1000, // 30 minutes default
535
- pollIntervalMs: options.pollIntervalMs || 5000,
536
- onTimeout: options.onTimeout || 'fail',
537
- onProgress: (pending, completed) => {
538
- if (completed.length > 0) {
539
- logger.info(`Dependencies progress: ${completed.length}/${deps.length} completed`);
540
- }
541
- },
542
- });
543
- if (!result.success) {
544
- if (result.timedOut) {
545
- throw new Error(`Dependency wait timed out after ${Math.round(result.elapsedMs / 1000)}s. Pending: ${result.failedDependencies.join(', ')}`);
546
- }
547
- throw new Error(`Dependencies failed: ${result.failedDependencies.join(', ')}`);
548
- }
549
- }
550
- /**
551
- * Merge branches from dependency lanes with safe merge
552
- */
553
- async function mergeDependencyBranches(deps, runDir, worktreeDir) {
554
- if (!deps || deps.length === 0)
555
- return;
556
- const lanesRoot = path.dirname(runDir);
557
- const lanesToMerge = new Set(deps.map(d => d.split(':')[0]));
558
- for (const laneName of lanesToMerge) {
559
- const depStatePath = (0, path_1.safeJoin)(lanesRoot, laneName, 'state.json');
560
- if (!fs.existsSync(depStatePath))
561
- continue;
562
- try {
563
- const state = (0, state_1.loadState)(depStatePath);
564
- if (!state?.pipelineBranch)
565
- continue;
566
- logger.info(`Merging branch from ${laneName}: ${state.pipelineBranch}`);
567
- // Ensure we have the latest
568
- git.runGit(['fetch', 'origin', state.pipelineBranch], { cwd: worktreeDir, silent: true });
569
- // Use safe merge with conflict detection
570
- const mergeResult = git.safeMerge(state.pipelineBranch, {
571
- cwd: worktreeDir,
572
- noFf: true,
573
- message: `chore: merge task dependency from ${laneName}`,
574
- abortOnConflict: true,
575
- });
576
- if (!mergeResult.success) {
577
- if (mergeResult.conflict) {
578
- logger.error(`Merge conflict with ${laneName}: ${mergeResult.conflictingFiles.join(', ')}`);
579
- throw new Error(`Merge conflict: ${mergeResult.conflictingFiles.join(', ')}`);
580
- }
581
- throw new Error(mergeResult.error || 'Merge failed');
582
- }
583
- logger.success(`✓ Merged ${laneName}`);
584
- }
585
- catch (e) {
586
- logger.error(`Failed to merge branch from ${laneName}: ${e}`);
587
- throw e;
588
- }
589
- }
590
- }
591
- /**
592
- * Run a single task
593
- */
594
- async function runTask({ task, config, index, worktreeDir, pipelineBranch, taskBranch, chatId, runDir, noGit = false, }) {
595
- const model = task.model || config.model || 'sonnet-4.5';
596
- const timeout = task.timeout || config.timeout;
597
- const convoPath = (0, path_1.safeJoin)(runDir, 'conversation.jsonl');
598
- logger.section(`[${index + 1}/${config.tasks.length}] ${task.name}`);
599
- logger.info(`Model: ${model}`);
600
- if (noGit) {
601
- logger.info('🚫 noGit mode: skipping branch operations');
602
- }
603
- else {
604
- logger.info(`Branch: ${taskBranch}`);
605
- }
606
- events_1.events.emit('task.started', {
607
- taskName: task.name,
608
- taskBranch,
609
- index,
610
- });
611
- // Checkout task branch (skip in noGit mode)
612
- if (!noGit) {
613
- git.runGit(['checkout', '-B', taskBranch], { cwd: worktreeDir });
614
- }
615
- // Apply dependency permissions
616
- applyDependencyFilePermissions(worktreeDir, config.dependencyPolicy);
617
- // Read previous task state if available
618
- let previousState = null;
619
- const stateFilePath = (0, path_1.safeJoin)(worktreeDir, LANE_STATE_FILE);
620
- if (fs.existsSync(stateFilePath)) {
621
- try {
622
- previousState = fs.readFileSync(stateFilePath, 'utf8');
623
- logger.info('Loaded previous task state from _cursorflow/lane-state.json');
624
- }
625
- catch (e) {
626
- logger.warn(`Failed to read inter-task state: ${e}`);
627
- }
628
- }
629
- // Wrap prompt with context, previous state, and completion instructions
630
- const wrappedPrompt = wrapPrompt(task.prompt, config, {
631
- noGit,
632
- isWorktree: !noGit,
633
- previousState
634
- });
635
- // Log ONLY the original prompt to keep logs clean
636
- (0, state_1.appendLog)(convoPath, (0, state_1.createConversationEntry)('user', task.prompt, {
637
- task: task.name,
638
- model,
639
- }));
640
- logger.info('Sending prompt to agent...');
641
- const startTime = Date.now();
642
- events_1.events.emit('agent.prompt_sent', {
643
- taskName: task.name,
644
- model,
645
- promptLength: wrappedPrompt.length,
646
- });
647
- const r1 = await cursorAgentSend({
648
- workspaceDir: worktreeDir,
649
- chatId,
650
- prompt: wrappedPrompt,
651
- model,
652
- signalDir: runDir,
653
- timeout,
654
- enableIntervention: config.enableIntervention,
655
- outputFormat: config.agentOutputFormat,
656
- taskName: task.name,
657
- });
658
- const duration = Date.now() - startTime;
659
- events_1.events.emit('agent.response_received', {
660
- taskName: task.name,
661
- ok: r1.ok,
662
- duration,
663
- responseLength: r1.resultText?.length || 0,
664
- error: r1.error,
665
- });
666
- (0, state_1.appendLog)(convoPath, (0, state_1.createConversationEntry)('assistant', r1.resultText || r1.error || 'No response', {
667
- task: task.name,
668
- model,
669
- }));
670
- if (!r1.ok) {
671
- events_1.events.emit('task.failed', {
672
- taskName: task.name,
673
- taskBranch,
674
- error: r1.error,
675
- });
676
- return {
677
- taskName: task.name,
678
- taskBranch,
679
- status: 'ERROR',
680
- error: r1.error,
681
- };
682
- }
683
- // Check for dependency request (file-based takes priority, then text-based)
684
- const fileDepReq = readDependencyRequestFile(worktreeDir);
685
- const textDepReq = extractDependencyRequest(r1.resultText || '');
686
- // Determine which request to use (file-based is preferred as it's more structured)
687
- const depReq = fileDepReq.required ? fileDepReq : textDepReq;
688
- if (depReq.required) {
689
- logger.info(`📦 Dependency change requested: ${depReq.plan?.reason || 'No reason provided'}`);
690
- if (depReq.plan) {
691
- logger.info(` Commands: ${depReq.plan.commands.join(', ')}`);
692
- }
693
- if (!config.dependencyPolicy.allowDependencyChange) {
694
- // Clear the file so it doesn't persist after resolution
695
- clearDependencyRequestFile(worktreeDir);
696
- return {
697
- taskName: task.name,
698
- taskBranch,
699
- status: 'BLOCKED_DEPENDENCY',
700
- dependencyRequest: depReq.plan || null,
701
- };
702
- }
703
- }
704
- // Push task branch (skip in noGit mode)
705
- if (!noGit) {
706
- git.push(taskBranch, { cwd: worktreeDir, setUpstream: true });
707
- }
708
- // Automatic Review
709
- const reviewEnabled = config.reviewAllTasks || task.acceptanceCriteria?.length || config.enableReview;
710
- if (reviewEnabled) {
711
- logger.section(`🔍 Reviewing Task: ${task.name}`);
712
- const reviewResult = await (0, reviewer_1.runReviewLoop)({
713
- taskResult: {
714
- taskName: task.name,
715
- taskBranch: taskBranch,
716
- acceptanceCriteria: task.acceptanceCriteria,
717
- },
718
- worktreeDir,
719
- runDir,
720
- config,
721
- workChatId: chatId,
722
- model, // Use the same model as requested
723
- cursorAgentSend,
724
- cursorAgentCreateChat,
725
- });
726
- if (!reviewResult.approved) {
727
- logger.error(`❌ Task review failed after ${reviewResult.iterations} iterations`);
728
- return {
729
- taskName: task.name,
730
- taskBranch,
731
- status: 'ERROR',
732
- error: reviewResult.error || 'Task failed to pass review criteria',
733
- };
734
- }
735
- }
736
- events_1.events.emit('task.completed', {
737
- taskName: task.name,
738
- taskBranch,
739
- status: 'FINISHED',
740
- });
741
- return {
742
- taskName: task.name,
743
- taskBranch,
744
- status: 'FINISHED',
745
- };
746
- }
747
- /**
748
- * Run all tasks in sequence
749
- */
750
- async function runTasks(tasksFile, config, runDir, options = {}) {
751
- const startIndex = options.startIndex || 0;
752
- const noGit = options.noGit || config.noGit || false;
753
- if (noGit) {
754
- logger.info('🚫 Running in noGit mode - Git operations will be skipped');
755
- }
756
- // Validate configuration before starting
757
- logger.info('Validating task configuration...');
758
- try {
759
- validateTaskConfig(config);
760
- logger.success('✓ Configuration valid');
761
- }
762
- catch (validationError) {
763
- logger.error('❌ Configuration validation failed');
764
- logger.error(` ${validationError.message}`);
765
- throw validationError;
766
- }
767
- // Run preflight checks (can be skipped for resume)
768
- if (!options.skipPreflight && startIndex === 0) {
769
- logger.info('Running preflight checks...');
770
- const preflight = await (0, health_1.preflightCheck)({
771
- requireRemote: !noGit,
772
- requireAuth: true,
773
- });
774
- if (!preflight.canProceed) {
775
- (0, health_1.printPreflightReport)(preflight);
776
- throw new Error('Preflight check failed. Please fix the blockers above.');
777
- }
778
- if (preflight.warnings.length > 0) {
779
- for (const warning of preflight.warnings) {
780
- logger.warn(`⚠️ ${warning}`);
781
- }
782
- }
783
- logger.success('✓ Preflight checks passed');
784
- }
785
- // Warn if baseBranch is set in config (it will be ignored)
786
- if (config.baseBranch) {
787
- logger.warn(`⚠️ config.baseBranch="${config.baseBranch}" will be ignored. Using current branch instead.`);
788
- }
789
- // Ensure cursor-agent is installed
790
- (0, cursor_agent_1.ensureCursorAgent)();
791
- // Check authentication before starting
792
- logger.info('Checking Cursor authentication...');
793
- const authStatus = (0, cursor_agent_1.checkCursorAuth)();
794
- if (!authStatus.authenticated) {
795
- logger.error('❌ Cursor authentication failed');
796
- logger.error(` ${authStatus.message}`);
797
- if (authStatus.details) {
798
- logger.error(` Details: ${authStatus.details}`);
799
- }
800
- if (authStatus.help) {
801
- logger.error(` ${authStatus.help}`);
802
- }
803
- console.log('');
804
- (0, cursor_agent_1.printAuthHelp)();
805
- throw new Error('Cursor authentication required. Please authenticate and try again.');
806
- }
807
- logger.success('✓ Cursor authentication OK');
808
- // In noGit mode, we don't need repoRoot - use current directory
809
- const repoRoot = noGit ? process.cwd() : git.getMainRepoRoot();
810
- // ALWAYS use current branch as base - ignore config.baseBranch
811
- // This ensures dependency structure is maintained in the worktree
812
- const currentBranch = noGit ? 'main' : git.getCurrentBranch(repoRoot);
813
- logger.info(`📍 Base branch: ${currentBranch} (current branch)`);
814
- // Load existing state if resuming
815
- const statePath = (0, path_1.safeJoin)(runDir, 'state.json');
816
- let state = null;
817
- if (fs.existsSync(statePath)) {
818
- // Check if state needs recovery
819
- if ((0, state_1.stateNeedsRecovery)(statePath)) {
820
- logger.warn('State file indicates incomplete previous run. Attempting recovery...');
821
- const repairedState = (0, state_1.repairLaneState)(statePath);
822
- if (repairedState) {
823
- state = repairedState;
824
- logger.success('✓ State recovered');
825
- }
826
- else {
827
- logger.warn('Could not recover state. Starting fresh.');
828
- }
829
- }
830
- else {
831
- state = (0, state_1.loadState)(statePath);
832
- // Validate loaded state
833
- if (state) {
834
- const validation = (0, state_1.validateLaneState)(statePath, {
835
- checkWorktree: !noGit,
836
- checkBranch: !noGit,
837
- autoRepair: true,
838
- });
839
- if (!validation.valid) {
840
- logger.warn(`State validation issues: ${validation.issues.join(', ')}`);
841
- if (validation.repaired) {
842
- logger.info('State was auto-repaired');
843
- state = validation.repairedState || state;
844
- }
845
- }
846
- }
847
- }
848
- }
849
- const randomSuffix = Math.random().toString(36).substring(2, 7);
850
- const pipelineBranch = state?.pipelineBranch || config.pipelineBranch || `${config.branchPrefix || 'cursorflow/'}${Date.now().toString(36)}-${randomSuffix}`;
851
- // In noGit mode, use a simple local directory instead of worktree
852
- // Flatten the path by replacing slashes with hyphens to avoid race conditions in parent directory creation
853
- const worktreeDir = state?.worktreeDir || config.worktreeDir || (noGit
854
- ? (0, path_1.safeJoin)(repoRoot, config.worktreeRoot || '_cursorflow/workdir', pipelineBranch.replace(/\//g, '-'))
855
- : (0, path_1.safeJoin)(repoRoot, config.worktreeRoot || '_cursorflow/worktrees', pipelineBranch.replace(/\//g, '-')));
856
- if (startIndex === 0) {
857
- logger.section('🚀 Starting Pipeline');
858
- }
859
- else {
860
- logger.section(`🔁 Resuming Pipeline from task ${startIndex + 1}`);
861
- }
862
- logger.info(`Pipeline Branch: ${pipelineBranch}`);
863
- logger.info(`Worktree: ${worktreeDir}`);
864
- logger.info(`Tasks: ${config.tasks.length}`);
865
- // Create worktree only if starting fresh and worktree doesn't exist
866
- if (!fs.existsSync(worktreeDir)) {
867
- if (noGit) {
868
- // In noGit mode, just create the directory
869
- logger.info(`Creating work directory: ${worktreeDir}`);
870
- fs.mkdirSync(worktreeDir, { recursive: true });
871
- }
872
- else {
873
- // Use a simple retry mechanism for Git worktree creation to handle potential race conditions
874
- let retries = 3;
875
- let lastError = null;
876
- while (retries > 0) {
877
- try {
878
- // Ensure parent directory exists before calling git worktree
879
- const worktreeParent = path.dirname(worktreeDir);
880
- if (!fs.existsSync(worktreeParent)) {
881
- fs.mkdirSync(worktreeParent, { recursive: true });
882
- }
883
- // Always use the current branch (already captured at start) as the base branch
884
- git.createWorktree(worktreeDir, pipelineBranch, {
885
- baseBranch: currentBranch,
886
- cwd: repoRoot,
887
- });
888
- break; // Success
889
- }
890
- catch (e) {
891
- lastError = e;
892
- retries--;
893
- if (retries > 0) {
894
- const delay = Math.floor(Math.random() * 1000) + 500;
895
- logger.warn(`Worktree creation failed, retrying in ${delay}ms... (${retries} retries left)`);
896
- await new Promise(resolve => setTimeout(resolve, delay));
897
- }
898
- }
899
- }
900
- if (retries === 0 && lastError) {
901
- throw new Error(`Failed to create Git worktree after retries: ${lastError.message}`);
902
- }
903
- }
904
- }
905
- else if (!noGit) {
906
- // If it exists but we are in Git mode, ensure it's actually a worktree and on the right branch
907
- logger.info(`Reusing existing worktree: ${worktreeDir}`);
908
- try {
909
- git.runGit(['checkout', pipelineBranch], { cwd: worktreeDir });
910
- }
911
- catch (e) {
912
- // If checkout fails, maybe the worktree is in a weird state.
913
- // For now, just log it. In a more robust impl, we might want to repair it.
914
- logger.warn(`Failed to checkout branch ${pipelineBranch} in existing worktree: ${e}`);
915
- }
916
- }
917
- // Create chat
918
- logger.info('Creating chat session...');
919
- const chatId = cursorAgentCreateChat();
920
- // Initialize state if not loaded
921
- if (!state) {
922
- state = {
923
- status: 'running',
924
- pipelineBranch,
925
- worktreeDir,
926
- totalTasks: config.tasks.length,
927
- currentTaskIndex: 0,
928
- label: pipelineBranch,
929
- startTime: Date.now(),
930
- endTime: null,
931
- error: null,
932
- dependencyRequest: null,
933
- tasksFile, // Store tasks file for resume
934
- dependsOn: config.dependsOn || [],
935
- completedTasks: [],
936
- };
937
- }
938
- else {
939
- state.status = 'running';
940
- state.error = null;
941
- state.dependencyRequest = null;
942
- state.pipelineBranch = pipelineBranch;
943
- state.worktreeDir = worktreeDir;
944
- state.label = state.label || pipelineBranch;
945
- state.dependsOn = config.dependsOn || [];
946
- state.completedTasks = state.completedTasks || [];
947
- }
948
- (0, state_1.saveState)(statePath, state);
949
- // Merge dependencies if any (skip in noGit mode)
950
- if (!noGit && startIndex === 0 && config.dependsOn && config.dependsOn.length > 0) {
951
- logger.section('🔗 Merging Dependencies');
952
- // The runDir for the lane is passed in. Dependencies are in ../<depName> relative to this runDir
953
- const lanesRoot = path.dirname(runDir);
954
- for (const depName of config.dependsOn) {
955
- const depRunDir = path.join(lanesRoot, depName); // nosemgrep
956
- const depStatePath = path.join(depRunDir, 'state.json'); // nosemgrep
957
- if (!fs.existsSync(depStatePath)) {
958
- logger.warn(`Dependency state not found for ${depName} at ${depStatePath}`);
959
- continue;
960
- }
961
- try {
962
- const depState = JSON.parse(fs.readFileSync(depStatePath, 'utf8'));
963
- if (depState.status !== 'completed') {
964
- logger.warn(`Dependency ${depName} is in status ${depState.status}, merge might be incomplete`);
965
- }
966
- if (depState.pipelineBranch) {
967
- logger.info(`Merging dependency branch: ${depState.pipelineBranch} (${depName})`);
968
- // Fetch first to ensure we have the branch
969
- git.runGit(['fetch', 'origin', depState.pipelineBranch], { cwd: worktreeDir, silent: true });
970
- // Merge
971
- git.merge(depState.pipelineBranch, {
972
- cwd: worktreeDir,
973
- noFf: true,
974
- message: `chore: merge dependency ${depName} (${depState.pipelineBranch})`
975
- });
976
- // Log changed files
977
- const stats = git.getLastOperationStats(worktreeDir);
978
- if (stats) {
979
- logger.info('Changed files:\n' + stats);
980
- }
981
- }
982
- }
983
- catch (e) {
984
- logger.error(`Failed to merge dependency ${depName}: ${e}`);
985
- }
986
- }
987
- // Push the merged state
988
- git.push(pipelineBranch, { cwd: worktreeDir });
989
- }
990
- else if (noGit && startIndex === 0 && config.dependsOn && config.dependsOn.length > 0) {
991
- logger.info('⚠️ Dependencies specified but Git is disabled - copying files instead of merging');
992
- // The runDir for the lane is passed in. Dependencies are in ../<depName> relative to this runDir
993
- const lanesRoot = path.dirname(runDir);
994
- for (const depName of config.dependsOn) {
995
- const depRunDir = (0, path_1.safeJoin)(lanesRoot, depName);
996
- const depStatePath = (0, path_1.safeJoin)(depRunDir, 'state.json');
997
- if (!fs.existsSync(depStatePath)) {
998
- continue;
999
- }
1000
- try {
1001
- const depState = JSON.parse(fs.readFileSync(depStatePath, 'utf8'));
1002
- if (depState.worktreeDir && fs.existsSync(depState.worktreeDir)) {
1003
- logger.info(`Copying files from dependency ${depName}: ${depState.worktreeDir} → ${worktreeDir}`);
1004
- // Use a simple recursive copy (excluding Git and internal dirs)
1005
- const copyFiles = (src, dest) => {
1006
- if (!fs.existsSync(dest))
1007
- fs.mkdirSync(dest, { recursive: true });
1008
- const entries = fs.readdirSync(src, { withFileTypes: true });
1009
- for (const entry of entries) {
1010
- if (entry.name === '.git' || entry.name === '_cursorflow' || entry.name === 'node_modules')
1011
- continue;
1012
- const srcPath = (0, path_1.safeJoin)(src, entry.name);
1013
- const destPath = (0, path_1.safeJoin)(dest, entry.name);
1014
- if (entry.isDirectory()) {
1015
- copyFiles(srcPath, destPath);
1016
- }
1017
- else {
1018
- fs.copyFileSync(srcPath, destPath);
1019
- }
1020
- }
1021
- };
1022
- copyFiles(depState.worktreeDir, worktreeDir);
1023
- }
1024
- }
1025
- catch (e) {
1026
- logger.error(`Failed to copy dependency ${depName}: ${e}`);
1027
- }
1028
- }
1029
- }
1030
- // Run tasks
1031
- const results = [];
1032
- const laneName = state.label || path.basename(runDir);
1033
- for (let i = startIndex; i < config.tasks.length; i++) {
1034
- const task = config.tasks[i];
1035
- const taskBranch = `${pipelineBranch}--${String(i + 1).padStart(2, '0')}-${task.name}`;
1036
- // Create checkpoint before each task
1037
- try {
1038
- await (0, checkpoint_1.createCheckpoint)(laneName, runDir, noGit ? null : worktreeDir, {
1039
- description: `Before task ${i + 1}: ${task.name}`,
1040
- maxCheckpoints: 5,
1041
- });
1042
- }
1043
- catch (e) {
1044
- logger.warn(`Failed to create checkpoint: ${e.message}`);
1045
- }
1046
- // Handle task-level dependencies
1047
- if (task.dependsOn && task.dependsOn.length > 0) {
1048
- state.status = 'waiting';
1049
- state.waitingFor = task.dependsOn;
1050
- (0, state_1.saveState)(statePath, state);
1051
- try {
1052
- // Use enhanced dependency wait with timeout
1053
- await waitForTaskDependencies(task.dependsOn, runDir, {
1054
- timeoutMs: config.timeout || 30 * 60 * 1000,
1055
- onTimeout: 'fail',
1056
- });
1057
- if (!noGit) {
1058
- await mergeDependencyBranches(task.dependsOn, runDir, worktreeDir);
1059
- }
1060
- state.status = 'running';
1061
- state.waitingFor = [];
1062
- (0, state_1.saveState)(statePath, state);
1063
- }
1064
- catch (e) {
1065
- state.status = 'failed';
1066
- state.waitingFor = [];
1067
- state.error = e.message;
1068
- (0, state_1.saveState)(statePath, state);
1069
- logger.error(`Task dependency wait/merge failed: ${e.message}`);
1070
- // Try to restore from checkpoint
1071
- const latestCheckpoint = (0, checkpoint_1.getLatestCheckpoint)(runDir);
1072
- if (latestCheckpoint) {
1073
- logger.info(`💾 Checkpoint available: ${latestCheckpoint.id}`);
1074
- logger.info(` Resume with: cursorflow resume --checkpoint ${latestCheckpoint.id}`);
1075
- }
1076
- process.exit(1);
1077
- }
1078
- }
1079
- const result = await runTask({
1080
- task,
1081
- config,
1082
- index: i,
1083
- worktreeDir,
1084
- pipelineBranch,
1085
- taskBranch,
1086
- chatId,
1087
- runDir,
1088
- noGit,
1089
- });
1090
- results.push(result);
1091
- // Update state
1092
- state.currentTaskIndex = i + 1;
1093
- state.completedTasks = state.completedTasks || [];
1094
- if (!state.completedTasks.includes(task.name)) {
1095
- state.completedTasks.push(task.name);
1096
- }
1097
- (0, state_1.saveState)(statePath, state);
1098
- // Handle blocked or error
1099
- if (result.status === 'BLOCKED_DEPENDENCY') {
1100
- state.status = 'failed';
1101
- state.dependencyRequest = result.dependencyRequest || null;
1102
- (0, state_1.saveState)(statePath, state);
1103
- if (result.dependencyRequest) {
1104
- events_1.events.emit('lane.dependency_requested', {
1105
- laneName: state.label,
1106
- dependencyRequest: result.dependencyRequest,
1107
- });
1108
- }
1109
- logger.warn('Task blocked on dependency change');
1110
- process.exit(2);
1111
- }
1112
- if (result.status !== 'FINISHED') {
1113
- state.status = 'failed';
1114
- state.error = result.error || 'Unknown error';
1115
- (0, state_1.saveState)(statePath, state);
1116
- logger.error(`Task failed: ${result.error}`);
1117
- process.exit(1);
1118
- }
1119
- // Merge into pipeline (skip in noGit mode)
1120
- if (!noGit) {
1121
- logger.info(`Merging ${taskBranch} → ${pipelineBranch}`);
1122
- git.merge(taskBranch, { cwd: worktreeDir, noFf: true });
1123
- // Log changed files
1124
- const stats = git.getLastOperationStats(worktreeDir);
1125
- if (stats) {
1126
- logger.info('Changed files:\n' + stats);
1127
- }
1128
- git.push(pipelineBranch, { cwd: worktreeDir });
1129
- }
1130
- else {
1131
- logger.info(`✓ Task ${task.name} completed (noGit mode - no branch operations)`);
1132
- }
1133
- }
1134
- // Complete
1135
- state.status = 'completed';
1136
- state.endTime = Date.now();
1137
- (0, state_1.saveState)(statePath, state);
1138
- // Log final file summary
1139
- if (noGit) {
1140
- const getFileSummary = (dir) => {
1141
- let stats = { files: 0, dirs: 0 };
1142
- if (!fs.existsSync(dir))
1143
- return stats;
1144
- const entries = fs.readdirSync(dir, { withFileTypes: true });
1145
- for (const entry of entries) {
1146
- if (entry.name === '.git' || entry.name === '_cursorflow' || entry.name === 'node_modules')
1147
- continue;
1148
- if (entry.isDirectory()) {
1149
- stats.dirs++;
1150
- const sub = getFileSummary((0, path_1.safeJoin)(dir, entry.name));
1151
- stats.files += sub.files;
1152
- stats.dirs += sub.dirs;
1153
- }
1154
- else {
1155
- stats.files++;
1156
- }
1157
- }
1158
- return stats;
1159
- };
1160
- const summary = getFileSummary(worktreeDir);
1161
- logger.info(`Final Workspace Summary (noGit): ${summary.files} files, ${summary.dirs} directories created/modified`);
1162
- }
1163
- else {
1164
- try {
1165
- // Always use current branch for comparison (already captured at start)
1166
- const stats = git.runGit(['diff', '--stat', currentBranch, pipelineBranch], { cwd: repoRoot, silent: true });
1167
- if (stats) {
1168
- logger.info('Final Workspace Summary (Git):\n' + stats);
1169
- }
1170
- }
1171
- catch (e) {
1172
- // Ignore
1173
- }
1174
- }
1175
- logger.success('All tasks completed!');
1176
- return results;
1177
- }
48
+ const events_1 = require("../utils/events");
49
+ // Re-export everything from modular components
50
+ __exportStar(require("./runner/index"), exports);
51
+ // Import necessary parts for the CLI entry point
52
+ const pipeline_1 = require("./runner/pipeline");
1178
53
  /**
1179
54
  * CLI entry point
1180
55
  */
@@ -1235,9 +110,9 @@ if (require.main === module) {
1235
110
  lockfileReadOnly: globalConfig?.lockfileReadOnly ?? true,
1236
111
  };
1237
112
  // Add agent output format default
1238
- config.agentOutputFormat = config.agentOutputFormat || globalConfig?.agentOutputFormat || 'stream-json';
113
+ config.agentOutputFormat = config.agentOutputFormat || globalConfig?.agentOutputFormat || 'json';
1239
114
  // Run tasks
1240
- runTasks(tasksFile, config, runDir, { startIndex, noGit })
115
+ (0, pipeline_1.runTasks)(tasksFile, config, runDir, { startIndex, noGit })
1241
116
  .then(() => {
1242
117
  process.exit(0);
1243
118
  })