@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 +1 -1
- package/src/commands/run.js +69 -33
- package/src/utils/github.js +14 -0
package/package.json
CHANGED
package/src/commands/run.js
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
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
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
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
|
-
|
|
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
|
-
|
|
396
|
-
|
|
397
|
-
if (choice === 'fix') {
|
|
407
|
+
if (maxRounds < HARD_MAX_ROUNDS) {
|
|
408
|
+
// Auto-extend: give it one more round
|
|
398
409
|
maxRounds++;
|
|
399
|
-
|
|
400
|
-
return;
|
|
410
|
+
log.warn(`Auto-extending fix rounds to ${maxRounds}`);
|
|
401
411
|
} else {
|
|
402
|
-
|
|
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
|
-
|
|
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(
|
|
437
|
+
await sleep(15000);
|
|
423
438
|
} else {
|
|
424
|
-
//
|
|
425
|
-
log.
|
|
439
|
+
// AI fix produced no code changes — it cannot resolve this issue
|
|
440
|
+
log.error('AI fix produced no changes — marking 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
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
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', {
|
package/src/utils/github.js
CHANGED
|
@@ -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
|
};
|