mirror of
https://github.com/jjbrunton/Sauci.git
synced 2026-10-31 10:19:43 +00:00
refactor(swipe): split swipe screen modules
Move swipe screen logic into hooks/services and extract state-specific UI components to keep files small while preserving existing behavior. Reference: US-002
This commit is contained in:
1 parent
209dc652d2
commit
8d4d9d8750
55 files changed
+11127
-1681
No files matched your search
@@ -0,0 +1,126 @@
|
||||
# Build
|
||||
|
||||
You are an autonomous coding agent. Your task is to complete the work for exactly one story and record the outcome.
|
||||
|
||||
## Paths
|
||||
- PRD: {{PRD_PATH}}
|
||||
- AGENTS (optional): {{AGENTS_PATH}}
|
||||
- Progress Log: {{PROGRESS_PATH}}
|
||||
- Guardrails: {{GUARDRAILS_PATH}}
|
||||
- Guardrails Reference: {{GUARDRAILS_REF}}
|
||||
- Context Reference: {{CONTEXT_REF}}
|
||||
- Errors Log: {{ERRORS_LOG_PATH}}
|
||||
- Activity Log: {{ACTIVITY_LOG_PATH}}
|
||||
- Activity Logger: {{ACTIVITY_CMD}}
|
||||
- No-commit: {{NO_COMMIT}}
|
||||
- Repo Root: {{REPO_ROOT}}
|
||||
- Run ID: {{RUN_ID}}
|
||||
- Iteration: {{ITERATION}}
|
||||
- Run Log: {{RUN_LOG_PATH}}
|
||||
- Run Summary: {{RUN_META_PATH}}
|
||||
|
||||
## Global Quality Gates (apply to every story)
|
||||
{{QUALITY_GATES}}
|
||||
|
||||
## Selected Story (Do not change scope)
|
||||
ID: {{STORY_ID}}
|
||||
Title: {{STORY_TITLE}}
|
||||
|
||||
Story details:
|
||||
{{STORY_BLOCK}}
|
||||
|
||||
If the story details are empty or missing, STOP and report that the PRD story format could not be parsed.
|
||||
|
||||
## Rules (Non-Negotiable)
|
||||
- Implement **only** the work required to complete the selected story.
|
||||
- Complete all tasks associated with this story (and only this story).
|
||||
- Do NOT ask the user questions.
|
||||
- Do NOT change unrelated code.
|
||||
- Do NOT assume something is unimplemented — confirm by reading code.
|
||||
- Implement completely; no placeholders or stubs.
|
||||
- If No-commit is true, do NOT commit or push changes.
|
||||
- Do NOT edit the PRD JSON (status is handled by the loop).
|
||||
- All changes made during the run must be committed (including updates to progress/logs).
|
||||
- Before committing, perform a final **security**, **performance**, and **regression** review of your changes.
|
||||
|
||||
## Your Task (Do this in order)
|
||||
1. Read {{GUARDRAILS_PATH}} before any code changes.
|
||||
2. Read {{ERRORS_LOG_PATH}} for repeated failures to avoid.
|
||||
3. Read {{PRD_PATH}} for global context (do not edit).
|
||||
4. Fully audit and read all necessary files to understand the task end-to-end before implementing. Do not assume missing functionality.
|
||||
5. If {{AGENTS_PATH}} exists, follow its build/test instructions.
|
||||
6. Implement only the tasks that belong to {{STORY_ID}}.
|
||||
7. Run verification commands listed in the story, the global quality gates, and in {{AGENTS_PATH}} (if required).
|
||||
8. If the project has a build or dev workflow, run what applies:
|
||||
- Build step (e.g., `npm run build`) if defined.
|
||||
- Dev server (e.g., `npm run dev`, `wrangler dev`) if it is the normal validation path.
|
||||
- Confirm no runtime/build errors in the console.
|
||||
9. Perform a brief audit before committing:
|
||||
- **Security:** check for obvious vulnerabilities or unsafe handling introduced by your changes.
|
||||
- **Performance:** check for avoidable regressions (extra queries, heavy loops, unnecessary re-renders).
|
||||
- **Regression:** verify existing behavior that could be impacted still works.
|
||||
10. If No-commit is false, commit changes using the `$commit` skill.
|
||||
- Stage everything: `git add -A`
|
||||
- Confirm a clean working tree after commit: `git status --porcelain` should be empty.
|
||||
- After committing, capture the commit hash and subject using:
|
||||
`git show -s --format="%h %s" HEAD`.
|
||||
11. Append a progress entry to {{PROGRESS_PATH}} with run/commit/test details (format below).
|
||||
If No-commit is true, skip committing and note it in the progress entry.
|
||||
|
||||
## Progress Entry Format (Append Only)
|
||||
```
|
||||
## [Date/Time] - {{STORY_ID}}: {{STORY_TITLE}}
|
||||
Thread: [codex exec session id if available, otherwise leave blank]
|
||||
Run: {{RUN_ID}} (iteration {{ITERATION}})
|
||||
Run log: {{RUN_LOG_PATH}}
|
||||
Run summary: {{RUN_META_PATH}}
|
||||
- Guardrails reviewed: yes
|
||||
- No-commit run: {{NO_COMMIT}}
|
||||
- Commit: <hash> <subject> (or `none` + reason)
|
||||
- Post-commit status: `clean` or list remaining files
|
||||
- Verification:
|
||||
- Command: <exact command> -> PASS/FAIL
|
||||
- Command: <exact command> -> PASS/FAIL
|
||||
- Files changed:
|
||||
- <file path>
|
||||
- <file path>
|
||||
- What was implemented
|
||||
- **Learnings for future iterations:**
|
||||
- Patterns discovered
|
||||
- Gotchas encountered
|
||||
- Useful context
|
||||
---
|
||||
```
|
||||
|
||||
## Completion Signal
|
||||
Only output the completion signal when the **selected story** is fully complete and verified.
|
||||
When the selected story is complete, output:
|
||||
<promise>COMPLETE</promise>
|
||||
|
||||
Otherwise, end normally without the signal.
|
||||
|
||||
## Additional Guardrails
|
||||
- When authoring documentation, capture the why (tests + implementation intent).
|
||||
- If you learn how to run/build/test the project, update {{AGENTS_PATH}} briefly (operational only).
|
||||
- Keep AGENTS operational only; progress notes belong in {{PROGRESS_PATH}}.
|
||||
- If you hit repeated errors, log them in {{ERRORS_LOG_PATH}} and add a Sign to {{GUARDRAILS_PATH}} using {{GUARDRAILS_REF}} as the template.
|
||||
|
||||
## Activity Logging (Required)
|
||||
Log major actions to {{ACTIVITY_LOG_PATH}} using the helper:
|
||||
```
|
||||
{{ACTIVITY_CMD}} "message"
|
||||
```
|
||||
Log at least:
|
||||
- Start of work on the story
|
||||
- After major code changes
|
||||
- After tests/verification
|
||||
- After updating progress log
|
||||
|
||||
## Browser Testing (Required for Frontend Stories)
|
||||
If the selected story changes UI, you MUST verify it in the browser:
|
||||
1. Load the `dev-browser` skill.
|
||||
2. Navigate to the relevant page.
|
||||
3. Verify the UI changes work as expected.
|
||||
4. Take a screenshot if helpful for the progress log.
|
||||
|
||||
A frontend story is NOT complete until browser verification passes.
|
||||
@@ -0,0 +1,190 @@
|
||||
# Ralph
|
||||
|
||||

|
||||
|
||||
Ralph is a minimal, file‑based agent loop for autonomous coding. Each iteration starts fresh, reads the same on‑disk state, and commits work for one story at a time.
|
||||
|
||||
## How it works
|
||||
|
||||
Ralph treats **files and git** as memory, not the model context:
|
||||
|
||||
- **PRD (JSON)** defines stories, gates, and status
|
||||
- **Loop** executes one story per iteration
|
||||
- **State** persists in `.ralph/`
|
||||
|
||||

