Architecture Decision Record · 0037

Ralph-Loop-Architektur — Mass-Implementation autonom über ready-for-agent Jira-Stories

Entscheidung. Ein einfaches Shell-Skript scripts/ralph.sh <n> ersetzt den deprecated implement-Skill und die worktree-basierte Parallel-Pipeline (ADR-0030, detached-Mode) als Mass-Implementation-Driver. Strikt sequenziell, kein Worktree. Pro Sub-Task läuft eine Bash-orchestrierte Mehrstufen-Pipeline mit drei Phase-Prompt-Files (analog mattpocock .sandcastle/): plan → handoff file → n Slice-Iterationen (implement → check, rework bei Fail) → commit pro Slice → Sub-Task done. Keine Custom-Sub-Agent-Definitionen, keine --agents-JSON — jede Phase ist ein claude -p "$(envsubst < scripts/prompts/<phase>-prompt.md)"-Call mit klarem Output-Kontrakt. Sonnet 4.6 für alle Phasen. Pickup-Selektor: Jira-Label ready-for-agent auf Story-Level. Per-Story PROD-DB-Snapshot-Restore. Pre-PR-Tail: solcom-review plus einmaliger auto-review-fix-Pass; unfixed Findings landen im PR-Body. QA-Plan wird aus dem Story-Hub übernommen (Refinement-Pflicht), nicht generiert.

Kontext

Der SOLCOM-MVP-Termin 08.07.2026 erzwingt einen hohen Implementations-Durchsatz auf dem refinierten Story-Backlog (Projekt P2503). Bisherige autonome Bau-Mechaniken im Repo:

Parallel dazu existiert in der externen Praxis das Ralph-Loop-Pattern (mattpocock/course-video-manager, chrismdp/ralph): ein minimaler Bash-Loop, der pro Iteration einen claude -p-Call ausführt, eine Aufgabe nach Priority pickt, lokal verifiziert (Typecheck, Tests, CI), committet und einen Exit-Marker in der LLM-Antwort sucht. Mattpococks Skript ist ~25 Zeilen Bash; chrismdps Variante ergänzt Recovery-Mechanik für unterbrochene Tickets und Branch-Discipline.

Die User-Anfrage 2026-05-17 stellt den Bezug her: man möchte einen analogen Mass-Loop für SOLCOM, der über Jira-ready-for-agent Stories iteriert, pro Story einen Feature-Branch anlegt, pro Sub-Task einen TDD-Loop ausführt, und am Ende einen Pull-Request mit konkreten human-testbaren Schritten öffnet. Ausdrücklich nicht-Ziele: Worktrees, Cron, Opus-Heavy-Brief, parallele Loops.

Der gegrillte Design-Tree (siehe Plan 2026-05-17-001-ralph-loop.html) hat folgende Entscheidungen zur Reife gebracht:

