@jojonax/codex-copilot 1.0.3 → 1.2.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/bin/cli.js +5 -0
- package/package.json +1 -1
- package/src/commands/init.js +44 -36
- package/src/commands/reset.js +16 -8
- package/src/commands/run.js +289 -161
- package/src/commands/status.js +24 -7
- package/src/utils/automator.js +279 -0
- package/src/utils/checkpoint.js +129 -0
- package/src/utils/git.js +6 -2
- package/src/utils/github.js +145 -8
- package/src/utils/provider.js +415 -0
package/src/commands/run.js
CHANGED
|
@@ -2,15 +2,17 @@
|
|
|
2
2
|
* codex-copilot run - Main orchestration loop
|
|
3
3
|
*
|
|
4
4
|
* Executes tasks one by one: Develop → PR → Review → Fix → Merge → Next
|
|
5
|
+
* Features fine-grained checkpoint/resume at every sub-step.
|
|
5
6
|
*/
|
|
6
7
|
|
|
7
8
|
import { readFileSync, writeFileSync, existsSync } from 'fs';
|
|
8
9
|
import { resolve } from 'path';
|
|
9
|
-
import { execSync } from 'child_process';
|
|
10
10
|
import { log, progressBar } from '../utils/logger.js';
|
|
11
11
|
import { git } from '../utils/git.js';
|
|
12
12
|
import { github } from '../utils/github.js';
|
|
13
|
-
import { ask,
|
|
13
|
+
import { ask, closePrompt } from '../utils/prompt.js';
|
|
14
|
+
import { createCheckpoint } from '../utils/checkpoint.js';
|
|
15
|
+
import { provider } from '../utils/provider.js';
|
|
14
16
|
|
|
15
17
|
function readJSON(path) {
|
|
16
18
|
return JSON.parse(readFileSync(path, 'utf-8'));
|
|
@@ -22,14 +24,14 @@ function writeJSON(path, data) {
|
|
|
22
24
|
|
|
23
25
|
export async function run(projectDir) {
|
|
24
26
|
const tasksPath = resolve(projectDir, '.codex-copilot/tasks.json');
|
|
25
|
-
const statePath = resolve(projectDir, '.codex-copilot/state.json');
|
|
26
27
|
const configPath = resolve(projectDir, '.codex-copilot/config.json');
|
|
28
|
+
const checkpoint = createCheckpoint(projectDir);
|
|
27
29
|
|
|
28
30
|
let tasks;
|
|
29
31
|
let state;
|
|
30
32
|
try {
|
|
31
33
|
tasks = readJSON(tasksPath);
|
|
32
|
-
state =
|
|
34
|
+
state = checkpoint.load();
|
|
33
35
|
} catch (err) {
|
|
34
36
|
log.error(`Failed to read task/state files: ${err.message}`);
|
|
35
37
|
log.warn('Files may be corrupted. Run: codex-copilot reset');
|
|
@@ -47,25 +49,63 @@ export async function run(projectDir) {
|
|
|
47
49
|
}
|
|
48
50
|
|
|
49
51
|
const baseBranch = config.base_branch || 'main';
|
|
52
|
+
const providerId = config.provider || 'codex-cli';
|
|
50
53
|
const maxReviewRounds = config.max_review_rounds || 2;
|
|
51
54
|
const pollInterval = config.review_poll_interval || 60;
|
|
52
55
|
const waitTimeout = config.review_wait_timeout || 600;
|
|
53
56
|
|
|
57
|
+
const providerInfo = provider.getProvider(providerId);
|
|
58
|
+
log.info(`AI Provider: ${providerInfo ? providerInfo.name : providerId}`);
|
|
59
|
+
|
|
60
|
+
// ===== Graceful shutdown handler =====
|
|
61
|
+
let shuttingDown = false;
|
|
62
|
+
const gracefulShutdown = () => {
|
|
63
|
+
if (shuttingDown) {
|
|
64
|
+
log.warn('Force exit');
|
|
65
|
+
process.exit(1);
|
|
66
|
+
}
|
|
67
|
+
shuttingDown = true;
|
|
68
|
+
log.blank();
|
|
69
|
+
log.warn('Interrupt received — saving checkpoint...');
|
|
70
|
+
// State is already saved at each step, just need to save tasks.json
|
|
71
|
+
writeJSON(tasksPath, tasks);
|
|
72
|
+
log.info('✅ Checkpoint saved. Run `codex-copilot run` to resume.');
|
|
73
|
+
log.blank();
|
|
74
|
+
closePrompt();
|
|
75
|
+
process.exit(0);
|
|
76
|
+
};
|
|
77
|
+
process.on('SIGINT', gracefulShutdown);
|
|
78
|
+
process.on('SIGTERM', gracefulShutdown);
|
|
79
|
+
|
|
54
80
|
log.title('🚀 Starting automated development loop');
|
|
55
81
|
log.blank();
|
|
56
82
|
log.info(`Project: ${tasks.project}`);
|
|
57
83
|
log.info(`Total tasks: ${tasks.total}`);
|
|
58
|
-
log.info(`Completed: ${state.current_task}`);
|
|
59
84
|
log.info(`Base branch: ${baseBranch}`);
|
|
85
|
+
|
|
86
|
+
// ===== Pre-flight: ensure base branch is committed & pushed =====
|
|
87
|
+
await ensureBaseReady(projectDir, baseBranch);
|
|
88
|
+
|
|
89
|
+
// Show resume info if resuming mid-task
|
|
90
|
+
if (state.phase && state.current_task > 0) {
|
|
91
|
+
log.blank();
|
|
92
|
+
log.info(`⏩ Resuming task #${state.current_task} from: ${state.phase} → ${state.phase_step}`);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const completedCount = tasks.tasks.filter(t => t.status === 'completed').length;
|
|
60
96
|
log.blank();
|
|
61
|
-
progressBar(
|
|
97
|
+
progressBar(completedCount, tasks.total, `${completedCount}/${tasks.total} tasks done`);
|
|
62
98
|
log.blank();
|
|
63
99
|
|
|
64
100
|
// Execute tasks one by one
|
|
65
101
|
for (const task of tasks.tasks) {
|
|
66
|
-
|
|
102
|
+
// Skip fully completed tasks
|
|
67
103
|
if (task.status === 'completed' || task.status === 'skipped') continue;
|
|
68
104
|
|
|
105
|
+
// Skip tasks whose ID is below the completed threshold (and not the resuming task)
|
|
106
|
+
const isResumingTask = state.current_task === task.id && state.phase;
|
|
107
|
+
if (task.id < state.current_task && !isResumingTask) continue;
|
|
108
|
+
|
|
69
109
|
log.blank();
|
|
70
110
|
log.title(`━━━ Task ${task.id}/${tasks.total}: ${task.title} ━━━`);
|
|
71
111
|
log.blank();
|
|
@@ -82,151 +122,183 @@ export async function run(projectDir) {
|
|
|
82
122
|
}
|
|
83
123
|
}
|
|
84
124
|
|
|
125
|
+
// Mark task as in_progress
|
|
126
|
+
task.status = 'in_progress';
|
|
127
|
+
writeJSON(tasksPath, tasks);
|
|
128
|
+
|
|
85
129
|
// ===== Phase 1: Develop =====
|
|
86
|
-
|
|
130
|
+
if (!checkpoint.isStepDone(task.id, 'develop', 'codex_complete')) {
|
|
131
|
+
await developPhase(projectDir, task, baseBranch, checkpoint, providerId);
|
|
132
|
+
} else {
|
|
133
|
+
log.dim('⏩ Skipping develop phase (already completed)');
|
|
134
|
+
}
|
|
87
135
|
|
|
88
136
|
// ===== Phase 2: Create PR =====
|
|
89
|
-
|
|
137
|
+
let prInfo;
|
|
138
|
+
if (!checkpoint.isStepDone(task.id, 'pr', 'pr_created')) {
|
|
139
|
+
prInfo = await prPhase(projectDir, task, baseBranch, checkpoint);
|
|
140
|
+
} else {
|
|
141
|
+
// PR already created, load from state
|
|
142
|
+
state = checkpoint.load();
|
|
143
|
+
prInfo = { number: state.current_pr, url: '' };
|
|
144
|
+
log.dim(`⏩ Skipping PR phase (PR #${prInfo.number} already created)`);
|
|
145
|
+
}
|
|
90
146
|
|
|
91
147
|
// ===== Phase 3: Review loop =====
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
148
|
+
if (!checkpoint.isStepDone(task.id, 'merge', 'merged')) {
|
|
149
|
+
await reviewLoop(projectDir, task, prInfo, {
|
|
150
|
+
maxRounds: maxReviewRounds,
|
|
151
|
+
pollInterval,
|
|
152
|
+
waitTimeout,
|
|
153
|
+
}, checkpoint, providerId);
|
|
154
|
+
} else {
|
|
155
|
+
log.dim('⏩ Skipping review phase (already completed)');
|
|
156
|
+
}
|
|
97
157
|
|
|
98
158
|
// ===== Phase 4: Merge =====
|
|
99
|
-
|
|
159
|
+
if (!checkpoint.isStepDone(task.id, 'merge', 'merged')) {
|
|
160
|
+
await mergePhase(projectDir, task, prInfo, baseBranch, checkpoint);
|
|
161
|
+
} else {
|
|
162
|
+
log.dim('⏩ Skipping merge phase (already merged)');
|
|
163
|
+
}
|
|
100
164
|
|
|
101
|
-
//
|
|
165
|
+
// Mark task complete
|
|
102
166
|
task.status = 'completed';
|
|
103
|
-
state.current_task = task.id;
|
|
104
|
-
state.current_pr = null;
|
|
105
|
-
state.review_round = 0;
|
|
106
167
|
writeJSON(tasksPath, tasks);
|
|
107
|
-
|
|
168
|
+
checkpoint.completeTask(task.id);
|
|
108
169
|
|
|
109
170
|
log.blank();
|
|
110
|
-
|
|
171
|
+
const done = tasks.tasks.filter(t => t.status === 'completed').length;
|
|
172
|
+
progressBar(done, tasks.total, `${done}/${tasks.total} tasks done`);
|
|
111
173
|
log.info(`✅ Task #${task.id} complete!`);
|
|
112
174
|
}
|
|
113
175
|
|
|
114
176
|
log.blank();
|
|
115
177
|
log.title('🎉 All tasks complete!');
|
|
116
178
|
log.blank();
|
|
179
|
+
process.removeListener('SIGINT', gracefulShutdown);
|
|
180
|
+
process.removeListener('SIGTERM', gracefulShutdown);
|
|
117
181
|
closePrompt();
|
|
118
182
|
}
|
|
119
183
|
|
|
120
184
|
// ──────────────────────────────────────────────
|
|
121
185
|
// Phase 1: Develop
|
|
122
186
|
// ──────────────────────────────────────────────
|
|
123
|
-
async function developPhase(projectDir, task, baseBranch) {
|
|
187
|
+
async function developPhase(projectDir, task, baseBranch, checkpoint, providerId) {
|
|
124
188
|
log.step('Phase 1/4: Develop');
|
|
125
189
|
|
|
126
|
-
// Switch to feature branch
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
if (codexAvailable) {
|
|
137
|
-
log.info('CodeX CLI detected, ready for auto-execution...');
|
|
138
|
-
const autoRun = await confirm('Auto-invoke CodeX for development?');
|
|
139
|
-
if (autoRun) {
|
|
140
|
-
try {
|
|
141
|
-
const promptPath = resolve(projectDir, '.codex-copilot/_current_prompt.md');
|
|
142
|
-
writeFileSync(promptPath, devPrompt);
|
|
143
|
-
execSync(`cat .codex-copilot/_current_prompt.md | codex exec --full-auto -`, {
|
|
144
|
-
cwd: projectDir,
|
|
145
|
-
stdio: 'inherit',
|
|
146
|
-
});
|
|
147
|
-
log.info('CodeX development complete');
|
|
148
|
-
return;
|
|
149
|
-
} catch (err) {
|
|
150
|
-
log.warn(`CodeX CLI invocation failed: ${err.message}`);
|
|
151
|
-
log.warn('Falling back to manual mode');
|
|
152
|
-
}
|
|
190
|
+
// Step 1: Switch to feature branch
|
|
191
|
+
if (!checkpoint.isStepDone(task.id, 'develop', 'branch_created')) {
|
|
192
|
+
git.checkoutBranch(projectDir, task.branch, baseBranch);
|
|
193
|
+
log.info(`Switched to branch: ${task.branch}`);
|
|
194
|
+
checkpoint.saveStep(task.id, 'develop', 'branch_created', { branch: task.branch });
|
|
195
|
+
} else {
|
|
196
|
+
// Ensure we're on the right branch
|
|
197
|
+
const current = git.currentBranch(projectDir);
|
|
198
|
+
if (current !== task.branch) {
|
|
199
|
+
git.checkoutBranch(projectDir, task.branch, baseBranch);
|
|
153
200
|
}
|
|
201
|
+
log.dim('⏩ Branch already created');
|
|
154
202
|
}
|
|
155
203
|
|
|
156
|
-
//
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
const promptPath = resolve(projectDir, '.codex-copilot/_current_prompt.md');
|
|
167
|
-
writeFileSync(promptPath, devPrompt);
|
|
168
|
-
log.dim('Prompt saved to .codex-copilot/_current_prompt.md (you can copy the file directly)');
|
|
169
|
-
log.blank();
|
|
170
|
-
|
|
171
|
-
// Try to copy to clipboard (via stdin to avoid shell injection)
|
|
172
|
-
copyToClipboard(devPrompt);
|
|
204
|
+
// Step 2: Build development prompt
|
|
205
|
+
if (!checkpoint.isStepDone(task.id, 'develop', 'prompt_ready')) {
|
|
206
|
+
const devPrompt = buildDevPrompt(task);
|
|
207
|
+
const promptPath = resolve(projectDir, '.codex-copilot/_current_prompt.md');
|
|
208
|
+
writeFileSync(promptPath, devPrompt);
|
|
209
|
+
checkpoint.saveStep(task.id, 'develop', 'prompt_ready', { branch: task.branch });
|
|
210
|
+
log.info('Development prompt generated');
|
|
211
|
+
} else {
|
|
212
|
+
log.dim('⏩ Prompt already generated');
|
|
213
|
+
}
|
|
173
214
|
|
|
174
|
-
|
|
215
|
+
// Step 3: Execute via AI Provider
|
|
216
|
+
if (!checkpoint.isStepDone(task.id, 'develop', 'codex_complete')) {
|
|
217
|
+
const promptPath = resolve(projectDir, '.codex-copilot/_current_prompt.md');
|
|
218
|
+
const ok = await provider.executePrompt(providerId, promptPath, projectDir);
|
|
219
|
+
if (ok) {
|
|
220
|
+
log.info('Development complete');
|
|
221
|
+
checkpoint.saveStep(task.id, 'develop', 'codex_complete', { branch: task.branch });
|
|
222
|
+
}
|
|
223
|
+
}
|
|
175
224
|
}
|
|
176
225
|
|
|
177
226
|
// ──────────────────────────────────────────────
|
|
178
227
|
// Phase 2: Create PR
|
|
179
228
|
// ──────────────────────────────────────────────
|
|
180
|
-
async function prPhase(projectDir, task, baseBranch) {
|
|
229
|
+
async function prPhase(projectDir, task, baseBranch, checkpoint) {
|
|
181
230
|
log.step('Phase 2/4: Submit PR');
|
|
182
231
|
|
|
183
|
-
//
|
|
184
|
-
if (!
|
|
185
|
-
|
|
186
|
-
|
|
232
|
+
// Step 1: Commit changes
|
|
233
|
+
if (!checkpoint.isStepDone(task.id, 'pr', 'committed')) {
|
|
234
|
+
if (!git.isClean(projectDir)) {
|
|
235
|
+
log.info('Uncommitted changes detected, auto-committing...');
|
|
236
|
+
git.commitAll(projectDir, `feat(task-${task.id}): ${task.title}`);
|
|
237
|
+
}
|
|
238
|
+
checkpoint.saveStep(task.id, 'pr', 'committed', { branch: task.branch });
|
|
239
|
+
log.info('Changes committed');
|
|
240
|
+
} else {
|
|
241
|
+
log.dim('⏩ Changes already committed');
|
|
187
242
|
}
|
|
188
243
|
|
|
189
|
-
// Push
|
|
190
|
-
|
|
191
|
-
|
|
244
|
+
// Step 2: Push
|
|
245
|
+
if (!checkpoint.isStepDone(task.id, 'pr', 'pushed')) {
|
|
246
|
+
git.pushBranch(projectDir, task.branch);
|
|
247
|
+
checkpoint.saveStep(task.id, 'pr', 'pushed', { branch: task.branch });
|
|
248
|
+
log.info('Code pushed');
|
|
249
|
+
} else {
|
|
250
|
+
log.dim('⏩ Code already pushed');
|
|
251
|
+
}
|
|
192
252
|
|
|
193
|
-
// Create PR
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
253
|
+
// Step 3: Create PR
|
|
254
|
+
if (!checkpoint.isStepDone(task.id, 'pr', 'pr_created')) {
|
|
255
|
+
const acceptanceList = Array.isArray(task.acceptance) && task.acceptance.length > 0
|
|
256
|
+
? task.acceptance.map(a => `- [ ] ${a}`).join('\n')
|
|
257
|
+
: '- [ ] Feature works correctly';
|
|
258
|
+
const prBody = `## Task #${task.id}: ${task.title}\n\n${task.description}\n\n### Acceptance Criteria\n${acceptanceList}\n\n---\n*Auto-created by Codex-Copilot*`;
|
|
198
259
|
|
|
199
|
-
|
|
200
|
-
const prInfo = github.
|
|
260
|
+
// Use auto-recovery: create → find existing → fix remote → retry
|
|
261
|
+
const prInfo = github.createPRWithRecovery(projectDir, {
|
|
201
262
|
title: `feat(task-${task.id}): ${task.title}`,
|
|
202
263
|
body: prBody,
|
|
203
264
|
base: baseBranch,
|
|
204
265
|
head: task.branch,
|
|
205
266
|
});
|
|
206
|
-
log.info(`PR
|
|
267
|
+
log.info(`PR ready: #${prInfo.number} ${prInfo.url}`);
|
|
268
|
+
|
|
269
|
+
checkpoint.saveStep(task.id, 'pr', 'pr_created', {
|
|
270
|
+
branch: task.branch,
|
|
271
|
+
current_pr: prInfo.number,
|
|
272
|
+
});
|
|
207
273
|
return prInfo;
|
|
208
|
-
} catch (err) {
|
|
209
|
-
// PR may already exist
|
|
210
|
-
log.warn(`PR creation error: ${err.message}`);
|
|
211
|
-
const prNumber = await ask('Enter existing PR number:');
|
|
212
|
-
const parsed = parseInt(prNumber, 10);
|
|
213
|
-
if (isNaN(parsed) || parsed <= 0) {
|
|
214
|
-
log.error('Invalid PR number');
|
|
215
|
-
throw new Error('Invalid PR number');
|
|
216
|
-
}
|
|
217
|
-
return { number: parsed, url: '' };
|
|
218
274
|
}
|
|
275
|
+
|
|
276
|
+
// If we get here, PR was already created — load from state
|
|
277
|
+
const state = checkpoint.load();
|
|
278
|
+
return { number: state.current_pr, url: '' };
|
|
219
279
|
}
|
|
220
280
|
|
|
221
281
|
// ──────────────────────────────────────────────
|
|
222
282
|
// Phase 3: Review loop
|
|
223
283
|
// ──────────────────────────────────────────────
|
|
224
|
-
async function reviewLoop(projectDir, task, prInfo, { maxRounds: _maxRounds, pollInterval, waitTimeout }) {
|
|
284
|
+
async function reviewLoop(projectDir, task, prInfo, { maxRounds: _maxRounds, pollInterval, waitTimeout }, checkpoint, providerId) {
|
|
225
285
|
let maxRounds = _maxRounds;
|
|
226
286
|
log.step('Phase 3/4: Waiting for review');
|
|
227
287
|
|
|
228
|
-
|
|
229
|
-
|
|
288
|
+
// Resume from saved review round
|
|
289
|
+
const state = checkpoint.load();
|
|
290
|
+
const startRound = (state.current_task === task.id && state.review_round > 0)
|
|
291
|
+
? state.review_round
|
|
292
|
+
: 1;
|
|
293
|
+
|
|
294
|
+
for (let round = startRound; round <= maxRounds; round++) {
|
|
295
|
+
// Step: Waiting for review
|
|
296
|
+
checkpoint.saveStep(task.id, 'review', 'waiting_review', {
|
|
297
|
+
branch: task.branch,
|
|
298
|
+
current_pr: prInfo.number,
|
|
299
|
+
review_round: round,
|
|
300
|
+
});
|
|
301
|
+
|
|
230
302
|
log.info(`Waiting for review feedback... (timeout: ${waitTimeout}s)`);
|
|
231
303
|
const gotReview = await waitForReview(projectDir, prInfo.number, pollInterval, waitTimeout);
|
|
232
304
|
|
|
@@ -240,22 +312,63 @@ async function reviewLoop(projectDir, task, prInfo, { maxRounds: _maxRounds, pol
|
|
|
240
312
|
break;
|
|
241
313
|
}
|
|
242
314
|
|
|
315
|
+
// Step: Feedback received
|
|
316
|
+
checkpoint.saveStep(task.id, 'review', 'feedback_received', {
|
|
317
|
+
branch: task.branch,
|
|
318
|
+
current_pr: prInfo.number,
|
|
319
|
+
review_round: round,
|
|
320
|
+
});
|
|
321
|
+
|
|
243
322
|
// Check review status
|
|
244
|
-
const
|
|
245
|
-
log.info(`Review status: ${
|
|
323
|
+
const reviewState = github.getLatestReviewState(projectDir, prInfo.number);
|
|
324
|
+
log.info(`Review status: ${reviewState}`);
|
|
246
325
|
|
|
247
|
-
if (
|
|
326
|
+
if (reviewState === 'APPROVED') {
|
|
248
327
|
log.info('✅ Review approved!');
|
|
249
328
|
return;
|
|
250
329
|
}
|
|
251
330
|
|
|
252
|
-
// Collect review feedback
|
|
331
|
+
// Collect review feedback (raw — classification done by AI)
|
|
253
332
|
const feedback = github.collectReviewFeedback(projectDir, prInfo.number);
|
|
254
333
|
if (!feedback) {
|
|
255
|
-
log.info('No
|
|
334
|
+
log.info('No review feedback found — proceeding ✅');
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// Use AI to classify the review feedback
|
|
339
|
+
log.info('Classifying review feedback via AI...');
|
|
340
|
+
const classification = await provider.classifyReview(providerId, feedback, projectDir);
|
|
341
|
+
|
|
342
|
+
if (classification === 'pass') {
|
|
343
|
+
log.info('AI determined no actionable issues — proceeding ✅');
|
|
256
344
|
return;
|
|
257
345
|
}
|
|
258
346
|
|
|
347
|
+
if (classification === null) {
|
|
348
|
+
// AI classification failed — fall back to structural GitHub API signals
|
|
349
|
+
log.dim('AI classification unavailable, using structural fallback');
|
|
350
|
+
const inlineComments = github.getReviewComments(projectDir, prInfo.number);
|
|
351
|
+
const hasInlineComments = inlineComments && inlineComments.length > 0;
|
|
352
|
+
|
|
353
|
+
if (reviewState === 'CHANGES_REQUESTED') {
|
|
354
|
+
// Explicit change request — always treat as needing fixes
|
|
355
|
+
log.info('Review state: CHANGES_REQUESTED — entering fix phase');
|
|
356
|
+
} else if (reviewState === 'COMMENTED' && !hasInlineComments) {
|
|
357
|
+
// General comment with no code-level feedback — treat as passing
|
|
358
|
+
log.info('COMMENTED with no inline code comments — treating as passed ✅');
|
|
359
|
+
return;
|
|
360
|
+
} else if (!hasInlineComments) {
|
|
361
|
+
// Any other state (PENDING, null, etc.) with no inline comments — treat as passing
|
|
362
|
+
log.info('No inline code comments found — treating as passed ✅');
|
|
363
|
+
return;
|
|
364
|
+
} else {
|
|
365
|
+
// Has inline comments — treat as needing fixes
|
|
366
|
+
log.info(`Found ${inlineComments.length} inline code comment(s) — entering fix phase`);
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// AI says FIX, or structural fallback indicates issues
|
|
371
|
+
|
|
259
372
|
log.blank();
|
|
260
373
|
log.warn(`Received review feedback (round ${round}/${maxRounds})`);
|
|
261
374
|
|
|
@@ -271,18 +384,28 @@ async function reviewLoop(projectDir, task, prInfo, { maxRounds: _maxRounds, pol
|
|
|
271
384
|
}
|
|
272
385
|
}
|
|
273
386
|
|
|
274
|
-
// Let
|
|
275
|
-
await fixPhase(projectDir, task, feedback, round);
|
|
387
|
+
// Let AI fix
|
|
388
|
+
await fixPhase(projectDir, task, feedback, round, providerId);
|
|
276
389
|
|
|
277
|
-
//
|
|
390
|
+
// Step: Fix applied
|
|
391
|
+
checkpoint.saveStep(task.id, 'review', 'fix_applied', {
|
|
392
|
+
branch: task.branch,
|
|
393
|
+
current_pr: prInfo.number,
|
|
394
|
+
review_round: round,
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
// Push fix — only if there are actual changes
|
|
278
398
|
if (!git.isClean(projectDir)) {
|
|
279
399
|
git.commitAll(projectDir, `fix(task-${task.id}): address review comments (round ${round})`);
|
|
400
|
+
git.pushBranch(projectDir, task.branch);
|
|
401
|
+
log.info('Fix pushed, waiting for next review round...');
|
|
402
|
+
// Brief wait for review bot to react
|
|
403
|
+
await sleep(10000);
|
|
404
|
+
} else {
|
|
405
|
+
// No actual changes were made by the fix — the AI determined nothing needed fixing
|
|
406
|
+
log.info('No changes needed after review — proceeding to merge ✅');
|
|
407
|
+
return;
|
|
280
408
|
}
|
|
281
|
-
git.pushBranch(projectDir, task.branch);
|
|
282
|
-
log.info('Fix pushed, waiting for next review round...');
|
|
283
|
-
|
|
284
|
-
// Brief wait for review bot to react
|
|
285
|
-
await sleep(10000);
|
|
286
409
|
}
|
|
287
410
|
}
|
|
288
411
|
|
|
@@ -317,12 +440,12 @@ async function waitForReview(projectDir, prNumber, pollInterval, timeout) {
|
|
|
317
440
|
// ──────────────────────────────────────────────
|
|
318
441
|
// Fix phase
|
|
319
442
|
// ──────────────────────────────────────────────
|
|
320
|
-
async function fixPhase(projectDir, task, feedback, round) {
|
|
443
|
+
async function fixPhase(projectDir, task, feedback, round, providerId) {
|
|
321
444
|
log.step(`Fixing review comments (round ${round})`);
|
|
322
445
|
|
|
323
|
-
const fixPrompt = `
|
|
446
|
+
const fixPrompt = `Please fix the following review comments on Task #${task.id}: ${task.title}
|
|
324
447
|
|
|
325
|
-
## Review
|
|
448
|
+
## Review Feedback
|
|
326
449
|
${feedback}
|
|
327
450
|
|
|
328
451
|
## Requirements
|
|
@@ -332,50 +455,21 @@ ${feedback}
|
|
|
332
455
|
4. When done, run: git add -A && git commit -m "fix(task-${task.id}): address review round ${round}"
|
|
333
456
|
`;
|
|
334
457
|
|
|
335
|
-
// Save to file and
|
|
458
|
+
// Save to file and execute via provider
|
|
336
459
|
const promptPath = resolve(projectDir, '.codex-copilot/_current_prompt.md');
|
|
337
460
|
writeFileSync(promptPath, fixPrompt);
|
|
338
461
|
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
if (codexAvailable) {
|
|
343
|
-
const autoFix = await confirm('Auto-invoke CodeX to fix?');
|
|
344
|
-
if (autoFix) {
|
|
345
|
-
try {
|
|
346
|
-
execSync(`cat .codex-copilot/_current_prompt.md | codex exec --full-auto -`, {
|
|
347
|
-
cwd: projectDir,
|
|
348
|
-
stdio: 'inherit',
|
|
349
|
-
});
|
|
350
|
-
log.info('CodeX fix complete');
|
|
351
|
-
return;
|
|
352
|
-
} catch {
|
|
353
|
-
log.warn('CodeX CLI invocation failed, falling back to manual mode');
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
// Manual mode
|
|
359
|
-
log.blank();
|
|
360
|
-
log.dim('Review fix prompt saved to .codex-copilot/_current_prompt.md');
|
|
361
|
-
log.dim('Paste the file content into CodeX to execute');
|
|
362
|
-
|
|
363
|
-
copyToClipboard(fixPrompt);
|
|
364
|
-
|
|
365
|
-
await ask('Press Enter after CodeX fix is complete...');
|
|
462
|
+
await provider.executePrompt(providerId, promptPath, projectDir);
|
|
463
|
+
log.info('Fix complete');
|
|
366
464
|
}
|
|
367
465
|
|
|
368
466
|
// ──────────────────────────────────────────────
|
|
369
467
|
// Phase 4: Merge
|
|
370
468
|
// ──────────────────────────────────────────────
|
|
371
|
-
async function mergePhase(projectDir, task, prInfo, baseBranch) {
|
|
469
|
+
async function mergePhase(projectDir, task, prInfo, baseBranch, checkpoint) {
|
|
372
470
|
log.step('Phase 4/4: Merge PR');
|
|
373
471
|
|
|
374
|
-
|
|
375
|
-
if (!doMerge) {
|
|
376
|
-
log.warn('User skipped merge');
|
|
377
|
-
return;
|
|
378
|
-
}
|
|
472
|
+
log.info(`Auto-merging PR #${prInfo.number}...`);
|
|
379
473
|
|
|
380
474
|
try {
|
|
381
475
|
github.mergePR(projectDir, prInfo.number);
|
|
@@ -386,10 +480,58 @@ async function mergePhase(projectDir, task, prInfo, baseBranch) {
|
|
|
386
480
|
await ask('Continue...');
|
|
387
481
|
}
|
|
388
482
|
|
|
483
|
+
checkpoint.saveStep(task.id, 'merge', 'merged', {
|
|
484
|
+
branch: task.branch,
|
|
485
|
+
current_pr: prInfo.number,
|
|
486
|
+
});
|
|
487
|
+
|
|
389
488
|
// Switch back to main branch
|
|
390
489
|
git.checkoutMain(projectDir, baseBranch);
|
|
391
490
|
}
|
|
392
491
|
|
|
492
|
+
// ──────────────────────────────────────────────
|
|
493
|
+
// Pre-flight: ensure base branch has commits & is pushed
|
|
494
|
+
// ──────────────────────────────────────────────
|
|
495
|
+
async function ensureBaseReady(projectDir, baseBranch) {
|
|
496
|
+
// Check if the repo has any commits at all
|
|
497
|
+
const hasCommits = git.execSafe('git rev-parse HEAD', projectDir);
|
|
498
|
+
if (!hasCommits.ok) {
|
|
499
|
+
// No commits yet — brand new repo
|
|
500
|
+
log.warn('No commits found in repository');
|
|
501
|
+
if (!git.isClean(projectDir)) {
|
|
502
|
+
log.info('Creating initial commit from existing code...');
|
|
503
|
+
git.commitAll(projectDir, 'chore: initial commit');
|
|
504
|
+
log.info('✅ Initial commit created');
|
|
505
|
+
} else {
|
|
506
|
+
log.warn('Repository is empty — no files to commit');
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
} else {
|
|
510
|
+
// Has commits, but base branch might have uncommitted changes
|
|
511
|
+
const currentBranch = git.currentBranch(projectDir);
|
|
512
|
+
if (currentBranch === baseBranch && !git.isClean(projectDir)) {
|
|
513
|
+
log.info('Uncommitted changes on base branch, committing first...');
|
|
514
|
+
git.commitAll(projectDir, 'chore: save current progress before automation');
|
|
515
|
+
log.info('✅ Base branch changes committed');
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// Ensure we're on the base branch
|
|
520
|
+
const currentBranch = git.currentBranch(projectDir);
|
|
521
|
+
if (currentBranch !== baseBranch) {
|
|
522
|
+
// If base branch doesn't exist locally, create it from current
|
|
523
|
+
const branchExists = git.execSafe(`git rev-parse --verify ${baseBranch}`, projectDir);
|
|
524
|
+
if (!branchExists.ok) {
|
|
525
|
+
log.info(`Creating base branch '${baseBranch}' from current branch...`);
|
|
526
|
+
git.execSafe(`git branch ${baseBranch}`, projectDir);
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
// Ensure base branch is pushed to remote
|
|
531
|
+
github.ensureRemoteBranch(projectDir, baseBranch);
|
|
532
|
+
log.info(`Base branch '${baseBranch}' ready ✓`);
|
|
533
|
+
}
|
|
534
|
+
|
|
393
535
|
function buildDevPrompt(task) {
|
|
394
536
|
const acceptanceList = Array.isArray(task.acceptance) && task.acceptance.length > 0
|
|
395
537
|
? task.acceptance.map(a => `- ${a}`).join('\n')
|
|
@@ -416,19 +558,5 @@ function sleep(ms) {
|
|
|
416
558
|
return new Promise(resolve => setTimeout(resolve, ms));
|
|
417
559
|
}
|
|
418
560
|
|
|
419
|
-
function isCodexAvailable() {
|
|
420
|
-
try {
|
|
421
|
-
const cmd = process.platform === 'win32' ? 'where codex' : 'which codex';
|
|
422
|
-
execSync(cmd, { stdio: 'pipe' });
|
|
423
|
-
return true;
|
|
424
|
-
} catch {
|
|
425
|
-
return false;
|
|
426
|
-
}
|
|
427
|
-
}
|
|
428
561
|
|
|
429
|
-
|
|
430
|
-
try {
|
|
431
|
-
execSync('pbcopy', { input: text, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
432
|
-
log.info('📋 Copied to clipboard');
|
|
433
|
-
} catch {}
|
|
434
|
-
}
|
|
562
|
+
|