oh-my-claude-sisyphus 2.5.0 → 2.6.1

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 (80) hide show
  1. package/README.md +101 -24
  2. package/dist/__tests__/hooks.test.js +255 -1
  3. package/dist/__tests__/hooks.test.js.map +1 -1
  4. package/dist/__tests__/installer.test.js +1 -1
  5. package/dist/__tests__/notepad.test.d.ts +2 -0
  6. package/dist/__tests__/notepad.test.d.ts.map +1 -0
  7. package/dist/__tests__/notepad.test.js +374 -0
  8. package/dist/__tests__/notepad.test.js.map +1 -0
  9. package/dist/__tests__/ralph-prd.test.d.ts +2 -0
  10. package/dist/__tests__/ralph-prd.test.d.ts.map +1 -0
  11. package/dist/__tests__/ralph-prd.test.js +308 -0
  12. package/dist/__tests__/ralph-prd.test.js.map +1 -0
  13. package/dist/__tests__/ralph-progress.test.d.ts +2 -0
  14. package/dist/__tests__/ralph-progress.test.d.ts.map +1 -0
  15. package/dist/__tests__/ralph-progress.test.js +312 -0
  16. package/dist/__tests__/ralph-progress.test.js.map +1 -0
  17. package/dist/__tests__/skills.test.js +5 -3
  18. package/dist/__tests__/skills.test.js.map +1 -1
  19. package/dist/agents/definitions.d.ts +4 -0
  20. package/dist/agents/definitions.d.ts.map +1 -1
  21. package/dist/agents/definitions.js +147 -3
  22. package/dist/agents/definitions.js.map +1 -1
  23. package/dist/agents/index.d.ts +1 -0
  24. package/dist/agents/index.d.ts.map +1 -1
  25. package/dist/agents/index.js +2 -0
  26. package/dist/agents/index.js.map +1 -1
  27. package/dist/agents/prometheus.js +2 -2
  28. package/dist/agents/prometheus.js.map +1 -1
  29. package/dist/features/builtin-skills/skills.d.ts.map +1 -1
  30. package/dist/features/builtin-skills/skills.js +61 -0
  31. package/dist/features/builtin-skills/skills.js.map +1 -1
  32. package/dist/features/magic-keywords.js +1 -1
  33. package/dist/hooks/index.d.ts +5 -1
  34. package/dist/hooks/index.d.ts.map +1 -1
  35. package/dist/hooks/index.js +15 -1
  36. package/dist/hooks/index.js.map +1 -1
  37. package/dist/hooks/notepad/index.d.ts +114 -0
  38. package/dist/hooks/notepad/index.d.ts.map +1 -0
  39. package/dist/hooks/notepad/index.js +372 -0
  40. package/dist/hooks/notepad/index.js.map +1 -0
  41. package/dist/hooks/persistent-mode/index.d.ts +5 -0
  42. package/dist/hooks/persistent-mode/index.d.ts.map +1 -1
  43. package/dist/hooks/persistent-mode/index.js +71 -5
  44. package/dist/hooks/persistent-mode/index.js.map +1 -1
  45. package/dist/hooks/ralph-loop/index.d.ts +48 -0
  46. package/dist/hooks/ralph-loop/index.d.ts.map +1 -1
  47. package/dist/hooks/ralph-loop/index.js +127 -0
  48. package/dist/hooks/ralph-loop/index.js.map +1 -1
  49. package/dist/hooks/ralph-prd/index.d.ts +130 -0
  50. package/dist/hooks/ralph-prd/index.d.ts.map +1 -0
  51. package/dist/hooks/ralph-prd/index.js +310 -0
  52. package/dist/hooks/ralph-prd/index.js.map +1 -0
  53. package/dist/hooks/ralph-progress/index.d.ts +102 -0
  54. package/dist/hooks/ralph-progress/index.d.ts.map +1 -0
  55. package/dist/hooks/ralph-progress/index.js +408 -0
  56. package/dist/hooks/ralph-progress/index.js.map +1 -0
  57. package/dist/hooks/sisyphus-orchestrator/index.d.ts.map +1 -1
  58. package/dist/hooks/sisyphus-orchestrator/index.js +26 -0
  59. package/dist/hooks/sisyphus-orchestrator/index.js.map +1 -1
  60. package/dist/hooks/ultraqa-loop/index.d.ts +94 -0
  61. package/dist/hooks/ultraqa-loop/index.d.ts.map +1 -0
  62. package/dist/hooks/ultraqa-loop/index.js +216 -0
  63. package/dist/hooks/ultraqa-loop/index.js.map +1 -0
  64. package/dist/installer/hooks.d.ts +28 -0
  65. package/dist/installer/hooks.d.ts.map +1 -1
  66. package/dist/installer/hooks.js +262 -2
  67. package/dist/installer/hooks.js.map +1 -1
  68. package/dist/installer/index.d.ts +1 -1
  69. package/dist/installer/index.d.ts.map +1 -1
  70. package/dist/installer/index.js +426 -12
  71. package/dist/installer/index.js.map +1 -1
  72. package/package.json +1 -1
  73. package/scripts/persistent-mode.mjs +187 -7
  74. package/scripts/post-tool-verifier.mjs +62 -1
  75. package/scripts/session-start.mjs +22 -0
  76. package/scripts/test-max-attempts.ts +94 -0
  77. package/scripts/test-mutual-exclusion.ts +152 -0
  78. package/scripts/test-notepad-integration.ts +495 -0
  79. package/scripts/test-remember-tags.ts +121 -0
  80. package/scripts/test-session-injection.ts +41 -0
