@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 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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jojonax/codex-copilot",
3
- "version": "1.3.6",
3
+ "version": "1.4.1",
4
4
  "description": "PRD-driven automated development orchestrator for CodeX / Cursor",
5
5
  "bin": {
6
6
  "codex-copilot": "./bin/cli.js"
@@ -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
+ }
@@ -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 completed threshold (and not the resuming task)
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
 
@@ -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, create if not exists
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
  };
@@ -306,14 +306,73 @@ export function isPrivateRepo(cwd) {
306
306
  }
307
307
 
308
308
  /**
309
- * Request bots to re-review the PR by posting a /review comment.
310
- * Triggers Gemini Code Assist and similar bots to run a fresh review.
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
- gh(`pr comment ${num} --body "/review"`, cwd);
316
- return true;
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
  }
@@ -225,7 +225,7 @@ async function executeCLI(prov, providerId, promptPath, cwd) {
225
225
  });
226
226
 
227
227
  let lastFile = '';
228
- let statusText = 'Working...';
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 \x1b[36m${frame}\x1b[0m ${statusText}`);
237
+ process.stdout.write(`\r\x1b[K \x1b[36m${frame}\x1b[0m ${statusText}`);
238
238
  spinIdx++;
239
239
  }, 80);
240
240