@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.
- package/LICENSE +21 -21
- package/README.md +144 -44
- package/bin/cli.js +189 -182
- package/package.json +39 -39
- package/src/commands/evolve.js +316 -316
- package/src/commands/fix.js +447 -447
- package/src/commands/init.js +298 -298
- package/src/commands/reset.js +61 -61
- package/src/commands/retry.js +190 -190
- package/src/commands/run.js +958 -958
- package/src/commands/skip.js +62 -62
- package/src/commands/status.js +95 -95
- package/src/commands/usage.js +361 -361
- package/src/utils/automator.js +279 -279
- package/src/utils/checkpoint.js +246 -246
- package/src/utils/detect-prd.js +137 -137
- package/src/utils/git.js +388 -388
- package/src/utils/github.js +486 -486
- package/src/utils/json.js +220 -220
- package/src/utils/logger.js +41 -41
- package/src/utils/prompt.js +49 -49
- package/src/utils/provider.js +770 -769
- package/src/utils/self-heal.js +330 -330
- package/src/utils/shell-bootstrap.js +404 -0
- package/src/utils/update-check.js +103 -103
package/src/commands/evolve.js
CHANGED
|
@@ -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
|
+
}
|