oh-my-claude-sisyphus 3.8.6 → 3.8.7

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,6 +1,6 @@
1
1
  {
2
2
  "name": "oh-my-claudecode",
3
- "version": "3.8.6",
3
+ "version": "3.8.7",
4
4
  "description": "Multi-agent orchestration system for Claude Code",
5
5
  "author": {
6
6
  "name": "oh-my-claudecode contributors"
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oh-my-claude-sisyphus",
3
- "version": "3.8.6",
3
+ "version": "3.8.7",
4
4
  "description": "Multi-agent orchestration system for Claude Code - Inspired by oh-my-opencode",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -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,104 +43,72 @@ 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
- ];
63
-
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
- }
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);
83
52
 
84
- return { hasPrd: false, allComplete: false, nextStory: null };
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
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;
@@ -163,286 +121,213 @@ async function main() {
163
121
  try { data = JSON.parse(input); } catch {}
164
122
 
165
123
  const directory = data.directory || process.cwd();
124
+ const sessionId = data.sessionId || data.session_id || '';
166
125
  const todosDir = join(homedir(), '.claude', 'todos');
126
+ const stateDir = join(directory, '.omc', 'state');
127
+ const globalStateDir = join(homedir(), '.omc', 'state');
128
+
129
+ // Read all mode states (local-first with fallback to global)
130
+ const ralph = readStateFile(stateDir, globalStateDir, 'ralph-state.json');
131
+ const autopilot = readStateFile(stateDir, globalStateDir, 'autopilot-state.json');
132
+ const ultrapilot = readStateFile(stateDir, globalStateDir, 'ultrapilot-state.json');
133
+ const ultrawork = readStateFile(stateDir, globalStateDir, 'ultrawork-state.json');
134
+ const ecomode = readStateFile(stateDir, globalStateDir, 'ecomode-state.json');
135
+ const ultraqa = readStateFile(stateDir, globalStateDir, 'ultraqa-state.json');
136
+ const pipeline = readStateFile(stateDir, globalStateDir, 'pipeline-state.json');
137
+
138
+ // Swarm uses swarm-summary.json (not swarm-state.json) + marker file
139
+ const swarmMarker = existsSync(join(stateDir, 'swarm-active.marker'));
140
+ const swarmSummary = readJsonFile(join(stateDir, 'swarm-summary.json'));
141
+
142
+ // Count incomplete items
143
+ const taskCount = countIncompleteTasks(sessionId);
144
+ const todoCount = countIncompleteTodos(todosDir, directory);
145
+ const totalIncomplete = taskCount + todoCount;
146
+
147
+ // Priority 1: Ralph Loop (explicit persistence mode)
148
+ if (ralph.state?.active) {
149
+ const iteration = ralph.state.iteration || 1;
150
+ const maxIter = ralph.state.max_iterations || 100;
167
151
 
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);
184
-
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
189
-
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
- }
152
+ if (iteration < maxIter) {
153
+ ralph.state.iteration = iteration + 1;
154
+ writeJsonFile(ralph.path, ralph.state);
203
155
 
204
156
  console.log(JSON.stringify({
205
- continue: true,
206
- reason: `[RALPH LOOP COMPLETE - PRD] All ${prdStatus.total} stories are complete! Great work!`
157
+ continue: false,
158
+ 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
159
  }));
208
160
  return;
209
161
  }
162
+ }
210
163
 
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>
164
+ // Priority 2: Autopilot (high-level orchestration)
165
+ if (autopilot.state?.active) {
166
+ const phase = autopilot.state.phase || 'unknown';
167
+ if (phase !== 'complete') {
168
+ const newCount = (autopilot.state.reinforcement_count || 0) + 1;
169
+ if (newCount <= 20) {
170
+ autopilot.state.reinforcement_count = newCount;
171
+ writeJsonFile(autopilot.path, autopilot.state);
172
+
173
+ console.log(JSON.stringify({
174
+ continue: false,
175
+ reason: `[AUTOPILOT - Phase: ${phase}] Autopilot not complete. Continue working.`
176
+ }));
177
+ return;
178
+ }
179
+ }
180
+ }
249
181
 
