@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/utils/checkpoint.js
CHANGED
|
@@ -1,246 +1,246 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Checkpoint state manager - Fine-grained state persistence
|
|
3
|
-
*
|
|
4
|
-
* Provides atomic save/load for checkpoint state, enabling
|
|
5
|
-
* resume from any sub-step within a task after interruption.
|
|
6
|
-
*/
|
|
7
|
-
|
|
8
|
-
import { readFileSync, writeFileSync, renameSync, existsSync } from 'fs';
|
|
9
|
-
import { resolve } from 'path';
|
|
10
|
-
|
|
11
|
-
const PHASE_ORDER = ['develop', 'pr', 'review', 'merge'];
|
|
12
|
-
const STEP_ORDERS = {
|
|
13
|
-
develop: ['branch_created', 'prompt_ready', 'codex_complete'],
|
|
14
|
-
pr: ['committed', 'pushed', 'pr_created'],
|
|
15
|
-
review: ['waiting_review', 'feedback_received', 'fix_applied'],
|
|
16
|
-
merge: ['merged'],
|
|
17
|
-
};
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* Create a checkpoint manager for a project
|
|
21
|
-
* @param {string} projectDir - Project root directory
|
|
22
|
-
*/
|
|
23
|
-
export function createCheckpoint(projectDir) {
|
|
24
|
-
const statePath = resolve(projectDir, '.codex-copilot/state.json');
|
|
25
|
-
const tempPath = resolve(projectDir, '.codex-copilot/state.json.tmp');
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Load current state from disk
|
|
29
|
-
*/
|
|
30
|
-
function load() {
|
|
31
|
-
try {
|
|
32
|
-
return JSON.parse(readFileSync(statePath, 'utf-8'));
|
|
33
|
-
} catch {
|
|
34
|
-
return getDefaultState();
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Save state atomically (write to temp file, then rename)
|
|
40
|
-
* @param {object} update - Partial state to merge
|
|
41
|
-
*/
|
|
42
|
-
function save(update) {
|
|
43
|
-
const current = load();
|
|
44
|
-
const merged = {
|
|
45
|
-
...current,
|
|
46
|
-
...update,
|
|
47
|
-
last_updated: new Date().toISOString(),
|
|
48
|
-
};
|
|
49
|
-
// Atomic write: temp file + rename prevents corruption on crash
|
|
50
|
-
writeFileSync(tempPath, JSON.stringify(merged, null, 2));
|
|
51
|
-
renameSync(tempPath, statePath);
|
|
52
|
-
return merged;
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
/**
|
|
56
|
-
* Mark a specific phase & step as reached.
|
|
57
|
-
* Includes monotonic progress guard — prevents regression from a later
|
|
58
|
-
* phase back to an earlier one (e.g., 'pr' → 'develop' is blocked).
|
|
59
|
-
*/
|
|
60
|
-
function saveStep(taskId, phase, phaseStep, extra = {}) {
|
|
61
|
-
const current = load();
|
|
62
|
-
|
|
63
|
-
// Monotonic progress guard: only allow forward movement within the same task
|
|
64
|
-
if (current.current_task === taskId && current.phase) {
|
|
65
|
-
const currentPhaseIdx = PHASE_ORDER.indexOf(current.phase);
|
|
66
|
-
const newPhaseIdx = PHASE_ORDER.indexOf(phase);
|
|
67
|
-
|
|
68
|
-
if (newPhaseIdx < currentPhaseIdx) {
|
|
69
|
-
// Attempting to go backward — ignore silently (likely a stale call)
|
|
70
|
-
return current;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
// Same phase: ensure step is not going backward
|
|
74
|
-
if (newPhaseIdx === currentPhaseIdx) {
|
|
75
|
-
const steps = STEP_ORDERS[phase] || [];
|
|
76
|
-
const currentStepIdx = steps.indexOf(current.phase_step);
|
|
77
|
-
const newStepIdx = steps.indexOf(phaseStep);
|
|
78
|
-
if (newStepIdx < currentStepIdx) {
|
|
79
|
-
return current; // Step regression — ignore
|
|
80
|
-
}
|
|
81
|
-
}
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
return save({
|
|
85
|
-
current_task: taskId,
|
|
86
|
-
phase,
|
|
87
|
-
phase_step: phaseStep,
|
|
88
|
-
...extra,
|
|
89
|
-
});
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
/**
|
|
93
|
-
* Mark current task as fully completed, clear phase info
|
|
94
|
-
*/
|
|
95
|
-
function completeTask(taskId) {
|
|
96
|
-
return save({
|
|
97
|
-
current_task: taskId,
|
|
98
|
-
phase: null,
|
|
99
|
-
phase_step: null,
|
|
100
|
-
current_pr: null,
|
|
101
|
-
review_round: 0,
|
|
102
|
-
branch: null,
|
|
103
|
-
});
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
* Check if a specific phase+step has been reached for a task
|
|
108
|
-
* Returns true if the saved state is at or past the given step
|
|
109
|
-
*/
|
|
110
|
-
function isStepDone(taskId, phase, phaseStep) {
|
|
111
|
-
const state = load();
|
|
112
|
-
if (state.current_task !== taskId) return false;
|
|
113
|
-
if (!state.phase) return false;
|
|
114
|
-
|
|
115
|
-
const savedPhaseIdx = PHASE_ORDER.indexOf(state.phase);
|
|
116
|
-
const targetPhaseIdx = PHASE_ORDER.indexOf(phase);
|
|
117
|
-
|
|
118
|
-
// If saved phase is ahead, this step is done
|
|
119
|
-
if (savedPhaseIdx > targetPhaseIdx) return true;
|
|
120
|
-
// If saved phase is behind, this step is not done
|
|
121
|
-
if (savedPhaseIdx < targetPhaseIdx) return false;
|
|
122
|
-
|
|
123
|
-
// Same phase: compare steps within phase
|
|
124
|
-
const steps = STEP_ORDERS[phase] || [];
|
|
125
|
-
const savedStepIdx = steps.indexOf(state.phase_step);
|
|
126
|
-
const targetStepIdx = steps.indexOf(phaseStep);
|
|
127
|
-
|
|
128
|
-
return savedStepIdx >= targetStepIdx;
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
/**
|
|
132
|
-
* Full reset to initial state
|
|
133
|
-
*/
|
|
134
|
-
function reset() {
|
|
135
|
-
const state = getDefaultState();
|
|
136
|
-
writeFileSync(statePath, JSON.stringify(state, null, 2));
|
|
137
|
-
return state;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
/**
|
|
141
|
-
* Clear all checkpoint data for a task (for retry)
|
|
142
|
-
*/
|
|
143
|
-
function clearTask(taskId) {
|
|
144
|
-
const state = load();
|
|
145
|
-
if (state.current_task === taskId) {
|
|
146
|
-
return save({
|
|
147
|
-
phase: null,
|
|
148
|
-
phase_step: null,
|
|
149
|
-
current_pr: null,
|
|
150
|
-
review_round: 0,
|
|
151
|
-
branch: null,
|
|
152
|
-
});
|
|
153
|
-
}
|
|
154
|
-
return state;
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
/**
|
|
158
|
-
* Clear a specific phase step (for partial retry, e.g. merge-only)
|
|
159
|
-
*/
|
|
160
|
-
function clearStep(taskId, phase, phaseStep) {
|
|
161
|
-
const state = load();
|
|
162
|
-
if (state.current_task === taskId && state.phase === phase && state.phase_step === phaseStep) {
|
|
163
|
-
return save({
|
|
164
|
-
phase: null,
|
|
165
|
-
phase_step: null,
|
|
166
|
-
});
|
|
167
|
-
}
|
|
168
|
-
return state;
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
/**
|
|
172
|
-
* E1/E4: Validate consistency between checkpoint state and tasks status.
|
|
173
|
-
* Detects and repairs mismatches such as:
|
|
174
|
-
* - Task marked 'in_progress' but no checkpoint data for it
|
|
175
|
-
* - Checkpoint points to a task that's already completed
|
|
176
|
-
* - Multiple tasks marked 'in_progress' simultaneously
|
|
177
|
-
* @param {object} tasks - The tasks.json data structure
|
|
178
|
-
* @returns {{ ok: boolean, repairs: string[] }}
|
|
179
|
-
*/
|
|
180
|
-
function validateConsistency(tasks) {
|
|
181
|
-
const state = load();
|
|
182
|
-
const repairs = [];
|
|
183
|
-
|
|
184
|
-
if (!tasks?.tasks || !Array.isArray(tasks.tasks)) {
|
|
185
|
-
return { ok: true, repairs: [] };
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
// Check 1: Multiple in_progress tasks (should only be one at a time)
|
|
189
|
-
const inProgress = tasks.tasks.filter(t => t.status === 'in_progress');
|
|
190
|
-
if (inProgress.length > 1) {
|
|
191
|
-
// Keep the one matching checkpoint, reset others to pending
|
|
192
|
-
for (const task of inProgress) {
|
|
193
|
-
if (task.id !== state.current_task) {
|
|
194
|
-
task.status = 'pending';
|
|
195
|
-
repairs.push(`Task #${task.id}: was 'in_progress' but not current — reset to 'pending'`);
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
// Check 2: Task is in_progress but checkpoint has no data for it
|
|
201
|
-
if (inProgress.length === 1 && state.current_task !== inProgress[0].id) {
|
|
202
|
-
if (!state.phase) {
|
|
203
|
-
// Checkpoint is empty — task was marked in_progress but never started
|
|
204
|
-
inProgress[0].status = 'pending';
|
|
205
|
-
repairs.push(`Task #${inProgress[0].id}: marked 'in_progress' but no checkpoint data — reset to 'pending'`);
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
// Check 3: Checkpoint points to a completed task (stale checkpoint)
|
|
210
|
-
if (state.current_task > 0 && state.phase) {
|
|
211
|
-
const checkpointTask = tasks.tasks.find(t => t.id === state.current_task);
|
|
212
|
-
if (checkpointTask && checkpointTask.status === 'completed') {
|
|
213
|
-
// Checkpoint is stale — clear it
|
|
214
|
-
save({ phase: null, phase_step: null, current_pr: null, review_round: 0, branch: null });
|
|
215
|
-
repairs.push(`Checkpoint for task #${state.current_task} cleared (task already completed)`);
|
|
216
|
-
}
|
|
217
|
-
}
|
|
218
|
-
|
|
219
|
-
// Check 4: Checkpoint task ID doesn't exist in tasks list
|
|
220
|
-
if (state.current_task > 0) {
|
|
221
|
-
const exists = tasks.tasks.find(t => t.id === state.current_task);
|
|
222
|
-
if (!exists) {
|
|
223
|
-
save({ current_task: 0, phase: null, phase_step: null, current_pr: null, review_round: 0, branch: null });
|
|
224
|
-
repairs.push(`Checkpoint referenced non-existent task #${state.current_task} — reset`);
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
return { ok: repairs.length === 0, repairs };
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
return { load, save, saveStep, completeTask, isStepDone, reset, clearTask, clearStep, validateConsistency };
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
function getDefaultState() {
|
|
235
|
-
return {
|
|
236
|
-
current_task: 0,
|
|
237
|
-
phase: null,
|
|
238
|
-
phase_step: null,
|
|
239
|
-
current_pr: null,
|
|
240
|
-
review_round: 0,
|
|
241
|
-
branch: null,
|
|
242
|
-
status: 'initialized',
|
|
243
|
-
last_updated: new Date().toISOString(),
|
|
244
|
-
};
|
|
245
|
-
}
|
|
246
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Checkpoint state manager - Fine-grained state persistence
|
|
3
|
+
*
|
|
4
|
+
* Provides atomic save/load for checkpoint state, enabling
|
|
5
|
+
* resume from any sub-step within a task after interruption.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import { readFileSync, writeFileSync, renameSync, existsSync } from 'fs';
|
|
9
|
+
import { resolve } from 'path';
|
|
10
|
+
|
|
11
|
+
const PHASE_ORDER = ['develop', 'pr', 'review', 'merge'];
|
|
12
|
+
const STEP_ORDERS = {
|
|
13
|
+
develop: ['branch_created', 'prompt_ready', 'codex_complete'],
|
|
14
|
+
pr: ['committed', 'pushed', 'pr_created'],
|
|
15
|
+
review: ['waiting_review', 'feedback_received', 'fix_applied'],
|
|
16
|
+
merge: ['merged'],
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Create a checkpoint manager for a project
|
|
21
|
+
* @param {string} projectDir - Project root directory
|
|
22
|
+
*/
|
|
23
|
+
export function createCheckpoint(projectDir) {
|
|
24
|
+
const statePath = resolve(projectDir, '.codex-copilot/state.json');
|
|
25
|
+
const tempPath = resolve(projectDir, '.codex-copilot/state.json.tmp');
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Load current state from disk
|
|
29
|
+
*/
|
|
30
|
+
function load() {
|
|
31
|
+
try {
|
|
32
|
+
return JSON.parse(readFileSync(statePath, 'utf-8'));
|
|
33
|
+
} catch {
|
|
34
|
+
return getDefaultState();
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Save state atomically (write to temp file, then rename)
|
|
40
|
+
* @param {object} update - Partial state to merge
|
|
41
|
+
*/
|
|
42
|
+
function save(update) {
|
|
43
|
+
const current = load();
|
|
44
|
+
const merged = {
|
|
45
|
+
...current,
|
|
46
|
+
...update,
|
|
47
|
+
last_updated: new Date().toISOString(),
|
|
48
|
+
};
|
|
49
|
+
// Atomic write: temp file + rename prevents corruption on crash
|
|
50
|
+
writeFileSync(tempPath, JSON.stringify(merged, null, 2));
|
|
51
|
+
renameSync(tempPath, statePath);
|
|
52
|
+
return merged;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Mark a specific phase & step as reached.
|
|
57
|
+
* Includes monotonic progress guard — prevents regression from a later
|
|
58
|
+
* phase back to an earlier one (e.g., 'pr' → 'develop' is blocked).
|
|
59
|
+
*/
|
|
60
|
+
function saveStep(taskId, phase, phaseStep, extra = {}) {
|
|
61
|
+
const current = load();
|
|
62
|
+
|
|
63
|
+
// Monotonic progress guard: only allow forward movement within the same task
|
|
64
|
+
if (current.current_task === taskId && current.phase) {
|
|
65
|
+
const currentPhaseIdx = PHASE_ORDER.indexOf(current.phase);
|
|
66
|
+
const newPhaseIdx = PHASE_ORDER.indexOf(phase);
|
|
67
|
+
|
|
68
|
+
if (newPhaseIdx < currentPhaseIdx) {
|
|
69
|
+
// Attempting to go backward — ignore silently (likely a stale call)
|
|
70
|
+
return current;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
// Same phase: ensure step is not going backward
|
|
74
|
+
if (newPhaseIdx === currentPhaseIdx) {
|
|
75
|
+
const steps = STEP_ORDERS[phase] || [];
|
|
76
|
+
const currentStepIdx = steps.indexOf(current.phase_step);
|
|
77
|
+
const newStepIdx = steps.indexOf(phaseStep);
|
|
78
|
+
if (newStepIdx < currentStepIdx) {
|
|
79
|
+
return current; // Step regression — ignore
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return save({
|
|
85
|
+
current_task: taskId,
|
|
86
|
+
phase,
|
|
87
|
+
phase_step: phaseStep,
|
|
88
|
+
...extra,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Mark current task as fully completed, clear phase info
|
|
94
|
+
*/
|
|
95
|
+
function completeTask(taskId) {
|
|
96
|
+
return save({
|
|
97
|
+
current_task: taskId,
|
|
98
|
+
phase: null,
|
|
99
|
+
phase_step: null,
|
|
100
|
+
current_pr: null,
|
|
101
|
+
review_round: 0,
|
|
102
|
+
branch: null,
|
|
103
|
+
});
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
/**
|
|
107
|
+
* Check if a specific phase+step has been reached for a task
|
|
108
|
+
* Returns true if the saved state is at or past the given step
|
|
109
|
+
*/
|
|
110
|
+
function isStepDone(taskId, phase, phaseStep) {
|
|
111
|
+
const state = load();
|
|
112
|
+
if (state.current_task !== taskId) return false;
|
|
113
|
+
if (!state.phase) return false;
|
|
114
|
+
|
|
115
|
+
const savedPhaseIdx = PHASE_ORDER.indexOf(state.phase);
|
|
116
|
+
const targetPhaseIdx = PHASE_ORDER.indexOf(phase);
|
|
117
|
+
|
|
118
|
+
// If saved phase is ahead, this step is done
|
|
119
|
+
if (savedPhaseIdx > targetPhaseIdx) return true;
|
|
120
|
+
// If saved phase is behind, this step is not done
|
|
121
|
+
if (savedPhaseIdx < targetPhaseIdx) return false;
|
|
122
|
+
|
|
123
|
+
// Same phase: compare steps within phase
|
|
124
|
+
const steps = STEP_ORDERS[phase] || [];
|
|
125
|
+
const savedStepIdx = steps.indexOf(state.phase_step);
|
|
126
|
+
const targetStepIdx = steps.indexOf(phaseStep);
|
|
127
|
+
|
|
128
|
+
return savedStepIdx >= targetStepIdx;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Full reset to initial state
|
|
133
|
+
*/
|
|
134
|
+
function reset() {
|
|
135
|
+
const state = getDefaultState();
|
|
136
|
+
writeFileSync(statePath, JSON.stringify(state, null, 2));
|
|
137
|
+
return state;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Clear all checkpoint data for a task (for retry)
|
|
142
|
+
*/
|
|
143
|
+
function clearTask(taskId) {
|
|
144
|
+
const state = load();
|
|
145
|
+
if (state.current_task === taskId) {
|
|
146
|
+
return save({
|
|
147
|
+
phase: null,
|
|
148
|
+
phase_step: null,
|
|
149
|
+
current_pr: null,
|
|
150
|
+
review_round: 0,
|
|
151
|
+
branch: null,
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
return state;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Clear a specific phase step (for partial retry, e.g. merge-only)
|
|
159
|
+
*/
|
|
160
|
+
function clearStep(taskId, phase, phaseStep) {
|
|
161
|
+
const state = load();
|
|
162
|
+
if (state.current_task === taskId && state.phase === phase && state.phase_step === phaseStep) {
|
|
163
|
+
return save({
|
|
164
|
+
phase: null,
|
|
165
|
+
phase_step: null,
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
return state;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* E1/E4: Validate consistency between checkpoint state and tasks status.
|
|
173
|
+
* Detects and repairs mismatches such as:
|
|
174
|
+
* - Task marked 'in_progress' but no checkpoint data for it
|
|
175
|
+
* - Checkpoint points to a task that's already completed
|
|
176
|
+
* - Multiple tasks marked 'in_progress' simultaneously
|
|
177
|
+
* @param {object} tasks - The tasks.json data structure
|
|
178
|
+
* @returns {{ ok: boolean, repairs: string[] }}
|
|
179
|
+
*/
|
|
180
|
+
function validateConsistency(tasks) {
|
|
181
|
+
const state = load();
|
|
182
|
+
const repairs = [];
|
|
183
|
+
|
|
184
|
+
if (!tasks?.tasks || !Array.isArray(tasks.tasks)) {
|
|
185
|
+
return { ok: true, repairs: [] };
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Check 1: Multiple in_progress tasks (should only be one at a time)
|
|
189
|
+
const inProgress = tasks.tasks.filter(t => t.status === 'in_progress');
|
|
190
|
+
if (inProgress.length > 1) {
|
|
191
|
+
// Keep the one matching checkpoint, reset others to pending
|
|
192
|
+
for (const task of inProgress) {
|
|
193
|
+
if (task.id !== state.current_task) {
|
|
194
|
+
task.status = 'pending';
|
|
195
|
+
repairs.push(`Task #${task.id}: was 'in_progress' but not current — reset to 'pending'`);
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Check 2: Task is in_progress but checkpoint has no data for it
|
|
201
|
+
if (inProgress.length === 1 && state.current_task !== inProgress[0].id) {
|
|
202
|
+
if (!state.phase) {
|
|
203
|
+
// Checkpoint is empty — task was marked in_progress but never started
|
|
204
|
+
inProgress[0].status = 'pending';
|
|
205
|
+
repairs.push(`Task #${inProgress[0].id}: marked 'in_progress' but no checkpoint data — reset to 'pending'`);
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// Check 3: Checkpoint points to a completed task (stale checkpoint)
|
|
210
|
+
if (state.current_task > 0 && state.phase) {
|
|
211
|
+
const checkpointTask = tasks.tasks.find(t => t.id === state.current_task);
|
|
212
|
+
if (checkpointTask && checkpointTask.status === 'completed') {
|
|
213
|
+
// Checkpoint is stale — clear it
|
|
214
|
+
save({ phase: null, phase_step: null, current_pr: null, review_round: 0, branch: null });
|
|
215
|
+
repairs.push(`Checkpoint for task #${state.current_task} cleared (task already completed)`);
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
// Check 4: Checkpoint task ID doesn't exist in tasks list
|
|
220
|
+
if (state.current_task > 0) {
|
|
221
|
+
const exists = tasks.tasks.find(t => t.id === state.current_task);
|
|
222
|
+
if (!exists) {
|
|
223
|
+
save({ current_task: 0, phase: null, phase_step: null, current_pr: null, review_round: 0, branch: null });
|
|
224
|
+
repairs.push(`Checkpoint referenced non-existent task #${state.current_task} — reset`);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return { ok: repairs.length === 0, repairs };
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
return { load, save, saveStep, completeTask, isStepDone, reset, clearTask, clearStep, validateConsistency };
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
function getDefaultState() {
|
|
235
|
+
return {
|
|
236
|
+
current_task: 0,
|
|
237
|
+
phase: null,
|
|
238
|
+
phase_step: null,
|
|
239
|
+
current_pr: null,
|
|
240
|
+
review_round: 0,
|
|
241
|
+
branch: null,
|
|
242
|
+
status: 'initialized',
|
|
243
|
+
last_updated: new Date().toISOString(),
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|