@litmers/cursorflow-orchestrator 0.1.14 → 0.1.18
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 +9 -0
- package/README.md +1 -0
- package/commands/cursorflow-run.md +2 -0
- package/commands/cursorflow-triggers.md +250 -0
- package/dist/cli/clean.js +1 -1
- package/dist/cli/clean.js.map +1 -1
- package/dist/cli/init.js +13 -8
- package/dist/cli/init.js.map +1 -1
- package/dist/cli/logs.js +66 -44
- package/dist/cli/logs.js.map +1 -1
- package/dist/cli/monitor.js +12 -3
- package/dist/cli/monitor.js.map +1 -1
- package/dist/cli/prepare.js +36 -13
- package/dist/cli/prepare.js.map +1 -1
- package/dist/cli/resume.js.map +1 -1
- package/dist/cli/run.js +7 -0
- package/dist/cli/run.js.map +1 -1
- package/dist/core/orchestrator.d.ts +3 -1
- package/dist/core/orchestrator.js +154 -11
- package/dist/core/orchestrator.js.map +1 -1
- package/dist/core/reviewer.d.ts +8 -4
- package/dist/core/reviewer.js +11 -7
- package/dist/core/reviewer.js.map +1 -1
- package/dist/core/runner.d.ts +17 -3
- package/dist/core/runner.js +326 -69
- package/dist/core/runner.js.map +1 -1
- package/dist/utils/config.js +17 -5
- package/dist/utils/config.js.map +1 -1
- package/dist/utils/doctor.js +28 -1
- package/dist/utils/doctor.js.map +1 -1
- package/dist/utils/enhanced-logger.d.ts +5 -4
- package/dist/utils/enhanced-logger.js +178 -43
- package/dist/utils/enhanced-logger.js.map +1 -1
- package/dist/utils/git.d.ts +6 -0
- package/dist/utils/git.js +15 -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/repro-thinking-logs.d.ts +1 -0
- package/dist/utils/repro-thinking-logs.js +80 -0
- package/dist/utils/repro-thinking-logs.js.map +1 -0
- package/dist/utils/types.d.ts +22 -0
- package/dist/utils/webhook.js +3 -0
- package/dist/utils/webhook.js.map +1 -1
- package/package.json +4 -1
- package/scripts/ai-security-check.js +3 -0
- package/scripts/local-security-gate.sh +9 -1
- package/scripts/patches/test-cursor-agent.js +1 -1
- package/scripts/verify-and-fix.sh +37 -0
- package/src/cli/clean.ts +1 -1
- package/src/cli/init.ts +12 -9
- package/src/cli/logs.ts +68 -43
- package/src/cli/monitor.ts +13 -4
- package/src/cli/prepare.ts +36 -15
- package/src/cli/resume.ts +1 -1
- package/src/cli/run.ts +8 -0
- package/src/core/orchestrator.ts +171 -11
- package/src/core/reviewer.ts +30 -11
- package/src/core/runner.ts +346 -71
- package/src/utils/config.ts +17 -6
- package/src/utils/doctor.ts +31 -1
- package/src/utils/enhanced-logger.ts +182 -48
- package/src/utils/git.ts +15 -0
- package/src/utils/logger.ts +4 -1
- package/src/utils/repro-thinking-logs.ts +54 -0
- package/src/utils/types.ts +22 -0
- package/src/utils/webhook.ts +3 -0
- package/scripts/simple-logging-test.sh +0 -97
- package/scripts/test-real-logging.sh +0 -289
- package/scripts/test-streaming-multi-task.sh +0 -247
package/src/utils/config.ts
CHANGED
|
@@ -17,7 +17,9 @@ export function findProjectRoot(cwd = process.cwd()): string {
|
|
|
17
17
|
|
|
18
18
|
while (current !== path.parse(current).root) {
|
|
19
19
|
const packagePath = path.join(current, 'package.json');
|
|
20
|
-
|
|
20
|
+
const configPath = path.join(current, 'cursorflow.config.js');
|
|
21
|
+
|
|
22
|
+
if (fs.existsSync(packagePath) || fs.existsSync(configPath)) {
|
|
21
23
|
return current;
|
|
22
24
|
}
|
|
23
25
|
current = path.dirname(current);
|
|
@@ -57,6 +59,7 @@ export function loadConfig(projectRoot: string | null = null): CursorFlowConfig
|
|
|
57
59
|
// Review
|
|
58
60
|
enableReview: false,
|
|
59
61
|
reviewModel: 'sonnet-4.5-thinking',
|
|
62
|
+
reviewAllTasks: false,
|
|
60
63
|
maxReviewIterations: 3,
|
|
61
64
|
|
|
62
65
|
// Lane defaults
|
|
@@ -72,6 +75,7 @@ export function loadConfig(projectRoot: string | null = null): CursorFlowConfig
|
|
|
72
75
|
// Advanced
|
|
73
76
|
worktreePrefix: 'cursorflow-',
|
|
74
77
|
maxConcurrentLanes: 10,
|
|
78
|
+
agentOutputFormat: 'stream-json',
|
|
75
79
|
|
|
76
80
|
// Webhooks
|
|
77
81
|
webhooks: [],
|
|
@@ -155,10 +159,6 @@ export function validateConfig(config: CursorFlowConfig): boolean {
|
|
|
155
159
|
export function createDefaultConfig(projectRoot: string, force = false): string {
|
|
156
160
|
const configPath = path.join(projectRoot, 'cursorflow.config.js');
|
|
157
161
|
|
|
158
|
-
if (fs.existsSync(configPath) && !force) {
|
|
159
|
-
throw new Error(`Config file already exists: ${configPath}`);
|
|
160
|
-
}
|
|
161
|
-
|
|
162
162
|
const template = `module.exports = {
|
|
163
163
|
// Directory configuration
|
|
164
164
|
tasksDir: '_cursorflow/tasks',
|
|
@@ -179,6 +179,7 @@ export function createDefaultConfig(projectRoot: string, force = false): string
|
|
|
179
179
|
// Review configuration
|
|
180
180
|
enableReview: false,
|
|
181
181
|
reviewModel: 'sonnet-4.5-thinking',
|
|
182
|
+
reviewAllTasks: false,
|
|
182
183
|
maxReviewIterations: 3,
|
|
183
184
|
|
|
184
185
|
// Lane configuration
|
|
@@ -194,6 +195,7 @@ export function createDefaultConfig(projectRoot: string, force = false): string
|
|
|
194
195
|
// Advanced
|
|
195
196
|
worktreePrefix: 'cursorflow-',
|
|
196
197
|
maxConcurrentLanes: 10,
|
|
198
|
+
agentOutputFormat: 'stream-json', // 'stream-json' | 'json' | 'plain'
|
|
197
199
|
|
|
198
200
|
// Webhook configuration
|
|
199
201
|
// webhooks: [
|
|
@@ -218,6 +220,15 @@ export function createDefaultConfig(projectRoot: string, force = false): string
|
|
|
218
220
|
};
|
|
219
221
|
`;
|
|
220
222
|
|
|
221
|
-
|
|
223
|
+
// Use atomic write with wx flag to avoid TOCTOU race condition (unless force is set)
|
|
224
|
+
try {
|
|
225
|
+
const writeFlag = force ? 'w' : 'wx';
|
|
226
|
+
fs.writeFileSync(configPath, template, { encoding: 'utf8', flag: writeFlag });
|
|
227
|
+
} catch (err: any) {
|
|
228
|
+
if (err.code === 'EEXIST') {
|
|
229
|
+
throw new Error(`Config file already exists: ${configPath}`);
|
|
230
|
+
}
|
|
231
|
+
throw err;
|
|
232
|
+
}
|
|
222
233
|
return configPath;
|
|
223
234
|
}
|
package/src/utils/doctor.ts
CHANGED
|
@@ -486,16 +486,46 @@ function validateBranchNames(
|
|
|
486
486
|
const remoteBranches = getAllRemoteBranches(repoRoot);
|
|
487
487
|
const allExistingBranches = new Set([...localBranches, ...remoteBranches]);
|
|
488
488
|
|
|
489
|
-
// Collect branch prefixes from lanes
|
|
489
|
+
// Collect branch prefixes and pipeline branches from lanes
|
|
490
490
|
const branchPrefixes: { laneName: string; prefix: string }[] = [];
|
|
491
|
+
const pipelineBranches: { laneName: string; branch: string }[] = [];
|
|
491
492
|
|
|
492
493
|
for (const lane of lanes) {
|
|
493
494
|
const branchPrefix = lane.json?.branchPrefix;
|
|
494
495
|
if (branchPrefix) {
|
|
495
496
|
branchPrefixes.push({ laneName: lane.fileName, prefix: branchPrefix });
|
|
496
497
|
}
|
|
498
|
+
|
|
499
|
+
const pipelineBranch = lane.json?.pipelineBranch;
|
|
500
|
+
if (pipelineBranch) {
|
|
501
|
+
pipelineBranches.push({ laneName: lane.fileName, branch: pipelineBranch });
|
|
502
|
+
}
|
|
497
503
|
}
|
|
498
504
|
|
|
505
|
+
// Check for pipeline branch collisions
|
|
506
|
+
const pipeMap = new Map<string, string[]>();
|
|
507
|
+
for (const { laneName, branch } of pipelineBranches) {
|
|
508
|
+
const existing = pipeMap.get(branch) || [];
|
|
509
|
+
existing.push(laneName);
|
|
510
|
+
pipeMap.set(branch, existing);
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
for (const [branch, laneNames] of pipeMap) {
|
|
514
|
+
if (laneNames.length > 1) {
|
|
515
|
+
addIssue(issues, {
|
|
516
|
+
id: 'branch.pipeline_collision',
|
|
517
|
+
severity: 'error',
|
|
518
|
+
title: 'Pipeline branch collision',
|
|
519
|
+
message: `Multiple lanes use the same pipelineBranch "${branch}": ${laneNames.join(', ')}`,
|
|
520
|
+
details: 'Each lane should have a unique pipelineBranch to avoid worktree conflicts during parallel execution.',
|
|
521
|
+
fixes: [
|
|
522
|
+
'Update the pipelineBranch in each lane JSON file to be unique',
|
|
523
|
+
'Or remove pipelineBranch to let CursorFlow generate unique ones',
|
|
524
|
+
],
|
|
525
|
+
});
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
|
|
499
529
|
// Check for branch prefix collisions between lanes
|
|
500
530
|
const prefixMap = new Map<string, string[]>();
|
|
501
531
|
for (const { laneName, prefix } of branchPrefixes) {
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
|
|
14
14
|
import * as fs from 'fs';
|
|
15
15
|
import * as path from 'path';
|
|
16
|
-
import {
|
|
16
|
+
import { Transform, TransformCallback } from 'stream';
|
|
17
17
|
import { EnhancedLogConfig } from './types';
|
|
18
18
|
|
|
19
19
|
// Re-export for backwards compatibility
|
|
@@ -163,6 +163,23 @@ export class StreamingMessageParser {
|
|
|
163
163
|
},
|
|
164
164
|
});
|
|
165
165
|
break;
|
|
166
|
+
|
|
167
|
+
case 'thinking':
|
|
168
|
+
// Thinking message (Claude 3.7+ etc.)
|
|
169
|
+
if (json.subtype === 'delta' && json.text) {
|
|
170
|
+
// Check if this is a new message or continuation
|
|
171
|
+
if (this.currentRole !== 'thinking') {
|
|
172
|
+
// Flush previous message if any
|
|
173
|
+
this.flush();
|
|
174
|
+
this.currentRole = 'thinking';
|
|
175
|
+
this.messageStartTime = json.timestamp_ms || Date.now();
|
|
176
|
+
}
|
|
177
|
+
this.currentMessage += json.text;
|
|
178
|
+
} else if (json.subtype === 'completed') {
|
|
179
|
+
// Thinking completed - flush immediately
|
|
180
|
+
this.flush();
|
|
181
|
+
}
|
|
182
|
+
break;
|
|
166
183
|
}
|
|
167
184
|
}
|
|
168
185
|
|
|
@@ -191,7 +208,7 @@ export class StreamingMessageParser {
|
|
|
191
208
|
}
|
|
192
209
|
|
|
193
210
|
export interface ParsedMessage {
|
|
194
|
-
type: 'system' | 'user' | 'assistant' | 'tool' | 'tool_result' | 'result';
|
|
211
|
+
type: 'system' | 'user' | 'assistant' | 'tool' | 'tool_result' | 'result' | 'thinking';
|
|
195
212
|
role: string;
|
|
196
213
|
content: string;
|
|
197
214
|
timestamp: number;
|
|
@@ -384,11 +401,13 @@ export class EnhancedLogManager {
|
|
|
384
401
|
private cleanTransform: CleanLogTransform | null = null;
|
|
385
402
|
private streamingParser: StreamingMessageParser | null = null;
|
|
386
403
|
private lineBuffer: string = '';
|
|
404
|
+
private onParsedMessage?: (msg: ParsedMessage) => void;
|
|
387
405
|
|
|
388
|
-
constructor(logDir: string, session: LogSession, config: Partial<EnhancedLogConfig> = {}) {
|
|
406
|
+
constructor(logDir: string, session: LogSession, config: Partial<EnhancedLogConfig> = {}, onParsedMessage?: (msg: ParsedMessage) => void) {
|
|
389
407
|
this.config = { ...DEFAULT_LOG_CONFIG, ...config };
|
|
390
408
|
this.session = session;
|
|
391
409
|
this.logDir = logDir;
|
|
410
|
+
this.onParsedMessage = onParsedMessage;
|
|
392
411
|
|
|
393
412
|
// Ensure log directory exists
|
|
394
413
|
fs.mkdirSync(logDir, { recursive: true });
|
|
@@ -450,6 +469,9 @@ export class EnhancedLogManager {
|
|
|
450
469
|
// Create streaming parser for readable log
|
|
451
470
|
this.streamingParser = new StreamingMessageParser((msg) => {
|
|
452
471
|
this.writeReadableMessage(msg);
|
|
472
|
+
if (this.onParsedMessage) {
|
|
473
|
+
this.onParsedMessage(msg);
|
|
474
|
+
}
|
|
453
475
|
});
|
|
454
476
|
}
|
|
455
477
|
|
|
@@ -464,41 +486,59 @@ export class EnhancedLogManager {
|
|
|
464
486
|
|
|
465
487
|
switch (msg.type) {
|
|
466
488
|
case 'system':
|
|
467
|
-
formatted =
|
|
489
|
+
formatted = `[${ts}] ⚙️ SYSTEM: ${msg.content}\n`;
|
|
468
490
|
break;
|
|
469
491
|
|
|
470
492
|
case 'user':
|
|
471
|
-
// Format user prompt nicely
|
|
472
|
-
const promptPreview = msg.content.length > 200
|
|
473
|
-
? msg.content.substring(0, 200) + '...'
|
|
474
|
-
: msg.content;
|
|
475
|
-
formatted = `\n${ts}\n┌─ 🧑 USER ─────────────────────────────────────────────\n${this.indentText(promptPreview, '│ ')}\n└───────────────────────────────────────────────────────\n`;
|
|
476
|
-
break;
|
|
477
|
-
|
|
478
493
|
case 'assistant':
|
|
479
494
|
case 'result':
|
|
480
|
-
// Format
|
|
495
|
+
// Format with brackets and line (compact)
|
|
496
|
+
const isUser = msg.type === 'user';
|
|
481
497
|
const isResult = msg.type === 'result';
|
|
482
|
-
const
|
|
498
|
+
const headerText = isUser ? '🧑 USER' : isResult ? '🤖 ASSISTANT (Final)' : '🤖 ASSISTANT';
|
|
483
499
|
const duration = msg.metadata?.duration_ms
|
|
484
500
|
? ` (${Math.round(msg.metadata.duration_ms / 1000)}s)`
|
|
485
501
|
: '';
|
|
486
|
-
|
|
502
|
+
|
|
503
|
+
const label = `[ ${headerText}${duration} ] `;
|
|
504
|
+
const totalWidth = 80;
|
|
505
|
+
const topBorder = `┌─${label}${'─'.repeat(Math.max(0, totalWidth - label.length - 2))}`;
|
|
506
|
+
const bottomBorder = `└─${'─'.repeat(totalWidth - 2)}`;
|
|
507
|
+
|
|
508
|
+
const lines = msg.content.split('\n');
|
|
509
|
+
formatted = `[${ts}] ${topBorder}\n`;
|
|
510
|
+
for (const line of lines) {
|
|
511
|
+
formatted += `[${ts}] │ ${line}\n`;
|
|
512
|
+
}
|
|
513
|
+
formatted += `[${ts}] ${bottomBorder}\n`;
|
|
487
514
|
break;
|
|
488
515
|
|
|
489
516
|
case 'tool':
|
|
490
|
-
|
|
491
|
-
formatted = `${ts} 🔧 ${msg.content}\n`;
|
|
517
|
+
formatted = `[${ts}] 🔧 TOOL: ${msg.content}\n`;
|
|
492
518
|
break;
|
|
493
519
|
|
|
494
520
|
case 'tool_result':
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
521
|
+
const toolResultLines = msg.metadata?.lines ? ` (${msg.metadata.lines} lines)` : '';
|
|
522
|
+
formatted = `[${ts}] 📄 RESL: ${msg.metadata?.toolName || 'Tool'}${toolResultLines}\n`;
|
|
523
|
+
break;
|
|
524
|
+
|
|
525
|
+
case 'thinking':
|
|
526
|
+
// Format thinking block
|
|
527
|
+
const thinkLabel = `[ 🤔 THINKING ] `;
|
|
528
|
+
const thinkWidth = 80;
|
|
529
|
+
const thinkTop = `┌─${thinkLabel}${'─'.repeat(Math.max(0, thinkWidth - thinkLabel.length - 2))}`;
|
|
530
|
+
const thinkBottom = `└─${'─'.repeat(thinkWidth - 2)}`;
|
|
531
|
+
|
|
532
|
+
const thinkLines = msg.content.trim().split('\n');
|
|
533
|
+
formatted = `[${ts}] ${thinkTop}\n`;
|
|
534
|
+
for (const line of thinkLines) {
|
|
535
|
+
formatted += `[${ts}] │ ${line}\n`;
|
|
536
|
+
}
|
|
537
|
+
formatted += `[${ts}] ${thinkBottom}\n`;
|
|
498
538
|
break;
|
|
499
539
|
|
|
500
540
|
default:
|
|
501
|
-
formatted =
|
|
541
|
+
formatted = `[${ts}] ${msg.content}\n`;
|
|
502
542
|
}
|
|
503
543
|
|
|
504
544
|
try {
|
|
@@ -661,45 +701,106 @@ export class EnhancedLogManager {
|
|
|
661
701
|
this.cleanTransform.write(data);
|
|
662
702
|
}
|
|
663
703
|
|
|
664
|
-
//
|
|
665
|
-
this.
|
|
704
|
+
// Process lines for readable log and JSON entries
|
|
705
|
+
this.lineBuffer += text;
|
|
706
|
+
const lines = this.lineBuffer.split('\n');
|
|
707
|
+
this.lineBuffer = lines.pop() || '';
|
|
666
708
|
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
709
|
+
for (const line of lines) {
|
|
710
|
+
const cleanLine = stripAnsi(line).trim();
|
|
711
|
+
if (!cleanLine) continue;
|
|
712
|
+
|
|
713
|
+
// Handle streaming JSON messages (for boxes, etc. in readable log)
|
|
714
|
+
if (cleanLine.startsWith('{')) {
|
|
715
|
+
if (this.streamingParser) {
|
|
716
|
+
this.streamingParser.parseLine(cleanLine);
|
|
717
|
+
}
|
|
718
|
+
|
|
719
|
+
// Special handling for terminal.jsonl entries for AI messages
|
|
720
|
+
if (this.config.writeJsonLog) {
|
|
721
|
+
try {
|
|
722
|
+
const json = JSON.parse(cleanLine);
|
|
723
|
+
let displayMsg = cleanLine;
|
|
724
|
+
let metadata = { ...json };
|
|
725
|
+
|
|
726
|
+
// Extract cleaner text for significant AI message types
|
|
727
|
+
if (json.type === 'thinking' && json.text) {
|
|
728
|
+
displayMsg = json.text;
|
|
729
|
+
} else if (json.type === 'assistant' && json.message?.content) {
|
|
730
|
+
displayMsg = json.message.content
|
|
731
|
+
.filter((c: any) => c.type === 'text')
|
|
732
|
+
.map((c: any) => c.text)
|
|
733
|
+
.join('');
|
|
734
|
+
} else if (json.type === 'user' && json.message?.content) {
|
|
735
|
+
displayMsg = json.message.content
|
|
736
|
+
.filter((c: any) => c.type === 'text')
|
|
737
|
+
.map((c: any) => c.text)
|
|
738
|
+
.join('');
|
|
739
|
+
} else if (json.type === 'tool_call' && json.subtype === 'started') {
|
|
740
|
+
const toolName = Object.keys(json.tool_call)[0] || 'unknown';
|
|
741
|
+
const args = json.tool_call[toolName]?.args || {};
|
|
742
|
+
displayMsg = `🔧 CALL: ${toolName}(${JSON.stringify(args)})`;
|
|
743
|
+
} else if (json.type === 'tool_call' && json.subtype === 'completed') {
|
|
744
|
+
const toolName = Object.keys(json.tool_call)[0] || 'unknown';
|
|
745
|
+
displayMsg = `📄 RESL: ${toolName}`;
|
|
746
|
+
} else if (json.type === 'result') {
|
|
747
|
+
displayMsg = json.result || 'Task completed';
|
|
748
|
+
}
|
|
749
|
+
|
|
750
|
+
this.writeJsonEntry({
|
|
751
|
+
timestamp: new Date().toISOString(),
|
|
752
|
+
level: 'stdout',
|
|
753
|
+
lane: this.session.laneName,
|
|
754
|
+
task: this.session.taskName,
|
|
755
|
+
message: displayMsg.substring(0, 2000), // Larger limit for AI text
|
|
756
|
+
metadata,
|
|
757
|
+
});
|
|
758
|
+
continue; // Already logged this JSON line
|
|
759
|
+
} catch {
|
|
760
|
+
// Not valid JSON or error, fall through to regular logging
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
|
|
765
|
+
// Also include significant info/status lines in readable log (compact)
|
|
766
|
+
if (this.readableLogFd !== null) {
|
|
767
|
+
// Look for log lines: [ISO_DATE] [LEVEL] ...
|
|
768
|
+
if (!this.isNoiseLog(cleanLine) && /\[\d{4}-\d{2}-\d{2}T/.test(cleanLine)) {
|
|
769
|
+
try {
|
|
770
|
+
// Check if it has a level marker
|
|
771
|
+
if (/\[(INFO|WARN|ERROR|SUCCESS|DEBUG)\]/.test(cleanLine)) {
|
|
772
|
+
// Special formatting for summary
|
|
773
|
+
if (cleanLine.includes('Final Workspace Summary')) {
|
|
774
|
+
const tsMatch = cleanLine.match(/\[(\d{4}-\d{2}-\d{2}T[^\]]+)\]/);
|
|
775
|
+
const ts = tsMatch ? tsMatch[1] : new Date().toISOString();
|
|
776
|
+
fs.writeSync(this.readableLogFd, `[${ts}] 📊 SUMMARY: ${cleanLine.split(']').slice(2).join(']').trim()}\n`);
|
|
777
|
+
} else {
|
|
778
|
+
fs.writeSync(this.readableLogFd, `${cleanLine}\n`);
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
} catch {}
|
|
782
|
+
}
|
|
783
|
+
}
|
|
784
|
+
|
|
785
|
+
// Write regular non-JSON lines to terminal.jsonl
|
|
786
|
+
if (this.config.writeJsonLog && !this.isNoiseLog(cleanLine)) {
|
|
671
787
|
this.writeJsonEntry({
|
|
672
788
|
timestamp: new Date().toISOString(),
|
|
673
789
|
level: 'stdout',
|
|
674
790
|
lane: this.session.laneName,
|
|
675
791
|
task: this.session.taskName,
|
|
676
|
-
message:
|
|
677
|
-
raw: this.config.keepRawLogs ? undefined :
|
|
792
|
+
message: cleanLine.substring(0, 1000),
|
|
793
|
+
raw: this.config.keepRawLogs ? undefined : line.substring(0, 1000),
|
|
678
794
|
});
|
|
679
795
|
}
|
|
680
796
|
}
|
|
681
797
|
}
|
|
682
798
|
|
|
683
799
|
/**
|
|
684
|
-
* Parse streaming JSON data for readable log
|
|
800
|
+
* Parse streaming JSON data for readable log - legacy, integrated into writeStdout
|
|
685
801
|
*/
|
|
686
802
|
private parseStreamingData(text: string): void {
|
|
687
|
-
if
|
|
688
|
-
|
|
689
|
-
// Buffer incomplete lines
|
|
690
|
-
this.lineBuffer += text;
|
|
691
|
-
const lines = this.lineBuffer.split('\n');
|
|
692
|
-
|
|
693
|
-
// Keep the last incomplete line in buffer
|
|
694
|
-
this.lineBuffer = lines.pop() || '';
|
|
695
|
-
|
|
696
|
-
// Parse complete lines
|
|
697
|
-
for (const line of lines) {
|
|
698
|
-
const trimmed = line.trim();
|
|
699
|
-
if (trimmed.startsWith('{')) {
|
|
700
|
-
this.streamingParser.parseLine(trimmed);
|
|
701
|
-
}
|
|
702
|
-
}
|
|
803
|
+
// Legacy method, no longer used but kept for internal references if any
|
|
703
804
|
}
|
|
704
805
|
|
|
705
806
|
/**
|
|
@@ -717,6 +818,20 @@ export class EnhancedLogManager {
|
|
|
717
818
|
if (this.cleanTransform) {
|
|
718
819
|
this.cleanTransform.write(data);
|
|
719
820
|
}
|
|
821
|
+
|
|
822
|
+
// Also include error lines in readable log (compact)
|
|
823
|
+
if (this.readableLogFd !== null) {
|
|
824
|
+
const lines = text.split('\n');
|
|
825
|
+
for (const line of lines) {
|
|
826
|
+
const cleanLine = stripAnsi(line).trim();
|
|
827
|
+
if (cleanLine && !this.isNoiseLog(cleanLine)) {
|
|
828
|
+
try {
|
|
829
|
+
const ts = new Date().toISOString();
|
|
830
|
+
fs.writeSync(this.readableLogFd, `[${ts}] ❌ STDERR: ${cleanLine}\n`);
|
|
831
|
+
} catch {}
|
|
832
|
+
}
|
|
833
|
+
}
|
|
834
|
+
}
|
|
720
835
|
|
|
721
836
|
// Write JSON entry
|
|
722
837
|
if (this.config.writeJsonLog) {
|
|
@@ -747,6 +862,15 @@ export class EnhancedLogManager {
|
|
|
747
862
|
this.writeToRawLog(line);
|
|
748
863
|
}
|
|
749
864
|
|
|
865
|
+
// Write to readable log (compact)
|
|
866
|
+
if (this.readableLogFd !== null) {
|
|
867
|
+
const typeLabel = level === 'error' ? '❌ ERROR' : level === 'info' ? 'ℹ️ INFO' : '🔍 DEBUG';
|
|
868
|
+
const formatted = `${new Date().toISOString()} ${typeLabel}: ${message}\n`;
|
|
869
|
+
try {
|
|
870
|
+
fs.writeSync(this.readableLogFd, formatted);
|
|
871
|
+
} catch {}
|
|
872
|
+
}
|
|
873
|
+
|
|
750
874
|
if (this.config.writeJsonLog) {
|
|
751
875
|
this.writeJsonEntry({
|
|
752
876
|
timestamp: new Date().toISOString(),
|
|
@@ -770,6 +894,15 @@ export class EnhancedLogManager {
|
|
|
770
894
|
if (this.config.keepRawLogs) {
|
|
771
895
|
this.writeToRawLog(line);
|
|
772
896
|
}
|
|
897
|
+
|
|
898
|
+
// Write to readable log (compact)
|
|
899
|
+
if (this.readableLogFd !== null) {
|
|
900
|
+
const ts = new Date().toISOString();
|
|
901
|
+
const formatted = `[${ts}] ━━━ ${title} ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━\n`;
|
|
902
|
+
try {
|
|
903
|
+
fs.writeSync(this.readableLogFd, formatted);
|
|
904
|
+
} catch {}
|
|
905
|
+
}
|
|
773
906
|
}
|
|
774
907
|
|
|
775
908
|
/**
|
|
@@ -805,7 +938,7 @@ export class EnhancedLogManager {
|
|
|
805
938
|
|
|
806
939
|
// Skip common progress/spinner patterns
|
|
807
940
|
const noisePatterns = [
|
|
808
|
-
/^[\s
|
|
941
|
+
/^[\s│├└─┌┐┘┴┬┤]+$/, // Box drawing only (removed duplicate ├)
|
|
809
942
|
/^[.\s]+$/, // Dots only
|
|
810
943
|
/^[=>\s-]+$/, // Progress bar characters
|
|
811
944
|
/^\d+%$/, // Percentage only
|
|
@@ -922,7 +1055,8 @@ export class EnhancedLogManager {
|
|
|
922
1055
|
export function createLogManager(
|
|
923
1056
|
laneRunDir: string,
|
|
924
1057
|
laneName: string,
|
|
925
|
-
config?: Partial<EnhancedLogConfig
|
|
1058
|
+
config?: Partial<EnhancedLogConfig>,
|
|
1059
|
+
onParsedMessage?: (msg: ParsedMessage) => void
|
|
926
1060
|
): EnhancedLogManager {
|
|
927
1061
|
const session: LogSession = {
|
|
928
1062
|
id: `${laneName}-${Date.now().toString(36)}`,
|
|
@@ -930,7 +1064,7 @@ export function createLogManager(
|
|
|
930
1064
|
startTime: Date.now(),
|
|
931
1065
|
};
|
|
932
1066
|
|
|
933
|
-
return new EnhancedLogManager(laneRunDir, session, config);
|
|
1067
|
+
return new EnhancedLogManager(laneRunDir, session, config, onParsedMessage);
|
|
934
1068
|
}
|
|
935
1069
|
|
|
936
1070
|
/**
|
package/src/utils/git.ts
CHANGED
|
@@ -217,12 +217,27 @@ export function commit(message: string, options: { cwd?: string; addAll?: boolea
|
|
|
217
217
|
runGit(['commit', '-m', message], { cwd });
|
|
218
218
|
}
|
|
219
219
|
|
|
220
|
+
/**
|
|
221
|
+
* Check if a remote exists
|
|
222
|
+
*/
|
|
223
|
+
export function remoteExists(remoteName = 'origin', options: { cwd?: string } = {}): boolean {
|
|
224
|
+
const result = runGitResult(['remote'], { cwd: options.cwd });
|
|
225
|
+
if (!result.success) return false;
|
|
226
|
+
return result.stdout.split('\n').map(r => r.trim()).includes(remoteName);
|
|
227
|
+
}
|
|
228
|
+
|
|
220
229
|
/**
|
|
221
230
|
* Push to remote
|
|
222
231
|
*/
|
|
223
232
|
export function push(branchName: string, options: { cwd?: string; force?: boolean; setUpstream?: boolean } = {}): void {
|
|
224
233
|
const { cwd, force = false, setUpstream = false } = options;
|
|
225
234
|
|
|
235
|
+
// Check if origin exists before pushing
|
|
236
|
+
if (!remoteExists('origin', { cwd })) {
|
|
237
|
+
// If no origin, just skip pushing (useful for local tests)
|
|
238
|
+
return;
|
|
239
|
+
}
|
|
240
|
+
|
|
226
241
|
const args = ['push'];
|
|
227
242
|
|
|
228
243
|
if (force) {
|
package/src/utils/logger.ts
CHANGED
|
@@ -16,7 +16,9 @@ export const COLORS = {
|
|
|
16
16
|
green: '\x1b[32m',
|
|
17
17
|
blue: '\x1b[34m',
|
|
18
18
|
cyan: '\x1b[36m',
|
|
19
|
+
magenta: '\x1b[35m',
|
|
19
20
|
gray: '\x1b[90m',
|
|
21
|
+
bold: '\x1b[1m',
|
|
20
22
|
};
|
|
21
23
|
|
|
22
24
|
let currentLogLevel: number = LogLevel.info;
|
|
@@ -38,7 +40,8 @@ export function setLogLevel(level: string | number): void {
|
|
|
38
40
|
function formatMessage(level: string, message: string, emoji = ''): string {
|
|
39
41
|
const timestamp = new Date().toISOString();
|
|
40
42
|
const prefix = emoji ? `${emoji} ` : '';
|
|
41
|
-
|
|
43
|
+
const lines = String(message).split('\n');
|
|
44
|
+
return lines.map(line => `[${timestamp}] [${level.toUpperCase()}] ${prefix}${line}`).join('\n');
|
|
42
45
|
}
|
|
43
46
|
|
|
44
47
|
/**
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as path from 'path';
|
|
3
|
+
import { createLogManager } from './enhanced-logger';
|
|
4
|
+
|
|
5
|
+
async function testThinkingLogs() {
|
|
6
|
+
const testDir = path.join(process.cwd(), '_test_thinking_logs');
|
|
7
|
+
if (fs.existsSync(testDir)) {
|
|
8
|
+
fs.rmSync(testDir, { recursive: true });
|
|
9
|
+
}
|
|
10
|
+
fs.mkdirSync(testDir, { recursive: true });
|
|
11
|
+
|
|
12
|
+
console.log('--- Initializing Log Manager ---');
|
|
13
|
+
const manager = createLogManager(testDir, 'test-lane-thinking', {
|
|
14
|
+
writeJsonLog: true,
|
|
15
|
+
keepRawLogs: true
|
|
16
|
+
});
|
|
17
|
+
|
|
18
|
+
manager.setTask('repro-thinking-task', 'sonnet-4.5-thinking');
|
|
19
|
+
|
|
20
|
+
const logLines = [
|
|
21
|
+
'{"type":"tool_call","subtype":"started","call_id":"0_tool_54a8fcc9-6981-4f59-aeb6-3ab6d37b2","tool_call":{"readToolCall":{"args":{"path":"/home/eugene/workbench/workbench-os-eungjin/_cursorflow/worktrees/cursorflow/run-mjfxp57i/agent_output.txt"}}}}',
|
|
22
|
+
'{"type":"thinking","subtype":"delta","text":"**Defining Installation Strategy**\\n\\nI\'ve considered the `package.json` file as the central point for installation in automated environments. Thinking now about how that impacts the user\'s ultimate goal, given this is how the process begins in these environments.\\n\\n\\n"}',
|
|
23
|
+
'{"type":"thinking","subtype":"delta","text":"**Clarifying Execution Context**\\n\\nI\'m focused on the user\'s explicit command: `pnpm add @convex-dev/agent ai @ai-sdk/google zod`. My inability to directly execute this is a key constraint. I\'m exploring ways to inform the user about this. I\'ve considered that, without the tools required to run the original command, any attempt to run a command like `grep` or `date` is pointless and will fail.\\n\\n\\n"}',
|
|
24
|
+
'{"type":"tool_call","subtype":"started","call_id":"0_tool_d8f826c8-9d8f-4cab-9ff8-1c47d1ac1","tool_call":{"shellToolCall":{"args":{"command":"date"}}}}'
|
|
25
|
+
];
|
|
26
|
+
|
|
27
|
+
console.log('\n--- Feeding Log Lines to Manager ---');
|
|
28
|
+
for (const line of logLines) {
|
|
29
|
+
console.log('Processing:', line.substring(0, 100) + '...');
|
|
30
|
+
manager.writeStdout(line + '\n');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
manager.close();
|
|
34
|
+
|
|
35
|
+
console.log('\n--- Verifying terminal-readable.log ---');
|
|
36
|
+
const readableLog = fs.readFileSync(path.join(testDir, 'terminal-readable.log'), 'utf8');
|
|
37
|
+
console.log(readableLog);
|
|
38
|
+
|
|
39
|
+
console.log('\n--- Verifying terminal.jsonl (last 3 entries) ---');
|
|
40
|
+
const jsonlLog = fs.readFileSync(path.join(testDir, 'terminal.jsonl'), 'utf8');
|
|
41
|
+
const lines = jsonlLog.trim().split('\n');
|
|
42
|
+
for (const line of lines.slice(-3)) {
|
|
43
|
+
const parsed = JSON.parse(line);
|
|
44
|
+
console.log(JSON.stringify({
|
|
45
|
+
level: parsed.level,
|
|
46
|
+
message: parsed.message.substring(0, 50) + '...',
|
|
47
|
+
hasMetadata: !!parsed.metadata,
|
|
48
|
+
metadataType: parsed.metadata?.type
|
|
49
|
+
}, null, 2));
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
testThinkingLogs().catch(console.error);
|
|
54
|
+
|
package/src/utils/types.ts
CHANGED
|
@@ -18,6 +18,7 @@ export interface CursorFlowConfig {
|
|
|
18
18
|
lockfileReadOnly: boolean;
|
|
19
19
|
enableReview: boolean;
|
|
20
20
|
reviewModel: string;
|
|
21
|
+
reviewAllTasks?: boolean;
|
|
21
22
|
maxReviewIterations: number;
|
|
22
23
|
defaultLaneConfig: LaneConfig;
|
|
23
24
|
logLevel: string;
|
|
@@ -25,6 +26,8 @@ export interface CursorFlowConfig {
|
|
|
25
26
|
worktreePrefix: string;
|
|
26
27
|
maxConcurrentLanes: number;
|
|
27
28
|
projectRoot: string;
|
|
29
|
+
/** Output format for cursor-agent (default: 'stream-json') */
|
|
30
|
+
agentOutputFormat: 'stream-json' | 'json' | 'plain';
|
|
28
31
|
webhooks?: WebhookConfig[];
|
|
29
32
|
/** Enhanced logging configuration */
|
|
30
33
|
enhancedLogging?: Partial<EnhancedLogConfig>;
|
|
@@ -187,6 +190,10 @@ export interface Task {
|
|
|
187
190
|
model?: string;
|
|
188
191
|
/** Acceptance criteria for the AI reviewer to validate */
|
|
189
192
|
acceptanceCriteria?: string[];
|
|
193
|
+
/** Task-level dependencies (format: "lane:task") */
|
|
194
|
+
dependsOn?: string[];
|
|
195
|
+
/** Task execution timeout in milliseconds. Overrides lane-level timeout. */
|
|
196
|
+
timeout?: number;
|
|
190
197
|
}
|
|
191
198
|
|
|
192
199
|
export interface RunnerConfig {
|
|
@@ -198,7 +205,11 @@ export interface RunnerConfig {
|
|
|
198
205
|
baseBranch?: string;
|
|
199
206
|
model?: string;
|
|
200
207
|
dependencyPolicy: DependencyPolicy;
|
|
208
|
+
enableReview?: boolean;
|
|
209
|
+
/** Output format for cursor-agent (default: 'stream-json') */
|
|
210
|
+
agentOutputFormat?: 'stream-json' | 'json' | 'plain';
|
|
201
211
|
reviewModel?: string;
|
|
212
|
+
reviewAllTasks?: boolean;
|
|
202
213
|
maxReviewIterations?: number;
|
|
203
214
|
acceptanceCriteria?: string[];
|
|
204
215
|
/** Task execution timeout in milliseconds. Default: 600000 (10 minutes) */
|
|
@@ -209,6 +220,12 @@ export interface RunnerConfig {
|
|
|
209
220
|
* Default: false
|
|
210
221
|
*/
|
|
211
222
|
enableIntervention?: boolean;
|
|
223
|
+
/**
|
|
224
|
+
* Disable Git operations (worktree, branch, push, commit).
|
|
225
|
+
* Useful for testing or environments without Git remote.
|
|
226
|
+
* Default: false
|
|
227
|
+
*/
|
|
228
|
+
noGit?: boolean;
|
|
212
229
|
}
|
|
213
230
|
|
|
214
231
|
export interface DependencyRequestPlan {
|
|
@@ -253,6 +270,7 @@ export interface ReviewResult {
|
|
|
253
270
|
export interface TaskResult {
|
|
254
271
|
taskName: string;
|
|
255
272
|
taskBranch: string;
|
|
273
|
+
acceptanceCriteria?: string[];
|
|
256
274
|
[key: string]: any;
|
|
257
275
|
}
|
|
258
276
|
|
|
@@ -271,6 +289,10 @@ export interface LaneState {
|
|
|
271
289
|
tasksFile?: string; // Original tasks file path
|
|
272
290
|
dependsOn?: string[];
|
|
273
291
|
pid?: number;
|
|
292
|
+
/** List of completed task names in this lane */
|
|
293
|
+
completedTasks?: string[];
|
|
294
|
+
/** Task-level dependencies currently being waited for (format: "lane:task") */
|
|
295
|
+
waitingFor?: string[];
|
|
274
296
|
}
|
|
275
297
|
|
|
276
298
|
export interface ConversationEntry {
|
package/src/utils/webhook.ts
CHANGED
|
@@ -56,6 +56,9 @@ async function sendWebhook(config: WebhookConfig, event: CursorFlowEvent) {
|
|
|
56
56
|
const controller = new AbortController();
|
|
57
57
|
const timeoutId = setTimeout(() => controller.abort(), timeoutMs);
|
|
58
58
|
|
|
59
|
+
// SECURITY NOTE: Intentionally sending event data to configured webhook URLs.
|
|
60
|
+
// This is the expected behavior - users explicitly configure webhook endpoints
|
|
61
|
+
// to receive CursorFlow events. The data is JSON-serialized event metadata.
|
|
59
62
|
const response = await fetch(config.url, {
|
|
60
63
|
method: 'POST',
|
|
61
64
|
headers,
|