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.
- package/README.md +101 -24
- package/dist/__tests__/hooks.test.js +255 -1
- package/dist/__tests__/hooks.test.js.map +1 -1
- package/dist/__tests__/installer.test.js +1 -1
- package/dist/__tests__/notepad.test.d.ts +2 -0
- package/dist/__tests__/notepad.test.d.ts.map +1 -0
- package/dist/__tests__/notepad.test.js +374 -0
- package/dist/__tests__/notepad.test.js.map +1 -0
- package/dist/__tests__/ralph-prd.test.d.ts +2 -0
- package/dist/__tests__/ralph-prd.test.d.ts.map +1 -0
- package/dist/__tests__/ralph-prd.test.js +308 -0
- package/dist/__tests__/ralph-prd.test.js.map +1 -0
- package/dist/__tests__/ralph-progress.test.d.ts +2 -0
- package/dist/__tests__/ralph-progress.test.d.ts.map +1 -0
- package/dist/__tests__/ralph-progress.test.js +312 -0
- package/dist/__tests__/ralph-progress.test.js.map +1 -0
- package/dist/__tests__/skills.test.js +5 -3
- package/dist/__tests__/skills.test.js.map +1 -1
- package/dist/agents/definitions.d.ts +4 -0
- package/dist/agents/definitions.d.ts.map +1 -1
- package/dist/agents/definitions.js +147 -3
- package/dist/agents/definitions.js.map +1 -1
- package/dist/agents/index.d.ts +1 -0
- package/dist/agents/index.d.ts.map +1 -1
- package/dist/agents/index.js +2 -0
- package/dist/agents/index.js.map +1 -1
- package/dist/agents/prometheus.js +2 -2
- package/dist/agents/prometheus.js.map +1 -1
- package/dist/features/builtin-skills/skills.d.ts.map +1 -1
- package/dist/features/builtin-skills/skills.js +61 -0
- package/dist/features/builtin-skills/skills.js.map +1 -1
- package/dist/features/magic-keywords.js +1 -1
- package/dist/hooks/index.d.ts +5 -1
- package/dist/hooks/index.d.ts.map +1 -1
- package/dist/hooks/index.js +15 -1
- package/dist/hooks/index.js.map +1 -1
- package/dist/hooks/notepad/index.d.ts +114 -0
- package/dist/hooks/notepad/index.d.ts.map +1 -0
- package/dist/hooks/notepad/index.js +372 -0
- package/dist/hooks/notepad/index.js.map +1 -0
- package/dist/hooks/persistent-mode/index.d.ts +5 -0
- package/dist/hooks/persistent-mode/index.d.ts.map +1 -1
- package/dist/hooks/persistent-mode/index.js +71 -5
- package/dist/hooks/persistent-mode/index.js.map +1 -1
- package/dist/hooks/ralph-loop/index.d.ts +48 -0
- package/dist/hooks/ralph-loop/index.d.ts.map +1 -1
- package/dist/hooks/ralph-loop/index.js +127 -0
- package/dist/hooks/ralph-loop/index.js.map +1 -1
- package/dist/hooks/ralph-prd/index.d.ts +130 -0
- package/dist/hooks/ralph-prd/index.d.ts.map +1 -0
- package/dist/hooks/ralph-prd/index.js +310 -0
- package/dist/hooks/ralph-prd/index.js.map +1 -0
- package/dist/hooks/ralph-progress/index.d.ts +102 -0
- package/dist/hooks/ralph-progress/index.d.ts.map +1 -0
- package/dist/hooks/ralph-progress/index.js +408 -0
- package/dist/hooks/ralph-progress/index.js.map +1 -0
- package/dist/hooks/sisyphus-orchestrator/index.d.ts.map +1 -1
- package/dist/hooks/sisyphus-orchestrator/index.js +26 -0
- package/dist/hooks/sisyphus-orchestrator/index.js.map +1 -1
- package/dist/hooks/ultraqa-loop/index.d.ts +94 -0
- package/dist/hooks/ultraqa-loop/index.d.ts.map +1 -0
- package/dist/hooks/ultraqa-loop/index.js +216 -0
- package/dist/hooks/ultraqa-loop/index.js.map +1 -0
- package/dist/installer/hooks.d.ts +28 -0
- package/dist/installer/hooks.d.ts.map +1 -1
- package/dist/installer/hooks.js +262 -2
- package/dist/installer/hooks.js.map +1 -1
- package/dist/installer/index.d.ts +1 -1
- package/dist/installer/index.d.ts.map +1 -1
- package/dist/installer/index.js +426 -12
- package/dist/installer/index.js.map +1 -1
- package/package.json +1 -1
- package/scripts/persistent-mode.mjs +187 -7
- package/scripts/post-tool-verifier.mjs +62 -1
- package/scripts/session-start.mjs +22 -0
- package/scripts/test-max-attempts.ts +94 -0
- package/scripts/test-mutual-exclusion.ts +152 -0
- package/scripts/test-notepad-integration.ts +495 -0
- package/scripts/test-remember-tags.ts +121 -0
- 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
|
-
//
|
|
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 ||
|
|
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 || '
|
|
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
|
+
});
|