250
- ---
251
- `
252
- }));
253
- return;
182
+ // Priority 3: Ultrapilot (parallel autopilot)
183
+ if (ultrapilot.state?.active) {
184
+ const workers = ultrapilot.state.workers || [];
185
+ const incomplete = workers.filter(w => w.status !== 'complete' && w.status !== 'failed').length;
186
+ if (incomplete > 0) {
187
+ const newCount = (ultrapilot.state.reinforcement_count || 0) + 1;
188
+ if (newCount <= 20) {
189
+ ultrapilot.state.reinforcement_count = newCount;
190
+ writeJsonFile(ultrapilot.path, ultrapilot.state);
191
+
192
+ console.log(JSON.stringify({
193
+ continue: false,
194
+ reason: `[ULTRAPILOT] ${incomplete} workers still running. Continue.`
195
+ }));
196
+ return;
197
+ }
254
198
  }
199
+ }
255
200
 
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
- }
201
+ // Priority 4: Swarm (coordinated agents with SQLite)
202
+ if (swarmMarker && swarmSummary?.active) {
203
+ const pending = (swarmSummary.tasks_pending || 0) + (swarmSummary.tasks_claimed || 0);
204
+ if (pending > 0) {
205
+ const newCount = (swarmSummary.reinforcement_count || 0) + 1;
206
+ if (newCount <= 15) {
207
+ swarmSummary.reinforcement_count = newCount;
208
+ writeJsonFile(join(stateDir, 'swarm-summary.json'), swarmSummary);
209
+
210
+ console.log(JSON.stringify({
211
+ continue: false,
212
+ reason: `[SWARM ACTIVE] ${pending} tasks remain. Continue working.`
213
+ }));
214
+ return;
286
215
  }
216
+ }
217
+ }
287
218
 
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
- `;
219
+ // Priority 5: Pipeline (sequential stages)
220
+ if (pipeline.state?.active) {
221
+ const currentStage = pipeline.state.current_stage || 0;
222
+ const totalStages = pipeline.state.stages?.length || 0;
223
+ if (currentStage < totalStages) {
224
+ const newCount = (pipeline.state.reinforcement_count || 0) + 1;
225
+ if (newCount <= 15) {
226
+ pipeline.state.reinforcement_count = newCount;
227
+ writeJsonFile(pipeline.path, pipeline.state);
228
+
229
+ console.log(JSON.stringify({
230
+ continue: false,
231
+ reason: `[PIPELINE - Stage ${currentStage + 1}/${totalStages}] Pipeline not complete. Continue.`
232
+ }));
233
+ return;
295
234
  }
235
+ }
236
+ }
237
+
238
+ // Priority 6: UltraQA (QA cycling)
239
+ if (ultraqa.state?.active) {
240
+ const cycle = ultraqa.state.cycle || 1;
241
+ const maxCycles = ultraqa.state.max_cycles || 10;
242
+ if (cycle < maxCycles && !ultraqa.state.all_passing) {
243
+ ultraqa.state.cycle = cycle + 1;
244
+ writeJsonFile(ultraqa.path, ultraqa.state);
296
245
 
297
246
  console.log(JSON.stringify({
298
247
  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
- `
248
+ reason: `[ULTRAQA - Cycle ${cycle + 1}/${maxCycles}] Tests not all passing. Continue fixing.`
318
249
  }));
319
250
  return;
320
251
  }
321
252
  }
322
253
 
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;
254
+ // Priority 7: Ultrawork with incomplete todos/tasks
255
+ if (ultrawork.state?.active && totalIncomplete > 0) {
256
+ const newCount = (ultrawork.state.reinforcement_count || 0) + 1;
257
+ const maxReinforcements = ultrawork.state.max_reinforcements || 15;
327
258
 
328
- // Escape mechanism: after max reinforcements, allow stopping
329
259
  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
260
  console.log(JSON.stringify({
343
261
  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.`
262
+ reason: `[ULTRAWORK ESCAPE] Max reinforcements reached. Allowing stop.`
345
263
  }));
346
264
  return;
347
265
  }
348
266
 
349
- ultraworkState.reinforcement_count = newCount;
350
- ultraworkState.last_checked_at = new Date().toISOString();
351
-
352
- writeJsonFile(join(directory, '.omc', 'state', 'ultrawork-state.json'), ultraworkState);
267
+ ultrawork.state.reinforcement_count = newCount;
268
+ ultrawork.state.last_checked_at = new Date().toISOString();
269
+ writeJsonFile(ultrawork.path, ultrawork.state);
353
270
 
271
+ const itemType = taskCount > 0 ? 'Tasks' : 'todos';
354
272
  console.log(JSON.stringify({
355
273
  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
274
+ reason: `[ULTRAWORK #${newCount}] ${totalIncomplete} incomplete ${itemType}. Continue working.\n${ultrawork.state.original_prompt ? `Task: ${ultrawork.state.original_prompt}` : ''}`
275
+ }));
276
+ return;
277
+ }
368
278
 