@@ -7,8 +7,22 @@
7
7
  */
8
8
 
9
9
  import { existsSync, readFileSync, writeFileSync, readdirSync } from 'fs';
10
- import { join } from 'path';
10
+ import { join, dirname } from 'path';
11
11
  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
+ }
12
26
 
13
27
  // Read all stdin
14
28
  async function readStdin() {
@@ -39,6 +53,77 @@ function writeJsonFile(path, data) {
39
53
  }
40
54
  }
41
55
 
56
+ // Read PRD and get status
57
+ function getPrdStatus(projectDir) {
58
+ // Check both root and .sisyphus for prd.json
59
+ const paths = [
60
+ join(projectDir, 'prd.json'),
61
+ join(projectDir, '.sisyphus', 'prd.json')
62
+ ];
63
+
64
+ for (const prdPath of paths) {
65
+ const prd = readJsonFile(prdPath);
66
+ if (prd && Array.isArray(prd.userStories)) {
67
+ const stories = prd.userStories;
68
+ const completed = stories.filter(s => s.passes === true);
69
+ const pending = stories.filter(s => s.passes !== true);
70
+ const sortedPending = [...pending].sort((a, b) => (a.priority || 999) - (b.priority || 999));
71
+
72
+ return {
73
+ hasPrd: true,
74
+ total: stories.length,
75
+ completed: completed.length,
76
+ pending: pending.length,
77
+ allComplete: pending.length === 0,
78
+ nextStory: sortedPending[0] || null,
79
+ incompleteIds: pending.map(s => s.id)
80
+ };
81
+ }
82
+ }
83
+
84
+ return { hasPrd: false, allComplete: false, nextStory: null };
85
+ }
86
+
87
+ // Read progress.txt patterns for context
88
+ function getProgressPatterns(projectDir) {
89
+ const paths = [
90
+ join(projectDir, 'progress.txt'),
91
+ join(projectDir, '.sisyphus', 'progress.txt')
92
+ ];
93
+
94
+ for (const progressPath of paths) {
95
+ if (existsSync(progressPath)) {
96
+ try {
97
+ const content = readFileSync(progressPath, 'utf-8');
98
+ const patterns = [];
99
+ let inPatterns = false;
100
+
101
+ for (const line of content.split('\n')) {
102
+ const trimmed = line.trim();
103
+ if (trimmed === '## Codebase Patterns') {
104
+ inPatterns = true;
105
+ continue;
106
+ }
107
+ if (trimmed === '---') {
108
+ inPatterns = false;
109
+ continue;
110
+ }
111
+ if (inPatterns && trimmed.startsWith('-')) {
112
+ const pattern = trimmed.slice(1).trim();
113
+ if (pattern && !pattern.includes('No patterns')) {
114
+ patterns.push(pattern);
115
+ }
116
+ }
117
+ }
118
+
119
+ return patterns;
120
+ } catch {}
121
+ }
122
+ }
123
+
124
+ return [];
125
+ }
126
+
42
127
  // Count incomplete todos
