oh-my-claude-sisyphus 2.5.0 → 2.6.0

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/dist/__tests__/hooks.test.js +255 -1
  2. package/dist/__tests__/hooks.test.js.map +1 -1
  3. package/dist/__tests__/installer.test.js +1 -1
  4. package/dist/__tests__/notepad.test.d.ts +2 -0
  5. package/dist/__tests__/notepad.test.d.ts.map +1 -0
  6. package/dist/__tests__/notepad.test.js +374 -0
  7. package/dist/__tests__/notepad.test.js.map +1 -0
  8. package/dist/__tests__/ralph-prd.test.d.ts +2 -0
  9. package/dist/__tests__/ralph-prd.test.d.ts.map +1 -0
  10. package/dist/__tests__/ralph-prd.test.js +308 -0
  11. package/dist/__tests__/ralph-prd.test.js.map +1 -0
  12. package/dist/__tests__/ralph-progress.test.d.ts +2 -0
  13. package/dist/__tests__/ralph-progress.test.d.ts.map +1 -0
  14. package/dist/__tests__/ralph-progress.test.js +312 -0
  15. package/dist/__tests__/ralph-progress.test.js.map +1 -0
  16. package/dist/__tests__/skills.test.js +5 -3
  17. package/dist/__tests__/skills.test.js.map +1 -1
  18. package/dist/agents/definitions.d.ts +4 -0
  19. package/dist/agents/definitions.d.ts.map +1 -1
  20. package/dist/agents/definitions.js +147 -3
  21. package/dist/agents/definitions.js.map +1 -1
  22. package/dist/agents/index.d.ts +1 -0
  23. package/dist/agents/index.d.ts.map +1 -1
  24. package/dist/agents/index.js +2 -0
  25. package/dist/agents/index.js.map +1 -1
  26. package/dist/agents/prometheus.js +2 -2
  27. package/dist/agents/prometheus.js.map +1 -1
  28. package/dist/cli/index.js +0 -0
  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 +167 -6
  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
@@ -9,6 +9,7 @@
9
9
  import { existsSync, readFileSync, writeFileSync, readdirSync } from 'fs';
10
10
  import { join } from 'path';
11
11
  import { homedir } from 'os';
12
+ import { pruneOldEntries } from '../dist/hooks/notepad/index.js';
12
13
 
13
14
  // Read all stdin
14
15
  async function readStdin() {
@@ -39,6 +40,77 @@ function writeJsonFile(path, data) {
39
40
  }
40
41
  }
41
42
 
43
+ // Read PRD and get status
44
+ function getPrdStatus(projectDir) {
45
+ // Check both root and .sisyphus for prd.json
46
+ const paths = [
47
+ join(projectDir, 'prd.json'),
48
+ join(projectDir, '.sisyphus', 'prd.json')
49
+ ];
50
+
51
+ for (const prdPath of paths) {
52
+ const prd = readJsonFile(prdPath);
53
+ if (prd && Array.isArray(prd.userStories)) {
54
+ const stories = prd.userStories;
55
+ const completed = stories.filter(s => s.passes === true);
56
+ const pending = stories.filter(s => s.passes !== true);
57
+ const sortedPending = [...pending].sort((a, b) => (a.priority || 999) - (b.priority || 999));
58
+
59
+ return {
60
+ hasPrd: true,
61
+ total: stories.length,
62
+ completed: completed.length,
63
+ pending: pending.length,
64
+ allComplete: pending.length === 0,
65
+ nextStory: sortedPending[0] || null,
66
+ incompleteIds: pending.map(s => s.id)
67
+ };
68
+ }
69
+ }
70
+
71
+ return { hasPrd: false, allComplete: false, nextStory: null };
72
+ }
73
+
74
+ // Read progress.txt patterns for context
75
+ function getProgressPatterns(projectDir) {
76
+ const paths = [
77
+ join(projectDir, 'progress.txt'),
78
+ join(projectDir, '.sisyphus', 'progress.txt')
79
+ ];
80
+
81
+ for (const progressPath of paths) {
82
+ if (existsSync(progressPath)) {
83
+ try {
84
+ const content = readFileSync(progressPath, 'utf-8');
85
+ const patterns = [];
86
+ let inPatterns = false;
87
+
88
+ for (const line of content.split('\n')) {
89
+ const trimmed = line.trim();
90
+ if (trimmed === '## Codebase Patterns') {
91
+ inPatterns = true;
92
+ continue;
93
+ }
94
+ if (trimmed === '---') {
95
+ inPatterns = false;
96
+ continue;
97
+ }
98
+ if (inPatterns && trimmed.startsWith('-')) {
99
+ const pattern = trimmed.slice(1).trim();
100
+ if (pattern && !pattern.includes('No patterns')) {
101
+ patterns.push(pattern);
102
+ }
103
+ }
104
+ }
105
+
106
+ return patterns;
107
+ } catch {}
108
+ }
109
+ }
110
+
111
+ return [];
112
+ }
113
+
42
114
  // Count incomplete todos