369
- Continue working on the next pending task. DO NOT STOP until all tasks are marked complete.
279
+ // Priority 8: Ecomode with incomplete todos/tasks
280
+ if (ecomode.state?.active && totalIncomplete > 0) {
281
+ const newCount = (ecomode.state.reinforcement_count || 0) + 1;
282
+ const maxReinforcements = ecomode.state.max_reinforcements || 15;
370
283
 
371
- ${ultraworkState.original_prompt ? `Original task: ${ultraworkState.original_prompt}` : ''}
284
+ if (newCount > maxReinforcements) {
285
+ console.log(JSON.stringify({
286
+ continue: true,
287
+ reason: `[ECOMODE ESCAPE] Max reinforcements reached. Allowing stop.`
288
+ }));
289
+ return;
290
+ }
372
291
 
373
- </ultrawork-persistence>
292
+ ecomode.state.reinforcement_count = newCount;
293
+ writeJsonFile(ecomode.path, ecomode.state);
374
294
 
375
- ---
376
- `
295
+ const itemType = taskCount > 0 ? 'Tasks' : 'todos';
296
+ console.log(JSON.stringify({
297
+ continue: false,
298
+ reason: `[ECOMODE #${newCount}] ${totalIncomplete} incomplete ${itemType}. Continue working.`
377
299
  }));
378
300
  return;
379
301
  }
380
302
 
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');
303
+ // Priority 9: Generic Task/Todo continuation (no specific mode)
304
+ if (totalIncomplete > 0) {
305
+ const contFile = join(stateDir, 'continuation-count.json');
385
306
  let contState = readJsonFile(contFile) || { count: 0 };
386
307
  contState.count = (contState.count || 0) + 1;
387
- contState.last_checked_at = new Date().toISOString();
388
308
  writeJsonFile(contFile, contState);
389
309
 
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
-
310
+ if (contState.count > 15) {
406
311
  console.log(JSON.stringify({
407
312
  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.`
313
+ reason: `[CONTINUATION ESCAPE] Max continuations reached. Allowing stop.`
409
314
  }));
410
315
  return;
411
316
  }
412
317
 
318
+ const itemType = taskCount > 0 ? 'Tasks' : 'todos';
413
319
  console.log(JSON.stringify({
414
320
  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
- `
321
+ reason: `[CONTINUATION ${contState.count}/15] ${totalIncomplete} incomplete ${itemType}. Continue working.`
429
322
  }));
430
323
  return;
431
324
  }
432
325
 
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
-
326
+ // No blocking needed
444
327
  console.log(JSON.stringify({ continue: true }));
445
328
  } catch (error) {
329
+ // On any error, allow stop rather than blocking forever
330
+ console.error(`[persistent-mode] Error: ${error.message}`);
446
331
  console.log(JSON.stringify({ continue: true }));
447
332
  }
448
333
  }
