spec-and-loop 2.1.1 → 3.0.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/OPENSPEC-RALPH-BP.md +564 -0
- package/QUICKSTART.md +32 -10
- package/README.md +70 -6
- package/lib/mini-ralph/history.js +37 -0
- package/lib/mini-ralph/invoker.js +108 -7
- package/lib/mini-ralph/lessons.js +93 -0
- package/lib/mini-ralph/progress.js +404 -0
- package/lib/mini-ralph/prompt.js +78 -6
- package/lib/mini-ralph/runner.js +592 -33
- package/lib/mini-ralph/state.js +57 -5
- package/lib/mini-ralph/tasks.js +5 -10
- package/package.json +6 -5
- package/scripts/mini-ralph-cli.js +18 -2
- package/scripts/ralph-run.sh +402 -79
package/lib/mini-ralph/state.js
CHANGED
|
@@ -55,11 +55,27 @@ function init(ralphDir, data) {
|
|
|
55
55
|
function read(ralphDir) {
|
|
56
56
|
const file = statePath(ralphDir);
|
|
57
57
|
if (!fs.existsSync(file)) return null;
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
58
|
+
// One-shot retry to paper over the vanishingly-rare race where we read the
|
|
59
|
+
// state file between `openSync('wx')` on the temp file and the rename. On
|
|
60
|
+
// POSIX `renameSync` is atomic, so the retry window is only meaningful on
|
|
61
|
+
// filesystems where it is not -- but the cost of retrying is tiny, so we
|
|
62
|
+
// do it uniformly for robustness.
|
|
63
|
+
for (let attempt = 0; attempt < 2; attempt++) {
|
|
64
|
+
try {
|
|
65
|
+
const raw = fs.readFileSync(file, 'utf8');
|
|
66
|
+
if (!raw) {
|
|
67
|
+
if (attempt === 0) continue;
|
|
68
|
+
return null;
|
|
69
|
+
}
|
|
70
|
+
return JSON.parse(raw);
|
|
71
|
+
} catch (err) {
|
|
72
|
+
if (attempt === 0 && (err.code === 'ENOENT' || err instanceof SyntaxError)) {
|
|
73
|
+
continue;
|
|
74
|
+
}
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
62
77
|
}
|
|
78
|
+
return null;
|
|
63
79
|
}
|
|
64
80
|
|
|
65
81
|
/**
|
|
@@ -182,7 +198,43 @@ function _ensureDir(ralphDir) {
|
|
|
182
198
|
|
|
183
199
|
function _write(ralphDir, data) {
|
|
184
200
|
_ensureDir(ralphDir);
|
|
185
|
-
|
|
201
|
+
const target = statePath(ralphDir);
|
|
202
|
+
// Atomic write: serialize to a temp file in the same directory, then rename.
|
|
203
|
+
// A concurrent `read()` either sees the fully-written old file or the fully-
|
|
204
|
+
// written new file -- never a partially-written one. This matters because
|
|
205
|
+
// `ralph-run --status` can race with the live loop's per-iteration
|
|
206
|
+
// `state.update()` calls, and a torn read used to surface as JSON.parse
|
|
207
|
+
// errors or the dashboard reporting a stale iteration counter.
|
|
208
|
+
const tmp = `${target}.tmp-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
209
|
+
const serialized = JSON.stringify(data, null, 2);
|
|
210
|
+
|
|
211
|
+
let handle = null;
|
|
212
|
+
try {
|
|
213
|
+
handle = fs.openSync(tmp, 'wx');
|
|
214
|
+
fs.writeFileSync(handle, serialized, 'utf8');
|
|
215
|
+
fs.fsyncSync(handle);
|
|
216
|
+
} finally {
|
|
217
|
+
if (handle !== null) {
|
|
218
|
+
try {
|
|
219
|
+
fs.closeSync(handle);
|
|
220
|
+
} catch {
|
|
221
|
+
/* best-effort close */
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
try {
|
|
227
|
+
fs.renameSync(tmp, target);
|
|
228
|
+
} catch (err) {
|
|
229
|
+
// Clean up the temp file if rename failed, then rethrow so the caller
|
|
230
|
+
// sees the real error (disk full, permissions, etc.).
|
|
231
|
+
try {
|
|
232
|
+
fs.unlinkSync(tmp);
|
|
233
|
+
} catch {
|
|
234
|
+
/* best-effort cleanup */
|
|
235
|
+
}
|
|
236
|
+
throw err;
|
|
237
|
+
}
|
|
186
238
|
}
|
|
187
239
|
|
|
188
240
|
function _createLockToken() {
|
package/lib/mini-ralph/tasks.js
CHANGED
|
@@ -157,24 +157,19 @@ function taskContext(tasksFile) {
|
|
|
157
157
|
all.find((task) => task.status === 'in_progress') ||
|
|
158
158
|
all.find((task) => task.status === 'incomplete') ||
|
|
159
159
|
null;
|
|
160
|
-
const
|
|
160
|
+
const completedCount = all.filter((task) => task.status === 'completed').length;
|
|
161
|
+
const total = all.length;
|
|
161
162
|
|
|
162
163
|
const sections = [];
|
|
163
164
|
|
|
164
165
|
if (current) {
|
|
165
166
|
sections.push('## Current Task');
|
|
166
167
|
sections.push(`- ${current.fullDescription || current.description}`);
|
|
168
|
+
sections.push('');
|
|
167
169
|
}
|
|
168
170
|
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
sections.push('');
|
|
172
|
-
}
|
|
173
|
-
sections.push('## Completed Tasks for Git Commit');
|
|
174
|
-
sections.push(
|
|
175
|
-
...completed.map((task) => `- [x] ${task.fullDescription || task.description}`)
|
|
176
|
-
);
|
|
177
|
-
}
|
|
171
|
+
sections.push('## Progress');
|
|
172
|
+
sections.push(`- ${completedCount} of ${total} tasks complete`);
|
|
178
173
|
|
|
179
174
|
return sections.join('\n');
|
|
180
175
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "spec-and-loop",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.0",
|
|
4
4
|
"description": "OpenSpec + Ralph Loop integration for iterative development with opencode",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
@@ -17,13 +17,13 @@
|
|
|
17
17
|
"lint": "shellcheck scripts/*.sh"
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
|
-
"@fission-ai/openspec": "1.
|
|
21
|
-
"spec-and-loop": "^2.
|
|
20
|
+
"@fission-ai/openspec": "1.3.1",
|
|
21
|
+
"spec-and-loop": "^2.1.2"
|
|
22
22
|
},
|
|
23
23
|
"devDependencies": {
|
|
24
|
-
"@types/jest": "^
|
|
24
|
+
"@types/jest": "^30.0.0",
|
|
25
25
|
"bats": "^1.13.0",
|
|
26
|
-
"jest": "^
|
|
26
|
+
"jest": "^30.3.0"
|
|
27
27
|
},
|
|
28
28
|
"jest": {
|
|
29
29
|
"testEnvironment": "node",
|
|
@@ -63,6 +63,7 @@
|
|
|
63
63
|
"bin/",
|
|
64
64
|
"lib/",
|
|
65
65
|
"scripts/",
|
|
66
|
+
"OPENSPEC-RALPH-BP.md",
|
|
66
67
|
"README.md",
|
|
67
68
|
"QUICKSTART.md"
|
|
68
69
|
],
|
|
@@ -19,11 +19,13 @@
|
|
|
19
19
|
* --tasks Enable tasks mode
|
|
20
20
|
* --min-iterations <n> Minimum iterations (default: 1)
|
|
21
21
|
* --max-iterations <n> Maximum iterations (default: 50)
|
|
22
|
+
* --stall-threshold <n> Halt after N consecutive no-op iterations (default: 3; 0 disables)
|
|
22
23
|
* --completion-promise <s> Completion promise string (default: COMPLETE)
|
|
23
24
|
* --task-promise <s> Task promise string (default: READY_FOR_NEXT_TASK)
|
|
24
25
|
* --no-commit Suppress auto-commit
|
|
25
26
|
* --model <name> Optional model override
|
|
26
27
|
* --verbose Verbose output
|
|
28
|
+
* --quiet Suppress the per-iteration progress stream
|
|
27
29
|
* --status Print status dashboard and exit
|
|
28
30
|
* --add-context <text> Add pending context and exit
|
|
29
31
|
* --clear-context Clear pending context and exit
|
|
@@ -48,11 +50,13 @@ function parseArgs(argv) {
|
|
|
48
50
|
tasksMode: false,
|
|
49
51
|
minIterations: 1,
|
|
50
52
|
maxIterations: 50,
|
|
53
|
+
stallThreshold: 3,
|
|
51
54
|
completionPromise: 'COMPLETE',
|
|
52
55
|
taskPromise: 'READY_FOR_NEXT_TASK',
|
|
53
56
|
noCommit: false,
|
|
54
57
|
model: '',
|
|
55
58
|
verbose: false,
|
|
59
|
+
quiet: false,
|
|
56
60
|
// Control commands
|
|
57
61
|
status: false,
|
|
58
62
|
addContext: null,
|
|
@@ -88,6 +92,9 @@ function parseArgs(argv) {
|
|
|
88
92
|
case '--max-iterations':
|
|
89
93
|
opts.maxIterations = parseInt(args[++i], 10);
|
|
90
94
|
break;
|
|
95
|
+
case '--stall-threshold':
|
|
96
|
+
opts.stallThreshold = parseInt(args[++i], 10);
|
|
97
|
+
break;
|
|
91
98
|
case '--completion-promise':
|
|
92
99
|
opts.completionPromise = args[++i];
|
|
93
100
|
break;
|
|
@@ -103,6 +110,9 @@ function parseArgs(argv) {
|
|
|
103
110
|
case '--verbose':
|
|
104
111
|
opts.verbose = true;
|
|
105
112
|
break;
|
|
113
|
+
case '--quiet':
|
|
114
|
+
opts.quiet = true;
|
|
115
|
+
break;
|
|
106
116
|
case '--status':
|
|
107
117
|
opts.status = true;
|
|
108
118
|
break;
|
|
@@ -141,11 +151,13 @@ Options:
|
|
|
141
151
|
--tasks Enable tasks mode
|
|
142
152
|
--min-iterations <n> Minimum iterations (default: 1)
|
|
143
153
|
--max-iterations <n> Maximum iterations (default: 50)
|
|
154
|
+
--stall-threshold <n> Halt after N consecutive no-op iterations (default: 3; 0 disables)
|
|
144
155
|
--completion-promise <s> Completion promise string
|
|
145
156
|
--task-promise <s> Task promise string
|
|
146
157
|
--no-commit Suppress auto-commit
|
|
147
158
|
--model <name> Model override
|
|
148
159
|
--verbose Verbose output
|
|
160
|
+
--quiet Suppress the per-iteration progress stream
|
|
149
161
|
--status Print status dashboard and exit
|
|
150
162
|
--add-context <text> Add pending context and exit
|
|
151
163
|
--clear-context Clear pending context and exit
|
|
@@ -197,11 +209,13 @@ async function main() {
|
|
|
197
209
|
tasksMode: opts.tasksMode,
|
|
198
210
|
minIterations: opts.minIterations,
|
|
199
211
|
maxIterations: opts.maxIterations,
|
|
212
|
+
stallThreshold: opts.stallThreshold,
|
|
200
213
|
completionPromise: opts.completionPromise,
|
|
201
214
|
taskPromise: opts.taskPromise,
|
|
202
215
|
noCommit: opts.noCommit,
|
|
203
216
|
model: opts.model,
|
|
204
217
|
verbose: opts.verbose,
|
|
218
|
+
quiet: opts.quiet,
|
|
205
219
|
};
|
|
206
220
|
|
|
207
221
|
try {
|
|
@@ -213,13 +227,15 @@ async function main() {
|
|
|
213
227
|
);
|
|
214
228
|
}
|
|
215
229
|
|
|
216
|
-
process.
|
|
230
|
+
process.exitCode = result.completed ? 0 : 1;
|
|
231
|
+
setTimeout(() => process.exit(result.completed ? 0 : 1), 5000).unref();
|
|
217
232
|
} catch (err) {
|
|
218
233
|
process.stderr.write(`[mini-ralph] error: ${err.message}\n`);
|
|
219
234
|
if (opts.verbose && err.stack) {
|
|
220
235
|
process.stderr.write(err.stack + '\n');
|
|
221
236
|
}
|
|
222
|
-
process.
|
|
237
|
+
process.exitCode = 1;
|
|
238
|
+
setTimeout(() => process.exit(1), 5000).unref();
|
|
223
239
|
}
|
|
224
240
|
}
|
|
225
241
|
|