@jojonax/codex-copilot 1.0.3 → 1.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/package.json +1 -1
- package/src/commands/init.js +44 -36
- package/src/commands/reset.js +16 -8
- package/src/commands/run.js +242 -147
- 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 +139 -3
- package/src/utils/provider.js +332 -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
13
|
import { ask, confirm, 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,11 +312,18 @@ 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
|
}
|
|
@@ -271,8 +350,15 @@ async function reviewLoop(projectDir, task, prInfo, { maxRounds: _maxRounds, pol
|
|
|
271
350
|
}
|
|
272
351
|
}
|
|
273
352
|
|
|
274
|
-
// Let
|
|
275
|
-
await fixPhase(projectDir, task, feedback, round);
|
|
353
|
+
// Let AI fix
|
|
354
|
+
await fixPhase(projectDir, task, feedback, round, providerId);
|
|
355
|
+
|
|
356
|
+
// Step: Fix applied
|
|
357
|
+
checkpoint.saveStep(task.id, 'review', 'fix_applied', {
|
|
358
|
+
branch: task.branch,
|
|
359
|
+
current_pr: prInfo.number,
|
|
360
|
+
review_round: round,
|
|
361
|
+
});
|
|
276
362
|
|
|
277
363
|
// Push fix
|
|
278
364
|
if (!git.isClean(projectDir)) {
|
|
@@ -317,12 +403,12 @@ async function waitForReview(projectDir, prNumber, pollInterval, timeout) {
|
|
|
317
403
|
// ──────────────────────────────────────────────
|
|
318
404
|
// Fix phase
|
|
319
405
|
// ──────────────────────────────────────────────
|
|
320
|
-
async function fixPhase(projectDir, task, feedback, round) {
|
|
406
|
+
async function fixPhase(projectDir, task, feedback, round, providerId) {
|
|
321
407
|
log.step(`Fixing review comments (round ${round})`);
|
|
322
408
|
|
|
323
|
-
const fixPrompt = `
|
|
409
|
+
const fixPrompt = `Please fix the following review comments on Task #${task.id}: ${task.title}
|
|
324
410
|
|
|
325
|
-
## Review
|
|
411
|
+
## Review Feedback
|
|
326
412
|
${feedback}
|
|
327
413
|
|
|
328
414
|
## Requirements
|
|
@@ -332,43 +418,18 @@ ${feedback}
|
|
|
332
418
|
4. When done, run: git add -A && git commit -m "fix(task-${task.id}): address review round ${round}"
|
|
333
419
|
`;
|
|
334
420
|
|
|
335
|
-
// Save to file and
|
|
421
|
+
// Save to file and execute via provider
|
|
336
422
|
const promptPath = resolve(projectDir, '.codex-copilot/_current_prompt.md');
|
|
337
423
|
writeFileSync(promptPath, fixPrompt);
|
|
338
424
|
|
|
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...');
|
|
425
|
+
await provider.executePrompt(providerId, promptPath, projectDir);
|
|
426
|
+
log.info('Fix complete');
|
|
366
427
|
}
|
|
367
428
|
|
|
368
429
|
// ──────────────────────────────────────────────
|
|
369
430
|
// Phase 4: Merge
|
|
370
431
|
// ──────────────────────────────────────────────
|
|
371
|
-
async function mergePhase(projectDir, task, prInfo, baseBranch) {
|
|
432
|
+
async function mergePhase(projectDir, task, prInfo, baseBranch, checkpoint) {
|
|
372
433
|
log.step('Phase 4/4: Merge PR');
|
|
373
434
|
|
|
374
435
|
const doMerge = await confirm(`Merge PR #${prInfo.number}?`);
|
|
@@ -386,10 +447,58 @@ async function mergePhase(projectDir, task, prInfo, baseBranch) {
|
|
|
386
447
|
await ask('Continue...');
|
|
387
448
|
}
|
|
388
449
|
|
|
450
|
+
checkpoint.saveStep(task.id, 'merge', 'merged', {
|
|
451
|
+
branch: task.branch,
|
|
452
|
+
current_pr: prInfo.number,
|
|
453
|
+
});
|
|
454
|
+
|
|
389
455
|
// Switch back to main branch
|
|
390
456
|
git.checkoutMain(projectDir, baseBranch);
|
|
391
457
|
}
|
|
392
458
|
|
|
459
|
+
// ──────────────────────────────────────────────
|
|
460
|
+
// Pre-flight: ensure base branch has commits & is pushed
|
|
461
|
+
// ──────────────────────────────────────────────
|
|
462
|
+
async function ensureBaseReady(projectDir, baseBranch) {
|
|
463
|
+
// Check if the repo has any commits at all
|
|
464
|
+
const hasCommits = git.execSafe('git rev-parse HEAD', projectDir);
|
|
465
|
+
if (!hasCommits.ok) {
|
|
466
|
+
// No commits yet — brand new repo
|
|
467
|
+
log.warn('No commits found in repository');
|
|
468
|
+
if (!git.isClean(projectDir)) {
|
|
469
|
+
log.info('Creating initial commit from existing code...');
|
|
470
|
+
git.commitAll(projectDir, 'chore: initial commit');
|
|
471
|
+
log.info('✅ Initial commit created');
|
|
472
|
+
} else {
|
|
473
|
+
log.warn('Repository is empty — no files to commit');
|
|
474
|
+
return;
|
|
475
|
+
}
|
|
476
|
+
} else {
|
|
477
|
+
// Has commits, but base branch might have uncommitted changes
|
|
478
|
+
const currentBranch = git.currentBranch(projectDir);
|
|
479
|
+
if (currentBranch === baseBranch && !git.isClean(projectDir)) {
|
|
480
|
+
log.info('Uncommitted changes on base branch, committing first...');
|
|
481
|
+
git.commitAll(projectDir, 'chore: save current progress before automation');
|
|
482
|
+
log.info('✅ Base branch changes committed');
|
|
483
|
+
}
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// Ensure we're on the base branch
|
|
487
|
+
const currentBranch = git.currentBranch(projectDir);
|
|
488
|
+
if (currentBranch !== baseBranch) {
|
|
489
|
+
// If base branch doesn't exist locally, create it from current
|
|
490
|
+
const branchExists = git.execSafe(`git rev-parse --verify ${baseBranch}`, projectDir);
|
|
491
|
+
if (!branchExists.ok) {
|
|
492
|
+
log.info(`Creating base branch '${baseBranch}' from current branch...`);
|
|
493
|
+
git.execSafe(`git branch ${baseBranch}`, projectDir);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
// Ensure base branch is pushed to remote
|
|
498
|
+
github.ensureRemoteBranch(projectDir, baseBranch);
|
|
499
|
+
log.info(`Base branch '${baseBranch}' ready ✓`);
|
|
500
|
+
}
|
|
501
|
+
|
|
393
502
|
function buildDevPrompt(task) {
|
|
394
503
|
const acceptanceList = Array.isArray(task.acceptance) && task.acceptance.length > 0
|
|
395
504
|
? task.acceptance.map(a => `- ${a}`).join('\n')
|
|
@@ -416,19 +525,5 @@ function sleep(ms) {
|
|
|
416
525
|
return new Promise(resolve => setTimeout(resolve, ms));
|
|
417
526
|
}
|
|
418
527
|
|
|
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
528
|
|
|
429
|
-
|
|
430
|
-
try {
|
|
431
|
-
execSync('pbcopy', { input: text, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
432
|
-
log.info('📋 Copied to clipboard');
|
|
433
|
-
} catch {}
|
|
434
|
-
}
|
|
529
|
+
|
package/src/commands/status.js
CHANGED
|
@@ -2,18 +2,19 @@
|
|
|
2
2
|
* codex-copilot status - Show current progress
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import { readFileSync } from 'fs';
|
|
5
|
+
import { readFileSync, existsSync } from 'fs';
|
|
6
6
|
import { resolve } from 'path';
|
|
7
7
|
import { log, progressBar } from '../utils/logger.js';
|
|
8
|
+
import { createCheckpoint } from '../utils/checkpoint.js';
|
|
8
9
|
|
|
9
10
|
export async function status(projectDir) {
|
|
10
11
|
const tasksPath = resolve(projectDir, '.codex-copilot/tasks.json');
|
|
11
|
-
const
|
|
12
|
+
const checkpoint = createCheckpoint(projectDir);
|
|
12
13
|
|
|
13
14
|
let tasks, state;
|
|
14
15
|
try {
|
|
15
16
|
tasks = JSON.parse(readFileSync(tasksPath, 'utf-8'));
|
|
16
|
-
state =
|
|
17
|
+
state = checkpoint.load();
|
|
17
18
|
} catch (err) {
|
|
18
19
|
log.error(`Failed to read files: ${err.message}`);
|
|
19
20
|
log.warn('Files may be corrupted. Run: codex-copilot reset');
|
|
@@ -39,9 +40,22 @@ export async function status(projectDir) {
|
|
|
39
40
|
if (skipped > 0) log.warn(`⏭ Skipped: ${skipped}`);
|
|
40
41
|
log.blank();
|
|
41
42
|
|
|
42
|
-
// Current state
|
|
43
|
-
if (state.
|
|
44
|
-
|
|
43
|
+
// Current checkpoint state
|
|
44
|
+
if (state.phase) {
|
|
45
|
+
const phaseLabels = {
|
|
46
|
+
develop: '🔨 Develop',
|
|
47
|
+
pr: '📦 PR',
|
|
48
|
+
review: '👀 Review',
|
|
49
|
+
merge: '🔀 Merge',
|
|
50
|
+
};
|
|
51
|
+
const phaseLabel = phaseLabels[state.phase] || state.phase;
|
|
52
|
+
log.info(`📍 Checkpoint: Task #${state.current_task} — ${phaseLabel} → ${state.phase_step}`);
|
|
53
|
+
if (state.branch) log.dim(` Branch: ${state.branch}`);
|
|
54
|
+
if (state.current_pr) log.dim(` PR: #${state.current_pr}`);
|
|
55
|
+
if (state.review_round > 0) log.dim(` Review round: ${state.review_round}`);
|
|
56
|
+
if (state.last_updated) log.dim(` Last saved: ${state.last_updated}`);
|
|
57
|
+
} else if (state.current_task > 0) {
|
|
58
|
+
log.info(`Last completed task: #${state.current_task}`);
|
|
45
59
|
}
|
|
46
60
|
log.blank();
|
|
47
61
|
|
|
@@ -49,11 +63,14 @@ export async function status(projectDir) {
|
|
|
49
63
|
log.title('Task list:');
|
|
50
64
|
log.blank();
|
|
51
65
|
for (const task of tasks.tasks) {
|
|
66
|
+
const isCurrentTask = state.current_task === task.id && state.phase;
|
|
52
67
|
const icon = task.status === 'completed' ? '✅' :
|
|
68
|
+
isCurrentTask ? '🔄' :
|
|
53
69
|
task.status === 'in_progress' ? '🔄' :
|
|
54
70
|
task.status === 'developed' ? '📦' :
|
|
55
71
|
task.status === 'skipped' ? '⏭ ' : '⬜';
|
|
56
|
-
|
|
72
|
+
const suffix = isCurrentTask ? ` ← ${state.phase}:${state.phase_step}` : '';
|
|
73
|
+
console.log(` ${icon} #${task.id} ${task.title} [${task.branch}]${suffix}`);
|
|
57
74
|
}
|
|
58
75
|
log.blank();
|
|
59
76
|
}
|