@@ -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,49 +76,39 @@ 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;
@@ -113,178 +120,205 @@ async function main() {
113
120
  let data = {};
114
121
  try { data = JSON.parse(input); } catch {}
115
122
 
116
- const stopReason = data.stop_reason || data.stopReason || '';
117
- const userRequested = data.user_requested || data.userRequested || false;
118
- const sessionId = data.sessionId || data.session_id || '';
119
-
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)) {
123
- console.log(JSON.stringify({ continue: true }));
124
- return;
125
- }
126
-
127
123
  const directory = data.directory || process.cwd();
124
+ const sessionId = data.sessionId || data.session_id || '';
128
125
  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'));
136
-
137
- // Check for verification state (oracle verification)
138
- const verificationState = readJsonFile(join(directory, '.omc', 'ralph-verification.json'));
139
-
140
- // Count incomplete todos
141
- const incompleteCount = countIncompleteTodos(todosDir, directory);
142
-
143
- // Count incomplete Tasks
126
+ const stateDir = join(directory, '.omc', 'state');
127
+ const globalStateDir = join(homedir(), '.omc', 'state');
128
+
129
+ // Read all mode states (local-first with fallback to global)
130
+ const ralph = readStateFile(stateDir, globalStateDir, 'ralph-state.json');
131
+ const autopilot = readStateFile(stateDir, globalStateDir, 'autopilot-state.json');
132
+ const ultrapilot = readStateFile(stateDir, globalStateDir, 'ultrapilot-state.json');
133
+ const ultrawork = readStateFile(stateDir, globalStateDir, 'ultrawork-state.json');
134
+ const ecomode = readStateFile(stateDir, globalStateDir, 'ecomode-state.json');
135
+ const ultraqa = readStateFile(stateDir, globalStateDir, 'ultraqa-state.json');
136
+ const pipeline = readStateFile(stateDir, globalStateDir, 'pipeline-state.json');
137
+
138
+ // Swarm uses swarm-summary.json (not swarm-state.json) + marker file
139
+ const swarmMarker = existsSync(join(stateDir, 'swarm-active.marker'));
140
+ const swarmSummary = readJsonFile(join(stateDir, 'swarm-summary.json'));
141
+
142
+ // Count incomplete items
144
143
  const taskCount = countIncompleteTasks(sessionId);
145
- const totalIncomplete = taskCount + incompleteCount;
144
+ const todoCount = countIncompleteTodos(todosDir, directory);
145
+ const totalIncomplete = taskCount + todoCount;
146
146
 
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;
147
+ // Priority 1: Ralph Loop (explicit persistence mode)
148
+ if (ralph.state?.active) {
149
+ const iteration = ralph.state.iteration || 1;
150
+ const maxIter = ralph.state.max_iterations || 100;
151
151
 
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;
152
+ if (iteration < maxIter) {
153
+ ralph.state.iteration = iteration + 1;
154
+ writeJsonFile(ralph.path, ralph.state);
156
155
 
157
156
  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
157
+ continue: false,
158
+ 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}` : ''}`
159
+ }));
160
+ return;
161
+ }
162
+ }
176
163
 
