codex-ralph 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/README.md +25 -0
- package/agent/prompt.md +18 -0
- package/agent/ralph-loop.sh +165 -0
- package/package.json +13 -0
package/README.md
ADDED
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Codex Ralph Loop
|
|
2
|
+
|
|
3
|
+
Minimal Ralph Loop runner that feeds a sprint requirement into Codex, one item per iteration.
|
|
4
|
+
|
|
5
|
+
## Quick start
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npx ralph-loop --sprint=path/to/Sprint_0001.md --max-iterations=1
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Sprint format (Markdown)
|
|
12
|
+
|
|
13
|
+
Each requirement is a level-2 heading with a checkbox. The loop picks the first unchecked item.
|
|
14
|
+
|
|
15
|
+
```markdown
|
|
16
|
+
## [ ] Requirement description
|
|
17
|
+
Description: Free-form task details.
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
## Behavior
|
|
21
|
+
|
|
22
|
+
- Reads the sprint file, finds the first unchecked item, and passes it to Codex.
|
|
23
|
+
- Derives a parallel notes file alongside the sprint (for example `Sprint_0001.md` -> `SprintNotes_0001.md`) and includes its path in the prompt.
|
|
24
|
+
- Marks items complete only when all steps are satisfied.
|
|
25
|
+
- Uses conventional commits for each completed requirement.
|
package/agent/prompt.md
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# Ralph Loop (Codex) Sprint Executor
|
|
2
|
+
|
|
3
|
+
You are running inside a loop. Each iteration must do **exactly one** sprint requirement from the sprint file provided below, in order.
|
|
4
|
+
|
|
5
|
+
Process:
|
|
6
|
+
1. Open the sprint file path provided below.
|
|
7
|
+
2. Find the first item that is unchecked.
|
|
8
|
+
3. Implement that requirement fully.
|
|
9
|
+
4. Update the sprint item to checked only when all its steps are satisfied.
|
|
10
|
+
5. Commit your changes with a **conventional commit** message.
|
|
11
|
+
|
|
12
|
+
If you cannot complete the requirement, leave it unchecked and record the blocker in the Sprint notes file.
|
|
13
|
+
|
|
14
|
+
After each iteration, update the Sprint notes file with what you did, including details or nuances that will help the next iteration.
|
|
15
|
+
|
|
16
|
+
When **all** items are checked, output:
|
|
17
|
+
|
|
18
|
+
<promise>DONE</promise>
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
|
|
3
|
+
set -euo pipefail
|
|
4
|
+
|
|
5
|
+
SCRIPT_PATH="$(python3 - <<PY
|
|
6
|
+
import os
|
|
7
|
+
print(os.path.realpath("${BASH_SOURCE[0]}"))
|
|
8
|
+
PY
|
|
9
|
+
)"
|
|
10
|
+
ROOT_DIR="$(cd "$(dirname "$SCRIPT_PATH")/.." && pwd)"
|
|
11
|
+
SPRINT_FILE=""
|
|
12
|
+
MAX_ITERATIONS=0
|
|
13
|
+
NOTES_FILE=""
|
|
14
|
+
|
|
15
|
+
usage() {
|
|
16
|
+
cat <<USAGE
|
|
17
|
+
Usage: ralph-loop --sprint=PATH [--max-iterations=N]
|
|
18
|
+
|
|
19
|
+
Options:
|
|
20
|
+
--sprint=PATH Path to the sprint markdown file (required).
|
|
21
|
+
--max-iterations=N Stop after N iterations (0 = no limit).
|
|
22
|
+
-h, --help Show this help.
|
|
23
|
+
USAGE
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
for arg in "$@"; do
|
|
27
|
+
case "$arg" in
|
|
28
|
+
--sprint=*)
|
|
29
|
+
SPRINT_FILE="${arg#*=}"
|
|
30
|
+
;;
|
|
31
|
+
--max-iterations=*)
|
|
32
|
+
MAX_ITERATIONS="${arg#*=}"
|
|
33
|
+
;;
|
|
34
|
+
-h|--help)
|
|
35
|
+
usage
|
|
36
|
+
exit 0
|
|
37
|
+
;;
|
|
38
|
+
*)
|
|
39
|
+
echo "Unknown argument: $arg" >&2
|
|
40
|
+
usage >&2
|
|
41
|
+
exit 1
|
|
42
|
+
;;
|
|
43
|
+
esac
|
|
44
|
+
done
|
|
45
|
+
|
|
46
|
+
if [[ -z "$SPRINT_FILE" ]]; then
|
|
47
|
+
echo "Sprint file path is required." >&2
|
|
48
|
+
usage >&2
|
|
49
|
+
exit 1
|
|
50
|
+
fi
|
|
51
|
+
|
|
52
|
+
if [[ ! -f "$SPRINT_FILE" ]]; then
|
|
53
|
+
echo "Sprint file not found: $SPRINT_FILE" >&2
|
|
54
|
+
exit 1
|
|
55
|
+
fi
|
|
56
|
+
|
|
57
|
+
if [[ -z "$NOTES_FILE" ]]; then
|
|
58
|
+
sprint_dir="$(cd "$(dirname "$SPRINT_FILE")" && pwd)"
|
|
59
|
+
sprint_base="$(basename "$SPRINT_FILE")"
|
|
60
|
+
notes_base="${sprint_base/Sprint_/SprintNotes_}"
|
|
61
|
+
if [[ "$notes_base" == "$sprint_base" ]]; then
|
|
62
|
+
notes_base="${sprint_base%.*}Notes.${sprint_base##*.}"
|
|
63
|
+
fi
|
|
64
|
+
NOTES_FILE="$sprint_dir/$notes_base"
|
|
65
|
+
fi
|
|
66
|
+
|
|
67
|
+
if [[ ! -f "$NOTES_FILE" ]]; then
|
|
68
|
+
mkdir -p "$(dirname "$NOTES_FILE")"
|
|
69
|
+
: > "$NOTES_FILE"
|
|
70
|
+
fi
|
|
71
|
+
|
|
72
|
+
PROMPT_FILE="$ROOT_DIR/agent/prompt.md"
|
|
73
|
+
if [[ ! -f "$PROMPT_FILE" ]]; then
|
|
74
|
+
echo "Prompt file not found: $PROMPT_FILE" >&2
|
|
75
|
+
exit 1
|
|
76
|
+
fi
|
|
77
|
+
|
|
78
|
+
remaining_count() {
|
|
79
|
+
python3 - <<PY
|
|
80
|
+
from pathlib import Path
|
|
81
|
+
import re
|
|
82
|
+
|
|
83
|
+
path = Path("$SPRINT_FILE")
|
|
84
|
+
text = path.read_text(encoding="utf-8")
|
|
85
|
+
items = re.findall(r"^## \[( |x|X)\] ", text, flags=re.M)
|
|
86
|
+
print(sum(1 for m in items if m.strip() != "x"))
|
|
87
|
+
PY
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
next_description() {
|
|
91
|
+
python3 - <<PY
|
|
92
|
+
from pathlib import Path
|
|
93
|
+
import re
|
|
94
|
+
|
|
95
|
+
path = Path("$SPRINT_FILE")
|
|
96
|
+
text = path.read_text(encoding="utf-8")
|
|
97
|
+
for match in re.finditer(r"^## \[( |x|X)\] (.+)$", text, flags=re.M):
|
|
98
|
+
checked = match.group(1)
|
|
99
|
+
desc = match.group(2).strip()
|
|
100
|
+
if checked != "x" and checked != "X":
|
|
101
|
+
print(desc)
|
|
102
|
+
break
|
|
103
|
+
PY
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
current_item_block() {
|
|
107
|
+
python3 - <<PY
|
|
108
|
+
from pathlib import Path
|
|
109
|
+
import re
|
|
110
|
+
|
|
111
|
+
path = Path("$SPRINT_FILE")
|
|
112
|
+
lines = path.read_text(encoding="utf-8").splitlines()
|
|
113
|
+
start = None
|
|
114
|
+
end = None
|
|
115
|
+
for i, line in enumerate(lines):
|
|
116
|
+
if re.match(r"^## \[( |x|X)\] ", line):
|
|
117
|
+
if start is None:
|
|
118
|
+
checked = line.split("[", 1)[1].split("]", 1)[0].strip()
|
|
119
|
+
if checked.lower() != "x":
|
|
120
|
+
start = i
|
|
121
|
+
elif start is not None:
|
|
122
|
+
end = i
|
|
123
|
+
break
|
|
124
|
+
if start is None:
|
|
125
|
+
print("<no remaining items>")
|
|
126
|
+
else:
|
|
127
|
+
block = lines[start:end] if end is not None else lines[start:]
|
|
128
|
+
print("\n".join(block))
|
|
129
|
+
PY
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
iteration=1
|
|
133
|
+
while true; do
|
|
134
|
+
remaining="$(remaining_count)"
|
|
135
|
+
if [[ "$remaining" == "0" ]]; then
|
|
136
|
+
echo "All sprint requirements complete."
|
|
137
|
+
echo "<promise>DONE</promise>"
|
|
138
|
+
exit 0
|
|
139
|
+
fi
|
|
140
|
+
|
|
141
|
+
if [[ "$MAX_ITERATIONS" -gt 0 && "$iteration" -gt "$MAX_ITERATIONS" ]]; then
|
|
142
|
+
echo "Reached max iterations ($MAX_ITERATIONS) with $remaining remaining." >&2
|
|
143
|
+
exit 2
|
|
144
|
+
fi
|
|
145
|
+
|
|
146
|
+
desc="$(next_description)"
|
|
147
|
+
echo "Iteration $iteration - next requirement: $desc"
|
|
148
|
+
|
|
149
|
+
prompt_tmp="$(mktemp)"
|
|
150
|
+
{
|
|
151
|
+
cat "$PROMPT_FILE"
|
|
152
|
+
echo ""
|
|
153
|
+
echo "Sprint file: $SPRINT_FILE"
|
|
154
|
+
echo "Sprint notes file: $NOTES_FILE"
|
|
155
|
+
echo ""
|
|
156
|
+
echo "Current requirement (Markdown):"
|
|
157
|
+
current_item_block
|
|
158
|
+
} > "$prompt_tmp"
|
|
159
|
+
|
|
160
|
+
codex exec - < "$prompt_tmp"
|
|
161
|
+
|
|
162
|
+
rm -f "$prompt_tmp"
|
|
163
|
+
|
|
164
|
+
iteration=$((iteration + 1))
|
|
165
|
+
done
|
package/package.json
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "codex-ralph",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Ralph Loop sprint runner for Codex.",
|
|
5
|
+
"bin": {
|
|
6
|
+
"ralph-loop": "agent/ralph-loop.sh"
|
|
7
|
+
},
|
|
8
|
+
"scripts": {
|
|
9
|
+
"ralph-loop": "agent/ralph-loop.sh"
|
|
10
|
+
},
|
|
11
|
+
"author": "Igor Sachivka <juicy.igor@gmail.com>",
|
|
12
|
+
"license": "MIT"
|
|
13
|
+
}
|