ralph-codex 0.1.2 → 0.1.3
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 +5 -0
- package/package.json +8 -1
- package/src/commands/completion.js +12 -4
- package/src/commands/docker.js +8 -7
- package/src/commands/init.js +21 -7
- package/src/commands/plan.js +62 -8
- package/src/commands/revise.js +6 -85
- package/src/commands/run.js +28 -3
- package/src/commands/view.js +3 -36
- package/src/lib/tasks.js +97 -0
- package/templates/ralph.config.yml +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -2,6 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
All notable changes to this project will be documented in this file.
|
|
4
4
|
|
|
5
|
+
## [0.1.3] - 2026-01-22
|
|
6
|
+
- Added comprehensive CLI test suite with vitest, harness, and fixtures.
|
|
7
|
+
- Added GitHub Actions CI for Node 18/20/22.
|
|
8
|
+
- Guarded run completion to require all tasks checked off.
|
|
9
|
+
|
|
5
10
|
## [0.1.2] - 2026-01-22
|
|
6
11
|
- Improved README with setup, defaults, Docker guidance, and troubleshooting.
|
|
7
12
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "ralph-codex",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.3",
|
|
4
4
|
"description": "Codex-first Ralph-style planning and run loops",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -25,6 +25,10 @@
|
|
|
25
25
|
"node": ">=18"
|
|
26
26
|
},
|
|
27
27
|
"packageManager": "npm@10.8.2",
|
|
28
|
+
"scripts": {
|
|
29
|
+
"test": "vitest run",
|
|
30
|
+
"test:watch": "vitest"
|
|
31
|
+
},
|
|
28
32
|
"dependencies": {
|
|
29
33
|
"cli-progress": "^3.12.0",
|
|
30
34
|
"enquirer": "^2.4.1",
|
|
@@ -32,6 +36,9 @@
|
|
|
32
36
|
"ora": "^5.4.1",
|
|
33
37
|
"picocolors": "^1.0.0"
|
|
34
38
|
},
|
|
39
|
+
"devDependencies": {
|
|
40
|
+
"vitest": "^1.6.0"
|
|
41
|
+
},
|
|
35
42
|
"files": [
|
|
36
43
|
"bin",
|
|
37
44
|
"src",
|
|
@@ -130,9 +130,9 @@ _ralph_codex_completion_promise_list() {
|
|
|
130
130
|
|
|
131
131
|
_ralph_codex() {
|
|
132
132
|
local cur prev cmd
|
|
133
|
-
cur="
|
|
134
|
-
prev="
|
|
135
|
-
cmd="
|
|
133
|
+
cur="\${COMP_WORDS[COMP_CWORD]}"
|
|
134
|
+
prev="\${COMP_WORDS[COMP_CWORD-1]}"
|
|
135
|
+
cmd="\${COMP_WORDS[1]}"
|
|
136
136
|
|
|
137
137
|
local commands="init plan run revise refine view reset docker completion help"
|
|
138
138
|
local root_opts="--help -h --version -v"
|
|
@@ -174,6 +174,10 @@ _ralph_codex() {
|
|
|
174
174
|
COMPREPLY=( $(compgen -W "$tasks" -- "$cur") $(compgen -f -- "$cur") )
|
|
175
175
|
return 0
|
|
176
176
|
;;
|
|
177
|
+
--idea-file)
|
|
178
|
+
COMPREPLY=( $(compgen -f -- "$cur") )
|
|
179
|
+
return 0
|
|
180
|
+
;;
|
|
177
181
|
--sandbox)
|
|
178
182
|
COMPREPLY=( $(compgen -W "read-only workspace-write danger-full-access" -- "$cur") )
|
|
179
183
|
return 0
|
|
@@ -199,7 +203,7 @@ _ralph_codex() {
|
|
|
199
203
|
return 0
|
|
200
204
|
;;
|
|
201
205
|
esac
|
|
202
|
-
local opts="--output --tasks --max-iterations --config --model -m --profile -p --sandbox --no-sandbox --ask-for-approval --full-auto --reasoning --detect-success-criteria --no-detect-success-criteria --help -h"
|
|
206
|
+
local opts="--output --tasks --idea-file --stdin --max-iterations --config --model -m --profile -p --sandbox --no-sandbox --ask-for-approval --full-auto --reasoning --detect-success-criteria --no-detect-success-criteria --help -h"
|
|
203
207
|
COMPREPLY=( $(compgen -W "$opts" -- "$cur") )
|
|
204
208
|
return 0
|
|
205
209
|
;;
|
|
@@ -525,6 +529,8 @@ _ralph_codex() {
|
|
|
525
529
|
_arguments \\
|
|
526
530
|
'--output[Write tasks to a custom file]:path:_ralph_codex_tasks' \\
|
|
527
531
|
'--tasks[Write tasks to a custom file]:path:_ralph_codex_tasks' \\
|
|
532
|
+
'--idea-file[Read idea from a markdown file]:file:_files' \\
|
|
533
|
+
'--stdin[Read idea from stdin]' \\
|
|
528
534
|
'--max-iterations[Max planning iterations]:number:' \\
|
|
529
535
|
'--config[Path to ralph.config.yml]:file:_ralph_codex_configs' \\
|
|
530
536
|
'(-m --model)'{-m,--model}'[Codex model]:model:_ralph_codex_models' \\
|
|
@@ -745,6 +751,8 @@ complete -c ralph-codex -n '__fish_seen_subcommand_from init' -l no-gitignore -d
|
|
|
745
751
|
|
|
746
752
|
complete -c ralph-codex -n '__fish_seen_subcommand_from plan' -l output -r -a '(__ralph_codex_tasks)' -d 'Write tasks to a custom file'
|
|
747
753
|
complete -c ralph-codex -n '__fish_seen_subcommand_from plan' -l tasks -r -a '(__ralph_codex_tasks)' -d 'Write tasks to a custom file'
|
|
754
|
+
complete -c ralph-codex -n '__fish_seen_subcommand_from plan' -l idea-file -r -a '(__fish_complete_path)' -d 'Read idea from a markdown file'
|
|
755
|
+
complete -c ralph-codex -n '__fish_seen_subcommand_from plan' -l stdin -d 'Read idea from stdin'
|
|
748
756
|
complete -c ralph-codex -n '__fish_seen_subcommand_from plan' -l max-iterations -r -d 'Max planning iterations'
|
|
749
757
|
complete -c ralph-codex -n '__fish_seen_subcommand_from plan' -l config -r -a '(__ralph_codex_configs)' -d 'Path to ralph.config.yml'
|
|
750
758
|
complete -c ralph-codex -n '__fish_seen_subcommand_from plan' -s m -l model -r -a '(__ralph_codex_models)' -d 'Codex model'
|
package/src/commands/docker.js
CHANGED
|
@@ -8,6 +8,7 @@ const { Confirm } = enquirer;
|
|
|
8
8
|
|
|
9
9
|
const root = process.cwd();
|
|
10
10
|
const defaultConfigPath = path.join(root, "ralph.config.yml");
|
|
11
|
+
const isTestMode = process.env.RALPH_TEST_MODE === "1";
|
|
11
12
|
|
|
12
13
|
const argv = process.argv.slice(2);
|
|
13
14
|
let configPath = null;
|
|
@@ -148,13 +149,13 @@ async function main() {
|
|
|
148
149
|
process.exit(1);
|
|
149
150
|
}
|
|
150
151
|
|
|
151
|
-
const
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
152
|
+
const approved = isTestMode
|
|
153
|
+
? true
|
|
154
|
+
: await new Confirm({
|
|
155
|
+
name: "confirm",
|
|
156
|
+
message: `Set docker.enabled=true and base_image=${baseImage}?`,
|
|
157
|
+
initial: true,
|
|
158
|
+
}).run();
|
|
158
159
|
if (!approved) {
|
|
159
160
|
process.stdout.write("Aborted by user.\n");
|
|
160
161
|
process.exit(1);
|
package/src/commands/init.js
CHANGED
|
@@ -7,6 +7,7 @@ const { AutoComplete, Confirm, Input, Toggle } = enquirer;
|
|
|
7
7
|
|
|
8
8
|
const root = process.cwd();
|
|
9
9
|
const argv = process.argv.slice(2);
|
|
10
|
+
const isTestMode = process.env.RALPH_TEST_MODE === "1";
|
|
10
11
|
|
|
11
12
|
let force = false;
|
|
12
13
|
let configPath = null;
|
|
@@ -42,6 +43,7 @@ const targetPath = configPath
|
|
|
42
43
|
: path.join(root, "ralph.config.yml");
|
|
43
44
|
|
|
44
45
|
async function confirmOverwrite() {
|
|
46
|
+
if (isTestMode) return true;
|
|
45
47
|
const confirm = new Confirm({
|
|
46
48
|
name: "overwrite",
|
|
47
49
|
message: `Overwrite existing ${path.relative(root, targetPath)}?`,
|
|
@@ -224,6 +226,16 @@ async function promptModelChoice() {
|
|
|
224
226
|
}
|
|
225
227
|
|
|
226
228
|
async function collectCodexConfig() {
|
|
229
|
+
if (isTestMode) {
|
|
230
|
+
return {
|
|
231
|
+
model: null,
|
|
232
|
+
profile: null,
|
|
233
|
+
sandbox: null,
|
|
234
|
+
ask_for_approval: null,
|
|
235
|
+
full_auto: false,
|
|
236
|
+
model_reasoning_effort: null,
|
|
237
|
+
};
|
|
238
|
+
}
|
|
227
239
|
const model = await promptModelChoice();
|
|
228
240
|
const profile = await promptOptionalInput(
|
|
229
241
|
"Codex CLI profile (optional; leave blank to use Codex default)",
|
|
@@ -359,13 +371,15 @@ async function main() {
|
|
|
359
371
|
|
|
360
372
|
const content = fs.readFileSync(templatePath, "utf8");
|
|
361
373
|
const codexConfig = await collectCodexConfig();
|
|
362
|
-
const useDocker =
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
374
|
+
const useDocker = isTestMode
|
|
375
|
+
? process.env.RALPH_TEST_USE_DOCKER === "1"
|
|
376
|
+
: await new Toggle({
|
|
377
|
+
name: "use_docker",
|
|
378
|
+
message: "Use Docker for the loop? (adds a docker section)",
|
|
379
|
+
enabled: "Yes",
|
|
380
|
+
disabled: "No",
|
|
381
|
+
initial: false,
|
|
382
|
+
}).run();
|
|
369
383
|
|
|
370
384
|
let updated = content;
|
|
371
385
|
updated = setYamlValue(updated, "model", codexConfig.model);
|
package/src/commands/plan.js
CHANGED
|
@@ -10,10 +10,13 @@ const { AutoComplete, Confirm, Editor, Input, MultiSelect } = enquirer;
|
|
|
10
10
|
|
|
11
11
|
const root = process.cwd();
|
|
12
12
|
const agentDir = path.join(root, ".ralph");
|
|
13
|
+
const isTestMode = process.env.RALPH_TEST_MODE === "1";
|
|
13
14
|
|
|
14
15
|
const argv = process.argv.slice(2);
|
|
15
16
|
let maxIterations = "1";
|
|
16
17
|
let tasksPath = "tasks.md";
|
|
18
|
+
let ideaFile = null;
|
|
19
|
+
let readStdin = false;
|
|
17
20
|
let noSandbox = false;
|
|
18
21
|
let sandbox = null;
|
|
19
22
|
let fullAuto = false;
|
|
@@ -27,6 +30,7 @@ let autoDetectSuccessCriteria = null;
|
|
|
27
30
|
let reasoningChoice;
|
|
28
31
|
let showHelp = false;
|
|
29
32
|
const ideaParts = [];
|
|
33
|
+
let idea = "";
|
|
30
34
|
|
|
31
35
|
for (let i = 0; i < argv.length; i += 1) {
|
|
32
36
|
const arg = argv[i];
|
|
@@ -49,6 +53,20 @@ for (let i = 0; i < argv.length; i += 1) {
|
|
|
49
53
|
i += 1;
|
|
50
54
|
continue;
|
|
51
55
|
}
|
|
56
|
+
if (arg === "--idea-file") {
|
|
57
|
+
const value = argv[i + 1];
|
|
58
|
+
if (!value || (value.startsWith("-") && value !== "-")) {
|
|
59
|
+
console.error("Missing --idea-file <path>.");
|
|
60
|
+
process.exit(1);
|
|
61
|
+
}
|
|
62
|
+
ideaFile = value;
|
|
63
|
+
i += 1;
|
|
64
|
+
continue;
|
|
65
|
+
}
|
|
66
|
+
if (arg === "--stdin") {
|
|
67
|
+
readStdin = true;
|
|
68
|
+
continue;
|
|
69
|
+
}
|
|
52
70
|
if (arg === "--no-sandbox") {
|
|
53
71
|
noSandbox = true;
|
|
54
72
|
continue;
|
|
@@ -109,6 +127,8 @@ function printHelp() {
|
|
|
109
127
|
`${colors.yellow("Options:")}\n` +
|
|
110
128
|
` ${colors.green("--output <path>")} Write tasks to a custom file (alias of --tasks)\n` +
|
|
111
129
|
` ${colors.green("--tasks <path>")} Write tasks to a custom file (default: tasks.md)\n` +
|
|
130
|
+
` ${colors.green("--idea-file <path>")} Read idea from a markdown file ('-' for stdin)\n` +
|
|
131
|
+
` ${colors.green("--stdin")} Read idea from stdin (paste then Ctrl-D)\n` +
|
|
112
132
|
` ${colors.green("--max-iterations <n>")} Max planning iterations (default: 1)\n` +
|
|
113
133
|
` ${colors.green("--config <path>")} Path to ralph.config.yml\n` +
|
|
114
134
|
` ${colors.green("--model <name>, -m")} Codex model\n` +
|
|
@@ -129,16 +149,24 @@ if (showHelp) {
|
|
|
129
149
|
process.exit(0);
|
|
130
150
|
}
|
|
131
151
|
|
|
132
|
-
const
|
|
152
|
+
const promptPath = path.join(agentDir, "ralph-plan-prompt.md");
|
|
133
153
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
)
|
|
138
|
-
|
|
154
|
+
function readIdeaFromStdin() {
|
|
155
|
+
try {
|
|
156
|
+
return fs.readFileSync(0, "utf8");
|
|
157
|
+
} catch (_) {
|
|
158
|
+
return "";
|
|
159
|
+
}
|
|
139
160
|
}
|
|
140
161
|
|
|
141
|
-
|
|
162
|
+
function readIdeaFromFile(filePath) {
|
|
163
|
+
const resolved = path.resolve(root, filePath);
|
|
164
|
+
if (!fs.existsSync(resolved)) {
|
|
165
|
+
console.error(`Missing idea file: ${filePath}`);
|
|
166
|
+
process.exit(1);
|
|
167
|
+
}
|
|
168
|
+
return fs.readFileSync(resolved, "utf8");
|
|
169
|
+
}
|
|
142
170
|
function loadConfig(configFilePath) {
|
|
143
171
|
if (!configFilePath) return {};
|
|
144
172
|
if (!fs.existsSync(configFilePath)) return {};
|
|
@@ -175,7 +203,7 @@ function resolveDockerConfig(config) {
|
|
|
175
203
|
? dockerConfig.pip_packages
|
|
176
204
|
: [],
|
|
177
205
|
useForPlan: Boolean(dockerConfig.use_for_plan),
|
|
178
|
-
tty: dockerConfig.tty ??
|
|
206
|
+
tty: dockerConfig.tty ?? false,
|
|
179
207
|
};
|
|
180
208
|
}
|
|
181
209
|
|
|
@@ -862,6 +890,13 @@ async function selectSuccessCriteria(
|
|
|
862
890
|
standardChoices,
|
|
863
891
|
detectedChoices = []
|
|
864
892
|
) {
|
|
893
|
+
if (isTestMode) {
|
|
894
|
+
const safeDefaults =
|
|
895
|
+
defaultCriteria && defaultCriteria.length > 0
|
|
896
|
+
? defaultCriteria
|
|
897
|
+
: standardChoices;
|
|
898
|
+
return { mode: "manual", criteria: safeDefaults || [] };
|
|
899
|
+
}
|
|
865
900
|
const autoChoiceValue = "__auto__";
|
|
866
901
|
const customChoiceValue = "__custom__";
|
|
867
902
|
const defaults =
|
|
@@ -955,6 +990,7 @@ async function selectSuccessCriteria(
|
|
|
955
990
|
}
|
|
956
991
|
|
|
957
992
|
async function confirmPlan(tasksFile) {
|
|
993
|
+
if (isTestMode) return true;
|
|
958
994
|
const content = fs.readFileSync(tasksFile, "utf8");
|
|
959
995
|
process.stdout.write(`\n${colors.cyan("--- Proposed tasks.md ---")}\n\n`);
|
|
960
996
|
process.stdout.write(content);
|
|
@@ -978,6 +1014,7 @@ async function readRevisionFeedback() {
|
|
|
978
1014
|
}
|
|
979
1015
|
|
|
980
1016
|
async function confirmResetState(tasksFilePath, agentPath) {
|
|
1017
|
+
if (isTestMode) return false;
|
|
981
1018
|
const hasTasks = fs.existsSync(tasksFilePath);
|
|
982
1019
|
const hasAgent = fs.existsSync(agentPath);
|
|
983
1020
|
if (!hasTasks && !hasAgent) return false;
|
|
@@ -1000,6 +1037,23 @@ function resetState(tasksFilePath, agentPath) {
|
|
|
1000
1037
|
}
|
|
1001
1038
|
|
|
1002
1039
|
async function main() {
|
|
1040
|
+
const ideaFromArgs = ideaParts.join(" ").trim();
|
|
1041
|
+
if (ideaFile) {
|
|
1042
|
+
idea = ideaFile === "-" ? readIdeaFromStdin() : readIdeaFromFile(ideaFile);
|
|
1043
|
+
} else if (readStdin || (!ideaFromArgs && !process.stdin.isTTY)) {
|
|
1044
|
+
idea = readIdeaFromStdin();
|
|
1045
|
+
} else {
|
|
1046
|
+
idea = ideaFromArgs;
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
idea = String(idea || "").trim();
|
|
1050
|
+
if (!idea) {
|
|
1051
|
+
console.error(
|
|
1052
|
+
'Usage: ralph-codex plan "<idea>" [--idea-file <path>] [--stdin] [--output <path>] [--tasks <path>] [--max-iterations <n>]',
|
|
1053
|
+
);
|
|
1054
|
+
process.exit(1);
|
|
1055
|
+
}
|
|
1056
|
+
|
|
1003
1057
|
const resolvedConfigPath = configPath || path.join(root, "ralph.config.yml");
|
|
1004
1058
|
const config = loadConfig(resolvedConfigPath);
|
|
1005
1059
|
const codexConfig = config?.codex || {};
|
package/src/commands/revise.js
CHANGED
|
@@ -4,6 +4,12 @@ import path from "path";
|
|
|
4
4
|
import enquirer from "enquirer";
|
|
5
5
|
import yaml from "js-yaml";
|
|
6
6
|
import { colors, createLogStyler, createSpinner } from "../ui/terminal.js";
|
|
7
|
+
import {
|
|
8
|
+
diffCriteria,
|
|
9
|
+
diffTasks,
|
|
10
|
+
parseSuccessCriteria,
|
|
11
|
+
parseTasks,
|
|
12
|
+
} from "../lib/tasks.js";
|
|
7
13
|
|
|
8
14
|
const { AutoComplete, Confirm, Editor, Input } = enquirer;
|
|
9
15
|
|
|
@@ -223,91 +229,6 @@ async function readFeedback(promptMessage) {
|
|
|
223
229
|
return input.run();
|
|
224
230
|
}
|
|
225
231
|
|
|
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
232
|
function renderChanges(changes) {
|
|
312
233
|
if (changes.additions.length > 0) {
|
|
313
234
|
process.stdout.write(`${colors.cyan("Proposed new tasks:")}\n`);
|
package/src/commands/run.js
CHANGED
|
@@ -278,7 +278,7 @@ function resolveDockerConfig(config) {
|
|
|
278
278
|
fixAttempts: Number(dockerConfig.fix_attempts || 2),
|
|
279
279
|
fixUseHost: dockerConfig.fix_use_host !== false,
|
|
280
280
|
fixLog: dockerConfig.fix_log || ".ralph/docker-build.log",
|
|
281
|
-
tty: dockerConfig.tty ??
|
|
281
|
+
tty: dockerConfig.tty ?? false,
|
|
282
282
|
cleanup: dockerConfig.cleanup || "none",
|
|
283
283
|
};
|
|
284
284
|
}
|
|
@@ -1144,8 +1144,33 @@ async function main() {
|
|
|
1144
1144
|
}
|
|
1145
1145
|
|
|
1146
1146
|
if (hasCompletion(result.output)) {
|
|
1147
|
-
|
|
1148
|
-
|
|
1147
|
+
const completionProgress = getTaskProgress(tasksFile);
|
|
1148
|
+
if (
|
|
1149
|
+
completionProgress.total > 0 &&
|
|
1150
|
+
completionProgress.completed === completionProgress.total
|
|
1151
|
+
) {
|
|
1152
|
+
completed = true;
|
|
1153
|
+
break;
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
const pendingCount = Math.max(
|
|
1157
|
+
0,
|
|
1158
|
+
completionProgress.total -
|
|
1159
|
+
completionProgress.completed -
|
|
1160
|
+
completionProgress.blocked,
|
|
1161
|
+
);
|
|
1162
|
+
const reason =
|
|
1163
|
+
completionProgress.total === 0
|
|
1164
|
+
? "no tasks detected"
|
|
1165
|
+
: `${pendingCount} pending, ${completionProgress.blocked} blocked`;
|
|
1166
|
+
notes.push(
|
|
1167
|
+
`Completion token received but tasks remain incomplete (${reason}).`,
|
|
1168
|
+
);
|
|
1169
|
+
process.stdout.write(
|
|
1170
|
+
`${colors.yellow(
|
|
1171
|
+
"Completion token received but tasks remain incomplete; continuing.",
|
|
1172
|
+
)}\n`,
|
|
1173
|
+
);
|
|
1149
1174
|
}
|
|
1150
1175
|
|
|
1151
1176
|
if (iterationTimedOut) {
|
package/src/commands/view.js
CHANGED
|
@@ -2,6 +2,7 @@ import fs from "fs";
|
|
|
2
2
|
import path from "path";
|
|
3
3
|
import yaml from "js-yaml";
|
|
4
4
|
import { colors } from "../ui/terminal.js";
|
|
5
|
+
import { parseSuccessCriteria, parseTasks } from "../lib/tasks.js";
|
|
5
6
|
|
|
6
7
|
const root = process.cwd();
|
|
7
8
|
const argv = process.argv.slice(2);
|
|
@@ -156,21 +157,6 @@ function formatStatus(status) {
|
|
|
156
157
|
return { raw, display: colors.gray(raw) };
|
|
157
158
|
}
|
|
158
159
|
|
|
159
|
-
function parseTasks(content) {
|
|
160
|
-
const tasks = [];
|
|
161
|
-
const lines = content.split(/\r?\n/);
|
|
162
|
-
let index = 0;
|
|
163
|
-
for (const line of lines) {
|
|
164
|
-
const match = line.match(/^\s*[-*]\s+\[([ x~])\]\s+(.*)$/);
|
|
165
|
-
if (!match) continue;
|
|
166
|
-
index += 1;
|
|
167
|
-
const statusToken = match[1].toLowerCase();
|
|
168
|
-
const status =
|
|
169
|
-
statusToken === "x" ? "done" : statusToken === "~" ? "blocked" : "pending";
|
|
170
|
-
tasks.push({ index, status, text: match[2].trim() });
|
|
171
|
-
}
|
|
172
|
-
return tasks;
|
|
173
|
-
}
|
|
174
160
|
|
|
175
161
|
function summarizeTasks(tasks) {
|
|
176
162
|
const total = tasks.length;
|
|
@@ -186,26 +172,6 @@ function filterTasks(tasks, filter) {
|
|
|
186
172
|
return tasks.filter((task) => task.status === filter);
|
|
187
173
|
}
|
|
188
174
|
|
|
189
|
-
function extractSuccessCriteria(content) {
|
|
190
|
-
const lines = content.split(/\r?\n/);
|
|
191
|
-
let start = -1;
|
|
192
|
-
for (let i = 0; i < lines.length; i += 1) {
|
|
193
|
-
if (/^(#+\s*)?success criteria\b/i.test(lines[i].trim())) {
|
|
194
|
-
start = i;
|
|
195
|
-
break;
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
|
-
if (start === -1) return [];
|
|
199
|
-
const items = [];
|
|
200
|
-
for (let i = start + 1; i < lines.length; i += 1) {
|
|
201
|
-
const line = lines[i].trim();
|
|
202
|
-
if (!line) continue;
|
|
203
|
-
if (/^#+\s+/.test(line)) break;
|
|
204
|
-
if (line.startsWith("- ")) items.push(line.slice(2).trim());
|
|
205
|
-
if (line.startsWith("* ")) items.push(line.slice(2).trim());
|
|
206
|
-
}
|
|
207
|
-
return items;
|
|
208
|
-
}
|
|
209
175
|
|
|
210
176
|
function getLastBlocker(logPath) {
|
|
211
177
|
if (!fs.existsSync(logPath)) return "";
|
|
@@ -339,6 +305,7 @@ function getConfigRows(config) {
|
|
|
339
305
|
use_for_plan: false,
|
|
340
306
|
base_image: "node:20-bullseye",
|
|
341
307
|
codex_install: "",
|
|
308
|
+
tty: false,
|
|
342
309
|
},
|
|
343
310
|
plan: {
|
|
344
311
|
tasks_path: "tasks.md",
|
|
@@ -468,7 +435,7 @@ function renderOnce({ allowMissingTasks }) {
|
|
|
468
435
|
const summary = summarizeTasks(allTasks);
|
|
469
436
|
const filtered = filterTasks(allTasks, only);
|
|
470
437
|
const sliced = limit > 0 ? filtered.slice(0, limit) : filtered;
|
|
471
|
-
const criteria =
|
|
438
|
+
const criteria = parseSuccessCriteria(tasksContent);
|
|
472
439
|
|
|
473
440
|
tasksData = {
|
|
474
441
|
path: resolvedTasksPath,
|
package/src/lib/tasks.js
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
function parseTasks(content) {
|
|
2
|
+
const tasks = [];
|
|
3
|
+
const lines = String(content || "").split(/\r?\n/);
|
|
4
|
+
let index = 0;
|
|
5
|
+
|
|
6
|
+
for (const line of lines) {
|
|
7
|
+
const match = line.match(/^\s*[-*]\s+\[([ x~])\]\s+(.*)$/);
|
|
8
|
+
if (!match) continue;
|
|
9
|
+
index += 1;
|
|
10
|
+
const statusToken = match[1].toLowerCase();
|
|
11
|
+
const status =
|
|
12
|
+
statusToken === "x" ? "done" : statusToken === "~" ? "blocked" : "pending";
|
|
13
|
+
tasks.push({
|
|
14
|
+
index,
|
|
15
|
+
status,
|
|
16
|
+
text: match[2].trim(),
|
|
17
|
+
raw: line.trim(),
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return tasks;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function parseSuccessCriteria(content) {
|
|
25
|
+
const lines = String(content || "").split(/\r?\n/);
|
|
26
|
+
let start = -1;
|
|
27
|
+
for (let i = 0; i < lines.length; i += 1) {
|
|
28
|
+
if (/^(#+\s*)?success criteria\b/i.test(lines[i].trim())) {
|
|
29
|
+
start = i;
|
|
30
|
+
break;
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
if (start === -1) return [];
|
|
34
|
+
const items = [];
|
|
35
|
+
for (let i = start + 1; i < lines.length; i += 1) {
|
|
36
|
+
const line = lines[i].trim();
|
|
37
|
+
if (!line) continue;
|
|
38
|
+
if (/^#+\s+/.test(line)) break;
|
|
39
|
+
if (line.startsWith("- ")) items.push(line.slice(2).trim());
|
|
40
|
+
if (line.startsWith("* ")) items.push(line.slice(2).trim());
|
|
41
|
+
}
|
|
42
|
+
return items;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function normalizeTaskText(text) {
|
|
46
|
+
return String(text || "")
|
|
47
|
+
.toLowerCase()
|
|
48
|
+
.replace(/\s+/g, " ")
|
|
49
|
+
.trim();
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function diffTasks(oldTasks, newTasks) {
|
|
53
|
+
const oldSet = new Set(oldTasks.map((task) => normalizeTaskText(task.text)));
|
|
54
|
+
const newSet = new Set(newTasks.map((task) => normalizeTaskText(task.text)));
|
|
55
|
+
|
|
56
|
+
const additions = newTasks.filter(
|
|
57
|
+
(task) => !oldSet.has(normalizeTaskText(task.text))
|
|
58
|
+
);
|
|
59
|
+
const removals = oldTasks.filter(
|
|
60
|
+
(task) => !newSet.has(normalizeTaskText(task.text))
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
const modified = [];
|
|
64
|
+
const compareCount = Math.min(oldTasks.length, newTasks.length);
|
|
65
|
+
for (let i = 0; i < compareCount; i += 1) {
|
|
66
|
+
const before = oldTasks[i];
|
|
67
|
+
const after = newTasks[i];
|
|
68
|
+
if (
|
|
69
|
+
before.status !== after.status ||
|
|
70
|
+
normalizeTaskText(before.text) !== normalizeTaskText(after.text)
|
|
71
|
+
) {
|
|
72
|
+
modified.push({
|
|
73
|
+
index: i + 1,
|
|
74
|
+
before,
|
|
75
|
+
after,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
return { additions, removals, modified };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function diffCriteria(oldCriteria, newCriteria) {
|
|
84
|
+
const oldSet = new Set(oldCriteria);
|
|
85
|
+
const newSet = new Set(newCriteria);
|
|
86
|
+
const added = newCriteria.filter((item) => !oldSet.has(item));
|
|
87
|
+
const removed = oldCriteria.filter((item) => !newSet.has(item));
|
|
88
|
+
return { added, removed };
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export {
|
|
92
|
+
diffCriteria,
|
|
93
|
+
diffTasks,
|
|
94
|
+
normalizeTaskText,
|
|
95
|
+
parseSuccessCriteria,
|
|
96
|
+
parseTasks,
|
|
97
|
+
};
|
|
@@ -18,7 +18,7 @@ docker:
|
|
|
18
18
|
codex_install: npm install -g @openai/codex
|
|
19
19
|
codex_home: .ralph/codex # Writable codex home inside the repo
|
|
20
20
|
mount_codex_config: true # Seed codex_home from host ~/.codex if empty
|
|
21
|
-
tty:
|
|
21
|
+
tty: false # auto | true | false
|
|
22
22
|
pass_env:
|
|
23
23
|
- OPENAI_API_KEY
|
|
24
24
|
apt_packages:
|