oh-my-claude-sisyphus 3.8.6 → 3.8.8

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,23 +1,16 @@
1
1
  #!/usr/bin/env node
2
- // OMC Persistent Mode Hook (Node.js)
3
- // Unified handler for ultrawork, ralph-loop, and todo continuation
4
- // Cross-platform: Windows, macOS, Linux
5
-
6
- import { existsSync, readFileSync, writeFileSync, readdirSync } from 'fs';
7
- import { join } from 'path';
8
- import { homedir } from 'os';
9
2
 
10
3
  /**
11
- * Validates session ID to prevent path traversal attacks.
12
- * @param {string} sessionId
13
- * @returns {boolean}
4
+ * OMC Persistent Mode Hook (Node.js)
5
+ * Minimal continuation enforcer for all OMC modes.
6
+ * Stripped down for reliability — no optional imports, no PRD, no notepad pruning.
7
+ *
8
+ * Supported modes: ralph, autopilot, ultrapilot, swarm, ultrawork, ecomode, ultraqa, pipeline
14
9
  */
15
- function isValidSessionId(sessionId) {
16
- if (!sessionId || typeof sessionId !== 'string') return false;
17
- // Allow alphanumeric, hyphens, and underscores only
18
- // Must not start with dot or hyphen
19
- return /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,255}$/.test(sessionId);
20
- }
10
+
11
+ import { existsSync, readFileSync, writeFileSync, readdirSync, mkdirSync } from 'fs';
12
+ import { join } from 'path';
13
+ import { homedir } from 'os';
21
14
 
22
15
  async function readStdin() {
23
16
  const chunks = [];
@@ -36,19 +29,43 @@ function readJsonFile(path) {
36
29
  }
37
30
  }
38
31
 
32
+ function writeJsonFile(path, data) {
33
+ try {
34
+ // Ensure directory exists
35
+ const dir = path.substring(0, path.lastIndexOf('/'));
36
+ if (dir && !existsSync(dir)) {
37
+ mkdirSync(dir, { recursive: true });
38
+ }
39
+ writeFileSync(path, JSON.stringify(data, null, 2));
40
+ return true;
41
+ } catch {
42
+ return false;
43
+ }
44
+ }
45
+
39
46
  /**
40
- * Count incomplete tasks in the new Task system.
41
- *
42
- * SYNC NOTICE: This function is intentionally duplicated across:
43
- * - templates/hooks/persistent-mode.mjs
44
- * - templates/hooks/stop-continuation.mjs
45
- * - src/hooks/todo-continuation/index.ts (as checkIncompleteTasks)
46
- *
47
- * Templates cannot import shared modules (they're standalone scripts).
48
- * When modifying this logic, update ALL THREE files to maintain consistency.
47
+ * Read state file from local or global location, tracking the source.
48
+ */
49
+ function readStateFile(stateDir, globalStateDir, filename) {
50
+ const localPath = join(stateDir, filename);
51
+ const globalPath = join(globalStateDir, filename);
52
+
53
+ let state = readJsonFile(localPath);
54
+ if (state) return { state, path: localPath };
55
+
56
+ state = readJsonFile(globalPath);
57
+ if (state) return { state, path: globalPath };
58
+
59
+ return { state: null, path: localPath }; // Default to local for new writes
60
+ }
61
+
62
+ /**
63
+ * Count incomplete Tasks from Claude Code's native Task system.
49
64
  */
50
65
  function countIncompleteTasks(sessionId) {
51
- if (!sessionId || !isValidSessionId(sessionId)) return 0;
66
+ if (!sessionId || typeof sessionId !== 'string') return 0;
67
+ if (!/^[a-zA-Z0-9][a-zA-Z0-9_-]{0,255}$/.test(sessionId)) return 0;
68
+
52
69
  const taskDir = join(homedir(), '.claude', 'tasks', sessionId);
53
70
  if (!existsSync(taskDir)) return 0;
54
71
 
@@ -59,232 +76,312 @@ function countIncompleteTasks(sessionId) {
59
76
  try {
60
77
  const content = readFileSync(join(taskDir, file), 'utf-8');
61
78
  const task = JSON.parse(content);
62
- // Match TypeScript isTaskIncomplete(): only pending/in_progress are incomplete
63
- // 'deleted' and 'completed' are both treated as done
64
79
  if (task.status === 'pending' || task.status === 'in_progress') count++;
65
- } catch { /* skip invalid files */ }
80
+ } catch { /* skip */ }
66
81
  }