|
||||
|
||||
## Global CLI (recommended)
|
||||
|
||||
Install and run Ralph from anywhere:
|
||||
|
||||
```bash
|
||||
npm i -g @iannuttall/ralph
|
||||
ralph prd # launches an interactive prompt
|
||||
ralph build 1 # one Ralph run
|
||||
```
|
||||
|
||||
### Template hierarchy
|
||||
|
||||
Ralph will look for templates in this order:
|
||||
|
||||
1. `.agents/ralph/` in the current project (if present)
|
||||
2. Bundled defaults shipped with this repo
|
||||
|
||||
State and logs always go to `.ralph/` in the project.
|
||||
|
||||
### Install templates into a project (optional overrides)
|
||||
|
||||
```bash
|
||||
ralph install
|
||||
```
|
||||
|
||||
This creates `.agents/ralph/` in the current repo so you can customize prompts and loop behavior. During install, you’ll be asked if you want to add the required skills.
|
||||
|
||||
### Install required skills (optional)
|
||||
|
||||
```bash
|
||||
ralph install --skills
|
||||
```
|
||||
|
||||
You’ll be prompted for agent (codex/claude/droid/opencode) and local vs global install. Skills installed: **commit**, **dev-browser**, **prd**.
|
||||
If you skipped skills during `ralph install`, you can run `ralph install --skills` anytime.
|
||||
|
||||
## Quick start (project)
|
||||
|
||||
1) Create your PRD (JSON) or generate one:
|
||||
```
|
||||
ralph prd
|
||||
```
|
||||
Requires the **prd** skill (install via `ralph install --skills`).
|
||||
|
||||
Example prompt text:
|
||||
```
|
||||
A lightweight uptime monitor (Hono app), deployed on Cloudflare, with email alerts via AWS SES
|
||||
```
|
||||
|
||||
Default output (agent chooses a short filename in `.agents/tasks/`):
|
||||
```
|
||||
.agents/tasks/prd-<short>.json
|
||||
```
|
||||
|
||||
2) Run one build iteration:
|
||||
```
|
||||
ralph build 1 # one Ralph run
|
||||
```
|
||||
|
||||
No‑commit dry run:
|
||||
```
|
||||
ralph build 1 --no-commit # one Ralph run
|
||||
```
|
||||
|
||||
Override PRD output for `ralph prd`:
|
||||
```
|
||||
ralph prd --out .agents/tasks/prd-api.json
|
||||
```
|
||||
Optional human overview (generated from JSON):
|
||||
```
|
||||
ralph overview
|
||||
```
|
||||
This writes a tiny overview alongside the PRD: `prd-<slug>.overview.md`.
|
||||
|
||||
PRD story status fields are updated automatically by the loop:
|
||||
- `open` → selectable
|
||||
- `in_progress` → locked by a running loop (with `startedAt`)
|
||||
- `done` → completed (with `completedAt`)
|
||||
|
||||
If a loop crashes and a story stays `in_progress`, you can set `STALE_SECONDS` in `.agents/ralph/config.sh` to allow Ralph to automatically reopen stalled stories.
|
||||
|
||||
## Override PRD paths
|
||||
|
||||
You can point Ralph at a different PRD JSON file via CLI flags:
|
||||
|
||||
```bash
|
||||
ralph build 1 --prd .agents/tasks/prd-api.json # one Ralph run
|
||||
```
|
||||
|
||||
Optional progress override:
|
||||
|
||||
```bash
|
||||
ralph build 1 --progress .ralph/progress-api.md # one Ralph run
|
||||
```
|
||||
|
||||
If multiple PRD JSON files exist in `.agents/tasks/` and you omit `--prd`, Ralph will prompt you to choose.
|
||||
|
||||
Optional config file (if you installed templates):
|
||||
|
||||
```
|
||||
.agents/ralph/config.sh
|
||||
```
|
||||
|
||||
## Choose the agent runner
|
||||
|
||||
Set `AGENT_CMD` in `.agents/ralph/config.sh` to switch agents:
|
||||
|
||||
```
|
||||
AGENT_CMD="codex exec --yolo -"
|
||||
AGENT_CMD="claude -p --dangerously-skip-permissions \"\$(cat {prompt})\""
|
||||
AGENT_CMD="droid exec --skip-permissions-unsafe -f {prompt}"
|
||||
AGENT_CMD="opencode run \"$(cat {prompt})\""
|
||||
```
|
||||
|
||||
Or override per run:
|
||||
|
||||
```
|
||||
ralph prd --agent=codex
|
||||
ralph build 1 --agent=codex # one Ralph run
|
||||
ralph build 1 --agent=claude # one Ralph run
|
||||
ralph build 1 --agent=droid # one Ralph run
|
||||
ralph build 1 --agent=opencode # one Ralph run
|
||||
```
|
||||
|
||||
If the CLI isn’t installed, Ralph prints install hints:
|
||||
|
||||
```
|
||||
codex -> npm i -g @openai/codex
|
||||
claude -> curl -fsSL https://claude.ai/install.sh | bash
|
||||
droid -> curl -fsSL https://app.factory.ai/cli | sh
|
||||
opencode -> curl -fsSL https://opencode.ai/install.sh | bash
|
||||
```
|
||||
|
||||
## State files (.ralph/)
|
||||
|
||||
- `progress.md` — append‑only progress log
|
||||
- `guardrails.md` — “Signs” (lessons learned)
|
||||
- `activity.log` — activity + timing log
|
||||
- `errors.log` — repeated failures and notes
|
||||
- `runs/` — raw run logs + summaries
|
||||
|
||||
## Notes
|
||||
|
||||
- `.agents/ralph` is portable and can be copied between repos.
|
||||
- `.ralph` is per‑project state.
|
||||
- Use `{prompt}` in `AGENT_CMD` when agent needs a file path instead of stdin.
|
||||
- Examples: see `examples/commands.md`.
|
||||
- **OpenCode server mode**: For faster performance with OpenCode, run `opencode serve` in a separate terminal and uncomment the `AGENT_OPENCODE_CMD` lines in `.agents/ralph/agents.sh` to use `--attach http://localhost:4096`. This avoids cold boot on every run.
|
||||
|
||||
## Tests
|
||||
|
||||
Dry-run smoke tests (no agent required):
|
||||
|
||||
```bash
|
||||
npm test
|
||||
```
|
||||
|
||||
Fast agent health check (real agent call, minimal output):
|
||||
|
||||
```bash
|
||||
npm run test:ping
|
||||
```
|
||||
|
||||
Optional integration test (requires agents installed):
|
||||
|
||||
```bash
|
||||
RALPH_INTEGRATION=1 npm test
|
||||
```
|
||||
|
||||
Full real-agent loop test:
|
||||
|
||||
```bash
|
||||
npm run test:real
|
||||
```
|
||||
@@ -0,0 +1,15 @@
|
||||
#!/usr/bin/env bash
|
||||
# Default agent command templates (used by loop.sh and CLI).
|
||||
|
||||
AGENT_CODEX_CMD="codex exec --yolo --skip-git-repo-check -"
|
||||
AGENT_CODEX_INTERACTIVE_CMD="codex --yolo {prompt}"
|
||||
AGENT_CLAUDE_CMD="claude -p --dangerously-skip-permissions \"\$(cat {prompt})\""
|
||||
AGENT_CLAUDE_INTERACTIVE_CMD="claude --dangerously-skip-permissions {prompt}"
|
||||
AGENT_DROID_CMD="droid exec --skip-permissions-unsafe -f {prompt}"
|
||||
AGENT_DROID_INTERACTIVE_CMD="droid --skip-permissions-unsafe {prompt}"
|
||||
AGENT_OPENCODE_CMD="opencode run \"\$(cat {prompt})\""
|
||||
AGENT_OPENCODE_INTERACTIVE_CMD="opencode --prompt {prompt}"
|
||||
# Uncomment to use server mode (faster, avoids cold boot):
|
||||
# AGENT_OPENCODE_CMD="opencode run --attach http://localhost:4096 \"\$(cat {prompt})\""
|
||||
# AGENT_OPENCODE_INTERACTIVE_CMD="opencode --prompt {prompt} --attach http://localhost:4096"
|
||||
DEFAULT_AGENT="codex"
|
||||
@@ -0,0 +1,25 @@
|
||||
# Optional Ralph config overrides.
|
||||
# All paths are relative to repo root unless absolute.
|
||||
# Uncomment and edit as needed.
|
||||
|
||||
# PRD_PATH=".agents/tasks/prd.json"
|
||||
# PROGRESS_PATH=".ralph/progress.md"
|
||||
# GUARDRAILS_PATH=".ralph/guardrails.md"
|
||||
# ERRORS_LOG_PATH=".ralph/errors.log"
|
||||
# ACTIVITY_LOG_PATH=".ralph/activity.log"
|
||||
# TMP_DIR=".ralph/.tmp"
|
||||
# RUNS_DIR=".ralph/runs"
|
||||
# GUARDRAILS_REF=".agents/ralph/references/GUARDRAILS.md"
|
||||
# CONTEXT_REF=".agents/ralph/references/CONTEXT_ENGINEERING.md"
|
||||
# ACTIVITY_CMD=".agents/ralph/log-activity.sh"
|
||||
# AGENT_CMD defaults are defined in agents.sh. Override here if needed.
|
||||
# AGENT_CMD="codex exec --yolo --skip-git-repo-check -"
|
||||
# PRD_AGENT_CMD defaults are defined in agents.sh (interactive).
|
||||
# PRD_AGENT_CMD="codex --yolo --skip-git-repo-check {prompt}"
|
||||
# AGENT_CMD="claude -p --dangerously-skip-permissions \"\$(cat {prompt})\""
|
||||
# AGENT_CMD="droid exec --skip-permissions-unsafe -f {prompt}"
|
||||
# AGENTS_PATH="AGENTS.md"
|
||||
# PROMPT_BUILD=".agents/ralph/PROMPT_build.md"
|
||||
# NO_COMMIT=false
|
||||
# MAX_ITERATIONS=25
|
||||
# STALE_SECONDS=0
|
||||
@@ -0,0 +1,55 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 1000 520" role="img" aria-label="Ralph architecture diagram">
|
||||
<style>
|
||||
.bg { fill: #0f1218; }
|
||||
.box { fill: #141a23; stroke: #e6e6e6; stroke-width: 1.5; }
|
||||
.line { stroke: #e6e6e6; stroke-width: 1.5; }
|
||||
.text { fill: #f3f3f3; font-family: Menlo, Consolas, Monaco, monospace; font-size: 16px; }
|
||||
.small { font-size: 13px; opacity: 0.9; }
|
||||
.muted { opacity: 0.7; }
|
||||
</style>
|
||||
|
||||
<rect class="bg" x="0" y="0" width="1000" height="520" rx="0" />
|
||||
|
||||
<!-- Top boxes -->
|
||||
<rect class="box" x="390" y="30" width="220" height="44" rx="0" />
|
||||
<text class="text" x="500" y="58" text-anchor="middle">ralph CLI</text>
|
||||
|
||||
<rect class="box" x="240" y="114" width="520" height="48" rx="0" />
|
||||
<text class="text small muted" x="500" y="144" text-anchor="middle">agent CLI: codex | claude | droid</text>
|
||||
|
||||
<!-- Arrow from top to agent (gap matches lower arrows) -->
|
||||
<line class="line" x1="500" y1="74" x2="500" y2="96" />
|
||||
<polygon points="496,96 500,104 504,96" fill="#e6e6e6" />
|
||||
|
||||
<!-- Branch lines (gap for heads) -->
|
||||
<line class="line" x1="500" y1="162" x2="500" y2="200" />
|
||||
<line class="line" x1="500" y1="200" x2="280" y2="200" />
|
||||
<line class="line" x1="500" y1="200" x2="720" y2="200" />
|
||||
<line class="line" x1="280" y1="200" x2="280" y2="214" />
|
||||
<line class="line" x1="720" y1="200" x2="720" y2="214" />
|
||||
<polygon points="276,214 280,222 284,214" fill="#e6e6e6" />
|
||||
<polygon points="716,214 720,222 724,214" fill="#e6e6e6" />
|
||||
|
||||
<!-- Bottom boxes -->
|
||||
<rect class="box" x="90" y="226" width="380" height="230" rx="0" />
|
||||
<text class="text" x="110" y="256">.agents/ralph/</text>
|
||||
<text class="text small muted" x="110" y="278">local overrides (optional)</text>
|
||||
<text class="text small" x="110" y="306">loop.sh</text>
|
||||
<text class="text small" x="110" y="328">PROMPT_build.md</text>
|
||||
<text class="text small" x="110" y="350">references/</text>
|
||||
<text class="text small" x="110" y="372">log-activity.sh</text>
|
||||
<text class="text small" x="110" y="394">config.sh (optional)</text>
|
||||
|
||||
<rect class="box" x="530" y="226" width="380" height="230" rx="0" />
|
||||
<text class="text" x="550" y="256">.ralph/</text>
|
||||
<text class="text small muted" x="550" y="278">state + logs</text>
|
||||
<text class="text small" x="550" y="306">errors.log</text>
|
||||
<text class="text small" x="550" y="328">progress.md</text>
|
||||
<text class="text small" x="550" y="350">guardrails.md</text>
|
||||
<text class="text small" x="550" y="372">activity.log</text>
|
||||
<text class="text small" x="550" y="394">runs/</text>
|
||||
|
||||
<text class="text small muted" x="500" y="500" text-anchor="middle">
|
||||
Local templates override bundled defaults. State persists between runs.
|
||||
</text>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 2.7 KiB |
Executable
+15
@@ -0,0 +1,15 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
ROOT_DIR="$(cd "${SCRIPT_DIR}/../.." && pwd)"
|
||||
ACTIVITY_LOG="$ROOT_DIR/.ralph/activity.log"
|
||||
|
||||
if [ $# -lt 1 ]; then
|
||||
echo "Usage: $0 \"message\""
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p "$(dirname "$ACTIVITY_LOG")"
|
||||
TS=$(date '+%Y-%m-%d %H:%M:%S')
|
||||
echo "[$TS] $*" >> "$ACTIVITY_LOG"
|
||||
Executable
+955
@@ -0,0 +1,955 @@
|
||||
#!/bin/bash
|
||||
# Ralph loop — simple, portable, single-agent
|
||||
# Usage:
|
||||
# ./.agents/ralph/loop.sh # build mode, default iterations
|
||||
# ./.agents/ralph/loop.sh build # build mode
|
||||
# ./.agents/ralph/loop.sh prd "request" # generate PRD via agent
|
||||
# ./.agents/ralph/loop.sh 10 # build mode, 10 iterations
|
||||
# ./.agents/ralph/loop.sh build 1 --no-commit
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
ROOT_DIR="$(cd "${RALPH_ROOT:-${SCRIPT_DIR}/../..}" && pwd)"
|
||||
CONFIG_FILE="${SCRIPT_DIR}/config.sh"
|
||||
|
||||
DEFAULT_PRD_PATH=".agents/tasks/prd.json"
|
||||
DEFAULT_PROGRESS_PATH=".ralph/progress.md"
|
||||
DEFAULT_AGENTS_PATH="AGENTS.md"
|
||||
DEFAULT_PROMPT_BUILD=".agents/ralph/PROMPT_build.md"
|
||||
DEFAULT_GUARDRAILS_PATH=".ralph/guardrails.md"
|
||||
DEFAULT_ERRORS_LOG_PATH=".ralph/errors.log"
|
||||
DEFAULT_ACTIVITY_LOG_PATH=".ralph/activity.log"
|
||||
DEFAULT_TMP_DIR=".ralph/.tmp"
|
||||
DEFAULT_RUNS_DIR=".ralph/runs"
|
||||
DEFAULT_GUARDRAILS_REF=".agents/ralph/references/GUARDRAILS.md"
|
||||
DEFAULT_CONTEXT_REF=".agents/ralph/references/CONTEXT_ENGINEERING.md"
|
||||
DEFAULT_ACTIVITY_CMD=".agents/ralph/log-activity.sh"
|
||||
if [[ -n "${RALPH_ROOT:-}" ]]; then
|
||||
agents_path="$RALPH_ROOT/.agents/ralph/agents.sh"
|
||||
else
|
||||
agents_path="$SCRIPT_DIR/agents.sh"
|
||||
fi
|
||||
if [[ -f "$agents_path" ]]; then
|
||||
# shellcheck source=/dev/null
|
||||
source "$agents_path"
|
||||
fi
|
||||
|
||||
DEFAULT_MAX_ITERATIONS=25
|
||||
DEFAULT_NO_COMMIT=false
|
||||
DEFAULT_STALE_SECONDS=0
|
||||
PRD_REQUEST_PATH=""
|
||||
PRD_INLINE=""
|
||||
|
||||
# Optional config overrides (simple shell vars)
|
||||
if [ -f "$CONFIG_FILE" ]; then
|
||||
# shellcheck source=/dev/null
|
||||
. "$CONFIG_FILE"
|
||||
fi
|
||||
|
||||
DEFAULT_AGENT_NAME="${DEFAULT_AGENT:-codex}"
|
||||
resolve_agent_cmd() {
|
||||
local name="$1"
|
||||
case "$name" in
|
||||
claude)
|
||||
echo "${AGENT_CLAUDE_CMD:-claude -p --dangerously-skip-permissions \"\$(cat {prompt})\"}"
|
||||
;;
|
||||
droid)
|
||||
echo "${AGENT_DROID_CMD:-droid exec --skip-permissions-unsafe -f {prompt}}"
|
||||
;;
|
||||
codex|"")
|
||||
echo "${AGENT_CODEX_CMD:-codex exec --yolo --skip-git-repo-check -}"
|
||||
;;
|
||||
*)
|
||||
echo "${AGENT_CODEX_CMD:-codex exec --yolo --skip-git-repo-check -}"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
DEFAULT_AGENT_CMD="$(resolve_agent_cmd "$DEFAULT_AGENT_NAME")"
|
||||
|
||||
PRD_PATH="${PRD_PATH:-$DEFAULT_PRD_PATH}"
|
||||
PROGRESS_PATH="${PROGRESS_PATH:-$DEFAULT_PROGRESS_PATH}"
|
||||
AGENTS_PATH="${AGENTS_PATH:-$DEFAULT_AGENTS_PATH}"
|
||||
PROMPT_BUILD="${PROMPT_BUILD:-$DEFAULT_PROMPT_BUILD}"
|
||||
GUARDRAILS_PATH="${GUARDRAILS_PATH:-$DEFAULT_GUARDRAILS_PATH}"
|
||||
ERRORS_LOG_PATH="${ERRORS_LOG_PATH:-$DEFAULT_ERRORS_LOG_PATH}"
|
||||
ACTIVITY_LOG_PATH="${ACTIVITY_LOG_PATH:-$DEFAULT_ACTIVITY_LOG_PATH}"
|
||||
TMP_DIR="${TMP_DIR:-$DEFAULT_TMP_DIR}"
|
||||
RUNS_DIR="${RUNS_DIR:-$DEFAULT_RUNS_DIR}"
|
||||
GUARDRAILS_REF="${GUARDRAILS_REF:-$DEFAULT_GUARDRAILS_REF}"
|
||||
CONTEXT_REF="${CONTEXT_REF:-$DEFAULT_CONTEXT_REF}"
|
||||
ACTIVITY_CMD="${ACTIVITY_CMD:-$DEFAULT_ACTIVITY_CMD}"
|
||||
AGENT_CMD="${AGENT_CMD:-$DEFAULT_AGENT_CMD}"
|
||||
MAX_ITERATIONS="${MAX_ITERATIONS:-$DEFAULT_MAX_ITERATIONS}"
|
||||
NO_COMMIT="${NO_COMMIT:-$DEFAULT_NO_COMMIT}"
|
||||
STALE_SECONDS="${STALE_SECONDS:-$DEFAULT_STALE_SECONDS}"
|
||||
|
||||
abs_path() {
|
||||
local p="$1"
|
||||
if [[ "$p" = /* ]]; then
|
||||
echo "$p"
|
||||
else
|
||||
echo "$ROOT_DIR/$p"
|
||||
fi
|
||||
}
|
||||
|
||||
PRD_PATH="$(abs_path "$PRD_PATH")"
|
||||
PROGRESS_PATH="$(abs_path "$PROGRESS_PATH")"
|
||||
AGENTS_PATH="$(abs_path "$AGENTS_PATH")"
|
||||
PROMPT_BUILD="$(abs_path "$PROMPT_BUILD")"
|
||||
GUARDRAILS_PATH="$(abs_path "$GUARDRAILS_PATH")"
|
||||
ERRORS_LOG_PATH="$(abs_path "$ERRORS_LOG_PATH")"
|
||||
ACTIVITY_LOG_PATH="$(abs_path "$ACTIVITY_LOG_PATH")"
|
||||
TMP_DIR="$(abs_path "$TMP_DIR")"
|
||||
RUNS_DIR="$(abs_path "$RUNS_DIR")"
|
||||
GUARDRAILS_REF="$(abs_path "$GUARDRAILS_REF")"
|
||||
CONTEXT_REF="$(abs_path "$CONTEXT_REF")"
|
||||
ACTIVITY_CMD="$(abs_path "$ACTIVITY_CMD")"
|
||||
|
||||
require_agent() {
|
||||
local agent_cmd="${1:-$AGENT_CMD}"
|
||||
local agent_bin
|
||||
agent_bin="${agent_cmd%% *}"
|
||||
if [ -z "$agent_bin" ]; then
|
||||
echo "AGENT_CMD is empty. Set it in config.sh."
|
||||
exit 1
|
||||
fi
|
||||
if ! command -v "$agent_bin" >/dev/null 2>&1; then
|
||||
echo "Agent command not found: $agent_bin"
|
||||
case "$agent_bin" in
|
||||
codex)
|
||||
echo "Install: npm i -g @openai/codex"
|
||||
;;
|
||||
claude)
|
||||
echo "Install: curl -fsSL https://claude.ai/install.sh | bash"
|
||||
;;
|
||||
droid)
|
||||
echo "Install: curl -fsSL https://app.factory.ai/cli | sh"
|
||||
;;
|
||||
opencode)
|
||||
echo "Install: curl -fsSL https://opencode.ai/install.sh | bash"
|
||||
;;
|
||||
esac
|
||||
echo "Then authenticate per the CLI's instructions."
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
run_agent() {
|
||||
local prompt_file="$1"
|
||||
if [[ "$AGENT_CMD" == *"{prompt}"* ]]; then
|
||||
local escaped
|
||||
escaped=$(printf '%q' "$prompt_file")
|
||||
local cmd="${AGENT_CMD//\{prompt\}/$escaped}"
|
||||
eval "$cmd"
|
||||
else
|
||||
cat "$prompt_file" | eval "$AGENT_CMD"
|
||||
fi
|
||||
}
|
||||
|
||||
run_agent_inline() {
|
||||
local prompt_file="$1"
|
||||
local prompt_content
|
||||
prompt_content="$(cat "$prompt_file")"
|
||||
local escaped
|
||||
escaped=$(printf "%s" "$prompt_content" | sed "s/'/'\\\\''/g")
|
||||
local cmd="${PRD_AGENT_CMD:-$AGENT_CMD}"
|
||||
if [[ "$cmd" == *"{prompt}"* ]]; then
|
||||
cmd="${cmd//\{prompt\}/'$escaped'}"
|
||||
else
|
||||
cmd="$cmd '$escaped'"
|
||||
fi
|
||||
eval "$cmd"
|
||||
}
|
||||
|
||||
MODE="build"
|
||||
while [ $# -gt 0 ]; do
|
||||
case "$1" in
|
||||
build|prd)
|
||||
MODE="$1"
|
||||
shift
|
||||
;;
|
||||
--prompt)
|
||||
PRD_REQUEST_PATH="$2"
|
||||
shift 2
|
||||
;;
|
||||
--no-commit)
|
||||
NO_COMMIT=true
|
||||
shift
|
||||
;;
|
||||
*)
|
||||
if [ "$MODE" = "prd" ]; then
|
||||
PRD_INLINE="${PRD_INLINE:+$PRD_INLINE }$1"
|
||||
shift
|
||||
elif [[ "$1" =~ ^[0-9]+$ ]]; then
|
||||
MAX_ITERATIONS="$1"
|
||||
shift
|
||||
else
|
||||
echo "Unknown arg: $1"
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
PROMPT_FILE="$PROMPT_BUILD"
|
||||
|
||||
if [ "$MODE" = "prd" ]; then
|
||||
PRD_USE_INLINE=1
|
||||
if [ -z "${PRD_AGENT_CMD:-}" ]; then
|
||||
PRD_AGENT_CMD="$AGENT_CMD"
|
||||
PRD_USE_INLINE=0
|
||||
fi
|
||||
if [ "${RALPH_DRY_RUN:-}" != "1" ]; then
|
||||
require_agent "${PRD_AGENT_CMD:-$AGENT_CMD}"
|
||||
fi
|
||||
|
||||
if [[ "$PRD_PATH" == *.json ]]; then
|
||||
mkdir -p "$(dirname "$PRD_PATH")" "$TMP_DIR"
|
||||
else
|
||||
mkdir -p "$PRD_PATH" "$TMP_DIR"
|
||||
fi
|
||||
|
||||
if [ -z "$PRD_REQUEST_PATH" ] && [ -n "$PRD_INLINE" ]; then
|
||||
PRD_REQUEST_PATH="$TMP_DIR/prd-request-$(date +%Y%m%d-%H%M%S)-$$.txt"
|
||||
printf '%s\n' "$PRD_INLINE" > "$PRD_REQUEST_PATH"
|
||||
fi
|
||||
|
||||
if [ -z "$PRD_REQUEST_PATH" ] || [ ! -f "$PRD_REQUEST_PATH" ]; then
|
||||
echo "PRD request missing. Provide a prompt string or --prompt <file>."
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "${RALPH_DRY_RUN:-}" = "1" ]; then
|
||||
if [[ "$PRD_PATH" == *.json ]]; then
|
||||
if [ ! -f "$PRD_PATH" ]; then
|
||||
{
|
||||
echo '{'
|
||||
echo ' "version": 1,'
|
||||
echo ' "project": "ralph",'
|
||||
echo ' "qualityGates": [],'
|
||||
echo ' "stories": []'
|
||||
echo '}'
|
||||
} > "$PRD_PATH"
|
||||
fi
|
||||
fi
|
||||
exit 0
|
||||
fi
|
||||
|
||||
PRD_PROMPT_FILE="$TMP_DIR/prd-prompt-$(date +%Y%m%d-%H%M%S)-$$.md"
|
||||
{
|
||||
echo "You are an autonomous coding agent."
|
||||
echo "Use the \$prd skill to create a Product Requirements Document in JSON."
|
||||
if [[ "$PRD_PATH" == *.json ]]; then
|
||||
echo "Save the PRD to: $PRD_PATH"
|
||||
else
|
||||
echo "Save the PRD as JSON in directory: $PRD_PATH"
|
||||
echo "Filename rules: prd-<short-slug>.json using 1-3 meaningful words."
|
||||
echo "Examples: prd-workout-tracker.json, prd-usage-billing.json"
|
||||
fi
|
||||
echo "Do NOT implement anything."
|
||||
echo "After creating the PRD, end with:"
|
||||
echo "PRD JSON saved to <path>. Close this chat and run \`ralph build\`."
|
||||
echo ""
|
||||
echo "User request:"
|
||||
cat "$PRD_REQUEST_PATH"
|
||||
} > "$PRD_PROMPT_FILE"
|
||||
|
||||
if [ "$PRD_USE_INLINE" -eq 1 ]; then
|
||||
run_agent_inline "$PRD_PROMPT_FILE"
|
||||
else
|
||||
run_agent "$PRD_PROMPT_FILE"
|
||||
fi
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [ "${RALPH_DRY_RUN:-}" != "1" ]; then
|
||||
require_agent
|
||||
fi
|
||||
|
||||
if [ ! -f "$PROMPT_FILE" ]; then
|
||||
echo "Prompt not found: $PROMPT_FILE"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
if [ "$MODE" != "prd" ] && [ ! -f "$PRD_PATH" ]; then
|
||||
echo "PRD not found: $PRD_PATH"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
mkdir -p "$(dirname "$PROGRESS_PATH")" "$TMP_DIR" "$RUNS_DIR"
|
||||
|
||||
if [ ! -f "$PROGRESS_PATH" ]; then
|
||||
{
|
||||
echo "# Progress Log"
|
||||
echo "Started: $(date)"
|
||||
echo ""
|
||||
echo "## Codebase Patterns"
|
||||
echo "- (add reusable patterns here)"
|
||||
echo ""
|
||||
echo "---"
|
||||
} > "$PROGRESS_PATH"
|
||||
fi
|
||||
|
||||
if [ ! -f "$GUARDRAILS_PATH" ]; then
|
||||
{
|
||||
echo "# Guardrails (Signs)"
|
||||
echo ""
|
||||
echo "> Lessons learned from failures. Read before acting."
|
||||
echo ""
|
||||
echo "## Core Signs"
|
||||
echo ""
|
||||
echo "### Sign: Read Before Writing"
|
||||
echo "- **Trigger**: Before modifying any file"
|
||||
echo "- **Instruction**: Read the file first"
|
||||
echo "- **Added after**: Core principle"
|
||||
echo ""
|
||||
echo "### Sign: Test Before Commit"
|
||||
echo "- **Trigger**: Before committing changes"
|
||||
echo "- **Instruction**: Run required tests and verify outputs"
|
||||
echo "- **Added after**: Core principle"
|
||||
echo ""
|
||||
echo "---"
|
||||
echo ""
|
||||
echo "## Learned Signs"
|
||||
echo ""
|
||||
} > "$GUARDRAILS_PATH"
|
||||
fi
|
||||
|
||||
if [ ! -f "$ERRORS_LOG_PATH" ]; then
|
||||
{
|
||||
echo "# Error Log"
|
||||
echo ""
|
||||
echo "> Failures and repeated issues. Use this to add guardrails."
|
||||
echo ""
|
||||
} > "$ERRORS_LOG_PATH"
|
||||
fi
|
||||
|
||||
if [ ! -f "$ACTIVITY_LOG_PATH" ]; then
|
||||
{
|
||||
echo "# Activity Log"
|
||||
echo ""
|
||||
echo "## Run Summary"
|
||||
echo ""
|
||||
echo "## Events"
|
||||
echo ""
|
||||
} > "$ACTIVITY_LOG_PATH"
|
||||
fi
|
||||
|
||||
RUN_TAG="$(date +%Y%m%d-%H%M%S)-$$"
|
||||
|
||||
render_prompt() {
|
||||
local src="$1"
|
||||
local dst="$2"
|
||||
local story_meta="$3"
|
||||
local story_block="$4"
|
||||
local run_id="$5"
|
||||
local iter="$6"
|
||||
local run_log="$7"
|
||||
local run_meta="$8"
|
||||
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'
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
src = Path(sys.argv[1]).read_text()
|
||||
prd, agents, progress, root = sys.argv[3:7]
|
||||
guardrails = sys.argv[7]
|
||||
errors_log = sys.argv[8]
|
||||
activity_log = sys.argv[9]
|
||||
guardrails_ref = sys.argv[10]
|
||||
context_ref = sys.argv[11]
|
||||
activity_cmd = sys.argv[12]
|
||||
no_commit = sys.argv[13]
|
||||
meta_path = sys.argv[14] if len(sys.argv) > 14 else ""
|
||||
block_path = sys.argv[15] if len(sys.argv) > 15 else ""
|
||||
run_id = sys.argv[16] if len(sys.argv) > 16 else ""
|
||||
iteration = sys.argv[17] if len(sys.argv) > 17 else ""
|
||||
run_log = sys.argv[18] if len(sys.argv) > 18 else ""
|
||||
run_meta = sys.argv[19] if len(sys.argv) > 19 else ""
|
||||
repl = {
|
||||
"PRD_PATH": prd,
|
||||
"AGENTS_PATH": agents,
|
||||
"PROGRESS_PATH": progress,
|
||||
"REPO_ROOT": root,
|
||||
"GUARDRAILS_PATH": guardrails,
|
||||
"ERRORS_LOG_PATH": errors_log,
|
||||
"ACTIVITY_LOG_PATH": activity_log,
|
||||
"GUARDRAILS_REF": guardrails_ref,
|
||||
"CONTEXT_REF": context_ref,
|
||||
"ACTIVITY_CMD": activity_cmd,
|
||||
"NO_COMMIT": no_commit,
|
||||
"RUN_ID": run_id,
|
||||
"ITERATION": iteration,
|
||||
"RUN_LOG_PATH": run_log,
|
||||
"RUN_META_PATH": run_meta,
|
||||
}
|
||||
story = {"id": "", "title": "", "block": ""}
|
||||
quality_gates = []
|
||||
if meta_path:
|
||||
try:
|
||||
import json
|
||||
meta = json.loads(Path(meta_path).read_text())
|
||||
story["id"] = meta.get("id", "") or ""
|
||||
story["title"] = meta.get("title", "") or ""
|
||||
quality_gates = meta.get("quality_gates", []) or []
|
||||
except Exception:
|
||||
pass
|
||||
if block_path and Path(block_path).exists():
|
||||
story["block"] = Path(block_path).read_text()
|
||||
repl["STORY_ID"] = story["id"]
|
||||
repl["STORY_TITLE"] = story["title"]
|
||||
repl["STORY_BLOCK"] = story["block"]
|
||||
if quality_gates:
|
||||
repl["QUALITY_GATES"] = "\n".join([f"- {g}" for g in quality_gates])
|
||||
else:
|
||||
repl["QUALITY_GATES"] = "- (none)"
|
||||
for k, v in repl.items():
|
||||
src = src.replace("{{" + k + "}}", v)
|
||||
Path(sys.argv[2]).write_text(src)
|
||||
PY
|
||||
}
|
||||
|
||||
select_story() {
|
||||
local meta_out="$1"
|
||||
local block_out="$2"
|
||||
python3 - "$PRD_PATH" "$meta_out" "$block_out" "$STALE_SECONDS" <<'PY'
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from datetime import datetime, timezone
|
||||
try:
|
||||
import fcntl
|
||||
except Exception:
|
||||
fcntl = None
|
||||
|
||||
prd_path = Path(sys.argv[1])
|
||||
meta_out = Path(sys.argv[2])
|
||||
block_out = Path(sys.argv[3])
|
||||
stale_seconds = 0
|
||||
if len(sys.argv) > 4:
|
||||
try:
|
||||
stale_seconds = int(sys.argv[4])
|
||||
except Exception:
|
||||
stale_seconds = 0
|
||||
|
||||
if not prd_path.exists():
|
||||
meta_out.write_text(json.dumps({"ok": False, "error": "PRD not found"}, indent=2) + "\n")
|
||||
block_out.write_text("")
|
||||
sys.exit(0)
|
||||
|
||||
def normalize_status(value):
|
||||
if value is None:
|
||||
return "open"
|
||||
return str(value).strip().lower()
|
||||
|
||||
def parse_ts(value):
|
||||
if not value:
|
||||
return None
|
||||
text = str(value).strip()
|
||||
if text.endswith("Z"):
|
||||
text = text[:-1] + "+00:00"
|
||||
try:
|
||||
return datetime.fromisoformat(text)
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
def now_iso():
|
||||
return datetime.now(timezone.utc).isoformat()
|
||||
|
||||
with prd_path.open("r+", encoding="utf-8") as fh:
|
||||
if fcntl is not None:
|
||||
fcntl.flock(fh.fileno(), fcntl.LOCK_EX)
|
||||
try:
|
||||
try:
|
||||
data = json.load(fh)
|
||||
except Exception as exc:
|
||||
meta_out.write_text(json.dumps({"ok": False, "error": f"Invalid PRD JSON: {exc}"}, indent=2) + "\n")
|
||||
block_out.write_text("")
|
||||
sys.exit(0)
|
||||
|
||||
stories = data.get("stories") if isinstance(data, dict) else None
|
||||
if not isinstance(stories, list) or not stories:
|
||||
meta_out.write_text(json.dumps({"ok": False, "error": "No stories found in PRD"}, indent=2) + "\n")
|
||||
block_out.write_text("")
|
||||
sys.exit(0)
|
||||
|
||||
story_index = {s.get("id"): s for s in stories if isinstance(s, dict)}
|
||||
|
||||
def is_done(story_id: str) -> bool:
|
||||
target = story_index.get(story_id)
|
||||
if not isinstance(target, dict):
|
||||
return False
|
||||
return normalize_status(target.get("status")) == "done"
|
||||
|
||||
if stale_seconds > 0:
|
||||
now = datetime.now(timezone.utc)
|
||||
for story in stories:
|
||||
if not isinstance(story, dict):
|
||||
continue
|
||||
if normalize_status(story.get("status")) != "in_progress":
|
||||
continue
|
||||
started = parse_ts(story.get("startedAt"))
|
||||
if started is None or (now - started).total_seconds() > stale_seconds:
|
||||
story["status"] = "open"
|
||||
story["startedAt"] = None
|
||||
story["completedAt"] = None
|
||||
story["updatedAt"] = now_iso()
|
||||
|
||||
candidate = None
|
||||
for story in stories:
|
||||
if not isinstance(story, dict):
|
||||
continue
|
||||
if normalize_status(story.get("status")) != "open":
|
||||
continue
|
||||
deps = story.get("dependsOn") or []
|
||||
if not isinstance(deps, list):
|
||||
deps = []
|
||||
if all(is_done(dep) for dep in deps):
|
||||
candidate = story
|
||||
break
|
||||
|
||||
remaining = sum(
|
||||
1 for story in stories
|
||||
if isinstance(story, dict) and normalize_status(story.get("status")) != "done"
|
||||
)
|
||||
|
||||
meta = {
|
||||
"ok": True,
|
||||
"total": len(stories),
|
||||
"remaining": remaining,
|
||||
"quality_gates": data.get("qualityGates", []) or [],
|
||||
}
|
||||
|
||||
if candidate:
|
||||
candidate["status"] = "in_progress"
|
||||
if not candidate.get("startedAt"):
|
||||
candidate["startedAt"] = now_iso()
|
||||
candidate["completedAt"] = None
|
||||
candidate["updatedAt"] = now_iso()
|
||||
meta.update({
|
||||
"id": candidate.get("id", ""),
|
||||
"title": candidate.get("title", ""),
|
||||
})
|
||||
|
||||
depends = candidate.get("dependsOn") or []
|
||||
if not isinstance(depends, list):
|
||||
depends = []
|
||||
acceptance = candidate.get("acceptanceCriteria") or []
|
||||
if not isinstance(acceptance, list):
|
||||
acceptance = []
|
||||
|
||||
description = candidate.get("description") or ""
|
||||
block_lines = []
|
||||
block_lines.append(f"### {candidate.get('id', '')}: {candidate.get('title', '')}")
|
||||
block_lines.append(f"Status: {candidate.get('status', 'open')}")
|
||||
block_lines.append(
|
||||
f"Depends on: {', '.join(depends) if depends else 'None'}"
|
||||
)
|
||||
block_lines.append("")
|
||||
block_lines.append("Description:")
|
||||
block_lines.append(description if description else "(none)")
|
||||
block_lines.append("")
|
||||
block_lines.append("Acceptance Criteria:")
|
||||
if acceptance:
|
||||
block_lines.extend([f"- [ ] {item}" for item in acceptance])
|
||||
else:
|
||||
block_lines.append("- (none)")
|
||||
block_out.write_text("\n".join(block_lines).rstrip() + "\n")
|
||||
else:
|
||||
block_out.write_text("")
|
||||
|
||||
fh.seek(0)
|
||||
fh.truncate()
|
||||
json.dump(data, fh, indent=2)
|
||||
fh.write("\n")
|
||||
fh.flush()
|
||||
os.fsync(fh.fileno())
|
||||
finally:
|
||||
if fcntl is not None:
|
||||
fcntl.flock(fh.fileno(), fcntl.LOCK_UN)
|
||||
|
||||
meta_out.write_text(json.dumps(meta, indent=2) + "\n")
|
||||
PY
|
||||
}
|
||||
|
||||
remaining_stories() {
|
||||
local meta_file="$1"
|
||||
python3 - "$meta_file" <<'PY'
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
data = json.loads(Path(sys.argv[1]).read_text())
|
||||
print(data.get("remaining", "unknown"))
|
||||
PY
|
||||
}
|
||||
|
||||
remaining_from_prd() {
|
||||
python3 - "$PRD_PATH" <<'PY'
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
prd_path = Path(sys.argv[1])
|
||||
if not prd_path.exists():
|
||||
print("unknown")
|
||||
sys.exit(0)
|
||||
|
||||
try:
|
||||
data = json.loads(prd_path.read_text())
|
||||
except Exception:
|
||||
print("unknown")
|
||||
sys.exit(0)
|
||||
|
||||
stories = data.get("stories") if isinstance(data, dict) else None
|
||||
if not isinstance(stories, list):
|
||||
print("unknown")
|
||||
sys.exit(0)
|
||||
|
||||
def normalize_status(value):
|
||||
if value is None:
|
||||
return "open"
|
||||
return str(value).strip().lower()
|
||||
|
||||
remaining = sum(
|
||||
1 for story in stories
|
||||
if isinstance(story, dict) and normalize_status(story.get("status")) != "done"
|
||||
)
|
||||
print(remaining)
|
||||
PY
|
||||
}
|
||||
|
||||
story_field() {
|
||||
local meta_file="$1"
|
||||
local field="$2"
|
||||
python3 - "$meta_file" "$field" <<'PY'
|
||||
import json
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
data = json.loads(Path(sys.argv[1]).read_text())
|
||||
field = sys.argv[2]
|
||||
print(data.get(field, ""))
|
||||
PY
|
||||
}
|
||||
|
||||
update_story_status() {
|
||||
local story_id="$1"
|
||||
local new_status="$2"
|
||||
python3 - "$PRD_PATH" "$story_id" "$new_status" <<'PY'
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from datetime import datetime, timezone
|
||||
try:
|
||||
import fcntl
|
||||
except Exception:
|
||||
fcntl = None
|
||||
|
||||
prd_path = Path(sys.argv[1])
|
||||
story_id = sys.argv[2]
|
||||
new_status = sys.argv[3]
|
||||
|
||||
if not story_id:
|
||||
sys.exit(0)
|
||||
|
||||
if not prd_path.exists():
|
||||
sys.exit(0)
|
||||
|
||||
def now_iso():
|
||||
return datetime.now(timezone.utc).isoformat()
|
||||
|
||||
with prd_path.open("r+", encoding="utf-8") as fh:
|
||||
if fcntl is not None:
|
||||
fcntl.flock(fh.fileno(), fcntl.LOCK_EX)
|
||||
try:
|
||||
data = json.load(fh)
|
||||
stories = data.get("stories") if isinstance(data, dict) else None
|
||||
if not isinstance(stories, list):
|
||||
sys.exit(0)
|
||||
for story in stories:
|
||||
if isinstance(story, dict) and story.get("id") == story_id:
|
||||
story["status"] = new_status
|
||||
story["updatedAt"] = now_iso()
|
||||
if new_status == "in_progress":
|
||||
if not story.get("startedAt"):
|
||||
story["startedAt"] = now_iso()
|
||||
story["completedAt"] = None
|
||||
elif new_status == "done":
|
||||
story["completedAt"] = now_iso()
|
||||
if not story.get("startedAt"):
|
||||
story["startedAt"] = now_iso()
|
||||
elif new_status == "open":
|
||||
story["startedAt"] = None
|
||||
story["completedAt"] = None
|
||||
break
|
||||
fh.seek(0)
|
||||
fh.truncate()
|
||||
json.dump(data, fh, indent=2)
|
||||
fh.write("\n")
|
||||
fh.flush()
|
||||
os.fsync(fh.fileno())
|
||||
finally:
|
||||
if fcntl is not None:
|
||||
fcntl.flock(fh.fileno(), fcntl.LOCK_UN)
|
||||
PY
|
||||
}
|
||||
|
||||
log_activity() {
|
||||
local message="$1"
|
||||
local timestamp
|
||||
timestamp=$(date '+%Y-%m-%d %H:%M:%S')
|
||||
echo "[$timestamp] $message" >> "$ACTIVITY_LOG_PATH"
|
||||
}
|
||||
|
||||
log_error() {
|
||||
local message="$1"
|
||||
local timestamp
|
||||
timestamp=$(date '+%Y-%m-%d %H:%M:%S')
|
||||
echo "[$timestamp] $message" >> "$ERRORS_LOG_PATH"
|
||||
}
|
||||
|
||||
append_run_summary() {
|
||||
local line="$1"
|
||||
python3 - "$ACTIVITY_LOG_PATH" "$line" <<'PY'
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
path = Path(sys.argv[1])
|
||||
line = sys.argv[2]
|
||||
text = path.read_text().splitlines()
|
||||
out = []
|
||||
inserted = False
|
||||
for l in text:
|
||||
out.append(l)
|
||||
if not inserted and l.strip() == "## Run Summary":
|
||||
out.append(f"- {line}")
|
||||
inserted = True
|
||||
if not inserted:
|
||||
out = [
|
||||
"# Activity Log",
|
||||
"",
|
||||
"## Run Summary",
|
||||
f"- {line}",
|
||||
"",
|
||||
"## Events",
|
||||
"",
|
||||
] + text
|
||||
Path(path).write_text("\n".join(out).rstrip() + "\n")
|
||||
PY
|
||||
}
|
||||
|
||||
write_run_meta() {
|
||||
local path="$1"
|
||||
local mode="$2"
|
||||
local iter="$3"
|
||||
local run_id="$4"
|
||||
local story_id="$5"
|
||||
local story_title="$6"
|
||||
local started="$7"
|
||||
local ended="$8"
|
||||
local duration="$9"
|
||||
local status="${10}"
|
||||
local log_file="${11}"
|
||||
local head_before="${12}"
|
||||
local head_after="${13}"
|
||||
local commit_list="${14}"
|
||||
local changed_files="${15}"
|
||||
local dirty_files="${16}"
|
||||
{
|
||||
echo "# Ralph Run Summary"
|
||||
echo ""
|
||||
echo "- Run ID: $run_id"
|
||||
echo "- Iteration: $iter"
|
||||
echo "- Mode: $mode"
|
||||
if [ -n "$story_id" ]; then
|
||||
echo "- Story: $story_id: $story_title"
|
||||
fi
|
||||
echo "- Started: $started"
|
||||
echo "- Ended: $ended"
|
||||
echo "- Duration: ${duration}s"
|
||||
echo "- Status: $status"
|
||||
echo "- Log: $log_file"
|
||||
echo ""
|
||||
echo "## Git"
|
||||
echo "- Head (before): ${head_before:-unknown}"
|
||||
echo "- Head (after): ${head_after:-unknown}"
|
||||
echo ""
|
||||
echo "### Commits"
|
||||
if [ -n "$commit_list" ]; then
|
||||
echo "$commit_list"
|
||||
else
|
||||
echo "- (none)"
|
||||
fi
|
||||
echo ""
|
||||
echo "### Changed Files (commits)"
|
||||
if [ -n "$changed_files" ]; then
|
||||
echo "$changed_files"
|
||||
else
|
||||
echo "- (none)"
|
||||
fi
|
||||
echo ""
|
||||
echo "### Uncommitted Changes"
|
||||
if [ -n "$dirty_files" ]; then
|
||||
echo "$dirty_files"
|
||||
else
|
||||
echo "- (clean)"
|
||||
fi
|
||||
echo ""
|
||||
} > "$path"
|
||||
}
|
||||
|
||||
git_head() {
|
||||
if git -C "$ROOT_DIR" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
||||
git -C "$ROOT_DIR" rev-parse HEAD 2>/dev/null || true
|
||||
else
|
||||
echo ""
|
||||
fi
|
||||
}
|
||||
|
||||
git_commit_list() {
|
||||
local before="$1"
|
||||
local after="$2"
|
||||
if [ -n "$before" ] && [ -n "$after" ] && [ "$before" != "$after" ]; then
|
||||
git -C "$ROOT_DIR" log --oneline "$before..$after" | sed 's/^/- /'
|
||||
else
|
||||
echo ""
|
||||
fi
|
||||
}
|
||||
|
||||
git_changed_files() {
|
||||
local before="$1"
|
||||
local after="$2"
|
||||
if [ -n "$before" ] && [ -n "$after" ] && [ "$before" != "$after" ]; then
|
||||
git -C "$ROOT_DIR" diff --name-only "$before" "$after" | sed 's/^/- /'
|
||||
else
|
||||
echo ""
|
||||
fi
|
||||
}
|
||||
|
||||
git_dirty_files() {
|
||||
if git -C "$ROOT_DIR" rev-parse --is-inside-work-tree >/dev/null 2>&1; then
|
||||
git -C "$ROOT_DIR" status --porcelain | awk '{print "- " $2}'
|
||||
else
|
||||
echo ""
|
||||
fi
|
||||
}
|
||||
|
||||
echo "Ralph mode: $MODE"
|
||||
echo "Max iterations: $MAX_ITERATIONS"
|
||||
echo "PRD: $PRD_PATH"
|
||||
HAS_ERROR="false"
|
||||
|
||||
for i in $(seq 1 "$MAX_ITERATIONS"); do
|
||||
echo ""
|
||||
echo "═══════════════════════════════════════════════════════"
|
||||
echo " Ralph Iteration $i of $MAX_ITERATIONS"
|
||||
echo "═══════════════════════════════════════════════════════"
|
||||
|
||||
STORY_META=""
|
||||
STORY_BLOCK=""
|
||||
ITER_START=$(date +%s)
|
||||
ITER_START_FMT=$(date '+%Y-%m-%d %H:%M:%S')
|
||||
if [ "$MODE" = "build" ]; then
|
||||
STORY_META="$TMP_DIR/story-$RUN_TAG-$i.json"
|
||||
STORY_BLOCK="$TMP_DIR/story-$RUN_TAG-$i.md"
|
||||
select_story "$STORY_META" "$STORY_BLOCK"
|
||||
REMAINING="$(remaining_stories "$STORY_META")"
|
||||
if [ "$REMAINING" = "unknown" ]; then
|
||||
echo "Could not parse stories from PRD: $PRD_PATH"
|
||||
exit 1
|
||||
fi
|
||||
if [ "$REMAINING" = "0" ]; then
|
||||
echo "No remaining stories."
|
||||
exit 0
|
||||
fi
|
||||
STORY_ID="$(story_field "$STORY_META" "id")"
|
||||
STORY_TITLE="$(story_field "$STORY_META" "title")"
|
||||
if [ -z "$STORY_ID" ]; then
|
||||
echo "No actionable open stories (all blocked or in progress). Remaining: $REMAINING"
|
||||
exit 0
|
||||
fi
|
||||
fi
|
||||
|
||||
HEAD_BEFORE="$(git_head)"
|
||||
PROMPT_RENDERED="$TMP_DIR/prompt-$RUN_TAG-$i.md"
|
||||
LOG_FILE="$RUNS_DIR/run-$RUN_TAG-iter-$i.log"
|
||||
RUN_META="$RUNS_DIR/run-$RUN_TAG-iter-$i.md"
|
||||
render_prompt "$PROMPT_FILE" "$PROMPT_RENDERED" "$STORY_META" "$STORY_BLOCK" "$RUN_TAG" "$i" "$LOG_FILE" "$RUN_META"
|
||||
|
||||
if [ "$MODE" = "build" ] && [ -n "${STORY_ID:-}" ]; then
|
||||
log_activity "ITERATION $i start (mode=$MODE story=$STORY_ID)"
|
||||
else
|
||||
log_activity "ITERATION $i start (mode=$MODE)"
|
||||
fi
|
||||
set +e
|
||||
if [ "${RALPH_DRY_RUN:-}" = "1" ]; then
|
||||
echo "[RALPH_DRY_RUN] Skipping agent execution." | tee "$LOG_FILE"
|
||||
CMD_STATUS=0
|
||||
else
|
||||
run_agent "$PROMPT_RENDERED" 2>&1 | tee "$LOG_FILE"
|
||||
CMD_STATUS=$?
|
||||
fi
|
||||
set -e
|
||||
if [ "$CMD_STATUS" -eq 130 ] || [ "$CMD_STATUS" -eq 143 ]; then
|
||||
echo "Interrupted."
|
||||
exit "$CMD_STATUS"
|
||||
fi
|
||||
ITER_END=$(date +%s)
|
||||
ITER_END_FMT=$(date '+%Y-%m-%d %H:%M:%S')
|
||||
ITER_DURATION=$((ITER_END - ITER_START))
|
||||
HEAD_AFTER="$(git_head)"
|
||||
log_activity "ITERATION $i end (duration=${ITER_DURATION}s)"
|
||||
if [ "$CMD_STATUS" -ne 0 ]; then
|
||||
log_error "ITERATION $i command failed (status=$CMD_STATUS)"
|
||||
HAS_ERROR="true"
|
||||
fi
|
||||
COMMIT_LIST="$(git_commit_list "$HEAD_BEFORE" "$HEAD_AFTER")"
|
||||
CHANGED_FILES="$(git_changed_files "$HEAD_BEFORE" "$HEAD_AFTER")"
|
||||
DIRTY_FILES="$(git_dirty_files)"
|
||||
STATUS_LABEL="success"
|
||||
if [ "$CMD_STATUS" -ne 0 ]; then
|
||||
STATUS_LABEL="error"
|
||||
fi
|
||||
if [ "$MODE" = "build" ] && [ "$NO_COMMIT" = "false" ] && [ -n "$DIRTY_FILES" ]; then
|
||||
log_error "ITERATION $i left uncommitted changes; review run summary at $RUN_META"
|
||||
fi
|
||||
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"
|
||||
if [ "$MODE" = "build" ] && [ -n "${STORY_ID:-}" ]; then
|
||||
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"
|
||||
else
|
||||
append_run_summary "$(date '+%Y-%m-%d %H:%M:%S') | run=$RUN_TAG | iter=$i | mode=$MODE | duration=${ITER_DURATION}s | status=$STATUS_LABEL"
|
||||
fi
|
||||
|
||||
if [ "$MODE" = "build" ]; then
|
||||
if [ "$CMD_STATUS" -ne 0 ]; then
|
||||
log_error "ITERATION $i exited non-zero; review $LOG_FILE"
|
||||
update_story_status "$STORY_ID" "open"
|
||||
echo "Iteration failed; story reset to open."
|
||||
elif grep -q "<promise>COMPLETE</promise>" "$LOG_FILE"; then
|
||||
update_story_status "$STORY_ID" "done"
|
||||
echo "Completion signal received; story marked done."
|
||||
else
|
||||
update_story_status "$STORY_ID" "open"
|
||||
echo "No completion signal; story reset to open."
|
||||
fi
|
||||
REMAINING="$(remaining_from_prd)"
|
||||
echo "Iteration $i complete. Remaining stories: $REMAINING"
|
||||
if [ "$REMAINING" = "0" ]; then
|
||||
echo "No remaining stories."
|
||||
exit 0
|
||||
fi
|
||||
else
|
||||
echo "Iteration $i complete."
|
||||
fi
|
||||
sleep 2
|
||||
|
||||
done
|
||||
|
||||
echo "Reached max iterations ($MAX_ITERATIONS)."
|
||||
if [ "$HAS_ERROR" = "true" ]; then
|
||||
exit 1
|
||||
fi
|
||||
exit 0
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 269 KiB |
@@ -0,0 +1,126 @@
|
||||
# Context Engineering Reference
|
||||
|
||||
This document explains the malloc/free metaphor for LLM context management that underlies the Ralph technique.
|
||||
|
||||
## The malloc() Metaphor
|
||||
|
||||
In traditional programming:
|
||||
- `malloc()` allocates memory
|
||||
- `free()` releases memory
|
||||
- Memory leaks occur when you allocate without freeing
|
||||
|
||||
In LLM context:
|
||||
- Reading files, receiving responses, tool outputs = `malloc()`
|
||||
- **There is no `free()`** - context cannot be released
|
||||
- The only way to "free" is to start a new conversation
|
||||
|
||||
## Why This Matters
|
||||
|
||||
### Context Pollution
|
||||
|
||||
When you work on multiple unrelated tasks in the same context:
|
||||
|
||||
```
|
||||
Task 1: Build authentication → context contains auth code, JWT docs, security patterns
|
||||
Task 2: Build UI components → context now ALSO contains auth stuff
|
||||
|
||||
Result: LLM might suggest auth-related patterns when building UI
|
||||
or mix concerns inappropriately
|
||||
```
|
||||
|
||||
### Autoregressive Failure
|
||||
|
||||
LLMs predict the next token based on ALL context. When context contains:
|
||||
- Unrelated information
|
||||
- Failed attempts
|
||||
- Mixed concerns
|
||||
|
||||
The model can "spiral" into wrong territory, generating increasingly off-base responses.
|
||||
|
||||
### The Gutter Metaphor
|
||||
|
||||
> "If the bowling ball is in the gutter, there's no saving it."
|
||||
|
||||
Once context is polluted with failed attempts or mixed concerns, the model will keep referencing that pollution. Starting fresh is often faster than trying to correct course.
|
||||
|
||||
## Context Health Indicators
|
||||
|
||||
### 🟢 Healthy Context
|
||||
- Single focused task
|
||||
- Relevant files only
|
||||
- Clear progress
|
||||
- Under 60% capacity
|
||||
|
||||
### 🟡 Warning Signs
|
||||
- Multiple unrelated topics discussed
|
||||
- Several failed attempts in history
|
||||
- Approaching 80% capacity
|
||||
- Repeated similar errors
|
||||
|
||||
### 🔴 Critical / Gutter
|
||||
- Mixed concerns throughout
|
||||
- Circular failure patterns
|
||||
- Over 90% capacity
|
||||
- Model suggesting irrelevant solutions
|
||||
|
||||
## Best Practices
|
||||
|
||||
### 1. One Task Per Context
|
||||
|
||||
Don't ask "fix the auth bug AND add the new feature". Do them in separate conversations.
|
||||
|
||||
### 2. Fresh Start on Topic Change
|
||||
|
||||
Finished auth? Start a new conversation for the next feature.
|
||||
|
||||
### 3. Don't Redline
|
||||
|
||||
Stay under 80% of context capacity. Quality degrades as you approach limits.
|
||||
|
||||
### 4. Recognize the Gutter
|
||||
|
||||
If you're seeing:
|
||||
- Same error 3+ times
|
||||
- Solutions that don't match the problem
|
||||
- Circular suggestions
|
||||
|
||||
Start fresh. Your progress is in the files.
|
||||
|
||||
### 5. State in Files, Not Context
|
||||
|
||||
Write progress to files. The next conversation can read them. Context is ephemeral; files are permanent.
|
||||
|
||||
## Ralph's Approach
|
||||
|
||||
The original Ralph technique (`while :; do cat PROMPT.md | agent ; done`) naturally implements these principles:
|
||||
|
||||
1. **Each iteration is a fresh process** - Context is freed
|
||||
2. **State persists in files** - Progress survives context resets
|
||||
3. **Same prompt each time** - Focused, single-task context
|
||||
4. **Failures inform guardrails** - Learning without context pollution
|
||||
|
||||
This Cursor implementation aims to bring these benefits while working within Cursor's session model.
|
||||
|
||||
## Measuring Context
|
||||
|
||||
Rough estimates:
|
||||
- 1 token ≈ 4 characters
|
||||
- Average code file: 500-2000 tokens
|
||||
- Large file: 5000+ tokens
|
||||
- Conversation history: 100-500 tokens per exchange
|
||||
|
||||
Track allocations in `.ralph/context-log.md` to stay aware.
|
||||
|
||||
## When to Start Fresh
|
||||
|
||||
**Definitely start fresh when:**
|
||||
- Switching to unrelated task
|
||||
- Context over 90% full
|
||||
- Same error 3+ times
|
||||
- Model suggestions are off-topic
|
||||
|
||||
**Consider starting fresh when:**
|
||||
- Context over 70% full
|
||||
- Significant topic shift within task
|
||||
- Feeling "stuck"
|
||||
- Multiple failed approaches in history
|
||||
@@ -0,0 +1,174 @@
|
||||
# Guardrails Reference ("Signs")
|
||||
|
||||
This document explains how to create and use guardrails in Ralph.
|
||||
|
||||
## The Signs Metaphor
|
||||
|
||||
From Geoffrey Huntley:
|
||||
|
||||
> "Ralph is very good at making playgrounds, but he comes home bruised because he fell off the slide, so one then tunes Ralph by adding a sign next to the slide saying 'SLIDE DOWN, DON'T JUMP, LOOK AROUND,' and Ralph is more likely to look and see the sign."
|
||||
|
||||
Signs are explicit instructions added to prevent known failure modes.
|
||||
|
||||
## Anatomy of a Sign
|
||||
|
||||
```markdown
|
||||
### Sign: [Descriptive Name]
|
||||
- **Trigger**: When this situation occurs
|
||||
- **Instruction**: What to do instead
|
||||
- **Added after**: When/why this was added
|
||||
- **Example**: Concrete example if helpful
|
||||
```
|
||||
|
||||
## Types of Signs
|
||||
|
||||
### 1. Preventive Signs
|
||||
|
||||
Stop problems before they happen:
|
||||
|
||||
```markdown
|
||||
### Sign: Validate Before Trust
|
||||
- **Trigger**: When receiving external input
|
||||
- **Instruction**: Always validate and sanitize input before using it
|
||||
- **Added after**: Iteration 3 - SQL injection vulnerability
|
||||
```
|
||||
|
||||
### 2. Corrective Signs
|
||||
|
||||
Fix recurring mistakes:
|
||||
|
||||
```markdown
|
||||
### Sign: Check Return Values
|
||||
- **Trigger**: When calling functions that can fail
|
||||
- **Instruction**: Always check return values and handle errors
|
||||
- **Added after**: Iteration 7 - Null pointer exception
|
||||
```
|
||||
|
||||
### 3. Process Signs
|
||||
|
||||
Enforce good practices:
|
||||
|
||||
```markdown
|
||||
### Sign: Test Before Commit
|
||||
- **Trigger**: Before committing changes
|
||||
- **Instruction**: Run the test suite and ensure all tests pass
|
||||
- **Added after**: Iteration 2 - Broken tests committed
|
||||
```
|
||||
|
||||
### 4. Architecture Signs
|
||||
|
||||
Guide design decisions:
|
||||
|
||||
```markdown
|
||||
### Sign: Single Responsibility
|
||||
- **Trigger**: When a function grows beyond 50 lines
|
||||
- **Instruction**: Consider splitting into smaller, focused functions
|
||||
- **Added after**: Iteration 12 - Unmaintainable god function
|
||||
```
|
||||
|
||||
## When to Add Signs
|
||||
|
||||
Add a sign when:
|
||||
|
||||
1. **The same mistake happens twice** - Once is learning, twice is a pattern
|
||||
2. **A subtle bug is found** - Prevent future occurrences
|
||||
3. **A best practice is violated** - Reinforce good habits
|
||||
4. **Context-specific knowledge is needed** - Project-specific conventions
|
||||
|
||||
## Sign Lifecycle
|
||||
|
||||
### Creation
|
||||
|
||||
```markdown
|
||||
### Sign: [New Sign]
|
||||
- **Trigger**: [When it applies]
|
||||
- **Instruction**: [What to do]
|
||||
- **Added after**: Iteration N - [What happened]
|
||||
```
|
||||
|
||||
### Refinement
|
||||
|
||||
If a sign isn't working:
|
||||
- Make the trigger more specific
|
||||
- Make the instruction clearer
|
||||
- Add examples
|
||||
|
||||
### Retirement
|
||||
|
||||
Signs can be removed when:
|
||||
- The underlying issue is fixed at a deeper level
|
||||
- The sign is no longer relevant
|
||||
- The sign is causing more problems than it solves
|
||||
|
||||
## Example Signs Library
|
||||
|
||||
### Security
|
||||
|
||||
```markdown
|
||||
### Sign: Sanitize All Input
|
||||
- **Trigger**: Any user-provided data
|
||||
- **Instruction**: Use parameterized queries, escape HTML, validate types
|
||||
- **Example**: `db.query("SELECT * FROM users WHERE id = ?", [userId])`
|
||||
```
|
||||
|
||||
### Error Handling
|
||||
|
||||
```markdown
|
||||
### Sign: Graceful Degradation
|
||||
- **Trigger**: External service calls
|
||||
- **Instruction**: Always have a fallback for when services are unavailable
|
||||
- **Example**: Cache results, provide default values, show friendly errors
|
||||
```
|
||||
|
||||
### Testing
|
||||
|
||||
```markdown
|
||||
### Sign: Test the Unhappy Path
|
||||
- **Trigger**: Writing tests for new functionality
|
||||
- **Instruction**: Include tests for error cases, edge cases, and invalid input
|
||||
```
|
||||
|
||||
### Code Quality
|
||||
|
||||
```markdown
|
||||
### Sign: Explain Why, Not What
|
||||
- **Trigger**: Writing comments
|
||||
- **Instruction**: Comments should explain reasoning, not describe obvious code
|
||||
- **Example**: `// Using retry because API is flaky under load` not `// Call the API`
|
||||
```
|
||||
|
||||
## Automatic Sign Detection
|
||||
|
||||
The Ralph hooks can automatically detect some patterns and suggest signs:
|
||||
|
||||
- **Thrashing**: Same file edited many times → "Step back and reconsider"
|
||||
- **Repeated errors**: Same test failing → "Check the test assumptions"
|
||||
- **Large changes**: Big diffs → "Consider smaller increments"
|
||||
|
||||
These are logged in `.ralph/failures.md` and can be promoted to guardrails.
|
||||
|
||||
## Using Signs Effectively
|
||||
|
||||
### Do
|
||||
|
||||
- Keep signs concise and actionable
|
||||
- Include concrete examples
|
||||
- Update signs when they're not working
|
||||
- Remove outdated signs
|
||||
|
||||
### Don't
|
||||
|
||||
- Add signs for every minor issue
|
||||
- Make signs too vague ("be careful")
|
||||
- Ignore signs that keep triggering
|
||||
- Let the guardrails file become overwhelming
|
||||
|
||||
## Integration with Ralph
|
||||
|
||||
Signs are:
|
||||
1. Stored in `.ralph/guardrails.md`
|
||||
2. Injected into context at the start of each iteration
|
||||
3. Referenced when relevant situations arise
|
||||
4. Updated based on observed failures
|
||||
|
||||
The goal is a self-improving system where each failure makes future iterations smarter.
|
||||
@@ -0,0 +1,142 @@
|
||||
{
|
||||
"version": 1,
|
||||
"project": "Mobile App File Size Refactor",
|
||||
"overview": "Refactor the existing React Native (Expo) mobile app to keep most files within ~300-400 lines by splitting into smaller modules without changing functionality.",
|
||||
"goals": [
|
||||
"Reduce oversized files to a target range of ~300-400 lines where practical",
|
||||
"Improve maintainability by splitting files into clearer layers (ui/components, hooks, services, utils)",
|
||||
"Preserve existing behavior and UI"
|
||||
],
|
||||
"nonGoals": [
|
||||
"No functional changes",
|
||||
"No UI changes",
|
||||
"No new product features"
|
||||
],
|
||||
"successMetrics": [
|
||||
"All files are within target size or documented as justified exceptions",
|
||||
"Refactor passes existing tests, lint, and typecheck"
|
||||
],
|
||||
"openQuestions": [
|
||||
"Which specific files currently exceed 400 lines and need priority refactors?",
|
||||
"Are there any existing architectural constraints that limit splitting (e.g., circular deps)?"
|
||||
],
|
||||
"stack": {
|
||||
"framework": "React Native (Expo)",
|
||||
"hosting": "App stores via Expo/EAS",
|
||||
"database": "Existing app data layer (no changes)",
|
||||
"auth": "Existing app auth (no changes)"
|
||||
},
|
||||
"routes": [
|
||||
{
|
||||
"path": "(existing)",
|
||||
"name": "App Routes",
|
||||
"purpose": "No route changes; refactor only"
|
||||
}
|
||||
],
|
||||
"uiNotes": [
|
||||
"No UI changes; keep visual output identical",
|
||||
"Split view logic into smaller components without altering layout"
|
||||
],
|
||||
"dataModel": [
|
||||
{
|
||||
"entity": "N/A",
|
||||
"fields": []
|
||||
}
|
||||
],
|
||||
"importFormat": {
|
||||
"description": "Not applicable",
|
||||
"example": {}
|
||||
},
|
||||
"rules": [
|
||||
"Target file size is ~300-400 lines; exceptions must include justification",
|
||||
"Refactor should follow layer-based split: ui/components, hooks, services, utils",
|
||||
"No behavior changes; identical inputs must yield identical outputs"
|
||||
],
|
||||
"qualityGates": [
|
||||
"npm test",
|
||||
"npm run lint",
|
||||
"npm run typecheck"
|
||||
],
|
||||
"stories": [
|
||||
{
|
||||
"id": "US-001",
|
||||
"title": "Inventory oversized files and refactor plan",
|
||||
"status": "done",
|
||||
"dependsOn": [],
|
||||
"description": "As a developer, I want an inventory of oversized files and a planned split strategy so refactors are systematic and safe.",
|
||||
"acceptanceCriteria": [
|
||||
"Produce a list of files > 400 lines with line counts",
|
||||
"For each file, define target split into layers (ui/components, hooks, services, utils)",
|
||||
"Example: a 900-line screen file -> screen container + UI subcomponents + hooks + service module",
|
||||
"Negative case: a file under 300 lines is not split unless a clear modularization need is documented"
|
||||
],
|
||||
"startedAt": "2026-01-25T20:40:12.238126+00:00",
|
||||
"completedAt": "2026-01-25T20:47:05.362525+00:00",
|
||||
"updatedAt": "2026-01-25T20:47:05.362425+00:00"
|
||||
},
|
||||
{
|
||||
"id": "US-002",
|
||||
"title": "Refactor screen-level files into layer-based modules",
|
||||
"status": "in_progress",
|
||||
"dependsOn": [
|
||||
"US-001"
|
||||
],
|
||||
"description": "As a developer, I want oversized screen files split into smaller modules so each file stays around 300-400 lines.",
|
||||
"acceptanceCriteria": [
|
||||
"Split screen files into container + UI components + hooks + services as needed",
|
||||
"Example: screen logic moves into `hooks/use<Screen>.ts` and data calls into `services/<screen>.ts`",
|
||||
"Negative case: moving a function that creates circular dependencies is rejected and alternative split is used",
|
||||
"All screens render identically before and after refactor"
|
||||
],
|
||||
"startedAt": "2026-01-25T20:47:07.436262+00:00",
|
||||
"completedAt": null,
|
||||
"updatedAt": "2026-01-25T20:47:07.436370+00:00"
|
||||
},
|
||||
{
|
||||
"id": "US-003",
|
||||
"title": "Refactor oversized shared components",
|
||||
"status": "open",
|
||||
"dependsOn": [
|
||||
"US-001"
|
||||
],
|
||||
"description": "As a developer, I want oversized shared components split into smaller pieces to improve clarity and reusability.",
|
||||
"acceptanceCriteria": [
|
||||
"Split shared UI components into smaller subcomponents and helper modules",
|
||||
"Example: extract nested UI blocks into `components/<Component>/SubComponent.tsx`",
|
||||
"Negative case: do not extract one-off UI fragments that would reduce cohesion",
|
||||
"No change to public component API unless required for split"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "US-004",
|
||||
"title": "Refactor oversized hooks/services/utils",
|
||||
"status": "open",
|
||||
"dependsOn": [
|
||||
"US-001"
|
||||
],
|
||||
"description": "As a developer, I want oversized logic files split into smaller, focused modules for readability.",
|
||||
"acceptanceCriteria": [
|
||||
"Split large hooks into smaller hooks or helper functions in utils",
|
||||
"Example: a 500-line hook becomes a main hook + `utils/<hook>-helpers.ts`",
|
||||
"Negative case: do not split logic in a way that increases public API surface without justification",
|
||||
"All previous exports remain available or are re-exported from the original entry point"
|
||||
]
|
||||
},
|
||||
{
|
||||
"id": "US-005",
|
||||
"title": "Validate refactor quality gates",
|
||||
"status": "open",
|
||||
"dependsOn": [
|
||||
"US-002",
|
||||
"US-003",
|
||||
"US-004"
|
||||
],
|
||||
"description": "As a developer, I want automated checks to pass to ensure the refactor preserved behavior.",
|
||||
"acceptanceCriteria": [
|
||||
"Run `npm test`, `npm run lint`, and `npm run typecheck` successfully",
|
||||
"Example: all tests pass without modifications to test expectations",
|
||||
"Negative case: if a check fails, refactor is incomplete and must be fixed before completion"
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in new issue
Block a user