@litmers/cursorflow-orchestrator 0.1.13 → 0.1.15
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 +37 -0
- package/README.md +83 -2
- package/commands/cursorflow-clean.md +20 -6
- package/commands/cursorflow-prepare.md +1 -1
- package/commands/cursorflow-resume.md +127 -6
- package/commands/cursorflow-run.md +2 -2
- package/commands/cursorflow-signal.md +11 -4
- package/dist/cli/clean.js +164 -12
- package/dist/cli/clean.js.map +1 -1
- package/dist/cli/index.d.ts +1 -0
- package/dist/cli/index.js +6 -1
- package/dist/cli/index.js.map +1 -1
- package/dist/cli/logs.d.ts +8 -0
- package/dist/cli/logs.js +759 -0
- package/dist/cli/logs.js.map +1 -0
- package/dist/cli/monitor.js +113 -30
- package/dist/cli/monitor.js.map +1 -1
- package/dist/cli/prepare.js +1 -1
- package/dist/cli/resume.js +367 -18
- package/dist/cli/resume.js.map +1 -1
- package/dist/cli/run.js +9 -0
- package/dist/cli/run.js.map +1 -1
- package/dist/cli/signal.js +34 -20
- package/dist/cli/signal.js.map +1 -1
- package/dist/core/orchestrator.d.ts +13 -1
- package/dist/core/orchestrator.js +396 -35
- package/dist/core/orchestrator.js.map +1 -1
- package/dist/core/reviewer.d.ts +2 -0
- package/dist/core/reviewer.js +24 -2
- package/dist/core/reviewer.js.map +1 -1
- package/dist/core/runner.d.ts +9 -3
- package/dist/core/runner.js +266 -61
- package/dist/core/runner.js.map +1 -1
- package/dist/utils/config.js +38 -1
- package/dist/utils/config.js.map +1 -1
- package/dist/utils/enhanced-logger.d.ts +210 -0
- package/dist/utils/enhanced-logger.js +1030 -0
- package/dist/utils/enhanced-logger.js.map +1 -0
- package/dist/utils/events.d.ts +59 -0
- package/dist/utils/events.js +37 -0
- package/dist/utils/events.js.map +1 -0
- package/dist/utils/git.d.ts +11 -0
- package/dist/utils/git.js +40 -0
- package/dist/utils/git.js.map +1 -1
- package/dist/utils/logger.d.ts +2 -0
- package/dist/utils/logger.js +4 -1
- package/dist/utils/logger.js.map +1 -1
- package/dist/utils/types.d.ts +132 -1
- package/dist/utils/webhook.d.ts +5 -0
- package/dist/utils/webhook.js +109 -0
- package/dist/utils/webhook.js.map +1 -0
- package/examples/README.md +1 -1
- package/package.json +2 -1
- package/scripts/patches/test-cursor-agent.js +1 -1
- package/scripts/simple-logging-test.sh +97 -0
- package/scripts/test-real-cursor-lifecycle.sh +289 -0
- package/scripts/test-real-logging.sh +289 -0
- package/scripts/test-streaming-multi-task.sh +247 -0
- package/src/cli/clean.ts +170 -13
- package/src/cli/index.ts +4 -1
- package/src/cli/logs.ts +863 -0
- package/src/cli/monitor.ts +123 -30
- package/src/cli/prepare.ts +1 -1
- package/src/cli/resume.ts +463 -22
- package/src/cli/run.ts +10 -0
- package/src/cli/signal.ts +43 -27
- package/src/core/orchestrator.ts +458 -36
- package/src/core/reviewer.ts +40 -4
- package/src/core/runner.ts +293 -60
- package/src/utils/config.ts +41 -1
- package/src/utils/enhanced-logger.ts +1166 -0
- package/src/utils/events.ts +117 -0
- package/src/utils/git.ts +40 -0
- package/src/utils/logger.ts +4 -1
- package/src/utils/types.ts +160 -1
- package/src/utils/webhook.ts +85 -0
package/src/core/reviewer.ts
CHANGED
|
@@ -8,6 +8,7 @@ import * as logger from '../utils/logger';
|
|
|
8
8
|
import { appendLog, createConversationEntry } from '../utils/state';
|
|
9
9
|
import * as path from 'path';
|
|
10
10
|
import { ReviewResult, ReviewIssue, TaskResult, RunnerConfig, AgentSendResult } from '../utils/types';
|
|
11
|
+
import { events } from '../utils/events';
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* Build review prompt
|
|
@@ -148,7 +149,13 @@ export async function reviewTask({ taskResult, worktreeDir, runDir, config, curs
|
|
|
148
149
|
worktreeDir: string;
|
|
149
150
|
runDir: string;
|
|
150
151
|
config: RunnerConfig;
|
|
151
|
-
cursorAgentSend: (options: {
|
|
152
|
+
cursorAgentSend: (options: {
|
|
153
|
+
workspaceDir: string;
|
|
154
|
+
chatId: string;
|
|
155
|
+
prompt: string;
|
|
156
|
+
model?: string;
|
|
157
|
+
outputFormat?: 'stream-json' | 'json' | 'plain';
|
|
158
|
+
}) => AgentSendResult;
|
|
152
159
|
cursorAgentCreateChat: () => string;
|
|
153
160
|
}): Promise<ReviewResult> {
|
|
154
161
|
const reviewPrompt = buildReviewPrompt({
|
|
@@ -159,12 +166,18 @@ export async function reviewTask({ taskResult, worktreeDir, runDir, config, curs
|
|
|
159
166
|
|
|
160
167
|
logger.info(`Reviewing: ${taskResult.taskName}`);
|
|
161
168
|
|
|
169
|
+
events.emit('review.started', {
|
|
170
|
+
taskName: taskResult.taskName,
|
|
171
|
+
taskBranch: taskResult.taskBranch,
|
|
172
|
+
});
|
|
173
|
+
|
|
162
174
|
const reviewChatId = cursorAgentCreateChat();
|
|
163
|
-
const reviewResult = cursorAgentSend({
|
|
175
|
+
const reviewResult = await cursorAgentSend({
|
|
164
176
|
workspaceDir: worktreeDir,
|
|
165
177
|
chatId: reviewChatId,
|
|
166
178
|
prompt: reviewPrompt,
|
|
167
179
|
model: config.reviewModel || 'sonnet-4.5-thinking',
|
|
180
|
+
outputFormat: config.agentOutputFormat,
|
|
168
181
|
});
|
|
169
182
|
|
|
170
183
|
const review = parseReviewResult(reviewResult.resultText || '');
|
|
@@ -178,6 +191,13 @@ export async function reviewTask({ taskResult, worktreeDir, runDir, config, curs
|
|
|
178
191
|
|
|
179
192
|
logger.info(`Review result: ${review.status} (${review.issues?.length || 0} issues)`);
|
|
180
193
|
|
|
194
|
+
events.emit('review.completed', {
|
|
195
|
+
taskName: taskResult.taskName,
|
|
196
|
+
status: review.status,
|
|
197
|
+
issueCount: review.issues?.length || 0,
|
|
198
|
+
summary: review.summary,
|
|
199
|
+
});
|
|
200
|
+
|
|
181
201
|
return review;
|
|
182
202
|
}
|
|
183
203
|
|
|
@@ -190,7 +210,13 @@ export async function runReviewLoop({ taskResult, worktreeDir, runDir, config, w
|
|
|
190
210
|
runDir: string;
|
|
191
211
|
config: RunnerConfig;
|
|
192
212
|
workChatId: string;
|
|
193
|
-
cursorAgentSend: (options: {
|
|
213
|
+
cursorAgentSend: (options: {
|
|
214
|
+
workspaceDir: string;
|
|
215
|
+
chatId: string;
|
|
216
|
+
prompt: string;
|
|
217
|
+
model?: string;
|
|
218
|
+
outputFormat?: 'stream-json' | 'json' | 'plain';
|
|
219
|
+
}) => AgentSendResult;
|
|
194
220
|
cursorAgentCreateChat: () => string;
|
|
195
221
|
}): Promise<{ approved: boolean; review: ReviewResult; iterations: number; error?: string }> {
|
|
196
222
|
const maxIterations = config.maxReviewIterations || 3;
|
|
@@ -209,6 +235,10 @@ export async function runReviewLoop({ taskResult, worktreeDir, runDir, config, w
|
|
|
209
235
|
|
|
210
236
|
if (currentReview.status === 'approved') {
|
|
211
237
|
logger.success(`Review passed: ${taskResult.taskName} (iteration ${iteration + 1})`);
|
|
238
|
+
events.emit('review.approved', {
|
|
239
|
+
taskName: taskResult.taskName,
|
|
240
|
+
iterations: iteration + 1,
|
|
241
|
+
});
|
|
212
242
|
return { approved: true, review: currentReview, iterations: iteration + 1 };
|
|
213
243
|
}
|
|
214
244
|
|
|
@@ -216,6 +246,11 @@ export async function runReviewLoop({ taskResult, worktreeDir, runDir, config, w
|
|
|
216
246
|
|
|
217
247
|
if (iteration >= maxIterations) {
|
|
218
248
|
logger.warn(`Max review iterations (${maxIterations}) reached: ${taskResult.taskName}`);
|
|
249
|
+
events.emit('review.rejected', {
|
|
250
|
+
taskName: taskResult.taskName,
|
|
251
|
+
reason: 'Max iterations reached',
|
|
252
|
+
iterations: iteration,
|
|
253
|
+
});
|
|
219
254
|
break;
|
|
220
255
|
}
|
|
221
256
|
|
|
@@ -223,11 +258,12 @@ export async function runReviewLoop({ taskResult, worktreeDir, runDir, config, w
|
|
|
223
258
|
logger.info(`Sending feedback (iteration ${iteration}/${maxIterations})`);
|
|
224
259
|
const feedbackPrompt = buildFeedbackPrompt(currentReview);
|
|
225
260
|
|
|
226
|
-
const fixResult = cursorAgentSend({
|
|
261
|
+
const fixResult = await cursorAgentSend({
|
|
227
262
|
workspaceDir: worktreeDir,
|
|
228
263
|
chatId: workChatId,
|
|
229
264
|
prompt: feedbackPrompt,
|
|
230
265
|
model: config.model,
|
|
266
|
+
outputFormat: config.agentOutputFormat,
|
|
231
267
|
});
|
|
232
268
|
|
|
233
269
|
if (!fixResult.ok) {
|
package/src/core/runner.ts
CHANGED
|
@@ -12,6 +12,10 @@ import * as git from '../utils/git';
|
|
|
12
12
|
import * as logger from '../utils/logger';
|
|
13
13
|
import { ensureCursorAgent, checkCursorAuth, printAuthHelp } from '../utils/cursor-agent';
|
|
14
14
|
import { saveState, appendLog, createConversationEntry } from '../utils/state';
|
|
15
|
+
import { events } from '../utils/events';
|
|
16
|
+
import { loadConfig } from '../utils/config';
|
|
17
|
+
import { registerWebhooks } from '../utils/webhook';
|
|
18
|
+
import { stripAnsi } from '../utils/enhanced-logger';
|
|
15
19
|
import {
|
|
16
20
|
RunnerConfig,
|
|
17
21
|
Task,
|
|
@@ -107,8 +111,8 @@ function parseJsonFromStdout(stdout: string): any {
|
|
|
107
111
|
return null;
|
|
108
112
|
}
|
|
109
113
|
|
|
110
|
-
/** Default timeout:
|
|
111
|
-
const DEFAULT_TIMEOUT_MS =
|
|
114
|
+
/** Default timeout: 10 minutes */
|
|
115
|
+
const DEFAULT_TIMEOUT_MS = 600000;
|
|
112
116
|
|
|
113
117
|
/** Heartbeat interval: 30 seconds */
|
|
114
118
|
const HEARTBEAT_INTERVAL_MS = 30000;
|
|
@@ -171,7 +175,7 @@ export function validateTaskConfig(config: RunnerConfig): void {
|
|
|
171
175
|
/**
|
|
172
176
|
* Execute cursor-agent command with streaming and better error handling
|
|
173
177
|
*/
|
|
174
|
-
export async function cursorAgentSend({ workspaceDir, chatId, prompt, model, signalDir, timeout, enableIntervention }: {
|
|
178
|
+
export async function cursorAgentSend({ workspaceDir, chatId, prompt, model, signalDir, timeout, enableIntervention, outputFormat }: {
|
|
175
179
|
workspaceDir: string;
|
|
176
180
|
chatId: string;
|
|
177
181
|
prompt: string;
|
|
@@ -180,10 +184,14 @@ export async function cursorAgentSend({ workspaceDir, chatId, prompt, model, sig
|
|
|
180
184
|
timeout?: number;
|
|
181
185
|
/** Enable stdin piping for intervention feature (may cause buffering issues on some systems) */
|
|
182
186
|
enableIntervention?: boolean;
|
|
187
|
+
/** Output format for cursor-agent (default: 'stream-json') */
|
|
188
|
+
outputFormat?: 'stream-json' | 'json' | 'plain';
|
|
183
189
|
}): Promise<AgentSendResult> {
|
|
190
|
+
// Use stream-json format for structured output with tool calls and results
|
|
191
|
+
const format = outputFormat || 'stream-json';
|
|
184
192
|
const args = [
|
|
185
193
|
'--print',
|
|
186
|
-
'--output-format',
|
|
194
|
+
'--output-format', format,
|
|
187
195
|
'--workspace', workspaceDir,
|
|
188
196
|
...(model ? ['--model', model] : []),
|
|
189
197
|
'--resume', chatId,
|
|
@@ -240,6 +248,7 @@ export async function cursorAgentSend({ workspaceDir, chatId, prompt, model, sig
|
|
|
240
248
|
|
|
241
249
|
let fullStdout = '';
|
|
242
250
|
let fullStderr = '';
|
|
251
|
+
let timeoutHandle: NodeJS.Timeout;
|
|
243
252
|
|
|
244
253
|
// Heartbeat logging to show progress
|
|
245
254
|
let lastHeartbeat = Date.now();
|
|
@@ -251,13 +260,15 @@ export async function cursorAgentSend({ workspaceDir, chatId, prompt, model, sig
|
|
|
251
260
|
}, HEARTBEAT_INTERVAL_MS);
|
|
252
261
|
const startTime = Date.now();
|
|
253
262
|
|
|
254
|
-
// Watch for "intervention.txt"
|
|
263
|
+
// Watch for "intervention.txt" or "timeout.txt" signal files
|
|
255
264
|
const interventionPath = signalDir ? path.join(signalDir, 'intervention.txt') : null;
|
|
256
|
-
|
|
265
|
+
const timeoutPath = signalDir ? path.join(signalDir, 'timeout.txt') : null;
|
|
266
|
+
let signalWatcher: fs.FSWatcher | null = null;
|
|
257
267
|
|
|
258
|
-
if (
|
|
259
|
-
|
|
260
|
-
|
|
268
|
+
if (signalDir && fs.existsSync(signalDir)) {
|
|
269
|
+
signalWatcher = fs.watch(signalDir, (event, filename) => {
|
|
270
|
+
// Handle intervention
|
|
271
|
+
if (filename === 'intervention.txt' && interventionPath && fs.existsSync(interventionPath)) {
|
|
261
272
|
try {
|
|
262
273
|
const message = fs.readFileSync(interventionPath, 'utf8').trim();
|
|
263
274
|
if (message) {
|
|
@@ -274,6 +285,40 @@ export async function cursorAgentSend({ workspaceDir, chatId, prompt, model, sig
|
|
|
274
285
|
logger.warn('Failed to read intervention file');
|
|
275
286
|
}
|
|
276
287
|
}
|
|
288
|
+
|
|
289
|
+
// Handle dynamic timeout update
|
|
290
|
+
if (filename === 'timeout.txt' && timeoutPath && fs.existsSync(timeoutPath)) {
|
|
291
|
+
try {
|
|
292
|
+
const newTimeoutStr = fs.readFileSync(timeoutPath, 'utf8').trim();
|
|
293
|
+
const newTimeoutMs = parseInt(newTimeoutStr);
|
|
294
|
+
|
|
295
|
+
if (!isNaN(newTimeoutMs) && newTimeoutMs > 0) {
|
|
296
|
+
logger.info(`⏱ Dynamic timeout update: ${Math.round(newTimeoutMs / 1000)}s`);
|
|
297
|
+
|
|
298
|
+
// Clear old timeout
|
|
299
|
+
if (timeoutHandle) clearTimeout(timeoutHandle);
|
|
300
|
+
|
|
301
|
+
// Set new timeout based on total elapsed time
|
|
302
|
+
const elapsed = Date.now() - startTime;
|
|
303
|
+
const remaining = Math.max(1000, newTimeoutMs - elapsed);
|
|
304
|
+
|
|
305
|
+
timeoutHandle = setTimeout(() => {
|
|
306
|
+
clearInterval(heartbeatInterval);
|
|
307
|
+
child.kill();
|
|
308
|
+
const totalSec = Math.round(newTimeoutMs / 1000);
|
|
309
|
+
resolve({
|
|
310
|
+
ok: false,
|
|
311
|
+
exitCode: -1,
|
|
312
|
+
error: `cursor-agent timed out after updated limit of ${totalSec} seconds.`,
|
|
313
|
+
});
|
|
314
|
+
}, remaining);
|
|
315
|
+
|
|
316
|
+
fs.unlinkSync(timeoutPath); // Clear it
|
|
317
|
+
}
|
|
318
|
+
} catch (e) {
|
|
319
|
+
logger.warn('Failed to read timeout update file');
|
|
320
|
+
}
|
|
321
|
+
}
|
|
277
322
|
});
|
|
278
323
|
}
|
|
279
324
|
|
|
@@ -295,7 +340,7 @@ export async function cursorAgentSend({ workspaceDir, chatId, prompt, model, sig
|
|
|
295
340
|
});
|
|
296
341
|
}
|
|
297
342
|
|
|
298
|
-
|
|
343
|
+
timeoutHandle = setTimeout(() => {
|
|
299
344
|
clearInterval(heartbeatInterval);
|
|
300
345
|
child.kill();
|
|
301
346
|
const timeoutSec = Math.round(timeoutMs / 1000);
|
|
@@ -309,7 +354,7 @@ export async function cursorAgentSend({ workspaceDir, chatId, prompt, model, sig
|
|
|
309
354
|
child.on('close', (code) => {
|
|
310
355
|
clearTimeout(timeoutHandle);
|
|
311
356
|
clearInterval(heartbeatInterval);
|
|
312
|
-
if (
|
|
357
|
+
if (signalWatcher) signalWatcher.close();
|
|
313
358
|
|
|
314
359
|
const json = parseJsonFromStdout(fullStdout);
|
|
315
360
|
|
|
@@ -384,33 +429,31 @@ export function extractDependencyRequest(text: string): { required: boolean; pla
|
|
|
384
429
|
/**
|
|
385
430
|
* Wrap prompt with dependency policy
|
|
386
431
|
*/
|
|
387
|
-
export function wrapPromptForDependencyPolicy(prompt: string, policy: DependencyPolicy): string {
|
|
388
|
-
|
|
432
|
+
export function wrapPromptForDependencyPolicy(prompt: string, policy: DependencyPolicy, options: { noGit?: boolean } = {}): string {
|
|
433
|
+
const { noGit = false } = options;
|
|
434
|
+
|
|
435
|
+
if (policy.allowDependencyChange && !policy.lockfileReadOnly && !noGit) {
|
|
389
436
|
return prompt;
|
|
390
437
|
}
|
|
391
438
|
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
{ "reason": "...", "changes": [...], "commands": ["pnpm add ..."], "notes": "..." }
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
---
|
|
412
|
-
|
|
413
|
-
${prompt}`;
|
|
439
|
+
let rules = '# Dependency Policy (MUST FOLLOW)\n\nYou are running in a restricted lane.\n\n';
|
|
440
|
+
|
|
441
|
+
rules += `- allowDependencyChange: ${policy.allowDependencyChange}\n`;
|
|
442
|
+
rules += `- lockfileReadOnly: ${policy.lockfileReadOnly}\n`;
|
|
443
|
+
|
|
444
|
+
if (noGit) {
|
|
445
|
+
rules += '- NO_GIT_MODE: Git is disabled. DO NOT run any git commands (commit, push, etc.). Just edit files.\n';
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
rules += '\nRules:\n';
|
|
449
|
+
rules += '- BEFORE making any code changes, decide whether dependency changes are required.\n';
|
|
450
|
+
rules += '- If dependency changes are required, DO NOT change any files. Instead reply with:\n\n';
|
|
451
|
+
rules += 'DEPENDENCY_CHANGE_REQUIRED\n';
|
|
452
|
+
rules += '```json\n{ "reason": "...", "changes": [...], "commands": ["pnpm add ..."], "notes": "..." }\n```\n\n';
|
|
453
|
+
rules += 'Then STOP.\n';
|
|
454
|
+
rules += '- If dependency changes are NOT required, proceed normally.\n';
|
|
455
|
+
|
|
456
|
+
return `${rules}\n---\n\n${prompt}`;
|
|
414
457
|
}
|
|
415
458
|
|
|
416
459
|
/**
|
|
@@ -449,9 +492,11 @@ export async function runTask({
|
|
|
449
492
|
config,
|
|
450
493
|
index,
|
|
451
494
|
worktreeDir,
|
|
495
|
+
pipelineBranch,
|
|
452
496
|
taskBranch,
|
|
453
497
|
chatId,
|
|
454
498
|
runDir,
|
|
499
|
+
noGit = false,
|
|
455
500
|
}: {
|
|
456
501
|
task: Task;
|
|
457
502
|
config: RunnerConfig;
|
|
@@ -461,22 +506,35 @@ export async function runTask({
|
|
|
461
506
|
taskBranch: string;
|
|
462
507
|
chatId: string;
|
|
463
508
|
runDir: string;
|
|
509
|
+
noGit?: boolean;
|
|
464
510
|
}): Promise<TaskExecutionResult> {
|
|
465
511
|
const model = task.model || config.model || 'sonnet-4.5';
|
|
466
512
|
const convoPath = path.join(runDir, 'conversation.jsonl');
|
|
467
513
|
|
|
468
514
|
logger.section(`[${index + 1}/${config.tasks.length}] ${task.name}`);
|
|
469
515
|
logger.info(`Model: ${model}`);
|
|
470
|
-
|
|
516
|
+
if (noGit) {
|
|
517
|
+
logger.info('🚫 noGit mode: skipping branch operations');
|
|
518
|
+
} else {
|
|
519
|
+
logger.info(`Branch: ${taskBranch}`);
|
|
520
|
+
}
|
|
471
521
|
|
|
472
|
-
|
|
473
|
-
|
|
522
|
+
events.emit('task.started', {
|
|
523
|
+
taskName: task.name,
|
|
524
|
+
taskBranch,
|
|
525
|
+
index,
|
|
526
|
+
});
|
|
527
|
+
|
|
528
|
+
// Checkout task branch (skip in noGit mode)
|
|
529
|
+
if (!noGit) {
|
|
530
|
+
git.runGit(['checkout', '-B', taskBranch], { cwd: worktreeDir });
|
|
531
|
+
}
|
|
474
532
|
|
|
475
533
|
// Apply dependency permissions
|
|
476
534
|
applyDependencyFilePermissions(worktreeDir, config.dependencyPolicy);
|
|
477
535
|
|
|
478
536
|
// Run prompt
|
|
479
|
-
const prompt1 = wrapPromptForDependencyPolicy(task.prompt, config.dependencyPolicy);
|
|
537
|
+
const prompt1 = wrapPromptForDependencyPolicy(task.prompt, config.dependencyPolicy, { noGit });
|
|
480
538
|
|
|
481
539
|
appendLog(convoPath, createConversationEntry('user', prompt1, {
|
|
482
540
|
task: task.name,
|
|
@@ -484,6 +542,13 @@ export async function runTask({
|
|
|
484
542
|
}));
|
|
485
543
|
|
|
486
544
|
logger.info('Sending prompt to agent...');
|
|
545
|
+
const startTime = Date.now();
|
|
546
|
+
events.emit('agent.prompt_sent', {
|
|
547
|
+
taskName: task.name,
|
|
548
|
+
model,
|
|
549
|
+
promptLength: prompt1.length,
|
|
550
|
+
});
|
|
551
|
+
|
|
487
552
|
const r1 = await cursorAgentSend({
|
|
488
553
|
workspaceDir: worktreeDir,
|
|
489
554
|
chatId,
|
|
@@ -492,14 +557,29 @@ export async function runTask({
|
|
|
492
557
|
signalDir: runDir,
|
|
493
558
|
timeout: config.timeout,
|
|
494
559
|
enableIntervention: config.enableIntervention,
|
|
560
|
+
outputFormat: config.agentOutputFormat,
|
|
495
561
|
});
|
|
496
562
|
|
|
563
|
+
const duration = Date.now() - startTime;
|
|
564
|
+
events.emit('agent.response_received', {
|
|
565
|
+
taskName: task.name,
|
|
566
|
+
ok: r1.ok,
|
|
567
|
+
duration,
|
|
568
|
+
responseLength: r1.resultText?.length || 0,
|
|
569
|
+
error: r1.error,
|
|
570
|
+
});
|
|
571
|
+
|
|
497
572
|
appendLog(convoPath, createConversationEntry('assistant', r1.resultText || r1.error || 'No response', {
|
|
498
573
|
task: task.name,
|
|
499
574
|
model,
|
|
500
575
|
}));
|
|
501
576
|
|
|
502
577
|
if (!r1.ok) {
|
|
578
|
+
events.emit('task.failed', {
|
|
579
|
+
taskName: task.name,
|
|
580
|
+
taskBranch,
|
|
581
|
+
error: r1.error,
|
|
582
|
+
});
|
|
503
583
|
return {
|
|
504
584
|
taskName: task.name,
|
|
505
585
|
taskBranch,
|
|
@@ -519,9 +599,17 @@ export async function runTask({
|
|
|
519
599
|
};
|
|
520
600
|
}
|
|
521
601
|
|
|
522
|
-
// Push task branch
|
|
523
|
-
|
|
602
|
+
// Push task branch (skip in noGit mode)
|
|
603
|
+
if (!noGit) {
|
|
604
|
+
git.push(taskBranch, { cwd: worktreeDir, setUpstream: true });
|
|
605
|
+
}
|
|
524
606
|
|
|
607
|
+
events.emit('task.completed', {
|
|
608
|
+
taskName: task.name,
|
|
609
|
+
taskBranch,
|
|
610
|
+
status: 'FINISHED',
|
|
611
|
+
});
|
|
612
|
+
|
|
525
613
|
return {
|
|
526
614
|
taskName: task.name,
|
|
527
615
|
taskBranch,
|
|
@@ -532,8 +620,13 @@ export async function runTask({
|
|
|
532
620
|
/**
|
|
533
621
|
* Run all tasks in sequence
|
|
534
622
|
*/
|
|
535
|
-
export async function runTasks(tasksFile: string, config: RunnerConfig, runDir: string, options: { startIndex?: number } = {}): Promise<TaskExecutionResult[]> {
|
|
623
|
+
export async function runTasks(tasksFile: string, config: RunnerConfig, runDir: string, options: { startIndex?: number; noGit?: boolean } = {}): Promise<TaskExecutionResult[]> {
|
|
536
624
|
const startIndex = options.startIndex || 0;
|
|
625
|
+
const noGit = options.noGit || config.noGit || false;
|
|
626
|
+
|
|
627
|
+
if (noGit) {
|
|
628
|
+
logger.info('🚫 Running in noGit mode - Git operations will be skipped');
|
|
629
|
+
}
|
|
537
630
|
|
|
538
631
|
// Validate configuration before starting
|
|
539
632
|
logger.info('Validating task configuration...');
|
|
@@ -573,7 +666,8 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
|
|
|
573
666
|
|
|
574
667
|
logger.success('✓ Cursor authentication OK');
|
|
575
668
|
|
|
576
|
-
|
|
669
|
+
// In noGit mode, we don't need repoRoot - use current directory
|
|
670
|
+
const repoRoot = noGit ? process.cwd() : git.getRepoRoot();
|
|
577
671
|
|
|
578
672
|
// Load existing state if resuming
|
|
579
673
|
const statePath = path.join(runDir, 'state.json');
|
|
@@ -584,7 +678,10 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
|
|
|
584
678
|
}
|
|
585
679
|
|
|
586
680
|
const pipelineBranch = state?.pipelineBranch || config.pipelineBranch || `${config.branchPrefix || 'cursorflow/'}${Date.now().toString(36)}`;
|
|
587
|
-
|
|
681
|
+
// In noGit mode, use a simple local directory instead of worktree
|
|
682
|
+
const worktreeDir = state?.worktreeDir || (noGit
|
|
683
|
+
? path.join(repoRoot, config.worktreeRoot || '_cursorflow/workdir', pipelineBranch.replace(/\//g, '-'))
|
|
684
|
+
: path.join(repoRoot, config.worktreeRoot || '_cursorflow/worktrees', pipelineBranch));
|
|
588
685
|
|
|
589
686
|
if (startIndex === 0) {
|
|
590
687
|
logger.section('🚀 Starting Pipeline');
|
|
@@ -598,10 +695,16 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
|
|
|
598
695
|
|
|
599
696
|
// Create worktree only if starting fresh
|
|
600
697
|
if (startIndex === 0 || !fs.existsSync(worktreeDir)) {
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
698
|
+
if (noGit) {
|
|
699
|
+
// In noGit mode, just create the directory
|
|
700
|
+
logger.info(`Creating work directory: ${worktreeDir}`);
|
|
701
|
+
fs.mkdirSync(worktreeDir, { recursive: true });
|
|
702
|
+
} else {
|
|
703
|
+
git.createWorktree(worktreeDir, pipelineBranch, {
|
|
704
|
+
baseBranch: config.baseBranch || 'main',
|
|
705
|
+
cwd: repoRoot,
|
|
706
|
+
});
|
|
707
|
+
}
|
|
605
708
|
}
|
|
606
709
|
|
|
607
710
|
// Create chat
|
|
@@ -633,8 +736,8 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
|
|
|
633
736
|
|
|
634
737
|
saveState(statePath, state);
|
|
635
738
|
|
|
636
|
-
// Merge dependencies if any
|
|
637
|
-
if (startIndex === 0 && config.dependsOn && config.dependsOn.length > 0) {
|
|
739
|
+
// Merge dependencies if any (skip in noGit mode)
|
|
740
|
+
if (!noGit && startIndex === 0 && config.dependsOn && config.dependsOn.length > 0) {
|
|
638
741
|
logger.section('🔗 Merging Dependencies');
|
|
639
742
|
|
|
640
743
|
// The runDir for the lane is passed in. Dependencies are in ../<depName> relative to this runDir
|
|
@@ -667,6 +770,12 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
|
|
|
667
770
|
noFf: true,
|
|
668
771
|
message: `chore: merge dependency ${depName} (${depState.pipelineBranch})`
|
|
669
772
|
});
|
|
773
|
+
|
|
774
|
+
// Log changed files
|
|
775
|
+
const stats = git.getLastOperationStats(worktreeDir);
|
|
776
|
+
if (stats) {
|
|
777
|
+
logger.info('Changed files:\n' + stats);
|
|
778
|
+
}
|
|
670
779
|
}
|
|
671
780
|
} catch (e) {
|
|
672
781
|
logger.error(`Failed to merge dependency ${depName}: ${e}`);
|
|
@@ -675,6 +784,50 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
|
|
|
675
784
|
|
|
676
785
|
// Push the merged state
|
|
677
786
|
git.push(pipelineBranch, { cwd: worktreeDir });
|
|
787
|
+
} else if (noGit && startIndex === 0 && config.dependsOn && config.dependsOn.length > 0) {
|
|
788
|
+
logger.info('⚠️ Dependencies specified but Git is disabled - copying files instead of merging');
|
|
789
|
+
|
|
790
|
+
// The runDir for the lane is passed in. Dependencies are in ../<depName> relative to this runDir
|
|
791
|
+
const lanesRoot = path.dirname(runDir);
|
|
792
|
+
|
|
793
|
+
for (const depName of config.dependsOn) {
|
|
794
|
+
const depRunDir = path.join(lanesRoot, depName);
|
|
795
|
+
const depStatePath = path.join(depRunDir, 'state.json');
|
|
796
|
+
|
|
797
|
+
if (!fs.existsSync(depStatePath)) {
|
|
798
|
+
continue;
|
|
799
|
+
}
|
|
800
|
+
|
|
801
|
+
try {
|
|
802
|
+
const depState = JSON.parse(fs.readFileSync(depStatePath, 'utf8')) as LaneState;
|
|
803
|
+
if (depState.worktreeDir && fs.existsSync(depState.worktreeDir)) {
|
|
804
|
+
logger.info(`Copying files from dependency ${depName}: ${depState.worktreeDir} → ${worktreeDir}`);
|
|
805
|
+
|
|
806
|
+
// Use a simple recursive copy (excluding Git and internal dirs)
|
|
807
|
+
const copyFiles = (src: string, dest: string) => {
|
|
808
|
+
if (!fs.existsSync(dest)) fs.mkdirSync(dest, { recursive: true });
|
|
809
|
+
const entries = fs.readdirSync(src, { withFileTypes: true });
|
|
810
|
+
|
|
811
|
+
for (const entry of entries) {
|
|
812
|
+
if (entry.name === '.git' || entry.name === '_cursorflow' || entry.name === 'node_modules') continue;
|
|
813
|
+
|
|
814
|
+
const srcPath = path.join(src, entry.name);
|
|
815
|
+
const destPath = path.join(dest, entry.name);
|
|
816
|
+
|
|
817
|
+
if (entry.isDirectory()) {
|
|
818
|
+
copyFiles(srcPath, destPath);
|
|
819
|
+
} else {
|
|
820
|
+
fs.copyFileSync(srcPath, destPath);
|
|
821
|
+
}
|
|
822
|
+
}
|
|
823
|
+
};
|
|
824
|
+
|
|
825
|
+
copyFiles(depState.worktreeDir, worktreeDir);
|
|
826
|
+
}
|
|
827
|
+
} catch (e) {
|
|
828
|
+
logger.error(`Failed to copy dependency ${depName}: ${e}`);
|
|
829
|
+
}
|
|
830
|
+
}
|
|
678
831
|
}
|
|
679
832
|
|
|
680
833
|
// Run tasks
|
|
@@ -693,6 +846,7 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
|
|
|
693
846
|
taskBranch,
|
|
694
847
|
chatId,
|
|
695
848
|
runDir,
|
|
849
|
+
noGit,
|
|
696
850
|
});
|
|
697
851
|
|
|
698
852
|
results.push(result);
|
|
@@ -706,6 +860,14 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
|
|
|
706
860
|
state.status = 'failed';
|
|
707
861
|
state.dependencyRequest = result.dependencyRequest || null;
|
|
708
862
|
saveState(statePath, state);
|
|
863
|
+
|
|
864
|
+
if (result.dependencyRequest) {
|
|
865
|
+
events.emit('lane.dependency_requested', {
|
|
866
|
+
laneName: state.label,
|
|
867
|
+
dependencyRequest: result.dependencyRequest,
|
|
868
|
+
});
|
|
869
|
+
}
|
|
870
|
+
|
|
709
871
|
logger.warn('Task blocked on dependency change');
|
|
710
872
|
process.exit(2);
|
|
711
873
|
}
|
|
@@ -718,10 +880,21 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
|
|
|
718
880
|
process.exit(1);
|
|
719
881
|
}
|
|
720
882
|
|
|
721
|
-
// Merge into pipeline
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
883
|
+
// Merge into pipeline (skip in noGit mode)
|
|
884
|
+
if (!noGit) {
|
|
885
|
+
logger.info(`Merging ${taskBranch} → ${pipelineBranch}`);
|
|
886
|
+
git.merge(taskBranch, { cwd: worktreeDir, noFf: true });
|
|
887
|
+
|
|
888
|
+
// Log changed files
|
|
889
|
+
const stats = git.getLastOperationStats(worktreeDir);
|
|
890
|
+
if (stats) {
|
|
891
|
+
logger.info('Changed files:\n' + stats);
|
|
892
|
+
}
|
|
893
|
+
|
|
894
|
+
git.push(pipelineBranch, { cwd: worktreeDir });
|
|
895
|
+
} else {
|
|
896
|
+
logger.info(`✓ Task ${task.name} completed (noGit mode - no branch operations)`);
|
|
897
|
+
}
|
|
725
898
|
}
|
|
726
899
|
|
|
727
900
|
// Complete
|
|
@@ -729,6 +902,41 @@ export async function runTasks(tasksFile: string, config: RunnerConfig, runDir:
|
|
|
729
902
|
state.endTime = Date.now();
|
|
730
903
|
saveState(statePath, state);
|
|
731
904
|
|
|
905
|
+
// Log final file summary
|
|
906
|
+
if (noGit) {
|
|
907
|
+
const getFileSummary = (dir: string): { files: number; dirs: number } => {
|
|
908
|
+
let stats = { files: 0, dirs: 0 };
|
|
909
|
+
if (!fs.existsSync(dir)) return stats;
|
|
910
|
+
|
|
911
|
+
const entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
912
|
+
for (const entry of entries) {
|
|
913
|
+
if (entry.name === '.git' || entry.name === '_cursorflow' || entry.name === 'node_modules') continue;
|
|
914
|
+
|
|
915
|
+
if (entry.isDirectory()) {
|
|
916
|
+
stats.dirs++;
|
|
917
|
+
const sub = getFileSummary(path.join(dir, entry.name));
|
|
918
|
+
stats.files += sub.files;
|
|
919
|
+
stats.dirs += sub.dirs;
|
|
920
|
+
} else {
|
|
921
|
+
stats.files++;
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
return stats;
|
|
925
|
+
};
|
|
926
|
+
|
|
927
|
+
const summary = getFileSummary(worktreeDir);
|
|
928
|
+
logger.info(`Final Workspace Summary (noGit): ${summary.files} files, ${summary.dirs} directories created/modified`);
|
|
929
|
+
} else {
|
|
930
|
+
try {
|
|
931
|
+
const stats = git.runGit(['diff', '--stat', config.baseBranch || 'main', pipelineBranch], { cwd: repoRoot, silent: true });
|
|
932
|
+
if (stats) {
|
|
933
|
+
logger.info('Final Workspace Summary (Git):\n' + stats);
|
|
934
|
+
}
|
|
935
|
+
} catch (e) {
|
|
936
|
+
// Ignore
|
|
937
|
+
}
|
|
938
|
+
}
|
|
939
|
+
|
|
732
940
|
logger.success('All tasks completed!');
|
|
733
941
|
return results;
|
|
734
942
|
}
|
|
@@ -747,11 +955,30 @@ if (require.main === module) {
|
|
|
747
955
|
const tasksFile = args[0]!;
|
|
748
956
|
const runDirIdx = args.indexOf('--run-dir');
|
|
749
957
|
const startIdxIdx = args.indexOf('--start-index');
|
|
750
|
-
|
|
958
|
+
const pipelineBranchIdx = args.indexOf('--pipeline-branch');
|
|
959
|
+
const noGit = args.includes('--no-git');
|
|
751
960
|
|
|
752
961
|
const runDir = runDirIdx >= 0 ? args[runDirIdx + 1]! : '.';
|
|
753
962
|
const startIndex = startIdxIdx >= 0 ? parseInt(args[startIdxIdx + 1] || '0') : 0;
|
|
754
|
-
|
|
963
|
+
const forcedPipelineBranch = pipelineBranchIdx >= 0 ? args[pipelineBranchIdx + 1] : null;
|
|
964
|
+
|
|
965
|
+
// Extract runId from runDir (format: .../runs/run-123/lanes/lane-name)
|
|
966
|
+
const parts = runDir.split(path.sep);
|
|
967
|
+
const runsIdx = parts.lastIndexOf('runs');
|
|
968
|
+
const runId = runsIdx >= 0 && parts[runsIdx + 1] ? parts[runsIdx + 1]! : `run-${Date.now()}`;
|
|
969
|
+
|
|
970
|
+
events.setRunId(runId);
|
|
971
|
+
|
|
972
|
+
// Load global config for defaults and webhooks
|
|
973
|
+
let globalConfig;
|
|
974
|
+
try {
|
|
975
|
+
globalConfig = loadConfig();
|
|
976
|
+
if (globalConfig.webhooks) {
|
|
977
|
+
registerWebhooks(globalConfig.webhooks);
|
|
978
|
+
}
|
|
979
|
+
} catch (e) {
|
|
980
|
+
// Non-blocking
|
|
981
|
+
}
|
|
755
982
|
|
|
756
983
|
if (!fs.existsSync(tasksFile)) {
|
|
757
984
|
console.error(`Tasks file not found: ${tasksFile}`);
|
|
@@ -762,19 +989,25 @@ if (require.main === module) {
|
|
|
762
989
|
let config: RunnerConfig;
|
|
763
990
|
try {
|
|
764
991
|
config = JSON.parse(fs.readFileSync(tasksFile, 'utf8')) as RunnerConfig;
|
|
992
|
+
if (forcedPipelineBranch) {
|
|
993
|
+
config.pipelineBranch = forcedPipelineBranch;
|
|
994
|
+
}
|
|
765
995
|
} catch (error: any) {
|
|
766
996
|
console.error(`Failed to load tasks file: ${error.message}`);
|
|
767
997
|
process.exit(1);
|
|
768
998
|
}
|
|
769
999
|
|
|
770
|
-
// Add
|
|
1000
|
+
// Add defaults from global config or hardcoded
|
|
771
1001
|
config.dependencyPolicy = config.dependencyPolicy || {
|
|
772
|
-
allowDependencyChange: false,
|
|
773
|
-
lockfileReadOnly: true,
|
|
1002
|
+
allowDependencyChange: globalConfig?.allowDependencyChange ?? false,
|
|
1003
|
+
lockfileReadOnly: globalConfig?.lockfileReadOnly ?? true,
|
|
774
1004
|
};
|
|
775
1005
|
|
|
1006
|
+
// Add agent output format default
|
|
1007
|
+
config.agentOutputFormat = config.agentOutputFormat || globalConfig?.agentOutputFormat || 'stream-json';
|
|
1008
|
+
|
|
776
1009
|
// Run tasks
|
|
777
|
-
runTasks(tasksFile, config, runDir, { startIndex })
|
|
1010
|
+
runTasks(tasksFile, config, runDir, { startIndex, noGit })
|
|
778
1011
|
.then(() => {
|
|
779
1012
|
process.exit(0);
|
|
780
1013
|
})
|