177
- 1. **Spawn Oracle Agent** for verification:
178
- \`\`\`
179
- Task(subagent_type="oracle", prompt="Verify this task completion claim...")
180
- \`\`\`
164
+ // Priority 2: Autopilot (high-level orchestration)
165
+ if (autopilot.state?.active) {
166
+ const phase = autopilot.state.phase || 'unknown';
167
+ if (phase !== 'complete') {
168
+ const newCount = (autopilot.state.reinforcement_count || 0) + 1;
169
+ if (newCount <= 20) {
170
+ autopilot.state.reinforcement_count = newCount;
171
+ writeJsonFile(autopilot.path, autopilot.state);
172
+
173
+ console.log(JSON.stringify({
174
+ continue: false,
175
+ reason: `[AUTOPILOT - Phase: ${phase}] Autopilot not complete. Continue working.`
176
+ }));
177
+ return;
178
+ }
179
+ }
180
+ }
181
181
 
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)?
182
+ // Priority 3: Ultrapilot (parallel autopilot)
183
+ if (ultrapilot.state?.active) {
184
+ const workers = ultrapilot.state.workers || [];
185
+ const incomplete = workers.filter(w => w.status !== 'complete' && w.status !== 'failed').length;
186
+ if (incomplete > 0) {
187
+ const newCount = (ultrapilot.state.reinforcement_count || 0) + 1;
188
+ if (newCount <= 20) {
189
+ ultrapilot.state.reinforcement_count = newCount;
190
+ writeJsonFile(ultrapilot.path, ultrapilot.state);
191
+
192
+ console.log(JSON.stringify({
193
+ continue: false,
194
+ reason: `[ULTRAPILOT] ${incomplete} workers still running. Continue.`
195
+ }));
196
+ return;
197
+ }
198
+ }
199
+ }
188
200
 
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
201
+ // Priority 4: Swarm (coordinated agents with SQLite)
202
+ if (swarmMarker && swarmSummary?.active) {
203
+ const pending = (swarmSummary.tasks_pending || 0) + (swarmSummary.tasks_claimed || 0);
204
+ if (pending > 0) {
205
+ const newCount = (swarmSummary.reinforcement_count || 0) + 1;
206
+ if (newCount <= 15) {
207
+ swarmSummary.reinforcement_count = newCount;
208
+ writeJsonFile(join(stateDir, 'swarm-summary.json'), swarmSummary);
209
+
210
+ console.log(JSON.stringify({
211
+ continue: false,
212
+ reason: `[SWARM ACTIVE] ${pending} tasks remain. Continue working.`
213
+ }));
214
+ return;
215
+ }
216
+ }
217
+ }
192
218
 
193
- DO NOT output the completion promise again until Oracle approves.
219
+ // Priority 5: Pipeline (sequential stages)
220
+ if (pipeline.state?.active) {
221
+ const currentStage = pipeline.state.current_stage || 0;
222
+ const totalStages = pipeline.state.stages?.length || 0;
223
+ if (currentStage < totalStages) {
224
+ const newCount = (pipeline.state.reinforcement_count || 0) + 1;
225
+ if (newCount <= 15) {
226
+ pipeline.state.reinforcement_count = newCount;
227
+ writeJsonFile(pipeline.path, pipeline.state);
228
+
229
+ console.log(JSON.stringify({
230
+ continue: false,
231
+ reason: `[PIPELINE - Stage ${currentStage + 1}/${totalStages}] Pipeline not complete. Continue.`
232
+ }));
233
+ return;
234
+ }
235
+ }
236
+ }
194
237
 
195
- </ralph-verification>
238
+ // Priority 6: UltraQA (QA cycling)
239
+ if (ultraqa.state?.active) {
240
+ const cycle = ultraqa.state.cycle || 1;
241
+ const maxCycles = ultraqa.state.max_cycles || 10;
242
+ if (cycle < maxCycles && !ultraqa.state.all_passing) {
243
+ ultraqa.state.cycle = cycle + 1;
244
+ writeJsonFile(ultraqa.path, ultraqa.state);
196
245
 
197
- ---
198
- `
246
+ console.log(JSON.stringify({
247
+ continue: false,
248
+ reason: `[ULTRAQA - Cycle ${cycle + 1}/${maxCycles}] Tests not all passing. Continue fixing.`
199
249
  }));
200
250
  return;
201
251
  }
252
+ }
202
253
 
