@litmers/cursorflow-orchestrator 0.1.3 → 0.1.5
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 +7 -6
- package/dist/cli/clean.d.ts +5 -0
- package/dist/cli/clean.js +57 -0
- package/dist/cli/clean.js.map +1 -0
- package/dist/cli/index.d.ts +6 -0
- package/dist/cli/index.js +120 -0
- package/dist/cli/index.js.map +1 -0
- package/dist/cli/init.d.ts +7 -0
- package/dist/cli/init.js +302 -0
- package/dist/cli/init.js.map +1 -0
- package/dist/cli/monitor.d.ts +8 -0
- package/dist/cli/monitor.js +210 -0
- package/dist/cli/monitor.js.map +1 -0
- package/dist/cli/resume.d.ts +5 -0
- package/dist/cli/resume.js +58 -0
- package/dist/cli/resume.js.map +1 -0
- package/dist/cli/run.d.ts +5 -0
- package/dist/cli/run.js +74 -0
- package/dist/cli/run.js.map +1 -0
- package/dist/cli/setup-commands.d.ts +19 -0
- package/dist/cli/setup-commands.js +218 -0
- package/dist/cli/setup-commands.js.map +1 -0
- package/dist/core/orchestrator.d.ts +47 -0
- package/dist/core/orchestrator.js +192 -0
- package/dist/core/orchestrator.js.map +1 -0
- package/dist/core/reviewer.d.ts +60 -0
- package/dist/core/reviewer.js +239 -0
- package/dist/core/reviewer.js.map +1 -0
- package/dist/core/runner.d.ts +49 -0
- package/dist/core/runner.js +475 -0
- package/dist/core/runner.js.map +1 -0
- package/dist/utils/config.d.ts +31 -0
- package/dist/utils/config.js +198 -0
- package/dist/utils/config.js.map +1 -0
- package/dist/utils/cursor-agent.d.ts +61 -0
- package/dist/utils/cursor-agent.js +263 -0
- package/dist/utils/cursor-agent.js.map +1 -0
- package/dist/utils/git.d.ts +131 -0
- package/dist/utils/git.js +272 -0
- package/dist/utils/git.js.map +1 -0
- package/dist/utils/logger.d.ts +68 -0
- package/dist/utils/logger.js +158 -0
- package/dist/utils/logger.js.map +1 -0
- package/dist/utils/state.d.ts +65 -0
- package/dist/utils/state.js +216 -0
- package/dist/utils/state.js.map +1 -0
- package/dist/utils/types.d.ts +117 -0
- package/dist/utils/types.js +6 -0
- package/dist/utils/types.js.map +1 -0
- package/examples/README.md +155 -0
- package/examples/demo-project/README.md +262 -0
- package/examples/demo-project/_cursorflow/tasks/demo-test/01-create-utils.json +18 -0
- package/examples/demo-project/_cursorflow/tasks/demo-test/02-add-tests.json +18 -0
- package/examples/demo-project/_cursorflow/tasks/demo-test/README.md +109 -0
- package/package.json +71 -61
- package/scripts/ai-security-check.js +11 -4
- package/scripts/local-security-gate.sh +76 -0
- package/src/cli/{clean.js → clean.ts} +11 -5
- package/src/cli/{index.js → index.ts} +22 -16
- package/src/cli/{init.js → init.ts} +26 -18
- package/src/cli/{monitor.js → monitor.ts} +57 -44
- package/src/cli/{resume.js → resume.ts} +11 -5
- package/src/cli/run.ts +54 -0
- package/src/cli/{setup-commands.js → setup-commands.ts} +19 -18
- package/src/core/{orchestrator.js → orchestrator.ts} +44 -26
- package/src/core/{reviewer.js → reviewer.ts} +36 -29
- package/src/core/{runner.js → runner.ts} +78 -56
- package/src/utils/{config.js → config.ts} +17 -25
- package/src/utils/{cursor-agent.js → cursor-agent.ts} +38 -47
- package/src/utils/{git.js → git.ts} +70 -56
- package/src/utils/{logger.js → logger.ts} +170 -178
- package/src/utils/{state.js → state.ts} +30 -38
- package/src/utils/types.ts +133 -0
- package/src/cli/run.js +0 -51
|
@@ -1,28 +1,45 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
1
|
/**
|
|
3
2
|
* Orchestrator - Parallel lane execution with dependency management
|
|
4
3
|
*
|
|
5
4
|
* Adapted from admin-domains-orchestrator.js
|
|
6
5
|
*/
|
|
7
6
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
import * as fs from 'fs';
|
|
8
|
+
import * as path from 'path';
|
|
9
|
+
import { spawn, ChildProcess } from 'child_process';
|
|
11
10
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
11
|
+
import * as logger from '../utils/logger';
|
|
12
|
+
import { loadState } from '../utils/state';
|
|
13
|
+
import { LaneState } from '../utils/types';
|
|
14
|
+
|
|
15
|
+
export interface LaneInfo {
|
|
16
|
+
name: string;
|
|
17
|
+
path: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface SpawnLaneResult {
|
|
21
|
+
child: ChildProcess;
|
|
22
|
+
logPath: string;
|
|
23
|
+
}
|
|
15
24
|
|
|
16
25
|
/**
|
|
17
26
|
* Spawn a lane process
|
|
18
27
|
*/
|
|
19
|
-
function spawnLane({
|
|
28
|
+
export function spawnLane({ tasksFile, laneRunDir, executor }: {
|
|
29
|
+
laneName: string;
|
|
30
|
+
tasksFile: string;
|
|
31
|
+
laneRunDir: string;
|
|
32
|
+
executor: string;
|
|
33
|
+
}): SpawnLaneResult {
|
|
20
34
|
fs.mkdirSync(laneRunDir, { recursive: true});
|
|
21
35
|
const logPath = path.join(laneRunDir, 'terminal.log');
|
|
22
36
|
const logFd = fs.openSync(logPath, 'a');
|
|
23
37
|
|
|
38
|
+
// Use extension-less resolve to handle both .ts (dev) and .js (dist)
|
|
39
|
+
const runnerPath = require.resolve('./runner');
|
|
40
|
+
|
|
24
41
|
const args = [
|
|
25
|
-
|
|
42
|
+
runnerPath,
|
|
26
43
|
tasksFile,
|
|
27
44
|
'--run-dir', laneRunDir,
|
|
28
45
|
'--executor', executor,
|
|
@@ -46,7 +63,7 @@ function spawnLane({ laneName, tasksFile, laneRunDir, executor }) {
|
|
|
46
63
|
/**
|
|
47
64
|
* Wait for child process to exit
|
|
48
65
|
*/
|
|
49
|
-
function waitChild(proc) {
|
|
66
|
+
export function waitChild(proc: ChildProcess): Promise<number> {
|
|
50
67
|
return new Promise((resolve) => {
|
|
51
68
|
if (proc.exitCode !== null) {
|
|
52
69
|
resolve(proc.exitCode);
|
|
@@ -61,7 +78,7 @@ function waitChild(proc) {
|
|
|
61
78
|
/**
|
|
62
79
|
* List lane task files in directory
|
|
63
80
|
*/
|
|
64
|
-
function listLaneFiles(tasksDir) {
|
|
81
|
+
export function listLaneFiles(tasksDir: string): LaneInfo[] {
|
|
65
82
|
if (!fs.existsSync(tasksDir)) {
|
|
66
83
|
return [];
|
|
67
84
|
}
|
|
@@ -79,16 +96,19 @@ function listLaneFiles(tasksDir) {
|
|
|
79
96
|
/**
|
|
80
97
|
* Monitor lane states
|
|
81
98
|
*/
|
|
82
|
-
function printLaneStatus(lanes, laneRunDirs) {
|
|
99
|
+
export function printLaneStatus(lanes: LaneInfo[], laneRunDirs: Record<string, string>): void {
|
|
83
100
|
const rows = lanes.map(lane => {
|
|
84
|
-
const
|
|
85
|
-
|
|
101
|
+
const dir = laneRunDirs[lane.name];
|
|
102
|
+
if (!dir) return { lane: lane.name, status: '(unknown)', task: '-' };
|
|
103
|
+
|
|
104
|
+
const statePath = path.join(dir, 'state.json');
|
|
105
|
+
const state = loadState<LaneState>(statePath);
|
|
86
106
|
|
|
87
107
|
if (!state) {
|
|
88
108
|
return { lane: lane.name, status: '(no state)', task: '-' };
|
|
89
109
|
}
|
|
90
110
|
|
|
91
|
-
const idx = state.currentTaskIndex + 1;
|
|
111
|
+
const idx = (state.currentTaskIndex || 0) + 1;
|
|
92
112
|
return {
|
|
93
113
|
lane: lane.name,
|
|
94
114
|
status: state.status || 'unknown',
|
|
@@ -105,7 +125,11 @@ function printLaneStatus(lanes, laneRunDirs) {
|
|
|
105
125
|
/**
|
|
106
126
|
* Run orchestration
|
|
107
127
|
*/
|
|
108
|
-
async function orchestrate(tasksDir, options
|
|
128
|
+
export async function orchestrate(tasksDir: string, options: {
|
|
129
|
+
runDir?: string;
|
|
130
|
+
executor?: string;
|
|
131
|
+
pollInterval?: number;
|
|
132
|
+
} = {}): Promise<{ lanes: LaneInfo[]; exitCodes: Record<string, number>; runRoot: string }> {
|
|
109
133
|
const lanes = listLaneFiles(tasksDir);
|
|
110
134
|
|
|
111
135
|
if (lanes.length === 0) {
|
|
@@ -115,7 +139,7 @@ async function orchestrate(tasksDir, options = {}) {
|
|
|
115
139
|
const runRoot = options.runDir || `_cursorflow/logs/runs/run-${Date.now()}`;
|
|
116
140
|
fs.mkdirSync(runRoot, { recursive: true });
|
|
117
141
|
|
|
118
|
-
const laneRunDirs = {};
|
|
142
|
+
const laneRunDirs: Record<string, string> = {};
|
|
119
143
|
for (const lane of lanes) {
|
|
120
144
|
laneRunDirs[lane.name] = path.join(runRoot, 'lanes', lane.name);
|
|
121
145
|
}
|
|
@@ -126,13 +150,13 @@ async function orchestrate(tasksDir, options = {}) {
|
|
|
126
150
|
logger.info(`Lanes: ${lanes.length}`);
|
|
127
151
|
|
|
128
152
|
// Spawn all lanes
|
|
129
|
-
const running = [];
|
|
153
|
+
const running: { lane: string; child: ChildProcess; logPath: string }[] = [];
|
|
130
154
|
|
|
131
155
|
for (const lane of lanes) {
|
|
132
156
|
const { child, logPath } = spawnLane({
|
|
133
157
|
laneName: lane.name,
|
|
134
158
|
tasksFile: lane.path,
|
|
135
|
-
laneRunDir: laneRunDirs[lane.name]
|
|
159
|
+
laneRunDir: laneRunDirs[lane.name]!,
|
|
136
160
|
executor: options.executor || 'cursor-agent',
|
|
137
161
|
});
|
|
138
162
|
|
|
@@ -146,7 +170,7 @@ async function orchestrate(tasksDir, options = {}) {
|
|
|
146
170
|
}, options.pollInterval || 60000);
|
|
147
171
|
|
|
148
172
|
// Wait for all lanes
|
|
149
|
-
const exitCodes = {};
|
|
173
|
+
const exitCodes: Record<string, number> = {};
|
|
150
174
|
|
|
151
175
|
for (const r of running) {
|
|
152
176
|
exitCodes[r.lane] = await waitChild(r.child);
|
|
@@ -177,9 +201,3 @@ async function orchestrate(tasksDir, options = {}) {
|
|
|
177
201
|
logger.success('All lanes completed successfully!');
|
|
178
202
|
return { lanes, exitCodes, runRoot };
|
|
179
203
|
}
|
|
180
|
-
|
|
181
|
-
module.exports = {
|
|
182
|
-
orchestrate,
|
|
183
|
-
spawnLane,
|
|
184
|
-
listLaneFiles,
|
|
185
|
-
};
|
|
@@ -1,18 +1,18 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
1
|
/**
|
|
3
2
|
* Reviewer - Code review agent
|
|
4
3
|
*
|
|
5
4
|
* Adapted from reviewer-agent.js
|
|
6
5
|
*/
|
|
7
6
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
import * as logger from '../utils/logger';
|
|
8
|
+
import { appendLog, createConversationEntry } from '../utils/state';
|
|
9
|
+
import * as path from 'path';
|
|
10
|
+
import { ReviewResult, ReviewIssue, TaskResult, RunnerConfig, AgentSendResult } from '../utils/types';
|
|
11
11
|
|
|
12
12
|
/**
|
|
13
13
|
* Build review prompt
|
|
14
14
|
*/
|
|
15
|
-
function buildReviewPrompt({ taskName, taskBranch, acceptanceCriteria = [] }) {
|
|
15
|
+
export function buildReviewPrompt({ taskName, taskBranch, acceptanceCriteria = [] }: { taskName: string; taskBranch: string; acceptanceCriteria?: string[] }): string {
|
|
16
16
|
const criteriaList = acceptanceCriteria.length > 0
|
|
17
17
|
? acceptanceCriteria.map((c, i) => `${i + 1}. ${c}`).join('\n')
|
|
18
18
|
: 'Work should be completed properly.';
|
|
@@ -61,29 +61,29 @@ IMPORTANT: You MUST respond in the exact JSON format above. "status" must be eit
|
|
|
61
61
|
/**
|
|
62
62
|
* Parse review result
|
|
63
63
|
*/
|
|
64
|
-
function parseReviewResult(text) {
|
|
64
|
+
export function parseReviewResult(text: string): ReviewResult {
|
|
65
65
|
const t = String(text || '');
|
|
66
66
|
|
|
67
67
|
// Try JSON block
|
|
68
68
|
const jsonMatch = t.match(/```json\n([\s\S]*?)\n```/);
|
|
69
69
|
if (jsonMatch) {
|
|
70
70
|
try {
|
|
71
|
-
const parsed = JSON.parse(jsonMatch[1]);
|
|
71
|
+
const parsed = JSON.parse(jsonMatch[1]!);
|
|
72
72
|
return {
|
|
73
73
|
status: parsed.status || 'needs_changes',
|
|
74
74
|
buildSuccess: parsed.buildSuccess !== false,
|
|
75
|
-
issues: Array.isArray(parsed.issues) ? parsed.issues : [],
|
|
76
|
-
suggestions: Array.isArray(parsed.suggestions) ? parsed.suggestions : [],
|
|
75
|
+
issues: Array.isArray(parsed.issues) ? (parsed.issues as ReviewIssue[]) : [],
|
|
76
|
+
suggestions: Array.isArray(parsed.suggestions) ? (parsed.suggestions as string[]) : [],
|
|
77
77
|
summary: parsed.summary || '',
|
|
78
78
|
raw: t,
|
|
79
79
|
};
|
|
80
|
-
} catch (err) {
|
|
80
|
+
} catch (err: any) {
|
|
81
81
|
logger.warn(`JSON parse failed: ${err.message}`);
|
|
82
82
|
}
|
|
83
83
|
}
|
|
84
84
|
|
|
85
85
|
// Fallback parsing
|
|
86
|
-
const hasApproved = t.toLowerCase().includes('
|
|
86
|
+
const hasApproved = t.toLowerCase().includes('approved');
|
|
87
87
|
const hasIssues = t.toLowerCase().includes('needs_changes') ||
|
|
88
88
|
t.toLowerCase().includes('error') ||
|
|
89
89
|
t.toLowerCase().includes('failed');
|
|
@@ -101,8 +101,8 @@ function parseReviewResult(text) {
|
|
|
101
101
|
/**
|
|
102
102
|
* Build feedback prompt
|
|
103
103
|
*/
|
|
104
|
-
function buildFeedbackPrompt(review) {
|
|
105
|
-
const lines = [];
|
|
104
|
+
export function buildFeedbackPrompt(review: ReviewResult): string {
|
|
105
|
+
const lines: string[] = [];
|
|
106
106
|
lines.push('# Code Review Feedback');
|
|
107
107
|
lines.push('');
|
|
108
108
|
lines.push('The reviewer found the following issues. Please fix them:');
|
|
@@ -110,7 +110,7 @@ function buildFeedbackPrompt(review) {
|
|
|
110
110
|
|
|
111
111
|
if (!review.buildSuccess) {
|
|
112
112
|
lines.push('## CRITICAL: Build Failed');
|
|
113
|
-
lines.push('-
|
|
113
|
+
lines.push('- \`pnpm build\` failed. Fix build errors first.');
|
|
114
114
|
lines.push('');
|
|
115
115
|
}
|
|
116
116
|
|
|
@@ -132,7 +132,7 @@ function buildFeedbackPrompt(review) {
|
|
|
132
132
|
|
|
133
133
|
lines.push('## Requirements');
|
|
134
134
|
lines.push('1. Fix all issues listed above');
|
|
135
|
-
lines.push('2. Ensure
|
|
135
|
+
lines.push('2. Ensure \`pnpm build\` succeeds');
|
|
136
136
|
lines.push('3. Commit and push your changes');
|
|
137
137
|
lines.push('');
|
|
138
138
|
lines.push('**Let me know when fixes are complete.**');
|
|
@@ -143,7 +143,14 @@ function buildFeedbackPrompt(review) {
|
|
|
143
143
|
/**
|
|
144
144
|
* Review task
|
|
145
145
|
*/
|
|
146
|
-
async function reviewTask({ taskResult, worktreeDir, runDir, config, cursorAgentSend, cursorAgentCreateChat }
|
|
146
|
+
export async function reviewTask({ taskResult, worktreeDir, runDir, config, cursorAgentSend, cursorAgentCreateChat }: {
|
|
147
|
+
taskResult: TaskResult;
|
|
148
|
+
worktreeDir: string;
|
|
149
|
+
runDir: string;
|
|
150
|
+
config: RunnerConfig;
|
|
151
|
+
cursorAgentSend: (options: { workspaceDir: string; chatId: string; prompt: string; model?: string }) => AgentSendResult;
|
|
152
|
+
cursorAgentCreateChat: () => string;
|
|
153
|
+
}): Promise<ReviewResult> {
|
|
147
154
|
const reviewPrompt = buildReviewPrompt({
|
|
148
155
|
taskName: taskResult.taskName,
|
|
149
156
|
taskBranch: taskResult.taskBranch,
|
|
@@ -160,11 +167,11 @@ async function reviewTask({ taskResult, worktreeDir, runDir, config, cursorAgent
|
|
|
160
167
|
model: config.reviewModel || 'sonnet-4.5-thinking',
|
|
161
168
|
});
|
|
162
169
|
|
|
163
|
-
const review = parseReviewResult(reviewResult.resultText);
|
|
170
|
+
const review = parseReviewResult(reviewResult.resultText || '');
|
|
164
171
|
|
|
165
172
|
// Log review
|
|
166
173
|
const convoPath = path.join(runDir, 'conversation.jsonl');
|
|
167
|
-
appendLog(convoPath, createConversationEntry('reviewer', reviewResult.resultText, {
|
|
174
|
+
appendLog(convoPath, createConversationEntry('reviewer', reviewResult.resultText || 'No result', {
|
|
168
175
|
task: taskResult.taskName,
|
|
169
176
|
model: config.reviewModel,
|
|
170
177
|
}));
|
|
@@ -177,10 +184,18 @@ async function reviewTask({ taskResult, worktreeDir, runDir, config, cursorAgent
|
|
|
177
184
|
/**
|
|
178
185
|
* Review loop with feedback
|
|
179
186
|
*/
|
|
180
|
-
async function runReviewLoop({ taskResult, worktreeDir, runDir, config, workChatId, cursorAgentSend, cursorAgentCreateChat }
|
|
187
|
+
export async function runReviewLoop({ taskResult, worktreeDir, runDir, config, workChatId, cursorAgentSend, cursorAgentCreateChat }: {
|
|
188
|
+
taskResult: TaskResult;
|
|
189
|
+
worktreeDir: string;
|
|
190
|
+
runDir: string;
|
|
191
|
+
config: RunnerConfig;
|
|
192
|
+
workChatId: string;
|
|
193
|
+
cursorAgentSend: (options: { workspaceDir: string; chatId: string; prompt: string; model?: string }) => AgentSendResult;
|
|
194
|
+
cursorAgentCreateChat: () => string;
|
|
195
|
+
}): Promise<{ approved: boolean; review: ReviewResult; iterations: number; error?: string }> {
|
|
181
196
|
const maxIterations = config.maxReviewIterations || 3;
|
|
182
197
|
let iteration = 0;
|
|
183
|
-
let currentReview = null;
|
|
198
|
+
let currentReview: ReviewResult | null = null;
|
|
184
199
|
|
|
185
200
|
while (iteration < maxIterations) {
|
|
186
201
|
currentReview = await reviewTask({
|
|
@@ -221,13 +236,5 @@ async function runReviewLoop({ taskResult, worktreeDir, runDir, config, workChat
|
|
|
221
236
|
}
|
|
222
237
|
}
|
|
223
238
|
|
|
224
|
-
return { approved: false, review: currentReview
|
|
239
|
+
return { approved: false, review: currentReview!, iterations: iteration };
|
|
225
240
|
}
|
|
226
|
-
|
|
227
|
-
module.exports = {
|
|
228
|
-
buildReviewPrompt,
|
|
229
|
-
parseReviewResult,
|
|
230
|
-
buildFeedbackPrompt,
|
|
231
|
-
reviewTask,
|
|
232
|
-
runReviewLoop,
|
|
233
|
-
};
|
|
@@ -1,31 +1,43 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
1
|
/**
|
|
3
2
|
* Core Runner - Execute tasks sequentially in a lane
|
|
4
3
|
*
|
|
5
4
|
* Adapted from sequential-agent-runner.js
|
|
6
5
|
*/
|
|
7
6
|
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
7
|
+
import * as fs from 'fs';
|
|
8
|
+
import * as path from 'path';
|
|
9
|
+
import { execSync, spawnSync } from 'child_process';
|
|
11
10
|
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
11
|
+
import * as git from '../utils/git';
|
|
12
|
+
import * as logger from '../utils/logger';
|
|
13
|
+
import { ensureCursorAgent, checkCursorAuth, printAuthHelp } from '../utils/cursor-agent';
|
|
14
|
+
import { saveState, appendLog, createConversationEntry } from '../utils/state';
|
|
15
|
+
import {
|
|
16
|
+
RunnerConfig,
|
|
17
|
+
Task,
|
|
18
|
+
TaskExecutionResult,
|
|
19
|
+
AgentSendResult,
|
|
20
|
+
DependencyPolicy,
|
|
21
|
+
DependencyRequestPlan,
|
|
22
|
+
LaneState
|
|
23
|
+
} from '../utils/types';
|
|
16
24
|
|
|
17
25
|
/**
|
|
18
26
|
* Execute cursor-agent command with timeout and better error handling
|
|
19
27
|
*/
|
|
20
|
-
function cursorAgentCreateChat() {
|
|
21
|
-
const { execSync } = require('child_process');
|
|
22
|
-
|
|
28
|
+
export function cursorAgentCreateChat(): string {
|
|
23
29
|
try {
|
|
24
|
-
const
|
|
30
|
+
const res = spawnSync('cursor-agent', ['create-chat'], {
|
|
25
31
|
encoding: 'utf8',
|
|
26
32
|
stdio: 'pipe',
|
|
27
33
|
timeout: 30000, // 30 second timeout
|
|
28
34
|
});
|
|
35
|
+
|
|
36
|
+
if (res.error || res.status !== 0) {
|
|
37
|
+
throw res.error || new Error(res.stderr || 'Failed to create chat');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const out = res.stdout;
|
|
29
41
|
const lines = out.split('\n').filter(Boolean);
|
|
30
42
|
const chatId = lines[lines.length - 1] || null;
|
|
31
43
|
|
|
@@ -35,7 +47,7 @@ function cursorAgentCreateChat() {
|
|
|
35
47
|
|
|
36
48
|
logger.info(`Created chat session: ${chatId}`);
|
|
37
49
|
return chatId;
|
|
38
|
-
} catch (error) {
|
|
50
|
+
} catch (error: any) {
|
|
39
51
|
// Check for common errors
|
|
40
52
|
if (error.message.includes('ENOENT')) {
|
|
41
53
|
throw new Error('cursor-agent CLI not found. Install with: npm install -g @cursor/agent');
|
|
@@ -77,14 +89,14 @@ function cursorAgentCreateChat() {
|
|
|
77
89
|
}
|
|
78
90
|
}
|
|
79
91
|
|
|
80
|
-
function parseJsonFromStdout(stdout) {
|
|
92
|
+
function parseJsonFromStdout(stdout: string): any {
|
|
81
93
|
const text = String(stdout || '').trim();
|
|
82
94
|
if (!text) return null;
|
|
83
95
|
const lines = text.split('\n').filter(Boolean);
|
|
84
96
|
|
|
85
97
|
for (let i = lines.length - 1; i >= 0; i--) {
|
|
86
|
-
const line = lines[i]
|
|
87
|
-
if (line
|
|
98
|
+
const line = lines[i]?.trim();
|
|
99
|
+
if (line?.startsWith('{') && line?.endsWith('}')) {
|
|
88
100
|
try {
|
|
89
101
|
return JSON.parse(line);
|
|
90
102
|
} catch {
|
|
@@ -95,9 +107,12 @@ function parseJsonFromStdout(stdout) {
|
|
|
95
107
|
return null;
|
|
96
108
|
}
|
|
97
109
|
|
|
98
|
-
function cursorAgentSend({ workspaceDir, chatId, prompt, model }
|
|
99
|
-
|
|
100
|
-
|
|
110
|
+
export function cursorAgentSend({ workspaceDir, chatId, prompt, model }: {
|
|
111
|
+
workspaceDir: string;
|
|
112
|
+
chatId: string;
|
|
113
|
+
prompt: string;
|
|
114
|
+
model?: string;
|
|
115
|
+
}): AgentSendResult {
|
|
101
116
|
const args = [
|
|
102
117
|
'--print',
|
|
103
118
|
'--output-format', 'json',
|
|
@@ -117,7 +132,7 @@ function cursorAgentSend({ workspaceDir, chatId, prompt, model }) {
|
|
|
117
132
|
|
|
118
133
|
// Check for timeout
|
|
119
134
|
if (res.error) {
|
|
120
|
-
if (res.error.code === 'ETIMEDOUT') {
|
|
135
|
+
if ((res.error as any).code === 'ETIMEDOUT') {
|
|
121
136
|
return {
|
|
122
137
|
ok: false,
|
|
123
138
|
exitCode: -1,
|
|
@@ -166,14 +181,14 @@ function cursorAgentSend({ workspaceDir, chatId, prompt, model }) {
|
|
|
166
181
|
|
|
167
182
|
return {
|
|
168
183
|
ok: false,
|
|
169
|
-
exitCode: res.status,
|
|
184
|
+
exitCode: res.status ?? -1,
|
|
170
185
|
error: errorMsg,
|
|
171
186
|
};
|
|
172
187
|
}
|
|
173
188
|
|
|
174
189
|
return {
|
|
175
190
|
ok: !json.is_error,
|
|
176
|
-
exitCode: res.status,
|
|
191
|
+
exitCode: res.status ?? 0,
|
|
177
192
|
sessionId: json.session_id || chatId,
|
|
178
193
|
resultText: json.result || '',
|
|
179
194
|
};
|
|
@@ -182,12 +197,12 @@ function cursorAgentSend({ workspaceDir, chatId, prompt, model }) {
|
|
|
182
197
|
/**
|
|
183
198
|
* Extract dependency change request from agent response
|
|
184
199
|
*/
|
|
185
|
-
function extractDependencyRequest(text) {
|
|
200
|
+
export function extractDependencyRequest(text: string): { required: boolean; plan?: DependencyRequestPlan; raw: string } {
|
|
186
201
|
const t = String(text || '');
|
|
187
202
|
const marker = 'DEPENDENCY_CHANGE_REQUIRED';
|
|
188
203
|
|
|
189
204
|
if (!t.includes(marker)) {
|
|
190
|
-
return { required: false };
|
|
205
|
+
return { required: false, raw: t };
|
|
191
206
|
}
|
|
192
207
|
|
|
193
208
|
const after = t.split(marker).slice(1).join(marker);
|
|
@@ -197,7 +212,7 @@ function extractDependencyRequest(text) {
|
|
|
197
212
|
try {
|
|
198
213
|
return {
|
|
199
214
|
required: true,
|
|
200
|
-
plan: JSON.parse(match[0]),
|
|
215
|
+
plan: JSON.parse(match[0]!) as DependencyRequestPlan,
|
|
201
216
|
raw: t,
|
|
202
217
|
};
|
|
203
218
|
} catch {
|
|
@@ -211,7 +226,7 @@ function extractDependencyRequest(text) {
|
|
|
211
226
|
/**
|
|
212
227
|
* Wrap prompt with dependency policy
|
|
213
228
|
*/
|
|
214
|
-
function wrapPromptForDependencyPolicy(prompt, policy) {
|
|
229
|
+
export function wrapPromptForDependencyPolicy(prompt: string, policy: DependencyPolicy): string {
|
|
215
230
|
if (policy.allowDependencyChange && !policy.lockfileReadOnly) {
|
|
216
231
|
return prompt;
|
|
217
232
|
}
|
|
@@ -243,8 +258,8 @@ ${prompt}`;
|
|
|
243
258
|
/**
|
|
244
259
|
* Apply file permissions based on dependency policy
|
|
245
260
|
*/
|
|
246
|
-
function applyDependencyFilePermissions(worktreeDir, policy) {
|
|
247
|
-
const targets = [];
|
|
261
|
+
export function applyDependencyFilePermissions(worktreeDir: string, policy: DependencyPolicy): void {
|
|
262
|
+
const targets: string[] = [];
|
|
248
263
|
|
|
249
264
|
if (!policy.allowDependencyChange) {
|
|
250
265
|
targets.push('package.json');
|
|
@@ -271,16 +286,24 @@ function applyDependencyFilePermissions(worktreeDir, policy) {
|
|
|
271
286
|
/**
|
|
272
287
|
* Run a single task
|
|
273
288
|
*/
|
|
274
|
-
async function runTask({
|
|
289
|
+
export async function runTask({
|
|
275
290
|
task,
|
|
276
291
|
config,
|
|
277
292
|
index,
|
|
278
293
|
worktreeDir,
|
|
279
|
-
pipelineBranch,
|
|
280
294
|
taskBranch,
|
|
281
295
|
chatId,
|
|
282
296
|
runDir,
|
|
283
|
-
}
|
|
297
|
+
}: {
|
|
298
|
+
task: Task;
|
|
299
|
+
config: RunnerConfig;
|
|
300
|
+
index: number;
|
|
301
|
+
worktreeDir: string;
|
|
302
|
+
pipelineBranch: string;
|
|
303
|
+
taskBranch: string;
|
|
304
|
+
chatId: string;
|
|
305
|
+
runDir: string;
|
|
306
|
+
}): Promise<TaskExecutionResult> {
|
|
284
307
|
const model = task.model || config.model || 'sonnet-4.5';
|
|
285
308
|
const convoPath = path.join(runDir, 'conversation.jsonl');
|
|
286
309
|
|
|
@@ -310,7 +333,7 @@ async function runTask({
|
|
|
310
333
|
model,
|
|
311
334
|
});
|
|
312
335
|
|
|
313
|
-
appendLog(convoPath, createConversationEntry('assistant', r1.resultText || r1.error, {
|
|
336
|
+
appendLog(convoPath, createConversationEntry('assistant', r1.resultText || r1.error || 'No response', {
|
|
314
337
|
task: task.name,
|
|
315
338
|
model,
|
|
316
339
|
}));
|
|
@@ -325,7 +348,7 @@ async function runTask({
|
|
|
325
348
|
}
|
|
326
349
|
|
|
327
350
|
// Check for dependency request
|
|
328
|
-
const depReq = extractDependencyRequest(r1.resultText);
|
|
351
|
+
const depReq = extractDependencyRequest(r1.resultText || '');
|
|
329
352
|
if (depReq.required && !config.dependencyPolicy.allowDependencyChange) {
|
|
330
353
|
return {
|
|
331
354
|
taskName: task.name,
|
|
@@ -348,9 +371,7 @@ async function runTask({
|
|
|
348
371
|
/**
|
|
349
372
|
* Run all tasks in sequence
|
|
350
373
|
*/
|
|
351
|
-
async function runTasks(config, runDir) {
|
|
352
|
-
const { checkCursorAuth, printAuthHelp } = require('../utils/cursor-agent');
|
|
353
|
-
|
|
374
|
+
export async function runTasks(config: RunnerConfig, runDir: string): Promise<TaskExecutionResult[]> {
|
|
354
375
|
// Ensure cursor-agent is installed
|
|
355
376
|
ensureCursorAgent();
|
|
356
377
|
|
|
@@ -379,7 +400,7 @@ async function runTasks(config, runDir) {
|
|
|
379
400
|
logger.success('✓ Cursor authentication OK');
|
|
380
401
|
|
|
381
402
|
const repoRoot = git.getRepoRoot();
|
|
382
|
-
const pipelineBranch = config.pipelineBranch || `${config.branchPrefix}${Date.now().toString(36)}`;
|
|
403
|
+
const pipelineBranch = config.pipelineBranch || `${config.branchPrefix || 'cursorflow/'}${Date.now().toString(36)}`;
|
|
383
404
|
const worktreeDir = path.join(repoRoot, config.worktreeRoot || '_cursorflow/worktrees', pipelineBranch);
|
|
384
405
|
|
|
385
406
|
logger.section('🚀 Starting Pipeline');
|
|
@@ -398,22 +419,26 @@ async function runTasks(config, runDir) {
|
|
|
398
419
|
const chatId = cursorAgentCreateChat();
|
|
399
420
|
|
|
400
421
|
// Save initial state
|
|
401
|
-
const state = {
|
|
422
|
+
const state: LaneState = {
|
|
402
423
|
status: 'running',
|
|
403
424
|
pipelineBranch,
|
|
404
425
|
worktreeDir,
|
|
405
|
-
chatId,
|
|
406
426
|
totalTasks: config.tasks.length,
|
|
407
427
|
currentTaskIndex: 0,
|
|
428
|
+
label: pipelineBranch,
|
|
429
|
+
startTime: Date.now(),
|
|
430
|
+
endTime: null,
|
|
431
|
+
error: null,
|
|
432
|
+
dependencyRequest: null,
|
|
408
433
|
};
|
|
409
434
|
|
|
410
435
|
saveState(path.join(runDir, 'state.json'), state);
|
|
411
436
|
|
|
412
437
|
// Run tasks
|
|
413
|
-
const results = [];
|
|
438
|
+
const results: TaskExecutionResult[] = [];
|
|
414
439
|
|
|
415
440
|
for (let i = 0; i < config.tasks.length; i++) {
|
|
416
|
-
const task = config.tasks[i]
|
|
441
|
+
const task = config.tasks[i]!;
|
|
417
442
|
const taskBranch = `${pipelineBranch}--${String(i + 1).padStart(2, '0')}-${task.name}`;
|
|
418
443
|
|
|
419
444
|
const result = await runTask({
|
|
@@ -435,8 +460,8 @@ async function runTasks(config, runDir) {
|
|
|
435
460
|
|
|
436
461
|
// Handle blocked or error
|
|
437
462
|
if (result.status === 'BLOCKED_DEPENDENCY') {
|
|
438
|
-
state.status = '
|
|
439
|
-
state.dependencyRequest = result.dependencyRequest;
|
|
463
|
+
state.status = 'failed'; // Or blocked if we had a blocked status in LaneState
|
|
464
|
+
state.dependencyRequest = result.dependencyRequest || null;
|
|
440
465
|
saveState(path.join(runDir, 'state.json'), state);
|
|
441
466
|
logger.warn('Task blocked on dependency change');
|
|
442
467
|
process.exit(2);
|
|
@@ -444,6 +469,7 @@ async function runTasks(config, runDir) {
|
|
|
444
469
|
|
|
445
470
|
if (result.status !== 'FINISHED') {
|
|
446
471
|
state.status = 'failed';
|
|
472
|
+
state.error = result.error || 'Unknown error';
|
|
447
473
|
saveState(path.join(runDir, 'state.json'), state);
|
|
448
474
|
logger.error(`Task failed: ${result.error}`);
|
|
449
475
|
process.exit(1);
|
|
@@ -457,17 +483,13 @@ async function runTasks(config, runDir) {
|
|
|
457
483
|
|
|
458
484
|
// Complete
|
|
459
485
|
state.status = 'completed';
|
|
486
|
+
state.endTime = Date.now();
|
|
460
487
|
saveState(path.join(runDir, 'state.json'), state);
|
|
461
488
|
|
|
462
489
|
logger.success('All tasks completed!');
|
|
463
490
|
return results;
|
|
464
491
|
}
|
|
465
492
|
|
|
466
|
-
module.exports = {
|
|
467
|
-
runTasks,
|
|
468
|
-
runTask,
|
|
469
|
-
};
|
|
470
|
-
|
|
471
493
|
/**
|
|
472
494
|
* CLI entry point
|
|
473
495
|
*/
|
|
@@ -479,12 +501,12 @@ if (require.main === module) {
|
|
|
479
501
|
process.exit(1);
|
|
480
502
|
}
|
|
481
503
|
|
|
482
|
-
const tasksFile = args[0]
|
|
504
|
+
const tasksFile = args[0]!;
|
|
483
505
|
const runDirIdx = args.indexOf('--run-dir');
|
|
484
|
-
const executorIdx = args.indexOf('--executor');
|
|
506
|
+
// const executorIdx = args.indexOf('--executor');
|
|
485
507
|
|
|
486
|
-
const runDir = runDirIdx >= 0 ? args[runDirIdx + 1] : '.';
|
|
487
|
-
const executor = executorIdx >= 0 ? args[executorIdx + 1] : 'cursor-agent';
|
|
508
|
+
const runDir = runDirIdx >= 0 ? args[runDirIdx + 1]! : '.';
|
|
509
|
+
// const executor = executorIdx >= 0 ? args[executorIdx + 1] : 'cursor-agent';
|
|
488
510
|
|
|
489
511
|
if (!fs.existsSync(tasksFile)) {
|
|
490
512
|
console.error(`Tasks file not found: ${tasksFile}`);
|
|
@@ -492,10 +514,10 @@ if (require.main === module) {
|
|
|
492
514
|
}
|
|
493
515
|
|
|
494
516
|
// Load tasks configuration
|
|
495
|
-
let config;
|
|
517
|
+
let config: RunnerConfig;
|
|
496
518
|
try {
|
|
497
|
-
config = JSON.parse(fs.readFileSync(tasksFile, 'utf8'));
|
|
498
|
-
} catch (error) {
|
|
519
|
+
config = JSON.parse(fs.readFileSync(tasksFile, 'utf8')) as RunnerConfig;
|
|
520
|
+
} catch (error: any) {
|
|
499
521
|
console.error(`Failed to load tasks file: ${error.message}`);
|
|
500
522
|
process.exit(1);
|
|
501
523
|
}
|
|
@@ -513,7 +535,7 @@ if (require.main === module) {
|
|
|
513
535
|
})
|
|
514
536
|
.catch(error => {
|
|
515
537
|
console.error(`Runner failed: ${error.message}`);
|
|
516
|
-
if (process.env
|
|
538
|
+
if (process.env['DEBUG']) {
|
|
517
539
|
console.error(error.stack);
|
|
518
540
|
}
|
|
519
541
|
process.exit(1);
|