oh-my-claude-sisyphus 3.8.11 → 3.8.12

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 (37) hide show
  1. package/.claude-plugin/plugin.json +1 -1
  2. package/dist/features/continuation-enforcement.js +1 -1
  3. package/dist/features/continuation-enforcement.js.map +1 -1
  4. package/dist/hooks/bridge.d.ts.map +1 -1
  5. package/dist/hooks/bridge.js +6 -29
  6. package/dist/hooks/bridge.js.map +1 -1
  7. package/dist/hooks/persistent-mode/index.d.ts +2 -1
  8. package/dist/hooks/persistent-mode/index.d.ts.map +1 -1
  9. package/dist/hooks/persistent-mode/index.js +7 -17
  10. package/dist/hooks/persistent-mode/index.js.map +1 -1
  11. package/dist/hooks/todo-continuation/index.d.ts.map +1 -1
  12. package/dist/hooks/todo-continuation/index.js +2 -15
  13. package/dist/hooks/todo-continuation/index.js.map +1 -1
  14. package/dist/installer/hooks.d.ts +8 -111
  15. package/dist/installer/hooks.d.ts.map +1 -1
  16. package/dist/installer/hooks.js +11 -124
  17. package/dist/installer/hooks.js.map +1 -1
  18. package/dist/installer/index.d.ts +2 -10
  19. package/dist/installer/index.d.ts.map +1 -1
  20. package/dist/installer/index.js +10 -23
  21. package/dist/installer/index.js.map +1 -1
  22. package/package.json +1 -1
  23. package/scripts/persistent-mode.mjs +26 -53
  24. package/templates/hooks/persistent-mode.mjs +26 -53
  25. package/templates/hooks/stop-continuation.mjs +6 -158
  26. package/hooks/keyword-detector.sh +0 -102
  27. package/hooks/persistent-mode.sh +0 -172
  28. package/hooks/session-start.sh +0 -62
  29. package/hooks/stop-continuation.sh +0 -40
  30. package/scripts/claude-sisyphus.sh +0 -9
  31. package/scripts/install.sh +0 -1673
  32. package/scripts/keyword-detector.sh +0 -71
  33. package/scripts/persistent-mode.sh +0 -311
  34. package/scripts/post-tool-verifier.sh +0 -196
  35. package/scripts/pre-tool-enforcer.sh +0 -76
  36. package/scripts/sisyphus-aliases.sh +0 -18
  37. package/scripts/stop-continuation.sh +0 -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')
@@ -171,7 +168,6 @@ async function main() {
171
168
 
172
169
  const directory = data.directory || process.cwd();
173
170
  const sessionId = data.sessionId || data.session_id || '';
174
- const todosDir = join(homedir(), '.claude', 'todos');
175
171
  const stateDir = join(directory, '.omc', 'state');
176
172
  const globalStateDir = join(homedir(), '.omc', 'state');
177
173
 
@@ -202,9 +198,9 @@ async function main() {
202
198
  const swarmMarker = existsSync(join(stateDir, 'swarm-active.marker'));
203
199
  const swarmSummary = readJsonFile(join(stateDir, 'swarm-summary.json'));
204
200
 
205
- // Count incomplete items
201
+ // Count incomplete items (session-specific + project-local only)
206
202
  const taskCount = countIncompleteTasks(sessionId);
207
- const todoCount = countIncompleteTodos(todosDir, directory);
203
+ const todoCount = countIncompleteTodos(sessionId, directory);
208
204
  const totalIncomplete = taskCount + todoCount;
209
205
 
210
206
  // Priority 1: Ralph Loop (explicit persistence mode)
@@ -217,8 +213,8 @@ async function main() {
217
213
  writeJsonFile(ralph.path, ralph.state);
218
214
 
219
215
  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}` : ''}`
216
+ continue: true,
217
+ 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
218
  }));
223
219
  return;
224
220
  }
