@litmers/cursorflow-orchestrator 0.1.0
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 +78 -0
- package/LICENSE +21 -0
- package/README.md +310 -0
- package/commands/cursorflow-clean.md +162 -0
- package/commands/cursorflow-init.md +67 -0
- package/commands/cursorflow-monitor.md +131 -0
- package/commands/cursorflow-prepare.md +134 -0
- package/commands/cursorflow-resume.md +181 -0
- package/commands/cursorflow-review.md +220 -0
- package/commands/cursorflow-run.md +129 -0
- package/package.json +52 -0
- package/scripts/postinstall.js +27 -0
- package/src/cli/clean.js +30 -0
- package/src/cli/index.js +93 -0
- package/src/cli/init.js +235 -0
- package/src/cli/monitor.js +29 -0
- package/src/cli/resume.js +31 -0
- package/src/cli/run.js +51 -0
- package/src/cli/setup-commands.js +210 -0
- package/src/core/orchestrator.js +185 -0
- package/src/core/reviewer.js +233 -0
- package/src/core/runner.js +343 -0
- package/src/utils/config.js +195 -0
- package/src/utils/cursor-agent.js +190 -0
- package/src/utils/git.js +311 -0
- package/src/utils/logger.js +178 -0
- package/src/utils/state.js +207 -0
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Setup Cursor commands
|
|
4
|
+
*
|
|
5
|
+
* Installs CursorFlow commands to .cursor/commands/cursorflow/
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const logger = require('../utils/logger');
|
|
11
|
+
const { findProjectRoot } = require('../utils/config');
|
|
12
|
+
|
|
13
|
+
function parseArgs(args) {
|
|
14
|
+
const options = {
|
|
15
|
+
force: false,
|
|
16
|
+
uninstall: false,
|
|
17
|
+
silent: false,
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
for (const arg of args) {
|
|
21
|
+
switch (arg) {
|
|
22
|
+
case '--force':
|
|
23
|
+
options.force = true;
|
|
24
|
+
break;
|
|
25
|
+
case '--uninstall':
|
|
26
|
+
options.uninstall = true;
|
|
27
|
+
break;
|
|
28
|
+
case '--silent':
|
|
29
|
+
options.silent = true;
|
|
30
|
+
break;
|
|
31
|
+
case '--help':
|
|
32
|
+
case '-h':
|
|
33
|
+
printHelp();
|
|
34
|
+
process.exit(0);
|
|
35
|
+
break;
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return options;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function printHelp() {
|
|
43
|
+
console.log(`
|
|
44
|
+
Usage: cursorflow-setup [options]
|
|
45
|
+
|
|
46
|
+
Install CursorFlow commands to Cursor IDE
|
|
47
|
+
|
|
48
|
+
Options:
|
|
49
|
+
--force Overwrite existing commands
|
|
50
|
+
--uninstall Remove installed commands
|
|
51
|
+
--silent Suppress output
|
|
52
|
+
--help, -h Show help
|
|
53
|
+
|
|
54
|
+
Examples:
|
|
55
|
+
cursorflow-setup
|
|
56
|
+
cursorflow-setup --force
|
|
57
|
+
cursorflow-setup --uninstall
|
|
58
|
+
`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function getCommandsSourceDir() {
|
|
62
|
+
// Commands are in the package directory
|
|
63
|
+
return path.join(__dirname, '..', '..', 'commands');
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function setupCommands(options = {}) {
|
|
67
|
+
const projectRoot = findProjectRoot();
|
|
68
|
+
const targetDir = path.join(projectRoot, '.cursor', 'commands', 'cursorflow');
|
|
69
|
+
const sourceDir = getCommandsSourceDir();
|
|
70
|
+
|
|
71
|
+
if (!options.silent) {
|
|
72
|
+
logger.info(`Installing commands to: ${path.relative(projectRoot, targetDir)}`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Create target directory
|
|
76
|
+
if (!fs.existsSync(targetDir)) {
|
|
77
|
+
fs.mkdirSync(targetDir, { recursive: true });
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// Get list of command files
|
|
81
|
+
if (!fs.existsSync(sourceDir)) {
|
|
82
|
+
throw new Error(`Commands directory not found: ${sourceDir}`);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const commandFiles = fs.readdirSync(sourceDir).filter(f => f.endsWith('.md'));
|
|
86
|
+
|
|
87
|
+
if (commandFiles.length === 0) {
|
|
88
|
+
throw new Error(`No command files found in ${sourceDir}`);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
let installed = 0;
|
|
92
|
+
let backed = 0;
|
|
93
|
+
let skipped = 0;
|
|
94
|
+
|
|
95
|
+
for (const file of commandFiles) {
|
|
96
|
+
const sourcePath = path.join(sourceDir, file);
|
|
97
|
+
const targetPath = path.join(targetDir, file);
|
|
98
|
+
|
|
99
|
+
// Check if file exists
|
|
100
|
+
if (fs.existsSync(targetPath)) {
|
|
101
|
+
if (options.force) {
|
|
102
|
+
// Backup existing file
|
|
103
|
+
const backupPath = `${targetPath}.backup`;
|
|
104
|
+
fs.copyFileSync(targetPath, backupPath);
|
|
105
|
+
backed++;
|
|
106
|
+
if (!options.silent) {
|
|
107
|
+
logger.info(`📦 Backed up: ${file}`);
|
|
108
|
+
}
|
|
109
|
+
} else {
|
|
110
|
+
skipped++;
|
|
111
|
+
if (!options.silent) {
|
|
112
|
+
logger.info(`⏭️ Skipped (exists): ${file}`);
|
|
113
|
+
}
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
// Copy file
|
|
119
|
+
fs.copyFileSync(sourcePath, targetPath);
|
|
120
|
+
installed++;
|
|
121
|
+
if (!options.silent) {
|
|
122
|
+
logger.success(`Installed: ${file}`);
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (!options.silent) {
|
|
127
|
+
logger.section('🎉 Setup complete!');
|
|
128
|
+
console.log(` Installed: ${installed} commands`);
|
|
129
|
+
if (backed > 0) {
|
|
130
|
+
console.log(` Backed up: ${backed} existing commands`);
|
|
131
|
+
}
|
|
132
|
+
if (skipped > 0) {
|
|
133
|
+
console.log(` Skipped: ${skipped} commands (use --force to overwrite)`);
|
|
134
|
+
}
|
|
135
|
+
console.log(`\n📍 Location: ${targetDir}`);
|
|
136
|
+
console.log('\n💡 Usage: Type "/" in Cursor chat to see commands');
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
return { installed, backed, skipped };
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
function uninstallCommands(options = {}) {
|
|
143
|
+
const projectRoot = findProjectRoot();
|
|
144
|
+
const targetDir = path.join(projectRoot, '.cursor', 'commands', 'cursorflow');
|
|
145
|
+
|
|
146
|
+
if (!fs.existsSync(targetDir)) {
|
|
147
|
+
if (!options.silent) {
|
|
148
|
+
logger.info('Commands directory not found, nothing to uninstall');
|
|
149
|
+
}
|
|
150
|
+
return { removed: 0 };
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
const commandFiles = fs.readdirSync(targetDir).filter(f => f.endsWith('.md'));
|
|
154
|
+
let removed = 0;
|
|
155
|
+
|
|
156
|
+
for (const file of commandFiles) {
|
|
157
|
+
const filePath = path.join(targetDir, file);
|
|
158
|
+
fs.unlinkSync(filePath);
|
|
159
|
+
removed++;
|
|
160
|
+
if (!options.silent) {
|
|
161
|
+
logger.info(`Removed: ${file}`);
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// Remove directory if empty
|
|
166
|
+
const remainingFiles = fs.readdirSync(targetDir);
|
|
167
|
+
if (remainingFiles.length === 0) {
|
|
168
|
+
fs.rmdirSync(targetDir);
|
|
169
|
+
if (!options.silent) {
|
|
170
|
+
logger.info(`Removed directory: ${targetDir}`);
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
if (!options.silent) {
|
|
175
|
+
logger.success(`Uninstalled ${removed} commands`);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return { removed };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async function main(args) {
|
|
182
|
+
const options = parseArgs(args);
|
|
183
|
+
|
|
184
|
+
try {
|
|
185
|
+
if (options.uninstall) {
|
|
186
|
+
return uninstallCommands(options);
|
|
187
|
+
} else {
|
|
188
|
+
return setupCommands(options);
|
|
189
|
+
}
|
|
190
|
+
} catch (error) {
|
|
191
|
+
if (!options.silent) {
|
|
192
|
+
logger.error(error.message);
|
|
193
|
+
}
|
|
194
|
+
throw error;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
if (require.main === module) {
|
|
199
|
+
main(process.argv.slice(2)).catch(error => {
|
|
200
|
+
console.error('❌ Error:', error.message);
|
|
201
|
+
if (process.env.DEBUG) {
|
|
202
|
+
console.error(error.stack);
|
|
203
|
+
}
|
|
204
|
+
process.exit(1);
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
module.exports = setupCommands;
|
|
209
|
+
module.exports.setupCommands = setupCommands;
|
|
210
|
+
module.exports.uninstallCommands = uninstallCommands;
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Orchestrator - Parallel lane execution with dependency management
|
|
4
|
+
*
|
|
5
|
+
* Adapted from admin-domains-orchestrator.js
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const fs = require('fs');
|
|
9
|
+
const path = require('path');
|
|
10
|
+
const { spawn } = require('child_process');
|
|
11
|
+
|
|
12
|
+
const logger = require('../utils/logger');
|
|
13
|
+
const { loadState, saveState } = require('../utils/state');
|
|
14
|
+
const { runTasks } = require('./runner');
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Spawn a lane process
|
|
18
|
+
*/
|
|
19
|
+
function spawnLane({ laneName, tasksFile, laneRunDir, executor }) {
|
|
20
|
+
fs.mkdirSync(laneRunDir, { recursive: true});
|
|
21
|
+
const logPath = path.join(laneRunDir, 'terminal.log');
|
|
22
|
+
const logFd = fs.openSync(logPath, 'a');
|
|
23
|
+
|
|
24
|
+
const args = [
|
|
25
|
+
require.resolve('./runner.js'),
|
|
26
|
+
tasksFile,
|
|
27
|
+
'--run-dir', laneRunDir,
|
|
28
|
+
'--executor', executor,
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
const child = spawn('node', args, {
|
|
32
|
+
stdio: ['ignore', logFd, logFd],
|
|
33
|
+
env: process.env,
|
|
34
|
+
detached: false,
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
try {
|
|
38
|
+
fs.closeSync(logFd);
|
|
39
|
+
} catch {
|
|
40
|
+
// Ignore
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return { child, logPath };
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Wait for child process to exit
|
|
48
|
+
*/
|
|
49
|
+
function waitChild(proc) {
|
|
50
|
+
return new Promise((resolve) => {
|
|
51
|
+
if (proc.exitCode !== null) {
|
|
52
|
+
resolve(proc.exitCode);
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
proc.once('exit', (code) => resolve(code ?? 1));
|
|
57
|
+
proc.once('error', () => resolve(1));
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* List lane task files in directory
|
|
63
|
+
*/
|
|
64
|
+
function listLaneFiles(tasksDir) {
|
|
65
|
+
if (!fs.existsSync(tasksDir)) {
|
|
66
|
+
return [];
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
const files = fs.readdirSync(tasksDir);
|
|
70
|
+
return files
|
|
71
|
+
.filter(f => f.endsWith('.json'))
|
|
72
|
+
.sort()
|
|
73
|
+
.map(f => ({
|
|
74
|
+
name: path.basename(f, '.json'),
|
|
75
|
+
path: path.join(tasksDir, f),
|
|
76
|
+
}));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Monitor lane states
|
|
81
|
+
*/
|
|
82
|
+
function printLaneStatus(lanes, laneRunDirs) {
|
|
83
|
+
const rows = lanes.map(lane => {
|
|
84
|
+
const statePath = path.join(laneRunDirs[lane.name], 'state.json');
|
|
85
|
+
const state = loadState(statePath);
|
|
86
|
+
|
|
87
|
+
if (!state) {
|
|
88
|
+
return { lane: lane.name, status: '(no state)', task: '-' };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const idx = state.currentTaskIndex + 1;
|
|
92
|
+
return {
|
|
93
|
+
lane: lane.name,
|
|
94
|
+
status: state.status || 'unknown',
|
|
95
|
+
task: `${idx}/${state.totalTasks || '?'}`,
|
|
96
|
+
};
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
logger.section('📡 Lane Status');
|
|
100
|
+
for (const r of rows) {
|
|
101
|
+
console.log(`- ${r.lane}: ${r.status} (${r.task})`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Run orchestration
|
|
107
|
+
*/
|
|
108
|
+
async function orchestrate(tasksDir, options = {}) {
|
|
109
|
+
const lanes = listLaneFiles(tasksDir);
|
|
110
|
+
|
|
111
|
+
if (lanes.length === 0) {
|
|
112
|
+
throw new Error(`No lane task files found in ${tasksDir}`);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
const runRoot = options.runDir || `_cursorflow/logs/runs/run-${Date.now()}`;
|
|
116
|
+
fs.mkdirSync(runRoot, { recursive: true });
|
|
117
|
+
|
|
118
|
+
const laneRunDirs = {};
|
|
119
|
+
for (const lane of lanes) {
|
|
120
|
+
laneRunDirs[lane.name] = path.join(runRoot, 'lanes', lane.name);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
logger.section('🧭 Starting Orchestration');
|
|
124
|
+
logger.info(`Tasks directory: ${tasksDir}`);
|
|
125
|
+
logger.info(`Run directory: ${runRoot}`);
|
|
126
|
+
logger.info(`Lanes: ${lanes.length}`);
|
|
127
|
+
|
|
128
|
+
// Spawn all lanes
|
|
129
|
+
const running = [];
|
|
130
|
+
|
|
131
|
+
for (const lane of lanes) {
|
|
132
|
+
const { child, logPath } = spawnLane({
|
|
133
|
+
laneName: lane.name,
|
|
134
|
+
tasksFile: lane.path,
|
|
135
|
+
laneRunDir: laneRunDirs[lane.name],
|
|
136
|
+
executor: options.executor || 'cursor-agent',
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
running.push({ lane: lane.name, child, logPath });
|
|
140
|
+
logger.info(`Lane started: ${lane.name}`);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Monitor lanes
|
|
144
|
+
const monitorInterval = setInterval(() => {
|
|
145
|
+
printLaneStatus(lanes, laneRunDirs);
|
|
146
|
+
}, options.pollInterval || 60000);
|
|
147
|
+
|
|
148
|
+
// Wait for all lanes
|
|
149
|
+
const exitCodes = {};
|
|
150
|
+
|
|
151
|
+
for (const r of running) {
|
|
152
|
+
exitCodes[r.lane] = await waitChild(r.child);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
clearInterval(monitorInterval);
|
|
156
|
+
printLaneStatus(lanes, laneRunDirs);
|
|
157
|
+
|
|
158
|
+
// Check for failures
|
|
159
|
+
const failed = Object.entries(exitCodes).filter(([, code]) => code !== 0 && code !== 2);
|
|
160
|
+
|
|
161
|
+
if (failed.length > 0) {
|
|
162
|
+
logger.error(`Lanes failed: ${failed.map(([l, c]) => `${l}(${c})`).join(', ')}`);
|
|
163
|
+
process.exit(1);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Check for blocked lanes
|
|
167
|
+
const blocked = Object.entries(exitCodes)
|
|
168
|
+
.filter(([, code]) => code === 2)
|
|
169
|
+
.map(([lane]) => lane);
|
|
170
|
+
|
|
171
|
+
if (blocked.length > 0) {
|
|
172
|
+
logger.warn(`Lanes blocked on dependency: ${blocked.join(', ')}`);
|
|
173
|
+
logger.info('Handle dependency changes manually and resume lanes');
|
|
174
|
+
process.exit(2);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
logger.success('All lanes completed successfully!');
|
|
178
|
+
return { lanes, exitCodes, runRoot };
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
module.exports = {
|
|
182
|
+
orchestrate,
|
|
183
|
+
spawnLane,
|
|
184
|
+
listLaneFiles,
|
|
185
|
+
};
|
|
@@ -0,0 +1,233 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Reviewer - Code review agent
|
|
4
|
+
*
|
|
5
|
+
* Adapted from reviewer-agent.js
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const logger = require('../utils/logger');
|
|
9
|
+
const { appendLog, createConversationEntry } = require('../utils/state');
|
|
10
|
+
const path = require('path');
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Build review prompt
|
|
14
|
+
*/
|
|
15
|
+
function buildReviewPrompt({ taskName, taskBranch, acceptanceCriteria = [] }) {
|
|
16
|
+
const criteriaList = acceptanceCriteria.length > 0
|
|
17
|
+
? acceptanceCriteria.map((c, i) => `${i + 1}. ${c}`).join('\n')
|
|
18
|
+
: 'Work should be completed properly.';
|
|
19
|
+
|
|
20
|
+
return `# Code Review: ${taskName}
|
|
21
|
+
|
|
22
|
+
## Role
|
|
23
|
+
You are a senior code reviewer. Please review the results of this task.
|
|
24
|
+
|
|
25
|
+
## Task Details
|
|
26
|
+
- Name: ${taskName}
|
|
27
|
+
- Branch: ${taskBranch}
|
|
28
|
+
|
|
29
|
+
## Acceptance Criteria
|
|
30
|
+
${criteriaList}
|
|
31
|
+
|
|
32
|
+
## Review Checklist
|
|
33
|
+
1. **Build Success**: Does \`pnpm build\` complete without errors?
|
|
34
|
+
2. **Code Quality**: Are there no linting or TypeScript type errors?
|
|
35
|
+
3. **Completeness**: Are all acceptance criteria met?
|
|
36
|
+
4. **Bugs**: Are there any obvious bugs or logic errors?
|
|
37
|
+
5. **Commit Status**: Are changes properly committed and pushed?
|
|
38
|
+
|
|
39
|
+
## Output Format (MUST follow exactly)
|
|
40
|
+
\`\`\`json
|
|
41
|
+
{
|
|
42
|
+
"status": "approved" | "needs_changes",
|
|
43
|
+
"buildSuccess": true | false,
|
|
44
|
+
"issues": [
|
|
45
|
+
{
|
|
46
|
+
"severity": "critical" | "major" | "minor",
|
|
47
|
+
"description": "...",
|
|
48
|
+
"file": "...",
|
|
49
|
+
"suggestion": "..."
|
|
50
|
+
}
|
|
51
|
+
],
|
|
52
|
+
"suggestions": ["..."],
|
|
53
|
+
"summary": "One-line summary"
|
|
54
|
+
}
|
|
55
|
+
\`\`\`
|
|
56
|
+
|
|
57
|
+
IMPORTANT: You MUST respond in the exact JSON format above. "status" must be either "approved" or "needs_changes".
|
|
58
|
+
`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Parse review result
|
|
63
|
+
*/
|
|
64
|
+
function parseReviewResult(text) {
|
|
65
|
+
const t = String(text || '');
|
|
66
|
+
|
|
67
|
+
// Try JSON block
|
|
68
|
+
const jsonMatch = t.match(/```json\n([\s\S]*?)\n```/);
|
|
69
|
+
if (jsonMatch) {
|
|
70
|
+
try {
|
|
71
|
+
const parsed = JSON.parse(jsonMatch[1]);
|
|
72
|
+
return {
|
|
73
|
+
status: parsed.status || 'needs_changes',
|
|
74
|
+
buildSuccess: parsed.buildSuccess !== false,
|
|
75
|
+
issues: Array.isArray(parsed.issues) ? parsed.issues : [],
|
|
76
|
+
suggestions: Array.isArray(parsed.suggestions) ? parsed.suggestions : [],
|
|
77
|
+
summary: parsed.summary || '',
|
|
78
|
+
raw: t,
|
|
79
|
+
};
|
|
80
|
+
} catch (err) {
|
|
81
|
+
logger.warn(`JSON parse failed: ${err.message}`);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// Fallback parsing
|
|
86
|
+
const hasApproved = t.toLowerCase().includes('"status": "approved"');
|
|
87
|
+
const hasIssues = t.toLowerCase().includes('needs_changes') ||
|
|
88
|
+
t.toLowerCase().includes('error') ||
|
|
89
|
+
t.toLowerCase().includes('failed');
|
|
90
|
+
|
|
91
|
+
return {
|
|
92
|
+
status: hasApproved && !hasIssues ? 'approved' : 'needs_changes',
|
|
93
|
+
buildSuccess: !t.toLowerCase().includes('build') || !t.toLowerCase().includes('fail'),
|
|
94
|
+
issues: hasIssues ? [{ severity: 'major', description: 'Parse failed, see logs' }] : [],
|
|
95
|
+
suggestions: [],
|
|
96
|
+
summary: 'Auto-parsed - check original response',
|
|
97
|
+
raw: t,
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Build feedback prompt
|
|
103
|
+
*/
|
|
104
|
+
function buildFeedbackPrompt(review) {
|
|
105
|
+
const lines = [];
|
|
106
|
+
lines.push('# Code Review Feedback');
|
|
107
|
+
lines.push('');
|
|
108
|
+
lines.push('The reviewer found the following issues. Please fix them:');
|
|
109
|
+
lines.push('');
|
|
110
|
+
|
|
111
|
+
if (!review.buildSuccess) {
|
|
112
|
+
lines.push('## CRITICAL: Build Failed');
|
|
113
|
+
lines.push('- `pnpm build` failed. Fix build errors first.');
|
|
114
|
+
lines.push('');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
for (const issue of review.issues || []) {
|
|
118
|
+
const severity = (issue.severity || 'major').toUpperCase();
|
|
119
|
+
lines.push(`## ${severity}: ${issue.description}`);
|
|
120
|
+
if (issue.file) lines.push(`- File: ${issue.file}`);
|
|
121
|
+
if (issue.suggestion) lines.push(`- Suggestion: ${issue.suggestion}`);
|
|
122
|
+
lines.push('');
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (review.suggestions && review.suggestions.length > 0) {
|
|
126
|
+
lines.push('## Additional Suggestions');
|
|
127
|
+
for (const s of review.suggestions) {
|
|
128
|
+
lines.push(`- ${s}`);
|
|
129
|
+
}
|
|
130
|
+
lines.push('');
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
lines.push('## Requirements');
|
|
134
|
+
lines.push('1. Fix all issues listed above');
|
|
135
|
+
lines.push('2. Ensure `pnpm build` succeeds');
|
|
136
|
+
lines.push('3. Commit and push your changes');
|
|
137
|
+
lines.push('');
|
|
138
|
+
lines.push('**Let me know when fixes are complete.**');
|
|
139
|
+
|
|
140
|
+
return lines.join('\n');
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Review task
|
|
145
|
+
*/
|
|
146
|
+
async function reviewTask({ taskResult, worktreeDir, runDir, config, cursorAgentSend, cursorAgentCreateChat }) {
|
|
147
|
+
const reviewPrompt = buildReviewPrompt({
|
|
148
|
+
taskName: taskResult.taskName,
|
|
149
|
+
taskBranch: taskResult.taskBranch,
|
|
150
|
+
acceptanceCriteria: config.acceptanceCriteria || [],
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
logger.info(`Reviewing: ${taskResult.taskName}`);
|
|
154
|
+
|
|
155
|
+
const reviewChatId = cursorAgentCreateChat();
|
|
156
|
+
const reviewResult = cursorAgentSend({
|
|
157
|
+
workspaceDir: worktreeDir,
|
|
158
|
+
chatId: reviewChatId,
|
|
159
|
+
prompt: reviewPrompt,
|
|
160
|
+
model: config.reviewModel || 'sonnet-4.5-thinking',
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
const review = parseReviewResult(reviewResult.resultText);
|
|
164
|
+
|
|
165
|
+
// Log review
|
|
166
|
+
const convoPath = path.join(runDir, 'conversation.jsonl');
|
|
167
|
+
appendLog(convoPath, createConversationEntry('reviewer', reviewResult.resultText, {
|
|
168
|
+
task: taskResult.taskName,
|
|
169
|
+
model: config.reviewModel,
|
|
170
|
+
}));
|
|
171
|
+
|
|
172
|
+
logger.info(`Review result: ${review.status} (${review.issues?.length || 0} issues)`);
|
|
173
|
+
|
|
174
|
+
return review;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* Review loop with feedback
|
|
179
|
+
*/
|
|
180
|
+
async function runReviewLoop({ taskResult, worktreeDir, runDir, config, workChatId, cursorAgentSend, cursorAgentCreateChat }) {
|
|
181
|
+
const maxIterations = config.maxReviewIterations || 3;
|
|
182
|
+
let iteration = 0;
|
|
183
|
+
let currentReview = null;
|
|
184
|
+
|
|
185
|
+
while (iteration < maxIterations) {
|
|
186
|
+
currentReview = await reviewTask({
|
|
187
|
+
taskResult,
|
|
188
|
+
worktreeDir,
|
|
189
|
+
runDir,
|
|
190
|
+
config,
|
|
191
|
+
cursorAgentSend,
|
|
192
|
+
cursorAgentCreateChat,
|
|
193
|
+
});
|
|
194
|
+
|
|
195
|
+
if (currentReview.status === 'approved') {
|
|
196
|
+
logger.success(`Review passed: ${taskResult.taskName} (iteration ${iteration + 1})`);
|
|
197
|
+
return { approved: true, review: currentReview, iterations: iteration + 1 };
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
iteration++;
|
|
201
|
+
|
|
202
|
+
if (iteration >= maxIterations) {
|
|
203
|
+
logger.warn(`Max review iterations (${maxIterations}) reached: ${taskResult.taskName}`);
|
|
204
|
+
break;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Send feedback
|
|
208
|
+
logger.info(`Sending feedback (iteration ${iteration}/${maxIterations})`);
|
|
209
|
+
const feedbackPrompt = buildFeedbackPrompt(currentReview);
|
|
210
|
+
|
|
211
|
+
const fixResult = cursorAgentSend({
|
|
212
|
+
workspaceDir: worktreeDir,
|
|
213
|
+
chatId: workChatId,
|
|
214
|
+
prompt: feedbackPrompt,
|
|
215
|
+
model: config.model,
|
|
216
|
+
});
|
|
217
|
+
|
|
218
|
+
if (!fixResult.ok) {
|
|
219
|
+
logger.error(`Feedback application failed: ${fixResult.error}`);
|
|
220
|
+
return { approved: false, review: currentReview, iterations: iteration, error: fixResult.error };
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
return { approved: false, review: currentReview, iterations: iteration };
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
module.exports = {
|
|
228
|
+
buildReviewPrompt,
|
|
229
|
+
parseReviewResult,
|
|
230
|
+
buildFeedbackPrompt,
|
|
231
|
+
reviewTask,
|
|
232
|
+
runReviewLoop,
|
|
233
|
+
};
|