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.
- package/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/dist/hooks/keyword-detector/index.js +1 -1
- package/docs/CLAUDE.md +5 -5
- package/package.json +1 -1
- package/scripts/keyword-detector.mjs +4 -2
- package/scripts/persistent-mode.mjs +264 -314
- package/templates/hooks/persistent-mode.mjs +276 -177
|
@@ -1,30 +1,17 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
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
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
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 {
|
|
59
|
+
return { state: null, path: localPath }; // Default to local for new writes
|
|
85
60
|
}
|
|
86
61
|
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
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
|
-
|
|
95
|
-
|
|
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
|
-
|
|
120
|
-
|
|
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
|
-
|
|
137
|
-
|
|
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
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
-
//
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
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
|
-
//
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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
|
-
|
|
191
|
-
|
|
192
|
-
|
|
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:
|
|
206
|
-
reason: `[RALPH LOOP
|
|
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
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
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
|
-
|
|
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
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
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
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
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:
|
|
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
|
|
324
|
-
if (
|
|
325
|
-
const newCount = (
|
|
326
|
-
const maxReinforcements =
|
|
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]
|
|
327
|
+
reason: `[ULTRAWORK ESCAPE] Max reinforcements reached. Allowing stop.`
|
|
345
328
|
}));
|
|
346
329
|
return;
|
|
347
330
|
}
|
|
348
331
|
|
|
349
|
-
|
|
350
|
-
|
|
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:
|
|
357
|
-
|
|
358
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
382
|
-
if (
|
|
383
|
-
|
|
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
|
-
|
|
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: `[
|
|
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:
|
|
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
|
|
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
|
}
|