67
- } catch { /* dir read error */ }
82
+ } catch { /* skip */ }
68
83
  return count;
69
84
  }
70
85
 
71
- function writeJsonFile(path, data) {
72
- try {
73
- writeFileSync(path, JSON.stringify(data, null, 2));
74
- return true;
75
- } catch {
76
- return false;
77
- }
78
- }
79
-
80
86
  function countIncompleteTodos(todosDir, projectDir) {
81
87
  let count = 0;
82
88
 
83
- // Check global todos
84
89
  if (existsSync(todosDir)) {
85
90
  try {
86
91
  const files = readdirSync(todosDir).filter(f => f.endsWith('.json'));
87
92
  for (const file of files) {
88
- const todos = readJsonFile(join(todosDir, file));
89
- if (Array.isArray(todos)) {
93
+ try {
94
+ const content = readFileSync(join(todosDir, file), 'utf-8');
95
+ const data = JSON.parse(content);
96
+ const todos = Array.isArray(data) ? data : (Array.isArray(data?.todos) ? data.todos : []);
90
97
  count += todos.filter(t => t.status !== 'completed' && t.status !== 'cancelled').length;
91
- }
98
+ } catch { /* skip */ }
92
99
  }
93
- } catch {}
100
+ } catch { /* skip */ }
94
101
  }
95
102
 
96
- // Check project todos
97
103
  for (const path of [
98
104
  join(projectDir, '.omc', 'todos.json'),
99
105
  join(projectDir, '.claude', 'todos.json')
100
106
  ]) {
101
- const todos = readJsonFile(path);
102
- if (Array.isArray(todos)) {
107
+ try {
108
+ const data = readJsonFile(path);
109
+ const todos = Array.isArray(data) ? data : (Array.isArray(data?.todos) ? data.todos : []);
103
110
  count += todos.filter(t => t.status !== 'completed' && t.status !== 'cancelled').length;
104
- }
111
+ } catch { /* skip */ }
105
112
  }
106
113
 
107
114
  return count;
108
115
  }
109
116
 
