Setzt die Architektur aus ADR-0037 in neun Tracer-Bullet-Slices um. Jede Slice ist end-to-end (Code + Test + Doc-Update) und für sich mergebar. Pilot-Lauf nach Slice 8 auf einer kleinen ready-for-agent Story; Memory-Updates und Skill-Deprecation erfolgen erst nach grünem Pilot.
scripts/ralph.sh
Slices 1–4 bauen die Infrastruktur (DB-Sync, PR-Body, drei Phase-Prompt-Files plan/implement/check, Shell-Orchestrator-Library mit Handoff-File-Mechanik). Slices 5–7 entkoppeln bestehende Skills (auto-review-fix single-shot, solcom-review no-persist, QA-Plan-Refinement-Pflicht). Slice 8 ist das eigentliche ralph.sh-Skript. Slice 9 ist das Single-Story-Skill solcom-implement-story für /goal "implementier P2503-X".
Vor Slice 1: Pre-Flight-Verifikation der Jira-Transition-Namen (acli jira workitem transition --key P2503-108 --list) — Loop-Konstanten JIRA_TRANSITION_IN_PROGRESS, JIRA_TRANSITION_DONE, JIRA_TRANSITION_BLOCKED entsprechend setzen.
Zwei Funktionen in einem POSIX-Shell-Skript: pull_prod_baseline() zieht via mw app exec <app-id> "drush sql-dump --gzip" > /tmp/prod-$(date +%s).sql.gz, importiert via ddev import-db --file=/tmp/...sql.gz, legt DDEV-Snapshot prod-baseline an. restore_prod_baseline() macht ddev snapshot restore prod-baseline && ddev drush cr.
Cleanup-Policy: SQL-Dumps unter /tmp/prod-*.sql.gz werden nach erfolgreichem Snapshot via rm entfernt; DDEV-Snapshot prod-baseline wird vor jedem neuen Pull überschrieben (ddev snapshot --delete=prod-baseline + neu).
bash scripts/lib/db-sync.sh pull erzeugt Snapshot prod-baseline ohne Fehler.bash scripts/lib/db-sync.sh restore bringt DB in den gepullten Zustand (verifiziert via Node-Count-Vergleich).MITTWALD_API_TOKEN, bricht das Skript mit klarem Fehler < 5 Zeilen ab.Markdown-Template mit vier Sektionen und Variablen-Substitution via envsubst:
## Summary
${STORY_SUMMARY}
## Sub-Task-Commits
${COMMIT_LIST}
## Open Findings
${OPEN_FINDINGS}
## QA-Plan
${QA_PLAN}
---
Closes ${JIRA_KEY}
Story-Hub: docs/stories/${JIRA_KEY}-${SLUG}.html
Befüllung: STORY_SUMMARY aus acli jira workitem view ${JIRA_KEY} --json --fields summary,description, COMMIT_LIST aus git log develop..HEAD --pretty=format:"- %s", OPEN_FINDINGS aus dem solcom-review-Output (Markdown-Liste), QA_PLAN aus der Story-Hub-Sektion id="qa-plan" (extrahiert via xmllint --xpath).
envsubst < template.md mit Mock-Werten).QA-Plan: (fehlt — Story-Hub muss QA-Plan-Sektion ergänzen) aus statt zu brechen.Drei Plain-Markdown-Prompt-Templates, je eine pro Workflow-Phase. Werte werden via envsubst aus dem Bash-Caller-Environment substituiert. Keine Sub-Agent-Definitionen, keine --agents-JSON — schlanker mattpocock/.sandcastle/-Stil.
scripts/prompts/plan-prompt.md (Skelett):
@${STORY_HUB_PATH}
@docs/adr/0037-ralph-loop-architecture.html
You are the planner for sub-task ${JIRA_KEY} (story ${STORY_KEY}).
Steps:
1. Read the sub-task acceptance criteria:
acli jira workitem view ${JIRA_KEY} --json --fields description
2. Inventory relevant code files via Grep on AK keywords.
3. Decompose the sub-task into 1-3 TDD slices, each covering ONE
behaviour with red (test) -> green (impl) -> refactor.
4. Write the handoff file at ${HANDOFF_PATH} as self-contained HTML.
Handoff file MUST contain:
- <meta> tags: ralph:subtask, ralph:story, ralph:branch, ralph:iter (0),
ralph:status (planned)
- <section id="slices"> with one <tr> per slice:
number, description, expected file changes, status=todo
- empty <section id="findings-log"> (will be appended to by check-phase)
Do NOT write code outside the handoff file.
End your run with: PLAN_DONE
scripts/prompts/implement-prompt.md (Skelett):
@${HANDOFF_PATH}
You are the implementer for sub-task ${JIRA_KEY}, slice ${SLICE_NUM},
iteration ${ITERATION}.
Steps:
1. Read the handoff file. Locate slice ${SLICE_NUM} in the slices table.
2. If ${ITERATION} > 1: read the most recent findings-log entry
and address each finding surgically. Do not rewrite unrelated parts.
3. Execute a TDD vertical slice for this slice ONLY:
a) write a failing test (PHPUnit Kernel/Functional or Twig snapshot)
b) implement minimal code to make the test green
c) refactor only if necessary
4. Follow solcom-tdd skill rules. byte_theme/AGENTS.md is non-negotiable.
Refactor != Rewrite (CLAUDE.md).
Do NOT commit. Do NOT touch other slices.
End your run with: IMPL_DONE
scripts/prompts/check-prompt.md (Skelett):
@${HANDOFF_PATH}
You are the checker for sub-task ${JIRA_KEY}, slice ${SLICE_NUM},
iteration ${ITERATION}. Diff range: ${SLICE_BASE}..HEAD.
Run three sub-stages, each emitting an explicit VERDICT marker:
(1) STANDARDS REVIEW
- Diff against byte_theme/AGENTS.md (13 canonical rules),
active ADRs, docs/wiki/ entities,
four code-reviewer skills.
- List findings: severity + file:line + rationale.
- End with: VERDICT: pass (or fail)
(2) TECH VERIFY
- Run drupal-pre-merge-gate: PHPCS, PHPStan, PHPUnit, config drift.
- If diff touches web/themes/custom/** or Canvas composition:
run agent-browser visual verify against story-hub QA-Plan URLs.
- Report pass/fail per stage with stack trace on fail.
- End with: VERDICT: pass (or fail)
(3) ACCEPTANCE CHECK
- Read sub-task AKs via acli jira workitem view ${JIRA_KEY}.
- For each AK: pass/fail/inconclusive plus proof reference
(test file, URL+step, or drush query).
- inconclusive counts as fail.
- End with: VERDICT: pass (or fail)
Append a new <section> with id="findings-iter-${ITERATION}" inside
<section id="findings-log"> of ${HANDOFF_PATH}. Each sub-stage MUST
contain its VERDICT marker as a recognisable line.
Do NOT write code outside the handoff file.
End your run with: CHECK_DONE
JIRA_KEY=P2503-127 STORY_KEY=P2503-108 STORY_HUB_PATH=docs/stories/P2503-108-….html HANDOFF_PATH=/tmp/test-handoff.html claude -p "$(envsubst < scripts/prompts/plan-prompt.md)" erzeugt gültiges Handoff-HTML in /tmp/test-handoff.html.VERDICT:-Marker im findings-log./goal "@scripts/prompts/plan-prompt.md plane mir P2503-127" funktioniert in-session (ad-hoc-Planning ohne ralph.sh).Bash-Library mit den Helper-Funktionen, die ralph.sh für den Sub-Task-Workflow aufruft. Keine LLM-Layer — reine Shell-Logik plus xmlstarlet für Handoff-Mutation.
Zu liefernde Funktionen:
next_todo_slice $HANDOFF — gibt die Nummer der ersten Slice mit Status todo aus, leerer Output wenn alle done.mark_slice_doing $HANDOFF $NUM, mark_slice_done $HANDOFF $NUM, mark_slice_blocked $HANDOFF $NUM — Status-Mutation via xmlstarlet ed -L.passes_check $HANDOFF $SLICE_NUM $ITER — liest die zuletzt geschriebene section#findings-iter-$ITER, prüft ob alle drei VERDICT: pass-Marker enthalten sind; gibt 0 (pass) oder 1 (fail) zurück.run_subtask_pipeline $SUB — Top-Level: ruft Plan-Phase, dann Slice-Loop, dann Rework-Loop pro Slice (siehe Skelett unten).Zusätzlich: Verzeichnis scripts/state/ralph/ existiert mit .gitkeep; .gitignore erweitern um scripts/state/ralph/*-handoff.html (nur Handoff-Files, nicht das Verzeichnis selbst).
scripts/lib/subtask-orchestrator.sh (Skelett, ~120 Zeilen):
run_subtask_pipeline() {
local SUB="$1"
export JIRA_KEY="$SUB"
export STORY_KEY="$STORY_KEY" # set by caller
export STORY_HUB_PATH HANDOFF_PATH
STORY_HUB_PATH="$(ls docs/stories/${STORY_KEY}-*.html | head -1)"
HANDOFF_PATH="scripts/state/ralph/${SUB}-handoff.html"
# 1) plan once (skip on recovery)
if [ ! -f "$HANDOFF_PATH" ]; then
claude -p --permission-mode acceptEdits \
--output-format stream-json --verbose \
"$(envsubst < scripts/prompts/plan-prompt.md)" \
| tee "/tmp/ralph-${SUB}-plan.jsonl" \
| jq -r 'select(.type=="assistant") | .message.content[]?.text // empty'
fi
# 2) slice loop
while SLICE_NUM=$(next_todo_slice "$HANDOFF_PATH") && [ -n "$SLICE_NUM" ]; do
mark_slice_doing "$HANDOFF_PATH" "$SLICE_NUM"
export SLICE_NUM
export SLICE_BASE
SLICE_BASE=$(git rev-parse HEAD)
# 3) rework loop (max 3)
for iter in 1 2 3; do
export ITERATION="$iter"
claude -p --permission-mode acceptEdits \
--output-format stream-json --verbose \
"$(envsubst < scripts/prompts/implement-prompt.md)" \
| tee "/tmp/ralph-${SUB}-s${SLICE_NUM}-i${iter}-impl.jsonl" \
| jq -r 'select(.type=="assistant") | .message.content[]?.text // empty'
claude -p \
--output-format stream-json --verbose \
"$(envsubst < scripts/prompts/check-prompt.md)" \
| tee "/tmp/ralph-${SUB}-s${SLICE_NUM}-i${iter}-check.jsonl" \
| jq -r 'select(.type=="assistant") | .message.content[]?.text // empty'
if passes_check "$HANDOFF_PATH" "$SLICE_NUM" "$iter"; then
git add .
git commit -m "feat(${SUB}): slice ${SLICE_NUM}"
mark_slice_done "$HANDOFF_PATH" "$SLICE_NUM"
break
fi
done
if ! passes_check "$HANDOFF_PATH" "$SLICE_NUM" 3; then
mark_slice_blocked "$HANDOFF_PATH" "$SLICE_NUM"
echo "<promise>SUBTASK_BLOCKED:${SUB}:slice${SLICE_NUM}</promise>"
return 1
fi
done
rm "$HANDOFF_PATH"
echo "<promise>SUBTASK_DONE:${SUB}</promise>"
}
next_todo_slice() {
xmlstarlet sel -t -v \
'//section[@id="slices"]//tr[td[last()]="todo"][1]/td[1]' "$1" 2>/dev/null
}
mark_slice_doing() { _set_slice_status "$1" "$2" "doing"; }
mark_slice_done() { _set_slice_status "$1" "$2" "done"; }
mark_slice_blocked() { _set_slice_status "$1" "$2" "blocked"; }
_set_slice_status() {
xmlstarlet ed -L -u \
"//section[@id='slices']//tr[td[1]='$2']/td[last()]" -v "$3" "$1"
}
passes_check() {
local HANDOFF="$1" SLICE_NUM="$2" ITER="$3"
local SECTION="//section[@id='findings-iter-${ITER}']"
local VERDICTS
VERDICTS=$(xmlstarlet sel -t -v "${SECTION}//text()[contains(.,'VERDICT')]" "$HANDOFF" 2>/dev/null)
local PASS_COUNT FAIL_COUNT
PASS_COUNT=$(echo "$VERDICTS" | grep -c 'VERDICT: pass' || true)
FAIL_COUNT=$(echo "$VERDICTS" | grep -c 'VERDICT: fail' || true)
[ "$PASS_COUNT" -eq 3 ] && [ "$FAIL_COUNT" -eq 0 ]
}
source scripts/lib/subtask-orchestrator.sh.passes_check liefert 0 bei drei VERDICT: pass-Markern, 1 bei einem fail.next_todo_slice liefert die richtige Slice-Nummer bei einem Handoff mit gemischten Status.scripts/state/ralph/.gitkeep ist committed; .gitignore-Eintrag scripts/state/ralph/*-handoff.html ist gesetzt.run_subtask_pipeline P2503-127 auf einer kleinen ready-for-agent Story endet mit <promise>SUBTASK_DONE...</promise> oder <promise>SUBTASK_BLOCKED...</promise>; Conventional-Commit pro Slice steht im Git-Log.Neues Flag --single-pass (Standard für RALPH_LOOP=1-Environment): keine Worktree-Erzeugung, kein Retry-bis-grün, kein Persistent-Monitor — nur einmaliger Pass über den Review-Report, alles deterministisch Fixbare (Imports, Whitespace, Phpcbf, Twig-Format) wird angewendet, der Rest bleibt unfixed und wird als OPEN_FINDINGS-Liste an stdout zurückgegeben.
Der Worktree-basierte Mode bleibt als Default für manuelle Aufrufe; der single-shot Mode ist additiv.
Skill respektiert Environment-Var RALPH_LOOP=1 als Override für das --persist-Default (Memory [[feedback_solcom_review_persist_default]]). Im ralph-Modus: Report nur als stdout-Markdown, kein File-Write nach docs/reports/, kein LOG.html-Eintrag.
Begründung: ohne Override entstehen bei 50 Stories pro Run 50 HTML-Reports — operativer Müll. Mass-Run-Reports sind ephemer; Persistierung ist nur für manuelle, ad-hoc Reviews wertvoll.
RALPH_LOOP=1 /solcom-review main gibt Report an stdout, schreibt nichts nach docs/reports/.[[feedback_solcom_review_persist_default]] mit Override-Klausel erweitert.Output-Templates der drei Skills um Pflicht-Sektion erweitern:
<section id="qa-plan">
<h2>QA-Plan</h2>
<p>Click-Pfade pro Story-AK für Mensch-QA.</p>
<table>
<thead><tr><th>AK</th><th>URL</th><th>Action</th><th>Expected</th></tr></thead>
<tbody>
<tr><td>…</td><td>…</td><td>…</td><td>…</td></tr>
</tbody>
</table>
</section>
Skill-Prompt erzwingt im Grilling: vor Schreiben des Story-Hubs muss QA-Plan-Sektion gefüllt sein. Stories ohne QA-Plan werden vom Ralph-Loop übersprungen und mit Jira-Comment „missing QA plan, refine before ready-for-agent" zurückgesetzt.
Eigenständige Mini-Action: bestehende Stories mit Label ready-for-agent (Stand 2026-05-17: via acli jira workitem search --jql "project=P2503 AND labels=ready-for-agent" ermitteln) müssen QA-Plan-Sektion nachgepflegt werden — Zähl-Aufgabe vor Pilot-Lauf.
<section id="qa-plan">.to-plan auf einer Test-PRD erzeugt Story-Hub mit befüllter QA-Plan-Sektion.Bash-Skript, ~120 Zeilen mit Kommentaren. Skelett:
#!/usr/bin/env bash
set -euo pipefail
if [ -z "${1:-}" ]; then echo "Usage: $0 <max-stories>"; exit 1; fi
export RALPH_LOOP=1
source scripts/lib/db-sync.sh
# Pre-Loop: PROD-Pull
pull_prod_baseline
for ((i=1; i<=$1; i++)); do
echo "=== Iteration $i ==="
# 0. Recovery-Check
IN_PROGRESS=$(acli jira workitem search --jql \
'project=P2503 AND status="In Progress" AND assignee=currentUser()' \
--json | jq -r '.[].key')
if [ -n "$IN_PROGRESS" ]; then
STORY_KEY=$(echo "$IN_PROGRESS" | head -1)
BRANCH="feature/$STORY_KEY"
echo "Recovery: resume $STORY_KEY on $BRANCH"
git checkout "$BRANCH" || { echo "branch missing, hard-stop"; exit 1; }
else
# 1. Pickup
# Label sitzt auf Sub-Tasks (Refinement-Granularität).
# Planner aggregiert Sub-Tasks zu Parent-Stories, picked die höchstrangige.
STORY_KEY=$(acli jira workitem search --jql \
'project=P2503 AND labels=ready-for-agent AND issuetype="Sub-Task"
AND status="Zu erledigen" ORDER BY rank ASC' \
--json --limit 50 \
| jq -r '[.[] | .fields.parent.key] | unique | .[0] // empty')
if [ -z "$STORY_KEY" ]; then
echo "<promise>NO_AGENT_READY</promise>"
exit 0
fi
# 2. Branch
git checkout develop && git pull origin develop --rebase
git checkout -b "feature/$STORY_KEY"
# 3. DB-Restore
restore_prod_baseline
# 4. Jira-Transition: In Progress
acli jira workitem transition --key "$STORY_KEY" \
--transition "In Progress" --yes
fi
# 5. Sub-Task-Loop — pipeline-Funktion aus scripts/lib/subtask-orchestrator.sh
source scripts/lib/subtask-orchestrator.sh
SUBTASKS=$(acli jira workitem view "$STORY_KEY" --json \
--fields subtasks | jq -r '.subtasks[].key')
for SUB in $SUBTASKS; do
SUB_STATUS=$(acli jira workitem view "$SUB" --json --fields status \
| jq -r '.status.name')
if [ "$SUB_STATUS" = "Fertig" ]; then continue; fi
acli jira workitem transition --key "$SUB" --transition "In Progress" --yes
if ! run_subtask_pipeline "$SUB"; then
# SUBTASK_BLOCKED — Jira-Transition + Comment, nächste Story
acli jira workitem transition --key "$SUB" --transition "Blocked" --yes
acli jira workitem comment create --key "$STORY_KEY" \
--body-file "scripts/state/ralph/${SUB}-handoff.html"
git checkout develop
continue 2 # nächste Story
fi
acli jira workitem transition --key "$SUB" --transition "Done" --yes
done
# 6. Pre-PR-Tail
REVIEW_REPORT=$(claude -p "$(cat <<EOF
/solcom-review develop
EOF
)")
FIX_RESULT=$(claude -p "$(cat <<EOF
/auto-review-fix --single-pass --report-stdin
$REVIEW_REPORT
EOF
)")
OPEN_FINDINGS=$(echo "$FIX_RESULT" | jq -r '.open_findings')
# 7. PR open
git push -u origin "feature/$STORY_KEY"
PR_BODY=$(STORY_KEY="$STORY_KEY" OPEN_FINDINGS="$OPEN_FINDINGS" \
bash scripts/lib/pr-body-template.sh)
gh pr create --base develop --head "feature/$STORY_KEY" \
--title "feat($STORY_KEY): $(acli jira workitem view "$STORY_KEY" --json \
--fields summary | jq -r '.summary')" \
--body "$PR_BODY"
# 8. Cleanup: lokaler Branch weg, zurück zu develop
git checkout develop
git branch -D "feature/$STORY_KEY"
# 9. Story-Transition: Done (oder „In Review" je nach Workflow)
acli jira workitem transition --key "$STORY_KEY" \
--transition "Done" --yes
done
./scripts/ralph.sh 1 läuft End-to-End auf einer Pilot-Story (kleinste verfügbare ready-for-agent Story mit QA-Plan).develop.solcom-implement-storySkill ruft dieselbe Phase-Pipeline (plan-prompt.md → implement-prompt.md → check-prompt.md, gesteuert durch scripts/lib/subtask-orchestrator.sh) wie ralph.sh auf — aber in-session, ohne Mass-Pickup-Loop. Eingabe: ein Jira-Story-Key. Output: gleiches Endergebnis (Feature-Branch + PR offen). Ad-hoc-Variante: /goal "@scripts/prompts/plan-prompt.md plane P2503-127" ruft nur die Plan-Phase auf, ohne durchzuiterieren.
Trigger-Phrasen: „implementier P2503-XXX", „bau Story P2503-XXX", „/goal "implementier P2503-XXX"". Damit ist /goal als Single-Story-Driver erschlossen ohne ralph.sh-Wrapper.
Stylistisch entspricht das Skill dem Pattern von backlog-to-jira: schmaler Wrapper über die operativen Bausteine. Keine eigene Choreographie.
/goal "implementier P2503-XXX" löst Skill aus, das die gleiche Pipeline wie ralph.sh durchläuft (Branch, DB, Sub-Tasks, Pre-PR-Tail, PR).[[project_implement_pipeline_triple]] ist re-written und referenziert dieses Skill plus ralph.sh.implement-Skill bekommt Deprecation-Header mit Link auf dieses Skill und ralph.sh.xmlstarlet korrumpiert HTML (XPath schlägt fehl, in-place edit zerstört Datei)next_todo_slice, _set_slice_status, passes_check gegen Mock-Handoffs. Backup-Strategie: cp $HANDOFF $HANDOFF.bak vor jeder Mutation, Rollback auf Fail.passes_check verlangt exakt drei VERDICT: pass-Marker; bei weniger gilt der Check als fail und löst Rework aus. Prompt enthält explizit „each sub-stage MUST contain its VERDICT marker as a recognisable line".acli jira workitem transition --key P2503-108 --list als allererster Schritt; Konstanten in ralph.sh entsprechend setzendb-sync.sh restore ruft vorab ddev stop && ddev start; falls Container nicht clean stoppt, exit-code-Check und Hard-Stopenvsubst (POSIX-safe); JSON-Outputs vorher mit jq -r entjsonifizieren<n>-Argument am Aufruf ist harter Cap; Pilot-Run mit n=1, dann n=3, dann frei wählbar je nach Pilot-Costgit branch -D löscht Story-Branch bevor PR-Push remote durch istgh pr view --json url-Check (PR muss existieren); bei Push-Fehler bleibt Branch lokal stehendevelop../scripts/ralph.sh 1 läuft auf der Pilot-Story durch, PR ist offen, Story ist in Jira auf „Done".COMMIT_TYPE: feat-Marker-Zeile, die der Shell-Caller parst — zuverlässiger als Issuetype-Mapping.
prod-baseline wird pro Run überschrieben — sollen alte Snapshots automatisch gelöscht werden, oder behalten wir Historie? Default-Vorschlag: automatisches Überschreiben (1 baseline pro Run, kein Lifecycle).
--max-turns)? Default-Vorschlag: zunächst nicht — <n>-Argument am Top-Level reicht als Cap.
--output-format stream-json --verbose plus tee + jq-Filter, damit Token-Stream während des Loops live im Terminal sichtbar ist und der finale Result-Frame strukturiert in $RESULT landet (chrismdp/ralph-Pattern). Per-Sub-Task wird ein /tmp/ralph-${STORY}-${SUB}.jsonl-Log behalten für Post-Mortem; Cleanup-Policy: nach erfolgreichem Story-Done rm /tmp/ralph-${STORY}-*.jsonl, bei Block-Status bleiben Logs für Mensch-Inspektion liegen.
Erst nach grünem Pilot:
[[project_implement_pipeline_triple]] rewriten: neue Triple ralph.sh / solcom-implement-story → solcom-tdd → solcom-review (no-persist) → auto-review-fix (single-shot).[[project_parallel_loops_architecture]] scope-narrow auf „Mode A (in-session Sub-Agent) bleibt; Mode B (detached background) deprecated".[[project_office_hours_cron]] als obsolete markieren, weil Cron entfällt.[[feedback_solcom_review_persist_default]] um Override-Klausel ergänzen.implement bekommt Deprecation-Header mit Link auf ralph.sh und solcom-implement-story.solcom-worktree + solcom-ddev-bootstrap: Header-Note „Mode B (detached) deprecated, Mode A bleibt für ad-hoc-Isolation".