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,30 +1,17 @@
1
1
  #!/usr/bin/env node
2
2
 
3
3
  /**
4
- * Sisyphus Persistent Mode Hook (Node.js)
5
- * Unified handler for ultrawork, ralph-loop, and todo continuation
6
- * Cross-platform: Windows, macOS, Linux
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
7
9
  */
8
10
 
9
- import { existsSync, readFileSync, writeFileSync, readdirSync } from 'fs';
10
- import { join, dirname } from 'path';
11
+ import { existsSync, readFileSync, writeFileSync, readdirSync, mkdirSync } from 'fs';
12
+ import { join } from 'path';
11
13
  import { homedir } from 'os';
12
- import { fileURLToPath } from 'url';
13
-
14
- // Dynamic import for notepad with fallback
15
- const __filename = fileURLToPath(import.meta.url);
16
- const __dirname = dirname(__filename);
17
- let pruneOldEntries = null;
18
-
19
- try {
20
- const notepadModule = await import(join(__dirname, '../dist/hooks/notepad/index.js'));
21
- pruneOldEntries = notepadModule.pruneOldEntries;
22
- } catch (err) {
23
- // Notepad module not available - pruning will be skipped
24
- // This can happen in older versions or if build failed
25
- }
26
14
 
27
- // Read all stdin
28
15
  async function readStdin() {
29
16
  const chunks = [];
30
17
  for await (const chunk of process.stdin) {
@@ -33,7 +20,6 @@ async function readStdin() {
33
20
  return Buffer.concat(chunks).toString('utf-8');
34
21
  }
35
22
 
36
- // Read JSON file safely
37
23
  function readJsonFile(path) {
38
24
  try {
39
25
  if (!existsSync(path)) return null;
@@ -43,9 +29,13 @@ function readJsonFile(path) {
43
29
  }
44
30
  }
45
31
 
46
- // Write JSON file safely
47
32
  function writeJsonFile(path, data) {
48
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
+ }
49
39
  writeFileSync(path, JSON.stringify(data, null, 2));
50
40
  return true;
51
41
  } catch {
@@ -53,109 +43,128 @@ function writeJsonFile(path, data) {
53
43
  }
54
44
  }
55
45
 
56
- // Read PRD and get status
57
- function getPrdStatus(projectDir) {
58
- // Check both root and .sisyphus for prd.json
59
- const paths = [
60
- join(projectDir, 'prd.json'),
61
- join(projectDir, '.omc', 'prd.json')
62
- ];
46
+ /**
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);
63
52
 
64
- for (const prdPath of paths) {
65
- const prd = readJsonFile(prdPath);
66
- if (prd && Array.isArray(prd.userStories)) {
67
- const stories = prd.userStories;
68
- const completed = stories.filter(s => s.passes === true);
69
- const pending = stories.filter(s => s.passes !== true);
70
- const sortedPending = [...pending].sort((a, b) => (a.priority || 999) - (b.priority || 999));
71
-
72
- return {
73
- hasPrd: true,
74
- total: stories.length,
75
- completed: completed.length,
76
- pending: pending.length,
77
- allComplete: pending.length === 0,
78
- nextStory: sortedPending[0] || null,
79
- incompleteIds: pending.map(s => s.id)
80
- };
81
- }
82
- }
53
+ let state = readJsonFile(localPath);
54
+ if (state) return { state, path: localPath };
55
+
56
+ state = readJsonFile(globalPath);
57
+ if (state) return { state, path: globalPath };
83
58
 
84
- return { hasPrd: false, allComplete: false, nextStory: null };
59
+ return { state: null, path: localPath }; // Default to local for new writes
85
60
  }
86
61
 
87
- // Read progress.txt patterns for context
88
- function getProgressPatterns(projectDir) {
89
- const paths = [
90
- join(projectDir, 'progress.txt'),
91
- join(projectDir, '.omc', 'progress.txt')
92
- ];
62
+ /**
63
+ * Count incomplete Tasks from Claude Code's native Task system.
64
+ */
65
+ function countIncompleteTasks(sessionId) {
66
+ if (!sessionId || typeof sessionId !== 'string') return 0;
67
+ if (!/^[a-zA-Z0-9][a-zA-Z0-9_-]{0,255}$/.test(sessionId)) return 0;
93
68
 
94
- for (const progressPath of paths) {
95
- if (existsSync(progressPath)) {
96
- try {
97
- const content = readFileSync(progressPath, 'utf-8');
98
- const patterns = [];
99
- let inPatterns = false;
100
-
101
- for (const line of content.split('\n')) {
102
- const trimmed = line.trim();
103
- if (trimmed === '## Codebase Patterns') {
104
- inPatterns = true;
105
- continue;
106
- }
107
- if (trimmed === '---') {
108
- inPatterns = false;
109
- continue;
110
- }
111
- if (inPatterns && trimmed.startsWith('-')) {
112
- const pattern = trimmed.slice(1).trim();
113
- if (pattern && !pattern.includes('No patterns')) {
114
- patterns.push(pattern);
115
- }
116
- }
117
- }
69
+ const taskDir = join(homedir(), '.claude', 'tasks', sessionId);
70
+ if (!existsSync(taskDir)) return 0;
118
71
 
119
- return patterns;
120
- } catch {}
72
+ let count = 0;
73
+ try {
74
+ const files = readdirSync(taskDir).filter(f => f.endsWith('.json') && f !== '.lock');
75
+ for (const file of files) {
76
+ try {
77
+ const content = readFileSync(join(taskDir, file), 'utf-8');
78
+ const task = JSON.parse(content);
79
+ if (task.status === 'pending' || task.status === 'in_progress') count++;
80
+ } catch { /* skip */ }
121
81
  }
122
- }
123
-
124
- return [];
82
+ } catch { /* skip */ }
83
+ return count;
125
84
  }
126
85
 
127
- // Count incomplete todos
128
86
  function countIncompleteTodos(todosDir, projectDir) {
129
87
  let count = 0;
130
88
 
131
- // Check global todos
132
89
  if (existsSync(todosDir)) {
133
90
  try {
134
91
  const files = readdirSync(todosDir).filter(f => f.endsWith('.json'));
135
92
  for (const file of files) {
136
- const todos = readJsonFile(join(todosDir, file));
137
- 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 : []);
138
97
  count += todos.filter(t => t.status !== 'completed' && t.status !== 'cancelled').length;
139
- }
98
+ } catch { /* skip */ }
140
99
  }
141
- } catch {}
100
+ } catch { /* skip */ }
142
101
  }
143
102
 
144
- // Check project todos
145
103
  for (const path of [
146
104
  join(projectDir, '.omc', 'todos.json'),
147
105
  join(projectDir, '.claude', 'todos.json')
148
106
  ]) {
149
- const data = readJsonFile(path);
150
- const todos = data?.todos || data;
151
- if (Array.isArray(todos)) {
107
+ try {
108
+ const data = readJsonFile(path);
109
+ const todos = Array.isArray(data) ? data : (Array.isArray(data?.todos) ? data.todos : []);
152
110
  count += todos.filter(t => t.status !== 'completed' && t.status !== 'cancelled').length;
153
- }
111
+ } catch { /* skip */ }
154
112
  }
155
113
 
156
114
  return count;
157
115
  }
