@jojonax/codex-copilot 1.3.5 → 1.4.0
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 +27 -2
- package/package.json +1 -1
- package/src/commands/evolve.js +321 -0
- package/src/commands/run.js +32 -2
- package/src/commands/status.js +18 -0
- package/src/commands/usage.js +329 -0
- 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,11 +97,32 @@ 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 {
|
|
101
|
-
execSync('npm install -g @jojonax/codex-copilot@latest --force', {
|
|
102
|
-
|
|
117
|
+
execSync('npm install -g @jojonax/codex-copilot@latest --force', {
|
|
118
|
+
encoding: 'utf-8',
|
|
119
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
120
|
+
});
|
|
121
|
+
const newVersion = execSync('npm view @jojonax/codex-copilot version', {
|
|
122
|
+
encoding: 'utf-8',
|
|
123
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
124
|
+
}).trim();
|
|
125
|
+
log.info(`✅ Updated to v${newVersion}`);
|
|
103
126
|
} catch {
|
|
104
127
|
log.error('Update failed. Try manually: npm install -g @jojonax/codex-copilot@latest --force');
|
|
105
128
|
}
|
|
@@ -125,6 +148,8 @@ async function main() {
|
|
|
125
148
|
console.log(' reset Reset state and start over');
|
|
126
149
|
console.log(' retry Retry blocked tasks with enhanced prompts');
|
|
127
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)');
|
|
128
153
|
console.log(' update Update to latest version');
|
|
129
154
|
console.log('');
|
|
130
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,22 @@ 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
|
+
// Checkpoint is NOT cleared — it stays at the last position for non-blocked tasks.
|
|
99
|
+
const blockedTasks = tasks.tasks.filter(t => t.status === 'blocked');
|
|
100
|
+
if (blockedTasks.length > 0) {
|
|
101
|
+
log.blank();
|
|
102
|
+
log.info(`🔄 Auto-retrying ${blockedTasks.length} blocked task(s)...`);
|
|
103
|
+
for (const bt of blockedTasks) {
|
|
104
|
+
bt.status = 'pending';
|
|
105
|
+
bt.retry_count = (bt.retry_count || 0) + 1;
|
|
106
|
+
bt._retrying = true; // Flag: don't skip even if below checkpoint
|
|
107
|
+
log.dim(` ↳ Task #${bt.id}: ${bt.title.substring(0, 50)} (attempt ${bt.retry_count + 1})`);
|
|
108
|
+
}
|
|
109
|
+
writeJSON(tasksPath, tasks);
|
|
110
|
+
}
|
|
111
|
+
|
|
96
112
|
const completedCount = tasks.tasks.filter(t => t.status === 'completed').length;
|
|
97
113
|
log.blank();
|
|
98
114
|
progressBar(completedCount, tasks.total, `${completedCount}/${tasks.total} tasks done`);
|
|
@@ -103,9 +119,12 @@ export async function run(projectDir) {
|
|
|
103
119
|
// Skip fully completed tasks
|
|
104
120
|
if (task.status === 'completed' || task.status === 'skipped') continue;
|
|
105
121
|
|
|
106
|
-
// Skip tasks whose ID is below the
|
|
122
|
+
// Skip tasks whose ID is below the checkpoint (unless retrying a blocked task)
|
|
107
123
|
const isResumingTask = state.current_task === task.id && state.phase;
|
|
108
|
-
if (task.id < state.current_task && !isResumingTask) continue;
|
|
124
|
+
if (task.id < state.current_task && !isResumingTask && !task._retrying) continue;
|
|
125
|
+
|
|
126
|
+
// Clean up retry flag (used only to bypass checkpoint skip)
|
|
127
|
+
delete task._retrying;
|
|
109
128
|
|
|
110
129
|
log.blank();
|
|
111
130
|
log.title(`━━━ Task ${task.id}/${tasks.total}: ${task.title} ━━━`);
|
|
@@ -217,6 +236,17 @@ export async function run(projectDir) {
|
|
|
217
236
|
log.blank();
|
|
218
237
|
process.removeListener('SIGINT', gracefulShutdown);
|
|
219
238
|
process.removeListener('SIGTERM', gracefulShutdown);
|
|
239
|
+
|
|
240
|
+
// ===== Auto-evolve: trigger next round if enabled =====
|
|
241
|
+
if (config.auto_evolve !== false) {
|
|
242
|
+
log.blank();
|
|
243
|
+
log.info('🔄 Auto-evolving to next round...');
|
|
244
|
+
log.blank();
|
|
245
|
+
const { evolve } = await import('./evolve.js');
|
|
246
|
+
await evolve(projectDir);
|
|
247
|
+
return; // evolve() calls run() internally
|
|
248
|
+
}
|
|
249
|
+
|
|
220
250
|
closePrompt();
|
|
221
251
|
}
|
|
222
252
|
|
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/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
|
|