@jojonax/codex-copilot 1.0.1 → 1.0.3
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 +33 -22
- package/package.json +1 -1
- package/src/commands/init.js +89 -86
- package/src/commands/reset.js +10 -10
- package/src/commands/run.js +145 -118
- package/src/commands/status.js +22 -15
- package/src/utils/detect-prd.js +20 -20
- package/src/utils/git.js +26 -13
- package/src/utils/github.js +41 -22
- package/src/utils/logger.js +19 -13
- package/src/utils/prompt.js +7 -7
- package/src/utils/update-check.js +103 -0
package/src/commands/run.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* codex-copilot run -
|
|
2
|
+
* codex-copilot run - Main orchestration loop
|
|
3
3
|
*
|
|
4
|
-
*
|
|
4
|
+
* Executes tasks one by one: Develop → PR → Review → Fix → Merge → Next
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
import { readFileSync, writeFileSync, existsSync } from 'fs';
|
|
@@ -25,63 +25,80 @@ export async function run(projectDir) {
|
|
|
25
25
|
const statePath = resolve(projectDir, '.codex-copilot/state.json');
|
|
26
26
|
const configPath = resolve(projectDir, '.codex-copilot/config.json');
|
|
27
27
|
|
|
28
|
-
|
|
29
|
-
let state
|
|
28
|
+
let tasks;
|
|
29
|
+
let state;
|
|
30
|
+
try {
|
|
31
|
+
tasks = readJSON(tasksPath);
|
|
32
|
+
state = readJSON(statePath);
|
|
33
|
+
} catch (err) {
|
|
34
|
+
log.error(`Failed to read task/state files: ${err.message}`);
|
|
35
|
+
log.warn('Files may be corrupted. Run: codex-copilot reset');
|
|
36
|
+
closePrompt();
|
|
37
|
+
process.exit(1);
|
|
38
|
+
}
|
|
30
39
|
const config = existsSync(configPath) ? readJSON(configPath) : {};
|
|
31
40
|
|
|
41
|
+
// Validate tasks.json structure
|
|
42
|
+
if (!tasks.tasks || !Array.isArray(tasks.tasks) || !tasks.total) {
|
|
43
|
+
log.error('Invalid tasks.json format: missing tasks array or total field');
|
|
44
|
+
log.warn('Please re-run codex-copilot init and let CodeX regenerate tasks.json');
|
|
45
|
+
closePrompt();
|
|
46
|
+
process.exit(1);
|
|
47
|
+
}
|
|
48
|
+
|
|
32
49
|
const baseBranch = config.base_branch || 'main';
|
|
33
50
|
const maxReviewRounds = config.max_review_rounds || 2;
|
|
34
51
|
const pollInterval = config.review_poll_interval || 60;
|
|
35
52
|
const waitTimeout = config.review_wait_timeout || 600;
|
|
36
53
|
|
|
37
|
-
log.title('🚀
|
|
54
|
+
log.title('🚀 Starting automated development loop');
|
|
38
55
|
log.blank();
|
|
39
|
-
log.info(
|
|
40
|
-
log.info(
|
|
41
|
-
log.info(
|
|
42
|
-
log.info(
|
|
56
|
+
log.info(`Project: ${tasks.project}`);
|
|
57
|
+
log.info(`Total tasks: ${tasks.total}`);
|
|
58
|
+
log.info(`Completed: ${state.current_task}`);
|
|
59
|
+
log.info(`Base branch: ${baseBranch}`);
|
|
43
60
|
log.blank();
|
|
44
|
-
progressBar(state.current_task, tasks.total, `${state.current_task}/${tasks.total}
|
|
61
|
+
progressBar(state.current_task, tasks.total, `${state.current_task}/${tasks.total} tasks done`);
|
|
45
62
|
log.blank();
|
|
46
63
|
|
|
47
|
-
//
|
|
64
|
+
// Execute tasks one by one
|
|
48
65
|
for (const task of tasks.tasks) {
|
|
49
|
-
if (task.id <= state.current_task) continue; //
|
|
66
|
+
if (task.id <= state.current_task) continue; // Skip completed
|
|
50
67
|
if (task.status === 'completed' || task.status === 'skipped') continue;
|
|
51
68
|
|
|
52
69
|
log.blank();
|
|
53
|
-
log.title(`━━━
|
|
70
|
+
log.title(`━━━ Task ${task.id}/${tasks.total}: ${task.title} ━━━`);
|
|
54
71
|
log.blank();
|
|
55
72
|
|
|
56
|
-
//
|
|
73
|
+
// Check dependencies
|
|
57
74
|
if (task.depends_on && task.depends_on.length > 0) {
|
|
58
75
|
const unfinished = task.depends_on.filter(dep => {
|
|
59
76
|
const depTask = tasks.tasks.find(t => t.id === dep);
|
|
60
77
|
return depTask && depTask.status !== 'completed';
|
|
61
78
|
});
|
|
62
79
|
if (unfinished.length > 0) {
|
|
63
|
-
log.warn(
|
|
80
|
+
log.warn(`Unfinished dependencies: ${unfinished.join(', ')} — skipping`);
|
|
64
81
|
continue;
|
|
65
82
|
}
|
|
66
83
|
}
|
|
67
84
|
|
|
68
|
-
// =====
|
|
85
|
+
// ===== Phase 1: Develop =====
|
|
69
86
|
await developPhase(projectDir, task, baseBranch);
|
|
70
87
|
|
|
71
|
-
// =====
|
|
88
|
+
// ===== Phase 2: Create PR =====
|
|
72
89
|
const prInfo = await prPhase(projectDir, task, baseBranch);
|
|
73
90
|
|
|
74
|
-
// =====
|
|
91
|
+
// ===== Phase 3: Review loop =====
|
|
75
92
|
await reviewLoop(projectDir, task, prInfo, {
|
|
76
93
|
maxRounds: maxReviewRounds,
|
|
77
94
|
pollInterval,
|
|
78
95
|
waitTimeout,
|
|
79
96
|
});
|
|
80
97
|
|
|
81
|
-
// =====
|
|
82
|
-
await mergePhase(projectDir, task, prInfo);
|
|
98
|
+
// ===== Phase 4: Merge =====
|
|
99
|
+
await mergePhase(projectDir, task, prInfo, baseBranch);
|
|
83
100
|
|
|
84
|
-
//
|
|
101
|
+
// Update task state
|
|
85
102
|
task.status = 'completed';
|
|
86
103
|
state.current_task = task.id;
|
|
87
104
|
state.current_pr = null;
|
|
@@ -90,91 +107,94 @@ export async function run(projectDir) {
|
|
|
90
107
|
writeJSON(statePath, state);
|
|
91
108
|
|
|
92
109
|
log.blank();
|
|
93
|
-
progressBar(task.id, tasks.total, `${task.id}/${tasks.total}
|
|
94
|
-
log.info(`✅
|
|
110
|
+
progressBar(task.id, tasks.total, `${task.id}/${tasks.total} tasks done`);
|
|
111
|
+
log.info(`✅ Task #${task.id} complete!`);
|
|
95
112
|
}
|
|
96
113
|
|
|
97
114
|
log.blank();
|
|
98
|
-
log.title('🎉
|
|
115
|
+
log.title('🎉 All tasks complete!');
|
|
99
116
|
log.blank();
|
|
100
117
|
closePrompt();
|
|
101
118
|
}
|
|
102
119
|
|
|
103
120
|
// ──────────────────────────────────────────────
|
|
104
|
-
//
|
|
121
|
+
// Phase 1: Develop
|
|
105
122
|
// ──────────────────────────────────────────────
|
|
106
123
|
async function developPhase(projectDir, task, baseBranch) {
|
|
107
|
-
log.step('
|
|
124
|
+
log.step('Phase 1/4: Develop');
|
|
108
125
|
|
|
109
|
-
//
|
|
126
|
+
// Switch to feature branch
|
|
110
127
|
git.checkoutBranch(projectDir, task.branch, baseBranch);
|
|
111
|
-
log.info(
|
|
128
|
+
log.info(`Switched to branch: ${task.branch}`);
|
|
112
129
|
|
|
113
|
-
//
|
|
130
|
+
// Build development prompt
|
|
114
131
|
const devPrompt = buildDevPrompt(task);
|
|
115
132
|
|
|
116
|
-
//
|
|
133
|
+
// Check if CodeX CLI is available
|
|
117
134
|
const codexAvailable = isCodexAvailable();
|
|
118
135
|
|
|
119
136
|
if (codexAvailable) {
|
|
120
|
-
log.info('
|
|
121
|
-
const autoRun = await confirm('
|
|
137
|
+
log.info('CodeX CLI detected, ready for auto-execution...');
|
|
138
|
+
const autoRun = await confirm('Auto-invoke CodeX for development?');
|
|
122
139
|
if (autoRun) {
|
|
123
140
|
try {
|
|
124
141
|
const promptPath = resolve(projectDir, '.codex-copilot/_current_prompt.md');
|
|
125
142
|
writeFileSync(promptPath, devPrompt);
|
|
126
|
-
execSync(`
|
|
143
|
+
execSync(`cat .codex-copilot/_current_prompt.md | codex exec --full-auto -`, {
|
|
127
144
|
cwd: projectDir,
|
|
128
145
|
stdio: 'inherit',
|
|
129
146
|
});
|
|
130
|
-
log.info('CodeX
|
|
147
|
+
log.info('CodeX development complete');
|
|
131
148
|
return;
|
|
132
149
|
} catch (err) {
|
|
133
|
-
log.warn(`CodeX CLI
|
|
134
|
-
log.warn('
|
|
150
|
+
log.warn(`CodeX CLI invocation failed: ${err.message}`);
|
|
151
|
+
log.warn('Falling back to manual mode');
|
|
135
152
|
}
|
|
136
153
|
}
|
|
137
154
|
}
|
|
138
155
|
|
|
139
|
-
//
|
|
156
|
+
// Manual mode: display prompt, wait for user confirmation
|
|
140
157
|
log.blank();
|
|
141
|
-
console.log(' ┌───
|
|
158
|
+
console.log(' ┌─── Paste the following into CodeX Desktop to execute ───┐');
|
|
142
159
|
log.blank();
|
|
143
160
|
console.log(devPrompt.split('\n').map(l => ` │ ${l}`).join('\n'));
|
|
144
161
|
log.blank();
|
|
145
|
-
console.log('
|
|
162
|
+
console.log(' └─────────────────────────────────────────────────────────┘');
|
|
146
163
|
log.blank();
|
|
147
164
|
|
|
148
|
-
//
|
|
165
|
+
// Also save prompt to file for easy copying
|
|
149
166
|
const promptPath = resolve(projectDir, '.codex-copilot/_current_prompt.md');
|
|
150
167
|
writeFileSync(promptPath, devPrompt);
|
|
151
|
-
log.dim(
|
|
168
|
+
log.dim('Prompt saved to .codex-copilot/_current_prompt.md (you can copy the file directly)');
|
|
152
169
|
log.blank();
|
|
153
170
|
|
|
154
|
-
//
|
|
171
|
+
// Try to copy to clipboard (via stdin to avoid shell injection)
|
|
155
172
|
copyToClipboard(devPrompt);
|
|
156
173
|
|
|
157
|
-
await ask('CodeX
|
|
174
|
+
await ask('Press Enter after CodeX development is complete...');
|
|
158
175
|
}
|
|
159
176
|
|
|
160
177
|
// ──────────────────────────────────────────────
|
|
161
|
-
//
|
|
178
|
+
// Phase 2: Create PR
|
|
162
179
|
// ──────────────────────────────────────────────
|
|
163
180
|
async function prPhase(projectDir, task, baseBranch) {
|
|
164
|
-
log.step('
|
|
181
|
+
log.step('Phase 2/4: Submit PR');
|
|
165
182
|
|
|
166
|
-
//
|
|
183
|
+
// Ensure changes are committed
|
|
167
184
|
if (!git.isClean(projectDir)) {
|
|
168
|
-
log.info('
|
|
185
|
+
log.info('Uncommitted changes detected, auto-committing...');
|
|
169
186
|
git.commitAll(projectDir, `feat(task-${task.id}): ${task.title}`);
|
|
170
187
|
}
|
|
171
188
|
|
|
172
|
-
//
|
|
189
|
+
// Push
|
|
173
190
|
git.pushBranch(projectDir, task.branch);
|
|
174
|
-
log.info('
|
|
191
|
+
log.info('Code pushed');
|
|
175
192
|
|
|
176
|
-
//
|
|
177
|
-
const
|
|
193
|
+
// Create PR
|
|
194
|
+
const acceptanceList = Array.isArray(task.acceptance) && task.acceptance.length > 0
|
|
195
|
+
? task.acceptance.map(a => `- [ ] ${a}`).join('\n')
|
|
196
|
+
: '- [ ] Feature works correctly';
|
|
197
|
+
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*`;
|
|
178
198
|
|
|
179
199
|
try {
|
|
180
200
|
const prInfo = github.createPR(projectDir, {
|
|
@@ -183,31 +203,36 @@ async function prPhase(projectDir, task, baseBranch) {
|
|
|
183
203
|
base: baseBranch,
|
|
184
204
|
head: task.branch,
|
|
185
205
|
});
|
|
186
|
-
log.info(`PR
|
|
206
|
+
log.info(`PR created: ${prInfo.url}`);
|
|
187
207
|
return prInfo;
|
|
188
208
|
} catch (err) {
|
|
189
|
-
// PR
|
|
190
|
-
log.warn(
|
|
191
|
-
const prNumber = await ask('
|
|
192
|
-
|
|
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: '' };
|
|
193
218
|
}
|
|
194
219
|
}
|
|
195
220
|
|
|
196
221
|
// ──────────────────────────────────────────────
|
|
197
|
-
//
|
|
222
|
+
// Phase 3: Review loop
|
|
198
223
|
// ──────────────────────────────────────────────
|
|
199
224
|
async function reviewLoop(projectDir, task, prInfo, { maxRounds: _maxRounds, pollInterval, waitTimeout }) {
|
|
200
225
|
let maxRounds = _maxRounds;
|
|
201
|
-
log.step('
|
|
226
|
+
log.step('Phase 3/4: Waiting for review');
|
|
202
227
|
|
|
203
228
|
for (let round = 1; round <= maxRounds; round++) {
|
|
204
|
-
//
|
|
205
|
-
log.info(
|
|
229
|
+
// Wait for review
|
|
230
|
+
log.info(`Waiting for review feedback... (timeout: ${waitTimeout}s)`);
|
|
206
231
|
const gotReview = await waitForReview(projectDir, prInfo.number, pollInterval, waitTimeout);
|
|
207
232
|
|
|
208
233
|
if (!gotReview) {
|
|
209
|
-
log.warn('
|
|
210
|
-
const action = await ask('
|
|
234
|
+
log.warn('Review wait timed out');
|
|
235
|
+
const action = await ask('Enter "skip" (skip review) or "wait" (keep waiting):');
|
|
211
236
|
if (action === 'wait') {
|
|
212
237
|
round--;
|
|
213
238
|
continue;
|
|
@@ -215,28 +240,28 @@ async function reviewLoop(projectDir, task, prInfo, { maxRounds: _maxRounds, pol
|
|
|
215
240
|
break;
|
|
216
241
|
}
|
|
217
242
|
|
|
218
|
-
//
|
|
243
|
+
// Check review status
|
|
219
244
|
const state = github.getLatestReviewState(projectDir, prInfo.number);
|
|
220
|
-
log.info(`Review
|
|
245
|
+
log.info(`Review status: ${state}`);
|
|
221
246
|
|
|
222
247
|
if (state === 'APPROVED') {
|
|
223
|
-
log.info('✅ Review
|
|
248
|
+
log.info('✅ Review approved!');
|
|
224
249
|
return;
|
|
225
250
|
}
|
|
226
251
|
|
|
227
|
-
//
|
|
252
|
+
// Collect review feedback
|
|
228
253
|
const feedback = github.collectReviewFeedback(projectDir, prInfo.number);
|
|
229
254
|
if (!feedback) {
|
|
230
|
-
log.info('
|
|
255
|
+
log.info('No specific change requests found, proceeding');
|
|
231
256
|
return;
|
|
232
257
|
}
|
|
233
258
|
|
|
234
259
|
log.blank();
|
|
235
|
-
log.warn(
|
|
260
|
+
log.warn(`Received review feedback (round ${round}/${maxRounds})`);
|
|
236
261
|
|
|
237
262
|
if (round >= maxRounds) {
|
|
238
|
-
log.warn(
|
|
239
|
-
const choice = await ask('
|
|
263
|
+
log.warn(`Max fix rounds reached (${maxRounds})`);
|
|
264
|
+
const choice = await ask('Enter "merge" (force merge) / "fix" (one more round) / "skip" (skip):');
|
|
240
265
|
if (choice === 'fix') {
|
|
241
266
|
maxRounds++;
|
|
242
267
|
} else if (choice === 'skip') {
|
|
@@ -246,17 +271,17 @@ async function reviewLoop(projectDir, task, prInfo, { maxRounds: _maxRounds, pol
|
|
|
246
271
|
}
|
|
247
272
|
}
|
|
248
273
|
|
|
249
|
-
//
|
|
274
|
+
// Let CodeX fix
|
|
250
275
|
await fixPhase(projectDir, task, feedback, round);
|
|
251
276
|
|
|
252
|
-
//
|
|
277
|
+
// Push fix
|
|
253
278
|
if (!git.isClean(projectDir)) {
|
|
254
279
|
git.commitAll(projectDir, `fix(task-${task.id}): address review comments (round ${round})`);
|
|
255
280
|
}
|
|
256
281
|
git.pushBranch(projectDir, task.branch);
|
|
257
|
-
log.info('
|
|
282
|
+
log.info('Fix pushed, waiting for next review round...');
|
|
258
283
|
|
|
259
|
-
//
|
|
284
|
+
// Brief wait for review bot to react
|
|
260
285
|
await sleep(10000);
|
|
261
286
|
}
|
|
262
287
|
}
|
|
@@ -272,11 +297,11 @@ async function waitForReview(projectDir, prNumber, pollInterval, timeout) {
|
|
|
272
297
|
const currentReviews = github.getReviews(projectDir, prNumber);
|
|
273
298
|
const currentComments = github.getIssueComments(projectDir, prNumber);
|
|
274
299
|
|
|
275
|
-
//
|
|
300
|
+
// Check for new reviews or bot comments
|
|
276
301
|
const hasNewReview = currentReviews.length > startReviewCount;
|
|
277
302
|
const hasBotComment = currentComments.some(c =>
|
|
278
303
|
(c.user?.type === 'Bot' || c.user?.login?.includes('bot')) &&
|
|
279
|
-
new Date(c.created_at) >
|
|
304
|
+
new Date(c.created_at).getTime() > (Date.now() - elapsed * 1000)
|
|
280
305
|
);
|
|
281
306
|
|
|
282
307
|
if (hasNewReview || hasBotComment) {
|
|
@@ -290,97 +315,98 @@ async function waitForReview(projectDir, prNumber, pollInterval, timeout) {
|
|
|
290
315
|
}
|
|
291
316
|
|
|
292
317
|
// ──────────────────────────────────────────────
|
|
293
|
-
//
|
|
318
|
+
// Fix phase
|
|
294
319
|
// ──────────────────────────────────────────────
|
|
295
320
|
async function fixPhase(projectDir, task, feedback, round) {
|
|
296
|
-
log.step(
|
|
321
|
+
log.step(`Fixing review comments (round ${round})`);
|
|
297
322
|
|
|
298
|
-
const fixPrompt =
|
|
323
|
+
const fixPrompt = `The following are PR review comments. Please fix each one:
|
|
299
324
|
|
|
300
|
-
## Review
|
|
325
|
+
## Review Comments
|
|
301
326
|
${feedback}
|
|
302
327
|
|
|
303
|
-
##
|
|
304
|
-
1.
|
|
305
|
-
2.
|
|
306
|
-
3.
|
|
307
|
-
4.
|
|
328
|
+
## Requirements
|
|
329
|
+
1. Fix each issue listed above
|
|
330
|
+
2. Suggestions (non-blocking) can be skipped — explain why in the commit message
|
|
331
|
+
3. Ensure fixes don't introduce new issues
|
|
332
|
+
4. When done, run: git add -A && git commit -m "fix(task-${task.id}): address review round ${round}"
|
|
308
333
|
`;
|
|
309
334
|
|
|
310
|
-
//
|
|
335
|
+
// Save to file and prompt user
|
|
311
336
|
const promptPath = resolve(projectDir, '.codex-copilot/_current_prompt.md');
|
|
312
337
|
writeFileSync(promptPath, fixPrompt);
|
|
313
338
|
|
|
314
|
-
//
|
|
339
|
+
// Try CodeX CLI
|
|
315
340
|
const codexAvailable = isCodexAvailable();
|
|
316
341
|
|
|
317
342
|
if (codexAvailable) {
|
|
318
|
-
const autoFix = await confirm('
|
|
343
|
+
const autoFix = await confirm('Auto-invoke CodeX to fix?');
|
|
319
344
|
if (autoFix) {
|
|
320
345
|
try {
|
|
321
|
-
execSync(`
|
|
346
|
+
execSync(`cat .codex-copilot/_current_prompt.md | codex exec --full-auto -`, {
|
|
322
347
|
cwd: projectDir,
|
|
323
348
|
stdio: 'inherit',
|
|
324
349
|
});
|
|
325
|
-
log.info('CodeX
|
|
350
|
+
log.info('CodeX fix complete');
|
|
326
351
|
return;
|
|
327
352
|
} catch {
|
|
328
|
-
log.warn('CodeX CLI
|
|
353
|
+
log.warn('CodeX CLI invocation failed, falling back to manual mode');
|
|
329
354
|
}
|
|
330
355
|
}
|
|
331
356
|
}
|
|
332
357
|
|
|
333
|
-
//
|
|
358
|
+
// Manual mode
|
|
334
359
|
log.blank();
|
|
335
|
-
log.dim(
|
|
336
|
-
log.dim('
|
|
360
|
+
log.dim('Review fix prompt saved to .codex-copilot/_current_prompt.md');
|
|
361
|
+
log.dim('Paste the file content into CodeX to execute');
|
|
337
362
|
|
|
338
363
|
copyToClipboard(fixPrompt);
|
|
339
364
|
|
|
340
|
-
await ask('CodeX
|
|
365
|
+
await ask('Press Enter after CodeX fix is complete...');
|
|
341
366
|
}
|
|
342
367
|
|
|
343
368
|
// ──────────────────────────────────────────────
|
|
344
|
-
//
|
|
369
|
+
// Phase 4: Merge
|
|
345
370
|
// ──────────────────────────────────────────────
|
|
346
|
-
async function mergePhase(projectDir, task, prInfo) {
|
|
347
|
-
log.step('
|
|
371
|
+
async function mergePhase(projectDir, task, prInfo, baseBranch) {
|
|
372
|
+
log.step('Phase 4/4: Merge PR');
|
|
348
373
|
|
|
349
|
-
const doMerge = await confirm(
|
|
374
|
+
const doMerge = await confirm(`Merge PR #${prInfo.number}?`);
|
|
350
375
|
if (!doMerge) {
|
|
351
|
-
log.warn('
|
|
376
|
+
log.warn('User skipped merge');
|
|
352
377
|
return;
|
|
353
378
|
}
|
|
354
379
|
|
|
355
380
|
try {
|
|
356
381
|
github.mergePR(projectDir, prInfo.number);
|
|
357
|
-
log.info(`PR #${prInfo.number}
|
|
382
|
+
log.info(`PR #${prInfo.number} merged ✅`);
|
|
358
383
|
} catch (err) {
|
|
359
|
-
log.error(
|
|
360
|
-
log.warn('
|
|
361
|
-
await ask('
|
|
384
|
+
log.error(`Merge failed: ${err.message}`);
|
|
385
|
+
log.warn('Please merge manually, then press Enter to continue');
|
|
386
|
+
await ask('Continue...');
|
|
362
387
|
}
|
|
363
388
|
|
|
364
|
-
//
|
|
365
|
-
|
|
366
|
-
const config = JSON.parse(readFileSync(configPath, 'utf-8'));
|
|
367
|
-
git.checkoutMain(projectDir, config.base_branch || 'main');
|
|
389
|
+
// Switch back to main branch
|
|
390
|
+
git.checkoutMain(projectDir, baseBranch);
|
|
368
391
|
}
|
|
369
392
|
|
|
370
393
|
function buildDevPrompt(task) {
|
|
371
|
-
|
|
394
|
+
const acceptanceList = Array.isArray(task.acceptance) && task.acceptance.length > 0
|
|
395
|
+
? task.acceptance.map(a => `- ${a}`).join('\n')
|
|
396
|
+
: '- Feature works correctly';
|
|
397
|
+
return `Please complete the following development task:
|
|
372
398
|
|
|
373
|
-
##
|
|
399
|
+
## Task #${task.id}: ${task.title}
|
|
374
400
|
|
|
375
401
|
${task.description}
|
|
376
402
|
|
|
377
|
-
##
|
|
378
|
-
${
|
|
403
|
+
## Acceptance Criteria
|
|
404
|
+
${acceptanceList}
|
|
379
405
|
|
|
380
|
-
##
|
|
381
|
-
1.
|
|
382
|
-
2.
|
|
383
|
-
3.
|
|
406
|
+
## Requirements
|
|
407
|
+
1. Strictly follow the project's existing code style and tech stack
|
|
408
|
+
2. Ensure the code compiles/runs correctly when done
|
|
409
|
+
3. When done, run:
|
|
384
410
|
git add -A
|
|
385
411
|
git commit -m "feat(task-${task.id}): ${task.title}"
|
|
386
412
|
`;
|
|
@@ -392,7 +418,8 @@ function sleep(ms) {
|
|
|
392
418
|
|
|
393
419
|
function isCodexAvailable() {
|
|
394
420
|
try {
|
|
395
|
-
|
|
421
|
+
const cmd = process.platform === 'win32' ? 'where codex' : 'which codex';
|
|
422
|
+
execSync(cmd, { stdio: 'pipe' });
|
|
396
423
|
return true;
|
|
397
424
|
} catch {
|
|
398
425
|
return false;
|
|
@@ -402,6 +429,6 @@ function isCodexAvailable() {
|
|
|
402
429
|
function copyToClipboard(text) {
|
|
403
430
|
try {
|
|
404
431
|
execSync('pbcopy', { input: text, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
405
|
-
log.info('📋
|
|
432
|
+
log.info('📋 Copied to clipboard');
|
|
406
433
|
} catch {}
|
|
407
434
|
}
|
package/src/commands/status.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* codex-copilot status -
|
|
2
|
+
* codex-copilot status - Show current progress
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import { readFileSync } from 'fs';
|
|
@@ -10,36 +10,43 @@ export async function status(projectDir) {
|
|
|
10
10
|
const tasksPath = resolve(projectDir, '.codex-copilot/tasks.json');
|
|
11
11
|
const statePath = resolve(projectDir, '.codex-copilot/state.json');
|
|
12
12
|
|
|
13
|
-
|
|
14
|
-
|
|
13
|
+
let tasks, state;
|
|
14
|
+
try {
|
|
15
|
+
tasks = JSON.parse(readFileSync(tasksPath, 'utf-8'));
|
|
16
|
+
state = JSON.parse(readFileSync(statePath, 'utf-8'));
|
|
17
|
+
} catch (err) {
|
|
18
|
+
log.error(`Failed to read files: ${err.message}`);
|
|
19
|
+
log.warn('Files may be corrupted. Run: codex-copilot reset');
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
15
22
|
|
|
16
|
-
log.title(`📊
|
|
23
|
+
log.title(`📊 Project: ${tasks.project}`);
|
|
17
24
|
log.blank();
|
|
18
25
|
|
|
19
|
-
//
|
|
26
|
+
// Progress bar
|
|
20
27
|
const completed = tasks.tasks.filter(t => t.status === 'completed').length;
|
|
21
28
|
const inProgress = tasks.tasks.filter(t => t.status === 'in_progress' || t.status === 'developed').length;
|
|
22
29
|
const pending = tasks.tasks.filter(t => t.status === 'pending').length;
|
|
23
30
|
const skipped = tasks.tasks.filter(t => t.status === 'skipped').length;
|
|
24
31
|
|
|
25
|
-
progressBar(completed, tasks.total, `${completed}/${tasks.total}
|
|
32
|
+
progressBar(completed, tasks.total, `${completed}/${tasks.total} done`);
|
|
26
33
|
log.blank();
|
|
27
34
|
|
|
28
|
-
//
|
|
29
|
-
log.info(`✅
|
|
30
|
-
if (inProgress > 0) log.info(`🔄
|
|
31
|
-
log.info(`⏳
|
|
32
|
-
if (skipped > 0) log.warn(`⏭
|
|
35
|
+
// Stats
|
|
36
|
+
log.info(`✅ Completed: ${completed}`);
|
|
37
|
+
if (inProgress > 0) log.info(`🔄 In progress: ${inProgress}`);
|
|
38
|
+
log.info(`⏳ Pending: ${pending}`);
|
|
39
|
+
if (skipped > 0) log.warn(`⏭ Skipped: ${skipped}`);
|
|
33
40
|
log.blank();
|
|
34
41
|
|
|
35
|
-
//
|
|
42
|
+
// Current state
|
|
36
43
|
if (state.current_pr) {
|
|
37
|
-
log.info(
|
|
44
|
+
log.info(`Current PR: #${state.current_pr} (review round ${state.review_round})`);
|
|
38
45
|
}
|
|
39
46
|
log.blank();
|
|
40
47
|
|
|
41
|
-
//
|
|
42
|
-
log.title('
|
|
48
|
+
// Task list
|
|
49
|
+
log.title('Task list:');
|
|
43
50
|
log.blank();
|
|
44
51
|
for (const task of tasks.tasks) {
|
|
45
52
|
const icon = task.status === 'completed' ? '✅' :
|