@jojonax/codex-copilot 1.4.2 → 1.4.3

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.4.2",
3
+ "version": "1.4.3",
4
4
  "description": "PRD-driven automated development orchestrator for CodeX / Cursor",
5
5
  "bin": {
6
6
  "codex-copilot": "./bin/cli.js"
@@ -156,6 +156,8 @@ export async function run(projectDir) {
156
156
 
157
157
  // Mark task as in_progress
158
158
  task.status = 'in_progress';
159
+ const isRetry = (task.retry_count || 0) > 0;
160
+ const retryStartedAt = isRetry ? new Date().toISOString() : null;
159
161
  writeJSON(tasksPath, tasks);
160
162
 
161
163
  // ===== Phase 1: Develop =====
@@ -185,10 +187,19 @@ export async function run(projectDir) {
185
187
 
186
188
  // ===== Phase 3: Review loop =====
187
189
  if (!checkpoint.isStepDone(task.id, 'merge', 'merged')) {
190
+ // On retry: request fresh re-review since old reviews may be stale
191
+ if (isRetry && prInfo?.number) {
192
+ log.info('🔄 Retry: requesting fresh re-review on existing PR...');
193
+ try {
194
+ github.requestReReview(projectDir, prInfo.number);
195
+ } catch { /* ignore — review bots may not be configured */ }
196
+ }
197
+
188
198
  await reviewLoop(projectDir, task, prInfo, {
189
199
  maxRounds: maxReviewRounds,
190
200
  pollInterval,
191
201
  waitTimeout,
202
+ retryStartedAt, // Only count reviews after this timestamp
192
203
  }, checkpoint, providerId, isPrivate);
193
204
  } else {
194
205
  log.dim('⏩ Skipping review phase (already completed)');
@@ -198,7 +209,18 @@ export async function run(projectDir) {
198
209
  if (!checkpoint.isStepDone(task.id, 'merge', 'merged')) {
199
210
  await mergePhase(projectDir, task, prInfo, baseBranch, checkpoint);
200
211
  } else {
201
- log.dim('⏩ Skipping merge phase (already merged)');
212
+ // Checkpoint says merged verify against GitHub
213
+ if (prInfo?.number) {
214
+ const prState = github.getPRState(projectDir, prInfo.number);
215
+ if (prState !== 'merged') {
216
+ log.warn(`⚠ Checkpoint says merged but PR #${prInfo.number} is ${prState} — re-entering merge`);
217
+ await mergePhase(projectDir, task, prInfo, baseBranch, checkpoint);
218
+ } else {
219
+ log.dim('⏩ Skipping merge phase (PR confirmed merged on GitHub)');
220
+ }
221
+ } else {
222
+ log.dim('⏩ Skipping merge phase (already merged)');
223
+ }
202
224
  }
203
225
 
204
226
  // Check if task was blocked during review/merge
@@ -357,7 +379,7 @@ async function prPhase(projectDir, task, baseBranch, checkpoint, isPrivate) {
357
379
  // ──────────────────────────────────────────────
358
380
  // Phase 3: Review loop
359
381
  // ──────────────────────────────────────────────
360
- async function reviewLoop(projectDir, task, prInfo, { maxRounds: _maxRounds, pollInterval, waitTimeout }, checkpoint, providerId, isPrivate) {
382
+ async function reviewLoop(projectDir, task, prInfo, { maxRounds: _maxRounds, pollInterval, waitTimeout, retryStartedAt }, checkpoint, providerId, isPrivate) {
361
383
  const HARD_MAX_ROUNDS = 5;
362
384
  const MAX_POLL_RETRIES = 3;
363
385
  let maxRounds = Math.min(_maxRounds, HARD_MAX_ROUNDS);
@@ -388,8 +410,19 @@ async function reviewLoop(projectDir, task, prInfo, { maxRounds: _maxRounds, pol
388
410
  // and fast bot responses after fix pushes.
389
411
  const existingReviews = github.getReviews(projectDir, prInfo.number);
390
412
  const existingComments = github.getIssueComments(projectDir, prInfo.number);
391
- const hasReview = existingReviews.some(r => r.state !== 'PENDING');
392
- const hasBotComment = existingComments.some(c =>
413
+
414
+ // On retry: only count reviews posted AFTER the retry started
415
+ const isReviewFresh = (item) => {
416
+ if (!retryStartedAt) return true; // Not a retry — all reviews are valid
417
+ const itemDate = item.submitted_at || item.created_at || item.updated_at;
418
+ return itemDate && new Date(itemDate) > new Date(retryStartedAt);
419
+ };
420
+
421
+ const freshReviews = existingReviews.filter(isReviewFresh);
422
+ const freshComments = existingComments.filter(isReviewFresh);
423
+
424
+ const hasReview = freshReviews.some(r => r.state !== 'PENDING');
425
+ const hasBotComment = freshComments.some(c =>
393
426
  c.user?.type === 'Bot' || c.user?.login?.includes('bot')
394
427
  );
395
428
 
@@ -383,7 +383,7 @@ export const github = {
383
383
  ensureRemoteBranch, hasCommitsBetween,
384
384
  getReviews, getReviewComments, getIssueComments,
385
385
  getLatestReviewState, mergePR, collectReviewFeedback,
386
- isPrivateRepo, requestReReview, closePR, deleteBranch,
386
+ isPrivateRepo, requestReReview, closePR, deleteBranch, getPRState,
387
387
  };
388
388
 
389
389
  /**
@@ -412,3 +412,18 @@ export function deleteBranch(cwd, branch) {
412
412
  return false;
413
413
  }
414
414
  }
415
+
416
+ /**
417
+ * Get the state of a PR: 'open', 'closed', or 'merged'
418
+ */
419
+ export function getPRState(cwd, prNumber) {
420
+ try {
421
+ const num = validatePRNumber(prNumber);
422
+ const output = gh(`pr view ${num} --json state,mergedAt`, cwd);
423
+ const data = JSON.parse(output);
424
+ if (data.mergedAt) return 'merged';
425
+ return (data.state || 'OPEN').toLowerCase();
426
+ } catch {
427
+ return 'unknown';
428
+ }
429
+ }