spec-and-loop 1.0.8 → 2.0.0-rc.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 +38 -27
- package/README.md +99 -152
- package/lib/mini-ralph/context.js +99 -0
- package/lib/mini-ralph/history.js +99 -0
- package/lib/mini-ralph/index.js +91 -0
- package/lib/mini-ralph/invoker.js +225 -0
- package/lib/mini-ralph/prompt.js +116 -0
- package/lib/mini-ralph/runner.js +415 -0
- package/lib/mini-ralph/state.js +93 -0
- package/lib/mini-ralph/status.js +211 -0
- package/lib/mini-ralph/tasks.js +209 -0
- package/package.json +3 -4
- package/scripts/mini-ralph-cli.js +226 -0
- package/scripts/ralph-run.sh +178 -72
- package/scripts/setup.js +39 -31
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* status.js - Status dashboard renderer for mini-ralph.
|
|
5
|
+
*
|
|
6
|
+
* Reads the current loop state, history, context, and tasks to produce
|
|
7
|
+
* a human-readable status report. Also surfaces struggle indicators when
|
|
8
|
+
* the loop appears stuck (no-progress or repeated errors).
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const state = require('./state');
|
|
12
|
+
const history = require('./history');
|
|
13
|
+
const context = require('./context');
|
|
14
|
+
const tasks = require('./tasks');
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Render a status dashboard string for the given .ralph/ directory.
|
|
18
|
+
*
|
|
19
|
+
* @param {string} ralphDir
|
|
20
|
+
* @param {string} [tasksFile] - Optional path to the tasks.md file for task progress
|
|
21
|
+
* @returns {string}
|
|
22
|
+
*/
|
|
23
|
+
function render(ralphDir, tasksFile) {
|
|
24
|
+
const lines = [];
|
|
25
|
+
|
|
26
|
+
const loopState = state.read(ralphDir);
|
|
27
|
+
if (!loopState) {
|
|
28
|
+
lines.push('No active or recent loop state found.');
|
|
29
|
+
lines.push(`(Looking in: ${ralphDir})`);
|
|
30
|
+
return lines.join('\n');
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Header
|
|
34
|
+
lines.push('=== mini-ralph status ===');
|
|
35
|
+
lines.push('');
|
|
36
|
+
|
|
37
|
+
// Loop state
|
|
38
|
+
const active = loopState.active ? 'ACTIVE' : 'INACTIVE';
|
|
39
|
+
lines.push(`Status: ${active}`);
|
|
40
|
+
lines.push(`Iteration: ${loopState.iteration || '?'} / ${loopState.maxIterations || '?'}`);
|
|
41
|
+
|
|
42
|
+
if (loopState.startedAt) {
|
|
43
|
+
const elapsed = _elapsed(loopState.startedAt);
|
|
44
|
+
lines.push(`Started: ${loopState.startedAt} (${elapsed} ago)`);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
if (loopState.completedAt) {
|
|
48
|
+
lines.push(`Completed: ${loopState.completedAt}`);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
lines.push(`Tasks mode: ${loopState.tasksMode ? 'yes' : 'no'}`);
|
|
52
|
+
lines.push(`Completion: <promise>${loopState.completionPromise || 'COMPLETE'}</promise>`);
|
|
53
|
+
lines.push(`Task promise: <promise>${loopState.taskPromise || 'READY_FOR_NEXT_TASK'}</promise>`);
|
|
54
|
+
|
|
55
|
+
// Prompt summary
|
|
56
|
+
const promptSummary = _promptSummary(loopState);
|
|
57
|
+
if (promptSummary) {
|
|
58
|
+
lines.push('');
|
|
59
|
+
lines.push(`Prompt: ${promptSummary}`);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Pending context
|
|
63
|
+
const pendingCtx = context.read(ralphDir);
|
|
64
|
+
if (pendingCtx) {
|
|
65
|
+
lines.push('');
|
|
66
|
+
lines.push('--- Pending Context (injected next iteration) ---');
|
|
67
|
+
lines.push(pendingCtx.substring(0, 500) + (pendingCtx.length > 500 ? '\n...(truncated)' : ''));
|
|
68
|
+
lines.push('-'.repeat(50));
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Task progress
|
|
72
|
+
const resolvedTasksFile = tasksFile || _findTasksFile(loopState);
|
|
73
|
+
if (resolvedTasksFile) {
|
|
74
|
+
try {
|
|
75
|
+
const counts = tasks.countTasks(resolvedTasksFile);
|
|
76
|
+
lines.push('');
|
|
77
|
+
lines.push(`Tasks: ${counts.completed} completed, ${counts.inProgress} in-progress, ${counts.incomplete} incomplete (${counts.total} total)`);
|
|
78
|
+
const current = tasks.currentTask(resolvedTasksFile);
|
|
79
|
+
if (current) {
|
|
80
|
+
const num = current.number ? `${current.number} ` : '';
|
|
81
|
+
lines.push(`Current task: ${num}${current.description}`);
|
|
82
|
+
}
|
|
83
|
+
} catch {
|
|
84
|
+
// tasks file unreadable - skip
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// Recent history
|
|
89
|
+
const recentHistory = history.recent(ralphDir, 5);
|
|
90
|
+
if (recentHistory.length > 0) {
|
|
91
|
+
lines.push('');
|
|
92
|
+
lines.push('--- Recent History ---');
|
|
93
|
+
for (const entry of recentHistory) {
|
|
94
|
+
const durationSec = entry.duration ? `${(entry.duration / 1000).toFixed(1)}s` : '?';
|
|
95
|
+
const completed = entry.completionDetected ? ' [COMPLETE]' : (entry.taskDetected ? ' [TASK]' : '');
|
|
96
|
+
const toolSummary = _formatToolUsage(entry.toolUsage);
|
|
97
|
+
lines.push(` Iteration ${entry.iteration}: ${durationSec}${completed}${toolSummary ? ` tools: ${toolSummary}` : ''}`);
|
|
98
|
+
}
|
|
99
|
+
lines.push('-'.repeat(50));
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// Struggle indicators
|
|
103
|
+
const struggles = _detectStruggles(recentHistory);
|
|
104
|
+
if (struggles.length > 0) {
|
|
105
|
+
lines.push('');
|
|
106
|
+
lines.push('--- Struggle Indicators ---');
|
|
107
|
+
for (const s of struggles) {
|
|
108
|
+
lines.push(` WARNING: ${s}`);
|
|
109
|
+
}
|
|
110
|
+
lines.push('Tip: Use `ralph-run --add-context "..."` to provide guidance for the next iteration.');
|
|
111
|
+
lines.push('-'.repeat(50));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
return lines.join('\n');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// ---------------------------------------------------------------------------
|
|
118
|
+
// Internal helpers
|
|
119
|
+
// ---------------------------------------------------------------------------
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* Format elapsed time from an ISO timestamp to a human-readable string.
|
|
123
|
+
*
|
|
124
|
+
* @param {string} isoString
|
|
125
|
+
* @returns {string}
|
|
126
|
+
*/
|
|
127
|
+
function _elapsed(isoString) {
|
|
128
|
+
const start = new Date(isoString);
|
|
129
|
+
const now = new Date();
|
|
130
|
+
const ms = now - start;
|
|
131
|
+
if (isNaN(ms) || ms < 0) return 'unknown';
|
|
132
|
+
const secs = Math.floor(ms / 1000);
|
|
133
|
+
if (secs < 60) return `${secs}s`;
|
|
134
|
+
const mins = Math.floor(secs / 60);
|
|
135
|
+
if (mins < 60) return `${mins}m ${secs % 60}s`;
|
|
136
|
+
const hours = Math.floor(mins / 60);
|
|
137
|
+
return `${hours}h ${mins % 60}m`;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Extract a brief prompt summary from loop state.
|
|
142
|
+
*
|
|
143
|
+
* @param {object} loopState
|
|
144
|
+
* @returns {string}
|
|
145
|
+
*/
|
|
146
|
+
function _promptSummary(loopState) {
|
|
147
|
+
if (loopState.promptFile) {
|
|
148
|
+
return `file: ${loopState.promptFile}`;
|
|
149
|
+
}
|
|
150
|
+
if (loopState.prompt) {
|
|
151
|
+
const firstLine = loopState.prompt.split('\n')[0].substring(0, 80);
|
|
152
|
+
return `"${firstLine}${firstLine.length >= 80 ? '...' : ''}"`;
|
|
153
|
+
}
|
|
154
|
+
return '';
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Try to find a tasks file path from loop state.
|
|
159
|
+
*
|
|
160
|
+
* @param {object} loopState
|
|
161
|
+
* @returns {string|null}
|
|
162
|
+
*/
|
|
163
|
+
function _findTasksFile(loopState) {
|
|
164
|
+
if (loopState.tasksFile) return loopState.tasksFile;
|
|
165
|
+
return null;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Format tool usage array to a short string.
|
|
170
|
+
*
|
|
171
|
+
* @param {Array} toolUsage
|
|
172
|
+
* @returns {string}
|
|
173
|
+
*/
|
|
174
|
+
function _formatToolUsage(toolUsage) {
|
|
175
|
+
if (!Array.isArray(toolUsage) || toolUsage.length === 0) return '';
|
|
176
|
+
return toolUsage.map((t) => `${t.tool}(${t.count})`).join(', ');
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Detect struggle indicators from recent history.
|
|
181
|
+
*
|
|
182
|
+
* @param {Array<object>} recentHistory
|
|
183
|
+
* @returns {Array<string>} Warning messages
|
|
184
|
+
*/
|
|
185
|
+
function _detectStruggles(recentHistory) {
|
|
186
|
+
const warnings = [];
|
|
187
|
+
if (recentHistory.length < 2) return warnings;
|
|
188
|
+
|
|
189
|
+
// No-progress: multiple iterations with no files changed
|
|
190
|
+
const noProgressCount = recentHistory.filter(
|
|
191
|
+
(e) => !e.filesChanged || e.filesChanged.length === 0
|
|
192
|
+
).length;
|
|
193
|
+
|
|
194
|
+
if (noProgressCount >= 2 && noProgressCount === recentHistory.length) {
|
|
195
|
+
warnings.push(
|
|
196
|
+
`No file changes detected in the last ${noProgressCount} iterations. The loop may be stuck.`
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Repeated errors: multiple non-zero exit codes
|
|
201
|
+
const errorCount = recentHistory.filter((e) => e.exitCode !== 0).length;
|
|
202
|
+
if (errorCount >= 2) {
|
|
203
|
+
warnings.push(
|
|
204
|
+
`OpenCode exited with errors in ${errorCount} of the last ${recentHistory.length} iterations.`
|
|
205
|
+
);
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return warnings;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
module.exports = { render, _elapsed, _detectStruggles, _formatToolUsage };
|
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* tasks.js - Task file helpers for mini-ralph.
|
|
5
|
+
*
|
|
6
|
+
* Provides utilities to:
|
|
7
|
+
* - Establish the .ralph/ralph-tasks.md symlink pointing to the OpenSpec tasks.md
|
|
8
|
+
* - Parse tasks from a tasks.md file (incomplete, in-progress, completed)
|
|
9
|
+
* - Compute a stable hash of the tasks file for change detection
|
|
10
|
+
*
|
|
11
|
+
* The OpenSpec tasks.md is always the source of truth. The symlink at
|
|
12
|
+
* .ralph/ralph-tasks.md exists only as a convenience reference for the loop
|
|
13
|
+
* engine; both paths resolve to the same inode.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const fs = require('fs');
|
|
17
|
+
const path = require('path');
|
|
18
|
+
const crypto = require('crypto');
|
|
19
|
+
|
|
20
|
+
const TASKS_LINK_NAME = 'ralph-tasks.md';
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Return the path to the managed tasks symlink inside ralphDir.
|
|
24
|
+
*
|
|
25
|
+
* @param {string} ralphDir
|
|
26
|
+
* @returns {string}
|
|
27
|
+
*/
|
|
28
|
+
function tasksLinkPath(ralphDir) {
|
|
29
|
+
return path.join(ralphDir, TASKS_LINK_NAME);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Create or update the .ralph/ralph-tasks.md symlink to point to tasksFile.
|
|
34
|
+
* Uses an absolute path for the symlink target so it is portable.
|
|
35
|
+
*
|
|
36
|
+
* @param {string} ralphDir - Absolute path to .ralph/ directory
|
|
37
|
+
* @param {string} tasksFile - Path to the OpenSpec tasks.md (absolute or resolvable)
|
|
38
|
+
*/
|
|
39
|
+
function syncLink(ralphDir, tasksFile) {
|
|
40
|
+
_ensureDir(ralphDir);
|
|
41
|
+
|
|
42
|
+
const absTasksFile = path.resolve(tasksFile);
|
|
43
|
+
const linkPath = tasksLinkPath(ralphDir);
|
|
44
|
+
|
|
45
|
+
if (!fs.existsSync(absTasksFile)) {
|
|
46
|
+
throw new Error(`mini-ralph tasks: tasks file not found: ${absTasksFile}`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Remove existing file or symlink if it already exists
|
|
50
|
+
if (fs.existsSync(linkPath) || _isSymlink(linkPath)) {
|
|
51
|
+
fs.unlinkSync(linkPath);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
fs.symlinkSync(absTasksFile, linkPath);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* Parse all tasks from a tasks.md file.
|
|
59
|
+
* Returns an array of task objects with: { number, description, status }
|
|
60
|
+
* where status is one of: 'incomplete', 'in_progress', 'completed'.
|
|
61
|
+
*
|
|
62
|
+
* Lines are matched by the checkbox prefix:
|
|
63
|
+
* - [ ] -> incomplete
|
|
64
|
+
* - [/] -> in_progress
|
|
65
|
+
* - [x] -> completed
|
|
66
|
+
*
|
|
67
|
+
* Task numbers are extracted from the description (e.g., "1.1 Some task").
|
|
68
|
+
*
|
|
69
|
+
* @param {string} tasksFile
|
|
70
|
+
* @returns {Array<{number: string, description: string, status: string, raw: string}>}
|
|
71
|
+
*/
|
|
72
|
+
function parseTasks(tasksFile) {
|
|
73
|
+
if (!fs.existsSync(tasksFile)) return [];
|
|
74
|
+
|
|
75
|
+
const lines = fs.readFileSync(tasksFile, 'utf8').split('\n');
|
|
76
|
+
const tasks = [];
|
|
77
|
+
|
|
78
|
+
for (const line of lines) {
|
|
79
|
+
const match = line.match(/^-\s+\[([ x/])\]\s+(.+)$/);
|
|
80
|
+
if (!match) continue;
|
|
81
|
+
|
|
82
|
+
const checkChar = match[1];
|
|
83
|
+
const description = match[2].trim();
|
|
84
|
+
let status;
|
|
85
|
+
if (checkChar === 'x') status = 'completed';
|
|
86
|
+
else if (checkChar === '/') status = 'in_progress';
|
|
87
|
+
else status = 'incomplete';
|
|
88
|
+
|
|
89
|
+
// Try to extract a leading task number like "1.1" or "4.2"
|
|
90
|
+
const numMatch = description.match(/^(\d+\.\d+)\s+(.+)$/);
|
|
91
|
+
const number = numMatch ? numMatch[1] : '';
|
|
92
|
+
const text = numMatch ? numMatch[2] : description;
|
|
93
|
+
|
|
94
|
+
tasks.push({ number, description: text, fullDescription: description, status, raw: line });
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return tasks;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Return the first incomplete or in-progress task, or null if all are done.
|
|
102
|
+
*
|
|
103
|
+
* @param {string} tasksFile
|
|
104
|
+
* @returns {object|null}
|
|
105
|
+
*/
|
|
106
|
+
function currentTask(tasksFile) {
|
|
107
|
+
const all = parseTasks(tasksFile);
|
|
108
|
+
return (
|
|
109
|
+
all.find((t) => t.status === 'in_progress') ||
|
|
110
|
+
all.find((t) => t.status === 'incomplete') ||
|
|
111
|
+
null
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Compute an MD5 hash of the tasks file content for change detection.
|
|
117
|
+
* Returns '0' if the file does not exist.
|
|
118
|
+
*
|
|
119
|
+
* @param {string} tasksFile
|
|
120
|
+
* @returns {string}
|
|
121
|
+
*/
|
|
122
|
+
function hashFile(tasksFile) {
|
|
123
|
+
if (!fs.existsSync(tasksFile)) return '0';
|
|
124
|
+
const content = fs.readFileSync(tasksFile);
|
|
125
|
+
return crypto.createHash('md5').update(content).digest('hex');
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Count tasks by status.
|
|
130
|
+
*
|
|
131
|
+
* @param {string} tasksFile
|
|
132
|
+
* @returns {{ total: number, completed: number, inProgress: number, incomplete: number }}
|
|
133
|
+
*/
|
|
134
|
+
function countTasks(tasksFile) {
|
|
135
|
+
const all = parseTasks(tasksFile);
|
|
136
|
+
return {
|
|
137
|
+
total: all.length,
|
|
138
|
+
completed: all.filter((t) => t.status === 'completed').length,
|
|
139
|
+
inProgress: all.filter((t) => t.status === 'in_progress').length,
|
|
140
|
+
incomplete: all.filter((t) => t.status === 'incomplete').length,
|
|
141
|
+
};
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Build a compact task-context block for the current tasks file.
|
|
146
|
+
* Mirrors the shell-side task context format so prompts can render a fresh
|
|
147
|
+
* snapshot on every iteration without regenerating the whole PRD.
|
|
148
|
+
*
|
|
149
|
+
* @param {string} tasksFile
|
|
150
|
+
* @returns {string}
|
|
151
|
+
*/
|
|
152
|
+
function taskContext(tasksFile) {
|
|
153
|
+
const all = parseTasks(tasksFile);
|
|
154
|
+
if (all.length === 0) return '';
|
|
155
|
+
|
|
156
|
+
const current =
|
|
157
|
+
all.find((task) => task.status === 'in_progress') ||
|
|
158
|
+
all.find((task) => task.status === 'incomplete') ||
|
|
159
|
+
null;
|
|
160
|
+
const completed = all.filter((task) => task.status === 'completed');
|
|
161
|
+
|
|
162
|
+
const sections = [];
|
|
163
|
+
|
|
164
|
+
if (current) {
|
|
165
|
+
sections.push('## Current Task');
|
|
166
|
+
sections.push(`- ${current.fullDescription || current.description}`);
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (completed.length > 0) {
|
|
170
|
+
if (sections.length > 0) {
|
|
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
|
+
}
|
|
178
|
+
|
|
179
|
+
return sections.join('\n');
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// ---------------------------------------------------------------------------
|
|
183
|
+
// Internal helpers
|
|
184
|
+
// ---------------------------------------------------------------------------
|
|
185
|
+
|
|
186
|
+
function _ensureDir(dir) {
|
|
187
|
+
if (!fs.existsSync(dir)) {
|
|
188
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
function _isSymlink(p) {
|
|
193
|
+
try {
|
|
194
|
+
fs.lstatSync(p);
|
|
195
|
+
return fs.lstatSync(p).isSymbolicLink();
|
|
196
|
+
} catch {
|
|
197
|
+
return false;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
module.exports = {
|
|
202
|
+
syncLink,
|
|
203
|
+
parseTasks,
|
|
204
|
+
currentTask,
|
|
205
|
+
hashFile,
|
|
206
|
+
countTasks,
|
|
207
|
+
taskContext,
|
|
208
|
+
tasksLinkPath,
|
|
209
|
+
};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "spec-and-loop",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "2.0.0-rc.2",
|
|
4
4
|
"description": "OpenSpec + Ralph Loop integration for iterative development with opencode",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"bin": {
|
|
@@ -17,8 +17,7 @@
|
|
|
17
17
|
"lint": "shellcheck scripts/*.sh"
|
|
18
18
|
},
|
|
19
19
|
"dependencies": {
|
|
20
|
-
"@fission-ai/openspec": "1.2.0"
|
|
21
|
-
"@th0rgal/ralph-wiggum": "1.2.2"
|
|
20
|
+
"@fission-ai/openspec": "1.2.0"
|
|
22
21
|
},
|
|
23
22
|
"devDependencies": {
|
|
24
23
|
"@types/jest": "^29.5.0",
|
|
@@ -49,7 +48,6 @@
|
|
|
49
48
|
},
|
|
50
49
|
"keywords": [
|
|
51
50
|
"openspec",
|
|
52
|
-
"ralph-wiggum",
|
|
53
51
|
"opencode",
|
|
54
52
|
"iterative-development",
|
|
55
53
|
"agentic-coding"
|
|
@@ -62,6 +60,7 @@
|
|
|
62
60
|
},
|
|
63
61
|
"files": [
|
|
64
62
|
"bin/",
|
|
63
|
+
"lib/",
|
|
65
64
|
"scripts/",
|
|
66
65
|
"README.md",
|
|
67
66
|
"QUICKSTART.md"
|
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* mini-ralph-cli.js - Private runner entrypoint for the internal mini Ralph loop engine.
|
|
6
|
+
*
|
|
7
|
+
* This script is invoked by scripts/ralph-run.sh after it prepares the OpenSpec
|
|
8
|
+
* artifacts. It is NOT a documented end-user interface. Users should use ralph-run.
|
|
9
|
+
*
|
|
10
|
+
* Usage (internal):
|
|
11
|
+
* node scripts/mini-ralph-cli.js [options]
|
|
12
|
+
*
|
|
13
|
+
* Options:
|
|
14
|
+
* --prompt-file <path> Prompt file to use (required unless --prompt-text given)
|
|
15
|
+
* --prompt-text <text> Inline prompt text
|
|
16
|
+
* --prompt-template <path> Prompt template file
|
|
17
|
+
* --ralph-dir <path> .ralph/ directory (default: .ralph)
|
|
18
|
+
* --tasks-file <path> Path to tasks.md for tasks mode
|
|
19
|
+
* --tasks Enable tasks mode
|
|
20
|
+
* --min-iterations <n> Minimum iterations (default: 1)
|
|
21
|
+
* --max-iterations <n> Maximum iterations (default: 50)
|
|
22
|
+
* --completion-promise <s> Completion promise string (default: COMPLETE)
|
|
23
|
+
* --task-promise <s> Task promise string (default: READY_FOR_NEXT_TASK)
|
|
24
|
+
* --no-commit Suppress auto-commit
|
|
25
|
+
* --model <name> Optional model override
|
|
26
|
+
* --verbose Verbose output
|
|
27
|
+
* --status Print status dashboard and exit
|
|
28
|
+
* --add-context <text> Add pending context and exit
|
|
29
|
+
* --clear-context Clear pending context and exit
|
|
30
|
+
* --help Show this help
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
const path = require('path');
|
|
34
|
+
const miniRalph = require('../lib/mini-ralph/index');
|
|
35
|
+
|
|
36
|
+
// ---------------------------------------------------------------------------
|
|
37
|
+
// Argument parsing
|
|
38
|
+
// ---------------------------------------------------------------------------
|
|
39
|
+
|
|
40
|
+
function parseArgs(argv) {
|
|
41
|
+
const args = argv.slice(2);
|
|
42
|
+
const opts = {
|
|
43
|
+
promptFile: null,
|
|
44
|
+
promptText: null,
|
|
45
|
+
promptTemplate: null,
|
|
46
|
+
ralphDir: '.ralph',
|
|
47
|
+
tasksFile: null,
|
|
48
|
+
tasksMode: false,
|
|
49
|
+
minIterations: 1,
|
|
50
|
+
maxIterations: 50,
|
|
51
|
+
completionPromise: 'COMPLETE',
|
|
52
|
+
taskPromise: 'READY_FOR_NEXT_TASK',
|
|
53
|
+
noCommit: false,
|
|
54
|
+
model: '',
|
|
55
|
+
verbose: false,
|
|
56
|
+
// Control commands
|
|
57
|
+
status: false,
|
|
58
|
+
addContext: null,
|
|
59
|
+
clearContext: false,
|
|
60
|
+
help: false,
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
let i = 0;
|
|
64
|
+
while (i < args.length) {
|
|
65
|
+
const arg = args[i];
|
|
66
|
+
switch (arg) {
|
|
67
|
+
case '--prompt-file':
|
|
68
|
+
opts.promptFile = args[++i];
|
|
69
|
+
break;
|
|
70
|
+
case '--prompt-text':
|
|
71
|
+
opts.promptText = args[++i];
|
|
72
|
+
break;
|
|
73
|
+
case '--prompt-template':
|
|
74
|
+
opts.promptTemplate = args[++i];
|
|
75
|
+
break;
|
|
76
|
+
case '--ralph-dir':
|
|
77
|
+
opts.ralphDir = args[++i];
|
|
78
|
+
break;
|
|
79
|
+
case '--tasks-file':
|
|
80
|
+
opts.tasksFile = args[++i];
|
|
81
|
+
break;
|
|
82
|
+
case '--tasks':
|
|
83
|
+
opts.tasksMode = true;
|
|
84
|
+
break;
|
|
85
|
+
case '--min-iterations':
|
|
86
|
+
opts.minIterations = parseInt(args[++i], 10);
|
|
87
|
+
break;
|
|
88
|
+
case '--max-iterations':
|
|
89
|
+
opts.maxIterations = parseInt(args[++i], 10);
|
|
90
|
+
break;
|
|
91
|
+
case '--completion-promise':
|
|
92
|
+
opts.completionPromise = args[++i];
|
|
93
|
+
break;
|
|
94
|
+
case '--task-promise':
|
|
95
|
+
opts.taskPromise = args[++i];
|
|
96
|
+
break;
|
|
97
|
+
case '--no-commit':
|
|
98
|
+
opts.noCommit = true;
|
|
99
|
+
break;
|
|
100
|
+
case '--model':
|
|
101
|
+
opts.model = args[++i];
|
|
102
|
+
break;
|
|
103
|
+
case '--verbose':
|
|
104
|
+
opts.verbose = true;
|
|
105
|
+
break;
|
|
106
|
+
case '--status':
|
|
107
|
+
opts.status = true;
|
|
108
|
+
break;
|
|
109
|
+
case '--add-context':
|
|
110
|
+
opts.addContext = args[++i];
|
|
111
|
+
break;
|
|
112
|
+
case '--clear-context':
|
|
113
|
+
opts.clearContext = true;
|
|
114
|
+
break;
|
|
115
|
+
case '--help':
|
|
116
|
+
case '-h':
|
|
117
|
+
opts.help = true;
|
|
118
|
+
break;
|
|
119
|
+
default:
|
|
120
|
+
process.stderr.write(`mini-ralph-cli: unknown option: ${arg}\n`);
|
|
121
|
+
process.exit(1);
|
|
122
|
+
}
|
|
123
|
+
i++;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return opts;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function printHelp() {
|
|
130
|
+
process.stdout.write(`
|
|
131
|
+
mini-ralph-cli - Internal mini Ralph loop engine runner
|
|
132
|
+
|
|
133
|
+
This is an internal script. Use ralph-run as the documented interface.
|
|
134
|
+
|
|
135
|
+
Options:
|
|
136
|
+
--prompt-file <path> Prompt file
|
|
137
|
+
--prompt-text <text> Inline prompt text
|
|
138
|
+
--prompt-template <path> Prompt template file
|
|
139
|
+
--ralph-dir <path> .ralph/ directory (default: .ralph)
|
|
140
|
+
--tasks-file <path> Path to tasks.md
|
|
141
|
+
--tasks Enable tasks mode
|
|
142
|
+
--min-iterations <n> Minimum iterations (default: 1)
|
|
143
|
+
--max-iterations <n> Maximum iterations (default: 50)
|
|
144
|
+
--completion-promise <s> Completion promise string
|
|
145
|
+
--task-promise <s> Task promise string
|
|
146
|
+
--no-commit Suppress auto-commit
|
|
147
|
+
--model <name> Model override
|
|
148
|
+
--verbose Verbose output
|
|
149
|
+
--status Print status dashboard and exit
|
|
150
|
+
--add-context <text> Add pending context and exit
|
|
151
|
+
--clear-context Clear pending context and exit
|
|
152
|
+
--help Show this help
|
|
153
|
+
`.trim() + '\n');
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// ---------------------------------------------------------------------------
|
|
157
|
+
// Main
|
|
158
|
+
// ---------------------------------------------------------------------------
|
|
159
|
+
|
|
160
|
+
async function main() {
|
|
161
|
+
const opts = parseArgs(process.argv);
|
|
162
|
+
|
|
163
|
+
if (opts.help) {
|
|
164
|
+
printHelp();
|
|
165
|
+
process.exit(0);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const ralphDir = path.resolve(opts.ralphDir);
|
|
169
|
+
|
|
170
|
+
// Handle control commands (status, add-context, clear-context)
|
|
171
|
+
if (opts.status) {
|
|
172
|
+
const tasksFile = opts.tasksFile ? path.resolve(opts.tasksFile) : null;
|
|
173
|
+
const output = miniRalph.getStatus(ralphDir, tasksFile);
|
|
174
|
+
process.stdout.write(output + '\n');
|
|
175
|
+
process.exit(0);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (opts.addContext !== null) {
|
|
179
|
+
miniRalph.addContext(ralphDir, opts.addContext);
|
|
180
|
+
process.stdout.write(`Context added to ${path.join(ralphDir, 'ralph-context.md')}\n`);
|
|
181
|
+
process.exit(0);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
if (opts.clearContext) {
|
|
185
|
+
miniRalph.clearContext(ralphDir);
|
|
186
|
+
process.stdout.write('Pending context cleared.\n');
|
|
187
|
+
process.exit(0);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Run the loop
|
|
191
|
+
const runOpts = {
|
|
192
|
+
promptFile: opts.promptFile ? path.resolve(opts.promptFile) : null,
|
|
193
|
+
promptText: opts.promptText || null,
|
|
194
|
+
promptTemplate: opts.promptTemplate ? path.resolve(opts.promptTemplate) : null,
|
|
195
|
+
ralphDir,
|
|
196
|
+
tasksFile: opts.tasksFile ? path.resolve(opts.tasksFile) : null,
|
|
197
|
+
tasksMode: opts.tasksMode,
|
|
198
|
+
minIterations: opts.minIterations,
|
|
199
|
+
maxIterations: opts.maxIterations,
|
|
200
|
+
completionPromise: opts.completionPromise,
|
|
201
|
+
taskPromise: opts.taskPromise,
|
|
202
|
+
noCommit: opts.noCommit,
|
|
203
|
+
model: opts.model,
|
|
204
|
+
verbose: opts.verbose,
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
try {
|
|
208
|
+
const result = await miniRalph.run(runOpts);
|
|
209
|
+
|
|
210
|
+
if (opts.verbose) {
|
|
211
|
+
process.stderr.write(
|
|
212
|
+
`[mini-ralph] loop finished: completed=${result.completed} iterations=${result.iterations} reason=${result.exitReason}\n`
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
process.exit(result.completed ? 0 : 1);
|
|
217
|
+
} catch (err) {
|
|
218
|
+
process.stderr.write(`[mini-ralph] error: ${err.message}\n`);
|
|
219
|
+
if (opts.verbose && err.stack) {
|
|
220
|
+
process.stderr.write(err.stack + '\n');
|
|
221
|
+
}
|
|
222
|
+
process.exit(1);
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
main();
|