spec-and-loop 1.0.8 → 2.0.0-rc.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.
@@ -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": "1.0.8",
3
+ "version": "2.0.0-rc.1",
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();