203
- if (iteration < maxIter) {
204
- const newIter = iteration + 1;
205
- ralphState.iteration = newIter;
206
- writeJsonFile(join(directory, '.omc', 'ralph-state.json'), ralphState);
254
+ // Priority 7: Ultrawork with incomplete todos/tasks
255
+ if (ultrawork.state?.active && totalIncomplete > 0) {
256
+ const newCount = (ultrawork.state.reinforcement_count || 0) + 1;
257
+ const maxReinforcements = ultrawork.state.max_reinforcements || 15;
207
258
 
259
+ if (newCount > maxReinforcements) {
208
260
  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
- `
261
+ continue: true,
262
+ reason: `[ULTRAWORK ESCAPE] Max reinforcements reached. Allowing stop.`
229
263
  }));
230
264
  return;
231
265
  }
232
- }
233
266
 
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
-
240
- writeJsonFile(join(directory, '.omc', 'ultrawork-state.json'), ultraworkState);
267
+ ultrawork.state.reinforcement_count = newCount;
268
+ ultrawork.state.last_checked_at = new Date().toISOString();
269
+ writeJsonFile(ultrawork.path, ultrawork.state);
241
270
 
271
+ const itemType = taskCount > 0 ? 'Tasks' : 'todos';
242
272
  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
273
+ continue: false,
274
+ reason: `[ULTRAWORK #${newCount}] ${totalIncomplete} incomplete ${itemType}. Continue working.\n${ultrawork.state.original_prompt ? `Task: ${ultrawork.state.original_prompt}` : ''}`
275
+ }));
276
+ return;
277
+ }
256
278
 
257
- Continue working on the next pending task. DO NOT STOP until all tasks are marked complete.
279
+ // Priority 8: Ecomode with incomplete todos/tasks
280
+ if (ecomode.state?.active && totalIncomplete > 0) {
281
+ const newCount = (ecomode.state.reinforcement_count || 0) + 1;
282
+ const maxReinforcements = ecomode.state.max_reinforcements || 15;
258
283
 
259
- ${ultraworkState.original_prompt ? `Original task: ${ultraworkState.original_prompt}` : ''}
284
+ if (newCount > maxReinforcements) {
285
+ console.log(JSON.stringify({
286
+ continue: true,
287
+ reason: `[ECOMODE ESCAPE] Max reinforcements reached. Allowing stop.`
288
+ }));
289
+ return;
290
+ }
260
291
 
261
- </ultrawork-persistence>
292
+ ecomode.state.reinforcement_count = newCount;
293
+ writeJsonFile(ecomode.path, ecomode.state);
262
294
 
263
- ---
264
- `
295
+ const itemType = taskCount > 0 ? 'Tasks' : 'todos';
296
+ console.log(JSON.stringify({
297
+ continue: false,
298
+ reason: `[ECOMODE #${newCount}] ${totalIncomplete} incomplete ${itemType}. Continue working.`
265
299
  }));
266
300
  return;
267
301
  }
268
302
 
269
- // Priority 3: Todo Continuation
303
+ // Priority 9: Generic Task/Todo continuation (no specific mode)
270
304
  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.
279
-
280
- - Proceed without asking for permission
281
- - Mark each item complete when finished
282
- - Do not stop until all items are done
305
+ const contFile = join(stateDir, 'continuation-count.json');
306
+ let contState = readJsonFile(contFile) || { count: 0 };
307
+ contState.count = (contState.count || 0) + 1;
308
+ writeJsonFile(contFile, contState);
283
309
 
284
- </todo-continuation>
310
+ if (contState.count > 15) {
311
+ console.log(JSON.stringify({
312
+ continue: true,
313
+ reason: `[CONTINUATION ESCAPE] Max continuations reached. Allowing stop.`
314
+ }));
315
+ return;
316
+ }
285
317
 
286
- ---
287
- `
318
+ const itemType = taskCount > 0 ? 'Tasks' : 'todos';
319
+ console.log(JSON.stringify({
320
+ continue: false,
321
+ reason: `[CONTINUATION ${contState.count}/15] ${totalIncomplete} incomplete ${itemType}. Continue working.`
288
322
  }));
289
323
  return;
290
324
  }
@@ -292,6 +326,8 @@ Incomplete ${itemType} remain (${totalIncomplete} remaining). Continue working o
292
326
  // No blocking needed
293
327
  console.log(JSON.stringify({ continue: true }));
294
328
  } catch (error) {
329
+ // On any error, allow stop rather than blocking forever
330
+ console.error(`[persistent-mode] Error: ${error.message}`);
295
331
  console.log(JSON.stringify({ continue: true }));
296
332
  }
297
333
  }