@jojonax/codex-copilot 1.0.2 → 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 +82 -82
- package/src/commands/reset.js +10 -10
- package/src/commands/run.js +121 -115
- package/src/commands/status.js +15 -15
- package/src/utils/detect-prd.js +18 -18
- package/src/utils/git.js +17 -17
- package/src/utils/github.js +19 -19
- package/src/utils/logger.js +14 -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';
|
|
@@ -31,17 +31,17 @@ export async function run(projectDir) {
|
|
|
31
31
|
tasks = readJSON(tasksPath);
|
|
32
32
|
state = readJSON(statePath);
|
|
33
33
|
} catch (err) {
|
|
34
|
-
log.error(
|
|
35
|
-
log.warn('
|
|
34
|
+
log.error(`Failed to read task/state files: ${err.message}`);
|
|
35
|
+
log.warn('Files may be corrupted. Run: codex-copilot reset');
|
|
36
36
|
closePrompt();
|
|
37
37
|
process.exit(1);
|
|
38
38
|
}
|
|
39
39
|
const config = existsSync(configPath) ? readJSON(configPath) : {};
|
|
40
40
|
|
|
41
|
-
//
|
|
41
|
+
// Validate tasks.json structure
|
|
42
42
|
if (!tasks.tasks || !Array.isArray(tasks.tasks) || !tasks.total) {
|
|
43
|
-
log.error('tasks.json
|
|
44
|
-
log.warn('
|
|
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
45
|
closePrompt();
|
|
46
46
|
process.exit(1);
|
|
47
47
|
}
|
|
@@ -51,54 +51,54 @@ export async function run(projectDir) {
|
|
|
51
51
|
const pollInterval = config.review_poll_interval || 60;
|
|
52
52
|
const waitTimeout = config.review_wait_timeout || 600;
|
|
53
53
|
|
|
54
|
-
log.title('🚀
|
|
54
|
+
log.title('🚀 Starting automated development loop');
|
|
55
55
|
log.blank();
|
|
56
|
-
log.info(
|
|
57
|
-
log.info(
|
|
58
|
-
log.info(
|
|
59
|
-
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}`);
|
|
60
60
|
log.blank();
|
|
61
|
-
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`);
|
|
62
62
|
log.blank();
|
|
63
63
|
|
|
64
|
-
//
|
|
64
|
+
// Execute tasks one by one
|
|
65
65
|
for (const task of tasks.tasks) {
|
|
66
|
-
if (task.id <= state.current_task) continue; //
|
|
66
|
+
if (task.id <= state.current_task) continue; // Skip completed
|
|
67
67
|
if (task.status === 'completed' || task.status === 'skipped') continue;
|
|
68
68
|
|
|
69
69
|
log.blank();
|
|
70
|
-
log.title(`━━━
|
|
70
|
+
log.title(`━━━ Task ${task.id}/${tasks.total}: ${task.title} ━━━`);
|
|
71
71
|
log.blank();
|
|
72
72
|
|
|
73
|
-
//
|
|
73
|
+
// Check dependencies
|
|
74
74
|
if (task.depends_on && task.depends_on.length > 0) {
|
|
75
75
|
const unfinished = task.depends_on.filter(dep => {
|
|
76
76
|
const depTask = tasks.tasks.find(t => t.id === dep);
|
|
77
77
|
return depTask && depTask.status !== 'completed';
|
|
78
78
|
});
|
|
79
79
|
if (unfinished.length > 0) {
|
|
80
|
-
log.warn(
|
|
80
|
+
log.warn(`Unfinished dependencies: ${unfinished.join(', ')} — skipping`);
|
|
81
81
|
continue;
|
|
82
82
|
}
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
-
// =====
|
|
85
|
+
// ===== Phase 1: Develop =====
|
|
86
86
|
await developPhase(projectDir, task, baseBranch);
|
|
87
87
|
|
|
88
|
-
// =====
|
|
88
|
+
// ===== Phase 2: Create PR =====
|
|
89
89
|
const prInfo = await prPhase(projectDir, task, baseBranch);
|
|
90
90
|
|
|
91
|
-
// =====
|
|
91
|
+
// ===== Phase 3: Review loop =====
|
|
92
92
|
await reviewLoop(projectDir, task, prInfo, {
|
|
93
93
|
maxRounds: maxReviewRounds,
|
|
94
94
|
pollInterval,
|
|
95
95
|
waitTimeout,
|
|
96
96
|
});
|
|
97
97
|
|
|
98
|
-
// =====
|
|
98
|
+
// ===== Phase 4: Merge =====
|
|
99
99
|
await mergePhase(projectDir, task, prInfo, baseBranch);
|
|
100
100
|
|
|
101
|
-
//
|
|
101
|
+
// Update task state
|
|
102
102
|
task.status = 'completed';
|
|
103
103
|
state.current_task = task.id;
|
|
104
104
|
state.current_pr = null;
|
|
@@ -107,91 +107,94 @@ export async function run(projectDir) {
|
|
|
107
107
|
writeJSON(statePath, state);
|
|
108
108
|
|
|
109
109
|
log.blank();
|
|
110
|
-
progressBar(task.id, tasks.total, `${task.id}/${tasks.total}
|
|
111
|
-
log.info(`✅
|
|
110
|
+
progressBar(task.id, tasks.total, `${task.id}/${tasks.total} tasks done`);
|
|
111
|
+
log.info(`✅ Task #${task.id} complete!`);
|
|
112
112
|
}
|
|
113
113
|
|
|
114
114
|
log.blank();
|
|
115
|
-
log.title('🎉
|
|
115
|
+
log.title('🎉 All tasks complete!');
|
|
116
116
|
log.blank();
|
|
117
117
|
closePrompt();
|
|
118
118
|
}
|
|
119
119
|
|
|
120
120
|
// ──────────────────────────────────────────────
|
|
121
|
-
//
|
|
121
|
+
// Phase 1: Develop
|
|
122
122
|
// ──────────────────────────────────────────────
|
|
123
123
|
async function developPhase(projectDir, task, baseBranch) {
|
|
124
|
-
log.step('
|
|
124
|
+
log.step('Phase 1/4: Develop');
|
|
125
125
|
|
|
126
|
-
//
|
|
126
|
+
// Switch to feature branch
|
|
127
127
|
git.checkoutBranch(projectDir, task.branch, baseBranch);
|
|
128
|
-
log.info(
|
|
128
|
+
log.info(`Switched to branch: ${task.branch}`);
|
|
129
129
|
|
|
130
|
-
//
|
|
130
|
+
// Build development prompt
|
|
131
131
|
const devPrompt = buildDevPrompt(task);
|
|
132
132
|
|
|
133
|
-
//
|
|
133
|
+
// Check if CodeX CLI is available
|
|
134
134
|
const codexAvailable = isCodexAvailable();
|
|
135
135
|
|
|
136
136
|
if (codexAvailable) {
|
|
137
|
-
log.info('
|
|
138
|
-
const autoRun = await confirm('
|
|
137
|
+
log.info('CodeX CLI detected, ready for auto-execution...');
|
|
138
|
+
const autoRun = await confirm('Auto-invoke CodeX for development?');
|
|
139
139
|
if (autoRun) {
|
|
140
140
|
try {
|
|
141
141
|
const promptPath = resolve(projectDir, '.codex-copilot/_current_prompt.md');
|
|
142
142
|
writeFileSync(promptPath, devPrompt);
|
|
143
|
-
execSync(`
|
|
143
|
+
execSync(`cat .codex-copilot/_current_prompt.md | codex exec --full-auto -`, {
|
|
144
144
|
cwd: projectDir,
|
|
145
145
|
stdio: 'inherit',
|
|
146
146
|
});
|
|
147
|
-
log.info('CodeX
|
|
147
|
+
log.info('CodeX development complete');
|
|
148
148
|
return;
|
|
149
149
|
} catch (err) {
|
|
150
|
-
log.warn(`CodeX CLI
|
|
151
|
-
log.warn('
|
|
150
|
+
log.warn(`CodeX CLI invocation failed: ${err.message}`);
|
|
151
|
+
log.warn('Falling back to manual mode');
|
|
152
152
|
}
|
|
153
153
|
}
|
|
154
154
|
}
|
|
155
155
|
|
|
156
|
-
//
|
|
156
|
+
// Manual mode: display prompt, wait for user confirmation
|
|
157
157
|
log.blank();
|
|
158
|
-
console.log(' ┌───
|
|
158
|
+
console.log(' ┌─── Paste the following into CodeX Desktop to execute ───┐');
|
|
159
159
|
log.blank();
|
|
160
160
|
console.log(devPrompt.split('\n').map(l => ` │ ${l}`).join('\n'));
|
|
161
161
|
log.blank();
|
|
162
|
-
console.log('
|
|
162
|
+
console.log(' └─────────────────────────────────────────────────────────┘');
|
|
163
163
|
log.blank();
|
|
164
164
|
|
|
165
|
-
//
|
|
165
|
+
// Also save prompt to file for easy copying
|
|
166
166
|
const promptPath = resolve(projectDir, '.codex-copilot/_current_prompt.md');
|
|
167
167
|
writeFileSync(promptPath, devPrompt);
|
|
168
|
-
log.dim(
|
|
168
|
+
log.dim('Prompt saved to .codex-copilot/_current_prompt.md (you can copy the file directly)');
|
|
169
169
|
log.blank();
|
|
170
170
|
|
|
171
|
-
//
|
|
171
|
+
// Try to copy to clipboard (via stdin to avoid shell injection)
|
|
172
172
|
copyToClipboard(devPrompt);
|
|
173
173
|
|
|
174
|
-
await ask('CodeX
|
|
174
|
+
await ask('Press Enter after CodeX development is complete...');
|
|
175
175
|
}
|
|
176
176
|
|
|
177
177
|
// ──────────────────────────────────────────────
|
|
178
|
-
//
|
|
178
|
+
// Phase 2: Create PR
|
|
179
179
|
// ──────────────────────────────────────────────
|
|
180
180
|
async function prPhase(projectDir, task, baseBranch) {
|
|
181
|
-
log.step('
|
|
181
|
+
log.step('Phase 2/4: Submit PR');
|
|
182
182
|
|
|
183
|
-
//
|
|
183
|
+
// Ensure changes are committed
|
|
184
184
|
if (!git.isClean(projectDir)) {
|
|
185
|
-
log.info('
|
|
185
|
+
log.info('Uncommitted changes detected, auto-committing...');
|
|
186
186
|
git.commitAll(projectDir, `feat(task-${task.id}): ${task.title}`);
|
|
187
187
|
}
|
|
188
188
|
|
|
189
|
-
//
|
|
189
|
+
// Push
|
|
190
190
|
git.pushBranch(projectDir, task.branch);
|
|
191
|
-
log.info('
|
|
191
|
+
log.info('Code pushed');
|
|
192
192
|
|
|
193
|
-
//
|
|
194
|
-
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*`;
|
|
195
198
|
|
|
196
199
|
try {
|
|
197
200
|
const prInfo = github.createPR(projectDir, {
|
|
@@ -200,36 +203,36 @@ async function prPhase(projectDir, task, baseBranch) {
|
|
|
200
203
|
base: baseBranch,
|
|
201
204
|
head: task.branch,
|
|
202
205
|
});
|
|
203
|
-
log.info(`PR
|
|
206
|
+
log.info(`PR created: ${prInfo.url}`);
|
|
204
207
|
return prInfo;
|
|
205
208
|
} catch (err) {
|
|
206
|
-
// PR
|
|
207
|
-
log.warn(
|
|
208
|
-
const prNumber = await ask('
|
|
209
|
+
// PR may already exist
|
|
210
|
+
log.warn(`PR creation error: ${err.message}`);
|
|
211
|
+
const prNumber = await ask('Enter existing PR number:');
|
|
209
212
|
const parsed = parseInt(prNumber, 10);
|
|
210
213
|
if (isNaN(parsed) || parsed <= 0) {
|
|
211
|
-
log.error('
|
|
212
|
-
throw new Error('
|
|
214
|
+
log.error('Invalid PR number');
|
|
215
|
+
throw new Error('Invalid PR number');
|
|
213
216
|
}
|
|
214
217
|
return { number: parsed, url: '' };
|
|
215
218
|
}
|
|
216
219
|
}
|
|
217
220
|
|
|
218
221
|
// ──────────────────────────────────────────────
|
|
219
|
-
//
|
|
222
|
+
// Phase 3: Review loop
|
|
220
223
|
// ──────────────────────────────────────────────
|
|
221
224
|
async function reviewLoop(projectDir, task, prInfo, { maxRounds: _maxRounds, pollInterval, waitTimeout }) {
|
|
222
225
|
let maxRounds = _maxRounds;
|
|
223
|
-
log.step('
|
|
226
|
+
log.step('Phase 3/4: Waiting for review');
|
|
224
227
|
|
|
225
228
|
for (let round = 1; round <= maxRounds; round++) {
|
|
226
|
-
//
|
|
227
|
-
log.info(
|
|
229
|
+
// Wait for review
|
|
230
|
+
log.info(`Waiting for review feedback... (timeout: ${waitTimeout}s)`);
|
|
228
231
|
const gotReview = await waitForReview(projectDir, prInfo.number, pollInterval, waitTimeout);
|
|
229
232
|
|
|
230
233
|
if (!gotReview) {
|
|
231
|
-
log.warn('
|
|
232
|
-
const action = await ask('
|
|
234
|
+
log.warn('Review wait timed out');
|
|
235
|
+
const action = await ask('Enter "skip" (skip review) or "wait" (keep waiting):');
|
|
233
236
|
if (action === 'wait') {
|
|
234
237
|
round--;
|
|
235
238
|
continue;
|
|
@@ -237,28 +240,28 @@ async function reviewLoop(projectDir, task, prInfo, { maxRounds: _maxRounds, pol
|
|
|
237
240
|
break;
|
|
238
241
|
}
|
|
239
242
|
|
|
240
|
-
//
|
|
243
|
+
// Check review status
|
|
241
244
|
const state = github.getLatestReviewState(projectDir, prInfo.number);
|
|
242
|
-
log.info(`Review
|
|
245
|
+
log.info(`Review status: ${state}`);
|
|
243
246
|
|
|
244
247
|
if (state === 'APPROVED') {
|
|
245
|
-
log.info('✅ Review
|
|
248
|
+
log.info('✅ Review approved!');
|
|
246
249
|
return;
|
|
247
250
|
}
|
|
248
251
|
|
|
249
|
-
//
|
|
252
|
+
// Collect review feedback
|
|
250
253
|
const feedback = github.collectReviewFeedback(projectDir, prInfo.number);
|
|
251
254
|
if (!feedback) {
|
|
252
|
-
log.info('
|
|
255
|
+
log.info('No specific change requests found, proceeding');
|
|
253
256
|
return;
|
|
254
257
|
}
|
|
255
258
|
|
|
256
259
|
log.blank();
|
|
257
|
-
log.warn(
|
|
260
|
+
log.warn(`Received review feedback (round ${round}/${maxRounds})`);
|
|
258
261
|
|
|
259
262
|
if (round >= maxRounds) {
|
|
260
|
-
log.warn(
|
|
261
|
-
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):');
|
|
262
265
|
if (choice === 'fix') {
|
|
263
266
|
maxRounds++;
|
|
264
267
|
} else if (choice === 'skip') {
|
|
@@ -268,17 +271,17 @@ async function reviewLoop(projectDir, task, prInfo, { maxRounds: _maxRounds, pol
|
|
|
268
271
|
}
|
|
269
272
|
}
|
|
270
273
|
|
|
271
|
-
//
|
|
274
|
+
// Let CodeX fix
|
|
272
275
|
await fixPhase(projectDir, task, feedback, round);
|
|
273
276
|
|
|
274
|
-
//
|
|
277
|
+
// Push fix
|
|
275
278
|
if (!git.isClean(projectDir)) {
|
|
276
279
|
git.commitAll(projectDir, `fix(task-${task.id}): address review comments (round ${round})`);
|
|
277
280
|
}
|
|
278
281
|
git.pushBranch(projectDir, task.branch);
|
|
279
|
-
log.info('
|
|
282
|
+
log.info('Fix pushed, waiting for next review round...');
|
|
280
283
|
|
|
281
|
-
//
|
|
284
|
+
// Brief wait for review bot to react
|
|
282
285
|
await sleep(10000);
|
|
283
286
|
}
|
|
284
287
|
}
|
|
@@ -294,7 +297,7 @@ async function waitForReview(projectDir, prNumber, pollInterval, timeout) {
|
|
|
294
297
|
const currentReviews = github.getReviews(projectDir, prNumber);
|
|
295
298
|
const currentComments = github.getIssueComments(projectDir, prNumber);
|
|
296
299
|
|
|
297
|
-
//
|
|
300
|
+
// Check for new reviews or bot comments
|
|
298
301
|
const hasNewReview = currentReviews.length > startReviewCount;
|
|
299
302
|
const hasBotComment = currentComments.some(c =>
|
|
300
303
|
(c.user?.type === 'Bot' || c.user?.login?.includes('bot')) &&
|
|
@@ -312,95 +315,98 @@ async function waitForReview(projectDir, prNumber, pollInterval, timeout) {
|
|
|
312
315
|
}
|
|
313
316
|
|
|
314
317
|
// ──────────────────────────────────────────────
|
|
315
|
-
//
|
|
318
|
+
// Fix phase
|
|
316
319
|
// ──────────────────────────────────────────────
|
|
317
320
|
async function fixPhase(projectDir, task, feedback, round) {
|
|
318
|
-
log.step(
|
|
321
|
+
log.step(`Fixing review comments (round ${round})`);
|
|
319
322
|
|
|
320
|
-
const fixPrompt =
|
|
323
|
+
const fixPrompt = `The following are PR review comments. Please fix each one:
|
|
321
324
|
|
|
322
|
-
## Review
|
|
325
|
+
## Review Comments
|
|
323
326
|
${feedback}
|
|
324
327
|
|
|
325
|
-
##
|
|
326
|
-
1.
|
|
327
|
-
2.
|
|
328
|
-
3.
|
|
329
|
-
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}"
|
|
330
333
|
`;
|
|
331
334
|
|
|
332
|
-
//
|
|
335
|
+
// Save to file and prompt user
|
|
333
336
|
const promptPath = resolve(projectDir, '.codex-copilot/_current_prompt.md');
|
|
334
337
|
writeFileSync(promptPath, fixPrompt);
|
|
335
338
|
|
|
336
|
-
//
|
|
339
|
+
// Try CodeX CLI
|
|
337
340
|
const codexAvailable = isCodexAvailable();
|
|
338
341
|
|
|
339
342
|
if (codexAvailable) {
|
|
340
|
-
const autoFix = await confirm('
|
|
343
|
+
const autoFix = await confirm('Auto-invoke CodeX to fix?');
|
|
341
344
|
if (autoFix) {
|
|
342
345
|
try {
|
|
343
|
-
execSync(`
|
|
346
|
+
execSync(`cat .codex-copilot/_current_prompt.md | codex exec --full-auto -`, {
|
|
344
347
|
cwd: projectDir,
|
|
345
348
|
stdio: 'inherit',
|
|
346
349
|
});
|
|
347
|
-
log.info('CodeX
|
|
350
|
+
log.info('CodeX fix complete');
|
|
348
351
|
return;
|
|
349
352
|
} catch {
|
|
350
|
-
log.warn('CodeX CLI
|
|
353
|
+
log.warn('CodeX CLI invocation failed, falling back to manual mode');
|
|
351
354
|
}
|
|
352
355
|
}
|
|
353
356
|
}
|
|
354
357
|
|
|
355
|
-
//
|
|
358
|
+
// Manual mode
|
|
356
359
|
log.blank();
|
|
357
|
-
log.dim(
|
|
358
|
-
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');
|
|
359
362
|
|
|
360
363
|
copyToClipboard(fixPrompt);
|
|
361
364
|
|
|
362
|
-
await ask('CodeX
|
|
365
|
+
await ask('Press Enter after CodeX fix is complete...');
|
|
363
366
|
}
|
|
364
367
|
|
|
365
368
|
// ──────────────────────────────────────────────
|
|
366
|
-
//
|
|
369
|
+
// Phase 4: Merge
|
|
367
370
|
// ──────────────────────────────────────────────
|
|
368
371
|
async function mergePhase(projectDir, task, prInfo, baseBranch) {
|
|
369
|
-
log.step('
|
|
372
|
+
log.step('Phase 4/4: Merge PR');
|
|
370
373
|
|
|
371
|
-
const doMerge = await confirm(
|
|
374
|
+
const doMerge = await confirm(`Merge PR #${prInfo.number}?`);
|
|
372
375
|
if (!doMerge) {
|
|
373
|
-
log.warn('
|
|
376
|
+
log.warn('User skipped merge');
|
|
374
377
|
return;
|
|
375
378
|
}
|
|
376
379
|
|
|
377
380
|
try {
|
|
378
381
|
github.mergePR(projectDir, prInfo.number);
|
|
379
|
-
log.info(`PR #${prInfo.number}
|
|
382
|
+
log.info(`PR #${prInfo.number} merged ✅`);
|
|
380
383
|
} catch (err) {
|
|
381
|
-
log.error(
|
|
382
|
-
log.warn('
|
|
383
|
-
await ask('
|
|
384
|
+
log.error(`Merge failed: ${err.message}`);
|
|
385
|
+
log.warn('Please merge manually, then press Enter to continue');
|
|
386
|
+
await ask('Continue...');
|
|
384
387
|
}
|
|
385
388
|
|
|
386
|
-
//
|
|
389
|
+
// Switch back to main branch
|
|
387
390
|
git.checkoutMain(projectDir, baseBranch);
|
|
388
391
|
}
|
|
389
392
|
|
|
390
393
|
function buildDevPrompt(task) {
|
|
391
|
-
|
|
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:
|
|
392
398
|
|
|
393
|
-
##
|
|
399
|
+
## Task #${task.id}: ${task.title}
|
|
394
400
|
|
|
395
401
|
${task.description}
|
|
396
402
|
|
|
397
|
-
##
|
|
398
|
-
${
|
|
403
|
+
## Acceptance Criteria
|
|
404
|
+
${acceptanceList}
|
|
399
405
|
|
|
400
|
-
##
|
|
401
|
-
1.
|
|
402
|
-
2.
|
|
403
|
-
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:
|
|
404
410
|
git add -A
|
|
405
411
|
git commit -m "feat(task-${task.id}): ${task.title}"
|
|
406
412
|
`;
|
|
@@ -423,6 +429,6 @@ function isCodexAvailable() {
|
|
|
423
429
|
function copyToClipboard(text) {
|
|
424
430
|
try {
|
|
425
431
|
execSync('pbcopy', { input: text, stdio: ['pipe', 'pipe', 'pipe'] });
|
|
426
|
-
log.info('📋
|
|
432
|
+
log.info('📋 Copied to clipboard');
|
|
427
433
|
} catch {}
|
|
428
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';
|
|
@@ -15,38 +15,38 @@ export async function status(projectDir) {
|
|
|
15
15
|
tasks = JSON.parse(readFileSync(tasksPath, 'utf-8'));
|
|
16
16
|
state = JSON.parse(readFileSync(statePath, 'utf-8'));
|
|
17
17
|
} catch (err) {
|
|
18
|
-
log.error(
|
|
19
|
-
log.warn('
|
|
18
|
+
log.error(`Failed to read files: ${err.message}`);
|
|
19
|
+
log.warn('Files may be corrupted. Run: codex-copilot reset');
|
|
20
20
|
return;
|
|
21
21
|
}
|
|
22
22
|
|
|
23
|
-
log.title(`📊
|
|
23
|
+
log.title(`📊 Project: ${tasks.project}`);
|
|
24
24
|
log.blank();
|
|
25
25
|
|
|
26
|
-
//
|
|
26
|
+
// Progress bar
|
|
27
27
|
const completed = tasks.tasks.filter(t => t.status === 'completed').length;
|
|
28
28
|
const inProgress = tasks.tasks.filter(t => t.status === 'in_progress' || t.status === 'developed').length;
|
|
29
29
|
const pending = tasks.tasks.filter(t => t.status === 'pending').length;
|
|
30
30
|
const skipped = tasks.tasks.filter(t => t.status === 'skipped').length;
|
|
31
31
|
|
|
32
|
-
progressBar(completed, tasks.total, `${completed}/${tasks.total}
|
|
32
|
+
progressBar(completed, tasks.total, `${completed}/${tasks.total} done`);
|
|
33
33
|
log.blank();
|
|
34
34
|
|
|
35
|
-
//
|
|
36
|
-
log.info(`✅
|
|
37
|
-
if (inProgress > 0) log.info(`🔄
|
|
38
|
-
log.info(`⏳
|
|
39
|
-
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}`);
|
|
40
40
|
log.blank();
|
|
41
41
|
|
|
42
|
-
//
|
|
42
|
+
// Current state
|
|
43
43
|
if (state.current_pr) {
|
|
44
|
-
log.info(
|
|
44
|
+
log.info(`Current PR: #${state.current_pr} (review round ${state.review_round})`);
|
|
45
45
|
}
|
|
46
46
|
log.blank();
|
|
47
47
|
|
|
48
|
-
//
|
|
49
|
-
log.title('
|
|
48
|
+
// Task list
|
|
49
|
+
log.title('Task list:');
|
|
50
50
|
log.blank();
|
|
51
51
|
for (const task of tasks.tasks) {
|
|
52
52
|
const icon = task.status === 'completed' ? '✅' :
|
package/src/utils/detect-prd.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* PRD
|
|
3
|
-
*
|
|
2
|
+
* PRD auto-detection module
|
|
3
|
+
* Automatically finds PRD documents in the project directory
|
|
4
4
|
*/
|
|
5
5
|
|
|
6
6
|
import { readdirSync, readFileSync, statSync } from 'fs';
|
|
7
7
|
import { resolve, extname, relative } from 'path';
|
|
8
8
|
import { log } from './logger.js';
|
|
9
9
|
|
|
10
|
-
// PRD
|
|
10
|
+
// PRD filename patterns (ordered by priority, highest first)
|
|
11
11
|
const PRD_PATTERNS = [
|
|
12
12
|
/prd/i,
|
|
13
13
|
/product.?requirement/i,
|
|
@@ -18,9 +18,9 @@ const PRD_PATTERNS = [
|
|
|
18
18
|
/spec/i,
|
|
19
19
|
];
|
|
20
20
|
|
|
21
|
-
//
|
|
21
|
+
// Directories to search (ordered by priority)
|
|
22
22
|
const SEARCH_DIRS = [
|
|
23
|
-
'.', //
|
|
23
|
+
'.', // Project root
|
|
24
24
|
'docs',
|
|
25
25
|
'doc',
|
|
26
26
|
'PRD',
|
|
@@ -30,16 +30,16 @@ const SEARCH_DIRS = [
|
|
|
30
30
|
'文档',
|
|
31
31
|
];
|
|
32
32
|
|
|
33
|
-
//
|
|
33
|
+
// Directories to ignore
|
|
34
34
|
const IGNORE_DIRS = new Set([
|
|
35
35
|
'node_modules', '.git', '.next', 'dist', 'build', 'vendor',
|
|
36
36
|
'.codex-copilot', '.vscode', '.idea', '__pycache__', 'coverage',
|
|
37
37
|
]);
|
|
38
38
|
|
|
39
39
|
/**
|
|
40
|
-
*
|
|
41
|
-
* @param {string} projectDir -
|
|
42
|
-
* @returns {Array<{path: string, score: number, name: string}>}
|
|
40
|
+
* Auto-detect PRD files in the project
|
|
41
|
+
* @param {string} projectDir - Project root directory
|
|
42
|
+
* @returns {Array<{path: string, score: number, name: string}>} Candidate PRD files sorted by match score
|
|
43
43
|
*/
|
|
44
44
|
export function detectPRD(projectDir) {
|
|
45
45
|
const candidates = [];
|
|
@@ -55,14 +55,14 @@ export function detectPRD(projectDir) {
|
|
|
55
55
|
scanDir(searchPath, projectDir, candidates, 0);
|
|
56
56
|
}
|
|
57
57
|
|
|
58
|
-
//
|
|
58
|
+
// Sort by match score (descending)
|
|
59
59
|
candidates.sort((a, b) => b.score - a.score);
|
|
60
60
|
|
|
61
61
|
return candidates;
|
|
62
62
|
}
|
|
63
63
|
|
|
64
64
|
function scanDir(dir, projectDir, candidates, depth) {
|
|
65
|
-
if (depth > 3) return; //
|
|
65
|
+
if (depth > 3) return; // Max 3 levels deep
|
|
66
66
|
|
|
67
67
|
let entries;
|
|
68
68
|
try {
|
|
@@ -81,10 +81,10 @@ function scanDir(dir, projectDir, candidates, depth) {
|
|
|
81
81
|
continue;
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
-
//
|
|
84
|
+
// Only scan markdown files
|
|
85
85
|
if (extname(entry.name).toLowerCase() !== '.md') continue;
|
|
86
86
|
|
|
87
|
-
//
|
|
87
|
+
// Calculate match score
|
|
88
88
|
const score = scorePRDMatch(entry.name, fullPath);
|
|
89
89
|
if (score > 0) {
|
|
90
90
|
candidates.push({
|
|
@@ -101,21 +101,21 @@ function scorePRDMatch(filename, fullPath) {
|
|
|
101
101
|
let score = 0;
|
|
102
102
|
const lower = filename.toLowerCase();
|
|
103
103
|
|
|
104
|
-
//
|
|
104
|
+
// Filename pattern matching
|
|
105
105
|
for (let i = 0; i < PRD_PATTERNS.length; i++) {
|
|
106
106
|
if (PRD_PATTERNS[i].test(filename)) {
|
|
107
|
-
score += (PRD_PATTERNS.length - i) * 10; //
|
|
107
|
+
score += (PRD_PATTERNS.length - i) * 10; // Higher priority patterns get higher scores
|
|
108
108
|
}
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
-
//
|
|
111
|
+
// File size bonus (PRDs are typically longer)
|
|
112
112
|
try {
|
|
113
113
|
const stat = statSync(fullPath);
|
|
114
114
|
if (stat.size > 5000) score += 5; // > 5KB
|
|
115
115
|
if (stat.size > 20000) score += 5; // > 20KB
|
|
116
116
|
} catch {}
|
|
117
117
|
|
|
118
|
-
//
|
|
118
|
+
// Content sampling (read first 2000 chars)
|
|
119
119
|
if (score > 0) {
|
|
120
120
|
try {
|
|
121
121
|
const content = readFileSync(fullPath, 'utf-8').slice(0, 2000);
|
|
@@ -130,7 +130,7 @@ function scorePRDMatch(filename, fullPath) {
|
|
|
130
130
|
}
|
|
131
131
|
|
|
132
132
|
/**
|
|
133
|
-
*
|
|
133
|
+
* Read PRD file content
|
|
134
134
|
*/
|
|
135
135
|
export function readPRD(prdPath) {
|
|
136
136
|
return readFileSync(prdPath, 'utf-8');
|