@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.
@@ -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 = readJSON(statePath);
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(state.current_task, tasks.total, `${state.current_task}/${tasks.total} tasks done`);
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; // Skip completed
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
- await developPhase(projectDir, task, baseBranch);
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
- const prInfo = await prPhase(projectDir, task, baseBranch);
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
- await reviewLoop(projectDir, task, prInfo, {
93
- maxRounds: maxReviewRounds,
94
- pollInterval,
95
- waitTimeout,
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
- await mergePhase(projectDir, task, prInfo, baseBranch);
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
- // Update task state
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} tasks done`);
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
- git.checkoutBranch(projectDir, task.branch, baseBranch);
128
- log.info(`Switched to branch: ${task.branch}`);
129
-
130
- // Build development prompt
131
- const devPrompt = buildDevPrompt(task);
132
-
133
- // Check if CodeX CLI is available
134
- const codexAvailable = isCodexAvailable();
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
- // Manual mode: display prompt, wait for user confirmation
157
- log.blank();
158
- console.log(' ┌─── Paste the following into CodeX Desktop to execute ───┐');
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
- // Also save prompt to file for easy copying
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
- await ask('Press Enter after CodeX development is complete...');
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
- // Ensure changes are committed
184
- if (!git.isClean(projectDir)) {
185
- log.info('Uncommitted changes detected, auto-committing...');
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
- // Push
190
- git.pushBranch(projectDir, task.branch);
191
- log.info('Code pushed');
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
- 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*`;
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
- try {
200
- const prInfo = github.createPR(projectDir, {
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 created: ${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
+ });
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
- for (let round = 1; round <= maxRounds; round++) {
229
- // Wait for review
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 state = github.getLatestReviewState(projectDir, prInfo.number);
245
- log.info(`Review status: ${state}`);
323
+ const reviewState = github.getLatestReviewState(projectDir, prInfo.number);
324
+ log.info(`Review status: ${reviewState}`);
246
325
 
247
- if (state === 'APPROVED') {
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 CodeX fix
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 = `The following are PR review comments. Please fix each one:
409
+ const fixPrompt = `Please fix the following review comments on Task #${task.id}: ${task.title}
324
410
 
325
- ## Review Comments
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 prompt user
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
- // Try CodeX CLI
340
- const codexAvailable = isCodexAvailable();
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
- function copyToClipboard(text) {
430
- try {
431
- execSync('pbcopy', { input: text, stdio: ['pipe', 'pipe', 'pipe'] });
432
- log.info('📋 Copied to clipboard');
433
- } catch {}
434
- }
529
+
@@ -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 statePath = resolve(projectDir, '.codex-copilot/state.json');
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 = JSON.parse(readFileSync(statePath, 'utf-8'));
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.current_pr) {
44
- log.info(`Current PR: #${state.current_pr} (review round ${state.review_round})`);
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
- console.log(` ${icon} #${task.id} ${task.title} [${task.branch}]`);
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
  }