@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.
@@ -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
+ };