@jojonax/codex-copilot 1.3.6 → 1.4.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/bin/cli.js +18 -0
- package/package.json +1 -1
- package/src/commands/evolve.js +321 -0
- package/src/commands/run.js +55 -2
- package/src/commands/status.js +18 -0
- package/src/commands/usage.js +329 -0
- package/src/utils/git.js +26 -2
- package/src/utils/github.js +63 -4
- package/src/utils/provider.js +2 -2
package/bin/cli.js
CHANGED
|
@@ -22,6 +22,8 @@ import { status } from '../src/commands/status.js';
|
|
|
22
22
|
import { reset } from '../src/commands/reset.js';
|
|
23
23
|
import { retry } from '../src/commands/retry.js';
|
|
24
24
|
import { skip } from '../src/commands/skip.js';
|
|
25
|
+
import { usage } from '../src/commands/usage.js';
|
|
26
|
+
import { evolve } from '../src/commands/evolve.js';
|
|
25
27
|
import { log } from '../src/utils/logger.js';
|
|
26
28
|
import { checkForUpdates } from '../src/utils/update-check.js';
|
|
27
29
|
|
|
@@ -95,6 +97,20 @@ async function main() {
|
|
|
95
97
|
break;
|
|
96
98
|
}
|
|
97
99
|
|
|
100
|
+
case 'usage':
|
|
101
|
+
await usage();
|
|
102
|
+
process.exit(0);
|
|
103
|
+
break;
|
|
104
|
+
|
|
105
|
+
case 'evolve':
|
|
106
|
+
case 'next':
|
|
107
|
+
if (!existsSync(resolve(projectDir, '.codex-copilot/config.json'))) {
|
|
108
|
+
log.error('Not initialized. Run: codex-copilot init');
|
|
109
|
+
process.exit(1);
|
|
110
|
+
}
|
|
111
|
+
await evolve(projectDir);
|
|
112
|
+
break;
|
|
113
|
+
|
|
98
114
|
case 'update':
|
|
99
115
|
log.info('Updating to latest version...');
|
|
100
116
|
try {
|
|
@@ -132,6 +148,8 @@ async function main() {
|
|
|
132
148
|
console.log(' reset Reset state and start over');
|
|
133
149
|
console.log(' retry Retry blocked tasks with enhanced prompts');
|
|
134
150
|
console.log(' skip <id> Force-skip a task to unblock dependents');
|
|
151
|
+
console.log(' usage Show AI token usage for recent sessions');
|
|
152
|
+
console.log(' evolve Start next PRD iteration round (gap analysis → plan → run)');
|
|
135
153
|
console.log(' update Update to latest version');
|
|
136
154
|
console.log('');
|
|
137
155
|
console.log(' Workflow:');
|
package/package.json
CHANGED
|
@@ -0,0 +1,321 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* codex-copilot evolve - Multi-round PRD iteration
|
|
3
|
+
*
|
|
4
|
+
* Flow: Archive current round → Gap analysis → Plan next round → Reset → Run
|
|
5
|
+
*
|
|
6
|
+
* Each round is tracked in .codex-copilot/rounds.json with summaries of
|
|
7
|
+
* what was built, enabling intelligent gap analysis against the full PRD.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import { readFileSync, writeFileSync, existsSync, copyFileSync } from 'fs';
|
|
11
|
+
import { resolve } from 'path';
|
|
12
|
+
import { log } from '../utils/logger.js';
|
|
13
|
+
import { provider } from '../utils/provider.js';
|
|
14
|
+
import { readPRD } from '../utils/detect-prd.js';
|
|
15
|
+
import { closePrompt } from '../utils/prompt.js';
|
|
16
|
+
import { run } from './run.js';
|
|
17
|
+
|
|
18
|
+
function readJSON(path) {
|
|
19
|
+
return JSON.parse(readFileSync(path, 'utf-8'));
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function writeJSON(path, data) {
|
|
23
|
+
writeFileSync(path, JSON.stringify(data, null, 2));
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Load or initialize rounds.json
|
|
28
|
+
*/
|
|
29
|
+
function loadRounds(copilotDir) {
|
|
30
|
+
const roundsPath = resolve(copilotDir, 'rounds.json');
|
|
31
|
+
if (existsSync(roundsPath)) {
|
|
32
|
+
return readJSON(roundsPath);
|
|
33
|
+
}
|
|
34
|
+
return { current_round: 0, rounds: [] };
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function saveRounds(copilotDir, rounds) {
|
|
38
|
+
writeJSON(resolve(copilotDir, 'rounds.json'), rounds);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Archive the current round: count results, save summary
|
|
43
|
+
*/
|
|
44
|
+
function archiveCurrentRound(copilotDir, tasks, roundNumber) {
|
|
45
|
+
const completed = tasks.tasks.filter(t => t.status === 'completed');
|
|
46
|
+
const blocked = tasks.tasks.filter(t => t.status === 'blocked');
|
|
47
|
+
const skipped = tasks.tasks.filter(t => t.status === 'skipped');
|
|
48
|
+
|
|
49
|
+
// Build a text summary of what was completed
|
|
50
|
+
const completedSummary = completed.map(t =>
|
|
51
|
+
`- Task #${t.id}: ${t.title}`
|
|
52
|
+
).join('\n');
|
|
53
|
+
|
|
54
|
+
const blockedSummary = blocked.length > 0
|
|
55
|
+
? '\n\nBlocked tasks:\n' + blocked.map(t => `- Task #${t.id}: ${t.title}`).join('\n')
|
|
56
|
+
: '';
|
|
57
|
+
|
|
58
|
+
const summary = `Round ${roundNumber} completed ${completed.length}/${tasks.total} tasks.\n\nCompleted:\n${completedSummary}${blockedSummary}`;
|
|
59
|
+
|
|
60
|
+
const roundEntry = {
|
|
61
|
+
round: roundNumber,
|
|
62
|
+
started_at: tasks.tasks[0]?.started_at || new Date().toISOString(),
|
|
63
|
+
completed_at: new Date().toISOString(),
|
|
64
|
+
tasks_total: tasks.total,
|
|
65
|
+
tasks_completed: completed.length,
|
|
66
|
+
tasks_blocked: blocked.length,
|
|
67
|
+
tasks_skipped: skipped.length,
|
|
68
|
+
summary,
|
|
69
|
+
// Store completed task titles + descriptions for gap analysis
|
|
70
|
+
completed_features: completed.map(t => ({
|
|
71
|
+
title: t.title,
|
|
72
|
+
description: t.description,
|
|
73
|
+
acceptance: t.acceptance,
|
|
74
|
+
})),
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
// Archive tasks.json → tasks_round_N.json
|
|
78
|
+
const archivePath = resolve(copilotDir, `tasks_round_${roundNumber}.json`);
|
|
79
|
+
copyFileSync(resolve(copilotDir, 'tasks.json'), archivePath);
|
|
80
|
+
log.dim(` Archived to tasks_round_${roundNumber}.json`);
|
|
81
|
+
|
|
82
|
+
return roundEntry;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Build the gap analysis + next-round planning prompt
|
|
87
|
+
*/
|
|
88
|
+
function buildEvolvePrompt(prdContent, rounds, copilotDir) {
|
|
89
|
+
// Collect all completed features across all rounds
|
|
90
|
+
const allCompleted = [];
|
|
91
|
+
for (const r of rounds.rounds) {
|
|
92
|
+
if (r.completed_features) {
|
|
93
|
+
for (const f of r.completed_features) {
|
|
94
|
+
allCompleted.push(f);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const completedSection = allCompleted.length > 0
|
|
100
|
+
? allCompleted.map((f, i) => `${i + 1}. **${f.title}**\n ${f.description?.substring(0, 200) || ''}`).join('\n\n')
|
|
101
|
+
: 'No features completed yet.';
|
|
102
|
+
|
|
103
|
+
const roundHistorySection = rounds.rounds.map(r =>
|
|
104
|
+
`### Round ${r.round}\n- Completed: ${r.tasks_completed}/${r.tasks_total} tasks\n- Summary: ${r.summary?.substring(0, 300) || 'N/A'}`
|
|
105
|
+
).join('\n\n');
|
|
106
|
+
|
|
107
|
+
const nextRound = rounds.current_round + 1;
|
|
108
|
+
|
|
109
|
+
return `You are a senior software architect planning the next development iteration.
|
|
110
|
+
|
|
111
|
+
## Context
|
|
112
|
+
|
|
113
|
+
This is Round ${nextRound} of an iterative PRD implementation.
|
|
114
|
+
|
|
115
|
+
### Full PRD Document
|
|
116
|
+
|
|
117
|
+
${prdContent}
|
|
118
|
+
|
|
119
|
+
### Development History
|
|
120
|
+
|
|
121
|
+
${roundHistorySection}
|
|
122
|
+
|
|
123
|
+
### Features Already Completed (across all rounds)
|
|
124
|
+
|
|
125
|
+
${completedSection}
|
|
126
|
+
|
|
127
|
+
## Your Task
|
|
128
|
+
|
|
129
|
+
1. **Gap Analysis**: Compare the PRD requirements against the completed features. Identify ALL remaining features, improvements, and details that have NOT been implemented yet.
|
|
130
|
+
|
|
131
|
+
2. **Priority Planning**: From the gaps identified, select the most important batch for Round ${nextRound}. Consider:
|
|
132
|
+
- Dependencies (build foundations before features)
|
|
133
|
+
- User value (core features before polish)
|
|
134
|
+
- Technical feasibility (group related changes)
|
|
135
|
+
- Moderate batch size (8-15 tasks per round)
|
|
136
|
+
|
|
137
|
+
3. **Generate tasks.json**: Output the next round's task plan.
|
|
138
|
+
|
|
139
|
+
## Output Format
|
|
140
|
+
|
|
141
|
+
Write the result to \`.codex-copilot/tasks.json\` in the following format:
|
|
142
|
+
|
|
143
|
+
\`\`\`json
|
|
144
|
+
{
|
|
145
|
+
"project": "Project Name",
|
|
146
|
+
"round": ${nextRound},
|
|
147
|
+
"total": N,
|
|
148
|
+
"tasks": [
|
|
149
|
+
{
|
|
150
|
+
"id": 1,
|
|
151
|
+
"title": "Task title (brief)",
|
|
152
|
+
"description": "Detailed task description with specific features and technical details",
|
|
153
|
+
"acceptance": ["Acceptance criteria 1", "Acceptance criteria 2"],
|
|
154
|
+
"branch": "feature/${String(nextRound).padStart(2, '0')}01-task-slug",
|
|
155
|
+
"status": "pending",
|
|
156
|
+
"depends_on": []
|
|
157
|
+
}
|
|
158
|
+
]
|
|
159
|
+
}
|
|
160
|
+
\`\`\`
|
|
161
|
+
|
|
162
|
+
## Important Rules
|
|
163
|
+
1. Do NOT re-implement features that are already completed
|
|
164
|
+
2. Branch names should be prefixed with the round number: feature/${String(nextRound).padStart(2, '0')}XX-slug
|
|
165
|
+
3. Each task must be specific and actionable (not vague like "improve performance")
|
|
166
|
+
4. Include detailed descriptions that reference the PRD's specific requirements
|
|
167
|
+
5. If the PRD has been fully implemented, output an empty tasks array with a comment
|
|
168
|
+
`;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export async function evolve(projectDir) {
|
|
172
|
+
log.title('🔄 Multi-Round PRD Evolution');
|
|
173
|
+
log.blank();
|
|
174
|
+
|
|
175
|
+
const copilotDir = resolve(projectDir, '.codex-copilot');
|
|
176
|
+
const tasksPath = resolve(copilotDir, 'tasks.json');
|
|
177
|
+
const configPath = resolve(copilotDir, 'config.json');
|
|
178
|
+
|
|
179
|
+
// Validate project state
|
|
180
|
+
if (!existsSync(copilotDir) || !existsSync(configPath)) {
|
|
181
|
+
log.error('Project not initialized. Run: codex-copilot init');
|
|
182
|
+
closePrompt();
|
|
183
|
+
process.exit(1);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
const config = readJSON(configPath);
|
|
187
|
+
const providerId = config.provider || 'codex-cli';
|
|
188
|
+
const rounds = loadRounds(copilotDir);
|
|
189
|
+
|
|
190
|
+
// ===== Step 1: Archive current round =====
|
|
191
|
+
if (existsSync(tasksPath)) {
|
|
192
|
+
const tasks = readJSON(tasksPath);
|
|
193
|
+
const hasCompletedTasks = tasks.tasks?.some(t => t.status === 'completed');
|
|
194
|
+
const roundNumber = tasks.round || rounds.current_round || 1;
|
|
195
|
+
|
|
196
|
+
// Check if this round was already archived (prevent duplicate archiving)
|
|
197
|
+
const archiveExists = existsSync(resolve(copilotDir, `tasks_round_${roundNumber}.json`));
|
|
198
|
+
const alreadyInHistory = rounds.rounds.some(r => r.round === roundNumber);
|
|
199
|
+
|
|
200
|
+
if (hasCompletedTasks && !archiveExists && !alreadyInHistory) {
|
|
201
|
+
log.step(`Step 1: Archiving Round ${roundNumber}...`);
|
|
202
|
+
|
|
203
|
+
const roundEntry = archiveCurrentRound(copilotDir, tasks, roundNumber);
|
|
204
|
+
rounds.rounds.push(roundEntry);
|
|
205
|
+
rounds.current_round = roundNumber;
|
|
206
|
+
saveRounds(copilotDir, rounds);
|
|
207
|
+
|
|
208
|
+
const pct = Math.round((roundEntry.tasks_completed / roundEntry.tasks_total) * 100);
|
|
209
|
+
log.info(` Round ${roundNumber}: ${roundEntry.tasks_completed}/${roundEntry.tasks_total} tasks (${pct}%)`);
|
|
210
|
+
log.blank();
|
|
211
|
+
} else if (hasCompletedTasks && (archiveExists || alreadyInHistory)) {
|
|
212
|
+
log.dim(` Round ${roundNumber} already archived — skipping`);
|
|
213
|
+
// Ensure rounds.current_round is up to date
|
|
214
|
+
if (rounds.current_round < roundNumber) {
|
|
215
|
+
rounds.current_round = roundNumber;
|
|
216
|
+
saveRounds(copilotDir, rounds);
|
|
217
|
+
}
|
|
218
|
+
log.blank();
|
|
219
|
+
} else if (rounds.current_round === 0) {
|
|
220
|
+
log.warn('No completed tasks found. Starting from Round 1.');
|
|
221
|
+
rounds.current_round = 0;
|
|
222
|
+
saveRounds(copilotDir, rounds);
|
|
223
|
+
log.blank();
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
// ===== Step 2 + 3: Gap analysis & next-round planning =====
|
|
228
|
+
const nextRound = rounds.current_round + 1;
|
|
229
|
+
log.step(`Step 2: Planning Round ${nextRound} (Gap Analysis + Task Planning)...`);
|
|
230
|
+
log.blank();
|
|
231
|
+
|
|
232
|
+
// Read PRD
|
|
233
|
+
let prdContent;
|
|
234
|
+
try {
|
|
235
|
+
prdContent = readPRD(config.prd_path);
|
|
236
|
+
} catch {
|
|
237
|
+
log.error(`Cannot read PRD: ${config.prd_path}`);
|
|
238
|
+
closePrompt();
|
|
239
|
+
process.exit(1);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// Build the evolve prompt
|
|
243
|
+
const evolvePrompt = buildEvolvePrompt(prdContent, rounds, copilotDir);
|
|
244
|
+
const promptPath = resolve(copilotDir, 'evolve-prompt.md');
|
|
245
|
+
writeFileSync(promptPath, evolvePrompt);
|
|
246
|
+
log.info(` Evolve prompt saved: .codex-copilot/evolve-prompt.md`);
|
|
247
|
+
log.blank();
|
|
248
|
+
|
|
249
|
+
// Execute via AI provider
|
|
250
|
+
log.step(`Invoking AI to analyze gaps and plan Round ${nextRound}...`);
|
|
251
|
+
const ok = await provider.executePrompt(providerId, promptPath, projectDir);
|
|
252
|
+
|
|
253
|
+
if (!ok) {
|
|
254
|
+
log.error('AI invocation failed. You can manually edit the prompt and retry.');
|
|
255
|
+
closePrompt();
|
|
256
|
+
process.exit(1);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// ===== Step 4: Validate new tasks.json =====
|
|
260
|
+
if (!existsSync(tasksPath)) {
|
|
261
|
+
log.error('AI did not generate tasks.json. Check the evolve prompt output.');
|
|
262
|
+
closePrompt();
|
|
263
|
+
process.exit(1);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
let newTasks;
|
|
267
|
+
try {
|
|
268
|
+
newTasks = readJSON(tasksPath);
|
|
269
|
+
} catch {
|
|
270
|
+
log.error('Generated tasks.json is invalid JSON.');
|
|
271
|
+
closePrompt();
|
|
272
|
+
process.exit(1);
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (!newTasks.tasks || newTasks.tasks.length === 0) {
|
|
276
|
+
log.blank();
|
|
277
|
+
log.title('🎉 PRD fully implemented!');
|
|
278
|
+
log.info('All features from the PRD have been built across all rounds.');
|
|
279
|
+
log.info(`Total rounds: ${rounds.current_round}`);
|
|
280
|
+
|
|
281
|
+
// Show round history
|
|
282
|
+
for (const r of rounds.rounds) {
|
|
283
|
+
log.dim(` Round ${r.round}: ${r.tasks_completed}/${r.tasks_total} tasks`);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
closePrompt();
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Update round tracking
|
|
291
|
+
rounds.current_round = nextRound;
|
|
292
|
+
saveRounds(copilotDir, rounds);
|
|
293
|
+
|
|
294
|
+
// Reset checkpoint for fresh round
|
|
295
|
+
const statePath = resolve(copilotDir, 'state.json');
|
|
296
|
+
writeJSON(statePath, {
|
|
297
|
+
current_task: 0,
|
|
298
|
+
phase: null,
|
|
299
|
+
phase_step: null,
|
|
300
|
+
current_pr: null,
|
|
301
|
+
review_round: 0,
|
|
302
|
+
branch: null,
|
|
303
|
+
status: 'initialized',
|
|
304
|
+
last_updated: new Date().toISOString(),
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
log.blank();
|
|
308
|
+
log.title(`✅ Round ${nextRound} planned — ${newTasks.tasks.length} tasks`);
|
|
309
|
+
log.blank();
|
|
310
|
+
|
|
311
|
+
for (const t of newTasks.tasks) {
|
|
312
|
+
log.dim(` ${t.id}. ${t.title}`);
|
|
313
|
+
}
|
|
314
|
+
log.blank();
|
|
315
|
+
|
|
316
|
+
// ===== Step 5: Auto-start run =====
|
|
317
|
+
log.step(`Starting Round ${nextRound} execution...`);
|
|
318
|
+
log.blank();
|
|
319
|
+
|
|
320
|
+
await run(projectDir);
|
|
321
|
+
}
|
package/src/commands/run.js
CHANGED
|
@@ -93,6 +93,45 @@ export async function run(projectDir) {
|
|
|
93
93
|
log.info(`⏩ Resuming task #${state.current_task} from: ${state.phase} → ${state.phase_step}`);
|
|
94
94
|
}
|
|
95
95
|
|
|
96
|
+
// ===== Auto-retry blocked tasks =====
|
|
97
|
+
// On each run, reset blocked tasks to pending so they're retried in order.
|
|
98
|
+
// Per-task checkpoint steps are cleared so the task starts fresh.
|
|
99
|
+
// The main checkpoint position (current_task) is preserved.
|
|
100
|
+
const blockedTasks = tasks.tasks.filter(t => t.status === 'blocked');
|
|
101
|
+
if (blockedTasks.length > 0) {
|
|
102
|
+
log.blank();
|
|
103
|
+
log.info(`🔄 Auto-retrying ${blockedTasks.length} blocked task(s)...`);
|
|
104
|
+
for (const bt of blockedTasks) {
|
|
105
|
+
bt.status = 'pending';
|
|
106
|
+
bt.retry_count = (bt.retry_count || 0) + 1;
|
|
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
|
+
}
|
|
129
|
+
}
|
|
130
|
+
// Switch back to base branch after all resets
|
|
131
|
+
git.checkoutMain(projectDir, baseBranch);
|
|
132
|
+
writeJSON(tasksPath, tasks);
|
|
133
|
+
}
|
|
134
|
+
|
|
96
135
|
const completedCount = tasks.tasks.filter(t => t.status === 'completed').length;
|
|
97
136
|
log.blank();
|
|
98
137
|
progressBar(completedCount, tasks.total, `${completedCount}/${tasks.total} tasks done`);
|
|
@@ -103,9 +142,12 @@ export async function run(projectDir) {
|
|
|
103
142
|
// Skip fully completed tasks
|
|
104
143
|
if (task.status === 'completed' || task.status === 'skipped') continue;
|
|
105
144
|
|
|
106
|
-
// Skip tasks whose ID is below the
|
|
145
|
+
// Skip tasks whose ID is below the checkpoint (unless retrying a blocked task)
|
|
107
146
|
const isResumingTask = state.current_task === task.id && state.phase;
|
|
108
|
-
if (task.id < state.current_task && !isResumingTask) continue;
|
|
147
|
+
if (task.id < state.current_task && !isResumingTask && !task._retrying) continue;
|
|
148
|
+
|
|
149
|
+
// Clean up retry flag (used only to bypass checkpoint skip)
|
|
150
|
+
delete task._retrying;
|
|
109
151
|
|
|
110
152
|
log.blank();
|
|
111
153
|
log.title(`━━━ Task ${task.id}/${tasks.total}: ${task.title} ━━━`);
|
|
@@ -217,6 +259,17 @@ export async function run(projectDir) {
|
|
|
217
259
|
log.blank();
|
|
218
260
|
process.removeListener('SIGINT', gracefulShutdown);
|
|
219
261
|
process.removeListener('SIGTERM', gracefulShutdown);
|
|
262
|
+
|
|
263
|
+
// ===== Auto-evolve: trigger next round if enabled =====
|
|
264
|
+
if (config.auto_evolve !== false) {
|
|
265
|
+
log.blank();
|
|
266
|
+
log.info('🔄 Auto-evolving to next round...');
|
|
267
|
+
log.blank();
|
|
268
|
+
const { evolve } = await import('./evolve.js');
|
|
269
|
+
await evolve(projectDir);
|
|
270
|
+
return; // evolve() calls run() internally
|
|
271
|
+
}
|
|
272
|
+
|
|
220
273
|
closePrompt();
|
|
221
274
|
}
|
|
222
275
|
|
package/src/commands/status.js
CHANGED
|
@@ -24,6 +24,24 @@ export async function status(projectDir) {
|
|
|
24
24
|
log.title(`📊 Project: ${tasks.project}`);
|
|
25
25
|
log.blank();
|
|
26
26
|
|
|
27
|
+
// Round info
|
|
28
|
+
const roundsPath = resolve(projectDir, '.codex-copilot/rounds.json');
|
|
29
|
+
if (existsSync(roundsPath)) {
|
|
30
|
+
try {
|
|
31
|
+
const rounds = JSON.parse(readFileSync(roundsPath, 'utf-8'));
|
|
32
|
+
const currentRound = tasks.round || rounds.current_round || 1;
|
|
33
|
+
const totalCompleted = rounds.rounds.reduce((s, r) => s + r.tasks_completed, 0);
|
|
34
|
+
log.info(`📦 Round: ${currentRound} | Total across all rounds: ${totalCompleted} tasks`);
|
|
35
|
+
if (rounds.rounds.length > 0) {
|
|
36
|
+
for (const r of rounds.rounds) {
|
|
37
|
+
const pct = Math.round((r.tasks_completed / r.tasks_total) * 100);
|
|
38
|
+
log.dim(` Round ${r.round}: ${r.tasks_completed}/${r.tasks_total} (${pct}%)`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
log.blank();
|
|
42
|
+
} catch { /* ignore parse errors */ }
|
|
43
|
+
}
|
|
44
|
+
|
|
27
45
|
// Progress bar
|
|
28
46
|
const completed = tasks.tasks.filter(t => t.status === 'completed').length;
|
|
29
47
|
const inProgress = tasks.tasks.filter(t => t.status === 'in_progress' || t.status === 'developed').length;
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* codex-copilot usage - Show token usage stats for recent AI sessions
|
|
3
|
+
*
|
|
4
|
+
* Data sources:
|
|
5
|
+
* Codex CLI: ~/.codex/state_5.sqlite (threads) + rollout JSONL (token breakdown)
|
|
6
|
+
* Cursor: ~/Library/Application Support/Cursor/User/globalStorage/state.vscdb
|
|
7
|
+
* Claude Code: ~/.claude/stats-cache.json
|
|
8
|
+
* Antigravity: (no local usage data available)
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { execSync } from 'child_process';
|
|
12
|
+
import { readFileSync, existsSync, readdirSync } from 'fs';
|
|
13
|
+
import { resolve, join } from 'path';
|
|
14
|
+
import { log } from '../utils/logger.js';
|
|
15
|
+
|
|
16
|
+
const HOME = process.env.HOME || process.env.USERPROFILE || '~';
|
|
17
|
+
|
|
18
|
+
// ─── Helpers ─────────────────────────────────────────────
|
|
19
|
+
|
|
20
|
+
function formatTokens(n) {
|
|
21
|
+
if (n >= 1_000_000) return `${(n / 1_000_000).toFixed(1)}M`;
|
|
22
|
+
if (n >= 1_000) return `${(n / 1_000).toFixed(1)}K`;
|
|
23
|
+
return String(n);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function padRight(s, len) {
|
|
27
|
+
const str = String(s);
|
|
28
|
+
return str.length >= len ? str.substring(0, len) : str + ' '.repeat(len - str.length);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function padLeft(s, len) {
|
|
32
|
+
const str = String(s);
|
|
33
|
+
return str.length >= len ? str.substring(0, len) : ' '.repeat(len - str.length) + str;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function printTable(headers, rows, colWidths, alignRight = []) {
|
|
37
|
+
const topBorder = ' ┌─' + colWidths.map(w => '─'.repeat(w)).join('─┬─') + '─┐';
|
|
38
|
+
const headSep = ' ├─' + colWidths.map(w => '─'.repeat(w)).join('─┼─') + '─┤';
|
|
39
|
+
const botBorder = ' └─' + colWidths.map(w => '─'.repeat(w)).join('─┴─') + '─┘';
|
|
40
|
+
|
|
41
|
+
const headerLine = headers.map((h, i) => padRight(h, colWidths[i])).join(' │ ');
|
|
42
|
+
console.log(topBorder);
|
|
43
|
+
console.log(` │ ${headerLine} │`);
|
|
44
|
+
console.log(headSep);
|
|
45
|
+
|
|
46
|
+
for (const row of rows) {
|
|
47
|
+
const cells = row.map((cell, i) =>
|
|
48
|
+
alignRight.includes(i) ? padLeft(String(cell), colWidths[i]) : padRight(String(cell), colWidths[i])
|
|
49
|
+
);
|
|
50
|
+
console.log(` │ ${cells.join(' │ ')} │`);
|
|
51
|
+
}
|
|
52
|
+
console.log(botBorder);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function safeExec(cmd) {
|
|
56
|
+
try {
|
|
57
|
+
return execSync(cmd, { encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
|
|
58
|
+
} catch { return ''; }
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// ─── Codex CLI ───────────────────────────────────────────
|
|
62
|
+
|
|
63
|
+
function getCodexAccount() {
|
|
64
|
+
const authPath = resolve(HOME, '.codex/auth.json');
|
|
65
|
+
if (!existsSync(authPath)) return { email: null, sub: null };
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
const auth = JSON.parse(readFileSync(authPath, 'utf-8'));
|
|
69
|
+
const token = auth.tokens?.access_token || '';
|
|
70
|
+
if (token.startsWith('eyJ')) {
|
|
71
|
+
const payload = JSON.parse(Buffer.from(token.split('.')[1], 'base64').toString());
|
|
72
|
+
return { email: payload.email || null, sub: payload.sub || null };
|
|
73
|
+
}
|
|
74
|
+
return { email: null, sub: null };
|
|
75
|
+
} catch { return { email: null, sub: null }; }
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function getCodexPlan() {
|
|
79
|
+
// Get plan_type from the most recent rollout's token_count event
|
|
80
|
+
const sessionsDir = resolve(HOME, '.codex/sessions');
|
|
81
|
+
if (!existsSync(sessionsDir)) return null;
|
|
82
|
+
|
|
83
|
+
try {
|
|
84
|
+
// Find the most recent rollout file
|
|
85
|
+
const raw = safeExec(`find "${sessionsDir}" -name "rollout-*.jsonl" -type f | sort -r | head -1`);
|
|
86
|
+
if (!raw) return null;
|
|
87
|
+
|
|
88
|
+
const planLine = safeExec(`grep '"plan_type"' "${raw}" | tail -1`);
|
|
89
|
+
if (!planLine) return null;
|
|
90
|
+
|
|
91
|
+
const d = JSON.parse(planLine);
|
|
92
|
+
return d?.payload?.rate_limits?.plan_type || null;
|
|
93
|
+
} catch { return null; }
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function getCodexSessions(limit = 3) {
|
|
97
|
+
const dbPath = resolve(HOME, '.codex/state_5.sqlite');
|
|
98
|
+
if (!existsSync(dbPath)) return [];
|
|
99
|
+
|
|
100
|
+
try {
|
|
101
|
+
const query = `SELECT id, replace(replace(substr(title,1,50),char(10),' '),char(13),' ') as title, tokens_used, model, datetime(created_at,'unixepoch','localtime') as created, datetime(updated_at,'unixepoch','localtime') as updated, cwd, rollout_path FROM threads WHERE archived=0 ORDER BY updated_at DESC LIMIT ${limit}`;
|
|
102
|
+
|
|
103
|
+
const raw = safeExec(`sqlite3 -json "${dbPath}" "${query}"`);
|
|
104
|
+
if (!raw) return [];
|
|
105
|
+
|
|
106
|
+
return JSON.parse(raw).map(r => ({
|
|
107
|
+
id: r.id,
|
|
108
|
+
title: (r.title || '').substring(0, 50),
|
|
109
|
+
tokens: r.tokens_used || 0,
|
|
110
|
+
model: r.model || 'unknown',
|
|
111
|
+
created: r.created || '',
|
|
112
|
+
updated: r.updated || '',
|
|
113
|
+
cwd: r.cwd || '',
|
|
114
|
+
rolloutPath: r.rollout_path || '',
|
|
115
|
+
}));
|
|
116
|
+
} catch { return []; }
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
function getCodexTokenBreakdown(rolloutPath) {
|
|
120
|
+
if (!rolloutPath || !existsSync(rolloutPath)) return null;
|
|
121
|
+
|
|
122
|
+
try {
|
|
123
|
+
// Get the last token_count event with info (has cumulative totals)
|
|
124
|
+
const raw = safeExec(`grep '"token_count"' "${rolloutPath}" | grep '"total_token_usage"' | tail -1`);
|
|
125
|
+
if (!raw) return null;
|
|
126
|
+
|
|
127
|
+
const d = JSON.parse(raw);
|
|
128
|
+
const usage = d?.payload?.info?.total_token_usage;
|
|
129
|
+
const limits = d?.payload?.rate_limits;
|
|
130
|
+
if (!usage) return null;
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
input: usage.input_tokens || 0,
|
|
134
|
+
cached: usage.cached_input_tokens || 0,
|
|
135
|
+
output: usage.output_tokens || 0,
|
|
136
|
+
reasoning: usage.reasoning_output_tokens || 0,
|
|
137
|
+
total: usage.total_tokens || 0,
|
|
138
|
+
rateLimits: limits ? {
|
|
139
|
+
primaryUsed: limits.primary?.used_percent,
|
|
140
|
+
secondaryUsed: limits.secondary?.used_percent,
|
|
141
|
+
primaryResets: limits.primary?.resets_at,
|
|
142
|
+
secondaryResets: limits.secondary?.resets_at,
|
|
143
|
+
} : null,
|
|
144
|
+
};
|
|
145
|
+
} catch { return null; }
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// ─── Cursor ──────────────────────────────────────────────
|
|
149
|
+
|
|
150
|
+
function getCursorAccount() {
|
|
151
|
+
const dbPath = resolve(HOME, 'Library/Application Support/Cursor/User/globalStorage/state.vscdb');
|
|
152
|
+
if (!existsSync(dbPath)) return null;
|
|
153
|
+
|
|
154
|
+
try {
|
|
155
|
+
const email = safeExec(`sqlite3 "${dbPath}" "SELECT value FROM ItemTable WHERE key='cursorAuth/cachedEmail'"`);
|
|
156
|
+
const plan = safeExec(`sqlite3 "${dbPath}" "SELECT value FROM ItemTable WHERE key='cursorAuth/stripeMembershipType'"`);
|
|
157
|
+
if (!email) return null;
|
|
158
|
+
return { email, plan: plan || 'free' };
|
|
159
|
+
} catch { return null; }
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
// ─── Claude Code ─────────────────────────────────────────
|
|
163
|
+
|
|
164
|
+
function getClaudeAccount() {
|
|
165
|
+
// Claude Code doesn't store email locally in an easily accessible way
|
|
166
|
+
const settingsPath = resolve(HOME, '.claude/settings.json');
|
|
167
|
+
if (!existsSync(settingsPath)) return null;
|
|
168
|
+
try {
|
|
169
|
+
const s = JSON.parse(readFileSync(settingsPath, 'utf-8'));
|
|
170
|
+
return s.email || null;
|
|
171
|
+
} catch { return null; }
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function getClaudeStats(days = 7) {
|
|
175
|
+
const statsPath = resolve(HOME, '.claude/stats-cache.json');
|
|
176
|
+
if (!existsSync(statsPath)) return null;
|
|
177
|
+
|
|
178
|
+
try {
|
|
179
|
+
const data = JSON.parse(readFileSync(statsPath, 'utf-8'));
|
|
180
|
+
if (!data.dailyActivity?.length) return null;
|
|
181
|
+
|
|
182
|
+
const recent = data.dailyActivity.slice(-days).filter(d => d.messageCount > 0);
|
|
183
|
+
return {
|
|
184
|
+
days: recent,
|
|
185
|
+
total: recent.reduce((s, d) => ({
|
|
186
|
+
messages: s.messages + (d.messageCount || 0),
|
|
187
|
+
sessions: s.sessions + (d.sessionCount || 0),
|
|
188
|
+
toolCalls: s.toolCalls + (d.toolCallCount || 0),
|
|
189
|
+
}), { messages: 0, sessions: 0, toolCalls: 0 }),
|
|
190
|
+
};
|
|
191
|
+
} catch { return null; }
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
// ─── Antigravity / Gemini ────────────────────────────────
|
|
195
|
+
|
|
196
|
+
function getAntigravityInfo() {
|
|
197
|
+
// Antigravity doesn't expose local usage data currently
|
|
198
|
+
return null;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
// ─── Main ────────────────────────────────────────────────
|
|
202
|
+
|
|
203
|
+
export async function usage() {
|
|
204
|
+
log.title('📊 AI Usage Report');
|
|
205
|
+
log.blank();
|
|
206
|
+
|
|
207
|
+
let hasAny = false;
|
|
208
|
+
|
|
209
|
+
// ═══ Codex CLI ═══
|
|
210
|
+
const codexAccount = getCodexAccount();
|
|
211
|
+
const codexPlan = getCodexPlan();
|
|
212
|
+
const codexSessions = getCodexSessions(3);
|
|
213
|
+
|
|
214
|
+
if (codexSessions.length > 0) {
|
|
215
|
+
hasAny = true;
|
|
216
|
+
const accountDisplay = codexAccount.email || codexAccount.sub || 'N/A';
|
|
217
|
+
console.log(`\x1b[36m ◆ Codex CLI\x1b[0m`);
|
|
218
|
+
console.log(` Account: ${accountDisplay}`);
|
|
219
|
+
if (codexPlan) console.log(` Plan: ${codexPlan}`);
|
|
220
|
+
log.blank();
|
|
221
|
+
|
|
222
|
+
// Session overview table
|
|
223
|
+
const rows = codexSessions.map(s => {
|
|
224
|
+
const project = s.cwd.split('/').pop() || s.cwd;
|
|
225
|
+
const title = s.title.length > 36 ? s.title.substring(0, 33) + '...' : s.title;
|
|
226
|
+
return [title, s.model, formatTokens(s.tokens), project, s.updated.substring(0, 16)];
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
printTable(
|
|
230
|
+
['Session', 'Model', 'Tokens', 'Project', 'Last Active'],
|
|
231
|
+
rows,
|
|
232
|
+
[36, 16, 10, 14, 16],
|
|
233
|
+
[2],
|
|
234
|
+
);
|
|
235
|
+
|
|
236
|
+
const totalTokens = codexSessions.reduce((sum, s) => sum + s.tokens, 0);
|
|
237
|
+
log.info(` Total (${codexSessions.length} sessions): ${formatTokens(totalTokens)} tokens`);
|
|
238
|
+
log.blank();
|
|
239
|
+
|
|
240
|
+
// Token breakdown per session
|
|
241
|
+
for (const session of codexSessions) {
|
|
242
|
+
const breakdown = getCodexTokenBreakdown(session.rolloutPath);
|
|
243
|
+
if (!breakdown) continue;
|
|
244
|
+
|
|
245
|
+
const title = session.title.length > 50 ? session.title.substring(0, 47) + '...' : session.title;
|
|
246
|
+
console.log(`\x1b[2m ┌ ${title}\x1b[0m`);
|
|
247
|
+
|
|
248
|
+
const bRows = [
|
|
249
|
+
['Input', formatTokens(breakdown.input)],
|
|
250
|
+
[' ↳ Cached', formatTokens(breakdown.cached)],
|
|
251
|
+
['Output', formatTokens(breakdown.output)],
|
|
252
|
+
[' ↳ Reasoning', formatTokens(breakdown.reasoning)],
|
|
253
|
+
['Total', formatTokens(breakdown.total)],
|
|
254
|
+
];
|
|
255
|
+
for (const [label, val] of bRows) {
|
|
256
|
+
console.log(`\x1b[2m │ ${padRight(label, 16)} ${padLeft(val, 10)}\x1b[0m`);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
if (breakdown.rateLimits) {
|
|
260
|
+
const rl = breakdown.rateLimits;
|
|
261
|
+
const resetPrimary = rl.primaryResets ? new Date(rl.primaryResets * 1000).toLocaleString() : 'N/A';
|
|
262
|
+
console.log(`\x1b[2m │ Quota: ${rl.primaryUsed ?? '?'}% (5h) / ${rl.secondaryUsed ?? '?'}% (7d) Resets: ${resetPrimary}\x1b[0m`);
|
|
263
|
+
}
|
|
264
|
+
console.log(`\x1b[2m └──\x1b[0m`);
|
|
265
|
+
}
|
|
266
|
+
log.blank();
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
// ═══ Cursor ═══
|
|
270
|
+
const cursorAccount = getCursorAccount();
|
|
271
|
+
if (cursorAccount) {
|
|
272
|
+
hasAny = true;
|
|
273
|
+
console.log(`\x1b[33m ◆ Cursor\x1b[0m`);
|
|
274
|
+
console.log(` Account: ${cursorAccount.email}`);
|
|
275
|
+
console.log(` Plan: ${cursorAccount.plan}`);
|
|
276
|
+
console.log(`\x1b[2m Note: Cursor does not expose per-session token usage locally.\x1b[0m`);
|
|
277
|
+
console.log(`\x1b[2m Check usage at: https://www.cursor.com/settings\x1b[0m`);
|
|
278
|
+
log.blank();
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
// ═══ Claude Code ═══
|
|
282
|
+
const claudeStats = getClaudeStats(7);
|
|
283
|
+
const claudeAccount = getClaudeAccount();
|
|
284
|
+
|
|
285
|
+
if (claudeStats) {
|
|
286
|
+
hasAny = true;
|
|
287
|
+
console.log(`\x1b[35m ◆ Claude Code\x1b[0m`);
|
|
288
|
+
console.log(` Account: ${claudeAccount || 'N/A (check ~/.claude/settings.json)'}`);
|
|
289
|
+
log.blank();
|
|
290
|
+
|
|
291
|
+
const rows = claudeStats.days.slice(-5).map(d => [
|
|
292
|
+
d.date, String(d.sessionCount), String(d.messageCount), String(d.toolCallCount),
|
|
293
|
+
]);
|
|
294
|
+
|
|
295
|
+
if (rows.length > 0) {
|
|
296
|
+
printTable(
|
|
297
|
+
['Date', 'Sessions', 'Messages', 'Tool Calls'],
|
|
298
|
+
rows,
|
|
299
|
+
[12, 10, 10, 12],
|
|
300
|
+
[1, 2, 3],
|
|
301
|
+
);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
log.info(` Total (7d): ${claudeStats.total.messages} msgs, ${claudeStats.total.toolCalls} tool calls, ${claudeStats.total.sessions} sessions`);
|
|
305
|
+
console.log(`\x1b[2m Note: Claude Code tracks message/tool counts, not token counts locally.\x1b[0m`);
|
|
306
|
+
console.log(`\x1b[2m Token-level billing: https://console.anthropic.com/settings/billing\x1b[0m`);
|
|
307
|
+
log.blank();
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
// ═══ Antigravity / Gemini ═══
|
|
311
|
+
const agInfo = getAntigravityInfo();
|
|
312
|
+
if (!agInfo) {
|
|
313
|
+
console.log(`\x1b[2m ◆ Antigravity / Gemini: No local usage data available.\x1b[0m`);
|
|
314
|
+
console.log(`\x1b[2m Check: https://console.cloud.google.com/billing\x1b[0m`);
|
|
315
|
+
log.blank();
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// ═══ Codex CLI vs Client Shared Pool Note ═══
|
|
319
|
+
if (codexSessions.length > 0) {
|
|
320
|
+
log.blank();
|
|
321
|
+
console.log(`\x1b[2m ℹ Codex CLI and the Codex VS Code extension share the same OpenAI account token pool.\x1b[0m`);
|
|
322
|
+
console.log(`\x1b[2m Usage shown above is CLI-only. Combined usage: https://platform.openai.com/usage\x1b[0m`);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
if (!hasAny) {
|
|
326
|
+
log.warn('No AI tool usage data found.');
|
|
327
|
+
log.info('Supported: Codex CLI, Cursor, Claude Code');
|
|
328
|
+
}
|
|
329
|
+
}
|
package/src/utils/git.js
CHANGED
|
@@ -65,13 +65,37 @@ export function checkoutBranch(cwd, branch, baseBranch = 'main') {
|
|
|
65
65
|
execSafe(`git checkout ${baseBranch}`, cwd);
|
|
66
66
|
execSafe(`git pull origin ${baseBranch}`, cwd);
|
|
67
67
|
|
|
68
|
-
// Try to switch
|
|
68
|
+
// Try to switch to existing branch
|
|
69
69
|
const result = execSafe(`git checkout ${branch}`, cwd);
|
|
70
70
|
if (!result.ok) {
|
|
71
|
+
// Branch doesn't exist or checkout failed — clean up and create fresh
|
|
72
|
+
execSafe(`git branch -D ${branch}`, cwd); // Delete if exists but broken
|
|
71
73
|
exec(`git checkout -b ${branch}`, cwd);
|
|
72
74
|
}
|
|
73
75
|
}
|
|
74
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
|
+
|
|
92
|
+
// Delete remote branch (ignore errors if doesn't exist)
|
|
93
|
+
execSafe(`git push origin --delete ${branch}`, cwd);
|
|
94
|
+
|
|
95
|
+
// Create fresh branch from base
|
|
96
|
+
exec(`git checkout -b ${branch}`, cwd);
|
|
97
|
+
}
|
|
98
|
+
|
|
75
99
|
/**
|
|
76
100
|
* Commit all changes
|
|
77
101
|
*/
|
|
@@ -111,6 +135,6 @@ export function checkoutMain(cwd, baseBranch = 'main') {
|
|
|
111
135
|
}
|
|
112
136
|
|
|
113
137
|
export const git = {
|
|
114
|
-
isClean, currentBranch, getRepoInfo, checkoutBranch,
|
|
138
|
+
isClean, currentBranch, getRepoInfo, checkoutBranch, resetBranch,
|
|
115
139
|
commitAll, pushBranch, checkoutMain, exec, execSafe,
|
|
116
140
|
};
|
package/src/utils/github.js
CHANGED
|
@@ -306,14 +306,73 @@ export function isPrivateRepo(cwd) {
|
|
|
306
306
|
}
|
|
307
307
|
|
|
308
308
|
/**
|
|
309
|
-
* Request bots to re-review the PR
|
|
310
|
-
*
|
|
309
|
+
* Request bots to re-review the PR.
|
|
310
|
+
* Auto-detects which review bots have previously commented on the PR
|
|
311
|
+
* and triggers only those. Falls back to all known bot triggers.
|
|
311
312
|
*/
|
|
312
313
|
export function requestReReview(cwd, prNumber) {
|
|
314
|
+
// Known review bots and their re-review trigger commands
|
|
315
|
+
const REVIEW_BOTS = [
|
|
316
|
+
{ login: 'gemini-code-assist', trigger: '/gemini review' },
|
|
317
|
+
{ login: 'coderabbitai', trigger: '@coderabbitai review' },
|
|
318
|
+
{ login: 'sourcery-ai', trigger: '@sourcery-ai review' },
|
|
319
|
+
{ login: 'deepsource', trigger: '@deepsource review' },
|
|
320
|
+
{ login: 'sweep-ai', trigger: 'sweep: review' },
|
|
321
|
+
{ login: 'openai-codex', trigger: '@codex review' },
|
|
322
|
+
{ login: 'claude', trigger: '@claude review' },
|
|
323
|
+
{ login: 'copilot', trigger: '@copilot review' },
|
|
324
|
+
];
|
|
325
|
+
|
|
313
326
|
try {
|
|
314
327
|
const num = validatePRNumber(prNumber);
|
|
315
|
-
|
|
316
|
-
|
|
328
|
+
|
|
329
|
+
// Detect which bots have previously interacted with this PR
|
|
330
|
+
const reviews = getReviews(cwd, num);
|
|
331
|
+
const comments = getIssueComments(cwd, num);
|
|
332
|
+
const reviewComments = getReviewComments(cwd, num);
|
|
333
|
+
|
|
334
|
+
const allLogins = new Set();
|
|
335
|
+
for (const r of reviews) if (r.user?.login) allLogins.add(r.user.login);
|
|
336
|
+
for (const c of comments) if (c.user?.login) allLogins.add(c.user.login);
|
|
337
|
+
for (const c of reviewComments) if (c.user?.login) allLogins.add(c.user.login);
|
|
338
|
+
|
|
339
|
+
// Find matching bots that have been active on this PR
|
|
340
|
+
const activeBots = REVIEW_BOTS.filter(bot =>
|
|
341
|
+
[...allLogins].some(login => login.includes(bot.login))
|
|
342
|
+
);
|
|
343
|
+
|
|
344
|
+
// Determine which triggers to fire
|
|
345
|
+
const triggers = activeBots.length > 0
|
|
346
|
+
? activeBots.filter(b => b.trigger).map(b => b.trigger)
|
|
347
|
+
: REVIEW_BOTS.filter(b => b.trigger).map(b => b.trigger).slice(0, 1); // fallback: try gemini
|
|
348
|
+
|
|
349
|
+
// Fire triggers as separate comments
|
|
350
|
+
let triggered = 0;
|
|
351
|
+
for (const trigger of triggers) {
|
|
352
|
+
try {
|
|
353
|
+
gh(`pr comment ${num} --body "${trigger}"`, cwd);
|
|
354
|
+
triggered++;
|
|
355
|
+
} catch { /* individual trigger failure is non-critical */ }
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// Also try to request re-review from previous human reviewers via GitHub API
|
|
359
|
+
try {
|
|
360
|
+
const prReviews = ghJSON(`pr view ${num} --json reviews --jq '.reviews[].author.login'`, cwd);
|
|
361
|
+
if (prReviews && typeof prReviews === 'string') {
|
|
362
|
+
const logins = [...new Set(prReviews.trim().split('\n').filter(l => l && !l.includes('[bot]')))];
|
|
363
|
+
if (logins.length > 0) {
|
|
364
|
+
const owner = execSync('gh repo view --json owner --jq .owner.login', { cwd, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
|
|
365
|
+
const repo = execSync('gh repo view --json name --jq .name', { cwd, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'] }).trim();
|
|
366
|
+
execSync(`gh api repos/${owner}/${repo}/pulls/${num}/requested_reviewers -f "reviewers[]=${logins[0]}" -X POST`, {
|
|
367
|
+
cwd, encoding: 'utf-8', stdio: ['pipe', 'pipe', 'pipe'],
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
} catch {
|
|
372
|
+
// Non-critical: human re-review request failed
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
return triggered > 0;
|
|
317
376
|
} catch {
|
|
318
377
|
return false;
|
|
319
378
|
}
|
package/src/utils/provider.js
CHANGED
|
@@ -225,7 +225,7 @@ async function executeCLI(prov, providerId, promptPath, cwd) {
|
|
|
225
225
|
});
|
|
226
226
|
|
|
227
227
|
let lastFile = '';
|
|
228
|
-
let statusText =
|
|
228
|
+
let statusText = command.substring(0, 80);
|
|
229
229
|
let lineBuffer = '';
|
|
230
230
|
const FILE_EXT = /(?:^|\s|['"|(/])([a-zA-Z0-9_.\/-]+\.(?:rs|ts|js|jsx|tsx|py|go|toml|yaml|yml|json|md|css|html|sh|sql|prisma|vue|svelte))\b/;
|
|
231
231
|
|
|
@@ -234,7 +234,7 @@ async function executeCLI(prov, providerId, promptPath, cwd) {
|
|
|
234
234
|
let spinIdx = 0;
|
|
235
235
|
const spinTimer = setInterval(() => {
|
|
236
236
|
const frame = SPINNER[spinIdx % SPINNER.length];
|
|
237
|
-
process.stdout.write(`\r\x1b[K
|
|
237
|
+
process.stdout.write(`\r\x1b[K \x1b[36m${frame}\x1b[0m ${statusText}`);
|
|
238
238
|
spinIdx++;
|
|
239
239
|
}, 80);
|
|
240
240
|
|