spec-and-loop 2.0.1 → 2.1.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/QUICKSTART.md +29 -16
- package/README.md +40 -38
- package/lib/mini-ralph/errors.js +144 -9
- package/lib/mini-ralph/invoker.js +154 -32
- package/lib/mini-ralph/prompt.js +9 -0
- package/lib/mini-ralph/runner.js +479 -163
- package/lib/mini-ralph/state.js +138 -1
- package/lib/mini-ralph/status.js +159 -14
- package/package.json +1 -1
- 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,26 +111,28 @@ 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
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
const
|
|
108
|
-
const preview =
|
|
122
|
+
const recentErrors = errors.readEntries(ralphDir, 5);
|
|
123
|
+
const errorCount = errors.count(ralphDir);
|
|
124
|
+
if (errorCount > 0) {
|
|
125
|
+
const latestError = recentErrors.length > 0 ? recentErrors[recentErrors.length - 1] : errors.latest(ralphDir);
|
|
126
|
+
const preview = _formatErrorPreview(latestError);
|
|
109
127
|
lines.push('');
|
|
110
128
|
lines.push('--- Error History ---');
|
|
111
|
-
lines.push(` Errors: ${
|
|
129
|
+
lines.push(` Errors: ${errorCount}`);
|
|
112
130
|
lines.push(` Most recent: ${preview}`);
|
|
113
131
|
lines.push('-'.repeat(50));
|
|
114
132
|
}
|
|
115
133
|
|
|
116
134
|
// Struggle indicators
|
|
117
|
-
const struggles = _detectStruggles(recentHistory);
|
|
135
|
+
const struggles = _detectStruggles(recentHistory, recentErrors);
|
|
118
136
|
if (struggles.length > 0) {
|
|
119
137
|
lines.push('');
|
|
120
138
|
lines.push('--- Struggle Indicators ---');
|
|
@@ -190,13 +208,33 @@ function _formatToolUsage(toolUsage) {
|
|
|
190
208
|
return toolUsage.map((t) => `${t.tool}(${t.count})`).join(', ');
|
|
191
209
|
}
|
|
192
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
|
+
|
|
193
231
|
/**
|
|
194
232
|
* Detect struggle indicators from recent history.
|
|
195
233
|
*
|
|
196
234
|
* @param {Array<object>} recentHistory
|
|
197
235
|
* @returns {Array<string>} Warning messages
|
|
198
236
|
*/
|
|
199
|
-
function _detectStruggles(recentHistory) {
|
|
237
|
+
function _detectStruggles(recentHistory, errorEntries = []) {
|
|
200
238
|
const warnings = [];
|
|
201
239
|
if (recentHistory.length < 2) return warnings;
|
|
202
240
|
|
|
@@ -211,15 +249,122 @@ function _detectStruggles(recentHistory) {
|
|
|
211
249
|
);
|
|
212
250
|
}
|
|
213
251
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
if (errorCount >= 2) {
|
|
252
|
+
const repeatedError = _detectRepeatedError(recentHistory, errorEntries);
|
|
253
|
+
if (repeatedError) {
|
|
217
254
|
warnings.push(
|
|
218
|
-
`
|
|
255
|
+
`Repeated error detected in ${repeatedError.count} of the last ${recentHistory.length} iterations: ${repeatedError.preview}`
|
|
219
256
|
);
|
|
220
257
|
}
|
|
221
258
|
|
|
222
259
|
return warnings;
|
|
223
260
|
}
|
|
224
261
|
|
|
225
|
-
|
|
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
|
+
|
|
326
|
+
function _formatErrorPreview(entry) {
|
|
327
|
+
if (!entry) return '';
|
|
328
|
+
|
|
329
|
+
const source = entry.stderr || entry.stdout || _fallbackErrorPreview(entry);
|
|
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();
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
function _fallbackErrorPreview(entry) {
|
|
343
|
+
const parts = [];
|
|
344
|
+
if (entry.task) parts.push(entry.task);
|
|
345
|
+
if (!Number.isNaN(entry.exitCode)) parts.push(`exit code ${entry.exitCode}`);
|
|
346
|
+
return parts.join(' | ');
|
|
347
|
+
}
|
|
348
|
+
|
|
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
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) {
|