@jojonax/codex-copilot 1.0.2 → 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.
@@ -1,16 +1,18 @@
1
1
  /**
2
- * codex-copilot run - 主编排循环
2
+ * codex-copilot run - Main orchestration loop
3
3
  *
4
- * 逐个任务执行: 开发 → PR → Review → 修复合并下一个
4
+ * Executes tasks one by one: Develop → PR → Review → FixMergeNext
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,214 +24,287 @@ 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 = readJSON(statePath);
34
+ state = checkpoint.load();
33
35
  } catch (err) {
34
- log.error(`读取任务/状态文件失败: ${err.message}`);
35
- log.warn('文件可能已损坏,请运行 codex-copilot reset');
36
+ log.error(`Failed to read task/state files: ${err.message}`);
37
+ log.warn('Files may be corrupted. Run: codex-copilot reset');
36
38
  closePrompt();
37
39
  process.exit(1);
38
40
  }
39
41
  const config = existsSync(configPath) ? readJSON(configPath) : {};
40
42
 
41
- // 校验 tasks.json 结构
43
+ // Validate tasks.json structure
42
44
  if (!tasks.tasks || !Array.isArray(tasks.tasks) || !tasks.total) {
43
- log.error('tasks.json 格式无效: 缺少 tasks 数组或 total 字段');
44
- log.warn('请重新运行 codex-copilot init 并让 CodeX 重新生成 tasks.json');
45
+ log.error('Invalid tasks.json format: missing tasks array or total field');
46
+ log.warn('Please re-run codex-copilot init and let CodeX regenerate tasks.json');
45
47
  closePrompt();
46
48
  process.exit(1);
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
 
54
- log.title('🚀 开始自动化开发循环');
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
+
80
+ log.title('🚀 Starting automated development loop');
55
81
  log.blank();
56
- log.info(`项目: ${tasks.project}`);
57
- log.info(`总任务数: ${tasks.total}`);
58
- log.info(`已完成: ${state.current_task}`);
59
- log.info(`基础分支: ${baseBranch}`);
82
+ log.info(`Project: ${tasks.project}`);
83
+ log.info(`Total tasks: ${tasks.total}`);
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(state.current_task, tasks.total, `${state.current_task}/${tasks.total} 任务完成`);
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
- if (task.id <= state.current_task) continue; // 跳过已完成的
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
- log.title(`━━━ 任务 ${task.id}/${tasks.total}: ${task.title} ━━━`);
110
+ log.title(`━━━ Task ${task.id}/${tasks.total}: ${task.title} ━━━`);
71
111
  log.blank();
72
112
 
73
- // 检查依赖是否已完成
113
+ // Check dependencies
74
114
  if (task.depends_on && task.depends_on.length > 0) {
75
115
  const unfinished = task.depends_on.filter(dep => {
76
116
  const depTask = tasks.tasks.find(t => t.id === dep);
77
117
  return depTask && depTask.status !== 'completed';
78
118
  });
79
119
  if (unfinished.length > 0) {
80
- log.warn(`依赖任务未完成: ${unfinished.join(', ')},跳过`);
120
+ log.warn(`Unfinished dependencies: ${unfinished.join(', ')} — skipping`);
81
121
  continue;
82
122
  }
83
123
  }
84
124
 
85
- // ===== 阶段 1: 开发 =====
86
- await developPhase(projectDir, task, baseBranch);
125
+ // Mark task as in_progress
126
+ task.status = 'in_progress';
127
+ writeJSON(tasksPath, tasks);
87
128
 
88
- // ===== 阶段 2: 创建 PR =====
89
- const prInfo = await prPhase(projectDir, task, baseBranch);
129
+ // ===== Phase 1: Develop =====
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
+ }
90
135
 
91
- // ===== 阶段 3: Review 循环 =====
92
- await reviewLoop(projectDir, task, prInfo, {
93
- maxRounds: maxReviewRounds,
94
- pollInterval,
95
- waitTimeout,
96
- });
136
+ // ===== Phase 2: Create PR =====
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
+ }
146
+
147
+ // ===== Phase 3: Review loop =====
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
- // ===== 阶段 4: 合并 =====
99
- await mergePhase(projectDir, task, prInfo, baseBranch);
158
+ // ===== Phase 4: Merge =====
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
- writeJSON(statePath, state);
168
+ checkpoint.completeTask(task.id);
108
169
 
109
170
  log.blank();
110
- progressBar(task.id, tasks.total, `${task.id}/${tasks.total} 任务完成`);
111
- log.info(`✅ 任务 #${task.id} 完成!`);
171
+ const done = tasks.tasks.filter(t => t.status === 'completed').length;
172
+ progressBar(done, tasks.total, `${done}/${tasks.total} tasks done`);
173
+ log.info(`✅ Task #${task.id} complete!`);
112
174
  }
113
175
 
114
176
  log.blank();
115
- log.title('🎉 所有任务已完成!');
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
- // 阶段 1: 开发
185
+ // Phase 1: Develop
122
186
  // ──────────────────────────────────────────────
123
- async function developPhase(projectDir, task, baseBranch) {
124
- log.step('阶段 1/4: 开发');
125
-
126
- // 切换到 feature 分支
127
- git.checkoutBranch(projectDir, task.branch, baseBranch);
128
- log.info(`已切换到分支: ${task.branch}`);
129
-
130
- // 构建开发 Prompt
131
- const devPrompt = buildDevPrompt(task);
132
-
133
- // 检查是否有 CodeX CLI
134
- const codexAvailable = isCodexAvailable();
135
-
136
- if (codexAvailable) {
137
- log.info('检测到 CodeX CLI,自动执行...');
138
- const autoRun = await confirm('自动调用 CodeX 开发?');
139
- if (autoRun) {
140
- try {
141
- const promptPath = resolve(projectDir, '.codex-copilot/_current_prompt.md');
142
- writeFileSync(promptPath, devPrompt);
143
- execSync(`codex -q --file .codex-copilot/_current_prompt.md`, {
144
- cwd: projectDir,
145
- stdio: 'inherit',
146
- });
147
- log.info('CodeX 开发完成');
148
- return;
149
- } catch (err) {
150
- log.warn(`CodeX CLI 调用失败: ${err.message}`);
151
- log.warn('回退到手动模式');
152
- }
187
+ async function developPhase(projectDir, task, baseBranch, checkpoint, providerId) {
188
+ log.step('Phase 1/4: Develop');
189
+
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
- // 手动模式:显示 Prompt,等待用户确认
157
- log.blank();
158
- console.log(' ┌─── 请将以下内容粘贴到 CodeX 桌面版中执行 ───┐');
159
- log.blank();
160
- console.log(devPrompt.split('\n').map(l => ` │ ${l}`).join('\n'));
161
- log.blank();
162
- console.log(' └──────────────────────────────────────────────┘');
163
- log.blank();
164
-
165
- // 同时将 Prompt 保存到文件,方便复制
166
- const promptPath = resolve(projectDir, '.codex-copilot/_current_prompt.md');
167
- writeFileSync(promptPath, devPrompt);
168
- log.dim(`Prompt 已保存到 .codex-copilot/_current_prompt.md (可直接复制文件内容)`);
169
- log.blank();
170
-
171
- // 尝试复制到剪贴板(通过 stdin 传入,避免 shell 注入)
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
- await ask('CodeX 开发完成后按 Enter 继续...');
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
- // 阶段 2: 创建 PR
227
+ // Phase 2: Create PR
179
228
  // ──────────────────────────────────────────────
180
- async function prPhase(projectDir, task, baseBranch) {
181
- log.step('阶段 2/4: 提交 PR');
229
+ async function prPhase(projectDir, task, baseBranch, checkpoint) {
230
+ log.step('Phase 2/4: Submit PR');
182
231
 
183
- // 确保变更已提交
184
- if (!git.isClean(projectDir)) {
185
- log.info('检测到未提交的变更,自动提交...');
186
- git.commitAll(projectDir, `feat(task-${task.id}): ${task.title}`);
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
- // 推送
190
- git.pushBranch(projectDir, task.branch);
191
- log.info('代码已推送');
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
- // 创建 PR
194
- const prBody = `## 任务 #${task.id}: ${task.title}\n\n${task.description}\n\n### 验收标准\n${task.acceptance.map(a => `- [ ] ${a}`).join('\n')}\n\n---\n*由 Codex-Copilot 自动创建*`;
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*`;
195
259
 
196
- try {
197
- const prInfo = github.createPR(projectDir, {
260
+ // Use auto-recovery: create → find existing → fix remote → retry
261
+ const prInfo = github.createPRWithRecovery(projectDir, {
198
262
  title: `feat(task-${task.id}): ${task.title}`,
199
263
  body: prBody,
200
264
  base: baseBranch,
201
265
  head: task.branch,
202
266
  });
203
- log.info(`PR 已创建: ${prInfo.url}`);
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
+ });
204
273
  return prInfo;
205
- } catch (err) {
206
- // PR 可能已存在
207
- log.warn(`创建 PR 异常: ${err.message}`);
208
- const prNumber = await ask('请输入已存在的 PR 编号:');
209
- const parsed = parseInt(prNumber, 10);
210
- if (isNaN(parsed) || parsed <= 0) {
211
- log.error('无效的 PR 编号');
212
- throw new Error('无效的 PR 编号');
213
- }
214
- return { number: parsed, url: '' };
215
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: '' };
216
279
  }
217
280
 
218
281
  // ──────────────────────────────────────────────
219
- // 阶段 3: Review 循环
282
+ // Phase 3: Review loop
220
283
  // ──────────────────────────────────────────────
221
- async function reviewLoop(projectDir, task, prInfo, { maxRounds: _maxRounds, pollInterval, waitTimeout }) {
284
+ async function reviewLoop(projectDir, task, prInfo, { maxRounds: _maxRounds, pollInterval, waitTimeout }, checkpoint, providerId) {
222
285
  let maxRounds = _maxRounds;
223
- log.step('阶段 3/4: 等待 Review');
286
+ log.step('Phase 3/4: Waiting for review');
287
+
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
+ });
224
301
 
225
- for (let round = 1; round <= maxRounds; round++) {
226
- // 等待 Review
227
- log.info(`等待 Review 意见... (超时: ${waitTimeout}s)`);
302
+ log.info(`Waiting for review feedback... (timeout: ${waitTimeout}s)`);
228
303
  const gotReview = await waitForReview(projectDir, prInfo.number, pollInterval, waitTimeout);
229
304
 
230
305
  if (!gotReview) {
231
- log.warn('等待 Review 超时');
232
- const action = await ask('输入 skip (跳过Review) wait (继续等待):');
306
+ log.warn('Review wait timed out');
307
+ const action = await ask('Enter "skip" (skip review) or "wait" (keep waiting):');
233
308
  if (action === 'wait') {
234
309
  round--;
235
310
  continue;
@@ -237,28 +312,35 @@ async function reviewLoop(projectDir, task, prInfo, { maxRounds: _maxRounds, pol
237
312
  break;
238
313
  }
239
314
 
240
- // 检查 Review 状态
241
- const state = github.getLatestReviewState(projectDir, prInfo.number);
242
- log.info(`Review 状态: ${state}`);
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
+
322
+ // Check review status
323
+ const reviewState = github.getLatestReviewState(projectDir, prInfo.number);
324
+ log.info(`Review status: ${reviewState}`);
243
325
 
244
- if (state === 'APPROVED') {
245
- log.info('✅ Review 已通过!');
326
+ if (reviewState === 'APPROVED') {
327
+ log.info('✅ Review approved!');
246
328
  return;
247
329
  }
248
330
 
249
- // 收集 Review 反馈
331
+ // Collect review feedback
250
332
  const feedback = github.collectReviewFeedback(projectDir, prInfo.number);
251
333
  if (!feedback) {
252
- log.info('未发现具体修改意见,继续');
334
+ log.info('No specific change requests found, proceeding');
253
335
  return;
254
336
  }
255
337
 
256
338
  log.blank();
257
- log.warn(`收到 Review 意见 ( ${round}/${maxRounds})`);
339
+ log.warn(`Received review feedback (round ${round}/${maxRounds})`);
258
340
 
259
341
  if (round >= maxRounds) {
260
- log.warn(`已达最大修复轮次 (${maxRounds})`);
261
- const choice = await ask('输入 merge (强制合并) / fix (再修一轮) / skip (跳过):');
342
+ log.warn(`Max fix rounds reached (${maxRounds})`);
343
+ const choice = await ask('Enter "merge" (force merge) / "fix" (one more round) / "skip" (skip):');
262
344
  if (choice === 'fix') {
263
345
  maxRounds++;
264
346
  } else if (choice === 'skip') {
@@ -268,17 +350,24 @@ async function reviewLoop(projectDir, task, prInfo, { maxRounds: _maxRounds, pol
268
350
  }
269
351
  }
270
352
 
271
- // CodeX 修复
272
- 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
+ });
273
362
 
274
- // 推送修复
363
+ // Push fix
275
364
  if (!git.isClean(projectDir)) {
276
365
  git.commitAll(projectDir, `fix(task-${task.id}): address review comments (round ${round})`);
277
366
  }
278
367
  git.pushBranch(projectDir, task.branch);
279
- log.info('修复已推送,等待新一轮 Review...');
368
+ log.info('Fix pushed, waiting for next review round...');
280
369
 
281
- // 稍等一下让 Review bot 反应
370
+ // Brief wait for review bot to react
282
371
  await sleep(10000);
283
372
  }
284
373
  }
@@ -294,7 +383,7 @@ async function waitForReview(projectDir, prNumber, pollInterval, timeout) {
294
383
  const currentReviews = github.getReviews(projectDir, prNumber);
295
384
  const currentComments = github.getIssueComments(projectDir, prNumber);
296
385
 
297
- // 检查是否有新的 Review Bot 评论
386
+ // Check for new reviews or bot comments
298
387
  const hasNewReview = currentReviews.length > startReviewCount;
299
388
  const hasBotComment = currentComments.some(c =>
300
389
  (c.user?.type === 'Bot' || c.user?.login?.includes('bot')) &&
@@ -312,95 +401,121 @@ async function waitForReview(projectDir, prNumber, pollInterval, timeout) {
312
401
  }
313
402
 
314
403
  // ──────────────────────────────────────────────
315
- // 修复阶段
404
+ // Fix phase
316
405
  // ──────────────────────────────────────────────
317
- async function fixPhase(projectDir, task, feedback, round) {
318
- log.step(`修复 Review 意见 ( ${round})`);
406
+ async function fixPhase(projectDir, task, feedback, round, providerId) {
407
+ log.step(`Fixing review comments (round ${round})`);
319
408
 
320
- const fixPrompt = `以下是 PR Review 的意见,请逐一修复:
409
+ const fixPrompt = `Please fix the following review comments on Task #${task.id}: ${task.title}
321
410
 
322
- ## Review 意见
411
+ ## Review Feedback
323
412
  ${feedback}
324
413
 
325
- ## 要求
326
- 1. 逐条修复上述问题
327
- 2. 建议性意见可以不修复,但在 commit message 中说明原因
328
- 3. 修复后确保不引入新问题
329
- 4. 完成后执行: git add -A && git commit -m "fix(task-${task.id}): address review round ${round}"
414
+ ## Requirements
415
+ 1. Fix each issue listed above
416
+ 2. Suggestions (non-blocking) can be skipped — explain why in the commit message
417
+ 3. Ensure fixes don't introduce new issues
418
+ 4. When done, run: git add -A && git commit -m "fix(task-${task.id}): address review round ${round}"
330
419
  `;
331
420
 
332
- // 保存到文件并提示用户
421
+ // Save to file and execute via provider
333
422
  const promptPath = resolve(projectDir, '.codex-copilot/_current_prompt.md');
334
423
  writeFileSync(promptPath, fixPrompt);
335
424
 
336
- // 尝试 CodeX CLI
337
- const codexAvailable = isCodexAvailable();
338
-
339
- if (codexAvailable) {
340
- const autoFix = await confirm('自动调用 CodeX 修复?');
341
- if (autoFix) {
342
- try {
343
- execSync(`codex -q --file .codex-copilot/_current_prompt.md`, {
344
- cwd: projectDir,
345
- stdio: 'inherit',
346
- });
347
- log.info('CodeX 修复完成');
348
- return;
349
- } catch {
350
- log.warn('CodeX CLI 调用失败,回退到手动模式');
351
- }
352
- }
353
- }
354
-
355
- // 手动模式
356
- log.blank();
357
- log.dim(`Review 修复 Prompt 已保存到 .codex-copilot/_current_prompt.md`);
358
- log.dim('请将文件内容粘贴到 CodeX 执行');
359
-
360
- copyToClipboard(fixPrompt);
361
-
362
- await ask('CodeX 修复完成后按 Enter 继续...');
425
+ await provider.executePrompt(providerId, promptPath, projectDir);
426
+ log.info('Fix complete');
363
427
  }
364
428
 
365
429
  // ──────────────────────────────────────────────
366
- // 阶段 4: 合并
430
+ // Phase 4: Merge
367
431
  // ──────────────────────────────────────────────
368
- async function mergePhase(projectDir, task, prInfo, baseBranch) {
369
- log.step('阶段 4/4: 合并 PR');
432
+ async function mergePhase(projectDir, task, prInfo, baseBranch, checkpoint) {
433
+ log.step('Phase 4/4: Merge PR');
370
434
 
371
- const doMerge = await confirm(`合并 PR #${prInfo.number}?`);
435
+ const doMerge = await confirm(`Merge PR #${prInfo.number}?`);
372
436
  if (!doMerge) {
373
- log.warn('用户跳过合并');
437
+ log.warn('User skipped merge');
374
438
  return;
375
439
  }
376
440
 
377
441
  try {
378
442
  github.mergePR(projectDir, prInfo.number);
379
- log.info(`PR #${prInfo.number} 已合并 ✅`);
443
+ log.info(`PR #${prInfo.number} merged ✅`);
380
444
  } catch (err) {
381
- log.error(`合并失败: ${err.message}`);
382
- log.warn('请手动合并后按 Enter 继续');
383
- await ask('继续...');
445
+ log.error(`Merge failed: ${err.message}`);
446
+ log.warn('Please merge manually, then press Enter to continue');
447
+ await ask('Continue...');
384
448
  }
385
449
 
386
- // 切回主分支
450
+ checkpoint.saveStep(task.id, 'merge', 'merged', {
451
+ branch: task.branch,
452
+ current_pr: prInfo.number,
453
+ });
454
+
455
+ // Switch back to main branch
387
456
  git.checkoutMain(projectDir, baseBranch);
388
457
  }
389
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
+
390
502
  function buildDevPrompt(task) {
391
- return `请完成以下开发任务:
503
+ const acceptanceList = Array.isArray(task.acceptance) && task.acceptance.length > 0
504
+ ? task.acceptance.map(a => `- ${a}`).join('\n')
505
+ : '- Feature works correctly';
506
+ return `Please complete the following development task:
392
507
 
393
- ## 任务 #${task.id}: ${task.title}
508
+ ## Task #${task.id}: ${task.title}
394
509
 
395
510
  ${task.description}
396
511
 
397
- ## 验收标准
398
- ${task.acceptance.map(a => `- ${a}`).join('\n')}
512
+ ## Acceptance Criteria
513
+ ${acceptanceList}
399
514
 
400
- ## 要求
401
- 1. 严格按照项目现有的代码规范和技术栈
402
- 2. 完成后确保代码可以正常编译/运行
403
- 3. 完成后执行:
515
+ ## Requirements
516
+ 1. Strictly follow the project's existing code style and tech stack
517
+ 2. Ensure the code compiles/runs correctly when done
518
+ 3. When done, run:
404
519
  git add -A
405
520
  git commit -m "feat(task-${task.id}): ${task.title}"
406
521
  `;
@@ -410,19 +525,5 @@ function sleep(ms) {
410
525
  return new Promise(resolve => setTimeout(resolve, ms));
411
526
  }
412
527
 
413
- function isCodexAvailable() {
414
- try {
415
- const cmd = process.platform === 'win32' ? 'where codex' : 'which codex';
416
- execSync(cmd, { stdio: 'pipe' });
417
- return true;
418
- } catch {
419
- return false;
420
- }
421
- }
422
528
 
423
- function copyToClipboard(text) {
424
- try {
425
- execSync('pbcopy', { input: text, stdio: ['pipe', 'pipe', 'pipe'] });
426
- log.info('📋 已复制到剪贴板');
427
- } catch {}
428
- }
529
+