One local service.
One Surface.
Every client.
You watch CI in the browser.
Agents drive the same runs through CLI or MCP.
This replaces v0.1’s run --agent architecture and the proposed REST API. Examples below specify future behavior.
Daemon remains after the agent exits.
| Project / branch | Run / SHA | State | Attention |
|---|---|---|---|
| app / fix | r01 · a1b2c3d | running | unit failed · e2e running |
| website / refresh | r02 · d4e5f6a | queueing | waiting for Darwin capacity |
| library / cleanup | r03 · b7c8d9e | passed | full pipeline green |
All registered runs across repositories and worktrees. Conventional CLI launches register even before web starts; older per-checkout history has an explicit import path.
The API is a Surface contract.
Odu defines CI semantics once.
Framework adapters expose them.
Typed live values over the framework WebSocket.
Generated verbs and readers; an Odu endpoint resolver dials the web daemon.
Same verbs as tools; reads as resources. HTTP transport at /mcp.
127.0.0.1:18440 serves the app, framework WebSocket, and MCP transportcheckout A · immutable run identity
checkout B · independent lifecycle
checkout C · independent lifecycle
local / SSH workers · leases · shards · logs · GitHub statusesPackage responsibilities · checked against actual source
| Package | Reuse | Odu supplies |
|---|---|---|
@kolu/surface | Effect RPC contracts, implementation, projection, transport clients, reactive subscriptions | Service schemas and handlers; bounded registry views |
@kolu/surface-app | HTTP shell + WebSocket serving, origin gate, heartbeat, stale-tab/reconnect lifecycle | Dashboard, loopback port, allowed origins, fault UI |
@kolu/surface-mcp | Tools/resources, schema projection, default-deny exposure, injectable MCP transport | Shared verb metadata; HTTP transport and request/session lifecycle integration |
@kolu/surface-cli | Verbs + get/keys/watch, JSON output, help, declared exit discipline | Endpoint resolver, concise examples, client-side forwarding only |
@kolu/surface-daemon | Singleton gate, control identity/drain, lifetime, daemon entry | Daemon home, fixed HTTP listener, registry recovery |
@kolu/surface-daemon-supervisor | Convergence, identity checks, survivable spawn, controlled recycle | Reuse/upgrade policy, environment, binary identity |
@kolu/surface-remote | Existing worker sessions, provisioning and reconnect | Existing coordinator scheduling and lease policy |
Current source is Effect RPC. The local consumer guide still mentions oRPC in places; implementation follows the checked source. No new REST routing, custom browser SSE bus, or second retry engine.
Transport choice · keep what is real separate from proposed integration
- Browser + generated CLI: dial the same daemon Surface over its framework WebSocket. Derive its route through the framework helper. CLI
watchis valid on this streaming endpoint. - HTTP agents: MCP
tools/callandresources/readat/mcp.surface-mcpaccepts an injected transport; mounting Streamable HTTP on Odu’s listener is integration work, not an existing one-call HTTP helper. - Stdio MCP hosts:
odu mcpbecomes a connection/projection bridge to this daemon. It owns no run lifecycle or independent command implementation. - CLI over MCP/HTTP also works in principle: Olai already has this pattern. A request/response endpoint must declare
streaming:false; do not advertise watch/follow until the endpoint actually supports subscriptions. - The initial CLI uses the framework WebSocket to keep live readers. Request/response HTTP clients use bounded
run_waitcalls; attention delivery does not require MCP resource notifications. - HTTP/WebSocket/MCP are protocol faces of the same service, not a chain in which the browser has to call MCP.
Seven packages. Three processes.
Split by authority, dependency cost,
and runtime. File count is irrelevant.
packages/ run-client/ live coordinator contract + dial run-history/ durable run / attempt / attention execution/ coordinator + worker implementation service-client/ service contract + verbs + dial service/ registry projection + orchestration web-ui/ browser application cli/ native + generated CLI / MCP bridge src/ main.ts binary composition root runner-main.ts remote worker composition root nix/ build and hydrate package graph website/ public docs; separate from web-ui
execution ───────→ run-history ──→ run-client
└───────────────────────────→ run-client
service ─────────→ service-client ─→ run-client/surface
├────────────→ run-history
└────────────→ run-client
web-ui ──────────→ service-client/surface
cli ─────────────→ service-client
├────────────→ run-client
└────────────→ run-history [native compatibility]
composition root → cli + service + executionService launches execution through an injected process port; it does not import the engine. Web UI reaches the service through Surface. UI assets are build outputs served by the root, not server-side UI imports.
| Package | Owns / public boundary | Why a package |
|---|---|---|
@odu/run-clientexisting | Coordinator wire, node vocabulary, local socket dial. Preserve current exports. | Existing lean downstream client; never pays for history, MCP, TUI, service or engine. |
@odu/run-history | Run/attempt IDs, record schema, append/finalize log and event operations, historical reads, attention replay. Exports /schema and /store. | Coordinator writes; CLI and service read. Neither must import the other's runtime to read evidence. |
@odu/execution | Strict snapshot, just DAG, lanes, shards, leases, remote worker protocol, log barrier, verdict and GitHub posting. Entry factories for coordinator / worker. | One run owns execution; reusable by standalone and service launches. No UI or service dependence. |
@odu/service-client | /surface schemas; /verbs shared metadata and forwarding descriptors; /dial endpoint connection. | Browsers, CLI and MCP need the service vocabulary without service implementation dependencies. |
@odu/service | Multi-run registry projection, launch/retry idempotency, service Surface handlers, MCP HTTP integration and recovery orchestration. | Cross-run authority; has no scheduling, log-verdict or GitHub posting implementation. |
@odu/web-ui | Runs board, run detail, live logs, create/retry/cancel controls, connection and refusal UI. | Browser-only dependency closure and static build. Uses Surface hooks; no filesystem or server imports. |
@odu/cli | Generated surface commands, stdio MCP bridge, bootstrap/upgrade commands, existing CLI/TUI presentation. | Process I/O and interaction. No scheduler, durable-store writer or alternate service handlers. |
Composition roots · who constructs and closes what
| Process | Root composes | Teardown boundary |
|---|---|---|
| odu client | CLI tree + shared verbs + endpoint resolver + supervisor policy; native launch port where needed | Closes its connections and observation only; explicit domain cancel is a separate request |
| odu web daemon | Daemon gate/control + RunHistory reader + service runtime + process-launch port + surface-app listener + UI assets + MCP transport | Closes web/RPC clients and service runtime. Independently owned coordinators remain live; restart reattaches. |
| run coordinator | Execution runtime + writable run history + display/event sink + local/SSH lane sessions | Owns only its run: settle/finalize, status debt, worker teardown and lease release. |
| remote worker | Execution worker entry + private lane Surface over stdio | Runs assigned recipes; reports observations. Never serves web/MCP or owns the global registry. |
Three local process roles; remote workers are a separate deployment of the execution package. The root owns runtime done/close and scoped lifetime. Presentation uses a supplied sink; extracting the engine must remove its current imports of CLI rendering.
State ownership · the registry is a view, not a second run database
| Fact | Single writer | Readers |
|---|---|---|
| Accepted service request + deduplication receipt | Service | All service clients; startup recovery |
| Run identity, snapshot, node attempts, logs, execution events, terminal outcome, posting debt | That run's coordinator | Service, native CLI, live/durable client projections |
| Multi-run board / attention summary | Derived by service from coordinator state and durable history | Browser, CLI, MCP |
| Delivered cursor / focused run | Each client | That client; does not clear shared failures |
| Proven process death with unfinished record | Recovery authority after ownership verification | Readers see incomplete/dead; never a fabricated failed test or pass |
user state / odu /
runs / RUN_ID /
manifest.json # coordinator identity + checkout + endpoint
events # durable sequence + attempt identity
attempts / NODE_KEY / N / log
service /
requests / REQUEST_ID # request → launch/retry receipt
checkout / .ci / … # compatibility paths / aliases
The canonical host-local run catalog exists independently of the web service. Every coordinator registers before executing, including a conventional CLI launch while web is absent. The daemon derives its board from this catalog at startup, then reconciles live coordinators. This makes “all in-flight runs” concrete without filesystem scans of arbitrary checkouts.
Versioned schemas, encoded node keys, atomic publication, and guarded ownership live in RunHistory. One run's writer cannot overwrite another attempt. Service request persistence and execution acceptance must be reconciled by the same launch ID so a crash between spawn and receipt cannot duplicate work.
Historical per-checkout records remain readable through an explicit legacy importer; source checkout removal does not erase new canonical evidence. Pruning is run-addressed and reports expiry.
Higher-level ports and invariants
- Service → execution:
RunLaunchertakes an immutable launch request and returns an addressed receipt. The root binds it to the packaged coordinator via the framework spawn driver. Legacy CLI and web launch through the same entry. - Live retry: service calls the existing coordinator Surface, which owns node reset and dependencies. Finalized retry uses recorded inputs to launch a new selection. There is one retry policy shared by native and service paths.
- Attention: one run-level reducer/journal query in RunHistory, used by service wait and native historical wait. Transport cancellation and deadlines wrap it; neither reimplements CI classification.
- Faces: service-client holds schemas/verb metadata, service binds implementation, CLI binds forwarding. No server closures leak into the shared verb table.
- No general-purpose “core” or “utils” package. Private worker protocol stays in execution. Service-private request storage stays in service. Helpers move only when an actual boundary requires them.
- Dependency tests: client → implementation imports are forbidden; browser export closures reject Node APIs and server dependencies; execution rejects presentation imports; root-to-package wiring is tested through real entrypoints.
Migration map · current source → owning package
| Current location | Destination / treatment |
|---|---|
packages/run-client | Keep its role and public compatibility; do not expand it into a service SDK. |
src/coordinator, src/runner, src/just, private lane protocol | Execution, separating durable storage into RunHistory and CLI display from domain state. |
src/common/runRecord, coordinator ledger / log persistence | RunHistory; migrate existing consumers in the same PR, with legacy record reads. |
src/mcp/*Tool business logic | Run-level operations go to execution/history; cross-run orchestration goes to service. MCP becomes forwarding/projection. |
src/mcp/agentSurface | Run-scoped data becomes service projections with explicit IDs; reusable wire schemas go to service-client. |
src/cli + TUI | CLI package; original commands stay complete while generated service commands are added. |
| New daemon, UI, Nix packaging | Root composes service/execution/CLI; web-ui builds assets; Nix bundles the graph. Website remains documentation. |
Declare what a run means.
Run IDs are global within the service.
No resource depends on the MCP server’s cwd.
cells
service identity, build, readiness
collections
runs[runId] live + finalized summaries
logTails[logKey] bounded, run/attempt-keyed
streams
nodes({runId}) initial snapshot → updates
procedures
run.start checkout + expected SHA → receipt
run.wait runId + after + timeoutMs → attention
run.retry runId + selectors → retry receipt
run.cancel runId + optional selectors
log.read logKey + offset + bytes → page| Shared name | Client operation |
|---|---|
run_start | Create from explicit checkout; idempotent request ID |
run_wait | Return new red with evidence, settlement, or deadline |
run_retry | Live attempt or linked retry from recorded inputs |
run_cancel | Explicit run/node/lane cancellation |
log_read | Read immutable attempt bytes after teardown |
One typed schema + one shared exposure/verb table. Any described convenience verbs only forward to service procedures. Execution, waiting, and retry decisions stay in the daemon/coordinator.
run.configure, worker leases, and private control verbs stay behind the appropriate boundary.Change the client. Keep the meaning.
Select a client and an outcome.
The domain response remains the same.
Important correction to v0.1 · preserve Surface CLI exits
| Exit | Surface meaning | CI interpretation |
|---|---|---|
| 0 | Call completed successfully | Read reason, settled, passed, scope and SHA |
| 1 | Declared domain refusal | Wrong run, unavailable snapshot, invalid host pool… |
| 2 | Local usage/input error | Fix arguments; request was not dispatched |
| 3 | Endpoint unavailable / transport failed | Ensure daemon, then reconnect; reconcile mutations by request ID |
| 130 | Client interrupted | Observer stops; run continues |
A successful run_wait can report failed CI and exit 0. MCP likewise returns a normal tool result for CI red; only a declared refusal is a tool error. Remove v0.1’s bespoke exit-10 deadline scheme. The skill branches on typed outcomes.
Fix while the rest keeps running.
Live subscriptions are a framework feature.
Durable replay is Odu’s responsibility.
Receipt r01 / c0.
Agent calls run_wait.
Return red + diagnostic.
e2e keeps running.
Resume after c17.
Return unseen c21.
Old red remains red.
Choose recovery; stop waiting.
run_wait defaults to a 30s observation deadline. Deadline → still_running, never a fabricated pass or cancellation. Independent concurrent callers own independent cursors.
Attention invariants
- Persist ordered actionable events and read state at a consistent journal position. Framework reconnect alone does not recover events lost during disconnect.
- Cursor consumption suppresses repeated delivery, not unresolved failure. New attempts and late failures have new event identity.
- Return only included events before advancing the cursor. Bounded pages expose total counts and
has_more. - A settled run always returns its terminal verdict. Do not spin on a known terminal response.
- Wrong-run or expired cursors produce typed refusal and a resync route. Link loss is not coordinator death.
- A completion notification depends on the agent host. Use a finite blocking wait; do not substitute polling just because the transport is HTTP.
Address evidence by identity.
Run + node + attempt.
Readable from every client after teardown.
Failure carries the diagnosis
Node, attempt, exit/signal, log key, completeness, and excerpt. Prefer reporter evidence; label tail fallback.
One bounded response
16 KiB default domain response; up to 4 KiB per excerpt. Measure encoded bytes, page excess. Transport envelopes are additional.
Evidence survives retries
Immutable run/attempt artifacts. Active records stay retained; finalized retention defaults to 30 days, with explicit expired-artifact errors.
Logs contract
Reuse the current log-finalization barrier. Publish red with the completed log, or explicit truncation if the producer is lost. Never infer “flake” from missing diagnostics. Sharded failures retain slice, attempt, placement, and logical recipe. logTails is a bounded live resource; log.read pages durable bytes. Composite keys use framework encoding, never guessed filenames.
The server owns the retry.
Retrying a snapshot and testing an edit
remain explicit, different requests.
New node attempt
Reset node + affected dependants. Preserve independent sibling execution.
Linked selection run
Reacquire capacity; replay required closure from recorded inputs. Parent remains failed.
New full run
run_start with new expected SHA. Explicit supersede cancels old work.
Recovery invariants
- Request ID deduplicates mutations. Retry receipt identifies effective run, attempts, selected roots, reset dependants, scope, SHA, and cursor. A stale expected attempt refuses.
- Choose live vs finalized retry atomically against settlement. A dropped transport means reconcile receipt, not blindly repeat a mutation.
- Shards preserve recorded partition identity and validate dependencies. Unavailable inputs require explicit broader recovery; no silent current-HEAD substitution.
- A passing selection is not a full-pipeline pass. GitHub reporting debt remains separate from test outcome.
- Completion releases leases. Durable recovery avoids keeping an idle fleet alive only to preserve
rerun.
Ensure, reuse, recover.
Use the framework’s singleton and spawn
mechanisms for the service and run owners.
Fixed port 18440; per-user daemon home and control socket. Gate verifies ownership; readiness includes successful HTTP bind. Concurrent launchers converge. An occupied unrelated port is a refusal.
The control socket is internal lifecycle infrastructure. User and agent interaction uses the local web service.
RunHistory owns the run catalog and evidence; service owns request receipts and projects the registry. Restart redials surviving coordinators and reconciles terminal records.
Use survivable spawn for run owners too. A detached child inside the web service’s cgroup is not enough to survive service teardown.
Version and survival policy
- The skill launches
nix run github:juspay/odu, with no consumer-project pin. Record the actual daemon/coordinator build and tested source SHA on each run. - Reuse a compatible daemon; build mismatch reports the active revision. Do not auto-recycle active CI. Explicit upgrade uses framework capture/drain/reattach, restoring the registry and leaving independently owned runs alive.
- The checked
survivableSpawnDriveralready usessystemd-run --userwhen launched under a systemd service, and detached spawn otherwise. Configure its environment and absolute binary correctly; test the actual host branches. - Reboot and unavailable workers remain real failure/recovery cases. A lost observer is not a lost run; lost process ownership is not a clean settlement.
- Share one runtime/handler authority across control socket and browser/MCP transport. Compose control and service surfaces as framework siblings; do not add a competing identity protocol.
Refuse with the next valid move.
All faces see the same declared error.
Provisioning stays visible in the dashboard.
| State / refusal | Response contains | Next move |
|---|---|---|
| invalid host pool | Config source, platform, allowed scoped alternatives | Explicit per-run host; no global edit |
| checkout already running | Existing run ID and tested SHA | Observe it or explicitly supersede |
| waiting for capacity | Accepted run ID, phase, lease/queue state | Wait on that run; no process polling |
| expected SHA differs | Requested and observed SHA | Resolve checkout state; never test a silent substitute |
| daemon/port unavailable | Identity or bind error | Ensure or repair service; never kill by process-name guess |
Validate before acceptance. Reserve run identity before slow provisioning. Recovery actions are structured inputs/argv, not strings to eval. Loopback + framework origin gate + explicit MCP access policy apply to mutating faces.
The skill teaches one loop.
CLI and MCP share schemas, verb names,
descriptions, defaults, and refusals.
nix run github:juspay/odu -- web
nix run github:juspay/odu -- surface run_start \
--input '{"checkout":"/code/app","expectedSha":"…",
"requestId":"ci-1"}' --json
nix run github:juspay/odu -- surface run_wait \
--input '{"runId":"r01","after":"c0"}' --json
# Read typed result → fix/retry → resume.
# Browser: http://127.0.0.1:18440| Typed answer | Agent action |
|---|---|
| failure | Use included evidence; fix or retry; resume cursor |
| still_running | Issue the next bounded wait; host may notify on completion |
| settled + passed | Verify SHA, scope, posting debt; finish |
| settled + failed | Recover; stop waiting on that attempt |
| declared refusal | Follow structured scoped recovery |
Mount under odu surface to preserve existing CLI names and avoid reserved get/keys/watch/list collisions. Conventional commands can migrate to thin clients without changing the domain core.
Generated readers and MCP resources
odu surface list odu surface keys runs --json # get/watch input grammar is generated from the declared key schema. # Help shows exact composite-key inputs; agents do not construct URI paths.
MCP resources and CLI readers expose the same run-addressed data. Browser reads use Surface hooks. The current cwd-bound MCP resource limitation disappears with service-level run keys.
Every merge stands on its own.
Two PRs recommended.
One cohesive PR is equally valid.
PR 1 → Durable execution
Standalone agent task · packages · invariants · acceptance gates
phases/1.md ↗PR 2 → Complete web service
Standalone agent task · all Surface clients · lifecycle · acceptance gates
phases/2.md ↗What “self-contained” means at each merge
- Every exposed command, tool, resource and control works end-to-end in that PR, with packaging, help/skill changes, migration and tests included.
- No public flags that parse but fail as “coming later”; no startup service with a placeholder board; no browser retry button waiting on a later engine change.
- No CLI or MCP implementation shipped after an already-advertised “one service, every client” release. PR 2 includes browser + generated CLI + HTTP MCP + stdio bridge together.
- PR 1 is released as durable per-run execution/history. It neither advertises nor exposes the future web service. It remains a complete improvement if PR 2 never ships.
- PR 2 builds on merged PR 1, not an unmerged companion branch or consumer pin. There are no runtime “PR 2 required” paths in PR 1.
- A framework extension, if genuinely needed, is a separate architectural decision requiring its own complete working consumer. It is not assumed as a deferred prerequisite in these PRs.
Cross-face acceptance gates · part of the PR that exposes each face
| Fixture | Required evidence |
|---|---|
| Same start/wait/retry through browser, CLI, MCP | Same identities, transitions, typed outcomes and declared refusals |
| unit fails at 8s; e2e ends at 90s | Wait returns finalized diagnosis before e2e settles; board agrees |
| Failure during disconnect / two independent agents | Replay without lost red or shared destructive acknowledgement |
| Deadline / CI failure / malformed input / wrong run | Domain outcomes and Surface CLI/MCP error contracts stay distinct |
| Simultaneous bootstrap / port occupied / daemon upgrade | One verified singleton or actionable refusal; surviving runs reattach |
| Native run launched before web starts | Board discovers it through the canonical run catalog, with exact identity |
| Service crash between launch and receipt | Request ID reconciles to one accepted execution, never duplicate work |
| Late log bytes / same-SHA rerun / deleted checkout | Completeness is honest; old attempt evidence remains accessible |
| Sharded retry / partial scope / unposted status | Partition and dependency correctness; no false full pass or hidden debt |
| MCP HTTP wait, cancellation, resource reads, teardown | Actual transport/session behavior verified, no imaginary push support |
| Browser and client package closure | No filesystem/engine dependency leaked into browser; no UI dependency in execution |
| New feature packaging on supported platforms | Nix entrypoints, worker closure, baked identity and client assets tested together |
Research basis · exact local framework revision inspected
Framework APIs checked at the Kolu revision currently recorded by this Odu checkout: f3ba6394b4ac667906190f2e5cda3b38ea871e47. This is an audit reference, not a proposed pin in the agent skill.
- Surface: Effect RPC contract
- surface-app: HTTP shell, WebSocket and lifecycle
- surface-mcp: shared verbs and injected transport
- surface-cli: endpoint seam, projection and exit codes
- surface-daemon: singleton gate and control fragment
- supervisor: survivable spawn implementation
Current Odu’s coordinator/run-client boundary remains the execution foundation. Selected session evidence from the earlier review motivates early attention, durable diagnostics and scoped recovery. It does not establish a population latency ranking.