diff --git a/docs/integrations/hermes-ralph-executor.md b/docs/integrations/hermes-ralph-executor.md new file mode 100644 index 0000000..9148102 --- /dev/null +++ b/docs/integrations/hermes-ralph-executor.md @@ -0,0 +1,134 @@ +# Design sketch: Hermes × Ralph — DAG-scheduled, honest-gated execution + +> Status: **design sketch** (not yet implemented). Captures the "best of both worlds" +> integration between [Hermes](https://github.com/NousResearch/hermes-agent)'s Kanban +> orchestrator and bmalph's Ralph execution loop. + +## Why + +The two systems are **complementary, not competing**: + +| | Strength | Weakness | +|---|---|---| +| **Hermes Kanban** | Durable SQLite DAG, *dynamic* dependency-aware scheduling, fleet observability, fine-grained (per-story) parallelism | Worker quality is bound to its configured provider/model + context window; nothing forces an honest, machine-checked "done" | +| **bmalph / Ralph** | Strong per-story execution (TDD-first, circuit breaker), honest verification when a real gate exists | Sequential by default; swarm is coarse (per-epic, static bin-pack, end-merge); file-based observability; no durable cross-run ledger | + +So: **let Hermes be the control plane and Ralph be the execution engine.** Hermes owns the +work DAG + observability; a Ralph loop running the Claude Code driver executes each card; +"done" is defined by a captured gate. This is the **C + D** pattern below (with **E** falling +out for free). + +### Anti-pattern to avoid (why not "just emit events to a board") + +A tempting shortcut is to keep Ralph running its own `@fix_plan` and have it *also* write +epics/stories to the Hermes board and emit matching events. That creates **two sources of +truth for the work list** (BMAD → `@fix_plan` *and* BMAD → board) and a permanent sync/mapping +layer. It's a sticking plaster. The design below removes the second board entirely: Ralph +becomes a **stateless per-card executor** with an ephemeral one-item plan. + +## C — Hermes dispatches Ralph + +**Single source of truth:** the Hermes board. Cards carry the story spec + `blockedBy` edges; +the dispatcher auto-promotes a card to `ready` when its parents complete and claims it under +the existing concurrency caps. The only change is **what the worker is**. + +Instead of spawning Hermes's built-in worker, the dispatcher invokes a **Ralph executor** +(`spawn_fn`) for the claimed card: + +1. **Provision** the card's git worktree + branch (Hermes already does this: + `/.worktrees/` on `story/`). +2. **Materialise an ephemeral, single-item `@fix_plan.md`** in that worktree from the card + body (the story spec). No epic list, no second board — one card, one plan item. +3. **Write a scoped `.ralph/.ralphrc`** (or reuse a project template) with: + - `PLATFORM_DRIVER=claude-code` (Opus via Claude Code / Max — see Cost), + - the **gate** as `QUALITY_GATES` (build + test + lint, language-agnostic), + - `REVIEW_ENABLED=true`, circuit-breaker defaults, + - `LIVE_OUTPUT=true` so the run streams incrementally (see Observability). +4. **Run** `bash .ralph/ralph_loop.sh` scoped to that one card. The loop's stream is tee'd + to a log Hermes ingests as card **heartbeats/events**. +5. **Terminal hand-back** (see D): on `plan_complete` with a green gate the worker emits + `kanban_complete(metadata={gate evidence})`; on circuit-break / unmet prerequisite it + emits `kanban_block(reason=…)` — honestly, no faked completion. + +Hermes keeps: DAG promotion, retries/diagnostics, per-card worktrees, concurrency caps, the +durable board. Ralph provides: the TDD loop, Opus-grade execution, the circuit breaker, the +honest gate. **Ralph holds no board state** — the `@fix_plan` is derived from the card and +thrown away, so there is nothing to keep in sync. + +## D — The honest-gate contract (the trust glue) + +The failure this fixes: an agent reporting "all tests pass / done" from *assertion* rather +than a *captured, machine-checked* result. Make "done" mean evidence: + +- Each card carries a **gate command** (its `QUALITY_GATES`) — e.g. build + test + lint, or + `dotnet build -c Release && dotnet test`, etc. **No toolchain is assumed** — it's per-card. +- The Ralph worker MUST run the gate and capture `exit_code` + a log excerpt + the commit SHA. +- `kanban_complete` is only accepted with gate evidence showing **exit 0**: + + ```jsonc + kanban_complete({ + summary: "S-1.2 …", + metadata: { + gate: { cmd: "pnpm build && pnpm test && pnpm lint", + exit_code: 0, commit: "39a1102", log_excerpt: "…12/12 …" }, + branch: "story/1-2-…", changed_files: 50 + } + }) + ``` + +- Hermes **rejects** a completion lacking `gate.exit_code === 0` and re-queues / blocks the + card. No prose-only "done." (This is exactly the over-claim caught in early testing, where a + review asserted "pnpm test passes" with no captured run.) + +## E — Parallelism falls out for free + +Because Hermes already does **dynamic, per-story, dependency-aware** scheduling with +concurrency caps, pointing it at a pool of Ralph executors gives **fine-grained parallel +execution** with no static epic bin-packing and no end-of-run merge bottleneck — strictly +better than the current `--swarm` model. A card becomes eligible the instant its deps finish +and any free Ralph worker takes it. + +## Cost note + +Use **Claude Code on a Max subscription** as the executor's driver: you get **Opus + 1M +context at flat marginal cost**. This strictly dominates "run Hermes against the Anthropic API +directly" (metered $) and beats the Copilot path (which caps context and, per the model +catalogue, tops out at `claude-opus-4.7` with no 1M window). + +## Contracts / interfaces + +**Hermes → Ralph (per card):** `{ card_id, story_spec (body), worktree_path, branch, +gate_command }` via env + the generated `@fix_plan`. + +**Ralph → Hermes:** incremental **heartbeats** (from the `LIVE_OUTPUT` stream) keyed by +`card_id`, then one terminal `kanban_complete{gate evidence}` **or** `kanban_block{reason}`. + +**Shared git convention:** both already use `/.worktrees/` on `story/` — +standardise on it so a card hands off cleanly between Hermes (schedule/provision) and Ralph +(execute), and Hermes can review/merge the branch afterward. + +## Phased implementation + +1. **Per-card Ralph spawn_fn** — Hermes runs one claimed card through a single-item Ralph loop + (claude-code driver). Prove a card completes via Ralph end-to-end. *(Smallest viable slice.)* +2. **Honest-gate contract** — enforce `gate.exit_code === 0` in `kanban_complete`; block + otherwise. *(Fixes the trust gap; model-independent.)* +3. **Stream → board heartbeats** — feed the `LIVE_OUTPUT` stream back as card events for the + durable fleet view. *(One-directional, keyed by `card_id` — NOT a second board.)* +4. **DAG-driven fleet** — let the dispatcher run N parallel Ralph workers off the DAG, replacing + `--swarm`. *(Parallelism + observability + Opus quality, together.)* + +## Open questions / risks + +- **Session continuity across retries**: Ralph's `--resume` vs Hermes's per-attempt re-spawn — + decide whether a retried card resumes the Ralph session or starts fresh. +- **Gate command provenance**: per-card vs per-project default; how `bmalph implement` / the + bridge derives it. +- **`.ralph/` placement**: per-worktree (isolated) vs a shared template injected at spawn. +- **Backpressure**: align Ralph's `MAX_CALLS_PER_HOUR` with Hermes concurrency caps and the + Max-plan rate limits so a fleet doesn't stampede. + +--- + +_Prereqs already in place that this builds on: the `LIVE_OUTPUT` headless streaming + worker +process-tree reaping (PR #1), and the language-agnostic build/compile gate nudge._