@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,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
+