@jojonax/codex-copilot 1.2.2 → 1.3.1

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jojonax/codex-copilot",
3
- "version": "1.2.2",
3
+ "version": "1.3.1",
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', {
@@ -302,8 +318,6 @@ async function reviewLoop(projectDir, task, prInfo, { maxRounds: _maxRounds, pol
302
318
  log.info('Checking for review feedback...');
303
319
 
304
320
  // Always proactively check for existing reviews first.
305
- // This handles: resume after restart, review arrived before polling starts,
306
- // or any case where the review is already available.
307
321
  let gotReview = false;
308
322
  const existingReviews = github.getReviews(projectDir, prInfo.number);
309
323
  const existingComments = github.getIssueComments(projectDir, prInfo.number);
@@ -322,14 +336,17 @@ async function reviewLoop(projectDir, task, prInfo, { maxRounds: _maxRounds, pol
322
336
  }
323
337
 
324
338
  if (!gotReview) {
325
- log.warn('Review wait timed out');
326
- const action = await ask('Enter "skip" (skip review) or "wait" (keep waiting):');
327
- if (action === 'wait') {
328
- round--;
329
- 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;
330
344
  }
331
- break;
345
+ log.warn(`Review wait timed out — auto-retrying (${pollRetries}/${MAX_POLL_RETRIES})...`);
346
+ round--; // retry same round
347
+ continue;
332
348
  }
349
+ pollRetries = 0; // reset on success
333
350
 
334
351
  // Step: Feedback received
335
352
  checkpoint.saveStep(task.id, 'review', 'feedback_received', {
@@ -370,40 +387,37 @@ async function reviewLoop(projectDir, task, prInfo, { maxRounds: _maxRounds, pol
370
387
  const hasInlineComments = inlineComments && inlineComments.length > 0;
371
388
 
372
389
  if (reviewState === 'CHANGES_REQUESTED') {
373
- // Explicit change request — always treat as needing fixes
374
390
  log.info('Review state: CHANGES_REQUESTED — entering fix phase');
375
391
  } else if (reviewState === 'COMMENTED' && !hasInlineComments) {
376
- // General comment with no code-level feedback — treat as passing
377
392
  log.info('COMMENTED with no inline code comments — treating as passed ✅');
378
393
  return;
379
394
  } else if (!hasInlineComments) {
380
- // Any other state (PENDING, null, etc.) with no inline comments — treat as passing
381
395
  log.info('No inline code comments found — treating as passed ✅');
382
396
  return;
383
397
  } else {
384
- // Has inline comments — treat as needing fixes
385
398
  log.info(`Found ${inlineComments.length} inline code comment(s) — entering fix phase`);
386
399
  }
387
400
  }
388
401
 
389
402
  // AI says FIX, or structural fallback indicates issues
390
-
391
403
  log.blank();
392
404
  log.warn(`Received review feedback (round ${round}/${maxRounds})`);
393
405
 
394
406
  if (round >= maxRounds) {
395
- log.warn(`Max fix rounds reached (${maxRounds})`);
396
- const choice = await ask('Enter "merge" (force merge) / "fix" (one more round) / "skip" (skip):');
397
- if (choice === 'fix') {
407
+ if (maxRounds < HARD_MAX_ROUNDS) {
408
+ // Auto-extend: give it one more round
398
409
  maxRounds++;
399
- } else if (choice === 'skip') {
400
- return;
410
+ log.warn(`Auto-extending fix rounds to ${maxRounds}`);
401
411
  } else {
402
- 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;
403
417
  }
404
418
  }
405
419
 
406
- // Let AI fix
420
+ // Let AI fix based on the specific review feedback
407
421
  await fixPhase(projectDir, task, feedback, round, providerId);
408
422
 
409
423
  // Step: Fix applied
@@ -415,14 +429,17 @@ async function reviewLoop(projectDir, task, prInfo, { maxRounds: _maxRounds, pol
415
429
 
416
430
  // Push fix — only if there are actual changes
417
431
  if (!git.isClean(projectDir)) {
418
- git.commitAll(projectDir, `fix(task-${task.id}): address review comments (round ${round})`);
432
+ const skipCI = github.isPrivateRepo(projectDir) ? ' [skip ci]' : '';
433
+ git.commitAll(projectDir, `fix(task-${task.id}): address review comments (round ${round})${skipCI}`);
419
434
  git.pushBranch(projectDir, task.branch);
420
435
  log.info('Fix pushed, waiting for next review round...');
421
436
  // Brief wait for review bot to react
422
- await sleep(10000);
437
+ await sleep(15000);
423
438
  } else {
424
- // No actual changes were made by the fix the AI determined nothing needed fixing
425
- log.info('No changes needed after reviewproceeding to merge ');
439
+ // AI fix produced no code changesit cannot resolve this issue
440
+ log.error('AI fix produced no changesmarking task as blocked');
441
+ log.error('This task needs manual code changes to resolve review issues');
442
+ task.status = 'blocked';
426
443
  return;
427
444
  }
428
445
  }
@@ -488,15 +505,34 @@ ${feedback}
488
505
  async function mergePhase(projectDir, task, prInfo, baseBranch, checkpoint) {
489
506
  log.step('Phase 4/4: Merge PR');
490
507
 
508
+ // Skip merge if task was blocked during review
509
+ if (task.status === 'blocked') {
510
+ log.warn(`Task #${task.id} is blocked — skipping merge`);
511
+ return;
512
+ }
513
+
491
514
  log.info(`Auto-merging PR #${prInfo.number}...`);
492
515
 
493
- try {
494
- github.mergePR(projectDir, prInfo.number);
495
- log.info(`PR #${prInfo.number} merged ✅`);
496
- } catch (err) {
497
- log.error(`Merge failed: ${err.message}`);
498
- log.warn('Please merge manually, then press Enter to continue');
499
- await ask('Continue...');
516
+ let merged = false;
517
+ for (let attempt = 1; attempt <= 3; attempt++) {
518
+ try {
519
+ github.mergePR(projectDir, prInfo.number);
520
+ log.info(`PR #${prInfo.number} merged ✅`);
521
+ merged = true;
522
+ break;
523
+ } catch (err) {
524
+ log.warn(`Merge attempt ${attempt}/3 failed: ${err.message}`);
525
+ if (attempt < 3) {
526
+ log.info('Retrying in 10s...');
527
+ await sleep(10000);
528
+ }
529
+ }
530
+ }
531
+
532
+ if (!merged) {
533
+ log.error('Merge failed after 3 attempts — marking task as blocked');
534
+ task.status = 'blocked';
535
+ return;
500
536
  }
501
537
 
502
538
  checkpoint.saveStep(task.id, 'merge', 'merged', {
@@ -292,9 +292,23 @@ export function collectReviewFeedback(cwd, prNumber) {
292
292
  return feedback.trim();
293
293
  }
294
294
 
295
+ /**
296
+ * Check if the current repo is private
297
+ * @returns {boolean} true if private, false if public or unknown
298
+ */
299
+ export function isPrivateRepo(cwd) {
300
+ try {
301
+ const result = ghJSON('repo view --json isPrivate', cwd);
302
+ return result?.isPrivate === true;
303
+ } catch {
304
+ return false; // Default to public behavior if detection fails
305
+ }
306
+ }
307
+
295
308
  export const github = {
296
309
  checkGhAuth, createPR, createPRWithRecovery, findExistingPR,
297
310
  ensureRemoteBranch, hasCommitsBetween,
298
311
  getReviews, getReviewComments, getIssueComments,
299
312
  getLatestReviewState, mergePR, collectReviewFeedback,
313
+ isPrivateRepo,
300
314
  };