@@ -234,8 +230,8 @@ async function main() {
234
230
  writeJsonFile(autopilot.path, autopilot.state);
235
231
 
236
232
  console.log(JSON.stringify({
237
- continue: false,
238
- reason: `[AUTOPILOT - Phase: ${phase}] Autopilot not complete. Continue working.`
233
+ continue: true,
234
+ message: `[AUTOPILOT - Phase: ${phase}] Autopilot not complete. Continue working.`
239
235
  }));
240
236
  return;
241
237
  }
@@ -253,8 +249,8 @@ async function main() {
253
249
  writeJsonFile(ultrapilot.path, ultrapilot.state);
254
250
 
255
251
  console.log(JSON.stringify({
256
- continue: false,
257
- reason: `[ULTRAPILOT] ${incomplete} workers still running. Continue.`
252
+ continue: true,
253
+ message: `[ULTRAPILOT] ${incomplete} workers still running. Continue.`
258
254
  }));
259
255
  return;
260
256
  }
@@ -271,8 +267,8 @@ async function main() {
271
267
  writeJsonFile(join(stateDir, 'swarm-summary.json'), swarmSummary);
272
268
 
273
269
  console.log(JSON.stringify({
274
- continue: false,
275
- reason: `[SWARM ACTIVE] ${pending} tasks remain. Continue working.`
270
+ continue: true,
271
+ message: `[SWARM ACTIVE] ${pending} tasks remain. Continue working.`
276
272
  }));
277
273
  return;
278
274
  }
@@ -290,8 +286,8 @@ async function main() {
290
286
  writeJsonFile(pipeline.path, pipeline.state);
291
287
 
292
288
  console.log(JSON.stringify({
293
- continue: false,
294
- reason: `[PIPELINE - Stage ${currentStage + 1}/${totalStages}] Pipeline not complete. Continue.`
289
+ continue: true,
290
+ message: `[PIPELINE - Stage ${currentStage + 1}/${totalStages}] Pipeline not complete. Continue.`
295
291
  }));
296
292
  return;
297
293
  }
@@ -307,8 +303,8 @@ async function main() {
307
303
  writeJsonFile(ultraqa.path, ultraqa.state);
308
304
 
309
305
  console.log(JSON.stringify({
310
- continue: false,
311
- reason: `[ULTRAQA - Cycle ${cycle + 1}/${maxCycles}] Tests not all passing. Continue fixing.`
306
+ continue: true,
307
+ message: `[ULTRAQA - Cycle ${cycle + 1}/${maxCycles}] Tests not all passing. Continue fixing.`
312
308
  }));
313
309
  return;
314
310
  }
@@ -342,8 +338,8 @@ async function main() {
342
338
  }
343
339
 
344
340
  console.log(JSON.stringify({
345
- continue: false,
346
- reason
341
+ continue: true,
342
+ message: reason
347
343
  }));
348
344
  return;
349
345
  }
@@ -371,31 +367,8 @@ async function main() {
371
367
  }
372
368
 