117
+ /**
118
+ * Detect if stop was triggered by context-limit related reasons.
119
+ * When context is exhausted, Claude Code needs to stop so it can compact.
120
+ * Blocking these stops causes a deadlock: can't compact because can't stop,
121
+ * can't continue because context is full.
122
+ *
123
+ * See: https://github.com/Yeachan-Heo/oh-my-claudecode/issues/213
124
+ */
125
+ function isContextLimitStop(data) {
126
+ const reason = (data.stop_reason || data.stopReason || '').toLowerCase();
127
+
128
+ const contextPatterns = [
129
+ 'context_limit',
130
+ 'context_window',
131
+ 'context_exceeded',
132
+ 'context_full',
133
+ 'max_context',
134
+ 'token_limit',
135
+ 'max_tokens',
136
+ 'conversation_too_long',
137
+ 'input_too_long',
138
+ ];
139
+
140
+ if (contextPatterns.some(p => reason.includes(p))) {
141
+ return true;
142
+ }
143
+
144
+ const endTurnReason = (data.end_turn_reason || data.endTurnReason || '').toLowerCase();
145
+ if (endTurnReason && contextPatterns.some(p => endTurnReason.includes(p))) {
146
+ return true;
147
+ }
148
+
149
+ return false;
150
+ }
151
+
152
+ /**
153
+ * Detect if stop was triggered by user abort (Ctrl+C, cancel button, etc.)
154
+ */
155
+ function isUserAbort(data) {
156
+ if (data.user_requested || data.userRequested) return true;
157
+
158
+ const reason = (data.stop_reason || data.stopReason || '').toLowerCase();
159
+ const abortPatterns = [
160
+ 'user_cancel', 'user_interrupt', 'ctrl_c', 'manual_stop',
161
+ 'aborted', 'abort', 'cancel', 'interrupt',
162
+ ];
163
+ return abortPatterns.some(p => reason.includes(p));
164
+ }
165
+
110
166
  async function main() {
111
167
  try {
112
168
  const input = await readStdin();
113
169
  let data = {};
114
170
  try { data = JSON.parse(input); } catch {}
115
171
 
116
- const stopReason = data.stop_reason || data.stopReason || '';
117
- const userRequested = data.user_requested || data.userRequested || false;
172
+ const directory = data.directory || process.cwd();
118
173
  const sessionId = data.sessionId || data.session_id || '';
174
+ const todosDir = join(homedir(), '.claude', 'todos');
175
+ const stateDir = join(directory, '.omc', 'state');
176
+ const globalStateDir = join(homedir(), '.omc', 'state');
119
177
 
120
- // Check for user abort - skip all continuation enforcement
121
- // NOTE: Abort patterns are assumed - verify against actual Claude Code API values
122
- if (userRequested || /abort|cancel|interrupt|ctrl_c|manual_stop/i.test(stopReason)) {
178
+ // CRITICAL: Never block context-limit stops.
179
+ // Blocking these causes a deadlock where Claude Code cannot compact.
180
+ // See: https://github.com/Yeachan-Heo/oh-my-claudecode/issues/213
181
+ if (isContextLimitStop(data)) {
123
182
  console.log(JSON.stringify({ continue: true }));
124
183
  return;
125
184
  }
126
185
 
127
- const directory = data.directory || process.cwd();
128
- const todosDir = join(homedir(), '.claude', 'todos');
129
-
130
- // Check for ultrawork state
131
- let ultraworkState = readJsonFile(join(directory, '.omc', 'ultrawork-state.json'))
132
- || readJsonFile(join(homedir(), '.claude', 'ultrawork-state.json'));
133
-
134
- // Check for ralph loop state
135
- const ralphState = readJsonFile(join(directory, '.omc', 'ralph-state.json'));
186
+ // Respect user abort (Ctrl+C, cancel)
187
+ if (isUserAbort(data)) {
188
+ console.log(JSON.stringify({ continue: true }));
189
+ return;
190
+ }
136
191
 
137
- // Check for verification state (oracle verification)
138
- const verificationState = readJsonFile(join(directory, '.omc', 'ralph-verification.json'));
192
+ // Read all mode states (local-first with fallback to global)
193
+ const ralph = readStateFile(stateDir, globalStateDir, 'ralph-state.json');
194
+ const autopilot = readStateFile(stateDir, globalStateDir, 'autopilot-state.json');
195
+ const ultrapilot = readStateFile(stateDir, globalStateDir, 'ultrapilot-state.json');
196
+ const ultrawork = readStateFile(stateDir, globalStateDir, 'ultrawork-state.json');
197
+ const ecomode = readStateFile(stateDir, globalStateDir, 'ecomode-state.json');
198
+ const ultraqa = readStateFile(stateDir, globalStateDir, 'ultraqa-state.json');
199
+ const pipeline = readStateFile(stateDir, globalStateDir, 'pipeline-state.json');
139
200
 
140
- // Count incomplete todos
141
- const incompleteCount = countIncompleteTodos(todosDir, directory);
201
+ // Swarm uses swarm-summary.json (not swarm-state.json) + marker file
202
+ const swarmMarker = existsSync(join(stateDir, 'swarm-active.marker'));
203
+ const swarmSummary = readJsonFile(join(stateDir, 'swarm-summary.json'));
142
204
 
143
- // Count incomplete Tasks
205
+ // Count incomplete items
144
206
  const taskCount = countIncompleteTasks(sessionId);
145
- const totalIncomplete = taskCount + incompleteCount;
207
+ const todoCount = countIncompleteTodos(todosDir, directory);
208
+ const totalIncomplete = taskCount + todoCount;
146
209
 
147
- // Priority 1: Ralph Loop with Oracle Verification
148
- if (ralphState?.active) {
149
- const iteration = ralphState.iteration || 1;
150
- const maxIter = ralphState.max_iterations || 10;
210
+ // Priority 1: Ralph Loop (explicit persistence mode)
211
+ if (ralph.state?.active) {
212
+ const iteration = ralph.state.iteration || 1;
213
+ const maxIter = ralph.state.max_iterations || 100;
151
214
 
152
- // Check if oracle verification is pending
153
- if (verificationState?.pending) {
154
- const attempt = (verificationState.verification_attempts || 0) + 1;
155
- const maxAttempts = verificationState.max_verification_attempts || 3;
215
+ if (iteration < maxIter) {
216
+ ralph.state.iteration = iteration + 1;
217
+ writeJsonFile(ralph.path, ralph.state);
156
218
 
157
219
  console.log(JSON.stringify({
158
- decision: "block",
159
- reason: `<ralph-verification>
160
-
161
- [ORACLE VERIFICATION REQUIRED - Attempt ${attempt}/${maxAttempts}]
162
-
163
- The agent claims the task is complete. Before accepting, YOU MUST verify with Oracle.
164
-
165
- **Original Task:**
166
- ${verificationState.original_task || ralphState.prompt || 'No task specified'}
167
-
168
- **Completion Claim:**
169
- ${verificationState.completion_claim || 'Task marked complete'}
170
-
171
- ${verificationState.oracle_feedback ? `**Previous Oracle Feedback (rejected):**
172
- ${verificationState.oracle_feedback}
173
- ` : ''}
174
-
175
- ## MANDATORY VERIFICATION STEPS
220
+ continue: false,
221
+ reason: `[RALPH LOOP - ITERATION ${iteration + 1}/${maxIter}] Work is NOT done. Continue. When complete, output: <promise>${ralph.state.completion_promise || 'DONE'}</promise>\n${ralph.state.prompt ? `Task: ${ralph.state.prompt}` : ''}`
222
+ }));
223
+ return;
224
+ }
225
+ }
176
226
 
177
- 1. **Spawn Oracle Agent** for verification:
178
- \`\`\`
179
- Task(subagent_type="oracle", prompt="Verify this task completion claim...")
180
- \`\`\`
227
+ // Priority 2: Autopilot (high-level orchestration)
228
+ if (autopilot.state?.active) {
229
+ const phase = autopilot.state.phase || 'unknown';
230
+ if (phase !== 'complete') {
231
+ const newCount = (autopilot.state.reinforcement_count || 0) + 1;
232
+ if (newCount <= 20) {
233
+ autopilot.state.reinforcement_count = newCount;
234
+ writeJsonFile(autopilot.path, autopilot.state);
235
+
236
+ console.log(JSON.stringify({
237
+ continue: false,
238
+ reason: `[AUTOPILOT - Phase: ${phase}] Autopilot not complete. Continue working.`
239
+ }));
240
+ return;
241
+ }
242
+ }
243
+ }
181
244
 
182
- 2. **Oracle must check:**
183
- - Are ALL requirements from the original task met?
184
- - Is the implementation complete, not partial?
185
- - Are there any obvious bugs or issues?
186
- - Does the code compile/run without errors?
187
- - Are tests passing (if applicable)?
245
+ // Priority 3: Ultrapilot (parallel autopilot)
246
+ if (ultrapilot.state?.active) {
247
+ const workers = ultrapilot.state.workers || [];
248
+ const incomplete = workers.filter(w => w.status !== 'complete' && w.status !== 'failed').length;
249
+ if (incomplete > 0) {
250
+ const newCount = (ultrapilot.state.reinforcement_count || 0) + 1;
251
+ if (newCount <= 20) {
252
+ ultrapilot.state.reinforcement_count = newCount;
253
+ writeJsonFile(ultrapilot.path, ultrapilot.state);
254
+
255
+ console.log(JSON.stringify({
256
+ continue: false,
257
+ reason: `[ULTRAPILOT] ${incomplete} workers still running. Continue.`
258
+ }));
259
+ return;
260
+ }
261
+ }
262
+ }
188
263
 
189
- 3. **Based on Oracle's response:**
190
- - If APPROVED: Output \`<oracle-approved>VERIFIED_COMPLETE</oracle-approved>\`
191
- - If REJECTED: Continue working on the identified issues
264
+ // Priority 4: Swarm (coordinated agents with SQLite)
265
+ if (swarmMarker && swarmSummary?.active) {
266
+ const pending = (swarmSummary.tasks_pending || 0) + (swarmSummary.tasks_claimed || 0);
267
+ if (pending > 0) {
268
+ const newCount = (swarmSummary.reinforcement_count || 0) + 1;
269
+ if (newCount <= 15) {
270
+ swarmSummary.reinforcement_count = newCount;
271
+ writeJsonFile(join(stateDir, 'swarm-summary.json'), swarmSummary);
272
+
273
+ console.log(JSON.stringify({
274
+ continue: false,
275
+ reason: `[SWARM ACTIVE] ${pending} tasks remain. Continue working.`
276
+ }));
277
+ return;
278
+ }
279
+ }
280
+ }
192
281
 
193
- DO NOT output the completion promise again until Oracle approves.
282
+ // Priority 5: Pipeline (sequential stages)
283
+ if (pipeline.state?.active) {
284
+ const currentStage = pipeline.state.current_stage || 0;
285
+ const totalStages = pipeline.state.stages?.length || 0;
286
+ if (currentStage < totalStages) {
287
+ const newCount = (pipeline.state.reinforcement_count || 0) + 1;
288
+ if (newCount <= 15) {
289
+ pipeline.state.reinforcement_count = newCount;
290
+ writeJsonFile(pipeline.path, pipeline.state);
291
+
292
+ console.log(JSON.stringify({
293
+ continue: false,
294
+ reason: `[PIPELINE - Stage ${currentStage + 1}/${totalStages}] Pipeline not complete. Continue.`
295
+ }));
296
+ return;
297
+ }
298
+ }
299
+ }
194
300
 
195
- </ralph-verification>
301
+ // Priority 6: UltraQA (QA cycling)
302
+ if (ultraqa.state?.active) {
303
+ const cycle = ultraqa.state.cycle || 1;
304
+ const maxCycles = ultraqa.state.max_cycles || 10;
305
+ if (cycle < maxCycles && !ultraqa.state.all_passing) {
306
+ ultraqa.state.cycle = cycle + 1;
307
+ writeJsonFile(ultraqa.path, ultraqa.state);
196
308
 
197
- ---
198
- `
309
+ console.log(JSON.stringify({
310
+ continue: false,
311
+ reason: `[ULTRAQA - Cycle ${cycle + 1}/${maxCycles}] Tests not all passing. Continue fixing.`
199
312
  }));
200
313
  return;
201
314
  }
315
+ }
202
316
 
203
- if (iteration < maxIter) {
204
- const newIter = iteration + 1;
205
- ralphState.iteration = newIter;
206
- writeJsonFile(join(directory, '.omc', 'ralph-state.json'), ralphState);
317
+ // Priority 7: Ultrawork with incomplete todos/tasks
318
+ if (ultrawork.state?.active && totalIncomplete > 0) {
319
+ const newCount = (ultrawork.state.reinforcement_count || 0) + 1;
320
+ const maxReinforcements = ultrawork.state.max_reinforcements || 15;
207
321
 
322
+ if (newCount > maxReinforcements) {
208
323
  console.log(JSON.stringify({
209
- decision: "block",
210
- reason: `<ralph-loop-continuation>
211
-
212
- [RALPH LOOP - ITERATION ${newIter}/${maxIter}]
213
-
214
- Your previous attempt did not output the completion promise. The work is NOT done yet.
215
-
216
- CRITICAL INSTRUCTIONS:
217
- 1. Review your progress and the original task
218
- 2. Check your todo list - are ALL items marked complete?
219
- 3. Continue from where you left off
220
- 4. When FULLY complete, output: <promise>${ralphState.completion_promise || 'TASK_COMPLETE'}</promise>
221
- 5. Do NOT stop until the task is truly done
222
-
223
- ${ralphState.prompt ? `Original task: ${ralphState.prompt}` : ''}
224
-
225
- </ralph-loop-continuation>
226
-
227
- ---
228
- `
324
+ continue: true,
325
+ reason: `[ULTRAWORK ESCAPE] Max reinforcements reached. Allowing stop.`
229
326
  }));
230
327
  return;
231
328
  }
232
- }
233
-
234
- // Priority 2: Ultrawork with incomplete todos
235
- if (ultraworkState?.active && totalIncomplete > 0) {
236
- const newCount = (ultraworkState.reinforcement_count || 0) + 1;
237
- ultraworkState.reinforcement_count = newCount;
238
- ultraworkState.last_checked_at = new Date().toISOString();
239
329
 
240
- writeJsonFile(join(directory, '.omc', 'ultrawork-state.json'), ultraworkState);
330
+ ultrawork.state.reinforcement_count = newCount;
331
+ ultrawork.state.last_checked_at = new Date().toISOString();
332
+ writeJsonFile(ultrawork.path, ultrawork.state);
241
333
 
334
+ const itemType = taskCount > 0 ? 'Tasks' : 'todos';
242
335
  console.log(JSON.stringify({
243
- decision: "block",
244
- reason: `<ultrawork-persistence>
245
-
246
- [ULTRAWORK MODE STILL ACTIVE - Reinforcement #${newCount}]
247
-
248
- Your ultrawork session is NOT complete. ${totalIncomplete} incomplete items remain.
249
-
250
- REMEMBER THE ULTRAWORK RULES:
251
- - **PARALLEL**: Fire independent calls simultaneously - NEVER wait sequentially
252
- - **BACKGROUND FIRST**: Use Task(run_in_background=true) for exploration (10+ concurrent)
253
- - **TODO**: Track EVERY step. Mark complete IMMEDIATELY after each
254
- - **VERIFY**: Check ALL requirements met before done
255
- - **NO Premature Stopping**: ALL TODOs must be complete
336
+ continue: false,
337
+ reason: `[ULTRAWORK #${newCount}] ${totalIncomplete} incomplete ${itemType}. Continue working.\n${ultrawork.state.original_prompt ? `Task: ${ultrawork.state.original_prompt}` : ''}`
338
+ }));
339
+ return;
340
+ }
256
341
 
257
- Continue working on the next pending task. DO NOT STOP until all tasks are marked complete.
342
+ // Priority 8: Ecomode with incomplete todos/tasks
343
+ if (ecomode.state?.active && totalIncomplete > 0) {
344
+ const newCount = (ecomode.state.reinforcement_count || 0) + 1;
345
+ const maxReinforcements = ecomode.state.max_reinforcements || 15;
258
346
 
259
- ${ultraworkState.original_prompt ? `Original task: ${ultraworkState.original_prompt}` : ''}
347
+ if (newCount > maxReinforcements) {
348
+ console.log(JSON.stringify({
349
+ continue: true,
350
+ reason: `[ECOMODE ESCAPE] Max reinforcements reached. Allowing stop.`
351
+ }));
352
+ return;
353
+ }
260
354
 
261
- </ultrawork-persistence>
355
+ ecomode.state.reinforcement_count = newCount;
356
+ writeJsonFile(ecomode.path, ecomode.state);
262
357
 
263
- ---
264
- `
358
+ const itemType = taskCount > 0 ? 'Tasks' : 'todos';
359
+ console.log(JSON.stringify({
360
+ continue: false,
361
+ reason: `[ECOMODE #${newCount}] ${totalIncomplete} incomplete ${itemType}. Continue working.`
265
362
  }));
266
363
  return;
267
364
  }
268
365
 
269
- // Priority 3: Todo Continuation
366
+ // Priority 9: Generic Task/Todo continuation (no specific mode)
270
367
  if (totalIncomplete > 0) {
271
- const itemType = taskCount > 0 ? 'Tasks' : 'todos';
272
- console.log(JSON.stringify({
273
- decision: "block",
274
- reason: `<todo-continuation>
275
-
276
- [SYSTEM REMINDER - CONTINUATION]
277
-
278
- Incomplete ${itemType} remain (${totalIncomplete} remaining). Continue working on the next pending item.
368
+ const contFile = join(stateDir, 'continuation-count.json');
369
+ let contState = readJsonFile(contFile) || { count: 0 };
370
+ contState.count = (contState.count || 0) + 1;
371
+ writeJsonFile(contFile, contState);
279
372
 
280
- - Proceed without asking for permission
281
- - Mark each item complete when finished
282
- - Do not stop until all items are done
283
-
284
- </todo-continuation>
373
+ if (contState.count > 15) {
374
+ console.log(JSON.stringify({
375
+ continue: true,
376
+ reason: `[CONTINUATION ESCAPE] Max continuations reached. Allowing stop.`
377
+ }));
378
+ return;
379
+ }
285
380
 
286
- ---
287
- `
381
+ const itemType = taskCount > 0 ? 'Tasks' : 'todos';
382
+ console.log(JSON.stringify({
383
+ continue: false,
384
+ reason: `[CONTINUATION ${contState.count}/15] ${totalIncomplete} incomplete ${itemType}. Continue working.`
288
385
  }));
289
386
  return;
290
387
  }
@@ -292,6 +389,8 @@ Incomplete ${itemType} remain (${totalIncomplete} remaining). Continue working o
292
389
  // No blocking needed
293
390
  console.log(JSON.stringify({ continue: true }));
294
391
  } catch (error) {
392
+ // On any error, allow stop rather than blocking forever
393
+ console.error(`[persistent-mode] Error: ${error.message}`);
295
394
  console.log(JSON.stringify({ continue: true }));
296
395
  }
297
396
  }