@jojonax/codex-copilot 1.4.1 → 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 +1 -1
- package/src/commands/run.js +40 -29
- package/src/utils/git.js +26 -33
- package/src/utils/github.js +16 -1
package/package.json
CHANGED
package/src/commands/run.js
CHANGED
|
@@ -95,8 +95,8 @@ export async function run(projectDir) {
|
|
|
95
95
|
|
|
96
96
|
// ===== Auto-retry blocked tasks =====
|
|
97
97
|
// On each run, reset blocked tasks to pending so they're retried in order.
|
|
98
|
-
//
|
|
99
|
-
//
|
|
98
|
+
// Branches and checkpoint steps are PRESERVED — the task resumes from where
|
|
99
|
+
// it left off, keeping all previously developed code.
|
|
100
100
|
const blockedTasks = tasks.tasks.filter(t => t.status === 'blocked');
|
|
101
101
|
if (blockedTasks.length > 0) {
|
|
102
102
|
log.blank();
|
|
@@ -105,30 +105,8 @@ export async function run(projectDir) {
|
|
|
105
105
|
bt.status = 'pending';
|
|
106
106
|
bt.retry_count = (bt.retry_count || 0) + 1;
|
|
107
107
|
bt._retrying = true; // Flag: don't skip even if below checkpoint
|
|
108
|
-
|
|
109
|
-
// Clear per-task checkpoint steps — the task needs a fresh start
|
|
110
|
-
// (The main checkpoint.current_task is NOT affected)
|
|
111
|
-
checkpoint.clearTask(bt.id);
|
|
112
|
-
|
|
113
|
-
// Clean up old branch to avoid "branch already exists" errors
|
|
114
|
-
try {
|
|
115
|
-
git.resetBranch(projectDir, bt.branch, baseBranch);
|
|
116
|
-
log.dim(` ↳ Task #${bt.id}: ${bt.title.substring(0, 50)} (attempt ${bt.retry_count + 1}, branch reset)`);
|
|
117
|
-
} catch {
|
|
118
|
-
// If branch cleanup fails, still let the task try
|
|
119
|
-
log.dim(` ↳ Task #${bt.id}: ${bt.title.substring(0, 50)} (attempt ${bt.retry_count + 1})`);
|
|
120
|
-
}
|
|
121
|
-
|
|
122
|
-
// Close any existing PR for this task (stale from previous attempt)
|
|
123
|
-
if (bt.pr_number) {
|
|
124
|
-
try {
|
|
125
|
-
github.closePR(projectDir, bt.pr_number);
|
|
126
|
-
delete bt.pr_number;
|
|
127
|
-
} catch { /* ignore */ }
|
|
128
|
-
}
|
|
108
|
+
log.dim(` ↳ Task #${bt.id}: ${bt.title.substring(0, 50)} (attempt ${bt.retry_count + 1})`);
|
|
129
109
|
}
|
|
130
|
-
// Switch back to base branch after all resets
|
|
131
|
-
git.checkoutMain(projectDir, baseBranch);
|
|
132
110
|
writeJSON(tasksPath, tasks);
|
|
133
111
|
}
|
|
134
112
|
|
|
@@ -178,6 +156,8 @@ export async function run(projectDir) {
|
|
|
178
156
|
|
|
179
157
|
// Mark task as in_progress
|
|
180
158
|
task.status = 'in_progress';
|
|
159
|
+
const isRetry = (task.retry_count || 0) > 0;
|
|
160
|
+
const retryStartedAt = isRetry ? new Date().toISOString() : null;
|
|
181
161
|
writeJSON(tasksPath, tasks);
|
|
182
162
|
|
|
183
163
|
// ===== Phase 1: Develop =====
|
|
@@ -207,10 +187,19 @@ export async function run(projectDir) {
|
|
|
207
187
|
|
|
208
188
|
// ===== Phase 3: Review loop =====
|
|
209
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
|
+
|
|
210
198
|
await reviewLoop(projectDir, task, prInfo, {
|
|
211
199
|
maxRounds: maxReviewRounds,
|
|
212
200
|
pollInterval,
|
|
213
201
|
waitTimeout,
|
|
202
|
+
retryStartedAt, // Only count reviews after this timestamp
|
|
214
203
|
}, checkpoint, providerId, isPrivate);
|
|
215
204
|
} else {
|
|
216
205
|
log.dim('⏩ Skipping review phase (already completed)');
|
|
@@ -220,7 +209,18 @@ export async function run(projectDir) {
|
|
|
220
209
|
if (!checkpoint.isStepDone(task.id, 'merge', 'merged')) {
|
|
221
210
|
await mergePhase(projectDir, task, prInfo, baseBranch, checkpoint);
|
|
222
211
|
} else {
|
|
223
|
-
|
|
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
|
+
}
|
|
224
224
|
}
|
|
225
225
|
|
|
226
226
|
// Check if task was blocked during review/merge
|
|
@@ -379,7 +379,7 @@ async function prPhase(projectDir, task, baseBranch, checkpoint, isPrivate) {
|
|
|
379
379
|
// ──────────────────────────────────────────────
|
|
380
380
|
// Phase 3: Review loop
|
|
381
381
|
// ──────────────────────────────────────────────
|
|
382
|
-
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) {
|
|
383
383
|
const HARD_MAX_ROUNDS = 5;
|
|
384
384
|
const MAX_POLL_RETRIES = 3;
|
|
385
385
|
let maxRounds = Math.min(_maxRounds, HARD_MAX_ROUNDS);
|
|
@@ -410,8 +410,19 @@ async function reviewLoop(projectDir, task, prInfo, { maxRounds: _maxRounds, pol
|
|
|
410
410
|
// and fast bot responses after fix pushes.
|
|
411
411
|
const existingReviews = github.getReviews(projectDir, prInfo.number);
|
|
412
412
|
const existingComments = github.getIssueComments(projectDir, prInfo.number);
|
|
413
|
-
|
|
414
|
-
|
|
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 =>
|
|
415
426
|
c.user?.type === 'Bot' || c.user?.login?.includes('bot')
|
|
416
427
|
);
|
|
417
428
|
|
package/src/utils/git.js
CHANGED
|
@@ -52,48 +52,41 @@ export function getRepoInfo(cwd) {
|
|
|
52
52
|
return { owner: match[1], repo: match[2] };
|
|
53
53
|
}
|
|
54
54
|
|
|
55
|
-
/**
|
|
56
|
-
* Switch to target branch (create if not exists)
|
|
57
|
-
*/
|
|
58
55
|
export function checkoutBranch(cwd, branch, baseBranch = 'main') {
|
|
59
56
|
validateBranch(branch);
|
|
60
57
|
validateBranch(baseBranch);
|
|
61
58
|
const current = currentBranch(cwd);
|
|
62
59
|
if (current === branch) return;
|
|
63
60
|
|
|
64
|
-
//
|
|
65
|
-
|
|
66
|
-
|
|
61
|
+
// Stash any uncommitted changes to allow safe branch switching
|
|
62
|
+
const hasChanges = !isClean(cwd);
|
|
63
|
+
if (hasChanges) {
|
|
64
|
+
execSafe('git stash --include-untracked', cwd);
|
|
65
|
+
}
|
|
67
66
|
|
|
68
|
-
//
|
|
69
|
-
const
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
67
|
+
// Check if the branch already exists locally
|
|
68
|
+
const branchExists = execSafe(`git rev-parse --verify ${branch}`, cwd).ok;
|
|
69
|
+
|
|
70
|
+
if (branchExists) {
|
|
71
|
+
// Branch exists — just switch to it (preserving all previous work)
|
|
72
|
+
exec(`git checkout ${branch}`, cwd);
|
|
73
|
+
|
|
74
|
+
// Try to rebase onto latest base to pick up any new changes
|
|
75
|
+
execSafe(`git fetch origin ${baseBranch}`, cwd);
|
|
76
|
+
execSafe(`git rebase origin/${baseBranch}`, cwd);
|
|
77
|
+
// If rebase fails (conflicts), abort and continue with existing state
|
|
78
|
+
execSafe('git rebase --abort', cwd);
|
|
79
|
+
} else {
|
|
80
|
+
// Branch doesn't exist — create from latest base
|
|
81
|
+
execSafe(`git checkout ${baseBranch}`, cwd);
|
|
82
|
+
execSafe(`git pull origin ${baseBranch}`, cwd);
|
|
73
83
|
exec(`git checkout -b ${branch}`, cwd);
|
|
74
84
|
}
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Delete a branch (local + remote) and recreate fresh from base.
|
|
79
|
-
* Used when retrying blocked tasks to ensure a clean slate.
|
|
80
|
-
*/
|
|
81
|
-
export function resetBranch(cwd, branch, baseBranch = 'main') {
|
|
82
|
-
validateBranch(branch);
|
|
83
|
-
validateBranch(baseBranch);
|
|
84
|
-
|
|
85
|
-
// Switch to base first (can't delete current branch)
|
|
86
|
-
execSafe(`git checkout ${baseBranch}`, cwd);
|
|
87
|
-
execSafe(`git pull origin ${baseBranch}`, cwd);
|
|
88
|
-
|
|
89
|
-
// Delete local branch (force, ignore errors if doesn't exist)
|
|
90
|
-
execSafe(`git branch -D ${branch}`, cwd);
|
|
91
85
|
|
|
92
|
-
//
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
exec(`git checkout -b ${branch}`, cwd);
|
|
86
|
+
// Restore stashed changes if we stashed earlier
|
|
87
|
+
if (hasChanges) {
|
|
88
|
+
execSafe('git stash pop', cwd);
|
|
89
|
+
}
|
|
97
90
|
}
|
|
98
91
|
|
|
99
92
|
/**
|
|
@@ -135,6 +128,6 @@ export function checkoutMain(cwd, baseBranch = 'main') {
|
|
|
135
128
|
}
|
|
136
129
|
|
|
137
130
|
export const git = {
|
|
138
|
-
isClean, currentBranch, getRepoInfo, checkoutBranch,
|
|
131
|
+
isClean, currentBranch, getRepoInfo, checkoutBranch,
|
|
139
132
|
commitAll, pushBranch, checkoutMain, exec, execSafe,
|
|
140
133
|
};
|
package/src/utils/github.js
CHANGED
|
@@ -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
|
+
}
|