oh-my-claude-sisyphus 3.8.11 → 3.8.13
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/__tests__/task-continuation.test.js +5 -2
- package/dist/__tests__/task-continuation.test.js.map +1 -1
- package/dist/features/continuation-enforcement.js +1 -1
- package/dist/features/continuation-enforcement.js.map +1 -1
- package/dist/hooks/__tests__/bridge-pkill.test.d.ts +8 -0
- package/dist/hooks/__tests__/bridge-pkill.test.d.ts.map +1 -0
- package/dist/hooks/__tests__/bridge-pkill.test.js +188 -0
- package/dist/hooks/__tests__/bridge-pkill.test.js.map +1 -0
- package/dist/hooks/bridge.d.ts.map +1 -1
- package/dist/hooks/bridge.js +23 -29
- package/dist/hooks/bridge.js.map +1 -1
- package/dist/hooks/clear-suggestions/index.d.ts.map +1 -1
- package/dist/hooks/clear-suggestions/index.js +6 -0
- package/dist/hooks/clear-suggestions/index.js.map +1 -1
- package/dist/hooks/index.d.ts +0 -1
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/index.js +0 -3
- package/dist/hooks/index.js.map +1 -1
- package/dist/hooks/learner/constants.d.ts +2 -0
- package/dist/hooks/learner/constants.d.ts.map +1 -1
- package/dist/hooks/learner/constants.js +2 -0
- package/dist/hooks/learner/constants.js.map +1 -1
- package/dist/hooks/mode-registry/index.d.ts +7 -0
- package/dist/hooks/mode-registry/index.d.ts.map +1 -1
- package/dist/hooks/mode-registry/index.js +9 -0
- package/dist/hooks/mode-registry/index.js.map +1 -1
- package/dist/hooks/persistent-mode/index.d.ts +2 -1
- package/dist/hooks/persistent-mode/index.d.ts.map +1 -1
- package/dist/hooks/persistent-mode/index.js +7 -17
- package/dist/hooks/persistent-mode/index.js.map +1 -1
- package/dist/hooks/todo-continuation/__tests__/isUserAbort.test.d.ts +2 -0
- package/dist/hooks/todo-continuation/__tests__/isUserAbort.test.d.ts.map +1 -0
- package/dist/hooks/todo-continuation/__tests__/isUserAbort.test.js +119 -0
- package/dist/hooks/todo-continuation/__tests__/isUserAbort.test.js.map +1 -0
- package/dist/hooks/todo-continuation/index.d.ts.map +1 -1
- package/dist/hooks/todo-continuation/index.js +8 -27
- package/dist/hooks/todo-continuation/index.js.map +1 -1
- package/dist/installer/hooks.d.ts +8 -111
- package/dist/installer/hooks.d.ts.map +1 -1
- package/dist/installer/hooks.js +11 -124
- package/dist/installer/hooks.js.map +1 -1
- package/dist/installer/index.d.ts +2 -10
- package/dist/installer/index.d.ts.map +1 -1
- package/dist/installer/index.js +10 -23
- package/dist/installer/index.js.map +1 -1
- package/package.json +1 -1
- package/scripts/persistent-mode.mjs +33 -58
- package/scripts/post-tool-verifier.mjs +1 -36
- package/skills/cancel/SKILL.md +1 -1
- package/templates/hooks/persistent-mode.mjs +33 -58
- package/templates/hooks/stop-continuation.mjs +6 -158
- package/hooks/keyword-detector.sh +0 -102
- package/hooks/persistent-mode.sh +0 -172
- package/hooks/session-start.sh +0 -62
- package/hooks/stop-continuation.sh +0 -40
- package/scripts/claude-sisyphus.sh +0 -9
- package/scripts/install.sh +0 -1673
- package/scripts/keyword-detector.sh +0 -71
- package/scripts/persistent-mode.sh +0 -311
- package/scripts/post-tool-verifier.sh +0 -196
- package/scripts/pre-tool-enforcer.sh +0 -76
- package/scripts/sisyphus-aliases.sh +0 -18
- package/scripts/stop-continuation.sh +0 -31
package/package.json
CHANGED
|
@@ -83,23 +83,20 @@ function countIncompleteTasks(sessionId) {
|
|
|
83
83
|
return count;
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
-
function countIncompleteTodos(
|
|
86
|
+
function countIncompleteTodos(sessionId, projectDir) {
|
|
87
87
|
let count = 0;
|
|
88
88
|
|
|
89
|
-
|
|
89
|
+
// Session-specific todos only (no global scan)
|
|
90
|
+
if (sessionId && typeof sessionId === 'string' && /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,255}$/.test(sessionId)) {
|
|
91
|
+
const sessionTodoPath = join(homedir(), '.claude', 'todos', `${sessionId}.json`);
|
|
90
92
|
try {
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
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 : []);
|
|
97
|
-
count += todos.filter(t => t.status !== 'completed' && t.status !== 'cancelled').length;
|
|
98
|
-
} catch { /* skip */ }
|
|
99
|
-
}
|
|
93
|
+
const data = readJsonFile(sessionTodoPath);
|
|
94
|
+
const todos = Array.isArray(data) ? data : (Array.isArray(data?.todos) ? data.todos : []);
|
|
95
|
+
count += todos.filter(t => t.status !== 'completed' && t.status !== 'cancelled').length;
|
|
100
96
|
} catch { /* skip */ }
|
|
101
97
|
}
|
|
102
98
|
|
|
99
|
+
// Project-local todos only
|
|
103
100
|
for (const path of [
|
|
104
101
|
join(projectDir, '.omc', 'todos.json'),
|
|
105
102
|
join(projectDir, '.claude', 'todos.json')
|
|
@@ -158,11 +155,13 @@ function isUserAbort(data) {
|
|
|
158
155
|
if (data.user_requested || data.userRequested) return true;
|
|
159
156
|
|
|
160
157
|
const reason = (data.stop_reason || data.stopReason || '').toLowerCase();
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
];
|
|
165
|
-
|
|
158
|
+
// Exact-match patterns: short generic words that cause false positives with .includes()
|
|
159
|
+
const exactPatterns = ['aborted', 'abort', 'cancel', 'interrupt'];
|
|
160
|
+
// Substring patterns: compound words safe for .includes() matching
|
|
161
|
+
const substringPatterns = ['user_cancel', 'user_interrupt', 'ctrl_c', 'manual_stop'];
|
|
162
|
+
|
|
163
|
+
return exactPatterns.some(p => reason === p) ||
|
|
164
|
+
substringPatterns.some(p => reason.includes(p));
|
|
166
165
|
}
|
|
167
166
|
|
|
168
167
|
async function main() {
|
|
@@ -173,7 +172,6 @@ async function main() {
|
|
|
173
172
|
|
|
174
173
|
const directory = data.directory || process.cwd();
|
|
175
174
|
const sessionId = data.sessionId || data.session_id || '';
|
|
176
|
-
const todosDir = join(homedir(), '.claude', 'todos');
|
|
177
175
|
const stateDir = join(directory, '.omc', 'state');
|
|
178
176
|
const globalStateDir = join(homedir(), '.omc', 'state');
|
|
179
177
|
|
|
@@ -204,9 +202,9 @@ async function main() {
|
|
|
204
202
|
const swarmMarker = existsSync(join(stateDir, 'swarm-active.marker'));
|
|
205
203
|
const swarmSummary = readJsonFile(join(stateDir, 'swarm-summary.json'));
|
|
206
204
|
|
|
207
|
-
// Count incomplete items
|
|
205
|
+
// Count incomplete items (session-specific + project-local only)
|
|
208
206
|
const taskCount = countIncompleteTasks(sessionId);
|
|
209
|
-
const todoCount = countIncompleteTodos(
|
|
207
|
+
const todoCount = countIncompleteTodos(sessionId, directory);
|
|
210
208
|
const totalIncomplete = taskCount + todoCount;
|
|
211
209
|
|
|
212
210
|
// Priority 1: Ralph Loop (explicit persistence mode)
|
|
@@ -219,8 +217,8 @@ async function main() {
|
|
|
219
217
|
writeJsonFile(ralph.path, ralph.state);
|
|
220
218
|
|
|
221
219
|
console.log(JSON.stringify({
|
|
222
|
-
continue:
|
|
223
|
-
|
|
220
|
+
continue: true,
|
|
221
|
+
message: `[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}` : ''}`
|
|
224
222
|
}));
|
|
225
223
|
return;
|
|
226
224
|
}
|
|
@@ -236,8 +234,8 @@ async function main() {
|
|
|
236
234
|
writeJsonFile(autopilot.path, autopilot.state);
|
|
237
235
|
|
|
238
236
|
console.log(JSON.stringify({
|
|
239
|
-
continue:
|
|
240
|
-
|
|
237
|
+
continue: true,
|
|
238
|
+
message: `[AUTOPILOT - Phase: ${phase}] Autopilot not complete. Continue working.`
|
|
241
239
|
}));
|
|
242
240
|
return;
|
|
243
241
|
}
|
|
@@ -255,8 +253,8 @@ async function main() {
|
|
|
255
253
|
writeJsonFile(ultrapilot.path, ultrapilot.state);
|
|
256
254
|
|
|
257
255
|
console.log(JSON.stringify({
|
|
258
|
-
continue:
|
|
259
|
-
|
|
256
|
+
continue: true,
|
|
257
|
+
message: `[ULTRAPILOT] ${incomplete} workers still running. Continue.`
|
|
260
258
|
}));
|
|
261
259
|
return;
|
|
262
260
|
}
|
|
@@ -273,8 +271,8 @@ async function main() {
|
|
|
273
271
|
writeJsonFile(join(stateDir, 'swarm-summary.json'), swarmSummary);
|
|
274
272
|
|
|
275
273
|
console.log(JSON.stringify({
|
|
276
|
-
continue:
|
|
277
|
-
|
|
274
|
+
continue: true,
|
|
275
|
+
message: `[SWARM ACTIVE] ${pending} tasks remain. Continue working.`
|
|
278
276
|
}));
|
|
279
277
|
return;
|
|
280
278
|
}
|
|
@@ -292,8 +290,8 @@ async function main() {
|
|
|
292
290
|
writeJsonFile(pipeline.path, pipeline.state);
|
|
293
291
|
|
|
294
292
|
console.log(JSON.stringify({
|
|
295
|
-
continue:
|
|
296
|
-
|
|
293
|
+
continue: true,
|
|
294
|
+
message: `[PIPELINE - Stage ${currentStage + 1}/${totalStages}] Pipeline not complete. Continue.`
|
|
297
295
|
}));
|
|
298
296
|
return;
|
|
299
297
|
}
|
|
@@ -309,8 +307,8 @@ async function main() {
|
|
|
309
307
|
writeJsonFile(ultraqa.path, ultraqa.state);
|
|
310
308
|
|
|
311
309
|
console.log(JSON.stringify({
|
|
312
|
-
continue:
|
|
313
|
-
|
|
310
|
+
continue: true,
|
|
311
|
+
message: `[ULTRAQA - Cycle ${cycle + 1}/${maxCycles}] Tests not all passing. Continue fixing.`
|
|
314
312
|
}));
|
|
315
313
|
return;
|
|
316
314
|
}
|
|
@@ -345,8 +343,8 @@ async function main() {
|
|
|
345
343
|
}
|
|
346
344
|
|
|
347
345
|
console.log(JSON.stringify({
|
|
348
|
-
continue:
|
|
349
|
-
reason
|
|
346
|
+
continue: true,
|
|
347
|
+
message: reason
|
|
350
348
|
}));
|
|
351
349
|
return;
|
|
352
350
|
}
|
|
@@ -374,31 +372,8 @@ async function main() {
|
|
|
374
372
|
}
|
|
375
373
|
|
|
376
374
|
console.log(JSON.stringify({
|
|
377
|
-
continue:
|
|
378
|
-
reason
|
|
379
|
-
}));
|
|
380
|
-
return;
|
|
381
|
-
}
|
|
382
|
-
|
|
383
|
-
// Priority 9: Generic Task/Todo continuation (no specific mode)
|
|
384
|
-
if (totalIncomplete > 0) {
|
|
385
|
-
const contFile = join(stateDir, 'continuation-count.json');
|
|
386
|
-
let contState = readJsonFile(contFile) || { count: 0 };
|
|
387
|
-
contState.count = (contState.count || 0) + 1;
|
|
388
|
-
writeJsonFile(contFile, contState);
|
|
389
|
-
|
|
390
|
-
if (contState.count > 15) {
|
|
391
|
-
console.log(JSON.stringify({
|
|
392
|
-
continue: true,
|
|
393
|
-
reason: `[CONTINUATION ESCAPE] Max continuations reached. Allowing stop.`
|
|
394
|
-
}));
|
|
395
|
-
return;
|
|
396
|
-
}
|
|
397
|
-
|
|
398
|
-
const itemType = taskCount > 0 ? 'Tasks' : 'todos';
|
|
399
|
-
console.log(JSON.stringify({
|
|
400
|
-
continue: false,
|
|
401
|
-
reason: `[CONTINUATION ${contState.count}/15] ${totalIncomplete} incomplete ${itemType}. Continue working.`
|
|
375
|
+
continue: true,
|
|
376
|
+
message: reason
|
|
402
377
|
}));
|
|
403
378
|
return;
|
|
404
379
|
}
|
|
@@ -15,7 +15,6 @@ import { fileURLToPath } from 'url';
|
|
|
15
15
|
const __filename = fileURLToPath(import.meta.url);
|
|
16
16
|
const __dirname = dirname(__filename);
|
|
17
17
|
const distDir = join(__dirname, '..', 'dist', 'hooks', 'notepad');
|
|
18
|
-
const clearSuggestionsDistDir = join(__dirname, '..', 'dist', 'hooks', 'clear-suggestions');
|
|
19
18
|
|
|
20
19
|
// Try to import notepad functions (may fail if not built)
|
|
21
20
|
let setPriorityContext = null;
|
|
@@ -28,15 +27,6 @@ try {
|
|
|
28
27
|
// Notepad module not available - remember tags will be silently ignored
|
|
29
28
|
}
|
|
30
29
|
|
|
31
|
-
// Try to import clear suggestions functions (may fail if not built)
|
|
32
|
-
let checkClearSuggestion = null;
|
|
33
|
-
try {
|
|
34
|
-
const clearSuggestionsModule = await import(join(clearSuggestionsDistDir, 'index.js'));
|
|
35
|
-
checkClearSuggestion = clearSuggestionsModule.checkClearSuggestion;
|
|
36
|
-
} catch {
|
|
37
|
-
// Clear suggestions module not available - will be silently skipped
|
|
38
|
-
}
|
|
39
|
-
|
|
40
30
|
// State file for session tracking
|
|
41
31
|
const STATE_FILE = join(homedir(), '.claude', '.session-stats.json');
|
|
42
32
|
|
|
@@ -272,34 +262,9 @@ async function main() {
|
|
|
272
262
|
// Generate contextual message
|
|
273
263
|
const message = generateMessage(toolName, toolOutput, sessionId, toolCount);
|
|
274
264
|
|
|
275
|
-
// Check for clear suggestions (complements /compact suggestions)
|
|
276
|
-
let clearSuggestionMessage = null;
|
|
277
|
-
if (checkClearSuggestion) {
|
|
278
|
-
try {
|
|
279
|
-
const stats = loadStats();
|
|
280
|
-
const session = stats.sessions[sessionId];
|
|
281
|
-
// Estimate context usage from total tool calls (rough heuristic)
|
|
282
|
-
const estimatedContextRatio = session ? Math.min(session.total_calls / 200, 1.0) : 0;
|
|
283
|
-
|
|
284
|
-
const clearResult = checkClearSuggestion({
|
|
285
|
-
sessionId,
|
|
286
|
-
directory,
|
|
287
|
-
toolName,
|
|
288
|
-
toolOutput,
|
|
289
|
-
contextUsageRatio: estimatedContextRatio,
|
|
290
|
-
});
|
|
291
|
-
|
|
292
|
-
if (clearResult.shouldSuggest && clearResult.message) {
|
|
293
|
-
clearSuggestionMessage = clearResult.message;
|
|
294
|
-
}
|
|
295
|
-
} catch {
|
|
296
|
-
// Clear suggestion check failed - continue without it
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
|
|
300
265
|
// Build response - use hookSpecificOutput.additionalContext for PostToolUse
|
|
301
266
|
const response = { continue: true };
|
|
302
|
-
const contextMessage =
|
|
267
|
+
const contextMessage = message;
|
|
303
268
|
if (contextMessage) {
|
|
304
269
|
response.hookSpecificOutput = {
|
|
305
270
|
hookEventName: 'PostToolUse',
|
package/skills/cancel/SKILL.md
CHANGED
|
@@ -83,23 +83,20 @@ function countIncompleteTasks(sessionId) {
|
|
|
83
83
|
return count;
|
|
84
84
|
}
|
|
85
85
|
|
|
86
|
-
function countIncompleteTodos(
|
|
86
|
+
function countIncompleteTodos(sessionId, projectDir) {
|
|
87
87
|
let count = 0;
|
|
88
88
|
|
|
89
|
-
|
|
89
|
+
// Session-specific todos only (no global scan)
|
|
90
|
+
if (sessionId && typeof sessionId === 'string' && /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,255}$/.test(sessionId)) {
|
|
91
|
+
const sessionTodoPath = join(homedir(), '.claude', 'todos', `${sessionId}.json`);
|
|
90
92
|
try {
|
|
91
|
-
const
|
|
92
|
-
|
|
93
|
-
|
|
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 : []);
|
|
97
|
-
count += todos.filter(t => t.status !== 'completed' && t.status !== 'cancelled').length;
|
|
98
|
-
} catch { /* skip */ }
|
|
99
|
-
}
|
|
93
|
+
const data = readJsonFile(sessionTodoPath);
|
|
94
|
+
const todos = Array.isArray(data) ? data : (Array.isArray(data?.todos) ? data.todos : []);
|
|
95
|
+
count += todos.filter(t => t.status !== 'completed' && t.status !== 'cancelled').length;
|
|
100
96
|
} catch { /* skip */ }
|
|
101
97
|
}
|
|
102
98
|
|
|
99
|
+
// Project-local todos only
|
|
103
100
|
for (const path of [
|
|
104
101
|
join(projectDir, '.omc', 'todos.json'),
|
|
105
102
|
join(projectDir, '.claude', 'todos.json')
|
|
@@ -156,11 +153,13 @@ function isUserAbort(data) {
|
|
|
156
153
|
if (data.user_requested || data.userRequested) return true;
|
|
157
154
|
|
|
158
155
|
const reason = (data.stop_reason || data.stopReason || '').toLowerCase();
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
];
|
|
163
|
-
|
|
156
|
+
// Exact-match patterns: short generic words that cause false positives with .includes()
|
|
157
|
+
const exactPatterns = ['aborted', 'abort', 'cancel', 'interrupt'];
|
|
158
|
+
// Substring patterns: compound words safe for .includes() matching
|
|
159
|
+
const substringPatterns = ['user_cancel', 'user_interrupt', 'ctrl_c', 'manual_stop'];
|
|
160
|
+
|
|
161
|
+
return exactPatterns.some(p => reason === p) ||
|
|
162
|
+
substringPatterns.some(p => reason.includes(p));
|
|
164
163
|
}
|
|
165
164
|
|
|
166
165
|
async function main() {
|
|
@@ -171,7 +170,6 @@ async function main() {
|
|
|
171
170
|
|
|
172
171
|
const directory = data.directory || process.cwd();
|
|
173
172
|
const sessionId = data.sessionId || data.session_id || '';
|
|
174
|
-
const todosDir = join(homedir(), '.claude', 'todos');
|
|
175
173
|
const stateDir = join(directory, '.omc', 'state');
|
|
176
174
|
const globalStateDir = join(homedir(), '.omc', 'state');
|
|
177
175
|
|
|
@@ -202,9 +200,9 @@ async function main() {
|
|
|
202
200
|
const swarmMarker = existsSync(join(stateDir, 'swarm-active.marker'));
|
|
203
201
|
const swarmSummary = readJsonFile(join(stateDir, 'swarm-summary.json'));
|
|
204
202
|
|
|
205
|
-
// Count incomplete items
|
|
203
|
+
// Count incomplete items (session-specific + project-local only)
|
|
206
204
|
const taskCount = countIncompleteTasks(sessionId);
|
|
207
|
-
const todoCount = countIncompleteTodos(
|
|
205
|
+
const todoCount = countIncompleteTodos(sessionId, directory);
|
|
208
206
|
const totalIncomplete = taskCount + todoCount;
|
|
209
207
|
|
|
210
208
|
// Priority 1: Ralph Loop (explicit persistence mode)
|
|
@@ -217,8 +215,8 @@ async function main() {
|
|
|
217
215
|
writeJsonFile(ralph.path, ralph.state);
|
|
218
216
|
|
|
219
217
|
console.log(JSON.stringify({
|
|
220
|
-
continue:
|
|
221
|
-
|
|
218
|
+
continue: true,
|
|
219
|
+
message: `[RALPH LOOP - ITERATION ${iteration + 1}/${maxIter}] Work is NOT done. Continue. When complete, output: <promise>${ralph.state.completion_promise || 'DONE'}</promise>\n${ralph.state.prompt ? `Task: ${ralph.state.prompt}` : ''}`
|
|
222
220
|
}));
|
|
223
221
|
return;
|
|
224
222
|
}
|
|
@@ -234,8 +232,8 @@ async function main() {
|
|
|
234
232
|
writeJsonFile(autopilot.path, autopilot.state);
|
|
235
233
|
|
|
236
234
|
console.log(JSON.stringify({
|
|
237
|
-
continue:
|
|
238
|
-
|
|
235
|
+
continue: true,
|
|
236
|
+
message: `[AUTOPILOT - Phase: ${phase}] Autopilot not complete. Continue working.`
|
|
239
237
|
}));
|
|
240
238
|
return;
|
|
241
239
|
}
|
|
@@ -253,8 +251,8 @@ async function main() {
|
|
|
253
251
|
writeJsonFile(ultrapilot.path, ultrapilot.state);
|
|
254
252
|
|
|
255
253
|
console.log(JSON.stringify({
|
|
256
|
-
continue:
|
|
257
|
-
|
|
254
|
+
continue: true,
|
|
255
|
+
message: `[ULTRAPILOT] ${incomplete} workers still running. Continue.`
|
|
258
256
|
}));
|
|
259
257
|
return;
|
|
260
258
|
}
|
|
@@ -271,8 +269,8 @@ async function main() {
|
|
|
271
269
|
writeJsonFile(join(stateDir, 'swarm-summary.json'), swarmSummary);
|
|
272
270
|
|
|
273
271
|
console.log(JSON.stringify({
|
|
274
|
-
continue:
|
|
275
|
-
|
|
272
|
+
continue: true,
|
|
273
|
+
message: `[SWARM ACTIVE] ${pending} tasks remain. Continue working.`
|
|
276
274
|
}));
|
|
277
275
|
return;
|
|
278
276
|
}
|
|
@@ -290,8 +288,8 @@ async function main() {
|
|
|
290
288
|
writeJsonFile(pipeline.path, pipeline.state);
|
|
291
289
|
|
|
292
290
|
console.log(JSON.stringify({
|
|
293
|
-
continue:
|
|
294
|
-
|
|
291
|
+
continue: true,
|
|
292
|
+
message: `[PIPELINE - Stage ${currentStage + 1}/${totalStages}] Pipeline not complete. Continue.`
|
|
295
293
|
}));
|
|
296
294
|
return;
|
|
297
295
|
}
|
|
@@ -307,8 +305,8 @@ async function main() {
|
|
|
307
305
|
writeJsonFile(ultraqa.path, ultraqa.state);
|
|
308
306
|
|
|
309
307
|
console.log(JSON.stringify({
|
|
310
|
-
continue:
|
|
311
|
-
|
|
308
|
+
continue: true,
|
|
309
|
+
message: `[ULTRAQA - Cycle ${cycle + 1}/${maxCycles}] Tests not all passing. Continue fixing.`
|
|
312
310
|
}));
|
|
313
311
|
return;
|
|
314
312
|
}
|
|
@@ -342,8 +340,8 @@ async function main() {
|
|
|
342
340
|
}
|
|
343
341
|
|
|
344
342
|
console.log(JSON.stringify({
|
|
345
|
-
continue:
|
|
346
|
-
reason
|
|
343
|
+
continue: true,
|
|
344
|
+
message: reason
|
|
347
345
|
}));
|
|
348
346
|
return;
|
|
349
347
|
}
|
|
@@ -371,31 +369,8 @@ async function main() {
|
|
|
371
369
|
}
|
|
372
370
|
|
|
373
371
|
console.log(JSON.stringify({
|
|
374
|
-
continue:
|
|
375
|
-
reason
|
|
376
|
-
}));
|
|
377
|
-
return;
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
// Priority 9: Generic Task/Todo continuation (no specific mode)
|
|
381
|
-
if (totalIncomplete > 0) {
|
|
382
|
-
const contFile = join(stateDir, 'continuation-count.json');
|
|
383
|
-
let contState = readJsonFile(contFile) || { count: 0 };
|
|
384
|
-
contState.count = (contState.count || 0) + 1;
|
|
385
|
-
writeJsonFile(contFile, contState);
|
|
386
|
-
|
|
387
|
-
if (contState.count > 15) {
|
|
388
|
-
console.log(JSON.stringify({
|
|
389
|
-
continue: true,
|
|
390
|
-
reason: `[CONTINUATION ESCAPE] Max continuations reached. Allowing stop.`
|
|
391
|
-
}));
|
|
392
|
-
return;
|
|
393
|
-
}
|
|
394
|
-
|
|
395
|
-
const itemType = taskCount > 0 ? 'Tasks' : 'todos';
|
|
396
|
-
console.log(JSON.stringify({
|
|
397
|
-
continue: false,
|
|
398
|
-
reason: `[CONTINUATION ${contState.count}/15] ${totalIncomplete} incomplete ${itemType}. Continue working.`
|
|
372
|
+
continue: true,
|
|
373
|
+
message: reason
|
|
399
374
|
}));
|
|
400
375
|
return;
|
|
401
376
|
}
|
|
@@ -1,167 +1,15 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
// OMC Stop Continuation Hook (
|
|
3
|
-
//
|
|
4
|
-
// Cross-platform: Windows, macOS, Linux
|
|
2
|
+
// OMC Stop Continuation Hook (Simplified)
|
|
3
|
+
// Always allows stop - soft enforcement via message injection only.
|
|
5
4
|
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
import { homedir } from 'os';
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* Validates session ID to prevent path traversal attacks.
|
|
12
|
-
* @param {string} sessionId
|
|
13
|
-
* @returns {boolean}
|
|
14
|
-
*/
|
|
15
|
-
function isValidSessionId(sessionId) {
|
|
16
|
-
if (!sessionId || typeof sessionId !== 'string') return false;
|
|
17
|
-
return /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,255}$/.test(sessionId);
|
|
18
|
-
}
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Count incomplete tasks in the new Task system.
|
|
22
|
-
*
|
|
23
|
-
* SYNC NOTICE: This function is intentionally duplicated across:
|
|
24
|
-
* - templates/hooks/persistent-mode.mjs
|
|
25
|
-
* - templates/hooks/stop-continuation.mjs
|
|
26
|
-
* - src/hooks/todo-continuation/index.ts (as checkIncompleteTasks)
|
|
27
|
-
*
|
|
28
|
-
* Templates cannot import shared modules (they're standalone scripts).
|
|
29
|
-
* When modifying this logic, update ALL THREE files to maintain consistency.
|
|
30
|
-
*/
|
|
31
|
-
function countIncompleteTasks(sessionId) {
|
|
32
|
-
if (!sessionId || !isValidSessionId(sessionId)) return 0;
|
|
33
|
-
const taskDir = join(homedir(), '.claude', 'tasks', sessionId);
|
|
34
|
-
if (!existsSync(taskDir)) return 0;
|
|
35
|
-
|
|
36
|
-
let count = 0;
|
|
37
|
-
try {
|
|
38
|
-
const files = readdirSync(taskDir).filter(f => f.endsWith('.json') && f !== '.lock');
|
|
39
|
-
for (const file of files) {
|
|
40
|
-
try {
|
|
41
|
-
const content = readFileSync(join(taskDir, file), 'utf-8');
|
|
42
|
-
const task = JSON.parse(content);
|
|
43
|
-
// Match TypeScript isTaskIncomplete(): only pending/in_progress are incomplete
|
|
44
|
-
// 'deleted' and 'completed' are both treated as done
|
|
45
|
-
if (task.status === 'pending' || task.status === 'in_progress') count++;
|
|
46
|
-
} catch { /* skip invalid files */ }
|
|
47
|
-
}
|
|
48
|
-
} catch { /* dir read error */ }
|
|
49
|
-
return count;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
// Read all stdin
|
|
53
|
-
async function readStdin() {
|
|
5
|
+
// Consume stdin (required for hook protocol)
|
|
6
|
+
async function main() {
|
|
54
7
|
const chunks = [];
|
|
55
8
|
for await (const chunk of process.stdin) {
|
|
56
9
|
chunks.push(chunk);
|
|
57
10
|
}
|
|
58
|
-
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
/**
|
|
62
|
-
* Detect if stop was triggered by context-limit related reasons.
|
|
63
|
-
* See: https://github.com/Yeachan-Heo/oh-my-claudecode/issues/213
|
|
64
|
-
*/
|
|
65
|
-
function isContextLimitStop(data) {
|
|
66
|
-
const reason = (data.stop_reason || data.stopReason || '').toLowerCase();
|
|
67
|
-
const endTurnReason = (data.end_turn_reason || data.endTurnReason || '').toLowerCase();
|
|
68
|
-
const contextPatterns = [
|
|
69
|
-
'context_limit', 'context_window', 'context_exceeded', 'context_full',
|
|
70
|
-
'max_context', 'token_limit', 'max_tokens', 'conversation_too_long', 'input_too_long',
|
|
71
|
-
];
|
|
72
|
-
return contextPatterns.some(p => reason.includes(p) || endTurnReason.includes(p));
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
function isUserAbort(data) {
|
|
76
|
-
if (data.user_requested || data.userRequested) return true;
|
|
77
|
-
const reason = (data.stop_reason || data.stopReason || '').toLowerCase();
|
|
78
|
-
const abortPatterns = [
|
|
79
|
-
'user_cancel', 'user_interrupt', 'ctrl_c', 'manual_stop',
|
|
80
|
-
'aborted', 'abort', 'cancel', 'interrupt',
|
|
81
|
-
];
|
|
82
|
-
return abortPatterns.some(p => reason.includes(p));
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// Main
|
|
86
|
-
async function main() {
|
|
87
|
-
try {
|
|
88
|
-
// Read stdin to get sessionId and consume it
|
|
89
|
-
const input = await readStdin();
|
|
90
|
-
|
|
91
|
-
// Parse sessionId from input
|
|
92
|
-
let data = {};
|
|
93
|
-
try {
|
|
94
|
-
data = JSON.parse(input);
|
|
95
|
-
} catch { /* invalid JSON - continue with empty data */ }
|
|
96
|
-
|
|
97
|
-
// Never block context-limit or user-abort stops
|
|
98
|
-
if (isContextLimitStop(data) || isUserAbort(data)) {
|
|
99
|
-
console.log(JSON.stringify({ continue: true }));
|
|
100
|
-
return;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
const sessionId = data.sessionId || data.session_id || '';
|
|
104
|
-
|
|
105
|
-
// Count incomplete Task system tasks
|
|
106
|
-
const taskCount = countIncompleteTasks(sessionId);
|
|
107
|
-
|
|
108
|
-
// Check for incomplete todos
|
|
109
|
-
const todosDir = join(homedir(), '.claude', 'todos');
|
|
110
|
-
|
|
111
|
-
if (!existsSync(todosDir)) {
|
|
112
|
-
console.log(JSON.stringify({ continue: true }));
|
|
113
|
-
return;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
let incompleteCount = 0;
|
|
117
|
-
|
|
118
|
-
try {
|
|
119
|
-
const files = readdirSync(todosDir).filter(f => f.endsWith('.json'));
|
|
120
|
-
|
|
121
|
-
for (const file of files) {
|
|
122
|
-
try {
|
|
123
|
-
const content = readFileSync(join(todosDir, file), 'utf-8');
|
|
124
|
-
const todos = JSON.parse(content);
|
|
125
|
-
|
|
126
|
-
if (Array.isArray(todos)) {
|
|
127
|
-
const incomplete = todos.filter(
|
|
128
|
-
t => t.status !== 'completed' && t.status !== 'cancelled'
|
|
129
|
-
);
|
|
130
|
-
incompleteCount += incomplete.length;
|
|
131
|
-
}
|
|
132
|
-
} catch {
|
|
133
|
-
// Skip files that can't be parsed
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
} catch {
|
|
137
|
-
// Directory read error - allow continuation
|
|
138
|
-
console.log(JSON.stringify({ continue: true }));
|
|
139
|
-
return;
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
// Combine both counts
|
|
143
|
-
const totalIncomplete = taskCount + incompleteCount;
|
|
144
|
-
|
|
145
|
-
if (totalIncomplete > 0) {
|
|
146
|
-
const sourceLabel = taskCount > 0 ? 'Task' : 'todo';
|
|
147
|
-
const reason = `[SYSTEM REMINDER - ${sourceLabel.toUpperCase()} CONTINUATION]
|
|
148
|
-
|
|
149
|
-
Incomplete ${sourceLabel}s remain (${totalIncomplete} remaining). Continue working on the next pending ${sourceLabel}.
|
|
150
|
-
|
|
151
|
-
- Proceed without asking for permission
|
|
152
|
-
- Mark each ${sourceLabel} complete when finished
|
|
153
|
-
- Do not stop until all ${sourceLabel}s are done`;
|
|
154
|
-
|
|
155
|
-
console.log(JSON.stringify({ decision: "block", reason }));
|
|
156
|
-
return;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
// No incomplete tasks or todos - allow stop
|
|
160
|
-
console.log(JSON.stringify({ continue: true }));
|
|
161
|
-
} catch (error) {
|
|
162
|
-
// On any error, allow continuation
|
|
163
|
-
console.log(JSON.stringify({ continue: true }));
|
|
164
|
-
}
|
|
11
|
+
// Always allow stop
|
|
12
|
+
console.log(JSON.stringify({ continue: true }));
|
|
165
13
|
}
|
|
166
14
|
|
|
167
15
|
main();
|