43
128
  function countIncompleteTodos(todosDir, projectDir) {
44
129
  let count = 0;
@@ -93,10 +178,35 @@ async function main() {
93
178
  // Count incomplete todos
94
179
  const incompleteCount = countIncompleteTodos(todosDir, directory);
95
180
 
96
- // Priority 1: Ralph Loop with Oracle Verification
181
+ // Check PRD status
182
+ const prdStatus = getPrdStatus(directory);
183
+ const progressPatterns = getProgressPatterns(directory);
184
+
185
+ // Priority 1: Ralph Loop with PRD and Oracle Verification
97
186
  if (ralphState?.active) {
98
187
  const iteration = ralphState.iteration || 1;
99
- const maxIter = ralphState.max_iterations || 10;
188
+ const maxIter = ralphState.max_iterations || 100; // Increased for PRD mode
189
+
190
+ // If PRD exists and all stories are complete, allow completion
191
+ if (prdStatus.hasPrd && prdStatus.allComplete) {
192
+ // Prune old notepad entries on clean session stop
193
+ try {
194
+ if (pruneOldEntries) {
195
+ const pruneResult = pruneOldEntries(directory, 7);
196
+ if (pruneResult.pruned > 0) {
197
+ // Optionally log: console.error(`Pruned ${pruneResult.pruned} old notepad entries`);
198
+ }
199
+ }
200
+ } catch (err) {
201
+ // Silently ignore prune errors
202
+ }
203
+
204
+ console.log(JSON.stringify({
205
+ continue: true,
206
+ reason: `[RALPH LOOP COMPLETE - PRD] All ${prdStatus.total} stories are complete! Great work!`
207
+ }));
208
+ return;
209
+ }
100
210
 
101
211
  // Check if oracle verification is pending
102
212
  if (verificationState?.pending) {
@@ -148,6 +258,42 @@ ${verificationState.oracle_feedback}
148
258
  ralphState.iteration = newIter;
149
259
  writeJsonFile(join(directory, '.sisyphus', 'ralph-state.json'), ralphState);
150
260
 
261
+ // Build continuation message with PRD context if available
262
+ let prdContext = '';
263
+ if (prdStatus.hasPrd) {
264
+ prdContext = `
265
+ ## PRD STATUS
266
+ ${prdStatus.completed}/${prdStatus.total} stories complete. Remaining: ${prdStatus.incompleteIds.join(', ')}
267
+ `;
268
+ if (prdStatus.nextStory) {
269
+ prdContext += `
270
+ ## CURRENT STORY: ${prdStatus.nextStory.id} - ${prdStatus.nextStory.title}
271
+
272
+ ${prdStatus.nextStory.description || ''}
273
+
274
+ **Acceptance Criteria:**
275
+ ${(prdStatus.nextStory.acceptanceCriteria || []).map((c, i) => `${i + 1}. ${c}`).join('\n')}
276
+
277
+ **Instructions:**
278
+ 1. Implement this story completely
279
+ 2. Verify ALL acceptance criteria are met
280
+ 3. Run quality checks (tests, typecheck, lint)
281
+ 4. Update prd.json to set passes: true for ${prdStatus.nextStory.id}
282
+ 5. Append progress to progress.txt
283
+ 6. Move to next story or output completion promise
284
+ `;
285
+ }
286
+ }
287
+
288
+ // Add patterns from progress.txt
289
+ let patternsContext = '';
290
+ if (progressPatterns.length > 0) {
291
+ patternsContext = `
292
+ ## CODEBASE PATTERNS (from previous iterations)
293
+ ${progressPatterns.map(p => `- ${p}`).join('\n')}
294
+ `;
295
+ }
296
+
151
297
  console.log(JSON.stringify({
152
298
  continue: false,
153
299
  reason: `<ralph-loop-continuation>
@@ -155,12 +301,12 @@ ${verificationState.oracle_feedback}
155
301
  [RALPH LOOP - ITERATION ${newIter}/${maxIter}]
156
302
 
157
303
  Your previous attempt did not output the completion promise. The work is NOT done yet.
158
-
304
+ ${prdContext}${patternsContext}
159
305
  CRITICAL INSTRUCTIONS:
160
306
  1. Review your progress and the original task
161
- 2. Check your todo list - are ALL items marked complete?
307
+ 2. ${prdStatus.hasPrd ? 'Check prd.json - are ALL stories marked passes: true?' : 'Check your todo list - are ALL items marked complete?'}
162
308
  3. Continue from where you left off
163
- 4. When FULLY complete, output: <promise>${ralphState.completion_promise || 'TASK_COMPLETE'}</promise>
309
+ 4. When FULLY complete, output: <promise>${ralphState.completion_promise || 'DONE'}</promise>
164
310
  5. Do NOT stop until the task is truly done
165
311
 
166
312
  ${ralphState.prompt ? `Original task: ${ralphState.prompt}` : ''}
@@ -181,6 +327,18 @@ ${ralphState.prompt ? `Original task: ${ralphState.prompt}` : ''}
181
327
 
182
328
  // Escape mechanism: after max reinforcements, allow stopping
183
329
  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
+
184
342
  console.log(JSON.stringify({
185
343
  continue: true,
186
344
  reason: `[ULTRAWORK ESCAPE] Maximum reinforcements (${maxReinforcements}) reached. Allowing stop despite ${incompleteCount} incomplete todos. If tasks are genuinely stuck, consider cancelling them or asking the user for help.`
@@ -233,6 +391,18 @@ ${ultraworkState.original_prompt ? `Original task: ${ultraworkState.original_pro
233
391
 
234
392
  // Escape mechanism: after max continuations, allow stopping
235
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
+
236
406
  console.log(JSON.stringify({
237
407
  continue: true,
238
408
  reason: `[TODO ESCAPE] Maximum continuation attempts (${maxContinuations}) reached. Allowing stop despite ${incompleteCount} incomplete todos. Tasks may be stuck - consider reviewing and clearing them.`
@@ -260,7 +430,17 @@ Incomplete tasks remain in your todo list (${incompleteCount} remaining). Contin
260
430
  return;
261
431
  }
262
432
 
263
- // No blocking needed
433
+ // No blocking needed - clean session stop
434
+ // Prune old notepad entries on clean session stop
435
+ try {
436
+ const pruneResult = pruneOldEntries(directory, 7);
437
+ if (pruneResult.pruned > 0) {
438
+ // Optionally log: console.error(`Pruned ${pruneResult.pruned} old notepad entries`);
439
+ }
440
+ } catch (err) {
441
+ // Silently ignore prune errors
442
+ }
443
+
264
444
  console.log(JSON.stringify({ continue: true }));
265
445
  } catch (error) {
266
446
  console.log(JSON.stringify({ continue: true }));
@@ -7,8 +7,25 @@
7
7
  */
8
8
 
9
9
  import { existsSync, readFileSync, writeFileSync, mkdirSync } from 'fs';
10
- import { join } from 'path';
10
+ import { join, dirname } from 'path';
11
11
  import { homedir } from 'os';
12
+ import { fileURLToPath } from 'url';
13
+
14
+ // Get the directory of this script to resolve the dist module
15
+ const __filename = fileURLToPath(import.meta.url);
16
+ const __dirname = dirname(__filename);
17
+ const distDir = join(__dirname, '..', 'dist', 'hooks', 'notepad');
18
+
19
+ // Try to import notepad functions (may fail if not built)
20
+ let setPriorityContext = null;
21
+ let addWorkingMemoryEntry = null;
22
+ try {
23
+ const notepadModule = await import(join(distDir, 'index.js'));
24
+ setPriorityContext = notepadModule.setPriorityContext;
25
+ addWorkingMemoryEntry = notepadModule.addWorkingMemoryEntry;
26
+ } catch {
27
+ // Notepad module not available - remember tags will be silently ignored
28
+ }
12
29
 
13
30
  // State file for session tracking
14
31
  const STATE_FILE = join(homedir(), '.claude', '.session-stats.json');
@@ -102,6 +119,44 @@ function detectBackgroundOperation(output) {
102
119
  return bgPatterns.some(pattern => pattern.test(output));
103
120
  }
104
121
 
122
+ /**
123
+ * Process <remember> tags from agent output
124
+ * <remember>content</remember> -> Working Memory
125
+ * <remember priority>content</remember> -> Priority Context
126
+ */
127
+ function processRememberTags(output, directory) {
128
+ if (!setPriorityContext || !addWorkingMemoryEntry) {
129
+ return; // Notepad module not available
130
+ }
131
+
132
+ if (!output || !directory) {
133
+ return;
134
+ }
135
+
136
+ // Process priority remember tags first
137
+ const priorityRegex = /<remember\s+priority>([\s\S]*?)<\/remember>/gi;
138
+ let match;
139
+ while ((match = priorityRegex.exec(output)) !== null) {
140
+ const content = match[1].trim();
141
+ if (content) {
142
+ try {
143
+ setPriorityContext(directory, content);
144
+ } catch {}
145
+ }
146
+ }
147
+
148
+ // Process regular remember tags
149
+ const regularRegex = /<remember>([\s\S]*?)<\/remember>/gi;
150
+ while ((match = regularRegex.exec(output)) !== null) {
151
+ const content = match[1].trim();
152
+ if (content) {
153
+ try {
154
+ addWorkingMemoryEntry(directory, content);
155
+ } catch {}
156
+ }
157
+ }
158
+ }
159
+
105
160
  // Detect write failure
106
161
  function detectWriteFailure(output) {
107
162
  const errorPatterns = [
@@ -194,10 +249,16 @@ async function main() {
194
249
  const toolName = data.toolName || '';
195
250
  const toolOutput = data.toolOutput || '';
196
251
  const sessionId = data.sessionId || 'unknown';
252
+ const directory = data.directory || process.cwd();
197
253
 
198
254
  // Update session statistics
199
255
  const toolCount = updateStats(toolName, sessionId);
200
256
 
257
+ // Process <remember> tags from Task agent output
258
+ if (toolName === 'Task' || toolName === 'task') {
259
+ processRememberTags(toolOutput, directory);
260
+ }
261
+
201
262
  // Generate contextual message
202
263
  const message = generateMessage(toolName, toolOutput, sessionId, toolCount);
203
264
 
@@ -113,6 +113,28 @@ Please continue working on these tasks.
113
113
  `);
114
114
  }
115
115
 
116
+ // Check for notepad Priority Context
117
+ const notepadPath = join(directory, '.sisyphus', 'notepad.md');
118
+ if (existsSync(notepadPath)) {
119
+ try {
120
+ const notepadContent = readFileSync(notepadPath, 'utf-8');
121
+ const priorityMatch = notepadContent.match(/## Priority Context\n([\s\S]*?)(?=## |$)/);
122
+ if (priorityMatch && priorityMatch[1].trim()) {
123
+ const priorityContext = priorityMatch[1].trim();
124
+ // Only inject if there's actual content (not just the placeholder comment)
125
+ const cleanContent = priorityContext.replace(/<!--[\s\S]*?-->/g, '').trim();
126
+ if (cleanContent) {
127
+ messages.push(`<notepad-context>
128
+ [NOTEPAD - Priority Context]
129
+ ${cleanContent}
130
+ </notepad-context>`);
131
+ }
132
+ }
133
+ } catch (err) {
134
+ // Silently ignore notepad read errors
135
+ }
136
+ }
137
+
116
138
  if (messages.length > 0) {
117
139
  console.log(JSON.stringify({ continue: true, message: messages.join('\n') }));
118
140
  } else {
@@ -0,0 +1,94 @@
1
+ #!/usr/bin/env tsx
2
+ /**
3
+ * Test script for max-attempts counter in todo-continuation
4
+ *
5
+ * Tests the resetTodoContinuationAttempts functionality to verify
6
+ * that the counter tracking mechanism works correctly.
7
+ */
8
+
9
+ import { resetTodoContinuationAttempts, checkPersistentModes } from '../src/hooks/persistent-mode/index.js';
10
+
11
+ async function runTests() {
12
+ console.log('Testing max-attempts counter...\n');
13
+
14
+ let testsPassed = 0;
15
+ let testsFailed = 0;
16
+
17
+ // Test 1: Basic reset functionality
18
+ try {
19
+ console.log('Test 1: Basic reset (should not throw)');
20
+ resetTodoContinuationAttempts('test-session-1');
21
+ console.log('✓ PASS: resetTodoContinuationAttempts executed without error\n');
22
+ testsPassed++;
23
+ } catch (error) {
24
+ console.error('✗ FAIL: resetTodoContinuationAttempts threw error:', error);
25
+ testsFailed++;
26
+ }
27
+
28
+ // Test 2: Multiple resets on same session
29
+ try {
30
+ console.log('Test 2: Multiple resets on same session');
31
+ resetTodoContinuationAttempts('test-session-2');
32
+ resetTodoContinuationAttempts('test-session-2');
33
+ resetTodoContinuationAttempts('test-session-2');
34
+ console.log('✓ PASS: Multiple resets work correctly\n');
35
+ testsPassed++;
36
+ } catch (error) {
37
+ console.error('✗ FAIL: Multiple resets failed:', error);
38
+ testsFailed++;
39
+ }
40
+
41
+ // Test 3: Reset different sessions
42
+ try {
43
+ console.log('Test 3: Reset different sessions');
44
+ resetTodoContinuationAttempts('session-a');
45
+ resetTodoContinuationAttempts('session-b');
46
+ resetTodoContinuationAttempts('session-c');
47
+ console.log('✓ PASS: Can reset different sessions independently\n');
48
+ testsPassed++;
49
+ } catch (error) {
50
+ console.error('✗ FAIL: Different session resets failed:', error);
51
+ testsFailed++;
52
+ }
53
+
54
+ // Test 4: Indirect test via checkPersistentModes (no todos should not throw)
55
+ try {
56
+ console.log('Test 4: Indirect test via checkPersistentModes');
57
+ const result = await checkPersistentModes('test-session-indirect');
58
+ console.log(`✓ PASS: checkPersistentModes executed (shouldBlock=${result.shouldBlock}, mode=${result.mode})\n`);
59
+ testsPassed++;
60
+ } catch (error) {
61
+ console.error('✗ FAIL: checkPersistentModes threw error:', error);
62
+ testsFailed++;
63
+ }
64
+
65
+ // Test 5: Reset with empty string session ID
66
+ try {
67
+ console.log('Test 5: Reset with empty string session ID');
68
+ resetTodoContinuationAttempts('');
69
+ console.log('✓ PASS: Empty session ID handled correctly\n');
70
+ testsPassed++;
71
+ } catch (error) {
72
+ console.error('✗ FAIL: Empty session ID failed:', error);
73
+ testsFailed++;
74
+ }
75
+
76
+ // Summary
77
+ console.log('═══════════════════════════════════════');
78
+ console.log('SUMMARY');
79
+ console.log('═══════════════════════════════════════');
80
+ console.log(`Total tests: ${testsPassed + testsFailed}`);
81
+ console.log(`Passed: ${testsPassed}`);
82
+ console.log(`Failed: ${testsFailed}`);
83
+ console.log('═══════════════════════════════════════\n');
84
+
85
+ if (testsFailed === 0) {
86
+ console.log('✓ ALL TESTS PASSED');
87
+ process.exit(0);
88
+ } else {
89
+ console.log('✗ SOME TESTS FAILED');
90
+ process.exit(1);
91
+ }
92
+ }
93
+
94
+ runTests();
@@ -0,0 +1,152 @@
1
+ #!/usr/bin/env tsx
2
+
3
+ import { mkdtempSync, rmSync } from 'fs';
4
+ import { join } from 'path';
5
+ import { tmpdir } from 'os';
6
+ import { mkdirSync } from 'fs';
7
+
8
+ // Import the hooks
9
+ import { startUltraQA, clearUltraQAState, isRalphLoopActive } from '../src/hooks/ultraqa-loop/index.js';
10
+ import { createRalphLoopHook, clearRalphState, isUltraQAActive } from '../src/hooks/ralph-loop/index.js';
11
+
12
+ // Test utilities
13
+ function printTest(testName: string, passed: boolean) {
14
+ const status = passed ? '\x1b[32m✓ PASS\x1b[0m' : '\x1b[31m✗ FAIL\x1b[0m';
15
+ console.log(`${status} - ${testName}`);
16
+ }
17
+
18
+ async function runTests() {
19
+ console.log('\n=== Testing Mutual Exclusion Between UltraQA and Ralph Loop ===\n');
20
+
21
+ // Create temp directory with .sisyphus subfolder
22
+ const tempDir = mkdtempSync(join(tmpdir(), 'sisyphus-test-'));
23
+ const sisyphusDir = join(tempDir, '.sisyphus');
24
+ mkdirSync(sisyphusDir, { recursive: true });
25
+
26
+ console.log(`Using temp directory: ${tempDir}\n`);
27
+
28
+ let allTestsPassed = true;
29
+
30
+ try {
31
+ // Test 1: Start Ralph Loop, try to start UltraQA - should fail
32
+ console.log('Test 1: Ralph Loop blocks UltraQA');
33
+ console.log(' - Starting Ralph Loop...');
34
+
35
+ const ralphHook = createRalphLoopHook(tempDir);
36
+ const ralphStarted = ralphHook.startLoop(
37
+ 'test-session-1',
38
+ 'test task',
39
+ { maxIterations: 5, completionPromise: 'TASK_COMPLETE' }
40
+ );
41
+
42
+ if (!ralphStarted) {
43
+ console.log(' Failed to start Ralph Loop');
44
+ allTestsPassed = false;
45
+ }
46
+
47
+ console.log(' - Attempting to start UltraQA (should fail)...');
48
+ const ultraQAResult1 = startUltraQA(
49
+ tempDir,
50
+ 'all-tests-pass',
51
+ 'test-session-2'
52
+ );
53
+
54
+ if (ultraQAResult1.success) {
55
+ printTest('Test 1: UltraQA should be blocked by Ralph Loop', false);
56
+ allTestsPassed = false;
57
+ } else if (ultraQAResult1.error?.includes('Ralph Loop is active')) {
58
+ printTest('Test 1: UltraQA correctly blocked by Ralph Loop', true);
59
+ } else {
60
+ printTest('Test 1: UltraQA correctly blocked by Ralph Loop', false);
61
+ console.log(` Unexpected error: ${ultraQAResult1.error}`);
62
+ allTestsPassed = false;
63
+ }
64
+
65
+ // Clear Ralph state
66
+ console.log(' - Clearing Ralph state...\n');
67
+ clearRalphState(tempDir);
68
+
69
+ // Test 2: Start UltraQA, try to start Ralph Loop - should fail
70
+ console.log('Test 2: UltraQA blocks Ralph Loop');
71
+ console.log(' - Starting UltraQA...');
72
+
73
+ const ultraQAResult2 = startUltraQA(
74
+ tempDir,
75
+ 'all-tests-pass',
76
+ 'test-session-3'
77
+ );
78
+
79
+ if (!ultraQAResult2.success) {
80
+ console.log(` Failed to start UltraQA: ${ultraQAResult2.error}`);
81
+ allTestsPassed = false;
82
+ }
83
+
84
+ console.log(' - Attempting to start Ralph Loop (should fail)...');
85
+ const ralphHook2 = createRalphLoopHook(tempDir);
86
+ const ralphStarted2 = ralphHook2.startLoop(
87
+ 'test-session-4',
88
+ 'test task',
89
+ { maxIterations: 5, completionPromise: 'TASK_COMPLETE' }
90
+ );
91
+
92
+ if (ralphStarted2) {
93
+ printTest('Test 2: Ralph Loop should be blocked by UltraQA', false);
94
+ allTestsPassed = false;
95
+ } else {
96
+ // Check if it was blocked due to UltraQA
97
+ if (isUltraQAActive(tempDir)) {
98
+ printTest('Test 2: Ralph Loop correctly blocked by UltraQA', true);
99
+ } else {
100
+ printTest('Test 2: Ralph Loop correctly blocked by UltraQA', false);
101
+ console.log(` Ralph Loop failed but UltraQA is not active`);
102
+ allTestsPassed = false;
103
+ }
104
+ }
105
+
106
+ // Clear UltraQA state
107
+ console.log(' - Clearing UltraQA state...\n');
108
+ clearUltraQAState(tempDir);
109
+
110
+ // Test 3: Start UltraQA without any blockers - should succeed
111
+ console.log('Test 3: UltraQA starts without blockers');
112
+ console.log(' - Attempting to start UltraQA (should succeed)...');
113
+ const ultraQAResult3 = startUltraQA(
114
+ tempDir,
115
+ 'all-tests-pass',
116
+ 'test-session-5'
117
+ );
118
+
119
+ if (ultraQAResult3.success) {
120
+ printTest('Test 3: UltraQA starts successfully without blockers', true);
121
+ } else {
122
+ printTest('Test 3: UltraQA should start without blockers', false);
123
+ console.log(` Unexpected error: ${ultraQAResult3.error}`);
124
+ allTestsPassed = false;
125
+ }
126
+
127
+ // Final cleanup
128
+ console.log(' - Clearing UltraQA state...\n');
129
+ clearUltraQAState(tempDir);
130
+
131
+ } finally {
132
+ // Clean up temp directory
133
+ console.log(`Cleaning up temp directory: ${tempDir}`);
134
+ rmSync(tempDir, { recursive: true, force: true });
135
+ }
136
+
137
+ // Summary
138
+ console.log('\n=== Test Summary ===');
139
+ if (allTestsPassed) {
140
+ console.log('\x1b[32m✓ All tests passed!\x1b[0m\n');
141
+ process.exit(0);
142
+ } else {
143
+ console.log('\x1b[31m✗ Some tests failed\x1b[0m\n');
144
+ process.exit(1);
145
+ }
146
+ }
147
+
148
+ // Run tests
149
+ runTests().catch(error => {
150
+ console.error('\x1b[31mTest execution failed:\x1b[0m', error);
151
+ process.exit(1);
152
+ });