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.
Files changed (65) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/dist/__tests__/task-continuation.test.js +5 -2
  4. package/dist/__tests__/task-continuation.test.js.map +1 -1
  5. package/dist/features/continuation-enforcement.js +1 -1
  6. package/dist/features/continuation-enforcement.js.map +1 -1
  7. package/dist/hooks/__tests__/bridge-pkill.test.d.ts +8 -0
  8. package/dist/hooks/__tests__/bridge-pkill.test.d.ts.map +1 -0
  9. package/dist/hooks/__tests__/bridge-pkill.test.js +188 -0
  10. package/dist/hooks/__tests__/bridge-pkill.test.js.map +1 -0
  11. package/dist/hooks/bridge.d.ts.map +1 -1
  12. package/dist/hooks/bridge.js +23 -29
  13. package/dist/hooks/bridge.js.map +1 -1
  14. package/dist/hooks/clear-suggestions/index.d.ts.map +1 -1
  15. package/dist/hooks/clear-suggestions/index.js +6 -0
  16. package/dist/hooks/clear-suggestions/index.js.map +1 -1
  17. package/dist/hooks/index.d.ts +0 -1
  18. package/dist/hooks/index.d.ts.map +1 -1
  19. package/dist/hooks/index.js +0 -3
  20. package/dist/hooks/index.js.map +1 -1
  21. package/dist/hooks/learner/constants.d.ts +2 -0
  22. package/dist/hooks/learner/constants.d.ts.map +1 -1
  23. package/dist/hooks/learner/constants.js +2 -0
  24. package/dist/hooks/learner/constants.js.map +1 -1
  25. package/dist/hooks/mode-registry/index.d.ts +7 -0
  26. package/dist/hooks/mode-registry/index.d.ts.map +1 -1
  27. package/dist/hooks/mode-registry/index.js +9 -0
  28. package/dist/hooks/mode-registry/index.js.map +1 -1
  29. package/dist/hooks/persistent-mode/index.d.ts +2 -1
  30. package/dist/hooks/persistent-mode/index.d.ts.map +1 -1
  31. package/dist/hooks/persistent-mode/index.js +7 -17
  32. package/dist/hooks/persistent-mode/index.js.map +1 -1
  33. package/dist/hooks/todo-continuation/__tests__/isUserAbort.test.d.ts +2 -0
  34. package/dist/hooks/todo-continuation/__tests__/isUserAbort.test.d.ts.map +1 -0
  35. package/dist/hooks/todo-continuation/__tests__/isUserAbort.test.js +119 -0
  36. package/dist/hooks/todo-continuation/__tests__/isUserAbort.test.js.map +1 -0
  37. package/dist/hooks/todo-continuation/index.d.ts.map +1 -1
  38. package/dist/hooks/todo-continuation/index.js +8 -27
  39. package/dist/hooks/todo-continuation/index.js.map +1 -1
  40. package/dist/installer/hooks.d.ts +8 -111
  41. package/dist/installer/hooks.d.ts.map +1 -1
  42. package/dist/installer/hooks.js +11 -124
  43. package/dist/installer/hooks.js.map +1 -1
  44. package/dist/installer/index.d.ts +2 -10
  45. package/dist/installer/index.d.ts.map +1 -1
  46. package/dist/installer/index.js +10 -23
  47. package/dist/installer/index.js.map +1 -1
  48. package/package.json +1 -1
  49. package/scripts/persistent-mode.mjs +33 -58
  50. package/scripts/post-tool-verifier.mjs +1 -36
  51. package/skills/cancel/SKILL.md +1 -1
  52. package/templates/hooks/persistent-mode.mjs +33 -58
  53. package/templates/hooks/stop-continuation.mjs +6 -158
  54. package/hooks/keyword-detector.sh +0 -102
  55. package/hooks/persistent-mode.sh +0 -172
  56. package/hooks/session-start.sh +0 -62
  57. package/hooks/stop-continuation.sh +0 -40
  58. package/scripts/claude-sisyphus.sh +0 -9
  59. package/scripts/install.sh +0 -1673
  60. package/scripts/keyword-detector.sh +0 -71
  61. package/scripts/persistent-mode.sh +0 -311
  62. package/scripts/post-tool-verifier.sh +0 -196
  63. package/scripts/pre-tool-enforcer.sh +0 -76
  64. package/scripts/sisyphus-aliases.sh +0 -18
  65. package/scripts/stop-continuation.sh +0 -31
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "oh-my-claude-sisyphus",
3
- "version": "3.8.11",
3
+ "version": "3.8.13",
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",
@@ -83,23 +83,20 @@ function countIncompleteTasks(sessionId) {
83
83
  return count;
84
84
  }