373
369
  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.`
370
+ continue: true,
371
+ message: reason
399
372
  }));
400
373
  return;
401
374
  }
@@ -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();
@@ -1,102 +0,0 @@
1
- #!/bin/bash
2
- # Sisyphus Keyword Detector Hook
3
- # Detects ultrawork/ultrathink/search/analyze keywords and injects enhanced mode messages
4
- # Also activates persistent ultrawork state when ultrawork keyword is detected
5
-
6
- # Read stdin (JSON input from Claude Code)
7
- INPUT=$(cat)
8
-
9
- # Extract directory from input
10
- DIRECTORY=""
11
- if command -v jq &> /dev/null; then
12
- DIRECTORY=$(echo "$INPUT" | jq -r '.directory // ""' 2>/dev/null)
13
- fi
14
- if [ -z "$DIRECTORY" ] || [ "$DIRECTORY" = "null" ]; then
15
- DIRECTORY=$(pwd)
16
- fi
17
-
18
- # Extract the prompt text - try multiple JSON paths
19
- PROMPT=""
20
- if command -v jq &> /dev/null; then
21
- # Try to extract from various possible JSON structures
22
- PROMPT=$(echo "$INPUT" | jq -r '
23
- if .prompt then .prompt
24
- elif .message.content then .message.content
25
- elif .parts then ([.parts[] | select(.type == "text") | .text] | join(" "))
26
- else ""
27
- end
28
- ' 2>/dev/null)
29
- fi
30
-
31
- # Fallback: simple grep extraction if jq fails
32
- if [ -z "$PROMPT" ] || [ "$PROMPT" = "null" ]; then
33
- PROMPT=$(echo "$INPUT" | grep -oP '"(prompt|content|text)"\s*:\s*"\K[^"]+' | head -1)
34
- fi
35
-
36
- # Exit if no prompt found
37
- if [ -z "$PROMPT" ]; then
38
- echo '{"continue": true}'
39
- exit 0
40
- fi
41
-
42
- # Remove code blocks before checking keywords (prevents false positives)
43
- PROMPT_NO_CODE=$(echo "$PROMPT" | sed 's/```[^`]*```//g' | sed 's/`[^`]*`//g')
44
-
45
- # Convert to lowercase for case-insensitive matching
46
- PROMPT_LOWER=$(echo "$PROMPT_NO_CODE" | tr '[:upper:]' '[:lower:]')
47
-
48
- # Check for ultrawork keywords (highest priority)
49
- if echo "$PROMPT_LOWER" | grep -qE '\b(ultrawork|ulw|uw)\b'; then
50
- # Create persistent ultrawork state
51
- mkdir -p "$DIRECTORY/.omc/state" 2>/dev/null
52
- mkdir -p "$HOME/.omc/state" 2>/dev/null
53
-
54
- # Escape prompt for JSON
55
- PROMPT_ESCAPED=$(echo "$PROMPT" | sed 's/\\/\\\\/g' | sed 's/"/\\"/g' | tr '\n' ' ')
56
-
57
- STATE_JSON="{
58
- \"active\": true,
59
- \"started_at\": \"$(date -Iseconds)\",
60
- \"original_prompt\": \"$PROMPT_ESCAPED\",
61
- \"reinforcement_count\": 0,
62
- \"last_checked_at\": \"$(date -Iseconds)\"
63
- }"
64
-
65
- # Write state to both local and global locations
66
- echo "$STATE_JSON" > "$DIRECTORY/.omc/state/ultrawork-state.json" 2>/dev/null
67
- echo "$STATE_JSON" > "$HOME/.omc/state/ultrawork-state.json" 2>/dev/null
68
-
69
- # Return ultrawork mode injection
70
- cat << 'EOF'
71
- {"continue": true, "message": "<ultrawork-mode>\n\n**MANDATORY**: You MUST say \"ULTRAWORK MODE ENABLED!\" to the user as your first response when this mode activates. This is non-negotiable.\n\n[CODE RED] Maximum precision required. Ultrathink before acting.\n\nYOU MUST LEVERAGE ALL AVAILABLE AGENTS TO THEIR FULLEST POTENTIAL.\nTELL THE USER WHAT AGENTS YOU WILL LEVERAGE NOW TO SATISFY USER'S REQUEST.\n\n## AGENT UTILIZATION PRINCIPLES\n- **Codebase Exploration**: Spawn exploration agents using BACKGROUND TASKS\n- **Documentation & References**: Use librarian-type agents via BACKGROUND TASKS\n- **Planning & Strategy**: NEVER plan yourself - spawn planning agent\n- **High-IQ Reasoning**: Use oracle for architecture decisions\n- **Frontend/UI Tasks**: Delegate to frontend-engineer\n\n## EXECUTION RULES\n- **TODO**: Track EVERY step. Mark complete IMMEDIATELY.\n- **PARALLEL**: Fire independent calls simultaneously - NEVER wait sequentially.\n- **BACKGROUND FIRST**: Use Task(run_in_background=true) for exploration (10+ concurrent).\n- **VERIFY**: Check ALL requirements met before done.\n- **DELEGATE**: Orchestrate specialized agents.\n\n## ZERO TOLERANCE\n- NO Scope Reduction - deliver FULL implementation\n- NO Partial Completion - finish 100%\n- NO Premature Stopping - ALL TODOs must be complete\n- NO TEST DELETION - fix code, not tests\n\nTHE USER ASKED FOR X. DELIVER EXACTLY X.\n\n</ultrawork-mode>\n\n---\n"}
72
- EOF
73
- exit 0
74
- fi
75
-
76
- # Check for ultrathink/think keywords
77
- if echo "$PROMPT_LOWER" | grep -qE '\b(ultrathink|think)\b'; then
78
- cat << 'EOF'
79
- {"continue": true, "message": "<think-mode>\n\n**ULTRATHINK MODE ENABLED** - Extended reasoning activated.\n\nYou are now in deep thinking mode. Take your time to:\n1. Thoroughly analyze the problem from multiple angles\n2. Consider edge cases and potential issues\n3. Think through the implications of each approach\n4. Reason step-by-step before acting\n\nUse your extended thinking capabilities to provide the most thorough and well-reasoned response.\n\n</think-mode>\n\n---\n"}
80
- EOF
81
- exit 0
82
- fi
83
-
84
- # Check for search keywords (EN + multilingual)
85
- if echo "$PROMPT_LOWER" | grep -qE '\b(search|find|locate|lookup|explore|discover|scan|grep|query|browse|detect|trace|seek|track|pinpoint|hunt)\b|where\s+is|show\s+me|list\s+all'; then
86
- cat << 'EOF'
87
- {"continue": true, "message": "<search-mode>\nMAXIMIZE SEARCH EFFORT. Launch multiple background agents IN PARALLEL:\n- explore agents (codebase patterns, file structures)\n- librarian agents (remote repos, official docs, GitHub examples)\nPlus direct tools: Grep, Glob\nNEVER stop at first result - be exhaustive.\n</search-mode>\n\n---\n"}
88
- EOF
89
- exit 0
90
- fi
91
-
92
- # Check for analyze keywords
93
- if echo "$PROMPT_LOWER" | grep -qE '\b(analyze|analyse|investigate|examine|research|study|deep.?dive|inspect|audit|evaluate|assess|review|diagnose|scrutinize|dissect|debug|comprehend|interpret|breakdown|understand)\b|why\s+is|how\s+does|how\s+to'; then
94
- cat << 'EOF'
95
- {"continue": true, "message": "<analyze-mode>\nANALYSIS MODE. Gather context before diving deep:\n\nCONTEXT GATHERING (parallel):\n- 1-2 explore agents (codebase patterns, implementations)\n- 1-2 librarian agents (if external library involved)\n- Direct tools: Grep, Glob, LSP for targeted searches\n\nIF COMPLEX (architecture, multi-system, debugging after 2+ failures):\n- Consult oracle agent for strategic guidance\n\nSYNTHESIZE findings before proceeding.\n</analyze-mode>\n\n---\n"}
96
- EOF
97
- exit 0
98
- fi
99
-
100
- # No keywords detected - continue without modification
101
- echo '{"continue": true}'
102
- exit 0
@@ -1,172 +0,0 @@
1
- #!/bin/bash
2
- # Sisyphus Persistent Mode Hook
3
- # Unified handler for ultrawork, ralph-loop, and todo continuation
4
- # Prevents stopping when work remains incomplete
5
-
6
- # Read stdin
7
- INPUT=$(cat)
8
-
9
- # Get session ID and directory
10
- SESSION_ID=""
11
- DIRECTORY=""
12
- if command -v jq &> /dev/null; then
13
- SESSION_ID=$(echo "$INPUT" | jq -r '.sessionId // .session_id // ""' 2>/dev/null)
14
- DIRECTORY=$(echo "$INPUT" | jq -r '.directory // ""' 2>/dev/null)
15
- fi
16
-
17
- # Default to current directory
18
- if [ -z "$DIRECTORY" ]; then
19
- DIRECTORY=$(pwd)
20
- fi
21
-
22
- # Check for active ultrawork state
23
- ULTRAWORK_STATE=""
24
- if [ -f "$DIRECTORY/.omc/state/ultrawork-state.json" ]; then
25
- ULTRAWORK_STATE=$(cat "$DIRECTORY/.omc/state/ultrawork-state.json" 2>/dev/null)
26
- elif [ -f "$HOME/.omc/state/ultrawork-state.json" ]; then
27
- ULTRAWORK_STATE=$(cat "$HOME/.omc/state/ultrawork-state.json" 2>/dev/null)
28
- fi
29
-
30
- # Check for active ralph loop
31
- RALPH_STATE=""
32
- if [ -f "$DIRECTORY/.omc/state/ralph-state.json" ]; then
33
- RALPH_STATE=$(cat "$DIRECTORY/.omc/state/ralph-state.json" 2>/dev/null)
34
- fi
35
-
36
- # Check for verification state (oracle verification)
37
- VERIFICATION_STATE=""
38
- if [ -f "$DIRECTORY/.omc/state/ralph-verification.json" ]; then
39
- VERIFICATION_STATE=$(cat "$DIRECTORY/.omc/state/ralph-verification.json" 2>/dev/null)
40
- fi
41
-
42
- # Check for incomplete todos
43
- INCOMPLETE_COUNT=0
44
- TODOS_DIR="$HOME/.claude/todos"
45
- if [ -d "$TODOS_DIR" ]; then
46
- for todo_file in "$TODOS_DIR"/*.json; do
47
- if [ -f "$todo_file" ]; then
48
- if command -v jq &> /dev/null; then
49
- COUNT=$(jq '[.[] | select(.status != "completed" and .status != "cancelled")] | length' "$todo_file" 2>/dev/null || echo "0")
50
- INCOMPLETE_COUNT=$((INCOMPLETE_COUNT + COUNT))
51
- else
52
- # Fallback: count "pending" or "in_progress" occurrences
53
- COUNT=$(grep -c '"status"[[:space:]]*:[[:space:]]*"pending\|in_progress"' "$todo_file" 2>/dev/null) || COUNT=0
54
- INCOMPLETE_COUNT=$((INCOMPLETE_COUNT + COUNT))
55
- fi
56
- fi
57
- done
58
- fi
59
-
60
- # Check project todos as well
61
- for todo_path in "$DIRECTORY/.omc/todos.json" "$DIRECTORY/.claude/todos.json"; do
62
- if [ -f "$todo_path" ]; then
63
- if command -v jq &> /dev/null; then
64
- COUNT=$(jq 'if type == "array" then [.[] | select(.status != "completed" and .status != "cancelled")] | length else 0 end' "$todo_path" 2>/dev/null || echo "0")
65
- INCOMPLETE_COUNT=$((INCOMPLETE_COUNT + COUNT))
66
- else
67
- # Fallback: count "pending" or "in_progress" occurrences
68
- COUNT=$(grep -c '"status"[[:space:]]*:[[:space:]]*"pending\|in_progress"' "$todo_path" 2>/dev/null) || COUNT=0
69
- INCOMPLETE_COUNT=$((INCOMPLETE_COUNT + COUNT))
70
- fi
71
- fi
72
- done
73
-
74
- # Priority 1: Ralph Loop with Oracle Verification
75
- if [ -n "$RALPH_STATE" ]; then
76
- IS_ACTIVE=$(echo "$RALPH_STATE" | jq -r '.active // false' 2>/dev/null)
77
- if [ "$IS_ACTIVE" = "true" ]; then
78
- ITERATION=$(echo "$RALPH_STATE" | jq -r '.iteration // 1' 2>/dev/null)
79
- MAX_ITER=$(echo "$RALPH_STATE" | jq -r '.max_iterations // 10' 2>/dev/null)
80
- PROMISE=$(echo "$RALPH_STATE" | jq -r '.completion_promise // "TASK_COMPLETE"' 2>/dev/null)
81
- PROMPT=$(echo "$RALPH_STATE" | jq -r '.prompt // ""' 2>/dev/null)
82
-
83
- # Check if oracle verification is pending
84
- if [ -n "$VERIFICATION_STATE" ]; then
85
- IS_PENDING=$(echo "$VERIFICATION_STATE" | jq -r '.pending // false' 2>/dev/null)
86
- if [ "$IS_PENDING" = "true" ]; then
87
- ATTEMPT=$(echo "$VERIFICATION_STATE" | jq -r '.verification_attempts // 0' 2>/dev/null)
88
- MAX_ATTEMPTS=$(echo "$VERIFICATION_STATE" | jq -r '.max_verification_attempts // 3' 2>/dev/null)
89
- ORIGINAL_TASK=$(echo "$VERIFICATION_STATE" | jq -r '.original_task // ""' 2>/dev/null)
90
- COMPLETION_CLAIM=$(echo "$VERIFICATION_STATE" | jq -r '.completion_claim // ""' 2>/dev/null)
91
- ORACLE_FEEDBACK=$(echo "$VERIFICATION_STATE" | jq -r '.oracle_feedback // ""' 2>/dev/null)
92
- NEXT_ATTEMPT=$((ATTEMPT + 1))
93
-
94
- FEEDBACK_SECTION=""
95
- if [ -n "$ORACLE_FEEDBACK" ] && [ "$ORACLE_FEEDBACK" != "null" ]; then
96
- FEEDBACK_SECTION="\n**Previous Oracle Feedback (rejected):**\n$ORACLE_FEEDBACK\n"
97
- fi
98
-
99
- cat << EOF
100
- {"continue": false, "reason": "<ralph-verification>\n\n[ORACLE VERIFICATION REQUIRED - Attempt $NEXT_ATTEMPT/$MAX_ATTEMPTS]\n\nThe agent claims the task is complete. Before accepting, YOU MUST verify with Oracle.\n\n**Original Task:**\n$ORIGINAL_TASK\n\n**Completion Claim:**\n$COMPLETION_CLAIM\n$FEEDBACK_SECTION\n## MANDATORY VERIFICATION STEPS\n\n1. **Spawn Oracle Agent** for verification:\n \`\`\`\n Task(subagent_type=\"oracle\", prompt=\"Verify this task completion claim...\")\n \`\`\`\n\n2. **Oracle must check:**\n - Are ALL requirements from the original task met?\n - Is the implementation complete, not partial?\n - Are there any obvious bugs or issues?\n - Does the code compile/run without errors?\n - Are tests passing (if applicable)?\n\n3. **Based on Oracle's response:**\n - If APPROVED: Output \`<oracle-approved>VERIFIED_COMPLETE</oracle-approved>\`\n - If REJECTED: Continue working on the identified issues\n\nDO NOT output the completion promise again until Oracle approves.\n\n</ralph-verification>\n\n---\n"}
101
- EOF
102
- exit 0
103
- fi
104
- fi
105
-
106
- if [ "$ITERATION" -lt "$MAX_ITER" ]; then
107
- # Increment iteration
108
- NEW_ITER=$((ITERATION + 1))
109
- echo "$RALPH_STATE" | jq ".iteration = $NEW_ITER" > "$DIRECTORY/.omc/state/ralph-state.json" 2>/dev/null
110
-
111
- cat << EOF
112
- {"continue": false, "reason": "<ralph-loop-continuation>\n\n[RALPH LOOP - ITERATION $NEW_ITER/$MAX_ITER]\n\nYour previous attempt did not output the completion promise. The work is NOT done yet.\n\nCRITICAL INSTRUCTIONS:\n1. Review your progress and the original task\n2. Check your todo list - are ALL items marked complete?\n3. Continue from where you left off\n4. When FULLY complete, output: <promise>$PROMISE</promise>\n5. Do NOT stop until the task is truly done\n\nOriginal task: $PROMPT\n\n</ralph-loop-continuation>\n\n---\n"}
113
- EOF
114
- exit 0
115
- fi
116
- fi
117
- fi
118
-
119
- # Priority 2: Ultrawork Mode with incomplete todos
120
- if [ -n "$ULTRAWORK_STATE" ] && [ "$INCOMPLETE_COUNT" -gt 0 ]; then
121
- # Check if active (with jq fallback)
122
- IS_ACTIVE=""
123
- if command -v jq &> /dev/null; then
124
- IS_ACTIVE=$(echo "$ULTRAWORK_STATE" | jq -r '.active // false' 2>/dev/null)
125
- else
126
- # Fallback: grep for "active": true
127
- if echo "$ULTRAWORK_STATE" | grep -q '"active"[[:space:]]*:[[:space:]]*true'; then
128
- IS_ACTIVE="true"
129
- fi
130
- fi
131
-
132
- if [ "$IS_ACTIVE" = "true" ]; then
133
- # Get reinforcement count (with fallback)
134
- REINFORCE_COUNT=0
135
- if command -v jq &> /dev/null; then
136
- REINFORCE_COUNT=$(echo "$ULTRAWORK_STATE" | jq -r '.reinforcement_count // 0' 2>/dev/null)
137
- else
138
- REINFORCE_COUNT=$(echo "$ULTRAWORK_STATE" | grep -oP '"reinforcement_count"[[:space:]]*:[[:space:]]*\K[0-9]+' 2>/dev/null) || REINFORCE_COUNT=0
139
- fi
140
- NEW_COUNT=$((REINFORCE_COUNT + 1))
141
-
142
- # Get original prompt (with fallback)
143
- ORIGINAL_PROMPT=""
144
- if command -v jq &> /dev/null; then
145
- ORIGINAL_PROMPT=$(echo "$ULTRAWORK_STATE" | jq -r '.original_prompt // ""' 2>/dev/null)
146
- else
147
- ORIGINAL_PROMPT=$(echo "$ULTRAWORK_STATE" | grep -oP '"original_prompt"[[:space:]]*:[[:space:]]*"\K[^"]+' 2>/dev/null) || ORIGINAL_PROMPT=""
148
- fi
149
-
150
- # Update state file (best effort)
151
- if command -v jq &> /dev/null; then
152
- echo "$ULTRAWORK_STATE" | jq ".reinforcement_count = $NEW_COUNT | .last_checked_at = \"$(date -Iseconds)\"" > "$DIRECTORY/.omc/state/ultrawork-state.json" 2>/dev/null
153
- fi
154
-
155
- cat << EOF
156
- {"continue": false, "reason": "<ultrawork-persistence>\n\n[ULTRAWORK MODE STILL ACTIVE - Reinforcement #$NEW_COUNT]\n\nYour ultrawork session is NOT complete. $INCOMPLETE_COUNT incomplete todos remain.\n\nREMEMBER THE ULTRAWORK RULES:\n- **PARALLEL**: Fire independent calls simultaneously - NEVER wait sequentially\n- **BACKGROUND FIRST**: Use Task(run_in_background=true) for exploration (10+ concurrent)\n- **TODO**: Track EVERY step. Mark complete IMMEDIATELY after each\n- **VERIFY**: Check ALL requirements met before done\n- **NO Premature Stopping**: ALL TODOs must be complete\n\nContinue working on the next pending task. DO NOT STOP until all tasks are marked complete.\n\nOriginal task: $ORIGINAL_PROMPT\n\n</ultrawork-persistence>\n\n---\n"}
157
- EOF
158
- exit 0
159
- fi
160
- fi
161
-
162
- # Priority 3: Todo Continuation (baseline)
163
- if [ "$INCOMPLETE_COUNT" -gt 0 ]; then
164
- cat << EOF
165
- {"continue": false, "reason": "<todo-continuation>\n\n[SYSTEM REMINDER - TODO CONTINUATION]\n\nIncomplete tasks remain in your todo list ($INCOMPLETE_COUNT remaining). Continue working on the next pending task.\n\n- Proceed without asking for permission\n- Mark each task complete when finished\n- Do not stop until all tasks are done\n\n</todo-continuation>\n\n---\n"}
166
- EOF
167
- exit 0
168
- fi
169
-
170
- # No blocking needed
171
- echo '{"continue": true}'
172
- exit 0
@@ -1,62 +0,0 @@
1
- #!/bin/bash
2
- # Sisyphus Session Start Hook
3
- # Restores persistent mode states and injects context when session starts
4
-
5
- # Read stdin
6
- INPUT=$(cat)
7
-
8
- # Get directory
9
- DIRECTORY=""
10
- if command -v jq &> /dev/null; then
11
- DIRECTORY=$(echo "$INPUT" | jq -r '.directory // ""' 2>/dev/null)
12
- fi
13
-
14
- if [ -z "$DIRECTORY" ]; then
15
- DIRECTORY=$(pwd)
16
- fi
17
-
18
- MESSAGES=""
19
-
20
- # Check for active ultrawork state
21
- if [ -f "$DIRECTORY/.omc/state/ultrawork-state.json" ] || [ -f "$HOME/.omc/state/ultrawork-state.json" ]; then
22
- if [ -f "$DIRECTORY/.omc/state/ultrawork-state.json" ]; then
23
- ULTRAWORK_STATE=$(cat "$DIRECTORY/.omc/state/ultrawork-state.json" 2>/dev/null)
24
- else
25
- ULTRAWORK_STATE=$(cat "$HOME/.omc/state/ultrawork-state.json" 2>/dev/null)
26
- fi
27
-
28
- IS_ACTIVE=$(echo "$ULTRAWORK_STATE" | jq -r '.active // false' 2>/dev/null)
29
- if [ "$IS_ACTIVE" = "true" ]; then
30
- STARTED_AT=$(echo "$ULTRAWORK_STATE" | jq -r '.started_at // ""' 2>/dev/null)
31
- PROMPT=$(echo "$ULTRAWORK_STATE" | jq -r '.original_prompt // ""' 2>/dev/null)
32
- MESSAGES="$MESSAGES<session-restore>\n\n[ULTRAWORK MODE RESTORED]\n\nYou have an active ultrawork session from $STARTED_AT.\nOriginal task: $PROMPT\n\nContinue working in ultrawork mode until all tasks are complete.\n\n</session-restore>\n\n---\n\n"
33
- fi
34
- fi
35
-
36
- # Check for incomplete todos
37
- INCOMPLETE_COUNT=0
38
- TODOS_DIR="$HOME/.claude/todos"
39
- if [ -d "$TODOS_DIR" ]; then
40
- for todo_file in "$TODOS_DIR"/*.json; do
41
- if [ -f "$todo_file" ]; then
42
- if command -v jq &> /dev/null; then
43
- COUNT=$(jq '[.[] | select(.status != "completed" and .status != "cancelled")] | length' "$todo_file" 2>/dev/null || echo "0")
44
- INCOMPLETE_COUNT=$((INCOMPLETE_COUNT + COUNT))
45
- fi
46
- fi
47
- done
48
- fi
49
-
50
- if [ "$INCOMPLETE_COUNT" -gt 0 ]; then
51
- MESSAGES="$MESSAGES<session-restore>\n\n[PENDING TASKS DETECTED]\n\nYou have $INCOMPLETE_COUNT incomplete tasks from a previous session.\nPlease continue working on these tasks.\n\n</session-restore>\n\n---\n\n"
52
- fi
53
-
54
- # Output message if we have any
55
- if [ -n "$MESSAGES" ]; then
56
- # Escape for JSON
57
- MESSAGES_ESCAPED=$(echo "$MESSAGES" | sed 's/"/\\"/g')
58
- echo "{\"continue\": true, \"message\": \"$MESSAGES_ESCAPED\"}"
59
- else
60
- echo '{"continue": true}'
61
- fi
62
- exit 0