Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
134 changes: 134 additions & 0 deletions docs/integrations/hermes-ralph-executor.md
Original file line number Diff line number Diff line change
@@ -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:
`<repo>/.worktrees/<story>` on `story/<kebab>`).
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 `<repo>/.worktrees/<story>` on `story/<kebab>` —
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._