Design: Agent Dispatch
Context
Agent dispatch is where Stet's "you read, it writes" promise is kept: a compiled
brief (SPEC-0002) becomes a real pull request, produced by an AI agent running
locally in a sandbox. It realizes ADR-0003 (pluggable container runtime), ADR-0004
(pluggable harness adapter), and ADR-0005 (declarative YAML workflows), and it
closes the loop back to SPEC-0001's Forge (PR creation) and SPEC-0002's Tour rail
(narration) and pending-review reply path.
The design centers on three abstractions that must each stay swappable — runtime, harness, workflow — because all three sit on young, fast-moving ecosystems.
Goals / Non-Goals
Goals
- A
ContainerRuntimeprotocol with Applecontainerand Docker backends, neither privileged in code. - A fresh, isolated sandbox per dispatch: container/VM + worktree + branch-scoped, revocable credential.
- A
Harnessprotocol with OpenHands, Crush, and Pi adapters, all bring-your-own OpenAI-compatible model. - A lightweight local workflow runner executing YAML verb definitions (single- and multi-step).
- Branch push + PR creation with an attached narration artifact.
- A readable live transcript and lifecycle state, normalized across harnesses.
- Per-dispatch cost/usage accounting where available.
Non-Goals
- Durable/resumable workflow execution (a Temporal-engine strength) — out of scope per ADR-0005; dispatches are re-runnable, not checkpoint-resumable.
- Remote/cloud execution — the thesis is local (ADR-0003).
- Authoring verb-pack YAML in-app — v1 ships a curated default pack (SPEC-0002).
- The markup capture and brief compilation themselves — SPEC-0002.
Decisions
ContainerRuntime
Protocol surface: createSandbox(image, mounts), run(process, stdio),
teardown(). The Apple backend embeds the Containerization Swift package
in-process (per-container lightweight VM, sub-second start); the Docker backend
drives the Docker API/CLI. A RuntimeCapabilities probe detects availability and
picks a backend (default: native Apple on capable hardware, else Docker, else a
clear error). A base OCI image carries git plus the selected harness's runtime.
Sandbox lifecycle
DispatchSandbox composes a runtime sandbox with an isolated git worktree (created
via SwiftGit2 / cloned into the container) and a ScopedCredential limited to the
dispatch's branch. Credentials are minted per dispatch from the forge token
(SPEC-0001) with the narrowest scope the forge allows, injected into the sandbox as
short-lived environment/secret, and revoked on teardown. Concurrent dispatches get
fully separate sandboxes.
Harness adapter
Protocol: run(brief step, worktree, model endpoint) -> stream<TranscriptEvent> plus
a finalize that yields { branch, commits, narration }. Adapters normalize each
harness's invocation and IO:
- OpenHands — SDK/container image; default harness (most batteries-included).
- Crush — Go CLI; OpenAI/Anthropic-compatible; MCP + LSP.
- Pi — BYOK CLI; minimal Read/Write/Edit/Bash core; extensible.
Each receives
OPENAI_BASE_URL/OPENAI_API_KEYfrom user config. Transcript events are normalized to a commonTranscriptEventenum so the live view (below) is harness-agnostic.
Workflow runner
A WorkflowRunner loads YAML workflow definitions (shape borrowed from the user's
Temporal YAML library — steps, inputs, gating — but executed by a small in-process
runner, not Temporal). Each brief verb references a workflow id; the runner
sequences steps, each of which renders a templated instruction (from the brief item
- prior results) and calls the
Harness. Validation rejects malformed YAML with a located error. Multi-step example:refactor-ugly→ refactor, then add tests, then update docs, gated on tests passing.
PR creation + narration
On success, the runner commits (if the harness hasn't), pushes the branch via the
runtime/Forge, and calls Forge.createPullRequest, attaching the Narration
artifact (ADR-0008) the harness produced. If the brief targets an existing PR
(SPEC-0002 iteration), it commits to that branch and updates the PR instead.
Live transcript + accounting
A DispatchSession exposes lifecycle state (queued → running → openingPR → done | failed) and a stream of normalized transcript events rendered as a narrated feed.
Where the model endpoint reports token usage, a UsageMeter accumulates and
surfaces per-dispatch cost.
Risks / Trade-offs
- Two runtimes + three harnesses from day one is real surface area (ADR-0003/ 0004 accepted this for neutrality). Mitigation: keep protocols minimal; add adapters incrementally behind a stable core.
- Apple Containerization 1.0 edge cases (networking, entitlements) may need workarounds; the Docker backend is the escape hatch.
- Scoped credentials differ by forge. GitHub and Gitea expose different
scoping/expiry; the
ScopedCredentialabstraction must degrade to the narrowest each forge supports and document the gap. - Heterogeneous transcripts. Normalizing three harnesses' output to one event model is ongoing work; unknown events render as raw passthrough.
Migration / Rollout
Order: (1) ContainerRuntime + one backend (Apple on capable dev hardware);
(2) DispatchSandbox + worktree + scoped credential; (3) one Harness adapter
(OpenHands) end-to-end to a PR; (4) WorkflowRunner single-step, then multi-step;
(5) narration attachment + Tour handoff; (6) live transcript + lifecycle;
(7) second runtime (Docker); (8) Crush + Pi adapters; (9) cost accounting;
(10) follow-up-against-existing-PR path.
Open Questions
- Base image strategy per harness (prebuilt vs. built on first run) and caching.
- Exact
ScopedCredentialcapabilities achievable on GitHub vs. Gitea. - Shared
Brief/Narrationschema versioning with SPEC-0002. - Concurrency ceiling for simultaneous local dispatches given VM memory footprint.