@jojonax/codex-copilot 1.2.1 → 1.3.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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/commands/run.js +83 -51
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jojonax/codex-copilot",
3
- "version": "1.2.1",
3
+ "version": "1.3.0",
4
4
  "description": "PRD-driven automated development orchestrator for CodeX / Cursor",
5
5
  "bin": {
6
6
  "codex-copilot": "./bin/cli.js"
@@ -10,7 +10,7 @@ import { resolve } from 'path';
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
- import { ask, closePrompt } from '../utils/prompt.js';
13
+ import { closePrompt } from '../utils/prompt.js';
14
14
  import { createCheckpoint } from '../utils/checkpoint.js';
15
15
  import { provider } from '../utils/provider.js';
16
16
 
@@ -162,6 +162,18 @@ export async function run(projectDir) {
162
162
  log.dim('⏩ Skipping merge phase (already merged)');
163
163
  }
164
164
 
165
+ // Check if task was blocked during review/merge
166
+ if (task.status === 'blocked') {
167
+ writeJSON(tasksPath, tasks);
168
+ log.blank();
169
+ log.warn(`⚠ Task #${task.id} is blocked — needs manual intervention`);
170
+ log.blank();
171
+ const done = tasks.tasks.filter(t => t.status === 'completed').length;
172
+ const blocked = tasks.tasks.filter(t => t.status === 'blocked').length;
173
+ progressBar(done, tasks.total, `${done}/${tasks.total} done, ${blocked} blocked`);
174
+ continue;
175
+ }
176
+
165
177
  // Mark task complete
166
178
  task.status = 'completed';
167
179
  writeJSON(tasksPath, tasks);
@@ -282,7 +294,9 @@ async function prPhase(projectDir, task, baseBranch, checkpoint) {
282
294
  // Phase 3: Review loop
283
295
  // ──────────────────────────────────────────────
284
296
  async function reviewLoop(projectDir, task, prInfo, { maxRounds: _maxRounds, pollInterval, waitTimeout }, checkpoint, providerId) {
285
- let maxRounds = _maxRounds;
297
+ const HARD_MAX_ROUNDS = 5;
298
+ const MAX_POLL_RETRIES = 3;
299
+ let maxRounds = Math.min(_maxRounds, HARD_MAX_ROUNDS);
286
300
  log.step('Phase 3/4: Waiting for review');
287
301
 
288
302
  // Resume from saved review round
@@ -291,6 +305,8 @@ async function reviewLoop(projectDir, task, prInfo, { maxRounds: _maxRounds, pol
291
305
  ? state.review_round
292
306
  : 1;
293
307
 
308
+ let pollRetries = 0;
309
+
294
310
  for (let round = startRound; round <= maxRounds; round++) {
295
311
  // Step: Waiting for review
296
312
  checkpoint.saveStep(task.id, 'review', 'waiting_review', {
@@ -299,40 +315,38 @@ async function reviewLoop(projectDir, task, prInfo, { maxRounds: _maxRounds, pol
299
315
  review_round: round,
300
316
  });
301
317
 
302
- log.info(`Waiting for review feedback... (timeout: ${waitTimeout}s)`);
303
-
304
- // When resuming from waiting_review, the review may have already arrived
305
- // before the restart. Check for existing reviews first before polling.
306
- const isResuming = (state.current_task === task.id && state.phase === 'review'
307
- && state.phase_step === 'waiting_review' && round === startRound);
308
-
309
- let gotReview;
310
- if (isResuming) {
311
- const existingReviews = github.getReviews(projectDir, prInfo.number);
312
- const existingComments = github.getIssueComments(projectDir, prInfo.number);
313
- const hasReview = existingReviews.some(r => r.state !== 'PENDING');
314
- const hasBotComment = existingComments.some(c =>
315
- c.user?.type === 'Bot' || c.user?.login?.includes('bot')
316
- );
317
- if (hasReview || hasBotComment) {
318
- log.info('Found existing review — processing immediately');
319
- gotReview = true;
320
- } else {
321
- gotReview = await waitForReview(projectDir, prInfo.number, pollInterval, waitTimeout);
322
- }
318
+ log.info('Checking for review feedback...');
319
+
320
+ // Always proactively check for existing reviews first.
321
+ let gotReview = false;
322
+ const existingReviews = github.getReviews(projectDir, prInfo.number);
323
+ const existingComments = github.getIssueComments(projectDir, prInfo.number);
324
+ const hasReview = existingReviews.some(r => r.state !== 'PENDING');
325
+ const hasBotComment = existingComments.some(c =>
326
+ c.user?.type === 'Bot' || c.user?.login?.includes('bot')
327
+ );
328
+
329
+ if (hasReview || hasBotComment) {
330
+ log.info('Review found processing immediately');
331
+ gotReview = true;
323
332
  } else {
333
+ // No reviews yet — enter polling mode
334
+ log.info(`No review yet, polling... (timeout: ${waitTimeout}s)`);
324
335
  gotReview = await waitForReview(projectDir, prInfo.number, pollInterval, waitTimeout);
325
336
  }
326
337
 
327
338
  if (!gotReview) {
328
- log.warn('Review wait timed out');
329
- const action = await ask('Enter "skip" (skip review) or "wait" (keep waiting):');
330
- if (action === 'wait') {
331
- round--;
332
- continue;
339
+ pollRetries++;
340
+ if (pollRetries >= MAX_POLL_RETRIES) {
341
+ log.error(`Review polling timed out ${MAX_POLL_RETRIES} times — marking task as blocked`);
342
+ task.status = 'blocked';
343
+ return;
333
344
  }
334
- break;
345
+ log.warn(`Review wait timed out — auto-retrying (${pollRetries}/${MAX_POLL_RETRIES})...`);
346
+ round--; // retry same round
347
+ continue;
335
348
  }
349
+ pollRetries = 0; // reset on success
336
350
 
337
351
  // Step: Feedback received
338
352
  checkpoint.saveStep(task.id, 'review', 'feedback_received', {
@@ -373,40 +387,37 @@ async function reviewLoop(projectDir, task, prInfo, { maxRounds: _maxRounds, pol
373
387
  const hasInlineComments = inlineComments && inlineComments.length > 0;
374
388
 
375
389
  if (reviewState === 'CHANGES_REQUESTED') {
376
- // Explicit change request — always treat as needing fixes
377
390
  log.info('Review state: CHANGES_REQUESTED — entering fix phase');
378
391
  } else if (reviewState === 'COMMENTED' && !hasInlineComments) {
379
- // General comment with no code-level feedback — treat as passing
380
392
  log.info('COMMENTED with no inline code comments — treating as passed ✅');
381
393
  return;
382
394
  } else if (!hasInlineComments) {
383
- // Any other state (PENDING, null, etc.) with no inline comments — treat as passing
384
395
  log.info('No inline code comments found — treating as passed ✅');
385
396
  return;
386
397
  } else {
387
- // Has inline comments — treat as needing fixes
388
398
  log.info(`Found ${inlineComments.length} inline code comment(s) — entering fix phase`);
389
399
  }
390
400
  }
391
401
 
392
402
  // AI says FIX, or structural fallback indicates issues
393
-
394
403
  log.blank();
395
404
  log.warn(`Received review feedback (round ${round}/${maxRounds})`);
396
405
 
397
406
  if (round >= maxRounds) {
398
- log.warn(`Max fix rounds reached (${maxRounds})`);
399
- const choice = await ask('Enter "merge" (force merge) / "fix" (one more round) / "skip" (skip):');
400
- if (choice === 'fix') {
407
+ if (maxRounds < HARD_MAX_ROUNDS) {
408
+ // Auto-extend: give it one more round
401
409
  maxRounds++;
402
- } else if (choice === 'skip') {
403
- return;
410
+ log.warn(`Auto-extending fix rounds to ${maxRounds}`);
404
411
  } else {
405
- return; // merge
412
+ // Hard limit reached — cannot keep fixing forever
413
+ log.error(`Hard limit of ${HARD_MAX_ROUNDS} fix rounds reached — marking task as blocked`);
414
+ log.error('This task needs manual intervention to resolve review issues');
415
+ task.status = 'blocked';
416
+ return;
406
417
  }
407
418
  }
408
419
 
409
- // Let AI fix
420
+ // Let AI fix based on the specific review feedback
410
421
  await fixPhase(projectDir, task, feedback, round, providerId);
411
422
 
412
423
  // Step: Fix applied
@@ -422,10 +433,12 @@ async function reviewLoop(projectDir, task, prInfo, { maxRounds: _maxRounds, pol
422
433
  git.pushBranch(projectDir, task.branch);
423
434
  log.info('Fix pushed, waiting for next review round...');
424
435
  // Brief wait for review bot to react
425
- await sleep(10000);
436
+ await sleep(15000);
426
437
  } else {
427
- // No actual changes were made by the fix the AI determined nothing needed fixing
428
- log.info('No changes needed after reviewproceeding to merge ');
438
+ // AI fix produced no code changesit cannot resolve this issue
439
+ log.error('AI fix produced no changesmarking task as blocked');
440
+ log.error('This task needs manual code changes to resolve review issues');
441
+ task.status = 'blocked';
429
442
  return;
430
443
  }
431
444
  }
@@ -491,15 +504,34 @@ ${feedback}
491
504
  async function mergePhase(projectDir, task, prInfo, baseBranch, checkpoint) {
492
505
  log.step('Phase 4/4: Merge PR');
493
506
 
507
+ // Skip merge if task was blocked during review
508
+ if (task.status === 'blocked') {
509
+ log.warn(`Task #${task.id} is blocked — skipping merge`);
510
+ return;
511
+ }
512
+
494
513
  log.info(`Auto-merging PR #${prInfo.number}...`);
495
514
 
496
- try {
497
- github.mergePR(projectDir, prInfo.number);
498
- log.info(`PR #${prInfo.number} merged ✅`);
499
- } catch (err) {
500
- log.error(`Merge failed: ${err.message}`);
501
- log.warn('Please merge manually, then press Enter to continue');
502
- await ask('Continue...');
515
+ let merged = false;
516
+ for (let attempt = 1; attempt <= 3; attempt++) {
517
+ try {
518
+ github.mergePR(projectDir, prInfo.number);
519
+ log.info(`PR #${prInfo.number} merged ✅`);
520
+ merged = true;
521
+ break;
522
+ } catch (err) {
523
+ log.warn(`Merge attempt ${attempt}/3 failed: ${err.message}`);
524
+ if (attempt < 3) {
525
+ log.info('Retrying in 10s...');
526
+ await sleep(10000);
527
+ }
528
+ }
529
+ }
530
+
531
+ if (!merged) {
532
+ log.error('Merge failed after 3 attempts — marking task as blocked');
533
+ task.status = 'blocked';
534
+ return;
503
535
  }
504
536
 
505
537
  checkpoint.saveStep(task.id, 'merge', 'merged', {