@jojonax/codex-copilot 1.5.5 → 1.6.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.
@@ -1,316 +1,316 @@
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 { readJSON, writeJSON } from '../utils/json.js';
17
- import { run } from './run.js';
18
-
19
-
20
-
21
- /**
22
- * Load or initialize rounds.json
23
- */
24
- function loadRounds(copilotDir) {
25
- const roundsPath = resolve(copilotDir, 'rounds.json');
26
- if (existsSync(roundsPath)) {
27
- return readJSON(roundsPath);
28
- }
29
- return { current_round: 0, rounds: [] };
30
- }
31
-
32
- function saveRounds(copilotDir, rounds) {
33
- writeJSON(resolve(copilotDir, 'rounds.json'), rounds);
34
- }
35
-
36
- /**
37
- * Archive the current round: count results, save summary
38
- */
39
- function archiveCurrentRound(copilotDir, tasks, roundNumber) {
40
- const completed = tasks.tasks.filter(t => t.status === 'completed');
41
- const blocked = tasks.tasks.filter(t => t.status === 'blocked');
42
- const skipped = tasks.tasks.filter(t => t.status === 'skipped');
43
-
44
- // Build a text summary of what was completed
45
- const completedSummary = completed.map(t =>
46
- `- Task #${t.id}: ${t.title}`
47
- ).join('\n');
48
-
49
- const blockedSummary = blocked.length > 0
50
- ? '\n\nBlocked tasks:\n' + blocked.map(t => `- Task #${t.id}: ${t.title}`).join('\n')
51
- : '';
52
-
53
- const summary = `Round ${roundNumber} completed ${completed.length}/${tasks.total} tasks.\n\nCompleted:\n${completedSummary}${blockedSummary}`;
54
-
55
- const roundEntry = {
56
- round: roundNumber,
57
- started_at: tasks.tasks[0]?.started_at || new Date().toISOString(),
58
- completed_at: new Date().toISOString(),
59
- tasks_total: tasks.total,
60
- tasks_completed: completed.length,
61
- tasks_blocked: blocked.length,
62
- tasks_skipped: skipped.length,
63
- summary,
64
- // Store completed task titles + descriptions for gap analysis
65
- completed_features: completed.map(t => ({
66
- title: t.title,
67
- description: t.description,
68
- acceptance: t.acceptance,
69
- })),
70
- };
71
-
72
- // Archive tasks.json → tasks_round_N.json
73
- const archivePath = resolve(copilotDir, `tasks_round_${roundNumber}.json`);
74
- copyFileSync(resolve(copilotDir, 'tasks.json'), archivePath);
75
- log.dim(` Archived to tasks_round_${roundNumber}.json`);
76
-
77
- return roundEntry;
78
- }
79
-
80
- /**
81
- * Build the gap analysis + next-round planning prompt
82
- */
83
- function buildEvolvePrompt(prdContent, rounds, copilotDir) {
84
- // Collect all completed features across all rounds
85
- const allCompleted = [];
86
- for (const r of rounds.rounds) {
87
- if (r.completed_features) {
88
- for (const f of r.completed_features) {
89
- allCompleted.push(f);
90
- }
91
- }
92
- }
93
-
94
- const completedSection = allCompleted.length > 0
95
- ? allCompleted.map((f, i) => `${i + 1}. **${f.title}**\n ${f.description?.substring(0, 200) || ''}`).join('\n\n')
96
- : 'No features completed yet.';
97
-
98
- const roundHistorySection = rounds.rounds.map(r =>
99
- `### Round ${r.round}\n- Completed: ${r.tasks_completed}/${r.tasks_total} tasks\n- Summary: ${r.summary?.substring(0, 300) || 'N/A'}`
100
- ).join('\n\n');
101
-
102
- const nextRound = rounds.current_round + 1;
103
-
104
- return `You are a senior software architect planning the next development iteration.
105
-
106
- ## Context
107
-
108
- This is Round ${nextRound} of an iterative PRD implementation.
109
-
110
- ### Full PRD Document
111
-
112
- ${prdContent}
113
-
114
- ### Development History
115
-
116
- ${roundHistorySection}
117
-
118
- ### Features Already Completed (across all rounds)
119
-
120
- ${completedSection}
121
-
122
- ## Your Task
123
-
124
- 1. **Gap Analysis**: Compare the PRD requirements against the completed features. Identify ALL remaining features, improvements, and details that have NOT been implemented yet.
125
-
126
- 2. **Priority Planning**: From the gaps identified, select the most important batch for Round ${nextRound}. Consider:
127
- - Dependencies (build foundations before features)
128
- - User value (core features before polish)
129
- - Technical feasibility (group related changes)
130
- - Moderate batch size (8-15 tasks per round)
131
-
132
- 3. **Generate tasks.json**: Output the next round's task plan.
133
-
134
- ## Output Format
135
-
136
- Write the result to \`.codex-copilot/tasks.json\` in the following format:
137
-
138
- \`\`\`json
139
- {
140
- "project": "Project Name",
141
- "round": ${nextRound},
142
- "total": N,
143
- "tasks": [
144
- {
145
- "id": 1,
146
- "title": "Task title (brief)",
147
- "description": "Detailed task description with specific features and technical details",
148
- "acceptance": ["Acceptance criteria 1", "Acceptance criteria 2"],
149
- "branch": "feature/${String(nextRound).padStart(2, '0')}01-task-slug",
150
- "status": "pending",
151
- "depends_on": []
152
- }
153
- ]
154
- }
155
- \`\`\`
156
-
157
- ## Important Rules
158
- 1. Do NOT re-implement features that are already completed
159
- 2. Branch names should be prefixed with the round number: feature/${String(nextRound).padStart(2, '0')}XX-slug
160
- 3. Each task must be specific and actionable (not vague like "improve performance")
161
- 4. Include detailed descriptions that reference the PRD's specific requirements
162
- 5. If the PRD has been fully implemented, output an empty tasks array with a comment
163
- `;
164
- }
165
-
166
- export async function evolve(projectDir) {
167
- log.title('🔄 Multi-Round PRD Evolution');
168
- log.blank();
169
-
170
- const copilotDir = resolve(projectDir, '.codex-copilot');
171
- const tasksPath = resolve(copilotDir, 'tasks.json');
172
- const configPath = resolve(copilotDir, 'config.json');
173
-
174
- // Validate project state
175
- if (!existsSync(copilotDir) || !existsSync(configPath)) {
176
- log.error('Project not initialized. Run: codex-copilot init');
177
- closePrompt();
178
- process.exit(1);
179
- }
180
-
181
- const config = readJSON(configPath);
182
- const providerId = config.provider || 'codex-cli';
183
- const rounds = loadRounds(copilotDir);
184
-
185
- // ===== Step 1: Archive current round =====
186
- if (existsSync(tasksPath)) {
187
- const tasks = readJSON(tasksPath);
188
- const hasCompletedTasks = tasks.tasks?.some(t => t.status === 'completed');
189
- const roundNumber = tasks.round || rounds.current_round || 1;
190
-
191
- // Check if this round was already archived (prevent duplicate archiving)
192
- const archiveExists = existsSync(resolve(copilotDir, `tasks_round_${roundNumber}.json`));
193
- const alreadyInHistory = rounds.rounds.some(r => r.round === roundNumber);
194
-
195
- if (hasCompletedTasks && !archiveExists && !alreadyInHistory) {
196
- log.step(`Step 1: Archiving Round ${roundNumber}...`);
197
-
198
- const roundEntry = archiveCurrentRound(copilotDir, tasks, roundNumber);
199
- rounds.rounds.push(roundEntry);
200
- rounds.current_round = roundNumber;
201
- saveRounds(copilotDir, rounds);
202
-
203
- const pct = Math.round((roundEntry.tasks_completed / roundEntry.tasks_total) * 100);
204
- log.info(` Round ${roundNumber}: ${roundEntry.tasks_completed}/${roundEntry.tasks_total} tasks (${pct}%)`);
205
- log.blank();
206
- } else if (hasCompletedTasks && (archiveExists || alreadyInHistory)) {
207
- log.dim(` Round ${roundNumber} already archived — skipping`);
208
- // Ensure rounds.current_round is up to date
209
- if (rounds.current_round < roundNumber) {
210
- rounds.current_round = roundNumber;
211
- saveRounds(copilotDir, rounds);
212
- }
213
- log.blank();
214
- } else if (rounds.current_round === 0) {
215
- log.warn('No completed tasks found. Starting from Round 1.');
216
- rounds.current_round = 0;
217
- saveRounds(copilotDir, rounds);
218
- log.blank();
219
- }
220
- }
221
-
222
- // ===== Step 2 + 3: Gap analysis & next-round planning =====
223
- const nextRound = rounds.current_round + 1;
224
- log.step(`Step 2: Planning Round ${nextRound} (Gap Analysis + Task Planning)...`);
225
- log.blank();
226
-
227
- // Read PRD
228
- let prdContent;
229
- try {
230
- prdContent = readPRD(config.prd_path);
231
- } catch {
232
- log.error(`Cannot read PRD: ${config.prd_path}`);
233
- closePrompt();
234
- process.exit(1);
235
- }
236
-
237
- // Build the evolve prompt
238
- const evolvePrompt = buildEvolvePrompt(prdContent, rounds, copilotDir);
239
- const promptPath = resolve(copilotDir, 'evolve-prompt.md');
240
- writeFileSync(promptPath, evolvePrompt);
241
- log.info(` Evolve prompt saved: .codex-copilot/evolve-prompt.md`);
242
- log.blank();
243
-
244
- // Execute via AI provider
245
- log.step(`Invoking AI to analyze gaps and plan Round ${nextRound}...`);
246
- const result = await provider.executePrompt(providerId, promptPath, projectDir);
247
-
248
- if (!result.ok) {
249
- log.error('AI invocation failed. You can manually edit the prompt and retry.');
250
- closePrompt();
251
- process.exit(1);
252
- }
253
-
254
- // ===== Step 4: Validate new tasks.json =====
255
- if (!existsSync(tasksPath)) {
256
- log.error('AI did not generate tasks.json. Check the evolve prompt output.');
257
- closePrompt();
258
- process.exit(1);
259
- }
260
-
261
- let newTasks;
262
- try {
263
- newTasks = readJSON(tasksPath);
264
- } catch {
265
- log.error('Generated tasks.json is invalid JSON.');
266
- closePrompt();
267
- process.exit(1);
268
- }
269
-
270
- if (!newTasks.tasks || newTasks.tasks.length === 0) {
271
- log.blank();
272
- log.title('🎉 PRD fully implemented!');
273
- log.info('All features from the PRD have been built across all rounds.');
274
- log.info(`Total rounds: ${rounds.current_round}`);
275
-
276
- // Show round history
277
- for (const r of rounds.rounds) {
278
- log.dim(` Round ${r.round}: ${r.tasks_completed}/${r.tasks_total} tasks`);
279
- }
280
-
281
- closePrompt();
282
- return;
283
- }
284
-
285
- // Update round tracking
286
- rounds.current_round = nextRound;
287
- saveRounds(copilotDir, rounds);
288
-
289
- // Reset checkpoint for fresh round
290
- const statePath = resolve(copilotDir, 'state.json');
291
- writeJSON(statePath, {
292
- current_task: 0,
293
- phase: null,
294
- phase_step: null,
295
- current_pr: null,
296
- review_round: 0,
297
- branch: null,
298
- status: 'initialized',
299
- last_updated: new Date().toISOString(),
300
- });
301
-
302
- log.blank();
303
- log.title(`✅ Round ${nextRound} planned — ${newTasks.tasks.length} tasks`);
304
- log.blank();
305
-
306
- for (const t of newTasks.tasks) {
307
- log.dim(` ${t.id}. ${t.title}`);
308
- }
309
- log.blank();
310
-
311
- // ===== Step 5: Auto-start run =====
312
- log.step(`Starting Round ${nextRound} execution...`);
313
- log.blank();
314
-
315
- await run(projectDir);
316
- }
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 { readJSON, writeJSON } from '../utils/json.js';
17
+ import { run } from './run.js';
18
+
19
+
20
+
21
+ /**
22
+ * Load or initialize rounds.json
23
+ */
24
+ function loadRounds(copilotDir) {
25
+ const roundsPath = resolve(copilotDir, 'rounds.json');
26
+ if (existsSync(roundsPath)) {
27
+ return readJSON(roundsPath);
28
+ }
29
+ return { current_round: 0, rounds: [] };
30
+ }
31
+
32
+ function saveRounds(copilotDir, rounds) {
33
+ writeJSON(resolve(copilotDir, 'rounds.json'), rounds);
34
+ }
35
+
36
+ /**
37
+ * Archive the current round: count results, save summary
38
+ */
39
+ function archiveCurrentRound(copilotDir, tasks, roundNumber) {
40
+ const completed = tasks.tasks.filter(t => t.status === 'completed');
41
+ const blocked = tasks.tasks.filter(t => t.status === 'blocked');
42
+ const skipped = tasks.tasks.filter(t => t.status === 'skipped');
43
+
44
+ // Build a text summary of what was completed
45
+ const completedSummary = completed.map(t =>
46
+ `- Task #${t.id}: ${t.title}`
47
+ ).join('\n');
48
+
49
+ const blockedSummary = blocked.length > 0
50
+ ? '\n\nBlocked tasks:\n' + blocked.map(t => `- Task #${t.id}: ${t.title}`).join('\n')
51
+ : '';
52
+
53
+ const summary = `Round ${roundNumber} completed ${completed.length}/${tasks.total} tasks.\n\nCompleted:\n${completedSummary}${blockedSummary}`;
54
+
55
+ const roundEntry = {
56
+ round: roundNumber,
57
+ started_at: tasks.tasks[0]?.started_at || new Date().toISOString(),
58
+ completed_at: new Date().toISOString(),
59
+ tasks_total: tasks.total,
60
+ tasks_completed: completed.length,
61
+ tasks_blocked: blocked.length,
62
+ tasks_skipped: skipped.length,
63
+ summary,
64
+ // Store completed task titles + descriptions for gap analysis
65
+ completed_features: completed.map(t => ({
66
+ title: t.title,
67
+ description: t.description,
68
+ acceptance: t.acceptance,
69
+ })),
70
+ };
71
+
72
+ // Archive tasks.json → tasks_round_N.json
73
+ const archivePath = resolve(copilotDir, `tasks_round_${roundNumber}.json`);
74
+ copyFileSync(resolve(copilotDir, 'tasks.json'), archivePath);
75
+ log.dim(` Archived to tasks_round_${roundNumber}.json`);
76
+
77
+ return roundEntry;
78
+ }
79
+
80
+ /**
81
+ * Build the gap analysis + next-round planning prompt
82
+ */
83
+ function buildEvolvePrompt(prdContent, rounds, copilotDir) {
84
+ // Collect all completed features across all rounds
85
+ const allCompleted = [];
86
+ for (const r of rounds.rounds) {
87
+ if (r.completed_features) {
88
+ for (const f of r.completed_features) {
89
+ allCompleted.push(f);
90
+ }
91
+ }
92
+ }
93
+
94
+ const completedSection = allCompleted.length > 0
95
+ ? allCompleted.map((f, i) => `${i + 1}. **${f.title}**\n ${f.description?.substring(0, 200) || ''}`).join('\n\n')
96
+ : 'No features completed yet.';
97
+
98
+ const roundHistorySection = rounds.rounds.map(r =>
99
+ `### Round ${r.round}\n- Completed: ${r.tasks_completed}/${r.tasks_total} tasks\n- Summary: ${r.summary?.substring(0, 300) || 'N/A'}`
100
+ ).join('\n\n');
101
+
102
+ const nextRound = rounds.current_round + 1;
103
+
104
+ return `You are a senior software architect planning the next development iteration.
105
+
106
+ ## Context
107
+
108
+ This is Round ${nextRound} of an iterative PRD implementation.
109
+
110
+ ### Full PRD Document
111
+
112
+ ${prdContent}
113
+
114
+ ### Development History
115
+
116
+ ${roundHistorySection}
117
+
118
+ ### Features Already Completed (across all rounds)
119
+
120
+ ${completedSection}
121
+
122
+ ## Your Task
123
+
124
+ 1. **Gap Analysis**: Compare the PRD requirements against the completed features. Identify ALL remaining features, improvements, and details that have NOT been implemented yet.
125
+
126
+ 2. **Priority Planning**: From the gaps identified, select the most important batch for Round ${nextRound}. Consider:
127
+ - Dependencies (build foundations before features)
128
+ - User value (core features before polish)
129
+ - Technical feasibility (group related changes)
130
+ - Moderate batch size (8-15 tasks per round)
131
+
132
+ 3. **Generate tasks.json**: Output the next round's task plan.
133
+
134
+ ## Output Format
135
+
136
+ Write the result to \`.codex-copilot/tasks.json\` in the following format:
137
+
138
+ \`\`\`json
139
+ {
140
+ "project": "Project Name",
141
+ "round": ${nextRound},
142
+ "total": N,
143
+ "tasks": [
144
+ {
145
+ "id": 1,
146
+ "title": "Task title (brief)",
147
+ "description": "Detailed task description with specific features and technical details",
148
+ "acceptance": ["Acceptance criteria 1", "Acceptance criteria 2"],
149
+ "branch": "feature/${String(nextRound).padStart(2, '0')}01-task-slug",
150
+ "status": "pending",
151
+ "depends_on": []
152
+ }
153
+ ]
154
+ }
155
+ \`\`\`
156
+
157
+ ## Important Rules
158
+ 1. Do NOT re-implement features that are already completed
159
+ 2. Branch names should be prefixed with the round number: feature/${String(nextRound).padStart(2, '0')}XX-slug
160
+ 3. Each task must be specific and actionable (not vague like "improve performance")
161
+ 4. Include detailed descriptions that reference the PRD's specific requirements
162
+ 5. If the PRD has been fully implemented, output an empty tasks array with a comment
163
+ `;
164
+ }
165
+
166
+ export async function evolve(projectDir) {
167
+ log.title('🔄 Multi-Round PRD Evolution');
168
+ log.blank();
169
+
170
+ const copilotDir = resolve(projectDir, '.codex-copilot');
171
+ const tasksPath = resolve(copilotDir, 'tasks.json');
172
+ const configPath = resolve(copilotDir, 'config.json');
173
+
174
+ // Validate project state
175
+ if (!existsSync(copilotDir) || !existsSync(configPath)) {
176
+ log.error('Project not initialized. Run: codex-copilot init');
177
+ closePrompt();
178
+ process.exit(1);
179
+ }
180
+
181
+ const config = readJSON(configPath);
182
+ const providerId = config.provider || 'codex-cli';
183
+ const rounds = loadRounds(copilotDir);
184
+
185
+ // ===== Step 1: Archive current round =====
186
+ if (existsSync(tasksPath)) {
187
+ const tasks = readJSON(tasksPath);
188
+ const hasCompletedTasks = tasks.tasks?.some(t => t.status === 'completed');
189
+ const roundNumber = tasks.round || rounds.current_round || 1;
190
+
191
+ // Check if this round was already archived (prevent duplicate archiving)
192
+ const archiveExists = existsSync(resolve(copilotDir, `tasks_round_${roundNumber}.json`));
193
+ const alreadyInHistory = rounds.rounds.some(r => r.round === roundNumber);
194
+
195
+ if (hasCompletedTasks && !archiveExists && !alreadyInHistory) {
196
+ log.step(`Step 1: Archiving Round ${roundNumber}...`);
197
+
198
+ const roundEntry = archiveCurrentRound(copilotDir, tasks, roundNumber);
199
+ rounds.rounds.push(roundEntry);
200
+ rounds.current_round = roundNumber;
201
+ saveRounds(copilotDir, rounds);
202
+
203
+ const pct = Math.round((roundEntry.tasks_completed / roundEntry.tasks_total) * 100);
204
+ log.info(` Round ${roundNumber}: ${roundEntry.tasks_completed}/${roundEntry.tasks_total} tasks (${pct}%)`);
205
+ log.blank();
206
+ } else if (hasCompletedTasks && (archiveExists || alreadyInHistory)) {
207
+ log.dim(` Round ${roundNumber} already archived — skipping`);
208
+ // Ensure rounds.current_round is up to date
209
+ if (rounds.current_round < roundNumber) {
210
+ rounds.current_round = roundNumber;
211
+ saveRounds(copilotDir, rounds);
212
+ }
213
+ log.blank();
214
+ } else if (rounds.current_round === 0) {
215
+ log.warn('No completed tasks found. Starting from Round 1.');
216
+ rounds.current_round = 0;
217
+ saveRounds(copilotDir, rounds);
218
+ log.blank();
219
+ }
220
+ }
221
+
222
+ // ===== Step 2 + 3: Gap analysis & next-round planning =====
223
+ const nextRound = rounds.current_round + 1;
224
+ log.step(`Step 2: Planning Round ${nextRound} (Gap Analysis + Task Planning)...`);
225
+ log.blank();
226
+
227
+ // Read PRD
228
+ let prdContent;
229
+ try {
230
+ prdContent = readPRD(config.prd_path);
231
+ } catch {
232
+ log.error(`Cannot read PRD: ${config.prd_path}`);
233
+ closePrompt();
234
+ process.exit(1);
235
+ }
236
+
237
+ // Build the evolve prompt
238
+ const evolvePrompt = buildEvolvePrompt(prdContent, rounds, copilotDir);
239
+ const promptPath = resolve(copilotDir, 'evolve-prompt.md');
240
+ writeFileSync(promptPath, evolvePrompt);
241
+ log.info(` Evolve prompt saved: .codex-copilot/evolve-prompt.md`);
242
+ log.blank();
243
+
244
+ // Execute via AI provider
245
+ log.step(`Invoking AI to analyze gaps and plan Round ${nextRound}...`);
246
+ const result = await provider.executePrompt(providerId, promptPath, projectDir);
247
+
248
+ if (!result.ok) {
249
+ log.error('AI invocation failed. You can manually edit the prompt and retry.');
250
+ closePrompt();
251
+ process.exit(1);
252
+ }
253
+
254
+ // ===== Step 4: Validate new tasks.json =====
255
+ if (!existsSync(tasksPath)) {
256
+ log.error('AI did not generate tasks.json. Check the evolve prompt output.');
257
+ closePrompt();
258
+ process.exit(1);
259
+ }
260
+
261
+ let newTasks;
262
+ try {
263
+ newTasks = readJSON(tasksPath);
264
+ } catch {
265
+ log.error('Generated tasks.json is invalid JSON.');
266
+ closePrompt();
267
+ process.exit(1);
268
+ }
269
+
270
+ if (!newTasks.tasks || newTasks.tasks.length === 0) {
271
+ log.blank();
272
+ log.title('🎉 PRD fully implemented!');
273
+ log.info('All features from the PRD have been built across all rounds.');
274
+ log.info(`Total rounds: ${rounds.current_round}`);
275
+
276
+ // Show round history
277
+ for (const r of rounds.rounds) {
278
+ log.dim(` Round ${r.round}: ${r.tasks_completed}/${r.tasks_total} tasks`);
279
+ }
280
+
281
+ closePrompt();
282
+ return;
283
+ }
284
+
285
+ // Update round tracking
286
+ rounds.current_round = nextRound;
287
+ saveRounds(copilotDir, rounds);
288
+
289
+ // Reset checkpoint for fresh round
290
+ const statePath = resolve(copilotDir, 'state.json');
291
+ writeJSON(statePath, {
292
+ current_task: 0,
293
+ phase: null,
294
+ phase_step: null,
295
+ current_pr: null,
296
+ review_round: 0,
297
+ branch: null,
298
+ status: 'initialized',
299
+ last_updated: new Date().toISOString(),
300
+ });
301
+
302
+ log.blank();
303
+ log.title(`✅ Round ${nextRound} planned — ${newTasks.tasks.length} tasks`);
304
+ log.blank();
305
+
306
+ for (const t of newTasks.tasks) {
307
+ log.dim(` ${t.id}. ${t.title}`);
308
+ }
309
+ log.blank();
310
+
311
+ // ===== Step 5: Auto-start run =====
312
+ log.step(`Starting Round ${nextRound} execution...`);
313
+ log.blank();
314
+
315
+ await run(projectDir);
316
+ }