158
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
+ // Known context-limit patterns (both confirmed and defensive)
129
+ const contextPatterns = [
130
+ 'context_limit',
131
+ 'context_window',
132
+ 'context_exceeded',
133
+ 'context_full',
134
+ 'max_context',
135
+ 'token_limit',
136
+ 'max_tokens',
137
+ 'conversation_too_long',
138
+ 'input_too_long',
139
+ ];
140
+
141
+ if (contextPatterns.some(p => reason.includes(p))) {
142
+ return true;
143
+ }
144
+
145
+ // Also check end_turn_reason (API-level field)
146
+ const endTurnReason = (data.end_turn_reason || data.endTurnReason || '').toLowerCase();
147
+ if (endTurnReason && contextPatterns.some(p => endTurnReason.includes(p))) {
148
+ return true;
149
+ }
150
+
151
+ return false;
152
+ }
153
+
154
+ /**
155
+ * Detect if stop was triggered by user abort (Ctrl+C, cancel button, etc.)
156
+ */
157
+ function isUserAbort(data) {
158
+ if (data.user_requested || data.userRequested) return true;
159
+
160
+ const reason = (data.stop_reason || data.stopReason || '').toLowerCase();
161
+ const abortPatterns = [
162
+ 'user_cancel', 'user_interrupt', 'ctrl_c', 'manual_stop',
163
+ 'aborted', 'abort', 'cancel', 'interrupt',
164
+ ];
165
+ return abortPatterns.some(p => reason.includes(p));
166
+ }
167
+
159
168
  async function main() {
160
169
  try {
161
170
  const input = await readStdin();
@@ -163,286 +172,227 @@ async function main() {
163
172
  try { data = JSON.parse(input); } catch {}
164
173
 
165
174
  const directory = data.directory || process.cwd();
175
+ const sessionId = data.sessionId || data.session_id || '';
166
176
  const todosDir = join(homedir(), '.claude', 'todos');
177
+ const stateDir = join(directory, '.omc', 'state');
178
+ const globalStateDir = join(homedir(), '.omc', 'state');
179
+
180
+ // CRITICAL: Never block context-limit stops.
181
+ // Blocking these causes a deadlock where Claude Code cannot compact.
182
+ // See: https://github.com/Yeachan-Heo/oh-my-claudecode/issues/213
183
+ if (isContextLimitStop(data)) {
184
+ console.log(JSON.stringify({ continue: true }));
185
+ return;
186
+ }
167
187
 
168
- // Check for ultrawork state
169
- let ultraworkState = readJsonFile(join(directory, '.omc', 'state', 'ultrawork-state.json'))
170
- || readJsonFile(join(homedir(), '.omc', 'state', 'ultrawork-state.json'));
171
-
172
- // Check for ralph loop state
173
- const ralphState = readJsonFile(join(directory, '.omc', 'state', 'ralph-state.json'));
174
-
175
- // Check for verification state
176
- const verificationState = readJsonFile(join(directory, '.omc', 'state', 'ralph-verification.json'));
177
-
178
- // Count incomplete todos
179
- const incompleteCount = countIncompleteTodos(todosDir, directory);
180
-
181
- // Check PRD status
182
- const prdStatus = getPrdStatus(directory);
183
- const progressPatterns = getProgressPatterns(directory);
188
+ // Respect user abort (Ctrl+C, cancel)
189
+ if (isUserAbort(data)) {
190
+ console.log(JSON.stringify({ continue: true }));
191
+ return;
192
+ }
184
193
 
185
- // Priority 1: Ralph Loop with PRD and Oracle Verification
186
- if (ralphState?.active) {
187
- const iteration = ralphState.iteration || 1;
188
- const maxIter = ralphState.max_iterations || 100; // Increased for PRD mode
194
+ // Read all mode states (local-first with fallback to global)
195
+ const ralph = readStateFile(stateDir, globalStateDir, 'ralph-state.json');
196
+ const autopilot = readStateFile(stateDir, globalStateDir, 'autopilot-state.json');
197
+ const ultrapilot = readStateFile(stateDir, globalStateDir, 'ultrapilot-state.json');
198
+ const ultrawork = readStateFile(stateDir, globalStateDir, 'ultrawork-state.json');
199
+ const ecomode = readStateFile(stateDir, globalStateDir, 'ecomode-state.json');
200
+ const ultraqa = readStateFile(stateDir, globalStateDir, 'ultraqa-state.json');
201
+ const pipeline = readStateFile(stateDir, globalStateDir, 'pipeline-state.json');
202
+
203
+ // Swarm uses swarm-summary.json (not swarm-state.json) + marker file
204
+ const swarmMarker = existsSync(join(stateDir, 'swarm-active.marker'));
205
+ const swarmSummary = readJsonFile(join(stateDir, 'swarm-summary.json'));
206
+
207
+ // Count incomplete items
208
+ const taskCount = countIncompleteTasks(sessionId);
209
+ const todoCount = countIncompleteTodos(todosDir, directory);
210
+ const totalIncomplete = taskCount + todoCount;
211
+
212
+ // Priority 1: Ralph Loop (explicit persistence mode)
213
+ if (ralph.state?.active) {
214
+ const iteration = ralph.state.iteration || 1;
215
+ const maxIter = ralph.state.max_iterations || 100;
189
216
 
190
- // If PRD exists and all stories are complete, allow completion
191
- if (prdStatus.hasPrd && prdStatus.allComplete) {
192
- // Prune old notepad entries on clean session stop
193
- try {
194
- if (pruneOldEntries) {
195
- const pruneResult = pruneOldEntries(directory, 7);
196
- if (pruneResult.pruned > 0) {
197
- // Optionally log: console.error(`Pruned ${pruneResult.pruned} old notepad entries`);
198
- }
199
- }
200
- } catch (err) {
201
- // Silently ignore prune errors
202
- }
217
+ if (iteration < maxIter) {
218
+ ralph.state.iteration = iteration + 1;
219
+ writeJsonFile(ralph.path, ralph.state);
203
220
 
204
221
  console.log(JSON.stringify({
205
- continue: true,
206
- reason: `[RALPH LOOP COMPLETE - PRD] All ${prdStatus.total} stories are complete! Great work!`
222
+ continue: false,
223
+ 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}` : ''}`
207
224
  }));
208
225
  return;
209
226
  }
227
+ }
210
228
 
211
- // Check if oracle verification is pending
212
- if (verificationState?.pending) {
213
- const attempt = (verificationState.verification_attempts || 0) + 1;
214
- const maxAttempts = verificationState.max_verification_attempts || 3;
215
-
216
- console.log(JSON.stringify({
217
- continue: false,
218
- reason: `<ralph-verification>
219
-
220
- [ORACLE VERIFICATION REQUIRED - Attempt ${attempt}/${maxAttempts}]
221
-
222
- The agent claims the task is complete. Before accepting, YOU MUST verify with Oracle.
223
-
224
- **Original Task:**
225
- ${verificationState.original_task || ralphState.prompt || 'No task specified'}
226
-
227
- **Completion Claim:**
228
- ${verificationState.completion_claim || 'Task marked complete'}
229
-
230
- ${verificationState.oracle_feedback ? `**Previous Oracle Feedback (rejected):**
231
- ${verificationState.oracle_feedback}
232
- ` : ''}
233
-
234
- ## MANDATORY VERIFICATION STEPS
235
-
236
- 1. **Spawn Oracle Agent** for verification
237
- 2. **Oracle must check:**
238
- - Are ALL requirements from the original task met?
239
- - Is the implementation complete, not partial?
240
- - Are there any obvious bugs or issues?
241
- - Does the code compile/run without errors?
242
- - Are tests passing (if applicable)?
243
-
244
- 3. **Based on Oracle's response:**
245
- - If APPROVED: Output \`<oracle-approved>VERIFIED_COMPLETE</oracle-approved>\`
246
- - If REJECTED: Continue working on the identified issues
247
-
248
- </ralph-verification>
229
+ // Priority 2: Autopilot (high-level orchestration)
230
+ if (autopilot.state?.active) {
231
+ const phase = autopilot.state.phase || 'unknown';
232
+ if (phase !== 'complete') {
233
+ const newCount = (autopilot.state.reinforcement_count || 0) + 1;
234
+ if (newCount <= 20) {
235
+ autopilot.state.reinforcement_count = newCount;
236
+ writeJsonFile(autopilot.path, autopilot.state);
237
+
238
+ console.log(JSON.stringify({
239
+ continue: false,
240
+ reason: `[AUTOPILOT - Phase: ${phase}] Autopilot not complete. Continue working.`
241
+ }));
242
+ return;
243
+ }
244
+ }
245
+ }
249
246
 
250
- ---
251
- `
252
- }));
253
- return;
247
+ // Priority 3: Ultrapilot (parallel autopilot)
248
+ if (ultrapilot.state?.active) {
249
+ const workers = ultrapilot.state.workers || [];
250
+ const incomplete = workers.filter(w => w.status !== 'complete' && w.status !== 'failed').length;
251
+ if (incomplete > 0) {
252
+ const newCount = (ultrapilot.state.reinforcement_count || 0) + 1;
253
+ if (newCount <= 20) {
254
+ ultrapilot.state.reinforcement_count = newCount;
255
+ writeJsonFile(ultrapilot.path, ultrapilot.state);
256
+
257
+ console.log(JSON.stringify({
258
+ continue: false,
259
+ reason: `[ULTRAPILOT] ${incomplete} workers still running. Continue.`
260
+ }));
261
+ return;
262
+ }
254
263
  }
264
+ }
255
265
 
256
- if (iteration < maxIter) {
257
- const newIter = iteration + 1;
258
- ralphState.iteration = newIter;
259
- writeJsonFile(join(directory, '.omc', 'state', 'ralph-state.json'), ralphState);
260
-
261
- // Build continuation message with PRD context if available
262
- let prdContext = '';
263
- if (prdStatus.hasPrd) {
264
- prdContext = `
265
- ## PRD STATUS
266
- ${prdStatus.completed}/${prdStatus.total} stories complete. Remaining: ${prdStatus.incompleteIds.join(', ')}
267
- `;
268
- if (prdStatus.nextStory) {
269
- prdContext += `
270
- ## CURRENT STORY: ${prdStatus.nextStory.id} - ${prdStatus.nextStory.title}
271
-
272
- ${prdStatus.nextStory.description || ''}
273
-
274
- **Acceptance Criteria:**
275
- ${(prdStatus.nextStory.acceptanceCriteria || []).map((c, i) => `${i + 1}. ${c}`).join('\n')}
276
-
277
- **Instructions:**
278
- 1. Implement this story completely
279
- 2. Verify ALL acceptance criteria are met
280
- 3. Run quality checks (tests, typecheck, lint)
281
- 4. Update prd.json to set passes: true for ${prdStatus.nextStory.id}
282
- 5. Append progress to progress.txt
283
- 6. Move to next story or output completion promise
284
- `;
285
- }
266
+ // Priority 4: Swarm (coordinated agents with SQLite)
267
+ if (swarmMarker && swarmSummary?.active) {
268
+ const pending = (swarmSummary.tasks_pending || 0) + (swarmSummary.tasks_claimed || 0);
269
+ if (pending > 0) {
270
+ const newCount = (swarmSummary.reinforcement_count || 0) + 1;
271
+ if (newCount <= 15) {
272
+ swarmSummary.reinforcement_count = newCount;
273
+ writeJsonFile(join(stateDir, 'swarm-summary.json'), swarmSummary);
274
+
275
+ console.log(JSON.stringify({
276
+ continue: false,
277
+ reason: `[SWARM ACTIVE] ${pending} tasks remain. Continue working.`
278
+ }));
279
+ return;
286
280
  }
281
+ }
282
+ }
287
283
 
288
- // Add patterns from progress.txt
289
- let patternsContext = '';
290
- if (progressPatterns.length > 0) {
291
- patternsContext = `
292
- ## CODEBASE PATTERNS (from previous iterations)
293
- ${progressPatterns.map(p => `- ${p}`).join('\n')}
294
- `;
284
+ // Priority 5: Pipeline (sequential stages)
285
+ if (pipeline.state?.active) {
286
+ const currentStage = pipeline.state.current_stage || 0;
287
+ const totalStages = pipeline.state.stages?.length || 0;
288
+ if (currentStage < totalStages) {
289
+ const newCount = (pipeline.state.reinforcement_count || 0) + 1;
290
+ if (newCount <= 15) {
291
+ pipeline.state.reinforcement_count = newCount;
292
+ writeJsonFile(pipeline.path, pipeline.state);
293
+
294
+ console.log(JSON.stringify({
295
+ continue: false,
296
+ reason: `[PIPELINE - Stage ${currentStage + 1}/${totalStages}] Pipeline not complete. Continue.`
297
+ }));
298
+ return;
295
299
  }
300
+ }
301
+ }
302
+
303
+ // Priority 6: UltraQA (QA cycling)
304
+ if (ultraqa.state?.active) {
305
+ const cycle = ultraqa.state.cycle || 1;
306
+ const maxCycles = ultraqa.state.max_cycles || 10;
307
+ if (cycle < maxCycles && !ultraqa.state.all_passing) {
308
+ ultraqa.state.cycle = cycle + 1;
309
+ writeJsonFile(ultraqa.path, ultraqa.state);
296
310
 
297
311
  console.log(JSON.stringify({
298
312
  continue: false,
299
- reason: `<ralph-loop-continuation>
300
-
301
- [RALPH LOOP - ITERATION ${newIter}/${maxIter}]
302
-
303
- Your previous attempt did not output the completion promise. The work is NOT done yet.
304
- ${prdContext}${patternsContext}
305
- CRITICAL INSTRUCTIONS:
306
- 1. Review your progress and the original task
307
- 2. ${prdStatus.hasPrd ? 'Check prd.json - are ALL stories marked passes: true?' : 'Check your todo list - are ALL items marked complete?'}
308
- 3. Continue from where you left off
309
- 4. When FULLY complete, output: <promise>${ralphState.completion_promise || 'DONE'}</promise>
310
- 5. Do NOT stop until the task is truly done
311
-
312
- ${ralphState.prompt ? `Original task: ${ralphState.prompt}` : ''}
313
-
314
- </ralph-loop-continuation>
315
-
316
- ---
317
- `
313
+ reason: `[ULTRAQA - Cycle ${cycle + 1}/${maxCycles}] Tests not all passing. Continue fixing.`
318
314
  }));
319
315
  return;
320
316
  }
321
317
  }
322
318
 
323
- // Priority 2: Ultrawork with incomplete todos
324
- if (ultraworkState?.active && incompleteCount > 0) {
325
- const newCount = (ultraworkState.reinforcement_count || 0) + 1;
326
- const maxReinforcements = ultraworkState.max_reinforcements || 10;
319
+ // Priority 7: Ultrawork with incomplete todos/tasks
320
+ if (ultrawork.state?.active && totalIncomplete > 0) {
321
+ const newCount = (ultrawork.state.reinforcement_count || 0) + 1;
322
+ const maxReinforcements = ultrawork.state.max_reinforcements || 15;
327
323
 
328
- // Escape mechanism: after max reinforcements, allow stopping
329
324
  if (newCount > maxReinforcements) {
330
- // Prune old notepad entries on clean session stop
331
- try {
332
- if (pruneOldEntries) {
333
- const pruneResult = pruneOldEntries(directory, 7);
334
- if (pruneResult.pruned > 0) {
335
- // Optionally log: console.error(`Pruned ${pruneResult.pruned} old notepad entries`);
336
- }
337
- }
338
- } catch (err) {
339
- // Silently ignore prune errors
340
- }
341
-
342
325
  console.log(JSON.stringify({
343
326
  continue: true,
344
- reason: `[ULTRAWORK ESCAPE] Maximum reinforcements (${maxReinforcements}) reached. Allowing stop despite ${incompleteCount} incomplete todos. If tasks are genuinely stuck, consider cancelling them or asking the user for help.`
327
+ reason: `[ULTRAWORK ESCAPE] Max reinforcements reached. Allowing stop.`
345
328
  }));
346
329
  return;
347
330
  }
348
331
 
349
- ultraworkState.reinforcement_count = newCount;
350
- ultraworkState.last_checked_at = new Date().toISOString();
351
-
352
- writeJsonFile(join(directory, '.omc', 'state', 'ultrawork-state.json'), ultraworkState);
332
+ ultrawork.state.reinforcement_count = newCount;
333
+ ultrawork.state.last_checked_at = new Date().toISOString();
334
+ writeJsonFile(ultrawork.path, ultrawork.state);
353
335
 
336
+ const itemType = taskCount > 0 ? 'Tasks' : 'todos';
354
337
  console.log(JSON.stringify({
355
338
  continue: false,
356
- reason: `<ultrawork-persistence>
357
-
358
- [ULTRAWORK MODE STILL ACTIVE - Reinforcement #${newCount}]
359
-
360
- Your ultrawork session is NOT complete. ${incompleteCount} incomplete todos remain.
361
-
362
- REMEMBER THE ULTRAWORK RULES:
363
- - **PARALLEL**: Fire independent calls simultaneously - NEVER wait sequentially
364
- - **BACKGROUND FIRST**: Use Task(run_in_background=true) for exploration (10+ concurrent)
365
- - **TODO**: Track EVERY step. Mark complete IMMEDIATELY after each
366
- - **VERIFY**: Check ALL requirements met before done
367
- - **NO Premature Stopping**: ALL TODOs must be complete
339
+ reason: `[ULTRAWORK #${newCount}] ${totalIncomplete} incomplete ${itemType}. Continue working.\n${ultrawork.state.original_prompt ? `Task: ${ultrawork.state.original_prompt}` : ''}`
340
+ }));
341
+ return;
342
+ }
368
343
 
369
- Continue working on the next pending task. DO NOT STOP until all tasks are marked complete.
344
+ // Priority 8: Ecomode with incomplete todos/tasks
345
+ if (ecomode.state?.active && totalIncomplete > 0) {
346
+ const newCount = (ecomode.state.reinforcement_count || 0) + 1;
347
+ const maxReinforcements = ecomode.state.max_reinforcements || 15;
370
348
 
371
- ${ultraworkState.original_prompt ? `Original task: ${ultraworkState.original_prompt}` : ''}
349
+ if (newCount > maxReinforcements) {
350
+ console.log(JSON.stringify({
351
+ continue: true,
352
+ reason: `[ECOMODE ESCAPE] Max reinforcements reached. Allowing stop.`
353
+ }));
354
+ return;
355
+ }
372
356
 
373
- </ultrawork-persistence>
357
+ ecomode.state.reinforcement_count = newCount;
358
+ writeJsonFile(ecomode.path, ecomode.state);
374
359
 
375
- ---
376
- `
360
+ const itemType = taskCount > 0 ? 'Tasks' : 'todos';
361
+ console.log(JSON.stringify({
362
+ continue: false,
363
+ reason: `[ECOMODE #${newCount}] ${totalIncomplete} incomplete ${itemType}. Continue working.`
377
364
  }));
378
365
  return;
379
366
  }
380
367
 
381
- // Priority 3: Todo Continuation (with escape mechanism)
382
- if (incompleteCount > 0) {
383
- // Track continuation attempts in a lightweight way
384
- const contFile = join(directory, '.omc', 'continuation-count.json');
368
+ // Priority 9: Generic Task/Todo continuation (no specific mode)
369
+ if (totalIncomplete > 0) {
370
+ const contFile = join(stateDir, 'continuation-count.json');
385
371
  let contState = readJsonFile(contFile) || { count: 0 };
386
372
  contState.count = (contState.count || 0) + 1;
387
- contState.last_checked_at = new Date().toISOString();
388
373
  writeJsonFile(contFile, contState);
389
374
 
390
- const maxContinuations = 15;
391
-
392
- // Escape mechanism: after max continuations, allow stopping
393
- if (contState.count > maxContinuations) {
394
- // Prune old notepad entries on clean session stop
395
- try {
396
- if (pruneOldEntries) {
397
- const pruneResult = pruneOldEntries(directory, 7);
398
- if (pruneResult.pruned > 0) {
399
- // Optionally log: console.error(`Pruned ${pruneResult.pruned} old notepad entries`);
400
- }
401
- }
402
- } catch (err) {
403
- // Silently ignore prune errors
404
- }
405
-
375
+ if (contState.count > 15) {
406
376
  console.log(JSON.stringify({
407
377
  continue: true,
408
- reason: `[TODO ESCAPE] Maximum continuation attempts (${maxContinuations}) reached. Allowing stop despite ${incompleteCount} incomplete todos. Tasks may be stuck - consider reviewing and clearing them.`
378
+ reason: `[CONTINUATION ESCAPE] Max continuations reached. Allowing stop.`
409
379
  }));
410
380
  return;
411
381
  }
412
382
 
383
+ const itemType = taskCount > 0 ? 'Tasks' : 'todos';
413
384
  console.log(JSON.stringify({
414
385
  continue: false,
415
- reason: `<todo-continuation>
416
-
417
- [SYSTEM REMINDER - TODO CONTINUATION ${contState.count}/${maxContinuations}]
418
-
419
- Incomplete tasks remain in your todo list (${incompleteCount} remaining). Continue working on the next pending task.
420
-
421
- - Proceed without asking for permission
422
- - Mark each task complete when finished
423
- - Do not stop until all tasks are done
424
-
425
- </todo-continuation>
426
-
427
- ---
428
- `
386
+ reason: `[CONTINUATION ${contState.count}/15] ${totalIncomplete} incomplete ${itemType}. Continue working.`
429
387
  }));
430
388
  return;
431
389
  }
432
390
 
433
- // No blocking needed - clean session stop
434
- // Prune old notepad entries on clean session stop
435
- try {
436
- const pruneResult = pruneOldEntries(directory, 7);
437
- if (pruneResult.pruned > 0) {
438
- // Optionally log: console.error(`Pruned ${pruneResult.pruned} old notepad entries`);
439
- }
440
- } catch (err) {
441
- // Silently ignore prune errors
442
- }
443
-
391
+ // No blocking needed
444
392
  console.log(JSON.stringify({ continue: true }));
445
393
  } catch (error) {
394
+ // On any error, allow stop rather than blocking forever
395
+ console.error(`[persistent-mode] Error: ${error.message}`);
446
396
  console.log(JSON.stringify({ continue: true }));
447
397
  }
448
398
  }