spec-and-loop 2.1.0 → 2.1.2
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/QUICKSTART.md +29 -16
- package/README.md +40 -38
- package/lib/mini-ralph/errors.js +118 -25
- package/lib/mini-ralph/invoker.js +131 -26
- package/lib/mini-ralph/prompt.js +9 -0
- package/lib/mini-ralph/runner.js +448 -141
- package/lib/mini-ralph/state.js +138 -1
- package/lib/mini-ralph/status.js +142 -10
- package/package.json +5 -5
- package/scripts/ralph-run.sh +9 -38
- package/scripts/setup.js +4 -3
package/lib/mini-ralph/state.js
CHANGED
|
@@ -12,6 +12,7 @@ const fs = require('fs');
|
|
|
12
12
|
const path = require('path');
|
|
13
13
|
|
|
14
14
|
const STATE_FILE = 'ralph-loop.state.json';
|
|
15
|
+
const LOCK_FILE = 'ralph-loop.lock.json';
|
|
15
16
|
|
|
16
17
|
/**
|
|
17
18
|
* Return the absolute path to the state file.
|
|
@@ -23,6 +24,16 @@ function statePath(ralphDir) {
|
|
|
23
24
|
return path.join(ralphDir, STATE_FILE);
|
|
24
25
|
}
|
|
25
26
|
|
|
27
|
+
/**
|
|
28
|
+
* Return the absolute path to the per-change run lock file.
|
|
29
|
+
*
|
|
30
|
+
* @param {string} ralphDir
|
|
31
|
+
* @returns {string}
|
|
32
|
+
*/
|
|
33
|
+
function lockPath(ralphDir) {
|
|
34
|
+
return path.join(ralphDir, LOCK_FILE);
|
|
35
|
+
}
|
|
36
|
+
|
|
26
37
|
/**
|
|
27
38
|
* Initialize the state file with the provided data.
|
|
28
39
|
* Creates ralphDir if it does not exist.
|
|
@@ -75,6 +86,90 @@ function remove(ralphDir) {
|
|
|
75
86
|
}
|
|
76
87
|
}
|
|
77
88
|
|
|
89
|
+
/**
|
|
90
|
+
* Acquire the per-change loop lock.
|
|
91
|
+
*
|
|
92
|
+
* If a prior lock exists and still points to a live process, this throws a
|
|
93
|
+
* descriptive error. If the prior lock is stale or unreadable, it is replaced.
|
|
94
|
+
*
|
|
95
|
+
* @param {string} ralphDir
|
|
96
|
+
* @param {object} metadata
|
|
97
|
+
* @returns {object}
|
|
98
|
+
*/
|
|
99
|
+
function acquireRunLock(ralphDir, metadata = {}) {
|
|
100
|
+
_ensureDir(ralphDir);
|
|
101
|
+
|
|
102
|
+
const file = lockPath(ralphDir);
|
|
103
|
+
const lock = Object.assign({}, metadata, {
|
|
104
|
+
pid: process.pid,
|
|
105
|
+
acquiredAt: new Date().toISOString(),
|
|
106
|
+
token: _createLockToken(),
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
while (true) {
|
|
110
|
+
try {
|
|
111
|
+
const fd = fs.openSync(file, 'wx');
|
|
112
|
+
fs.writeFileSync(fd, JSON.stringify(lock, null, 2), 'utf8');
|
|
113
|
+
fs.closeSync(fd);
|
|
114
|
+
return lock;
|
|
115
|
+
} catch (err) {
|
|
116
|
+
if (err.code !== 'EEXIST') {
|
|
117
|
+
throw err;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const existingLock = readRunLock(ralphDir);
|
|
121
|
+
if (_isLiveLock(existingLock)) {
|
|
122
|
+
const liveError = new Error(
|
|
123
|
+
`another loop is already active for this change (pid ${existingLock.pid})`
|
|
124
|
+
);
|
|
125
|
+
liveError.code = 'RALPH_ACTIVE_LOOP_LOCK';
|
|
126
|
+
liveError.lock = existingLock;
|
|
127
|
+
throw liveError;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
_removeLockIfPresent(file);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Read the current run lock. Returns null when the lock is absent or invalid.
|
|
137
|
+
*
|
|
138
|
+
* @param {string} ralphDir
|
|
139
|
+
* @returns {object|null}
|
|
140
|
+
*/
|
|
141
|
+
function readRunLock(ralphDir) {
|
|
142
|
+
const file = lockPath(ralphDir);
|
|
143
|
+
if (!fs.existsSync(file)) return null;
|
|
144
|
+
|
|
145
|
+
try {
|
|
146
|
+
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
147
|
+
} catch {
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Release the current run lock when it still belongs to the provided owner.
|
|
154
|
+
*
|
|
155
|
+
* @param {string} ralphDir
|
|
156
|
+
* @param {object|null} lock
|
|
157
|
+
*/
|
|
158
|
+
function releaseRunLock(ralphDir, lock) {
|
|
159
|
+
const file = lockPath(ralphDir);
|
|
160
|
+
if (!fs.existsSync(file)) return;
|
|
161
|
+
|
|
162
|
+
if (!lock || !lock.token) {
|
|
163
|
+
_removeLockIfPresent(file);
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const existingLock = readRunLock(ralphDir);
|
|
168
|
+
if (!existingLock || existingLock.token === lock.token) {
|
|
169
|
+
_removeLockIfPresent(file);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
|
|
78
173
|
// ---------------------------------------------------------------------------
|
|
79
174
|
// Internal helpers
|
|
80
175
|
// ---------------------------------------------------------------------------
|
|
@@ -90,4 +185,46 @@ function _write(ralphDir, data) {
|
|
|
90
185
|
fs.writeFileSync(statePath(ralphDir), JSON.stringify(data, null, 2), 'utf8');
|
|
91
186
|
}
|
|
92
187
|
|
|
93
|
-
|
|
188
|
+
function _createLockToken() {
|
|
189
|
+
return `${process.pid}:${Date.now()}:${Math.random().toString(36).slice(2)}`;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function _isLiveLock(lock) {
|
|
193
|
+
if (!lock || typeof lock.pid !== 'number' || lock.pid <= 0) {
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
try {
|
|
198
|
+
process.kill(lock.pid, 0);
|
|
199
|
+
return true;
|
|
200
|
+
} catch (err) {
|
|
201
|
+
if (err && err.code === 'EPERM') {
|
|
202
|
+
return true;
|
|
203
|
+
}
|
|
204
|
+
return false;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
function _removeLockIfPresent(file) {
|
|
209
|
+
try {
|
|
210
|
+
if (fs.existsSync(file)) {
|
|
211
|
+
fs.unlinkSync(file);
|
|
212
|
+
}
|
|
213
|
+
} catch (err) {
|
|
214
|
+
if (err.code !== 'ENOENT') {
|
|
215
|
+
throw err;
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
module.exports = {
|
|
221
|
+
init,
|
|
222
|
+
read,
|
|
223
|
+
update,
|
|
224
|
+
remove,
|
|
225
|
+
statePath,
|
|
226
|
+
lockPath,
|
|
227
|
+
acquireRunLock,
|
|
228
|
+
readRunLock,
|
|
229
|
+
releaseRunLock,
|
|
230
|
+
};
|
package/lib/mini-ralph/status.js
CHANGED
|
@@ -40,6 +40,11 @@ function render(ralphDir, tasksFile) {
|
|
|
40
40
|
lines.push(`Status: ${active}`);
|
|
41
41
|
lines.push(`Iteration: ${loopState.iteration || '?'} / ${loopState.maxIterations || '?'}`);
|
|
42
42
|
|
|
43
|
+
const lifecycle = loopState.active
|
|
44
|
+
? 'running'
|
|
45
|
+
: (loopState.completedAt ? 'completed' : 'stopped (incomplete)');
|
|
46
|
+
lines.push(`Lifecycle: ${lifecycle}`);
|
|
47
|
+
|
|
43
48
|
if (loopState.startedAt) {
|
|
44
49
|
const elapsed = _elapsed(loopState.startedAt);
|
|
45
50
|
lines.push(`Started: ${loopState.startedAt} (${elapsed} ago)`);
|
|
@@ -47,6 +52,17 @@ function render(ralphDir, tasksFile) {
|
|
|
47
52
|
|
|
48
53
|
if (loopState.completedAt) {
|
|
49
54
|
lines.push(`Completed: ${loopState.completedAt}`);
|
|
55
|
+
} else if (loopState.stoppedAt) {
|
|
56
|
+
lines.push(`Stopped: ${loopState.stoppedAt}`);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (loopState.exitReason) {
|
|
60
|
+
lines.push(`Exit reason: ${loopState.exitReason}`);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
const latestCommitAnomaly = _latestCommitAnomaly(history.recent(ralphDir, 20));
|
|
64
|
+
if (latestCommitAnomaly) {
|
|
65
|
+
lines.push(`Commit issue: ${latestCommitAnomaly.commitAnomaly}`);
|
|
50
66
|
}
|
|
51
67
|
|
|
52
68
|
lines.push(`Tasks mode: ${loopState.tasksMode ? 'yes' : 'no'}`);
|
|
@@ -95,15 +111,18 @@ function render(ralphDir, tasksFile) {
|
|
|
95
111
|
const durationSec = entry.duration ? `${(entry.duration / 1000).toFixed(1)}s` : '?';
|
|
96
112
|
const completed = entry.completionDetected ? ' [COMPLETE]' : (entry.taskDetected ? ' [TASK]' : '');
|
|
97
113
|
const toolSummary = _formatToolUsage(entry.toolUsage);
|
|
98
|
-
|
|
114
|
+
const failureSummary = _formatHistoryFailure(entry);
|
|
115
|
+
const commitSuffix = entry.commitAnomaly ? ` commit: ${entry.commitAnomaly}` : '';
|
|
116
|
+
lines.push(` Iteration ${entry.iteration}: ${durationSec}${completed}${toolSummary ? ` tools: ${toolSummary}` : ''}${failureSummary ? ` failure: ${failureSummary}` : ''}${commitSuffix}`);
|
|
99
117
|
}
|
|
100
118
|
lines.push('-'.repeat(50));
|
|
101
119
|
}
|
|
102
120
|
|
|
103
121
|
// Error history
|
|
122
|
+
const recentErrors = errors.readEntries(ralphDir, 5);
|
|
104
123
|
const errorCount = errors.count(ralphDir);
|
|
105
124
|
if (errorCount > 0) {
|
|
106
|
-
const latestError = errors.latest(ralphDir);
|
|
125
|
+
const latestError = recentErrors.length > 0 ? recentErrors[recentErrors.length - 1] : errors.latest(ralphDir);
|
|
107
126
|
const preview = _formatErrorPreview(latestError);
|
|
108
127
|
lines.push('');
|
|
109
128
|
lines.push('--- Error History ---');
|
|
@@ -113,7 +132,7 @@ function render(ralphDir, tasksFile) {
|
|
|
113
132
|
}
|
|
114
133
|
|
|
115
134
|
// Struggle indicators
|
|
116
|
-
const struggles = _detectStruggles(recentHistory);
|
|
135
|
+
const struggles = _detectStruggles(recentHistory, recentErrors);
|
|
117
136
|
if (struggles.length > 0) {
|
|
118
137
|
lines.push('');
|
|
119
138
|
lines.push('--- Struggle Indicators ---');
|
|
@@ -189,13 +208,33 @@ function _formatToolUsage(toolUsage) {
|
|
|
189
208
|
return toolUsage.map((t) => `${t.tool}(${t.count})`).join(', ');
|
|
190
209
|
}
|
|
191
210
|
|
|
211
|
+
function _isFailedHistoryEntry(entry) {
|
|
212
|
+
if (!entry || typeof entry !== 'object') return false;
|
|
213
|
+
if (entry.signal) return true;
|
|
214
|
+
if (entry.failureStage) return true;
|
|
215
|
+
return entry.exitCode !== 0;
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
function _formatHistoryFailure(entry) {
|
|
219
|
+
if (!entry || typeof entry !== 'object') return '';
|
|
220
|
+
|
|
221
|
+
if (entry.signal) return `signal ${entry.signal}`;
|
|
222
|
+
if (entry.failureStage) return `stage ${entry.failureStage}`;
|
|
223
|
+
|
|
224
|
+
if (entry.exitCode !== null && entry.exitCode !== undefined && entry.exitCode !== 0) {
|
|
225
|
+
return `exit code ${entry.exitCode}`;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
return '';
|
|
229
|
+
}
|
|
230
|
+
|
|
192
231
|
/**
|
|
193
232
|
* Detect struggle indicators from recent history.
|
|
194
233
|
*
|
|
195
234
|
* @param {Array<object>} recentHistory
|
|
196
235
|
* @returns {Array<string>} Warning messages
|
|
197
236
|
*/
|
|
198
|
-
function _detectStruggles(recentHistory) {
|
|
237
|
+
function _detectStruggles(recentHistory, errorEntries = []) {
|
|
199
238
|
const warnings = [];
|
|
200
239
|
if (recentHistory.length < 2) return warnings;
|
|
201
240
|
|
|
@@ -210,22 +249,94 @@ function _detectStruggles(recentHistory) {
|
|
|
210
249
|
);
|
|
211
250
|
}
|
|
212
251
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
if (errorCount >= 2) {
|
|
252
|
+
const repeatedError = _detectRepeatedError(recentHistory, errorEntries);
|
|
253
|
+
if (repeatedError) {
|
|
216
254
|
warnings.push(
|
|
217
|
-
`
|
|
255
|
+
`Repeated error detected in ${repeatedError.count} of the last ${recentHistory.length} iterations: ${repeatedError.preview}`
|
|
218
256
|
);
|
|
219
257
|
}
|
|
220
258
|
|
|
221
259
|
return warnings;
|
|
222
260
|
}
|
|
223
261
|
|
|
262
|
+
function _detectRepeatedError(recentHistory, errorEntries) {
|
|
263
|
+
if (!Array.isArray(errorEntries) || errorEntries.length < 2) {
|
|
264
|
+
return null;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const recentFailedIterations = new Set(
|
|
268
|
+
recentHistory
|
|
269
|
+
.filter((entry) => _isFailedHistoryEntry(entry))
|
|
270
|
+
.map((entry) => entry.iteration)
|
|
271
|
+
);
|
|
272
|
+
|
|
273
|
+
if (recentFailedIterations.size < 2) {
|
|
274
|
+
return null;
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const signatureCounts = new Map();
|
|
278
|
+
|
|
279
|
+
for (const entry of errorEntries) {
|
|
280
|
+
if (!entry || !recentFailedIterations.has(entry.iteration)) {
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
const preview = _formatErrorPreview(entry);
|
|
285
|
+
const signature = _normalizeErrorSignature(preview);
|
|
286
|
+
if (!signature) {
|
|
287
|
+
continue;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
const existing = signatureCounts.get(signature) || { count: 0, preview };
|
|
291
|
+
existing.count += 1;
|
|
292
|
+
if (!existing.preview && preview) {
|
|
293
|
+
existing.preview = preview;
|
|
294
|
+
}
|
|
295
|
+
signatureCounts.set(signature, existing);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
let bestMatch = null;
|
|
299
|
+
for (const candidate of signatureCounts.values()) {
|
|
300
|
+
if (!bestMatch || candidate.count > bestMatch.count) {
|
|
301
|
+
bestMatch = candidate;
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (!bestMatch || bestMatch.count < 2) {
|
|
306
|
+
return null;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
return bestMatch;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function _normalizeErrorSignature(text) {
|
|
313
|
+
if (!text) return '';
|
|
314
|
+
|
|
315
|
+
return text
|
|
316
|
+
.toLowerCase()
|
|
317
|
+
.replace(/\b0x[0-9a-f]+\b/g, '<hex>')
|
|
318
|
+
.replace(/[A-Z]:\\[^\s]+/g, '<path>')
|
|
319
|
+
.replace(/(?:\/[^\s:]+)+/g, '<path>')
|
|
320
|
+
.replace(/:\d+:\d+/g, ':<n>:<n>')
|
|
321
|
+
.replace(/\b\d+\b/g, '<n>')
|
|
322
|
+
.replace(/\s+/g, ' ')
|
|
323
|
+
.trim();
|
|
324
|
+
}
|
|
325
|
+
|
|
224
326
|
function _formatErrorPreview(entry) {
|
|
225
327
|
if (!entry) return '';
|
|
226
328
|
|
|
227
329
|
const source = entry.stderr || entry.stdout || _fallbackErrorPreview(entry);
|
|
228
|
-
|
|
330
|
+
const metadata = [];
|
|
331
|
+
|
|
332
|
+
if (entry.signal) metadata.push(`signal ${entry.signal}`);
|
|
333
|
+
if (entry.failureStage) metadata.push(`stage ${entry.failureStage}`);
|
|
334
|
+
|
|
335
|
+
const preview = metadata.length > 0
|
|
336
|
+
? `${metadata.join(' | ')} | ${source}`
|
|
337
|
+
: source;
|
|
338
|
+
|
|
339
|
+
return preview.substring(0, 200).trim();
|
|
229
340
|
}
|
|
230
341
|
|
|
231
342
|
function _fallbackErrorPreview(entry) {
|
|
@@ -235,4 +346,25 @@ function _fallbackErrorPreview(entry) {
|
|
|
235
346
|
return parts.join(' | ');
|
|
236
347
|
}
|
|
237
348
|
|
|
238
|
-
|
|
349
|
+
function _latestCommitAnomaly(recentHistory) {
|
|
350
|
+
if (!Array.isArray(recentHistory) || recentHistory.length === 0) return null;
|
|
351
|
+
|
|
352
|
+
for (let idx = recentHistory.length - 1; idx >= 0; idx--) {
|
|
353
|
+
if (recentHistory[idx] && recentHistory[idx].commitAnomaly) {
|
|
354
|
+
return recentHistory[idx];
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
return null;
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
module.exports = {
|
|
362
|
+
render,
|
|
363
|
+
_elapsed,
|
|
364
|
+
_detectStruggles,
|
|
365
|
+
_formatToolUsage,
|
|
366
|
+
_formatErrorPreview,
|
|
367
|
+
_latestCommitAnomaly,
|
|
368
|
+
_formatHistoryFailure,
|
|
369
|
+
_isFailedHistoryEntry,
|
|
370
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "spec-and-loop",
|
|
3
|
-
"version": "2.1.
|
|
3
|
+
"version": "2.1.2",
|
|
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.0",
|
|
21
|
+
"spec-and-loop": "^2.1.1"
|
|
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.0.0"
|
|
27
27
|
},
|
|
28
28
|
"jest": {
|
|
29
29
|
"testEnvironment": "node",
|
package/scripts/ralph-run.sh
CHANGED
|
@@ -181,7 +181,7 @@ EXAMPLES:
|
|
|
181
181
|
|
|
182
182
|
PREREQUISITES:
|
|
183
183
|
- Git repository (git init)
|
|
184
|
-
- OpenSpec artifacts created (openspec init, openspec new,
|
|
184
|
+
- OpenSpec artifacts created (openspec init, openspec new change, then complete the generated artifacts)
|
|
185
185
|
- opencode CLI installed (npm install -g opencode-ai)
|
|
186
186
|
|
|
187
187
|
EOF
|
|
@@ -757,6 +757,10 @@ Include full context from openspec artifacts in {{change_dir}}:
|
|
|
757
757
|
- Read {{change_dir}}/design.md for the technical design approach
|
|
758
758
|
- Read {{change_dir}}/specs/*/spec.md for the detailed specifications
|
|
759
759
|
|
|
760
|
+
## Invocation-Time PRD Snapshot
|
|
761
|
+
|
|
762
|
+
{{base_prompt}}
|
|
763
|
+
|
|
760
764
|
## Task List
|
|
761
765
|
|
|
762
766
|
{{tasks}}
|
|
@@ -780,8 +784,7 @@ Include full context from openspec artifacts in {{change_dir}}:
|
|
|
780
784
|
3. **Complete** task:
|
|
781
785
|
- Verify that the implementation meets the requirements
|
|
782
786
|
- When the task is successfully completed, mark it as [x] in the tasks file
|
|
783
|
-
|
|
784
|
-
- Output: `<promise>{{task_promise}}</promise>`
|
|
787
|
+
- Output: `<promise>{{task_promise}}</promise>`
|
|
785
788
|
|
|
786
789
|
4. **Continue** to the next task:
|
|
787
790
|
- The loop will continue with the next iteration
|
|
@@ -800,41 +803,9 @@ Include full context from openspec artifacts in {{change_dir}}:
|
|
|
800
803
|
- If stuck, try a different approach
|
|
801
804
|
- Check your work before claiming completion
|
|
802
805
|
|
|
803
|
-
##
|
|
804
|
-
|
|
805
|
-
|
|
806
|
-
|
|
807
|
-
```
|
|
808
|
-
Ralph iteration <N>: <brief description of work completed>
|
|
809
|
-
|
|
810
|
-
Tasks completed:
|
|
811
|
-
- [x] <task.number> <task description text>
|
|
812
|
-
- [x] <task.number> <task description text>
|
|
813
|
-
...
|
|
814
|
-
```
|
|
815
|
-
|
|
816
|
-
**Requirements:**
|
|
817
|
-
1. Use iteration number from Ralph's state (e.g., "Ralph iteration 7")
|
|
818
|
-
2. Include a BRIEF description summarizing what was done
|
|
819
|
-
3. List ALL completed tasks with their numbers and full descriptions
|
|
820
|
-
4. Use the EXACT format: "- [x] <task.number> <task description>"
|
|
821
|
-
5. Read the "## Completed Tasks for Git Commit" section from the PRD for the task list
|
|
822
|
-
|
|
823
|
-
**FORBIDDEN:**
|
|
824
|
-
- DO NOT use generic messages like "work in progress" or "iteration N"
|
|
825
|
-
- DO NOT skip task numbers
|
|
826
|
-
- DO NOT truncate task descriptions
|
|
827
|
-
- DO NOT create commits without task information
|
|
828
|
-
|
|
829
|
-
**Example:**
|
|
830
|
-
```
|
|
831
|
-
Ralph iteration 7: Implement unit tests for response processing
|
|
832
|
-
|
|
833
|
-
Tasks completed:
|
|
834
|
-
- [x] 11.6 Write unit test for personality state management
|
|
835
|
-
- [x] 11.7 Write unit test for personality validation
|
|
836
|
-
- [x] 11.8 Write unit test for system prompt validation
|
|
837
|
-
```
|
|
806
|
+
## Commit Contract
|
|
807
|
+
|
|
808
|
+
{{commit_contract}}
|
|
838
809
|
|
|
839
810
|
{{context}}
|
|
840
811
|
EOF
|
package/scripts/setup.js
CHANGED
|
@@ -32,16 +32,17 @@ function runSetup() {
|
|
|
32
32
|
console.log('Usage:');
|
|
33
33
|
console.log(' cd /path/to/your/project');
|
|
34
34
|
console.log(' openspec init # Initialize OpenSpec');
|
|
35
|
-
console.log(' openspec new <name>
|
|
36
|
-
console.log(' openspec ff <name> # Fast-forward artifacts');
|
|
35
|
+
console.log(' openspec new change <name> # Create a new change');
|
|
37
36
|
console.log(' ralph-run --change <name> # Run ralph loop');
|
|
38
37
|
console.log(' ralph-run # Auto-detect change');
|
|
38
|
+
console.log(' ralph-run --status # Show loop status');
|
|
39
39
|
console.log('');
|
|
40
40
|
console.log('Prerequisites:');
|
|
41
|
-
console.log(' - openspec CLI: npm install -g openspec');
|
|
41
|
+
console.log(' - openspec CLI: npm install -g @fission-ai/openspec');
|
|
42
42
|
console.log(' - opencode CLI: npm install -g opencode-ai');
|
|
43
43
|
console.log(' - jq CLI: apt install jq / brew install jq');
|
|
44
44
|
console.log(' - git: git init');
|
|
45
|
+
console.log(' - supported OS: Linux or macOS');
|
|
45
46
|
}
|
|
46
47
|
|
|
47
48
|
if (require.main === module) {
|