spec-and-loop 2.0.1 → 2.1.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.
- package/lib/mini-ralph/errors.js +48 -6
- package/lib/mini-ralph/invoker.js +24 -7
- package/lib/mini-ralph/runner.js +42 -33
- package/lib/mini-ralph/status.js +20 -7
- package/package.json +1 -1
package/lib/mini-ralph/errors.js
CHANGED
|
@@ -10,11 +10,7 @@ function errorsPath(ralphDir) {
|
|
|
10
10
|
}
|
|
11
11
|
|
|
12
12
|
function read(ralphDir, limit) {
|
|
13
|
-
const
|
|
14
|
-
if (!fs.existsSync(file)) return '';
|
|
15
|
-
const content = fs.readFileSync(file, 'utf8').trim();
|
|
16
|
-
if (!content) return '';
|
|
17
|
-
const entries = content.split(/^---$/m).filter(e => e.trim());
|
|
13
|
+
const entries = _readRawEntries(ralphDir);
|
|
18
14
|
if (!entries.length) return '';
|
|
19
15
|
if (limit !== undefined && limit < entries.length) {
|
|
20
16
|
return entries.slice(-limit).join('\n---\n');
|
|
@@ -22,6 +18,23 @@ function read(ralphDir, limit) {
|
|
|
22
18
|
return entries.join('\n---\n');
|
|
23
19
|
}
|
|
24
20
|
|
|
21
|
+
function readEntries(ralphDir, limit) {
|
|
22
|
+
const entries = _readRawEntries(ralphDir).map(_parseEntry).filter(Boolean);
|
|
23
|
+
if (limit !== undefined && limit < entries.length) {
|
|
24
|
+
return entries.slice(-limit);
|
|
25
|
+
}
|
|
26
|
+
return entries;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function count(ralphDir) {
|
|
30
|
+
return _readRawEntries(ralphDir).length;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function latest(ralphDir) {
|
|
34
|
+
const entries = readEntries(ralphDir, 1);
|
|
35
|
+
return entries.length > 0 ? entries[0] : null;
|
|
36
|
+
}
|
|
37
|
+
|
|
25
38
|
function append(ralphDir, entry) {
|
|
26
39
|
_ensureDir(ralphDir);
|
|
27
40
|
const file = errorsPath(ralphDir);
|
|
@@ -67,4 +80,33 @@ function _ensureDir(ralphDir) {
|
|
|
67
80
|
}
|
|
68
81
|
}
|
|
69
82
|
|
|
70
|
-
|
|
83
|
+
function _readRawEntries(ralphDir) {
|
|
84
|
+
const file = errorsPath(ralphDir);
|
|
85
|
+
if (!fs.existsSync(file)) return [];
|
|
86
|
+
const content = fs.readFileSync(file, 'utf8').trim();
|
|
87
|
+
if (!content) return [];
|
|
88
|
+
return content.split(/^---$/m).filter((entry) => entry.trim());
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function _parseEntry(entry) {
|
|
92
|
+
const trimmed = entry.trim();
|
|
93
|
+
if (!trimmed) return null;
|
|
94
|
+
|
|
95
|
+
const timestampMatch = trimmed.match(/^Timestamp: (.+)$/m);
|
|
96
|
+
const iterationMatch = trimmed.match(/^Iteration: (.+)$/m);
|
|
97
|
+
const taskMatch = trimmed.match(/^Task: (.+)$/m);
|
|
98
|
+
const exitCodeMatch = trimmed.match(/^Exit Code: (.+)$/m);
|
|
99
|
+
const stderrMatch = trimmed.match(/(?:^|\n)### stderr\n([\s\S]*?)(?=\n### stdout\n|$)/);
|
|
100
|
+
const stdoutMatch = trimmed.match(/(?:^|\n)### stdout\n([\s\S]*?)$/);
|
|
101
|
+
|
|
102
|
+
return {
|
|
103
|
+
timestamp: timestampMatch ? timestampMatch[1].trim() : '',
|
|
104
|
+
iteration: iterationMatch ? Number(iterationMatch[1].trim()) : NaN,
|
|
105
|
+
task: taskMatch ? taskMatch[1].trim() : '',
|
|
106
|
+
exitCode: exitCodeMatch ? Number(exitCodeMatch[1].trim()) : NaN,
|
|
107
|
+
stderr: stderrMatch ? stderrMatch[1].trim() : '',
|
|
108
|
+
stdout: stdoutMatch ? stdoutMatch[1].trim() : '',
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
module.exports = { errorsPath, read, readEntries, count, latest, append, clear, archive };
|
|
@@ -127,13 +127,30 @@ function _spawnOpenCode(args, verbose) {
|
|
|
127
127
|
function _looksLikeCliHelp(text) {
|
|
128
128
|
if (!text) return false;
|
|
129
129
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
130
|
+
// Only inspect the opening portion of the transcript so help-like strings
|
|
131
|
+
// echoed later in diffs or test output do not masquerade as a CLI banner.
|
|
132
|
+
const normalized = text
|
|
133
|
+
.replace(/\u001b\[[0-?]*[ -/]*[@-~]/g, '')
|
|
134
|
+
.replace(/\r/g, '');
|
|
135
|
+
const lines = normalized
|
|
136
|
+
.split('\n')
|
|
137
|
+
.map((line) => line.trim())
|
|
138
|
+
.filter(Boolean);
|
|
139
|
+
const openingText = lines.slice(0, 40).join('\n');
|
|
140
|
+
|
|
141
|
+
const looksLikeRunHelp =
|
|
142
|
+
openingText.includes('opencode run [message..]') &&
|
|
143
|
+
openingText.includes('run opencode with a message') &&
|
|
144
|
+
openingText.includes('Positionals:') &&
|
|
145
|
+
openingText.includes('Options:');
|
|
146
|
+
|
|
147
|
+
const looksLikeTopLevelHelp =
|
|
148
|
+
openingText.includes('Commands:') &&
|
|
149
|
+
openingText.includes('Options:') &&
|
|
150
|
+
openingText.includes('opencode [project]') &&
|
|
151
|
+
openingText.includes('opencode run [message..]');
|
|
152
|
+
|
|
153
|
+
return looksLikeRunHelp || looksLikeTopLevelHelp;
|
|
137
154
|
}
|
|
138
155
|
|
|
139
156
|
/**
|
package/lib/mini-ralph/runner.js
CHANGED
|
@@ -94,8 +94,8 @@ async function run(opts) {
|
|
|
94
94
|
|
|
95
95
|
// Build the prompt for this iteration
|
|
96
96
|
const renderedPrompt = await prompt.render(options, iterationCount);
|
|
97
|
-
const
|
|
98
|
-
const iterationFeedback = _buildIterationFeedback(history.recent(ralphDir, 3),
|
|
97
|
+
const errorEntries = errors.readEntries(ralphDir, 3);
|
|
98
|
+
const iterationFeedback = _buildIterationFeedback(history.recent(ralphDir, 3), errorEntries);
|
|
99
99
|
|
|
100
100
|
// Inject any pending context
|
|
101
101
|
const pendingContext = context.consume(ralphDir);
|
|
@@ -187,17 +187,16 @@ async function run(opts) {
|
|
|
187
187
|
}
|
|
188
188
|
}
|
|
189
189
|
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
process.stderr.write(`[mini-ralph] errors archived to ${archivePath}\n`);
|
|
190
|
+
try {
|
|
191
|
+
if (completed) {
|
|
192
|
+
_cleanupCompletedErrors(ralphDir, options.verbose);
|
|
194
193
|
}
|
|
195
|
-
|
|
194
|
+
} finally {
|
|
195
|
+
// A completed loop must not remain marked active because post-completion
|
|
196
|
+
// cleanup failed.
|
|
197
|
+
state.update(ralphDir, { active: false, completedAt: new Date().toISOString() });
|
|
196
198
|
}
|
|
197
199
|
|
|
198
|
-
// Mark loop as inactive
|
|
199
|
-
state.update(ralphDir, { active: false, completedAt: new Date().toISOString() });
|
|
200
|
-
|
|
201
200
|
return { completed, iterations: iterationCount, exitReason };
|
|
202
201
|
}
|
|
203
202
|
|
|
@@ -342,7 +341,7 @@ function _formatAutoCommitMessage(iteration, completedTasks) {
|
|
|
342
341
|
* @param {Array<object>} recentHistory
|
|
343
342
|
* @returns {string}
|
|
344
343
|
*/
|
|
345
|
-
function _buildIterationFeedback(recentHistory,
|
|
344
|
+
function _buildIterationFeedback(recentHistory, errorEntries) {
|
|
346
345
|
if (!Array.isArray(recentHistory) || recentHistory.length === 0) {
|
|
347
346
|
return '';
|
|
348
347
|
}
|
|
@@ -367,8 +366,8 @@ function _buildIterationFeedback(recentHistory, errorContent) {
|
|
|
367
366
|
if (issues.length > 0) {
|
|
368
367
|
let line = `- Iteration ${entry.iteration}: ${issues.join('; ')}.`;
|
|
369
368
|
|
|
370
|
-
if (entry.exitCode !== 0 &&
|
|
371
|
-
const errorDetails = _extractErrorForIteration(
|
|
369
|
+
if (entry.exitCode !== 0 && errorEntries) {
|
|
370
|
+
const errorDetails = _extractErrorForIteration(errorEntries, entry.iteration);
|
|
372
371
|
if (errorDetails) {
|
|
373
372
|
line += '\n Error output:';
|
|
374
373
|
if (errorDetails.stderr) {
|
|
@@ -394,30 +393,19 @@ function _buildIterationFeedback(recentHistory, errorContent) {
|
|
|
394
393
|
].join('\n');
|
|
395
394
|
}
|
|
396
395
|
|
|
397
|
-
function _extractErrorForIteration(
|
|
398
|
-
if (!
|
|
399
|
-
|
|
400
|
-
const entries = errorContent.split(/^---$/m).filter((e) => e.trim());
|
|
401
|
-
|
|
402
|
-
for (const entry of entries) {
|
|
403
|
-
if (!entry.includes(`Iteration: ${iteration}`)) continue;
|
|
396
|
+
function _extractErrorForIteration(errorEntries, iteration) {
|
|
397
|
+
if (!Array.isArray(errorEntries) || errorEntries.length === 0) return null;
|
|
404
398
|
|
|
405
|
-
|
|
406
|
-
|
|
399
|
+
const match = errorEntries.find((entry) => entry.iteration === iteration);
|
|
400
|
+
if (!match) return null;
|
|
407
401
|
|
|
408
|
-
|
|
409
|
-
|
|
402
|
+
let stderr = match.stderr || '';
|
|
403
|
+
let stdout = match.stdout || '';
|
|
410
404
|
|
|
411
|
-
|
|
412
|
-
|
|
405
|
+
if (stderr.length > 2000) stderr = stderr.substring(0, 2000) + '...';
|
|
406
|
+
if (stdout.length > 500) stdout = stdout.substring(0, 500) + '...';
|
|
413
407
|
|
|
414
|
-
|
|
415
|
-
if (stdout.length > 500) stdout = stdout.substring(0, 500) + '...';
|
|
416
|
-
|
|
417
|
-
return { stderr, stdout };
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
return null;
|
|
408
|
+
return { stderr, stdout };
|
|
421
409
|
}
|
|
422
410
|
|
|
423
411
|
function _getCurrentTaskDescription(tasksBefore) {
|
|
@@ -427,6 +415,26 @@ function _getCurrentTaskDescription(tasksBefore) {
|
|
|
427
415
|
return 'N/A';
|
|
428
416
|
}
|
|
429
417
|
|
|
418
|
+
function _cleanupCompletedErrors(ralphDir, verbose) {
|
|
419
|
+
let archivePath = null;
|
|
420
|
+
|
|
421
|
+
try {
|
|
422
|
+
archivePath = errors.archive(ralphDir);
|
|
423
|
+
if (archivePath && verbose) {
|
|
424
|
+
process.stderr.write(`[mini-ralph] errors archived to ${archivePath}\n`);
|
|
425
|
+
}
|
|
426
|
+
} catch (err) {
|
|
427
|
+
process.stderr.write(`[mini-ralph] warning: failed to archive error history: ${err.message}\n`);
|
|
428
|
+
return;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
try {
|
|
432
|
+
errors.clear(ralphDir);
|
|
433
|
+
} catch (err) {
|
|
434
|
+
process.stderr.write(`[mini-ralph] warning: failed to clear active error history: ${err.message}\n`);
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
430
438
|
function _taskIdentity(task) {
|
|
431
439
|
return task.number
|
|
432
440
|
? `${task.number}|${task.fullDescription || task.description}`
|
|
@@ -483,4 +491,5 @@ module.exports = {
|
|
|
483
491
|
_buildIterationFeedback,
|
|
484
492
|
_extractErrorForIteration,
|
|
485
493
|
_getCurrentTaskDescription,
|
|
494
|
+
_cleanupCompletedErrors,
|
|
486
495
|
};
|
package/lib/mini-ralph/status.js
CHANGED
|
@@ -101,14 +101,13 @@ function render(ralphDir, tasksFile) {
|
|
|
101
101
|
}
|
|
102
102
|
|
|
103
103
|
// Error history
|
|
104
|
-
const
|
|
105
|
-
if (
|
|
106
|
-
const
|
|
107
|
-
const
|
|
108
|
-
const preview = entries[entries.length - 1].substring(0, 200).trim();
|
|
104
|
+
const errorCount = errors.count(ralphDir);
|
|
105
|
+
if (errorCount > 0) {
|
|
106
|
+
const latestError = errors.latest(ralphDir);
|
|
107
|
+
const preview = _formatErrorPreview(latestError);
|
|
109
108
|
lines.push('');
|
|
110
109
|
lines.push('--- Error History ---');
|
|
111
|
-
lines.push(` Errors: ${
|
|
110
|
+
lines.push(` Errors: ${errorCount}`);
|
|
112
111
|
lines.push(` Most recent: ${preview}`);
|
|
113
112
|
lines.push('-'.repeat(50));
|
|
114
113
|
}
|
|
@@ -222,4 +221,18 @@ function _detectStruggles(recentHistory) {
|
|
|
222
221
|
return warnings;
|
|
223
222
|
}
|
|
224
223
|
|
|
225
|
-
|
|
224
|
+
function _formatErrorPreview(entry) {
|
|
225
|
+
if (!entry) return '';
|
|
226
|
+
|
|
227
|
+
const source = entry.stderr || entry.stdout || _fallbackErrorPreview(entry);
|
|
228
|
+
return source.substring(0, 200).trim();
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
function _fallbackErrorPreview(entry) {
|
|
232
|
+
const parts = [];
|
|
233
|
+
if (entry.task) parts.push(entry.task);
|
|
234
|
+
if (!Number.isNaN(entry.exitCode)) parts.push(`exit code ${entry.exitCode}`);
|
|
235
|
+
return parts.join(' | ');
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
module.exports = { render, _elapsed, _detectStruggles, _formatToolUsage, _formatErrorPreview };
|