85
85
 
86
- function countIncompleteTodos(todosDir, projectDir) {
86
+ function countIncompleteTodos(sessionId, projectDir) {
87
87
  let count = 0;
88
88
 
89
- if (existsSync(todosDir)) {
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 files = readdirSync(todosDir).filter(f => f.endsWith('.json'));
92
- for (const file of files) {
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 : []);
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
- 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));
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(todosDir, directory);
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: 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}` : ''}`
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: false,
240
- reason: `[AUTOPILOT - Phase: ${phase}] Autopilot not complete. Continue working.`
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: false,
259
- reason: `[ULTRAPILOT] ${incomplete} workers still running. Continue.`
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: false,
277
- reason: `[SWARM ACTIVE] ${pending} tasks remain. Continue working.`
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: false,
296
- reason: `[PIPELINE - Stage ${currentStage + 1}/${totalStages}] Pipeline not complete. Continue.`
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: false,
313
- reason: `[ULTRAQA - Cycle ${cycle + 1}/${maxCycles}] Tests not all passing. Continue fixing.`
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: false,
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: false,
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 = clearSuggestionMessage || message;
267
+ const contextMessage = message;
303
268
  if (contextMessage) {
304
269
  response.hookSpecificOutput = {
305
270
  hookEventName: 'PostToolUse',
@@ -25,7 +25,7 @@ Automatically detects which mode is active and cancels it:
25
25
  /oh-my-claudecode:cancel
26
26
  ```
27
27
 
28
- Or say: "stop", "cancel", "abort"
28
+ Or say: "cancelomc", "stopomc"
29
29
 
30
30
  ## Auto-Detection
31
31
 
@@ -83,23 +83,20 @@ function countIncompleteTasks(sessionId) {
83
83
  return count;
84
84
  }
85
85
 
86
- function countIncompleteTodos(todosDir, projectDir) {
86
+ function countIncompleteTodos(sessionId, projectDir) {
87
87
  let count = 0;
88
88
 
89
- if (existsSync(todosDir)) {
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 files = readdirSync(todosDir).filter(f => f.endsWith('.json'));
92
- for (const file of files) {
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 : []);
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
- const abortPatterns = [
160
- 'user_cancel', 'user_interrupt', 'ctrl_c', 'manual_stop',
161
- 'aborted', 'abort', 'cancel', 'interrupt',
162
- ];
163
- return abortPatterns.some(p => reason.includes(p));
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(todosDir, directory);
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: false,
221
- reason: `[RALPH LOOP - ITERATION ${iteration + 1}/${maxIter}] Work is NOT done. Continue. When complete, output: <promise>${ralph.state.completion_promise || 'DONE'}</promise>\n${ralph.state.prompt ? `Task: ${ralph.state.prompt}` : ''}`
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: false,
238
- reason: `[AUTOPILOT - Phase: ${phase}] Autopilot not complete. Continue working.`
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: false,
257
- reason: `[ULTRAPILOT] ${incomplete} workers still running. Continue.`
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: false,
275
- reason: `[SWARM ACTIVE] ${pending} tasks remain. Continue working.`
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: false,
294
- reason: `[PIPELINE - Stage ${currentStage + 1}/${totalStages}] Pipeline not complete. Continue.`
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: false,
311
- reason: `[ULTRAQA - Cycle ${cycle + 1}/${maxCycles}] Tests not all passing. Continue fixing.`
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: false,
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: false,
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 (Node.js)
3
- // Checks for incomplete todos and injects continuation prompt
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
- import { readdirSync, readFileSync, existsSync } from 'fs';
7
- import { join } from 'path';
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
- return Buffer.concat(chunks).toString('utf-8');
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();