ralph-codex 0.1.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/CHANGELOG.md +6 -0
- package/LICENSE +21 -0
- package/README.md +216 -0
- package/bin/ralph-codex.js +56 -0
- package/package.json +49 -0
- package/src/commands/docker.js +174 -0
- package/src/commands/init.js +413 -0
- package/src/commands/plan.js +1129 -0
- package/src/commands/refine.js +1 -0
- package/src/commands/reset.js +105 -0
- package/src/commands/revise.js +571 -0
- package/src/commands/run.js +1225 -0
- package/src/commands/view.js +695 -0
- package/src/ui/terminal.js +137 -0
- package/templates/ralph.config.yml +53 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
import "./revise.js";
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import fs from "fs";
|
|
2
|
+
import path from "path";
|
|
3
|
+
import yaml from "js-yaml";
|
|
4
|
+
import { colors } from "../ui/terminal.js";
|
|
5
|
+
|
|
6
|
+
const root = process.cwd();
|
|
7
|
+
const argv = process.argv.slice(2);
|
|
8
|
+
|
|
9
|
+
let tasksPath = "tasks.md";
|
|
10
|
+
let configPath = null;
|
|
11
|
+
let showHelp = false;
|
|
12
|
+
|
|
13
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
14
|
+
const arg = argv[i];
|
|
15
|
+
if (arg === "--help" || arg === "-h" || arg === "help") {
|
|
16
|
+
showHelp = true;
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
if (arg === "--tasks") {
|
|
20
|
+
tasksPath = argv[i + 1];
|
|
21
|
+
i += 1;
|
|
22
|
+
continue;
|
|
23
|
+
}
|
|
24
|
+
if (arg === "--config") {
|
|
25
|
+
configPath = argv[i + 1];
|
|
26
|
+
i += 1;
|
|
27
|
+
continue;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function printHelp() {
|
|
32
|
+
process.stdout.write(
|
|
33
|
+
`\n${colors.cyan("ralph-codex reset [options]")}\n\n` +
|
|
34
|
+
`${colors.yellow("Options:")}\n` +
|
|
35
|
+
` ${colors.green("--tasks <path>")} Tasks file to reset (default: tasks.md)\n` +
|
|
36
|
+
` ${colors.green("--config <path>")} Path to ralph.config.yml\n` +
|
|
37
|
+
` ${colors.green("-h, --help")} Show help\n\n`
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
if (showHelp) {
|
|
42
|
+
printHelp();
|
|
43
|
+
process.exit(0);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function loadConfig(configFilePath) {
|
|
47
|
+
if (!configFilePath) return {};
|
|
48
|
+
if (!fs.existsSync(configFilePath)) return {};
|
|
49
|
+
try {
|
|
50
|
+
const content = fs.readFileSync(configFilePath, "utf8");
|
|
51
|
+
return yaml.load(content) || {};
|
|
52
|
+
} catch (error) {
|
|
53
|
+
console.error(
|
|
54
|
+
`Failed to read config at ${configFilePath}: ${error?.message || error}`
|
|
55
|
+
);
|
|
56
|
+
process.exit(1);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const resolvedConfigPath = configPath || path.join(root, "ralph.config.yml");
|
|
61
|
+
const config = loadConfig(resolvedConfigPath);
|
|
62
|
+
const planConfig = config?.plan || {};
|
|
63
|
+
const runConfig = config?.run || {};
|
|
64
|
+
if (tasksPath === "tasks.md") {
|
|
65
|
+
if (planConfig.tasks_path) {
|
|
66
|
+
tasksPath = planConfig.tasks_path;
|
|
67
|
+
} else if (runConfig.tasks_path) {
|
|
68
|
+
tasksPath = runConfig.tasks_path;
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const tasksFile = path.join(root, tasksPath);
|
|
73
|
+
if (!fs.existsSync(tasksFile)) {
|
|
74
|
+
console.error(`Missing ${tasksPath}. Run ralph-codex plan or create it first.`);
|
|
75
|
+
process.exit(1);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const content = fs.readFileSync(tasksFile, "utf8");
|
|
79
|
+
const lines = content.split(/\r?\n/);
|
|
80
|
+
let changed = 0;
|
|
81
|
+
const updatedLines = lines.map((line) => {
|
|
82
|
+
const updated = line.replace(
|
|
83
|
+
/^(\s*(?:[-*]|\d+[.)])\s+\[)[xX~](\])/,
|
|
84
|
+
"$1 $2"
|
|
85
|
+
);
|
|
86
|
+
if (updated !== line) changed += 1;
|
|
87
|
+
return updated;
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
if (changed === 0) {
|
|
91
|
+
process.stdout.write(
|
|
92
|
+
`No completed or blocked tasks to reset in ${path.relative(root, tasksFile)}.\n`
|
|
93
|
+
);
|
|
94
|
+
process.exit(0);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
const trailingNewline = content.endsWith("\n");
|
|
98
|
+
const output = `${updatedLines.join("\n")}${trailingNewline ? "\n" : ""}`;
|
|
99
|
+
fs.writeFileSync(tasksFile, output, "utf8");
|
|
100
|
+
process.stdout.write(
|
|
101
|
+
`Reset ${changed} task${changed === 1 ? "" : "s"} in ${path.relative(
|
|
102
|
+
root,
|
|
103
|
+
tasksFile
|
|
104
|
+
)}.\n`
|
|
105
|
+
);
|
|
@@ -0,0 +1,571 @@
|
|
|
1
|
+
import { spawn, spawnSync } from "child_process";
|
|
2
|
+
import fs from "fs";
|
|
3
|
+
import path from "path";
|
|
4
|
+
import enquirer from "enquirer";
|
|
5
|
+
import yaml from "js-yaml";
|
|
6
|
+
import { colors, createLogStyler, createSpinner } from "../ui/terminal.js";
|
|
7
|
+
|
|
8
|
+
const { AutoComplete, Confirm, Editor, Input } = enquirer;
|
|
9
|
+
|
|
10
|
+
const root = process.cwd();
|
|
11
|
+
const agentDir = path.join(root, ".ralph");
|
|
12
|
+
|
|
13
|
+
const argv = process.argv.slice(2);
|
|
14
|
+
let tasksPath = "tasks.md";
|
|
15
|
+
let configPath = null;
|
|
16
|
+
let model = null;
|
|
17
|
+
let profile = null;
|
|
18
|
+
let sandbox = null;
|
|
19
|
+
let noSandbox = false;
|
|
20
|
+
let fullAuto = false;
|
|
21
|
+
let askForApproval = null;
|
|
22
|
+
let modelReasoningEffort = null;
|
|
23
|
+
let reasoningChoice;
|
|
24
|
+
let runAfter = false;
|
|
25
|
+
let showHelp = false;
|
|
26
|
+
const feedbackParts = [];
|
|
27
|
+
|
|
28
|
+
for (let i = 0; i < argv.length; i += 1) {
|
|
29
|
+
const arg = argv[i];
|
|
30
|
+
if (arg === "--help" || arg === "-h" || arg === "help") {
|
|
31
|
+
showHelp = true;
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
if (arg === "--tasks") {
|
|
35
|
+
tasksPath = argv[i + 1];
|
|
36
|
+
i += 1;
|
|
37
|
+
continue;
|
|
38
|
+
}
|
|
39
|
+
if (arg === "--config") {
|
|
40
|
+
configPath = argv[i + 1];
|
|
41
|
+
i += 1;
|
|
42
|
+
continue;
|
|
43
|
+
}
|
|
44
|
+
if (arg === "--model" || arg === "-m") {
|
|
45
|
+
model = argv[i + 1];
|
|
46
|
+
i += 1;
|
|
47
|
+
continue;
|
|
48
|
+
}
|
|
49
|
+
if (arg === "--profile" || arg === "-p") {
|
|
50
|
+
profile = argv[i + 1];
|
|
51
|
+
i += 1;
|
|
52
|
+
continue;
|
|
53
|
+
}
|
|
54
|
+
if (arg === "--sandbox") {
|
|
55
|
+
sandbox = argv[i + 1];
|
|
56
|
+
i += 1;
|
|
57
|
+
continue;
|
|
58
|
+
}
|
|
59
|
+
if (arg === "--no-sandbox") {
|
|
60
|
+
noSandbox = true;
|
|
61
|
+
continue;
|
|
62
|
+
}
|
|
63
|
+
if (arg === "--ask-for-approval") {
|
|
64
|
+
askForApproval = argv[i + 1];
|
|
65
|
+
i += 1;
|
|
66
|
+
continue;
|
|
67
|
+
}
|
|
68
|
+
if (arg === "--full-auto") {
|
|
69
|
+
fullAuto = true;
|
|
70
|
+
continue;
|
|
71
|
+
}
|
|
72
|
+
if (arg === "--reasoning") {
|
|
73
|
+
const next = argv[i + 1];
|
|
74
|
+
if (next && !next.startsWith("-")) {
|
|
75
|
+
reasoningChoice = next;
|
|
76
|
+
i += 1;
|
|
77
|
+
} else {
|
|
78
|
+
reasoningChoice = "__prompt__";
|
|
79
|
+
}
|
|
80
|
+
continue;
|
|
81
|
+
}
|
|
82
|
+
if (arg === "--run") {
|
|
83
|
+
runAfter = true;
|
|
84
|
+
continue;
|
|
85
|
+
}
|
|
86
|
+
feedbackParts.push(arg);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function printHelp() {
|
|
90
|
+
process.stdout.write(
|
|
91
|
+
`\n${colors.cyan("ralph-codex revise \"<feedback>\" [options]")}\n\n` +
|
|
92
|
+
`${colors.yellow("Options:")}\n` +
|
|
93
|
+
` ${colors.green("--tasks <path>")} Tasks file to update (default: tasks.md)\n` +
|
|
94
|
+
` ${colors.green("--config <path>")} Path to ralph.config.yml\n` +
|
|
95
|
+
` ${colors.green("--model <name>, -m")} Codex model\n` +
|
|
96
|
+
` ${colors.green("--profile <name>, -p")} Codex CLI profile\n` +
|
|
97
|
+
` ${colors.green("--sandbox <mode>")} read-only | workspace-write | danger-full-access\n` +
|
|
98
|
+
` ${colors.green("--no-sandbox")} Use danger-full-access\n` +
|
|
99
|
+
` ${colors.green("--ask-for-approval <mode>")} untrusted | on-failure | on-request | never\n` +
|
|
100
|
+
` ${colors.green("--full-auto")} workspace-write + on-request\n` +
|
|
101
|
+
` ${colors.green("--reasoning [effort]")} low | medium | high | xhigh (omit to pick)\n` +
|
|
102
|
+
` ${colors.green("--run")} Run after approving changes\n` +
|
|
103
|
+
` ${colors.green("-h, --help")} Show help\n\n` +
|
|
104
|
+
`${colors.yellow("Examples:")}\n` +
|
|
105
|
+
` ralph-codex revise "Improve onboarding copy"\n` +
|
|
106
|
+
` ralph-codex revise "Fix layout issues" --run\n\n`
|
|
107
|
+
);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (showHelp) {
|
|
111
|
+
printHelp();
|
|
112
|
+
process.exit(0);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function loadConfig(configFilePath) {
|
|
116
|
+
if (!configFilePath) return {};
|
|
117
|
+
if (!fs.existsSync(configFilePath)) return {};
|
|
118
|
+
try {
|
|
119
|
+
const content = fs.readFileSync(configFilePath, "utf8");
|
|
120
|
+
return yaml.load(content) || {};
|
|
121
|
+
} catch (error) {
|
|
122
|
+
console.error(
|
|
123
|
+
`Failed to read config at ${configFilePath}: ${error?.message || error}`
|
|
124
|
+
);
|
|
125
|
+
process.exit(1);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function resolveTasksPath(config, currentPath) {
|
|
130
|
+
if (currentPath !== "tasks.md") return currentPath;
|
|
131
|
+
if (config?.plan?.tasks_path) return config.plan.tasks_path;
|
|
132
|
+
if (config?.run?.tasks_path) return config.run.tasks_path;
|
|
133
|
+
return currentPath;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function normalizeReasoningEffort(value) {
|
|
137
|
+
if (value === null || value === undefined) return null;
|
|
138
|
+
const trimmed = String(value).trim();
|
|
139
|
+
if (!trimmed) return null;
|
|
140
|
+
const lowered = trimmed.toLowerCase();
|
|
141
|
+
if (["null", "unset", "none", "default"].includes(lowered)) return null;
|
|
142
|
+
if (lowered === "extra-high" || lowered === "extra_high") return "xhigh";
|
|
143
|
+
if (["low", "medium", "high", "xhigh"].includes(lowered)) return lowered;
|
|
144
|
+
return trimmed;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
async function promptReasoningEffort(currentValue) {
|
|
148
|
+
const choices = [
|
|
149
|
+
{
|
|
150
|
+
name: "unset",
|
|
151
|
+
message: "unset (null)",
|
|
152
|
+
value: null,
|
|
153
|
+
hint: "Use the Codex default",
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
name: "low",
|
|
157
|
+
message: "low",
|
|
158
|
+
value: "low",
|
|
159
|
+
hint: "Faster, less thorough reasoning.",
|
|
160
|
+
},
|
|
161
|
+
{
|
|
162
|
+
name: "medium",
|
|
163
|
+
message: "medium",
|
|
164
|
+
value: "medium",
|
|
165
|
+
hint: "Default balance of speed + depth.",
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
name: "high",
|
|
169
|
+
message: "high",
|
|
170
|
+
value: "high",
|
|
171
|
+
hint: "Deeper reasoning, slower.",
|
|
172
|
+
},
|
|
173
|
+
{
|
|
174
|
+
name: "xhigh",
|
|
175
|
+
message: "xhigh",
|
|
176
|
+
value: "xhigh",
|
|
177
|
+
hint: "Maximum depth, slowest.",
|
|
178
|
+
},
|
|
179
|
+
];
|
|
180
|
+
const normalized = normalizeReasoningEffort(currentValue) || "medium";
|
|
181
|
+
const initial = Math.max(
|
|
182
|
+
0,
|
|
183
|
+
choices.findIndex((choice) => choice.value === normalized)
|
|
184
|
+
);
|
|
185
|
+
const prompt = new AutoComplete({
|
|
186
|
+
name: "reasoning",
|
|
187
|
+
message: "Select model reasoning effort:",
|
|
188
|
+
choices,
|
|
189
|
+
initial,
|
|
190
|
+
limit: Math.min(choices.length, 7),
|
|
191
|
+
});
|
|
192
|
+
return prompt.run();
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function resetStdin() {
|
|
196
|
+
if (process.stdin.isTTY && typeof process.stdin.setRawMode === "function") {
|
|
197
|
+
process.stdin.setRawMode(false);
|
|
198
|
+
}
|
|
199
|
+
process.stdin.resume();
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
async function readFeedback(promptMessage) {
|
|
203
|
+
resetStdin();
|
|
204
|
+
if (process.stdin.isTTY) {
|
|
205
|
+
try {
|
|
206
|
+
const editor = new Editor({
|
|
207
|
+
name: "feedback",
|
|
208
|
+
message: promptMessage ||
|
|
209
|
+
"Enter revision feedback in the editor, then save and close:",
|
|
210
|
+
});
|
|
211
|
+
const value = await editor.run();
|
|
212
|
+
if (String(value || "").trim()) return value;
|
|
213
|
+
} catch (_) {
|
|
214
|
+
// Fall through to input.
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const input = new Input({
|
|
219
|
+
name: "feedback",
|
|
220
|
+
message: promptMessage ||
|
|
221
|
+
"Enter revision feedback (single line). Use ';' to separate items:",
|
|
222
|
+
});
|
|
223
|
+
return input.run();
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function parseTasks(content) {
|
|
227
|
+
const tasks = [];
|
|
228
|
+
const lines = content.split(/\r?\n/);
|
|
229
|
+
for (const line of lines) {
|
|
230
|
+
const match = line.match(/^\s*[-*]\s+\[([ x~])\]\s+(.*)$/);
|
|
231
|
+
if (!match) continue;
|
|
232
|
+
const statusToken = match[1].toLowerCase();
|
|
233
|
+
const status =
|
|
234
|
+
statusToken === "x" ? "done" : statusToken === "~" ? "blocked" : "pending";
|
|
235
|
+
tasks.push({
|
|
236
|
+
status,
|
|
237
|
+
text: match[2].trim(),
|
|
238
|
+
raw: line.trim(),
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
return tasks;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function normalizeTaskText(text) {
|
|
245
|
+
return String(text || "")
|
|
246
|
+
.toLowerCase()
|
|
247
|
+
.replace(/\s+/g, " ")
|
|
248
|
+
.trim();
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function parseSuccessCriteria(content) {
|
|
252
|
+
const lines = content.split(/\r?\n/);
|
|
253
|
+
let start = -1;
|
|
254
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
255
|
+
if (/^(#+\s*)?success criteria\b/i.test(lines[i].trim())) {
|
|
256
|
+
start = i;
|
|
257
|
+
break;
|
|
258
|
+
}
|
|
259
|
+
}
|
|
260
|
+
if (start === -1) return [];
|
|
261
|
+
const items = [];
|
|
262
|
+
for (let i = start + 1; i < lines.length; i += 1) {
|
|
263
|
+
const line = lines[i].trim();
|
|
264
|
+
if (!line) continue;
|
|
265
|
+
if (/^#+\s+/.test(line)) break;
|
|
266
|
+
if (line.startsWith("- ")) items.push(line.slice(2).trim());
|
|
267
|
+
if (line.startsWith("* ")) items.push(line.slice(2).trim());
|
|
268
|
+
}
|
|
269
|
+
return items;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
function diffTasks(oldTasks, newTasks) {
|
|
273
|
+
const oldSet = new Set(oldTasks.map((task) => normalizeTaskText(task.text)));
|
|
274
|
+
const newSet = new Set(newTasks.map((task) => normalizeTaskText(task.text)));
|
|
275
|
+
|
|
276
|
+
const additions = newTasks.filter(
|
|
277
|
+
(task) => !oldSet.has(normalizeTaskText(task.text))
|
|
278
|
+
);
|
|
279
|
+
const removals = oldTasks.filter(
|
|
280
|
+
(task) => !newSet.has(normalizeTaskText(task.text))
|
|
281
|
+
);
|
|
282
|
+
|
|
283
|
+
const modified = [];
|
|
284
|
+
const compareCount = Math.min(oldTasks.length, newTasks.length);
|
|
285
|
+
for (let i = 0; i < compareCount; i += 1) {
|
|
286
|
+
const before = oldTasks[i];
|
|
287
|
+
const after = newTasks[i];
|
|
288
|
+
if (
|
|
289
|
+
before.status !== after.status ||
|
|
290
|
+
normalizeTaskText(before.text) !== normalizeTaskText(after.text)
|
|
291
|
+
) {
|
|
292
|
+
modified.push({
|
|
293
|
+
index: i + 1,
|
|
294
|
+
before,
|
|
295
|
+
after,
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
return { additions, removals, modified };
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
function diffCriteria(oldCriteria, newCriteria) {
|
|
304
|
+
const oldSet = new Set(oldCriteria);
|
|
305
|
+
const newSet = new Set(newCriteria);
|
|
306
|
+
const added = newCriteria.filter((item) => !oldSet.has(item));
|
|
307
|
+
const removed = oldCriteria.filter((item) => !newSet.has(item));
|
|
308
|
+
return { added, removed };
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
function renderChanges(changes) {
|
|
312
|
+
if (changes.additions.length > 0) {
|
|
313
|
+
process.stdout.write(`${colors.cyan("Proposed new tasks:")}\n`);
|
|
314
|
+
changes.additions.forEach((task) => {
|
|
315
|
+
const status =
|
|
316
|
+
task.status === "done" ? "[x]" : task.status === "blocked" ? "[~]" : "[ ]";
|
|
317
|
+
process.stdout.write(`- ${status} ${task.text}\n`);
|
|
318
|
+
});
|
|
319
|
+
process.stdout.write("\n");
|
|
320
|
+
} else {
|
|
321
|
+
process.stdout.write(`${colors.cyan("Proposed new tasks:")} none\n\n`);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
const criteriaHasChanges =
|
|
325
|
+
changes.criteria.added.length > 0 || changes.criteria.removed.length > 0;
|
|
326
|
+
if (criteriaHasChanges) {
|
|
327
|
+
process.stdout.write(`${colors.cyan("Success criteria changes:")}\n`);
|
|
328
|
+
changes.criteria.added.forEach((item) => {
|
|
329
|
+
process.stdout.write(`${colors.green(`+ ${item}`)}\n`);
|
|
330
|
+
});
|
|
331
|
+
changes.criteria.removed.forEach((item) => {
|
|
332
|
+
process.stdout.write(`${colors.yellow(`- ${item}`)}\n`);
|
|
333
|
+
});
|
|
334
|
+
process.stdout.write("\n");
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
const hasUnexpected =
|
|
338
|
+
changes.modified.length > 0 || changes.removals.length > 0;
|
|
339
|
+
if (hasUnexpected) {
|
|
340
|
+
process.stdout.write(
|
|
341
|
+
`${colors.yellow("Warning: existing tasks changed (expected append-only).")}\n`
|
|
342
|
+
);
|
|
343
|
+
const preview = changes.modified.slice(0, 5);
|
|
344
|
+
preview.forEach((item) => {
|
|
345
|
+
process.stdout.write(
|
|
346
|
+
`- #${item.index} ${item.before.raw} -> ${item.after.raw}\n`
|
|
347
|
+
);
|
|
348
|
+
});
|
|
349
|
+
if (changes.modified.length > preview.length) {
|
|
350
|
+
process.stdout.write(
|
|
351
|
+
`${colors.yellow(`...and ${changes.modified.length - preview.length} more changes`)}\n`
|
|
352
|
+
);
|
|
353
|
+
}
|
|
354
|
+
if (changes.removals.length > 0) {
|
|
355
|
+
process.stdout.write(
|
|
356
|
+
`${colors.yellow(`Removed tasks: ${changes.removals.length}`)}\n`
|
|
357
|
+
);
|
|
358
|
+
}
|
|
359
|
+
process.stdout.write("\n");
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
function buildPrompt(params) {
|
|
364
|
+
const {
|
|
365
|
+
feedback,
|
|
366
|
+
revisionFeedback,
|
|
367
|
+
tasksFileRelative,
|
|
368
|
+
outputRelative,
|
|
369
|
+
} = params;
|
|
370
|
+
|
|
371
|
+
return `# Ralph Revise\n\nYou are updating the task list based on feedback.\n\nFiles:\n- Current tasks: ${tasksFileRelative}\n- Output file: ${outputRelative}\n\nFeedback:\n${feedback}\n${revisionFeedback ? `\nRevision feedback:\n${revisionFeedback}\n` : ""}\nRules:\n- Read ${tasksFileRelative} and write updated tasks to ${outputRelative}.\n- Do NOT edit ${tasksFileRelative} directly.\n- Do not remove or modify existing tasks or their statuses.\n- Append new tasks immediately after the last existing task and before the Success criteria section.\n- New tasks must be atomic, ordered, and verifiable. Use \`- [ ]\` checkboxes.\n- If a task is incomplete, add a new task that references the original task and why it is incomplete.\n- Update the Success criteria section only if needed to reflect the feedback.\n- Keep the Required tools section format exactly as-is.\n- Do not edit any other files.\n- You may run read-only commands (cat, rg, ls) to inspect files.\n- Do not run tests or write files other than ${outputRelative}.\n\nWhen done, output exactly: LOOP_COMPLETE\n`;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
async function runCodex(prompt, codexOptions, spinnerText) {
|
|
375
|
+
const args = ["exec"];
|
|
376
|
+
if (codexOptions.model) args.push("--model", codexOptions.model);
|
|
377
|
+
if (codexOptions.profile) args.push("--profile", codexOptions.profile);
|
|
378
|
+
if (codexOptions.fullAuto) args.push("--full-auto");
|
|
379
|
+
if (codexOptions.askForApproval) {
|
|
380
|
+
args.push("--config", `ask_for_approval=${codexOptions.askForApproval}`);
|
|
381
|
+
}
|
|
382
|
+
if (codexOptions.modelReasoningEffort) {
|
|
383
|
+
args.push(
|
|
384
|
+
"--config",
|
|
385
|
+
`model_reasoning_effort=${codexOptions.modelReasoningEffort}`
|
|
386
|
+
);
|
|
387
|
+
}
|
|
388
|
+
if (codexOptions.sandbox) args.push("--sandbox", codexOptions.sandbox);
|
|
389
|
+
args.push("-");
|
|
390
|
+
|
|
391
|
+
const spinner = createSpinner(spinnerText || "Updating tasks...");
|
|
392
|
+
return new Promise((resolve) => {
|
|
393
|
+
const styler = createLogStyler();
|
|
394
|
+
const child = spawn("codex", args, {
|
|
395
|
+
cwd: root,
|
|
396
|
+
env: process.env,
|
|
397
|
+
stdio: ["pipe", "pipe", "pipe"],
|
|
398
|
+
});
|
|
399
|
+
let stdout = "";
|
|
400
|
+
let stderr = "";
|
|
401
|
+
child.stdout.on("data", (data) => {
|
|
402
|
+
stdout += data.toString();
|
|
403
|
+
});
|
|
404
|
+
child.stderr.on("data", (data) => {
|
|
405
|
+
stderr += data.toString();
|
|
406
|
+
});
|
|
407
|
+
child.on("error", (error) => {
|
|
408
|
+
spinner.stop();
|
|
409
|
+
console.error(error?.message || error);
|
|
410
|
+
process.exit(1);
|
|
411
|
+
});
|
|
412
|
+
child.on("close", (code) => {
|
|
413
|
+
spinner.stop();
|
|
414
|
+
const combined = `${stdout}\n${stderr}`;
|
|
415
|
+
const lines = combined.split(/\r?\n/);
|
|
416
|
+
for (const line of lines) {
|
|
417
|
+
if (!line) continue;
|
|
418
|
+
process.stdout.write(`${styler.formatLine(line)}\n`);
|
|
419
|
+
}
|
|
420
|
+
resolve({ status: code ?? 0, output: combined.trim() });
|
|
421
|
+
});
|
|
422
|
+
child.stdin.write(prompt);
|
|
423
|
+
child.stdin.end();
|
|
424
|
+
});
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
async function main() {
|
|
428
|
+
const resolvedConfigPath = configPath || path.join(root, "ralph.config.yml");
|
|
429
|
+
const config = loadConfig(resolvedConfigPath);
|
|
430
|
+
const codexConfig = config?.codex || {};
|
|
431
|
+
|
|
432
|
+
if (!model && codexConfig.model) model = codexConfig.model;
|
|
433
|
+
if (!profile && codexConfig.profile) profile = codexConfig.profile;
|
|
434
|
+
if (!sandbox && codexConfig.sandbox) sandbox = codexConfig.sandbox;
|
|
435
|
+
if (!askForApproval && codexConfig.ask_for_approval) {
|
|
436
|
+
askForApproval = codexConfig.ask_for_approval;
|
|
437
|
+
}
|
|
438
|
+
if (!fullAuto && codexConfig.full_auto) fullAuto = true;
|
|
439
|
+
if (!modelReasoningEffort && codexConfig.model_reasoning_effort) {
|
|
440
|
+
modelReasoningEffort = codexConfig.model_reasoning_effort;
|
|
441
|
+
}
|
|
442
|
+
if (typeof reasoningChoice !== "undefined") {
|
|
443
|
+
if (reasoningChoice === "__prompt__") {
|
|
444
|
+
modelReasoningEffort = await promptReasoningEffort(modelReasoningEffort);
|
|
445
|
+
} else {
|
|
446
|
+
modelReasoningEffort = normalizeReasoningEffort(reasoningChoice);
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
tasksPath = resolveTasksPath(config, tasksPath);
|
|
451
|
+
const tasksFilePath = path.join(root, tasksPath);
|
|
452
|
+
if (!fs.existsSync(tasksFilePath)) {
|
|
453
|
+
console.error(`Missing ${tasksPath}. Run ralph-codex plan first.`);
|
|
454
|
+
process.exit(1);
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
const feedback = feedbackParts.join(" ").trim() ||
|
|
458
|
+
(await readFeedback("Enter feedback to revise the plan:"));
|
|
459
|
+
if (!feedback || !String(feedback).trim()) {
|
|
460
|
+
console.error("No feedback provided. Aborting.");
|
|
461
|
+
process.exit(1);
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
fs.mkdirSync(agentDir, { recursive: true });
|
|
465
|
+
const proposedPath = path.join(agentDir, "tasks.revise.md");
|
|
466
|
+
const tasksFileRelative = path.relative(root, tasksFilePath);
|
|
467
|
+
const outputRelative = path.relative(root, proposedPath);
|
|
468
|
+
|
|
469
|
+
let revisionFeedback = "";
|
|
470
|
+
const codexOptions = {
|
|
471
|
+
model,
|
|
472
|
+
profile,
|
|
473
|
+
sandbox: noSandbox ? "danger-full-access" : sandbox,
|
|
474
|
+
fullAuto,
|
|
475
|
+
askForApproval,
|
|
476
|
+
modelReasoningEffort,
|
|
477
|
+
};
|
|
478
|
+
|
|
479
|
+
while (true) {
|
|
480
|
+
if (fs.existsSync(proposedPath)) {
|
|
481
|
+
fs.rmSync(proposedPath, { force: true });
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
const prompt = buildPrompt({
|
|
485
|
+
feedback,
|
|
486
|
+
revisionFeedback,
|
|
487
|
+
tasksFileRelative,
|
|
488
|
+
outputRelative,
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
const spinnerText = revisionFeedback
|
|
492
|
+
? "Updating tasks with your feedback..."
|
|
493
|
+
: "Updating tasks...";
|
|
494
|
+
await runCodex(prompt, codexOptions, spinnerText);
|
|
495
|
+
|
|
496
|
+
if (!fs.existsSync(proposedPath)) {
|
|
497
|
+
console.error("Revise did not produce a proposed tasks file.");
|
|
498
|
+
process.exit(1);
|
|
499
|
+
}
|
|
500
|
+
|
|
501
|
+
const currentContent = fs.readFileSync(tasksFilePath, "utf8");
|
|
502
|
+
const proposedContent = fs.readFileSync(proposedPath, "utf8");
|
|
503
|
+
const currentTasks = parseTasks(currentContent);
|
|
504
|
+
const proposedTasks = parseTasks(proposedContent);
|
|
505
|
+
const currentCriteria = parseSuccessCriteria(currentContent);
|
|
506
|
+
const proposedCriteria = parseSuccessCriteria(proposedContent);
|
|
507
|
+
|
|
508
|
+
const taskDiff = diffTasks(currentTasks, proposedTasks);
|
|
509
|
+
const criteriaDiff = diffCriteria(currentCriteria, proposedCriteria);
|
|
510
|
+
|
|
511
|
+
const hasAdditions = taskDiff.additions.length > 0;
|
|
512
|
+
const hasCriteriaChanges =
|
|
513
|
+
criteriaDiff.added.length > 0 || criteriaDiff.removed.length > 0;
|
|
514
|
+
const hasUnexpected = taskDiff.modified.length > 0 || taskDiff.removals.length > 0;
|
|
515
|
+
|
|
516
|
+
if (!hasAdditions && !hasCriteriaChanges && !hasUnexpected) {
|
|
517
|
+
process.stdout.write(
|
|
518
|
+
`${colors.yellow("No changes detected. Nothing to apply.")}\n`
|
|
519
|
+
);
|
|
520
|
+
process.exit(0);
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
renderChanges({
|
|
524
|
+
additions: taskDiff.additions,
|
|
525
|
+
criteria: criteriaDiff,
|
|
526
|
+
modified: taskDiff.modified,
|
|
527
|
+
removals: taskDiff.removals,
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
const confirm = new Confirm({
|
|
531
|
+
name: "confirm",
|
|
532
|
+
message: "Apply these changes to tasks.md?",
|
|
533
|
+
initial: true,
|
|
534
|
+
});
|
|
535
|
+
const approved = await confirm.run();
|
|
536
|
+
if (approved) {
|
|
537
|
+
fs.writeFileSync(tasksFilePath, proposedContent, "utf8");
|
|
538
|
+
process.stdout.write(
|
|
539
|
+
`Updated ${tasksFileRelative} with revision changes.\n`
|
|
540
|
+
);
|
|
541
|
+
break;
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
revisionFeedback = await readFeedback(
|
|
545
|
+
"Enter revision feedback (single line). Use ';' to separate items:",
|
|
546
|
+
);
|
|
547
|
+
if (!revisionFeedback || !String(revisionFeedback).trim()) {
|
|
548
|
+
console.error("No revision feedback provided. Aborting.");
|
|
549
|
+
process.exit(1);
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
if (runAfter) {
|
|
554
|
+
const runScript = path.join(root, "src", "commands", "run.js");
|
|
555
|
+
const runArgs = [];
|
|
556
|
+
if (tasksPath !== "tasks.md") {
|
|
557
|
+
runArgs.push("--tasks", tasksPath);
|
|
558
|
+
}
|
|
559
|
+
if (configPath) {
|
|
560
|
+
runArgs.push("--config", configPath);
|
|
561
|
+
}
|
|
562
|
+
const result = spawnSync(process.execPath, [runScript, ...runArgs], {
|
|
563
|
+
stdio: "inherit",
|
|
564
|
+
cwd: root,
|
|
565
|
+
env: process.env,
|
|
566
|
+
});
|
|
567
|
+
process.exit(result.status ?? 0);
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
void main();
|