43
115
  function countIncompleteTodos(todosDir, projectDir) {
44
116
  let count = 0;
@@ -93,10 +165,33 @@ async function main() {
93
165
  // Count incomplete todos
94
166
  const incompleteCount = countIncompleteTodos(todosDir, directory);
95
167
 
96
- // Priority 1: Ralph Loop with Oracle Verification
168
+ // Check PRD status
169
+ const prdStatus = getPrdStatus(directory);
170
+ const progressPatterns = getProgressPatterns(directory);
171
+
172
+ // Priority 1: Ralph Loop with PRD and Oracle Verification
97
173
  if (ralphState?.active) {
98
174
  const iteration = ralphState.iteration || 1;
99
- const maxIter = ralphState.max_iterations || 10;
175
+ const maxIter = ralphState.max_iterations || 100; // Increased for PRD mode
176
+
177
+ // If PRD exists and all stories are complete, allow completion
178
+ if (prdStatus.hasPrd && prdStatus.allComplete) {
179
+ // Prune old notepad entries on clean session stop
180
+ try {
181
+ const pruneResult = pruneOldEntries(directory, 7);
182
+ if (pruneResult.pruned > 0) {
183
+ // Optionally log: console.error(`Pruned ${pruneResult.pruned} old notepad entries`);
184
+ }
185
+ } catch (err) {
186
+ // Silently ignore prune errors
187
+ }
188
+
189
+ console.log(JSON.stringify({
190
+ continue: true,
191
+ reason: `[RALPH LOOP COMPLETE - PRD] All ${prdStatus.total} stories are complete! Great work!`
192
+ }));
193
+ return;
194
+ }
100
195
 
101
196
  // Check if oracle verification is pending
102
197
  if (verificationState?.pending) {
@@ -148,6 +243,42 @@ ${verificationState.oracle_feedback}
148
243
  ralphState.iteration = newIter;
149
244
  writeJsonFile(join(directory, '.sisyphus', 'ralph-state.json'), ralphState);
150
245
 
246
+ // Build continuation message with PRD context if available
247
+ let prdContext = '';
248
+ if (prdStatus.hasPrd) {
249
+ prdContext = `
250
+ ## PRD STATUS
251
+ ${prdStatus.completed}/${prdStatus.total} stories complete. Remaining: ${prdStatus.incompleteIds.join(', ')}
252
+ `;
253
+ if (prdStatus.nextStory) {
254
+ prdContext += `
255
+ ## CURRENT STORY: ${prdStatus.nextStory.id} - ${prdStatus.nextStory.title}
256
+
257
+ ${prdStatus.nextStory.description || ''}
258
+
259
+ **Acceptance Criteria:**
260
+ ${(prdStatus.nextStory.acceptanceCriteria || []).map((c, i) => `${i + 1}. ${c}`).join('\n')}
261
+
262
+ **Instructions:**
263
+ 1. Implement this story completely
264
+ 2. Verify ALL acceptance criteria are met
265
+ 3. Run quality checks (tests, typecheck, lint)
266
+ 4. Update prd.json to set passes: true for ${prdStatus.nextStory.id}
267
+ 5. Append progress to progress.txt
268
+ 6. Move to next story or output completion promise
269
+ `;
270
+ }
271
+ }
272
+
273
+ // Add patterns from progress.txt
274
+ let patternsContext = '';
275
+ if (progressPatterns.length > 0) {
276
+ patternsContext = `
277
+ ## CODEBASE PATTERNS (from previous iterations)
278
+ ${progressPatterns.map(p => `- ${p}`).join('\n')}
279
+ `;
280
+ }
281
+
151
282
  console.log(JSON.stringify({
152
283
  continue: false,
153
284
  reason: `<ralph-loop-continuation>
@@ -155,12 +286,12 @@ ${verificationState.oracle_feedback}
155
286
  [RALPH LOOP - ITERATION ${newIter}/${maxIter}]
156
287
 
157
288
  Your previous attempt did not output the completion promise. The work is NOT done yet.
158
-
289
+ ${prdContext}${patternsContext}
159
290
  CRITICAL INSTRUCTIONS:
160
291
  1. Review your progress and the original task
161
- 2. Check your todo list - are ALL items marked complete?
292
+ 2. ${prdStatus.hasPrd ? 'Check prd.json - are ALL stories marked passes: true?' : 'Check your todo list - are ALL items marked complete?'}
162
293
  3. Continue from where you left off
163
- 4. When FULLY complete, output: <promise>${ralphState.completion_promise || 'TASK_COMPLETE'}</promise>
294
+ 4. When FULLY complete, output: <promise>${ralphState.completion_promise || 'DONE'}</promise>
164
295
  5. Do NOT stop until the task is truly done
165
296
 
166
297
  ${ralphState.prompt ? `Original task: ${ralphState.prompt}` : ''}
@@ -181,6 +312,16 @@ ${ralphState.prompt ? `Original task: ${ralphState.prompt}` : ''}
181
312
 
182
313
  // Escape mechanism: after max reinforcements, allow stopping
183
314
  if (newCount > maxReinforcements) {
315
+ // Prune old notepad entries on clean session stop
316
+ try {
317
+ const pruneResult = pruneOldEntries(directory, 7);
318
+ if (pruneResult.pruned > 0) {
319
+ // Optionally log: console.error(`Pruned ${pruneResult.pruned} old notepad entries`);
320
+ }
321
+ } catch (err) {
322
+ // Silently ignore prune errors
323
+ }
324
+
184
325
  console.log(JSON.stringify({
185
326
  continue: true,
186
327
  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 +374,16 @@ ${ultraworkState.original_prompt ? `Original task: ${ultraworkState.original_pro
233
374
 
234
375
  // Escape mechanism: after max continuations, allow stopping
235
376
  if (contState.count > maxContinuations) {
377
+ // Prune old notepad entries on clean session stop
378
+ try {
379
+ const pruneResult = pruneOldEntries(directory, 7);
380
+ if (pruneResult.pruned > 0) {
381
+ // Optionally log: console.error(`Pruned ${pruneResult.pruned} old notepad entries`);
382
+ }
383
+ } catch (err) {
384
+ // Silently ignore prune errors
385
+ }
386
+
236
387
  console.log(JSON.stringify({
237
388
  continue: true,
238
389
  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 +411,17 @@ Incomplete tasks remain in your todo list (${incompleteCount} remaining). Contin
260
411
  return;
261
412
  }
262
413
 
263
- // No blocking needed
414
+ // No blocking needed - clean session stop
415
+ // Prune old notepad entries on clean session stop
416
+ try {
417
+ const pruneResult = pruneOldEntries(directory, 7);
418
+ if (pruneResult.pruned > 0) {
419
+ // Optionally log: console.error(`Pruned ${pruneResult.pruned} old notepad entries`);
420
+ }
421
+ } catch (err) {
422
+ // Silently ignore prune errors
423
+ }
424
+
264
425
  console.log(JSON.stringify({ continue: true }));
265
426
  } catch (error) {
266
427
  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
+ });