@canivel/ralph 0.2.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/.agents/ralph/PROMPT_build.md +126 -0
- package/.agents/ralph/agents.sh +15 -0
- package/.agents/ralph/config.sh +25 -0
- package/.agents/ralph/log-activity.sh +15 -0
- package/.agents/ralph/loop.sh +1001 -0
- package/.agents/ralph/references/CONTEXT_ENGINEERING.md +126 -0
- package/.agents/ralph/references/GUARDRAILS.md +174 -0
- package/AGENTS.md +20 -0
- package/README.md +266 -0
- package/bin/ralph +766 -0
- package/diagram.svg +55 -0
- package/examples/commands.md +46 -0
- package/package.json +39 -0
- package/ralph.webp +0 -0
- package/skills/commit/SKILL.md +219 -0
- package/skills/commit/references/commit_examples.md +292 -0
- package/skills/dev-browser/SKILL.md +211 -0
- package/skills/dev-browser/bun.lock +443 -0
- package/skills/dev-browser/package-lock.json +2988 -0
- package/skills/dev-browser/package.json +31 -0
- package/skills/dev-browser/references/scraping.md +155 -0
- package/skills/dev-browser/scripts/start-relay.ts +32 -0
- package/skills/dev-browser/scripts/start-server.ts +117 -0
- package/skills/dev-browser/server.sh +24 -0
- package/skills/dev-browser/src/client.ts +474 -0
- package/skills/dev-browser/src/index.ts +287 -0
- package/skills/dev-browser/src/relay.ts +731 -0
- package/skills/dev-browser/src/snapshot/__tests__/snapshot.test.ts +223 -0
- package/skills/dev-browser/src/snapshot/browser-script.ts +877 -0
- package/skills/dev-browser/src/snapshot/index.ts +14 -0
- package/skills/dev-browser/src/snapshot/inject.ts +13 -0
- package/skills/dev-browser/src/types.ts +34 -0
- package/skills/dev-browser/tsconfig.json +36 -0
- package/skills/dev-browser/vitest.config.ts +12 -0
- package/skills/prd/SKILL.md +235 -0
- package/tests/agent-loops.mjs +79 -0
- package/tests/agent-ping.mjs +39 -0
- package/tests/audit.md +56 -0
- package/tests/cli-smoke.mjs +47 -0
- package/tests/real-agents.mjs +127 -0
|
@@ -0,0 +1,1001 @@
|
|
|
1
|
+
#!/bin/bash
|
|
2
|
+
# Ralph loop — simple, portable, single-agent
|
|
3
|
+
# Usage:
|
|
4
|
+
# ./.agents/ralph/loop.sh # build mode, default iterations
|
|
5
|
+
# ./.agents/ralph/loop.sh build # build mode
|
|
6
|
+
# ./.agents/ralph/loop.sh prd "request" # generate PRD via agent
|
|
7
|
+
# ./.agents/ralph/loop.sh 10 # build mode, 10 iterations
|
|
8
|
+
# ./.agents/ralph/loop.sh build 1 --no-commit
|
|
9
|
+
|
|
10
|
+
set -euo pipefail
|
|
11
|
+
|
|
12
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
13
|
+
ROOT_DIR="$(cd "${RALPH_ROOT:-${SCRIPT_DIR}/../..}" && pwd)"
|
|
14
|
+
CONFIG_FILE="${SCRIPT_DIR}/config.sh"
|
|
15
|
+
|
|
16
|
+
DEFAULT_PRD_PATH=".agents/tasks/prd.json"
|
|
17
|
+
DEFAULT_PROGRESS_PATH=".ralph/progress.md"
|
|
18
|
+
DEFAULT_AGENTS_PATH="AGENTS.md"
|
|
19
|
+
DEFAULT_PROMPT_BUILD=".agents/ralph/PROMPT_build.md"
|
|
20
|
+
DEFAULT_GUARDRAILS_PATH=".ralph/guardrails.md"
|
|
21
|
+
DEFAULT_ERRORS_LOG_PATH=".ralph/errors.log"
|
|
22
|
+
DEFAULT_ACTIVITY_LOG_PATH=".ralph/activity.log"
|
|
23
|
+
DEFAULT_TMP_DIR=".ralph/.tmp"
|
|
24
|
+
DEFAULT_RUNS_DIR=".ralph/runs"
|
|
25
|
+
DEFAULT_GUARDRAILS_REF=".agents/ralph/references/GUARDRAILS.md"
|
|
26
|
+
DEFAULT_CONTEXT_REF=".agents/ralph/references/CONTEXT_ENGINEERING.md"
|
|
27
|
+
DEFAULT_ACTIVITY_CMD=".agents/ralph/log-activity.sh"
|
|
28
|
+
if [[ -n "${RALPH_ROOT:-}" ]]; then
|
|
29
|
+
agents_path="$RALPH_ROOT/.agents/ralph/agents.sh"
|
|
30
|
+
else
|
|
31
|
+
agents_path="$SCRIPT_DIR/agents.sh"
|
|
32
|
+
fi
|
|
33
|
+
if [[ -f "$agents_path" ]]; then
|
|
34
|
+
# shellcheck source=/dev/null
|
|
35
|
+
source "$agents_path"
|
|
36
|
+
fi
|
|
37
|
+
|
|
38
|
+
DEFAULT_MAX_ITERATIONS=25
|
|
39
|
+
DEFAULT_NO_COMMIT=false
|
|
40
|
+
DEFAULT_STALE_SECONDS=0
|
|
41
|
+
PRD_REQUEST_PATH=""
|
|
42
|
+
PRD_INLINE=""
|
|
43
|
+
|
|
44
|
+
# Optional config overrides (simple shell vars)
|
|
45
|
+
if [ -f "$CONFIG_FILE" ]; then
|
|
46
|
+
# shellcheck source=/dev/null
|
|
47
|
+
. "$CONFIG_FILE"
|
|
48
|
+
fi
|
|
49
|
+
|
|
50
|
+
DEFAULT_AGENT_NAME="${DEFAULT_AGENT:-codex}"
|
|
51
|
+
resolve_agent_cmd() {
|
|
52
|
+
local name="$1"
|
|
53
|
+
local interactive="${2:-0}"
|
|
54
|
+
case "$name" in
|
|
55
|
+
claude)
|
|
56
|
+
if [ "$interactive" = "1" ]; then
|
|
57
|
+
echo "${AGENT_CLAUDE_INTERACTIVE_CMD:-claude --dangerously-skip-permissions {prompt}}"
|
|
58
|
+
else
|
|
59
|
+
echo "${AGENT_CLAUDE_CMD:-claude -p --dangerously-skip-permissions {prompt}}"
|
|
60
|
+
fi
|
|
61
|
+
;;
|
|
62
|
+
droid)
|
|
63
|
+
if [ "$interactive" = "1" ]; then
|
|
64
|
+
echo "${AGENT_DROID_INTERACTIVE_CMD:-droid --skip-permissions-unsafe {prompt}}"
|
|
65
|
+
else
|
|
66
|
+
echo "${AGENT_DROID_CMD:-droid exec --skip-permissions-unsafe -f {prompt}}"
|
|
67
|
+
fi
|
|
68
|
+
;;
|
|
69
|
+
codex|"")
|
|
70
|
+
if [ "$interactive" = "1" ]; then
|
|
71
|
+
echo "${AGENT_CODEX_INTERACTIVE_CMD:-codex --yolo {prompt}}"
|
|
72
|
+
else
|
|
73
|
+
echo "${AGENT_CODEX_CMD:-codex exec --yolo --skip-git-repo-check -}"
|
|
74
|
+
fi
|
|
75
|
+
;;
|
|
76
|
+
opencode)
|
|
77
|
+
if [ "$interactive" = "1" ]; then
|
|
78
|
+
echo "${AGENT_OPENCODE_INTERACTIVE_CMD:-opencode --prompt {prompt}}"
|
|
79
|
+
else
|
|
80
|
+
echo "${AGENT_OPENCODE_CMD:-opencode run {prompt}}"
|
|
81
|
+
fi
|
|
82
|
+
;;
|
|
83
|
+
*)
|
|
84
|
+
echo "${AGENT_CODEX_CMD:-codex exec --yolo --skip-git-repo-check -}"
|
|
85
|
+
;;
|
|
86
|
+
esac
|
|
87
|
+
}
|
|
88
|
+
DEFAULT_AGENT_CMD="$(resolve_agent_cmd "$DEFAULT_AGENT_NAME" 0)"
|
|
89
|
+
|
|
90
|
+
PRD_PATH="${PRD_PATH:-$DEFAULT_PRD_PATH}"
|
|
91
|
+
PROGRESS_PATH="${PROGRESS_PATH:-$DEFAULT_PROGRESS_PATH}"
|
|
92
|
+
AGENTS_PATH="${AGENTS_PATH:-$DEFAULT_AGENTS_PATH}"
|
|
93
|
+
PROMPT_BUILD="${PROMPT_BUILD:-$DEFAULT_PROMPT_BUILD}"
|
|
94
|
+
GUARDRAILS_PATH="${GUARDRAILS_PATH:-$DEFAULT_GUARDRAILS_PATH}"
|
|
95
|
+
ERRORS_LOG_PATH="${ERRORS_LOG_PATH:-$DEFAULT_ERRORS_LOG_PATH}"
|
|
96
|
+
ACTIVITY_LOG_PATH="${ACTIVITY_LOG_PATH:-$DEFAULT_ACTIVITY_LOG_PATH}"
|
|
97
|
+
TMP_DIR="${TMP_DIR:-$DEFAULT_TMP_DIR}"
|
|
98
|
+
RUNS_DIR="${RUNS_DIR:-$DEFAULT_RUNS_DIR}"
|
|
99
|
+
GUARDRAILS_REF="${GUARDRAILS_REF:-$DEFAULT_GUARDRAILS_REF}"
|
|
100
|
+
CONTEXT_REF="${CONTEXT_REF:-$DEFAULT_CONTEXT_REF}"
|
|
101
|
+
ACTIVITY_CMD="${ACTIVITY_CMD:-$DEFAULT_ACTIVITY_CMD}"
|
|
102
|
+
AGENT_CMD="${AGENT_CMD:-$DEFAULT_AGENT_CMD}"
|
|
103
|
+
MAX_ITERATIONS="${MAX_ITERATIONS:-$DEFAULT_MAX_ITERATIONS}"
|
|
104
|
+
NO_COMMIT="${NO_COMMIT:-$DEFAULT_NO_COMMIT}"
|
|
105
|
+
STALE_SECONDS="${STALE_SECONDS:-$DEFAULT_STALE_SECONDS}"
|
|
106
|
+
|
|
107
|
+
abs_path() {
|
|
108
|
+
local p="$1"
|
|
109
|
+
if [[ "$p" = /* ]]; then
|
|
110
|
+
echo "$p"
|
|
111
|
+
else
|
|
112
|
+
echo "$ROOT_DIR/$p"
|
|
113
|
+
fi
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
PRD_PATH="$(abs_path "$PRD_PATH")"
|
|
117
|
+
PROGRESS_PATH="$(abs_path "$PROGRESS_PATH")"
|
|
118
|
+
AGENTS_PATH="$(abs_path "$AGENTS_PATH")"
|
|
119
|
+
PROMPT_BUILD="$(abs_path "$PROMPT_BUILD")"
|
|
120
|
+
GUARDRAILS_PATH="$(abs_path "$GUARDRAILS_PATH")"
|
|
121
|
+
ERRORS_LOG_PATH="$(abs_path "$ERRORS_LOG_PATH")"
|
|
122
|
+
ACTIVITY_LOG_PATH="$(abs_path "$ACTIVITY_LOG_PATH")"
|
|
123
|
+
TMP_DIR="$(abs_path "$TMP_DIR")"
|
|
124
|
+
RUNS_DIR="$(abs_path "$RUNS_DIR")"
|
|
125
|
+
GUARDRAILS_REF="$(abs_path "$GUARDRAILS_REF")"
|
|
126
|
+
CONTEXT_REF="$(abs_path "$CONTEXT_REF")"
|
|
127
|
+
ACTIVITY_CMD="$(abs_path "$ACTIVITY_CMD")"
|
|
128
|
+
|
|
129
|
+
require_agent() {
|
|
130
|
+
local agent_cmd="${1:-$AGENT_CMD}"
|
|
131
|
+
local agent_bin
|
|
132
|
+
agent_bin="${agent_cmd%% *}"
|
|
133
|
+
if [ -z "$agent_bin" ]; then
|
|
134
|
+
echo "AGENT_CMD is empty. Set it in config.sh."
|
|
135
|
+
exit 1
|
|
136
|
+
fi
|
|
137
|
+
if ! command -v "$agent_bin" >/dev/null 2>&1; then
|
|
138
|
+
echo "Agent command not found: $agent_bin"
|
|
139
|
+
case "$agent_bin" in
|
|
140
|
+
codex)
|
|
141
|
+
echo "Install: npm i -g @openai/codex"
|
|
142
|
+
;;
|
|
143
|
+
claude)
|
|
144
|
+
echo "Install: curl -fsSL https://claude.ai/install.sh | bash"
|
|
145
|
+
;;
|
|
146
|
+
droid)
|
|
147
|
+
echo "Install: curl -fsSL https://app.factory.ai/cli | sh"
|
|
148
|
+
;;
|
|
149
|
+
opencode)
|
|
150
|
+
echo "Install: curl -fsSL https://opencode.ai/install.sh | bash"
|
|
151
|
+
;;
|
|
152
|
+
esac
|
|
153
|
+
echo "Then authenticate per the CLI's instructions."
|
|
154
|
+
exit 1
|
|
155
|
+
fi
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
run_agent() {
|
|
159
|
+
local prompt_file="$1"
|
|
160
|
+
if [[ "$AGENT_CMD" == *"{prompt}"* ]]; then
|
|
161
|
+
local escaped
|
|
162
|
+
escaped=$(printf '%q' "$prompt_file")
|
|
163
|
+
local cmd="${AGENT_CMD//\{prompt\}/$escaped}"
|
|
164
|
+
eval "$cmd"
|
|
165
|
+
else
|
|
166
|
+
cat "$prompt_file" | eval "$AGENT_CMD"
|
|
167
|
+
fi
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
run_agent_inline() {
|
|
171
|
+
local prompt_file="$1"
|
|
172
|
+
local prompt_content
|
|
173
|
+
prompt_content="$(cat "$prompt_file")"
|
|
174
|
+
local escaped
|
|
175
|
+
escaped=$(printf "%s" "$prompt_content" | sed "s/'/'\\\\''/g")
|
|
176
|
+
local cmd="${PRD_AGENT_CMD:-$AGENT_CMD}"
|
|
177
|
+
if [[ "$cmd" == *"{prompt}"* ]]; then
|
|
178
|
+
cmd="${cmd//\{prompt\}/'$escaped'}"
|
|
179
|
+
else
|
|
180
|
+
cmd="$cmd '$escaped'"
|
|
181
|
+
fi
|
|
182
|
+
eval "$cmd"
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
MODE="build"
|
|
186
|
+
while [ $# -gt 0 ]; do
|
|
187
|
+
case "$1" in
|
|
188
|
+
build|prd)
|
|
189
|
+
MODE="$1"
|
|
190
|
+
shift
|
|
191
|
+
;;
|
|
192
|
+
--prompt)
|
|
193
|
+
PRD_REQUEST_PATH="$2"
|
|
194
|
+
shift 2
|
|
195
|
+
;;
|
|
196
|
+
--no-commit)
|
|
197
|
+
NO_COMMIT=true
|
|
198
|
+
shift
|
|
199
|
+
;;
|
|
200
|
+
*)
|
|
201
|
+
if [ "$MODE" = "prd" ]; then
|
|
202
|
+
PRD_INLINE="${PRD_INLINE:+$PRD_INLINE }$1"
|
|
203
|
+
shift
|
|
204
|
+
elif [[ "$1" =~ ^[0-9]+$ ]]; then
|
|
205
|
+
MAX_ITERATIONS="$1"
|
|
206
|
+
shift
|
|
207
|
+
else
|
|
208
|
+
echo "Unknown arg: $1"
|
|
209
|
+
exit 1
|
|
210
|
+
fi
|
|
211
|
+
;;
|
|
212
|
+
esac
|
|
213
|
+
done
|
|
214
|
+
|
|
215
|
+
PROMPT_FILE="$PROMPT_BUILD"
|
|
216
|
+
|
|
217
|
+
if [ "$MODE" = "prd" ]; then
|
|
218
|
+
# For PRD mode, use interactive agent command (file-based with {prompt} placeholder)
|
|
219
|
+
if [ -z "${PRD_AGENT_CMD:-}" ]; then
|
|
220
|
+
PRD_AGENT_CMD="$(resolve_agent_cmd "$DEFAULT_AGENT_NAME" 1)"
|
|
221
|
+
fi
|
|
222
|
+
if [ "${RALPH_DRY_RUN:-}" != "1" ]; then
|
|
223
|
+
require_agent "${PRD_AGENT_CMD}"
|
|
224
|
+
fi
|
|
225
|
+
|
|
226
|
+
if [[ "$PRD_PATH" == *.json ]]; then
|
|
227
|
+
mkdir -p "$(dirname "$PRD_PATH")" "$TMP_DIR"
|
|
228
|
+
else
|
|
229
|
+
mkdir -p "$PRD_PATH" "$TMP_DIR"
|
|
230
|
+
fi
|
|
231
|
+
|
|
232
|
+
if [ -z "$PRD_REQUEST_PATH" ] && [ -n "$PRD_INLINE" ]; then
|
|
233
|
+
PRD_REQUEST_PATH="$TMP_DIR/prd-request-$(date +%Y%m%d-%H%M%S)-$$.txt"
|
|
234
|
+
printf '%s\n' "$PRD_INLINE" > "$PRD_REQUEST_PATH"
|
|
235
|
+
fi
|
|
236
|
+
|
|
237
|
+
if [ -z "$PRD_REQUEST_PATH" ] || [ ! -f "$PRD_REQUEST_PATH" ]; then
|
|
238
|
+
echo "PRD request missing. Provide a prompt string or --prompt <file>."
|
|
239
|
+
exit 1
|
|
240
|
+
fi
|
|
241
|
+
|
|
242
|
+
if [ "${RALPH_DRY_RUN:-}" = "1" ]; then
|
|
243
|
+
if [[ "$PRD_PATH" == *.json ]]; then
|
|
244
|
+
if [ ! -f "$PRD_PATH" ]; then
|
|
245
|
+
{
|
|
246
|
+
echo '{'
|
|
247
|
+
echo ' "version": 1,'
|
|
248
|
+
echo ' "project": "ralph",'
|
|
249
|
+
echo ' "qualityGates": [],'
|
|
250
|
+
echo ' "stories": []'
|
|
251
|
+
echo '}'
|
|
252
|
+
} > "$PRD_PATH"
|
|
253
|
+
fi
|
|
254
|
+
fi
|
|
255
|
+
exit 0
|
|
256
|
+
fi
|
|
257
|
+
|
|
258
|
+
PRD_PROMPT_FILE="$TMP_DIR/prd-prompt-$(date +%Y%m%d-%H%M%S)-$$.md"
|
|
259
|
+
# Look for prd skill in project skills or ralph bundled skills
|
|
260
|
+
PRD_SKILL_FILE=""
|
|
261
|
+
for skill_path in "$ROOT_DIR/.claude/skills/prd/SKILL.md" "$ROOT_DIR/skills/prd/SKILL.md" "$SCRIPT_DIR/../skills/prd/SKILL.md"; do
|
|
262
|
+
if [[ -f "$skill_path" ]]; then
|
|
263
|
+
PRD_SKILL_FILE="$skill_path"
|
|
264
|
+
break
|
|
265
|
+
fi
|
|
266
|
+
done
|
|
267
|
+
# Also check ralph's bundled skills directory
|
|
268
|
+
if [[ -z "$PRD_SKILL_FILE" ]]; then
|
|
269
|
+
ralph_skills_dir="$(cd "$SCRIPT_DIR/../.." 2>/dev/null && pwd)/skills/prd/SKILL.md"
|
|
270
|
+
if [[ -f "$ralph_skills_dir" ]]; then
|
|
271
|
+
PRD_SKILL_FILE="$ralph_skills_dir"
|
|
272
|
+
fi
|
|
273
|
+
fi
|
|
274
|
+
{
|
|
275
|
+
echo "You are an autonomous coding agent."
|
|
276
|
+
echo ""
|
|
277
|
+
# Include the PRD skill instructions if available
|
|
278
|
+
if [[ -n "$PRD_SKILL_FILE" && -f "$PRD_SKILL_FILE" ]]; then
|
|
279
|
+
cat "$PRD_SKILL_FILE"
|
|
280
|
+
echo ""
|
|
281
|
+
echo "---"
|
|
282
|
+
echo ""
|
|
283
|
+
fi
|
|
284
|
+
echo "# Output Requirements"
|
|
285
|
+
echo ""
|
|
286
|
+
if [[ "$PRD_PATH" == *.json ]]; then
|
|
287
|
+
echo "Save the PRD JSON to: $PRD_PATH"
|
|
288
|
+
else
|
|
289
|
+
echo "Save the PRD as JSON in directory: $PRD_PATH"
|
|
290
|
+
echo "Filename rules: prd-<short-slug>.json using 1-3 meaningful words."
|
|
291
|
+
echo "Examples: prd-workout-tracker.json, prd-usage-billing.json"
|
|
292
|
+
fi
|
|
293
|
+
echo ""
|
|
294
|
+
echo "Do NOT implement anything. Only generate the JSON PRD file."
|
|
295
|
+
echo ""
|
|
296
|
+
echo "After saving the PRD, end your response with:"
|
|
297
|
+
echo "PRD JSON saved to <path>. Close this chat and run \`ralph build\`."
|
|
298
|
+
echo ""
|
|
299
|
+
echo "---"
|
|
300
|
+
echo ""
|
|
301
|
+
echo "# User Request"
|
|
302
|
+
echo ""
|
|
303
|
+
cat "$PRD_REQUEST_PATH"
|
|
304
|
+
} > "$PRD_PROMPT_FILE"
|
|
305
|
+
|
|
306
|
+
# Use PRD_AGENT_CMD for the agent call (always file-based for PRD mode)
|
|
307
|
+
AGENT_CMD="$PRD_AGENT_CMD"
|
|
308
|
+
run_agent "$PRD_PROMPT_FILE"
|
|
309
|
+
exit 0
|
|
310
|
+
fi
|
|
311
|
+
|
|
312
|
+
if [ "${RALPH_DRY_RUN:-}" != "1" ]; then
|
|
313
|
+
require_agent
|
|
314
|
+
fi
|
|
315
|
+
|
|
316
|
+
if [ ! -f "$PROMPT_FILE" ]; then
|
|
317
|
+
echo "Prompt not found: $PROMPT_FILE"
|
|
318
|
+
exit 1
|
|
319
|
+
fi
|
|
320
|
+
|
|
321
|
+
if [ "$MODE" != "prd" ] && [ ! -f "$PRD_PATH" ]; then
|
|
322
|
+
echo "PRD not found: $PRD_PATH"
|
|
323
|
+
exit 1
|
|
324
|
+
fi
|
|
325
|
+
|
|
326
|
+
mkdir -p "$(dirname "$PROGRESS_PATH")" "$TMP_DIR" "$RUNS_DIR"
|
|
327
|
+
|
|
328
|
+
if [ ! -f "$PROGRESS_PATH" ]; then
|
|
329
|
+
{
|
|
330
|
+
echo "# Progress Log"
|
|
331
|
+
echo "Started: $(date)"
|
|
332
|
+
echo ""
|
|
333
|
+
echo "## Codebase Patterns"
|
|
334
|
+
echo "- (add reusable patterns here)"
|
|
335
|
+
echo ""
|
|
336
|
+
echo "---"
|
|
337
|
+
} > "$PROGRESS_PATH"
|
|
338
|
+
fi
|
|
339
|
+
|
|
340
|
+
if [ ! -f "$GUARDRAILS_PATH" ]; then
|
|
341
|
+
{
|
|
342
|
+
echo "# Guardrails (Signs)"
|
|
343
|
+
echo ""
|
|
344
|
+
echo "> Lessons learned from failures. Read before acting."
|
|
345
|
+
echo ""
|
|
346
|
+
echo "## Core Signs"
|
|
347
|
+
echo ""
|
|
348
|
+
echo "### Sign: Read Before Writing"
|
|
349
|
+
echo "- **Trigger**: Before modifying any file"
|
|
350
|
+
echo "- **Instruction**: Read the file first"
|
|
351
|
+
echo "- **Added after**: Core principle"
|
|
352
|
+
echo ""
|
|
353
|
+
echo "### Sign: Test Before Commit"
|
|
354
|
+
echo "- **Trigger**: Before committing changes"
|
|
355
|
+
echo "- **Instruction**: Run required tests and verify outputs"
|
|
356
|
+
echo "- **Added after**: Core principle"
|
|
357
|
+
echo ""
|
|
358
|
+
echo "---"
|
|
359
|
+
echo ""
|
|
360
|
+
echo "## Learned Signs"
|
|
361
|
+
echo ""
|
|
362
|
+
} > "$GUARDRAILS_PATH"
|
|
363
|
+
fi
|
|
364
|
+
|
|
365
|
+
if [ ! -f "$ERRORS_LOG_PATH" ]; then
|
|
366
|
+
{
|
|
367
|
+
echo "# Error Log"
|
|
368
|
+
echo ""
|
|
369
|
+
echo "> Failures and repeated issues. Use this to add guardrails."
|
|
370
|
+
echo ""
|
|
371
|
+
} > "$ERRORS_LOG_PATH"
|
|
372
|
+
fi
|
|
373
|
+
|
|
374
|
+
if [ ! -f "$ACTIVITY_LOG_PATH" ]; then
|
|
375
|
+
{
|
|
376
|
+
echo "# Activity Log"
|
|
377
|
+
echo ""
|
|
378
|
+
echo "## Run Summary"
|
|
379
|
+
echo ""
|
|
380
|
+
echo "## Events"
|
|
381
|
+
echo ""
|
|
382
|
+
} > "$ACTIVITY_LOG_PATH"
|
|
383
|
+
fi
|
|
384
|
+
|
|
385
|
+
RUN_TAG="$(date +%Y%m%d-%H%M%S)-$$"
|
|
386
|
+
|
|
387
|
+
render_prompt() {
|
|
388
|
+
local src="$1"
|
|
389
|
+
local dst="$2"
|
|
390
|
+
local story_meta="$3"
|
|
391
|
+
local story_block="$4"
|
|
392
|
+
local run_id="$5"
|
|
393
|
+
local iter="$6"
|
|
394
|
+
local run_log="$7"
|
|
395
|
+
local run_meta="$8"
|
|
396
|
+
python3 - "$src" "$dst" "$PRD_PATH" "$AGENTS_PATH" "$PROGRESS_PATH" "$ROOT_DIR" "$GUARDRAILS_PATH" "$ERRORS_LOG_PATH" "$ACTIVITY_LOG_PATH" "$GUARDRAILS_REF" "$CONTEXT_REF" "$ACTIVITY_CMD" "$NO_COMMIT" "$story_meta" "$story_block" "$run_id" "$iter" "$run_log" "$run_meta" <<'PY'
|
|
397
|
+
import sys
|
|
398
|
+
from pathlib import Path
|
|
399
|
+
|
|
400
|
+
src = Path(sys.argv[1]).read_text()
|
|
401
|
+
prd, agents, progress, root = sys.argv[3:7]
|
|
402
|
+
guardrails = sys.argv[7]
|
|
403
|
+
errors_log = sys.argv[8]
|
|
404
|
+
activity_log = sys.argv[9]
|
|
405
|
+
guardrails_ref = sys.argv[10]
|
|
406
|
+
context_ref = sys.argv[11]
|
|
407
|
+
activity_cmd = sys.argv[12]
|
|
408
|
+
no_commit = sys.argv[13]
|
|
409
|
+
meta_path = sys.argv[14] if len(sys.argv) > 14 else ""
|
|
410
|
+
block_path = sys.argv[15] if len(sys.argv) > 15 else ""
|
|
411
|
+
run_id = sys.argv[16] if len(sys.argv) > 16 else ""
|
|
412
|
+
iteration = sys.argv[17] if len(sys.argv) > 17 else ""
|
|
413
|
+
run_log = sys.argv[18] if len(sys.argv) > 18 else ""
|
|
414
|
+
run_meta = sys.argv[19] if len(sys.argv) > 19 else ""
|
|
415
|
+
repl = {
|
|
416
|
+
"PRD_PATH": prd,
|
|
417
|
+
"AGENTS_PATH": agents,
|
|
418
|
+
"PROGRESS_PATH": progress,
|
|
419
|
+
"REPO_ROOT": root,
|
|
420
|
+
"GUARDRAILS_PATH": guardrails,
|
|
421
|
+
"ERRORS_LOG_PATH": errors_log,
|
|
422
|
+
"ACTIVITY_LOG_PATH": activity_log,
|
|
423
|
+
"GUARDRAILS_REF": guardrails_ref,
|
|
424
|
+
"CONTEXT_REF": context_ref,
|
|
425
|
+
"ACTIVITY_CMD": activity_cmd,
|
|
426
|
+
"NO_COMMIT": no_commit,
|
|
427
|
+
"RUN_ID": run_id,
|
|
428
|
+
"ITERATION": iteration,
|
|
429
|
+
"RUN_LOG_PATH": run_log,
|
|
430
|
+
"RUN_META_PATH": run_meta,
|
|
431
|
+
}
|
|
432
|
+
story = {"id": "", "title": "", "block": ""}
|
|
433
|
+
quality_gates = []
|
|
434
|
+
if meta_path:
|
|
435
|
+
try:
|
|
436
|
+
import json
|
|
437
|
+
meta = json.loads(Path(meta_path).read_text())
|
|
438
|
+
story["id"] = meta.get("id", "") or ""
|
|
439
|
+
story["title"] = meta.get("title", "") or ""
|
|
440
|
+
quality_gates = meta.get("quality_gates", []) or []
|
|
441
|
+
except Exception:
|
|
442
|
+
pass
|
|
443
|
+
if block_path and Path(block_path).exists():
|
|
444
|
+
story["block"] = Path(block_path).read_text()
|
|
445
|
+
repl["STORY_ID"] = story["id"]
|
|
446
|
+
repl["STORY_TITLE"] = story["title"]
|
|
447
|
+
repl["STORY_BLOCK"] = story["block"]
|
|
448
|
+
if quality_gates:
|
|
449
|
+
repl["QUALITY_GATES"] = "\n".join([f"- {g}" for g in quality_gates])
|
|
450
|
+
else:
|
|
451
|
+
repl["QUALITY_GATES"] = "- (none)"
|
|
452
|
+
for k, v in repl.items():
|
|
453
|
+
src = src.replace("{{" + k + "}}", v)
|
|
454
|
+
Path(sys.argv[2]).write_text(src)
|
|
455
|
+
PY
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
select_story() {
|
|
459
|
+
local meta_out="$1"
|
|
460
|
+
local block_out="$2"
|
|
461
|
+
python3 - "$PRD_PATH" "$meta_out" "$block_out" "$STALE_SECONDS" <<'PY'
|
|
462
|
+
import json
|
|
463
|
+
import os
|
|
464
|
+
import sys
|
|
465
|
+
from pathlib import Path
|
|
466
|
+
from datetime import datetime, timezone
|
|
467
|
+
try:
|
|
468
|
+
import fcntl
|
|
469
|
+
except Exception:
|
|
470
|
+
fcntl = None
|
|
471
|
+
|
|
472
|
+
prd_path = Path(sys.argv[1])
|
|
473
|
+
meta_out = Path(sys.argv[2])
|
|
474
|
+
block_out = Path(sys.argv[3])
|
|
475
|
+
stale_seconds = 0
|
|
476
|
+
if len(sys.argv) > 4:
|
|
477
|
+
try:
|
|
478
|
+
stale_seconds = int(sys.argv[4])
|
|
479
|
+
except Exception:
|
|
480
|
+
stale_seconds = 0
|
|
481
|
+
|
|
482
|
+
if not prd_path.exists():
|
|
483
|
+
meta_out.write_text(json.dumps({"ok": False, "error": "PRD not found"}, indent=2) + "\n")
|
|
484
|
+
block_out.write_text("")
|
|
485
|
+
sys.exit(0)
|
|
486
|
+
|
|
487
|
+
def normalize_status(value):
|
|
488
|
+
if value is None:
|
|
489
|
+
return "open"
|
|
490
|
+
return str(value).strip().lower()
|
|
491
|
+
|
|
492
|
+
def parse_ts(value):
|
|
493
|
+
if not value:
|
|
494
|
+
return None
|
|
495
|
+
text = str(value).strip()
|
|
496
|
+
if text.endswith("Z"):
|
|
497
|
+
text = text[:-1] + "+00:00"
|
|
498
|
+
try:
|
|
499
|
+
return datetime.fromisoformat(text)
|
|
500
|
+
except Exception:
|
|
501
|
+
return None
|
|
502
|
+
|
|
503
|
+
def now_iso():
|
|
504
|
+
return datetime.now(timezone.utc).isoformat()
|
|
505
|
+
|
|
506
|
+
with prd_path.open("r+", encoding="utf-8") as fh:
|
|
507
|
+
if fcntl is not None:
|
|
508
|
+
fcntl.flock(fh.fileno(), fcntl.LOCK_EX)
|
|
509
|
+
try:
|
|
510
|
+
try:
|
|
511
|
+
data = json.load(fh)
|
|
512
|
+
except Exception as exc:
|
|
513
|
+
meta_out.write_text(json.dumps({"ok": False, "error": f"Invalid PRD JSON: {exc}"}, indent=2) + "\n")
|
|
514
|
+
block_out.write_text("")
|
|
515
|
+
sys.exit(0)
|
|
516
|
+
|
|
517
|
+
stories = data.get("stories") if isinstance(data, dict) else None
|
|
518
|
+
if not isinstance(stories, list) or not stories:
|
|
519
|
+
meta_out.write_text(json.dumps({"ok": False, "error": "No stories found in PRD"}, indent=2) + "\n")
|
|
520
|
+
block_out.write_text("")
|
|
521
|
+
sys.exit(0)
|
|
522
|
+
|
|
523
|
+
story_index = {s.get("id"): s for s in stories if isinstance(s, dict)}
|
|
524
|
+
|
|
525
|
+
def is_done(story_id: str) -> bool:
|
|
526
|
+
target = story_index.get(story_id)
|
|
527
|
+
if not isinstance(target, dict):
|
|
528
|
+
return False
|
|
529
|
+
return normalize_status(target.get("status")) == "done"
|
|
530
|
+
|
|
531
|
+
if stale_seconds > 0:
|
|
532
|
+
now = datetime.now(timezone.utc)
|
|
533
|
+
for story in stories:
|
|
534
|
+
if not isinstance(story, dict):
|
|
535
|
+
continue
|
|
536
|
+
if normalize_status(story.get("status")) != "in_progress":
|
|
537
|
+
continue
|
|
538
|
+
started = parse_ts(story.get("startedAt"))
|
|
539
|
+
if started is None or (now - started).total_seconds() > stale_seconds:
|
|
540
|
+
story["status"] = "open"
|
|
541
|
+
story["startedAt"] = None
|
|
542
|
+
story["completedAt"] = None
|
|
543
|
+
story["updatedAt"] = now_iso()
|
|
544
|
+
|
|
545
|
+
candidate = None
|
|
546
|
+
for story in stories:
|
|
547
|
+
if not isinstance(story, dict):
|
|
548
|
+
continue
|
|
549
|
+
if normalize_status(story.get("status")) != "open":
|
|
550
|
+
continue
|
|
551
|
+
deps = story.get("dependsOn") or []
|
|
552
|
+
if not isinstance(deps, list):
|
|
553
|
+
deps = []
|
|
554
|
+
if all(is_done(dep) for dep in deps):
|
|
555
|
+
candidate = story
|
|
556
|
+
break
|
|
557
|
+
|
|
558
|
+
remaining = sum(
|
|
559
|
+
1 for story in stories
|
|
560
|
+
if isinstance(story, dict) and normalize_status(story.get("status")) != "done"
|
|
561
|
+
)
|
|
562
|
+
|
|
563
|
+
meta = {
|
|
564
|
+
"ok": True,
|
|
565
|
+
"total": len(stories),
|
|
566
|
+
"remaining": remaining,
|
|
567
|
+
"quality_gates": data.get("qualityGates", []) or [],
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
if candidate:
|
|
571
|
+
candidate["status"] = "in_progress"
|
|
572
|
+
if not candidate.get("startedAt"):
|
|
573
|
+
candidate["startedAt"] = now_iso()
|
|
574
|
+
candidate["completedAt"] = None
|
|
575
|
+
candidate["updatedAt"] = now_iso()
|
|
576
|
+
meta.update({
|
|
577
|
+
"id": candidate.get("id", ""),
|
|
578
|
+
"title": candidate.get("title", ""),
|
|
579
|
+
})
|
|
580
|
+
|
|
581
|
+
depends = candidate.get("dependsOn") or []
|
|
582
|
+
if not isinstance(depends, list):
|
|
583
|
+
depends = []
|
|
584
|
+
acceptance = candidate.get("acceptanceCriteria") or []
|
|
585
|
+
if not isinstance(acceptance, list):
|
|
586
|
+
acceptance = []
|
|
587
|
+
|
|
588
|
+
description = candidate.get("description") or ""
|
|
589
|
+
block_lines = []
|
|
590
|
+
block_lines.append(f"### {candidate.get('id', '')}: {candidate.get('title', '')}")
|
|
591
|
+
block_lines.append(f"Status: {candidate.get('status', 'open')}")
|
|
592
|
+
block_lines.append(
|
|
593
|
+
f"Depends on: {', '.join(depends) if depends else 'None'}"
|
|
594
|
+
)
|
|
595
|
+
block_lines.append("")
|
|
596
|
+
block_lines.append("Description:")
|
|
597
|
+
block_lines.append(description if description else "(none)")
|
|
598
|
+
block_lines.append("")
|
|
599
|
+
block_lines.append("Acceptance Criteria:")
|
|
600
|
+
if acceptance:
|
|
601
|
+
block_lines.extend([f"- [ ] {item}" for item in acceptance])
|
|
602
|
+
else:
|
|
603
|
+
block_lines.append("- (none)")
|
|
604
|
+
block_out.write_text("\n".join(block_lines).rstrip() + "\n")
|
|
605
|
+
else:
|
|
606
|
+
block_out.write_text("")
|
|
607
|
+
|
|
608
|
+
fh.seek(0)
|
|
609
|
+
fh.truncate()
|
|
610
|
+
json.dump(data, fh, indent=2)
|
|
611
|
+
fh.write("\n")
|
|
612
|
+
fh.flush()
|
|
613
|
+
os.fsync(fh.fileno())
|
|
614
|
+
finally:
|
|
615
|
+
if fcntl is not None:
|
|
616
|
+
fcntl.flock(fh.fileno(), fcntl.LOCK_UN)
|
|
617
|
+
|
|
618
|
+
meta_out.write_text(json.dumps(meta, indent=2) + "\n")
|
|
619
|
+
PY
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
remaining_stories() {
|
|
623
|
+
local meta_file="$1"
|
|
624
|
+
python3 - "$meta_file" <<'PY'
|
|
625
|
+
import json
|
|
626
|
+
import sys
|
|
627
|
+
from pathlib import Path
|
|
628
|
+
|
|
629
|
+
data = json.loads(Path(sys.argv[1]).read_text())
|
|
630
|
+
print(data.get("remaining", "unknown"))
|
|
631
|
+
PY
|
|
632
|
+
}
|
|
633
|
+
|
|
634
|
+
remaining_from_prd() {
|
|
635
|
+
python3 - "$PRD_PATH" <<'PY'
|
|
636
|
+
import json
|
|
637
|
+
import sys
|
|
638
|
+
from pathlib import Path
|
|
639
|
+
|
|
640
|
+
prd_path = Path(sys.argv[1])
|
|
641
|
+
if not prd_path.exists():
|
|
642
|
+
print("unknown")
|
|
643
|
+
sys.exit(0)
|
|
644
|
+
|
|
645
|
+
try:
|
|
646
|
+
data = json.loads(prd_path.read_text())
|
|
647
|
+
except Exception:
|
|
648
|
+
print("unknown")
|
|
649
|
+
sys.exit(0)
|
|
650
|
+
|
|
651
|
+
stories = data.get("stories") if isinstance(data, dict) else None
|
|
652
|
+
if not isinstance(stories, list):
|
|
653
|
+
print("unknown")
|
|
654
|
+
sys.exit(0)
|
|
655
|
+
|
|
656
|
+
def normalize_status(value):
|
|
657
|
+
if value is None:
|
|
658
|
+
return "open"
|
|
659
|
+
return str(value).strip().lower()
|
|
660
|
+
|
|
661
|
+
remaining = sum(
|
|
662
|
+
1 for story in stories
|
|
663
|
+
if isinstance(story, dict) and normalize_status(story.get("status")) != "done"
|
|
664
|
+
)
|
|
665
|
+
print(remaining)
|
|
666
|
+
PY
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
story_field() {
|
|
670
|
+
local meta_file="$1"
|
|
671
|
+
local field="$2"
|
|
672
|
+
python3 - "$meta_file" "$field" <<'PY'
|
|
673
|
+
import json
|
|
674
|
+
import sys
|
|
675
|
+
from pathlib import Path
|
|
676
|
+
|
|
677
|
+
data = json.loads(Path(sys.argv[1]).read_text())
|
|
678
|
+
field = sys.argv[2]
|
|
679
|
+
print(data.get(field, ""))
|
|
680
|
+
PY
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
update_story_status() {
|
|
684
|
+
local story_id="$1"
|
|
685
|
+
local new_status="$2"
|
|
686
|
+
python3 - "$PRD_PATH" "$story_id" "$new_status" <<'PY'
|
|
687
|
+
import json
|
|
688
|
+
import os
|
|
689
|
+
import sys
|
|
690
|
+
from pathlib import Path
|
|
691
|
+
from datetime import datetime, timezone
|
|
692
|
+
try:
|
|
693
|
+
import fcntl
|
|
694
|
+
except Exception:
|
|
695
|
+
fcntl = None
|
|
696
|
+
|
|
697
|
+
prd_path = Path(sys.argv[1])
|
|
698
|
+
story_id = sys.argv[2]
|
|
699
|
+
new_status = sys.argv[3]
|
|
700
|
+
|
|
701
|
+
if not story_id:
|
|
702
|
+
sys.exit(0)
|
|
703
|
+
|
|
704
|
+
if not prd_path.exists():
|
|
705
|
+
sys.exit(0)
|
|
706
|
+
|
|
707
|
+
def now_iso():
|
|
708
|
+
return datetime.now(timezone.utc).isoformat()
|
|
709
|
+
|
|
710
|
+
with prd_path.open("r+", encoding="utf-8") as fh:
|
|
711
|
+
if fcntl is not None:
|
|
712
|
+
fcntl.flock(fh.fileno(), fcntl.LOCK_EX)
|
|
713
|
+
try:
|
|
714
|
+
data = json.load(fh)
|
|
715
|
+
stories = data.get("stories") if isinstance(data, dict) else None
|
|
716
|
+
if not isinstance(stories, list):
|
|
717
|
+
sys.exit(0)
|
|
718
|
+
for story in stories:
|
|
719
|
+
if isinstance(story, dict) and story.get("id") == story_id:
|
|
720
|
+
story["status"] = new_status
|
|
721
|
+
story["updatedAt"] = now_iso()
|
|
722
|
+
if new_status == "in_progress":
|
|
723
|
+
if not story.get("startedAt"):
|
|
724
|
+
story["startedAt"] = now_iso()
|
|
725
|
+
story["completedAt"] = None
|
|
726
|
+
elif new_status == "done":
|
|
727
|
+
story["completedAt"] = now_iso()
|
|
728
|
+
if not story.get("startedAt"):
|
|
729
|
+
story["startedAt"] = now_iso()
|
|
730
|
+
elif new_status == "open":
|
|
731
|
+
story["startedAt"] = None
|
|
732
|
+
story["completedAt"] = None
|
|
733
|
+
break
|
|
734
|
+
fh.seek(0)
|
|
735
|
+
fh.truncate()
|
|
736
|
+
json.dump(data, fh, indent=2)
|
|
737
|
+
fh.write("\n")
|
|
738
|
+
fh.flush()
|
|
739
|
+
os.fsync(fh.fileno())
|
|
740
|
+
finally:
|
|
741
|
+
if fcntl is not None:
|
|
742
|
+
fcntl.flock(fh.fileno(), fcntl.LOCK_UN)
|
|
743
|
+
PY
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
log_activity() {
|
|
747
|
+
local message="$1"
|
|
748
|
+
local timestamp
|
|
749
|
+
timestamp=$(date '+%Y-%m-%d %H:%M:%S')
|
|
750
|
+
echo "[$timestamp] $message" >> "$ACTIVITY_LOG_PATH"
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
log_error() {
|
|
754
|
+
local message="$1"
|
|
755
|
+
local timestamp
|
|
756
|
+
timestamp=$(date '+%Y-%m-%d %H:%M:%S')
|
|
757
|
+
echo "[$timestamp] $message" >> "$ERRORS_LOG_PATH"
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
append_run_summary() {
|
|
761
|
+
local line="$1"
|
|
762
|
+
python3 - "$ACTIVITY_LOG_PATH" "$line" <<'PY'
|
|
763
|
+
import sys
|
|
764
|
+
from pathlib import Path
|
|
765
|
+
|
|
766
|
+
path = Path(sys.argv[1])
|
|
767
|
+
line = sys.argv[2]
|
|
768
|
+
text = path.read_text().splitlines()
|
|
769
|
+
out = []
|
|
770
|
+
inserted = False
|
|
771
|
+
for l in text:
|
|
772
|
+
out.append(l)
|
|
773
|
+
if not inserted and l.strip() == "## Run Summary":
|
|
774
|
+
out.append(f"- {line}")
|
|
775
|
+
inserted = True
|
|
776
|
+
if not inserted:
|
|
777
|
+
out = [
|
|
778
|
+
"# Activity Log",
|
|
779
|
+
"",
|
|
780
|
+
"## Run Summary",
|
|
781
|
+
f"- {line}",
|
|
782
|
+
"",
|
|
783
|
+
"## Events",
|
|
784
|
+
"",
|
|
785
|
+
] + text
|
|
786
|
+
Path(path).write_text("\n".join(out).rstrip() + "\n")
|
|
787
|
+
PY
|
|
788
|
+
}
|
|
789
|
+
|
|
790
|
+
write_run_meta() {
|
|
791
|
+
local path="$1"
|
|
792
|
+
local mode="$2"
|
|
793
|
+
local iter="$3"
|
|
794
|
+
local run_id="$4"
|
|
795
|
+
local story_id="$5"
|
|
796
|
+
local story_title="$6"
|
|
797
|
+
local started="$7"
|
|
798
|
+
local ended="$8"
|
|
799
|
+
local duration="$9"
|
|
800
|
+
local status="${10}"
|
|
801
|
+
local log_file="${11}"
|
|
802
|
+
local head_before="${12}"
|
|
803
|
+
local head_after="${13}"
|
|
804
|
+
local commit_list="${14}"
|
|
805
|
+
local changed_files="${15}"
|
|
806
|
+
local dirty_files="${16}"
|
|
807
|
+
{
|
|
808
|
+
echo "# Ralph Run Summary"
|
|
809
|
+
echo ""
|
|
810
|
+
echo "- Run ID: $run_id"
|
|
811
|
+
echo "- Iteration: $iter"
|
|
812
|
+
echo "- Mode: $mode"
|
|
813
|
+
if [ -n "$story_id" ]; then
|
|
814
|
+
echo "- Story: $story_id: $story_title"
|
|
815
|
+
fi
|
|
816
|
+
echo "- Started: $started"
|
|
817
|
+
echo "- Ended: $ended"
|
|
818
|
+
echo "- Duration: ${duration}s"
|
|
819
|
+
echo "- Status: $status"
|
|
820
|
+
echo "- Log: $log_file"
|
|
821
|
+
echo ""
|
|
822
|
+
echo "## Git"
|
|
823
|
+
echo "- Head (before): ${head_before:-unknown}"
|
|
824
|
+
echo "- Head (after): ${head_after:-unknown}"
|
|
825
|
+
echo ""
|
|
826
|
+
echo "### Commits"
|
|
827
|
+
if [ -n "$commit_list" ]; then
|
|
828
|
+
echo "$commit_list"
|
|
829
|
+
else
|
|
830
|
+
echo "- (none)"
|
|
831
|
+
fi
|
|
832
|
+
echo ""
|
|
833
|
+
echo "### Changed Files (commits)"
|
|
834
|
+
if [ -n "$changed_files" ]; then
|
|
835
|
+
echo "$changed_files"
|
|
836
|
+
else
|
|
837
|
+
echo "- (none)"
|
|
838
|
+
fi
|
|
839
|
+
echo ""
|
|
840
|
+
echo "### Uncommitted Changes"
|
|
841
|
+
if [ -n "$dirty_files" ]; then
|
|
842
|
+
echo "$dirty_files"
|
|
843
|
+
else
|
|
844
|
+
echo "- (clean)"
|
|
845
|
+
fi
|
|
846
|
+
echo ""
|
|
847
|
+
} > "$path"
|
|
848
|
+
}
|
|
849
|
+
|
|
850
|
+
git_head() {
|
|
851
|
+
if git -C "$ROOT_DIR" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
|
852
|
+
git -C "$ROOT_DIR" rev-parse HEAD 2>/dev/null || true
|
|
853
|
+
else
|
|
854
|
+
echo ""
|
|
855
|
+
fi
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
git_commit_list() {
|
|
859
|
+
local before="$1"
|
|
860
|
+
local after="$2"
|
|
861
|
+
if [ -n "$before" ] && [ -n "$after" ] && [ "$before" != "$after" ]; then
|
|
862
|
+
git -C "$ROOT_DIR" log --oneline "$before..$after" | sed 's/^/- /'
|
|
863
|
+
else
|
|
864
|
+
echo ""
|
|
865
|
+
fi
|
|
866
|
+
}
|
|
867
|
+
|
|
868
|
+
git_changed_files() {
|
|
869
|
+
local before="$1"
|
|
870
|
+
local after="$2"
|
|
871
|
+
if [ -n "$before" ] && [ -n "$after" ] && [ "$before" != "$after" ]; then
|
|
872
|
+
git -C "$ROOT_DIR" diff --name-only "$before" "$after" | sed 's/^/- /'
|
|
873
|
+
else
|
|
874
|
+
echo ""
|
|
875
|
+
fi
|
|
876
|
+
}
|
|
877
|
+
|
|
878
|
+
git_dirty_files() {
|
|
879
|
+
if git -C "$ROOT_DIR" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
|
880
|
+
git -C "$ROOT_DIR" status --porcelain | awk '{print "- " $2}'
|
|
881
|
+
else
|
|
882
|
+
echo ""
|
|
883
|
+
fi
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
echo "Ralph mode: $MODE"
|
|
887
|
+
echo "Max iterations: $MAX_ITERATIONS"
|
|
888
|
+
echo "PRD: $PRD_PATH"
|
|
889
|
+
HAS_ERROR="false"
|
|
890
|
+
|
|
891
|
+
for i in $(seq 1 "$MAX_ITERATIONS"); do
|
|
892
|
+
echo ""
|
|
893
|
+
echo "═══════════════════════════════════════════════════════"
|
|
894
|
+
echo " Ralph Iteration $i of $MAX_ITERATIONS"
|
|
895
|
+
echo "═══════════════════════════════════════════════════════"
|
|
896
|
+
|
|
897
|
+
STORY_META=""
|
|
898
|
+
STORY_BLOCK=""
|
|
899
|
+
ITER_START=$(date +%s)
|
|
900
|
+
ITER_START_FMT=$(date '+%Y-%m-%d %H:%M:%S')
|
|
901
|
+
if [ "$MODE" = "build" ]; then
|
|
902
|
+
STORY_META="$TMP_DIR/story-$RUN_TAG-$i.json"
|
|
903
|
+
STORY_BLOCK="$TMP_DIR/story-$RUN_TAG-$i.md"
|
|
904
|
+
select_story "$STORY_META" "$STORY_BLOCK"
|
|
905
|
+
REMAINING="$(remaining_stories "$STORY_META")"
|
|
906
|
+
if [ "$REMAINING" = "unknown" ]; then
|
|
907
|
+
echo "Could not parse stories from PRD: $PRD_PATH"
|
|
908
|
+
exit 1
|
|
909
|
+
fi
|
|
910
|
+
if [ "$REMAINING" = "0" ]; then
|
|
911
|
+
echo "No remaining stories."
|
|
912
|
+
exit 0
|
|
913
|
+
fi
|
|
914
|
+
STORY_ID="$(story_field "$STORY_META" "id")"
|
|
915
|
+
STORY_TITLE="$(story_field "$STORY_META" "title")"
|
|
916
|
+
if [ -z "$STORY_ID" ]; then
|
|
917
|
+
echo "No actionable open stories (all blocked or in progress). Remaining: $REMAINING"
|
|
918
|
+
exit 0
|
|
919
|
+
fi
|
|
920
|
+
fi
|
|
921
|
+
|
|
922
|
+
HEAD_BEFORE="$(git_head)"
|
|
923
|
+
PROMPT_RENDERED="$TMP_DIR/prompt-$RUN_TAG-$i.md"
|
|
924
|
+
LOG_FILE="$RUNS_DIR/run-$RUN_TAG-iter-$i.log"
|
|
925
|
+
RUN_META="$RUNS_DIR/run-$RUN_TAG-iter-$i.md"
|
|
926
|
+
render_prompt "$PROMPT_FILE" "$PROMPT_RENDERED" "$STORY_META" "$STORY_BLOCK" "$RUN_TAG" "$i" "$LOG_FILE" "$RUN_META"
|
|
927
|
+
|
|
928
|
+
if [ "$MODE" = "build" ] && [ -n "${STORY_ID:-}" ]; then
|
|
929
|
+
log_activity "ITERATION $i start (mode=$MODE story=$STORY_ID)"
|
|
930
|
+
else
|
|
931
|
+
log_activity "ITERATION $i start (mode=$MODE)"
|
|
932
|
+
fi
|
|
933
|
+
set +e
|
|
934
|
+
if [ "${RALPH_DRY_RUN:-}" = "1" ]; then
|
|
935
|
+
echo "[RALPH_DRY_RUN] Skipping agent execution." | tee "$LOG_FILE"
|
|
936
|
+
CMD_STATUS=0
|
|
937
|
+
else
|
|
938
|
+
run_agent "$PROMPT_RENDERED" 2>&1 | tee "$LOG_FILE"
|
|
939
|
+
CMD_STATUS=$?
|
|
940
|
+
fi
|
|
941
|
+
set -e
|
|
942
|
+
if [ "$CMD_STATUS" -eq 130 ] || [ "$CMD_STATUS" -eq 143 ]; then
|
|
943
|
+
echo "Interrupted."
|
|
944
|
+
exit "$CMD_STATUS"
|
|
945
|
+
fi
|
|
946
|
+
ITER_END=$(date +%s)
|
|
947
|
+
ITER_END_FMT=$(date '+%Y-%m-%d %H:%M:%S')
|
|
948
|
+
ITER_DURATION=$((ITER_END - ITER_START))
|
|
949
|
+
HEAD_AFTER="$(git_head)"
|
|
950
|
+
log_activity "ITERATION $i end (duration=${ITER_DURATION}s)"
|
|
951
|
+
if [ "$CMD_STATUS" -ne 0 ]; then
|
|
952
|
+
log_error "ITERATION $i command failed (status=$CMD_STATUS)"
|
|
953
|
+
HAS_ERROR="true"
|
|
954
|
+
fi
|
|
955
|
+
COMMIT_LIST="$(git_commit_list "$HEAD_BEFORE" "$HEAD_AFTER")"
|
|
956
|
+
CHANGED_FILES="$(git_changed_files "$HEAD_BEFORE" "$HEAD_AFTER")"
|
|
957
|
+
DIRTY_FILES="$(git_dirty_files)"
|
|
958
|
+
STATUS_LABEL="success"
|
|
959
|
+
if [ "$CMD_STATUS" -ne 0 ]; then
|
|
960
|
+
STATUS_LABEL="error"
|
|
961
|
+
fi
|
|
962
|
+
if [ "$MODE" = "build" ] && [ "$NO_COMMIT" = "false" ] && [ -n "$DIRTY_FILES" ]; then
|
|
963
|
+
log_error "ITERATION $i left uncommitted changes; review run summary at $RUN_META"
|
|
964
|
+
fi
|
|
965
|
+
write_run_meta "$RUN_META" "$MODE" "$i" "$RUN_TAG" "${STORY_ID:-}" "${STORY_TITLE:-}" "$ITER_START_FMT" "$ITER_END_FMT" "$ITER_DURATION" "$STATUS_LABEL" "$LOG_FILE" "$HEAD_BEFORE" "$HEAD_AFTER" "$COMMIT_LIST" "$CHANGED_FILES" "$DIRTY_FILES"
|
|
966
|
+
if [ "$MODE" = "build" ] && [ -n "${STORY_ID:-}" ]; then
|
|
967
|
+
append_run_summary "$(date '+%Y-%m-%d %H:%M:%S') | run=$RUN_TAG | iter=$i | mode=$MODE | story=$STORY_ID | duration=${ITER_DURATION}s | status=$STATUS_LABEL"
|
|
968
|
+
else
|
|
969
|
+
append_run_summary "$(date '+%Y-%m-%d %H:%M:%S') | run=$RUN_TAG | iter=$i | mode=$MODE | duration=${ITER_DURATION}s | status=$STATUS_LABEL"
|
|
970
|
+
fi
|
|
971
|
+
|
|
972
|
+
if [ "$MODE" = "build" ]; then
|
|
973
|
+
if [ "$CMD_STATUS" -ne 0 ]; then
|
|
974
|
+
log_error "ITERATION $i exited non-zero; review $LOG_FILE"
|
|
975
|
+
update_story_status "$STORY_ID" "open"
|
|
976
|
+
echo "Iteration failed; story reset to open."
|
|
977
|
+
elif grep -q "<promise>COMPLETE</promise>" "$LOG_FILE"; then
|
|
978
|
+
update_story_status "$STORY_ID" "done"
|
|
979
|
+
echo "Completion signal received; story marked done."
|
|
980
|
+
else
|
|
981
|
+
update_story_status "$STORY_ID" "open"
|
|
982
|
+
echo "No completion signal; story reset to open."
|
|
983
|
+
fi
|
|
984
|
+
REMAINING="$(remaining_from_prd)"
|
|
985
|
+
echo "Iteration $i complete. Remaining stories: $REMAINING"
|
|
986
|
+
if [ "$REMAINING" = "0" ]; then
|
|
987
|
+
echo "No remaining stories."
|
|
988
|
+
exit 0
|
|
989
|
+
fi
|
|
990
|
+
else
|
|
991
|
+
echo "Iteration $i complete."
|
|
992
|
+
fi
|
|
993
|
+
sleep 2
|
|
994
|
+
|
|
995
|
+
done
|
|
996
|
+
|
|
997
|
+
echo "Reached max iterations ($MAX_ITERATIONS)."
|
|
998
|
+
if [ "$HAS_ERROR" = "true" ]; then
|
|
999
|
+
exit 1
|
|
1000
|
+
fi
|
|
1001
|
+
exit 0
|