@jojonax/codex-copilot 1.5.1 → 1.5.2
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 +8 -0
- package/package.json +1 -1
- package/src/commands/evolve.js +1 -6
- package/src/commands/fix.js +399 -0
- package/src/commands/reset.js +4 -3
- package/src/commands/retry.js +1 -6
- package/src/commands/run.js +47 -15
- package/src/commands/skip.js +1 -7
- package/src/commands/status.js +4 -3
- package/src/utils/json.js +190 -0
- package/src/utils/provider.js +1 -1
package/bin/cli.js
CHANGED
|
@@ -24,6 +24,7 @@ import { retry } from '../src/commands/retry.js';
|
|
|
24
24
|
import { skip } from '../src/commands/skip.js';
|
|
25
25
|
import { usage } from '../src/commands/usage.js';
|
|
26
26
|
import { evolve } from '../src/commands/evolve.js';
|
|
27
|
+
import { fix } from '../src/commands/fix.js';
|
|
27
28
|
import { log } from '../src/utils/logger.js';
|
|
28
29
|
import { checkForUpdates } from '../src/utils/update-check.js';
|
|
29
30
|
|
|
@@ -102,6 +103,12 @@ async function main() {
|
|
|
102
103
|
process.exit(0);
|
|
103
104
|
break;
|
|
104
105
|
|
|
106
|
+
case 'fix':
|
|
107
|
+
case 'repair':
|
|
108
|
+
await fix(projectDir);
|
|
109
|
+
process.exit(0);
|
|
110
|
+
break;
|
|
111
|
+
|
|
105
112
|
case 'evolve':
|
|
106
113
|
case 'next':
|
|
107
114
|
if (!existsSync(resolve(projectDir, '.codex-copilot/config.json'))) {
|
|
@@ -150,6 +157,7 @@ async function main() {
|
|
|
150
157
|
console.log(' skip <id> Force-skip a task to unblock dependents');
|
|
151
158
|
console.log(' usage Show AI token usage for recent sessions');
|
|
152
159
|
console.log(' evolve Start next PRD iteration round (gap analysis → plan → run)');
|
|
160
|
+
console.log(' fix Repair corrupted project files');
|
|
153
161
|
console.log(' update Update to latest version');
|
|
154
162
|
console.log('');
|
|
155
163
|
console.log(' Workflow:');
|
package/package.json
CHANGED
package/src/commands/evolve.js
CHANGED
|
@@ -13,15 +13,10 @@ import { log } from '../utils/logger.js';
|
|
|
13
13
|
import { provider } from '../utils/provider.js';
|
|
14
14
|
import { readPRD } from '../utils/detect-prd.js';
|
|
15
15
|
import { closePrompt } from '../utils/prompt.js';
|
|
16
|
+
import { readJSON, writeJSON } from '../utils/json.js';
|
|
16
17
|
import { run } from './run.js';
|
|
17
18
|
|
|
18
|
-
function readJSON(path) {
|
|
19
|
-
return JSON.parse(readFileSync(path, 'utf-8'));
|
|
20
|
-
}
|
|
21
19
|
|
|
22
|
-
function writeJSON(path, data) {
|
|
23
|
-
writeFileSync(path, JSON.stringify(data, null, 2));
|
|
24
|
-
}
|
|
25
20
|
|
|
26
21
|
/**
|
|
27
22
|
* Load or initialize rounds.json
|
|
@@ -0,0 +1,399 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* codex-copilot fix - Repair corrupted or malformed project files
|
|
3
|
+
*
|
|
4
|
+
* Scans and repairs all JSON files in .codex-copilot/:
|
|
5
|
+
* - tasks.json: validates structure, fixes missing fields, re-indexes
|
|
6
|
+
* - state.json: validates checkpoint schema, resets if unrecoverable
|
|
7
|
+
* - config.json: validates required fields
|
|
8
|
+
* - rounds.json: validates round history structure
|
|
9
|
+
*
|
|
10
|
+
* Can also be invoked programmatically via autoFix() for self-healing.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import { existsSync, readdirSync } from 'fs';
|
|
14
|
+
import { resolve, basename } from 'path';
|
|
15
|
+
import { log } from '../utils/logger.js';
|
|
16
|
+
import { readJSON, writeJSON, repairRawJSON } from '../utils/json.js';
|
|
17
|
+
|
|
18
|
+
// ─── Schema Validators & Fixers ───────────────────────────
|
|
19
|
+
|
|
20
|
+
const TASK_REQUIRED_FIELDS = ['id', 'title', 'description', 'branch', 'status'];
|
|
21
|
+
const VALID_TASK_STATUSES = ['pending', 'in_progress', 'completed', 'blocked', 'skipped', 'developed'];
|
|
22
|
+
|
|
23
|
+
const DEFAULT_STATE = {
|
|
24
|
+
current_task: 0,
|
|
25
|
+
phase: null,
|
|
26
|
+
phase_step: null,
|
|
27
|
+
current_pr: null,
|
|
28
|
+
review_round: 0,
|
|
29
|
+
branch: null,
|
|
30
|
+
status: 'initialized',
|
|
31
|
+
last_updated: new Date().toISOString(),
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Fix a single JSON file: parse → validate → repair → write back.
|
|
36
|
+
* Returns { fixed, errors, warnings } report.
|
|
37
|
+
*/
|
|
38
|
+
function fixFile(filePath, validator) {
|
|
39
|
+
const report = { file: basename(filePath), fixed: false, repairs: [], errors: [] };
|
|
40
|
+
|
|
41
|
+
if (!existsSync(filePath)) {
|
|
42
|
+
report.errors.push('File does not exist');
|
|
43
|
+
return report;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Step 1: Try to parse (with auto-repair in readJSON)
|
|
47
|
+
let data;
|
|
48
|
+
try {
|
|
49
|
+
data = readJSON(filePath);
|
|
50
|
+
// readJSON already does basic corruption repair (BOM, trailing commas, etc.)
|
|
51
|
+
} catch (parseErr) {
|
|
52
|
+
// readJSON's auto-repair failed — try our deeper repair
|
|
53
|
+
try {
|
|
54
|
+
data = repairRawJSON(filePath);
|
|
55
|
+
report.repairs.push('Recovered from deep corruption via raw repair');
|
|
56
|
+
} catch {
|
|
57
|
+
report.errors.push(`Unrecoverable parse error: ${parseErr.message}`);
|
|
58
|
+
return report;
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Step 2: Run schema validator/fixer
|
|
63
|
+
if (validator) {
|
|
64
|
+
const { data: fixedData, repairs } = validator(data);
|
|
65
|
+
if (repairs.length > 0) {
|
|
66
|
+
data = fixedData;
|
|
67
|
+
report.repairs.push(...repairs);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Step 3: Write back if any repairs were made
|
|
72
|
+
if (report.repairs.length > 0) {
|
|
73
|
+
try {
|
|
74
|
+
writeJSON(filePath, data);
|
|
75
|
+
report.fixed = true;
|
|
76
|
+
} catch (writeErr) {
|
|
77
|
+
report.errors.push(`Failed to write repaired file: ${writeErr.message}`);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return report;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ─── Validators ───────────────────────────────────────────
|
|
85
|
+
|
|
86
|
+
function validateTasks(data) {
|
|
87
|
+
const repairs = [];
|
|
88
|
+
|
|
89
|
+
// Ensure top-level structure
|
|
90
|
+
if (!data || typeof data !== 'object') {
|
|
91
|
+
return { data: { project: 'unknown', total: 0, tasks: [] }, repairs: ['Rebuilt empty tasks structure'] };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (!Array.isArray(data.tasks)) {
|
|
95
|
+
if (data.tasks && typeof data.tasks === 'object') {
|
|
96
|
+
data.tasks = Object.values(data.tasks);
|
|
97
|
+
repairs.push('Converted tasks from object to array');
|
|
98
|
+
} else {
|
|
99
|
+
data.tasks = [];
|
|
100
|
+
repairs.push('Missing tasks array — initialized empty');
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Validate each task
|
|
105
|
+
for (let i = 0; i < data.tasks.length; i++) {
|
|
106
|
+
const task = data.tasks[i];
|
|
107
|
+
|
|
108
|
+
// Ensure task is an object
|
|
109
|
+
if (!task || typeof task !== 'object') {
|
|
110
|
+
data.tasks[i] = { id: i + 1, title: `Task ${i + 1}`, description: '', branch: `feature/${String(i + 1).padStart(3, '0')}-unknown`, status: 'pending' };
|
|
111
|
+
repairs.push(`Task at index ${i}: rebuilt from invalid value`);
|
|
112
|
+
continue;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
// Fix missing ID
|
|
116
|
+
if (task.id === undefined || task.id === null) {
|
|
117
|
+
task.id = i + 1;
|
|
118
|
+
repairs.push(`Task at index ${i}: assigned missing id=${task.id}`);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Fix missing required fields
|
|
122
|
+
if (!task.title) { task.title = `Untitled Task ${task.id}`; repairs.push(`Task #${task.id}: added missing title`); }
|
|
123
|
+
if (!task.description) { task.description = task.title; repairs.push(`Task #${task.id}: added missing description`); }
|
|
124
|
+
if (!task.branch) {
|
|
125
|
+
const slug = task.title.toLowerCase().replace(/[^a-z0-9]+/g, '-').substring(0, 30);
|
|
126
|
+
task.branch = `feature/${String(task.id).padStart(3, '0')}-${slug}`;
|
|
127
|
+
repairs.push(`Task #${task.id}: generated missing branch name`);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Fix invalid status
|
|
131
|
+
if (!task.status || !VALID_TASK_STATUSES.includes(task.status)) {
|
|
132
|
+
const oldStatus = task.status;
|
|
133
|
+
task.status = 'pending';
|
|
134
|
+
repairs.push(`Task #${task.id}: invalid status '${oldStatus}' → 'pending'`);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Ensure depends_on is an array
|
|
138
|
+
if (task.depends_on && !Array.isArray(task.depends_on)) {
|
|
139
|
+
task.depends_on = [];
|
|
140
|
+
repairs.push(`Task #${task.id}: fixed invalid depends_on`);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Ensure acceptance is an array
|
|
144
|
+
if (task.acceptance && !Array.isArray(task.acceptance)) {
|
|
145
|
+
if (typeof task.acceptance === 'string') {
|
|
146
|
+
task.acceptance = [task.acceptance];
|
|
147
|
+
repairs.push(`Task #${task.id}: converted acceptance string to array`);
|
|
148
|
+
} else {
|
|
149
|
+
task.acceptance = [];
|
|
150
|
+
repairs.push(`Task #${task.id}: fixed invalid acceptance`);
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// Check for duplicate IDs
|
|
156
|
+
const idMap = new Map();
|
|
157
|
+
for (const task of data.tasks) {
|
|
158
|
+
if (idMap.has(task.id)) {
|
|
159
|
+
// Reassign duplicate ID
|
|
160
|
+
const newId = Math.max(...data.tasks.map(t => t.id)) + 1;
|
|
161
|
+
repairs.push(`Task #${task.id}: duplicate ID → reassigned to #${newId}`);
|
|
162
|
+
task.id = newId;
|
|
163
|
+
}
|
|
164
|
+
idMap.set(task.id, true);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Fix total count
|
|
168
|
+
if (data.total !== data.tasks.length) {
|
|
169
|
+
repairs.push(`total: ${data.total} → ${data.tasks.length}`);
|
|
170
|
+
data.total = data.tasks.length;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Ensure project name exists
|
|
174
|
+
if (!data.project) {
|
|
175
|
+
data.project = 'Unknown Project';
|
|
176
|
+
repairs.push('Added missing project name');
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
return { data, repairs };
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
function validateState(data) {
|
|
183
|
+
const repairs = [];
|
|
184
|
+
|
|
185
|
+
if (!data || typeof data !== 'object') {
|
|
186
|
+
return { data: { ...DEFAULT_STATE }, repairs: ['Rebuilt from scratch — was not an object'] };
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
// Check and fix each expected field
|
|
190
|
+
if (typeof data.current_task !== 'number') {
|
|
191
|
+
data.current_task = parseInt(data.current_task, 10) || 0;
|
|
192
|
+
repairs.push(`current_task: fixed type → ${data.current_task}`);
|
|
193
|
+
}
|
|
194
|
+
if (typeof data.review_round !== 'number') {
|
|
195
|
+
data.review_round = parseInt(data.review_round, 10) || 0;
|
|
196
|
+
repairs.push(`review_round: fixed type → ${data.review_round}`);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Validate phase
|
|
200
|
+
const validPhases = ['develop', 'pr', 'review', 'merge', null];
|
|
201
|
+
if (!validPhases.includes(data.phase)) {
|
|
202
|
+
repairs.push(`phase: invalid '${data.phase}' → null`);
|
|
203
|
+
data.phase = null;
|
|
204
|
+
data.phase_step = null;
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// Ensure last_updated exists
|
|
208
|
+
if (!data.last_updated) {
|
|
209
|
+
data.last_updated = new Date().toISOString();
|
|
210
|
+
repairs.push('Added missing last_updated');
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
return { data, repairs };
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
function validateConfig(data) {
|
|
217
|
+
const repairs = [];
|
|
218
|
+
|
|
219
|
+
if (!data || typeof data !== 'object') {
|
|
220
|
+
return { data: {}, repairs: ['Config was not an object — replaced with empty'] };
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Validate provider
|
|
224
|
+
if (data.provider && typeof data.provider !== 'string') {
|
|
225
|
+
data.provider = 'codex-cli';
|
|
226
|
+
repairs.push('Invalid provider — reset to codex-cli');
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
// Validate numeric fields
|
|
230
|
+
for (const field of ['max_review_rounds', 'review_poll_interval', 'review_wait_timeout']) {
|
|
231
|
+
if (data[field] !== undefined && typeof data[field] !== 'number') {
|
|
232
|
+
const parsed = parseInt(data[field], 10);
|
|
233
|
+
if (!isNaN(parsed)) {
|
|
234
|
+
data[field] = parsed;
|
|
235
|
+
repairs.push(`${field}: converted string to number`);
|
|
236
|
+
} else {
|
|
237
|
+
delete data[field];
|
|
238
|
+
repairs.push(`${field}: removed invalid value`);
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
return { data, repairs };
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
function validateRounds(data) {
|
|
247
|
+
const repairs = [];
|
|
248
|
+
|
|
249
|
+
if (!data || typeof data !== 'object') {
|
|
250
|
+
return { data: { current_round: 0, rounds: [] }, repairs: ['Rebuilt rounds structure'] };
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
if (typeof data.current_round !== 'number') {
|
|
254
|
+
data.current_round = parseInt(data.current_round, 10) || 0;
|
|
255
|
+
repairs.push(`current_round: fixed type → ${data.current_round}`);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
if (!Array.isArray(data.rounds)) {
|
|
259
|
+
data.rounds = [];
|
|
260
|
+
repairs.push('Missing rounds array — initialized empty');
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return { data, repairs };
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
// ─── File → Validator mapping ─────────────────────────────
|
|
267
|
+
|
|
268
|
+
const FILE_VALIDATORS = {
|
|
269
|
+
'tasks.json': validateTasks,
|
|
270
|
+
'state.json': validateState,
|
|
271
|
+
'config.json': validateConfig,
|
|
272
|
+
'rounds.json': validateRounds,
|
|
273
|
+
};
|
|
274
|
+
|
|
275
|
+
// ─── Public API ───────────────────────────────────────────
|
|
276
|
+
|
|
277
|
+
/**
|
|
278
|
+
* Interactive fix command — scans and repairs all project files.
|
|
279
|
+
* Called when user runs: codex-copilot fix
|
|
280
|
+
*/
|
|
281
|
+
export async function fix(projectDir) {
|
|
282
|
+
log.title('🔧 Codex-Copilot File Repair');
|
|
283
|
+
log.blank();
|
|
284
|
+
|
|
285
|
+
const copilotDir = resolve(projectDir, '.codex-copilot');
|
|
286
|
+
if (!existsSync(copilotDir)) {
|
|
287
|
+
log.error('Project not initialized. Run: codex-copilot init');
|
|
288
|
+
process.exit(1);
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
log.info('Scanning .codex-copilot/ for issues...');
|
|
292
|
+
log.blank();
|
|
293
|
+
|
|
294
|
+
const results = runRepairs(copilotDir);
|
|
295
|
+
printReport(results);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Auto-fix — called programmatically by other commands when errors are detected.
|
|
300
|
+
* Returns true if all files are healthy (either clean or successfully repaired).
|
|
301
|
+
* @param {string} projectDir - Project root directory
|
|
302
|
+
* @param {object} [options]
|
|
303
|
+
* @param {boolean} [options.silent] - Suppress non-error output
|
|
304
|
+
* @param {string[]} [options.files] - Only fix specific files (e.g. ['tasks.json'])
|
|
305
|
+
* @returns {{ ok: boolean, repaired: string[] }}
|
|
306
|
+
*/
|
|
307
|
+
export function autoFix(projectDir, options = {}) {
|
|
308
|
+
const copilotDir = resolve(projectDir, '.codex-copilot');
|
|
309
|
+
if (!existsSync(copilotDir)) {
|
|
310
|
+
return { ok: false, repaired: [] };
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
if (!options.silent) {
|
|
314
|
+
log.info('🔧 Auto-repairing project files...');
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
const results = runRepairs(copilotDir, options.files);
|
|
318
|
+
const repaired = results.filter(r => r.fixed).map(r => r.file);
|
|
319
|
+
const failed = results.filter(r => r.errors.length > 0).map(r => r.file);
|
|
320
|
+
|
|
321
|
+
if (!options.silent) {
|
|
322
|
+
for (const r of results) {
|
|
323
|
+
if (r.fixed) {
|
|
324
|
+
log.info(` ✅ ${r.file}: ${r.repairs.length} issue(s) repaired`);
|
|
325
|
+
}
|
|
326
|
+
for (const err of r.errors) {
|
|
327
|
+
log.error(` ❌ ${r.file}: ${err}`);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
return { ok: failed.length === 0, repaired };
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
// ─── Internal helpers ─────────────────────────────────────
|
|
336
|
+
|
|
337
|
+
function runRepairs(copilotDir, filterFiles) {
|
|
338
|
+
const results = [];
|
|
339
|
+
|
|
340
|
+
for (const [fileName, validator] of Object.entries(FILE_VALIDATORS)) {
|
|
341
|
+
if (filterFiles && !filterFiles.includes(fileName)) continue;
|
|
342
|
+
const filePath = resolve(copilotDir, fileName);
|
|
343
|
+
if (!existsSync(filePath)) continue;
|
|
344
|
+
results.push(fixFile(filePath, validator));
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
// Also scan for any other .json files (e.g. tasks_round_N.json)
|
|
348
|
+
if (!filterFiles) {
|
|
349
|
+
try {
|
|
350
|
+
const files = readdirSync(copilotDir).filter(f =>
|
|
351
|
+
f.endsWith('.json') &&
|
|
352
|
+
!f.endsWith('.tmp') &&
|
|
353
|
+
!f.endsWith('.bak') &&
|
|
354
|
+
!Object.keys(FILE_VALIDATORS).includes(f)
|
|
355
|
+
);
|
|
356
|
+
for (const fileName of files) {
|
|
357
|
+
const filePath = resolve(copilotDir, fileName);
|
|
358
|
+
results.push(fixFile(filePath, null)); // Parse-only repair, no schema validation
|
|
359
|
+
}
|
|
360
|
+
} catch { /* ignore directory read errors */ }
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
return results;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function printReport(results) {
|
|
367
|
+
const totalFixed = results.filter(r => r.fixed).length;
|
|
368
|
+
const totalErrors = results.filter(r => r.errors.length > 0).length;
|
|
369
|
+
const totalClean = results.filter(r => !r.fixed && r.errors.length === 0).length;
|
|
370
|
+
|
|
371
|
+
for (const r of results) {
|
|
372
|
+
if (r.errors.length > 0) {
|
|
373
|
+
log.error(`❌ ${r.file}:`);
|
|
374
|
+
for (const err of r.errors) {
|
|
375
|
+
log.error(` ${err}`);
|
|
376
|
+
}
|
|
377
|
+
} else if (r.fixed) {
|
|
378
|
+
log.info(`🔧 ${r.file}: ${r.repairs.length} issue(s) repaired`);
|
|
379
|
+
for (const repair of r.repairs) {
|
|
380
|
+
log.dim(` ↳ ${repair}`);
|
|
381
|
+
}
|
|
382
|
+
} else {
|
|
383
|
+
log.info(`✅ ${r.file}: OK`);
|
|
384
|
+
}
|
|
385
|
+
log.blank();
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// Summary
|
|
389
|
+
log.blank();
|
|
390
|
+
if (totalErrors > 0) {
|
|
391
|
+
log.error(`Summary: ${totalFixed} file(s) repaired, ${totalErrors} file(s) have unrecoverable errors`);
|
|
392
|
+
log.warn('For unrecoverable files, try: codex-copilot reset');
|
|
393
|
+
} else if (totalFixed > 0) {
|
|
394
|
+
log.title(`✅ Repaired ${totalFixed} file(s), ${totalClean} file(s) were already OK`);
|
|
395
|
+
} else {
|
|
396
|
+
log.title('✅ All files are healthy — no repairs needed');
|
|
397
|
+
}
|
|
398
|
+
log.blank();
|
|
399
|
+
}
|
package/src/commands/reset.js
CHANGED
|
@@ -2,11 +2,12 @@
|
|
|
2
2
|
* codex-copilot reset - Reset state
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import { existsSync, unlinkSync } from 'fs';
|
|
6
6
|
import { resolve } from 'path';
|
|
7
7
|
import { log } from '../utils/logger.js';
|
|
8
8
|
import { confirm, closePrompt } from '../utils/prompt.js';
|
|
9
9
|
import { createCheckpoint } from '../utils/checkpoint.js';
|
|
10
|
+
import { readJSON, writeJSON } from '../utils/json.js';
|
|
10
11
|
|
|
11
12
|
export async function reset(projectDir) {
|
|
12
13
|
const statePath = resolve(projectDir, '.codex-copilot/state.json');
|
|
@@ -38,11 +39,11 @@ export async function reset(projectDir) {
|
|
|
38
39
|
|
|
39
40
|
// Reset task statuses
|
|
40
41
|
if (resetTasks && existsSync(tasksPath)) {
|
|
41
|
-
const tasks =
|
|
42
|
+
const tasks = readJSON(tasksPath);
|
|
42
43
|
for (const task of tasks.tasks) {
|
|
43
44
|
task.status = 'pending';
|
|
44
45
|
}
|
|
45
|
-
|
|
46
|
+
writeJSON(tasksPath, tasks);
|
|
46
47
|
log.info('Task statuses reset');
|
|
47
48
|
}
|
|
48
49
|
|
package/src/commands/retry.js
CHANGED
|
@@ -13,14 +13,9 @@ import { log, progressBar } from '../utils/logger.js';
|
|
|
13
13
|
import { git } from '../utils/git.js';
|
|
14
14
|
import { github } from '../utils/github.js';
|
|
15
15
|
import { createCheckpoint } from '../utils/checkpoint.js';
|
|
16
|
+
import { readJSON, writeJSON } from '../utils/json.js';
|
|
16
17
|
|
|
17
|
-
function readJSON(path) {
|
|
18
|
-
return JSON.parse(readFileSync(path, 'utf-8'));
|
|
19
|
-
}
|
|
20
18
|
|
|
21
|
-
function writeJSON(path, data) {
|
|
22
|
-
writeFileSync(path, JSON.stringify(data, null, 2));
|
|
23
|
-
}
|
|
24
19
|
|
|
25
20
|
export async function retry(projectDir) {
|
|
26
21
|
const tasksPath = resolve(projectDir, '.codex-copilot/tasks.json');
|
package/src/commands/run.js
CHANGED
|
@@ -13,17 +13,10 @@ import { github } from '../utils/github.js';
|
|
|
13
13
|
import { closePrompt } from '../utils/prompt.js';
|
|
14
14
|
import { createCheckpoint } from '../utils/checkpoint.js';
|
|
15
15
|
import { provider } from '../utils/provider.js';
|
|
16
|
+
import { readJSON, writeJSON } from '../utils/json.js';
|
|
16
17
|
|
|
17
18
|
const maxRateLimitRetries = 3;
|
|
18
19
|
|
|
19
|
-
function readJSON(path) {
|
|
20
|
-
return JSON.parse(readFileSync(path, 'utf-8'));
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
function writeJSON(path, data) {
|
|
24
|
-
writeFileSync(path, JSON.stringify(data, null, 2));
|
|
25
|
-
}
|
|
26
|
-
|
|
27
20
|
export async function run(projectDir) {
|
|
28
21
|
const tasksPath = resolve(projectDir, '.codex-copilot/tasks.json');
|
|
29
22
|
const configPath = resolve(projectDir, '.codex-copilot/config.json');
|
|
@@ -31,14 +24,53 @@ export async function run(projectDir) {
|
|
|
31
24
|
|
|
32
25
|
let tasks;
|
|
33
26
|
let state;
|
|
27
|
+
|
|
28
|
+
// Load tasks.json (required)
|
|
34
29
|
try {
|
|
35
30
|
tasks = readJSON(tasksPath);
|
|
36
|
-
state = checkpoint.load();
|
|
37
31
|
} catch (err) {
|
|
38
|
-
log.
|
|
39
|
-
log.
|
|
40
|
-
|
|
41
|
-
|
|
32
|
+
log.warn(`tasks.json read issue: ${err.message}`);
|
|
33
|
+
log.info('Attempting auto-repair...');
|
|
34
|
+
|
|
35
|
+
// Try auto-fix (deep repair + schema validation)
|
|
36
|
+
const { autoFix } = await import('./fix.js');
|
|
37
|
+
const fixResult = autoFix(projectDir, { files: ['tasks.json'] });
|
|
38
|
+
|
|
39
|
+
if (fixResult.ok && fixResult.repaired.includes('tasks.json')) {
|
|
40
|
+
try {
|
|
41
|
+
tasks = readJSON(tasksPath);
|
|
42
|
+
log.info('✅ tasks.json auto-repaired successfully');
|
|
43
|
+
} catch {
|
|
44
|
+
// Auto-fix wrote the file but it's still not readable (shouldn't happen)
|
|
45
|
+
log.error('Auto-repair wrote file but it remains unreadable');
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// If still no tasks, try backup
|
|
50
|
+
if (!tasks && existsSync(tasksPath + '.bak')) {
|
|
51
|
+
log.info('Trying backup file...');
|
|
52
|
+
try {
|
|
53
|
+
tasks = readJSON(tasksPath + '.bak');
|
|
54
|
+
writeJSON(tasksPath, tasks);
|
|
55
|
+
log.info('✅ Restored tasks.json from backup');
|
|
56
|
+
} catch {
|
|
57
|
+
log.error('Backup is also corrupted');
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (!tasks) {
|
|
62
|
+
log.error('Cannot recover tasks.json. Run: codex-copilot fix');
|
|
63
|
+
closePrompt();
|
|
64
|
+
process.exit(1);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Load state.json (optional — falls back to default)
|
|
69
|
+
try {
|
|
70
|
+
state = checkpoint.load();
|
|
71
|
+
} catch {
|
|
72
|
+
log.warn('State file corrupted — resetting to initial state');
|
|
73
|
+
state = checkpoint.reset();
|
|
42
74
|
}
|
|
43
75
|
const config = existsSync(configPath) ? readJSON(configPath) : {};
|
|
44
76
|
|
|
@@ -670,14 +702,14 @@ ${feedback}
|
|
|
670
702
|
writeFileSync(promptPath, fixPrompt);
|
|
671
703
|
|
|
672
704
|
let rateLimitRetries = 0;
|
|
673
|
-
while (rateLimitRetries <
|
|
705
|
+
while (rateLimitRetries < maxRateLimitRetries) {
|
|
674
706
|
const result = await provider.executePrompt(providerId, promptPath, projectDir);
|
|
675
707
|
if (result.ok) {
|
|
676
708
|
break;
|
|
677
709
|
}
|
|
678
710
|
if (result.rateLimited && result.retryAt) {
|
|
679
711
|
rateLimitRetries++;
|
|
680
|
-
if (rateLimitRetries >=
|
|
712
|
+
if (rateLimitRetries >= maxRateLimitRetries) {
|
|
681
713
|
log.warn('Max rate limit retries reached during fix phase');
|
|
682
714
|
break;
|
|
683
715
|
}
|
package/src/commands/skip.js
CHANGED
|
@@ -6,17 +6,11 @@
|
|
|
6
6
|
* stuck and downstream work should continue regardless.
|
|
7
7
|
*/
|
|
8
8
|
|
|
9
|
-
import { readFileSync, writeFileSync } from 'fs';
|
|
10
9
|
import { resolve } from 'path';
|
|
11
10
|
import { log } from '../utils/logger.js';
|
|
11
|
+
import { readJSON, writeJSON } from '../utils/json.js';
|
|
12
12
|
|
|
13
|
-
function readJSON(path) {
|
|
14
|
-
return JSON.parse(readFileSync(path, 'utf-8'));
|
|
15
|
-
}
|
|
16
13
|
|
|
17
|
-
function writeJSON(path, data) {
|
|
18
|
-
writeFileSync(path, JSON.stringify(data, null, 2));
|
|
19
|
-
}
|
|
20
14
|
|
|
21
15
|
export async function skip(projectDir, taskId) {
|
|
22
16
|
const tasksPath = resolve(projectDir, '.codex-copilot/tasks.json');
|
package/src/commands/status.js
CHANGED
|
@@ -2,10 +2,11 @@
|
|
|
2
2
|
* codex-copilot status - Show current progress
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import { existsSync } from 'fs';
|
|
6
6
|
import { resolve } from 'path';
|
|
7
7
|
import { log, progressBar } from '../utils/logger.js';
|
|
8
8
|
import { createCheckpoint } from '../utils/checkpoint.js';
|
|
9
|
+
import { readJSON } from '../utils/json.js';
|
|
9
10
|
|
|
10
11
|
export async function status(projectDir) {
|
|
11
12
|
const tasksPath = resolve(projectDir, '.codex-copilot/tasks.json');
|
|
@@ -13,7 +14,7 @@ export async function status(projectDir) {
|
|
|
13
14
|
|
|
14
15
|
let tasks, state;
|
|
15
16
|
try {
|
|
16
|
-
tasks =
|
|
17
|
+
tasks = readJSON(tasksPath);
|
|
17
18
|
state = checkpoint.load();
|
|
18
19
|
} catch (err) {
|
|
19
20
|
log.error(`Failed to read files: ${err.message}`);
|
|
@@ -28,7 +29,7 @@ export async function status(projectDir) {
|
|
|
28
29
|
const roundsPath = resolve(projectDir, '.codex-copilot/rounds.json');
|
|
29
30
|
if (existsSync(roundsPath)) {
|
|
30
31
|
try {
|
|
31
|
-
const rounds =
|
|
32
|
+
const rounds = readJSON(roundsPath);
|
|
32
33
|
const currentRound = tasks.round || rounds.current_round || 1;
|
|
33
34
|
const totalCompleted = rounds.rounds.reduce((s, r) => s + r.tasks_completed, 0);
|
|
34
35
|
log.info(`📦 Round: ${currentRound} | Total across all rounds: ${totalCompleted} tasks`);
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Safe JSON read/write utilities
|
|
3
|
+
*
|
|
4
|
+
* Provides resilient JSON file operations with:
|
|
5
|
+
* - Auto-repair of common corruption patterns (trailing commas, BOM, control chars)
|
|
6
|
+
* - Truncation recovery via progressive trimming and bracket closure
|
|
7
|
+
* - Atomic writes (temp file + rename) to prevent corruption on crash
|
|
8
|
+
* - Backup creation on auto-repair
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { readFileSync, writeFileSync, existsSync, renameSync, copyFileSync } from 'fs';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Read and parse a JSON file with auto-repair on corruption.
|
|
15
|
+
* Creates a .bak backup before overwriting with repaired content.
|
|
16
|
+
* @param {string} filePath - Path to JSON file
|
|
17
|
+
* @returns {any} Parsed JSON data
|
|
18
|
+
* @throws {Error} If JSON is unrecoverable
|
|
19
|
+
*/
|
|
20
|
+
export function readJSON(filePath) {
|
|
21
|
+
let raw = readFileSync(filePath, 'utf-8');
|
|
22
|
+
// Strip BOM if present
|
|
23
|
+
if (raw.charCodeAt(0) === 0xFEFF) raw = raw.slice(1);
|
|
24
|
+
try {
|
|
25
|
+
return JSON.parse(raw);
|
|
26
|
+
} catch (firstErr) {
|
|
27
|
+
const data = tryRepair(raw);
|
|
28
|
+
if (data !== null) {
|
|
29
|
+
// Save repaired version and backup original
|
|
30
|
+
try {
|
|
31
|
+
copyFileSync(filePath, filePath + '.bak');
|
|
32
|
+
writeJSON(filePath, data);
|
|
33
|
+
} catch { /* best effort */ }
|
|
34
|
+
return data;
|
|
35
|
+
}
|
|
36
|
+
throw new Error(`Invalid JSON in ${filePath}: ${firstErr.message}`);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Multi-strategy JSON repair pipeline.
|
|
42
|
+
* @param {string} raw - Raw file content
|
|
43
|
+
* @returns {any|null} Parsed data or null if all strategies fail
|
|
44
|
+
*/
|
|
45
|
+
function tryRepair(raw) {
|
|
46
|
+
// Strategy 1: Basic cleanup (trailing commas, control chars)
|
|
47
|
+
let cleaned = raw
|
|
48
|
+
.replace(/,\s*([}\]])/g, '$1')
|
|
49
|
+
.replace(/[\x00-\x08\x0E-\x1F]/g, '')
|
|
50
|
+
.trim();
|
|
51
|
+
|
|
52
|
+
let result = tryParse(cleaned);
|
|
53
|
+
if (result !== null) return result;
|
|
54
|
+
|
|
55
|
+
// Strategy 2: Truncation recovery — find the last complete } or ] and close
|
|
56
|
+
// Work backwards: try truncating at each } or ] from the end
|
|
57
|
+
for (let i = cleaned.length - 1; i >= 0; i--) {
|
|
58
|
+
if (cleaned[i] === '}' || cleaned[i] === ']') {
|
|
59
|
+
let candidate = cleaned.substring(0, i + 1);
|
|
60
|
+
candidate = closeBrackets(candidate);
|
|
61
|
+
result = tryParse(candidate);
|
|
62
|
+
if (result !== null) return result;
|
|
63
|
+
|
|
64
|
+
// Also try stripping garbage prefix (start from first open bracket)
|
|
65
|
+
const firstOpen = findFirstOpen(candidate);
|
|
66
|
+
if (firstOpen > 0) {
|
|
67
|
+
let stripped = candidate.substring(firstOpen);
|
|
68
|
+
stripped = closeBrackets(stripped);
|
|
69
|
+
result = tryParse(stripped);
|
|
70
|
+
if (result !== null) return result;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Strategy 3: Extract {…} or […] from garbage-surrounded content
|
|
76
|
+
const firstOpen = findFirstOpen(cleaned);
|
|
77
|
+
if (firstOpen >= 0) {
|
|
78
|
+
// Find the last } or ] in the string
|
|
79
|
+
const lastClose = Math.max(cleaned.lastIndexOf('}'), cleaned.lastIndexOf(']'));
|
|
80
|
+
if (lastClose > firstOpen) {
|
|
81
|
+
let candidate = cleaned.substring(firstOpen, lastClose + 1);
|
|
82
|
+
candidate = closeBrackets(candidate);
|
|
83
|
+
result = tryParse(candidate);
|
|
84
|
+
if (result !== null) return result;
|
|
85
|
+
}
|
|
86
|
+
// Also try from firstOpen to end with bracket closing
|
|
87
|
+
let candidate = cleaned.substring(firstOpen);
|
|
88
|
+
candidate = closeBrackets(candidate);
|
|
89
|
+
result = tryParse(candidate);
|
|
90
|
+
if (result !== null) return result;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Find the first { or [ in a string
|
|
98
|
+
*/
|
|
99
|
+
function findFirstOpen(s) {
|
|
100
|
+
for (let i = 0; i < s.length; i++) {
|
|
101
|
+
if (s[i] === '{' || s[i] === '[') return i;
|
|
102
|
+
}
|
|
103
|
+
return -1;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Close unmatched brackets/braces in a JSON string.
|
|
108
|
+
* Properly handles strings (doesn't count brackets inside quoted strings).
|
|
109
|
+
*/
|
|
110
|
+
function closeBrackets(s) {
|
|
111
|
+
const stack = [];
|
|
112
|
+
let inString = false;
|
|
113
|
+
let escaped = false;
|
|
114
|
+
|
|
115
|
+
for (let i = 0; i < s.length; i++) {
|
|
116
|
+
const ch = s[i];
|
|
117
|
+
if (escaped) { escaped = false; continue; }
|
|
118
|
+
if (ch === '\\' && inString) { escaped = true; continue; }
|
|
119
|
+
if (ch === '"') { inString = !inString; continue; }
|
|
120
|
+
if (inString) continue;
|
|
121
|
+
if (ch === '{') stack.push('}');
|
|
122
|
+
else if (ch === '[') stack.push(']');
|
|
123
|
+
else if (ch === '}' || ch === ']') {
|
|
124
|
+
if (stack.length > 0) stack.pop();
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Append closers in reverse order
|
|
129
|
+
while (stack.length > 0) s += stack.pop();
|
|
130
|
+
return s;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Try to parse JSON, return data or null.
|
|
135
|
+
*/
|
|
136
|
+
function tryParse(s) {
|
|
137
|
+
try {
|
|
138
|
+
return JSON.parse(s);
|
|
139
|
+
} catch {
|
|
140
|
+
return null;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Write JSON data to file atomically (temp file + rename).
|
|
146
|
+
* Prevents corruption if process is killed mid-write.
|
|
147
|
+
* @param {string} filePath - Path to JSON file
|
|
148
|
+
* @param {any} data - Data to serialize
|
|
149
|
+
*/
|
|
150
|
+
export function writeJSON(filePath, data) {
|
|
151
|
+
const tempPath = filePath + '.tmp';
|
|
152
|
+
writeFileSync(tempPath, JSON.stringify(data, null, 2));
|
|
153
|
+
renameSync(tempPath, filePath);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Deep repair of a severely corrupted JSON file.
|
|
158
|
+
* Tries readJSON's repair pipeline, then falls back to .bak file.
|
|
159
|
+
* Used by the fix command when readJSON fails.
|
|
160
|
+
* @param {string} filePath - Path to corrupted JSON file
|
|
161
|
+
* @returns {any} Parsed JSON data
|
|
162
|
+
* @throws {Error} If all repair strategies fail
|
|
163
|
+
*/
|
|
164
|
+
export function repairRawJSON(filePath) {
|
|
165
|
+
let raw = readFileSync(filePath, 'utf-8');
|
|
166
|
+
if (raw.charCodeAt(0) === 0xFEFF) raw = raw.slice(1);
|
|
167
|
+
|
|
168
|
+
// Try the full repair pipeline
|
|
169
|
+
const data = tryRepair(raw);
|
|
170
|
+
if (data !== null) {
|
|
171
|
+
try {
|
|
172
|
+
copyFileSync(filePath, filePath + '.bak');
|
|
173
|
+
writeJSON(filePath, data);
|
|
174
|
+
} catch { /* best effort */ }
|
|
175
|
+
return data;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Try to recover from .bak file
|
|
179
|
+
const bakPath = filePath + '.bak';
|
|
180
|
+
if (existsSync(bakPath)) {
|
|
181
|
+
try {
|
|
182
|
+
const bakRaw = readFileSync(bakPath, 'utf-8');
|
|
183
|
+
const bakData = tryRepair(bakRaw) ?? JSON.parse(bakRaw);
|
|
184
|
+
writeJSON(filePath, bakData);
|
|
185
|
+
return bakData;
|
|
186
|
+
} catch { /* backup also corrupt */ }
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
throw new Error(`All repair strategies failed for ${filePath}`);
|
|
190
|
+
}
|
package/src/utils/provider.js
CHANGED
|
@@ -718,7 +718,7 @@ function parseRateLimitError(output) {
|
|
|
718
718
|
* Check current Codex 7d quota usage before execution.
|
|
719
719
|
* Reads the latest rollout file for quota data.
|
|
720
720
|
*
|
|
721
|
-
* @param {number} threshold - Percentage at which to block (default
|
|
721
|
+
* @param {number} threshold - Percentage at which to block (default 97)
|
|
722
722
|
* @returns {{ ok: boolean, quota5h: number|null, quota7d: number|null, warning: boolean }}
|
|
723
723
|
*/
|
|
724
724
|
export function checkQuotaBeforeExecution(threshold = 97) {
|