AchseEntscheidung
TopologieStrikt sequenziell, ein Story-Branch nach dem anderen, kein Worktree
Pickup-SelektorJira-Label ready-for-agent auf Sub-Task (Refinement-Marker pro Sub-Task), ORDER BY rank ASC. Planner aggregiert eligible Sub-Tasks zu Parent-Stories und picked die höchstrangige Story. Eine Story ist eligible, sobald mindestens ein ihrer Sub-Tasks ready-for-agent trägt; das subtask_keys-Feld im Plan-Output enthält ausschließlich die ready-for-agent-markierten Sub-Tasks in Status Zu erledigen — andere Sub-Tasks der Story bleiben unberührt (Korrektur 2026-05-17, siehe Rejected-Alternatives unten).
Granularität pro IterationPro Sub-Task: Plan-Phase + n Slice-Iterationen, jede Slice mit Implement- und Check-Phase (Bash-orchestriert, Handoff-File als State)
Workflow-PhasenDrei Phase-Prompt-Files: plan-prompt.md → Handoff-File mit Slices; implement-prompt.md pro Slice; check-prompt.md kombiniert Standards-Review + Tech-Verify + Acceptance-Check in einem Pass mit drei VERDICT:-Markern
ModelSonnet 4.6 für alle Phasen, kein Heavy-Brief, keine Custom-Sub-Agents
Verify-Achse pro Sub-Taskdrupal-pre-merge-gate + (bei Touch von web/themes/custom/** oder Canvas-Komposition) agent-browser
Loop-Termination./scripts/ralph.sh <n>, Exit-Marker <promise>NO_AGENT_READY</promise> oder Iterations-Erschöpfung
Failure-ModeStory stoppen, Sub-Task → Blocked, Jira-Comment, nächste Story
PR-Trigger1 PR pro Story nach letztem grünen Sub-Task
Pre-PR-Tailsolcom-review + einmaliger auto-review-fix-Pass, unfixed Findings → PR-Body
QA-Plan-QuelleStory-Hub-Sektion QA-Plan, Refinement-Pflicht; Loop kopiert in PR-Body
DB-SyncPre-Loop: mw app exec ... drush sql-dump → DDEV-Snapshot prod-baseline. Pro Story: ddev snapshot restore prod-baseline + ddev drush cr
RecoveryBei Re-Start: JQL status="In Progress"-Check, Branch-Exists-Check, Pre-Merge-Gate auf bestehenden Commits — resume statt skip
Driver-ModiMass-Loop = Shell-Skript; Single-Story = neues Skill solcom-implement-story, in-session, kompatibel mit /goal "implementier P2503-X"

Verifizierte technische Voraussetzung (Pre-Write-Check 2026-05-17): claude -p in Version 2.1.143 unterstützt --permission-mode acceptEdits, --output-format stream-json --verbose und --allowedTools. Damit ist der phasenbasierte Workflow im non-interaktiven Modus baubar — Custom-Sub-Agent-Definitionen werden bewusst nicht genutzt.

Entscheidung

1. Shell-Skript scripts/ralph.sh als Mass-Loop-Driver

Ein ~80 Zeilen langes Bash-Skript wird unter scripts/ralph.sh abgelegt und vom Repo-Root mit ./scripts/ralph.sh <n> aufgerufen. <n> ist die Maximum-Anzahl an Story-Iterationen pro Lauf (mattpocock-Style; harter Cost-Cap pro Run). Das Skript läuft außerhalb der Claude-Code-Session als unabhängiger lokaler Prozess.

source scripts/lib/subtask-orchestrator.sh

for ((i=1; i<=$1; i++)); do
  # 0. Recovery-Check: gibt es eine In-Progress-Story (+ Handoff-Files)?
  # 1. Pickup: JQL ready-for-agent Sub-Tasks → Parent-Story-Aggregation (rank ASC)
  # 2. Branch: git checkout develop && git pull && git checkout -b feature/<KEY>
  # 3. DB: ddev snapshot restore prod-baseline && ddev drush cr
  # 4. Pro Sub-Task: run_subtask_pipeline "$SUB"
  #      → plan-prompt.md (einmalig) → Handoff-File mit Slices
  #      → pro Slice: implement-prompt.md → check-prompt.md → bei Pass: commit
  #      → SUBTASK_DONE | SUBTASK_BLOCKED
  # 5. Pre-PR-Tail: solcom-review + auto-review-fix single-shot
  # 6. PR: gh pr create mit Story-Hub-QA-Plan im Body
  # 7. Loop-Exit-Marker prüfen
done

2. Phase-Prompt-Files — Shell ruft pro Phase einen Plain-claude -p-Call mit klarem Output-Kontrakt

Die Orchestrierung läuft in Bash, nicht über einen LLM-Orchestrator-Agenten. Jede Workflow-Phase ist ein eigenes Prompt-File unter scripts/prompts/ mit klarem Input/Output-Kontrakt (analog mattpocock/.sandcastle/). ralph.sh (bzw. die ausgelagerte Bash-Funktion in scripts/lib/subtask-orchestrator.sh) fährt die Phasen sequenziell ab, übergibt Werte via Environment-Vars + envsubst, liest strukturierten Output aus dem persistenten Handoff-File und entscheidet Pass/Fail/Loop selbst.

Keine Custom-Sub-Agent-Definitionen, keine --agents-JSON, kein --agent-Flag — schlanker als das Sub-Agent-Konstrukt. Vorteile: (a) claude -p ohne Agent-Setup ist schneller und billiger pro Call; (b) Prompts sind versionsverwaltete .md-Files, debuggbar und einzeln testbar; (c) keine Spawn-Komplexität, keine JSON-Parser-Disziplin; (d) /goal "@scripts/prompts/plan-prompt.md ad-hoc P2503-127" ruft eine Phase auch interaktiv auf.

Drei Phase-Prompt-Files pro Sub-Task:

Die Phasen-Trennung folgt der Karpathy-/mattpocock-Discipline: ein Prompt = eine Phase = ein klarer Output-Kontrakt. Der Shell-Caller liest nur die VERDICT:-Marker im Handoff-File und entscheidet Pass/Loop. Keine LLM-zu-LLM-Kommunikation.

Handoff-File-Layout

Pro Sub-Task entsteht ein selbst-enthaltenes HTML-File unter scripts/state/ralph/${SUB}-handoff.html (gitignored, Verzeichnis wird beim ersten Bootstrap angelegt). Format folgt dem Karpathy-„doing"-Pattern (chrismdp/ralph): persistenter Workflow-State, der Mac-Restarts und Loop-Abbrüche überlebt.

<!doctype html>
<meta name="ralph:subtask" content="P2503-127">
<meta name="ralph:story"   content="P2503-108">
<meta name="ralph:branch"  content="feature/P2503-108">
<meta name="ralph:iter"    content="2">
<meta name="ralph:status"  content="doing">
<title>P2503-127 — handoff</title>

<section id="slices">
  <table>
    <tr><th>#</th><th>Slice</th><th>Expected changes</th><th>Status</th></tr>
    <tr><td>1</td><td>Add migrate_plus source plugin for SF REST</td><td>config/sync/migrate_plus.*.yml + src/Plugin/migrate/source/*</td><td>done</td></tr>
    <tr><td>2</td><td>Wire EventSubscriber for hard-delete sweep</td><td>src/EventSubscriber/* + tests/Kernel/*</td><td>doing</td></tr>
  </table>
</section>

<section id="findings-log">
  <h3>Iteration 1 (slice 2)</h3>
  <p>reviewer: 1 high finding — EventSubscriber missing @priority arg</p>
  <p>verifier: PHPCS clean, PHPStan level 5 ok, 2 Kernel tests added pass</p>
  <p>acceptance-checker: AK1 pass, AK2 inconclusive (no DB-assertion of delete-sweep)</p>
</section>

Innerer Loop pro Sub-Task (Shell-implementiert)

Workflow: plan → Slice-Loop (n Slices) → pro Slice Rework-Loop (max 3 Iterationen).

export JIRA_KEY="$SUB"
export STORY_KEY="$STORY_KEY"
export STORY_HUB_PATH="$(ls docs/stories/${STORY_KEY}-*.html | head -1)"
export HANDOFF_PATH="scripts/state/ralph/${SUB}-handoff.html"

# 1) Plan once per sub-task (skip if handoff already exists from 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-filter
fi

# 2) Slice loop — iterate todo-slices until all done or one blocks
while next_todo_slice "$HANDOFF_PATH" > /dev/null; do
  SLICE_NUM=$(next_todo_slice "$HANDOFF_PATH")
  mark_slice_doing "$HANDOFF_PATH" "$SLICE_NUM"
  export SLICE_NUM
  export SLICE_BASE=$(git rev-parse HEAD)

  # 3) Rework loop — max 3 iterations per slice
  for iter in 1 2 3; do
    export ITERATION="$iter"

    # implement-phase
    claude -p --permission-mode acceptEdits \
      --output-format stream-json --verbose \
      "$(envsubst < scripts/prompts/implement-prompt.md)" \
      | tee "/tmp/ralph-${SUB}-slice${SLICE_NUM}-iter${iter}-impl.jsonl" | jq-filter

    # check-phase (appends to handoff)
    claude -p \
      --output-format stream-json --verbose \
      "$(envsubst < scripts/prompts/check-prompt.md)" \
      | tee "/tmp/ralph-${SUB}-slice${SLICE_NUM}-iter${iter}-check.jsonl" | jq-filter

    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" "$iter"; then
    mark_slice_blocked "$HANDOFF_PATH" "$SLICE_NUM"
    echo "<promise>SUBTASK_BLOCKED:${SUB}:slice${SLICE_NUM}</promise>"
    return 1
  fi
done

# 4) All slices done — clean up handoff, emit done-promise
rm "$HANDOFF_PATH"
echo "<promise>SUBTASK_DONE:${SUB}</promise>"

Helper-Funktionen next_todo_slice, mark_slice_doing, mark_slice_done, mark_slice_blocked, passes_check liegen in scripts/lib/subtask-orchestrator.sh. passes_check liest die zuletzt geschriebene Findings-Log-Sektion des Handoff-Files (XPath auf section#findings-log/section[last()]) und prüft ob alle drei VERDICT:-Marker auf pass stehen. Handoff-Mutation läuft über xmlstarlet ed -L (in-place edit auf konkreten XPath-Knoten), nicht über naives sed.

Cleanup-Policy: bei SUBTASK_DONE wird das Handoff-File gelöscht (Sub-Task abgeschlossen, Diff in Git committed). Bei SUBTASK_BLOCKED bleibt das File für Mensch-Inspektion liegen — der Handoff-Status (welche Slices done, welche blocked, welche Findings) ist dann der Hand-Over für den manuellen Pickup.

Vorteile gegenüber einem LLM-Orchestrator: (a) deterministischer Kontrollfluss in Bash, debuggbar; (b) Persistenter Sub-Task-State im Handoff-File, übersteht Mac-Restart; (c) Sub-Agent-Outputs persistieren als /tmp/ralph-*.jsonl für Post-Mortem; (d) keine zusätzliche LLM-Token-Schicht; (e) set -euo pipefail fängt Bash-Fehler hart auf; (f) Multi-Slice-Sub-Tasks bekommen pro Slice einen sauberen Conventional-Commit.

3. Branch-Lifecycle zwischen Stories

Nach Pull-Request-Open für Story A: git checkout develop, git pull origin develop --rebase, git branch -D feature/P2503-A (lokaler Branch entfernt — der PR-Branch lebt remote weiter). Die nächste Story startet auf einem frischen Pull von develop. Damit basiert jeder neue Feature-Branch garantiert auf dem aktuellsten develop-Stand.

4. PROD-DB-Sync

Pre-Loop (einmal pro ralph.sh-Aufruf): mw app exec <app-id> "drush sql-dump --gzip" > /tmp/prod.sql.gz, gefolgt von ddev import-db --file=/tmp/prod.sql.gz und ddev snapshot prod-baseline. Per-Story: ddev snapshot restore prod-baseline && ddev drush cr (~30-60 Sekunden Overhead pro Story). Setzt Skill mittwald-hosting + die Mittwald-CLI mw mit gesetzter MITTWALD_API_TOKEN-Env-Var voraus.

5. Pre-PR-Tail

Nach dem letzten grünen Sub-Task öffnet das Skript zwei weitere claude -p-Calls:

  1. /solcom-review auf den Story-Diff, mit explizit --no-persist im ralph-Modus (sonst entstehen pro Mass-Run bis zu 50 HTML-Reports unter docs/reports/). Der Report wird stattdessen als Inline-Sektion in den PR-Body übernommen. Memory [[feedback_solcom_review_persist_default]] bleibt für manuelle Aufrufe gültig; der ralph-Modus überschreibt das Default.
  2. auto-review-fix im neuen single-shot Mode: liest den Review-Report, fixt was deterministisch fixbar ist (Typos, Imports, Format), kein Retry-bis-grün-Loop. Unfixed Findings landen als TODO-Liste im PR-Body.

Der PR-Body folgt dem neuen Template (siehe Plan) mit Sektionen: Story-Summary, Sub-Task-Commits, Open Findings, QA-Plan (aus Story-Hub kopiert).

6. Failure-Mode

Wenn ein Sub-Task nach maximal drei TDD-Loop-Versuchen rote Tests oder rotes drupal-pre-merge-gate liefert: Sub-Task wird in Jira auf Blocked transitioniert, Jira-Comment mit den letzten drei Versuchsbeschreibungen plus Branch-Name plus Stack-Trace, Branch bleibt lokal offen ohne PR, Loop kehrt zu develop zurück und picked die nächste ready-for-agent Story. Manueller Pickup durch den Menschen entscheidet ob der Branch fortgesetzt oder verworfen wird.

7. Recovery bei abgebrochenem Lauf

Pre-Loop-Check: JQL project = P2503 AND status = "In Progress" AND assignee = currentUser(). Bei null Treffern: regulärer Pickup. Bei mehr als einem Treffer: harter Stopp (Inconsistent State, Mensch muss aufräumen).

Bei einem Treffer: Branch-Exists-Check git rev-parse --verify feature/<STORY_KEY>; wenn vorhanden, Branch auschecken. Dann Handoff-File-Check über alle Sub-Tasks der Story:

  1. Pro Sub-Task: Datei scripts/state/ralph/${SUB}-handoff.html existiert? Wenn ja, lese solcom:ralph:status-Meta und Slice-Tabelle. Status done → Sub-Task war abgeschlossen, springe zum nächsten. Status doing oder Slice-Tabelle hat noch todo/doing-Einträge → resume in der inneren Sub-Task-Loop ab der ersten nicht-done-Slice; das Handoff-File enthält bereits Plan und Findings-Log. Status blocked → Sub-Task wird übersprungen und Story landet im Failure-Mode (manueller Pickup).
  2. Wenn Sub-Task in Jira „In Progress" ist, aber kein Handoff-File existiert: Inkonsistenz; Sub-Task wird zurückgesetzt auf den vorherigen Status und der reguläre Plan-Step neu ausgeführt.
  3. Pre-Merge-Gate auf existierenden Commits ausführen, bevor weitere Slices angefasst werden — schlägt das Gate fehl, ist die Branch in einem ungültigen Zwischenstand, harter Stopp.

8. Driver-Trennung

Mass-Loop ist scripts/ralph.sh. Single-Story ist ein neues Skill solcom-implement-story, das in-session aufgerufen wird, denselben Sub-Task-Choreographen nutzt, aber nur einen Story-Key entgegennimmt. /goal "implementier P2503-X" ist der Wrapper für das Skill.

Rejected alternatives

Consequences

Deprecation und Supersession

Neue Artefakte

Skill-Updates

Operative Annahmen

Branch-Lifecycle-Hinweis. Loop führt git branch -D feature/<KEY> erst aus, nachdem der PR remote angelegt und das CI-Build gestartet ist (gh pr view --json url liefert Bestätigung). Bei lokalem Push-Fehler bleibt der Branch lokal stehen und Loop transitioniert in den Failure-Mode für diese Story.

Migrations-Pfad

  1. Plan 2026-05-17-001-ralph-loop.html baut die neun Tracer-Bullet-Slices.
  2. Nach grünem End-to-End-Lauf auf einer Pilot-Story (Empfehlung: kleinster offener ready-for-agent Sub-Task): Memory-Updates und Deprecation-Header in den alten Skills.
  3. Cron-Job-Abbau (Office-Hours) separat im selben Sprint.

Amendment 2026-05-20 — Anthropic-Harness-Adaption

Diese ADR ist partially superseded durch das ADR-Quartett 0040 / 0041 / 0042 / 0043. Drei Veränderungen sind material:

  1. Loop-Phasen-Anordnung erweitert — zwischen plan und implement kommen zwei neue Phasen CONTRACT (Sprint-Contract pro Story) und CONTRACT-EVAL (4-Check-Verdict, bis zu 3 Retries). Details: ADR-0040.
  2. Handoff-File-Pattern endgültig gestrichen — das ursprünglich in dieser ADR vorgesehene ${HANDOFF_PATH}-HTML mit <section id="findings-log"> wurde in der tatsächlichen Implementation weggelassen und nie durch ein gleichwertiges Konstrukt ersetzt. ADR-0041 ersetzt es durch eine Hub-interne <section id="implementation-notes"> mit Sub-Task-Granularität, akkumuliert über drei Phasen (implement / review / merge). Die in dieser ADR oben referenzierten Abschnitte "Handoff-File-Layout" und das im Plan 2026-05-17-001 erwähnte ${HANDOFF_PATH} sind durch ADR-0041 obsolet.
  3. Sonnet-only und Reviewer-Calibration — User-Direktive vom 2026-05-20: alle Loop-Phasen laufen Sonnet. Das Heavy-Brief-Pattern aus Memory project_implement_pipeline_triple (Opus orchestriert, Sonnet implementiert) ist damit für den Ralph-Loop relativiert. Da Sonnet als QA-Agent kalibriert werden muss (Anthropic-Warnung), wird in ADR-0042 ein zwei-stufiges Calibration-Pattern eingeführt: Few-shot-Examples plus Post-mortem-Lessons-Log unter docs/agents/reviewer-calibration.html.

Zusätzlich wird der Out-of-Scope-Pool (ADR-0043) als eigenständiger Strang eingeführt: docs/refactoring-pool.html als append-only Audit-Trail für Refactoring-Funde, bewusst nicht als Jira-Sub-Tasks. Dies ist eine bewusste Abkehr von der Jira-als-kanonische-Senke-Konvention und ist nur für den Refactoring-Strang zulässig.

Die in dieser ADR beschriebene 4-Phasen-Pipeline bleibt strukturell gültig (plan / implement / review / merge); ADR-0040 fügt zwei Phasen vor implement ein, verändert aber weder Story-Pickup noch Sub-Task-Loop noch Promise-Marker-Mechanik. Implementation-Plan: docs/plans/2026-05-20-001-anthropic-harness-adaption.html.

Amendment 2026-05-21 — Deprecation of Git Worktrees

Diese ADR und der Ralph-Loop werden am 2026-05-21 im Rahmen von ADR-0045 angepasst:

  1. Verzicht auf Git-Worktrees: Der Ralph-Loop läuft nicht mehr in isolierten, parallelen Worktrees mit dynamischen DDEV-Port-Zuweisungen. Alle Ausführungen laufen sequentiell direkt im Hauptarbeitsverzeichnis (primary workspace).
  2. Sequentielle Ausführung: Da keine parallelen Instanzen mehr gestartet werden können (Gefahr von Port- und Datenbankkonflikten), werden offene Aufgaben nacheinander abgearbeitet.
  3. Sauberes Arbeitsverzeichnis als Voraussetzung: Vor dem Start eines Loops oder Skills (z. B. auto-review-fix) wird zwingend geprüft, ob das Arbeitsverzeichnis sauber ist (git status --porcelain ist leer), um uncommittete Änderungen des Entwicklers vor Überschreiben zu schützen.