PROPOSED · v0.4
6 September 2026 · revised architecture

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.

BOOTSTRAP FROM ANY CHECKOUTfixed loopback port
$ nix run github:juspay/odu -- web

Odu daemon ready / reused
http://127.0.0.1:18440

Ensure singleton → verify readiness → return.
Daemon remains after the agent exits.
127.0.0.1:18440 / runsMOCK DASHBOARD · click a row
Project / branchRun / SHAStateAttention
app / fixr01 · a1b2c3drunningunit failed · e2e running
website / refreshr02 · d4e5f6aqueueingwaiting for Darwin capacity
library / cleanupr03 · b7c8d9epassedfull pipeline green
unit / attempt 1 · expected 200, received 500Logs · Retry node · Cancel run

All registered runs across repositories and worktrees. Conventional CLI launches register even before web starts; older per-checkout history has an explicit import path.

01 / EVENTSWait for actionable change02 / LOGSRead a durable attempt03 / RECOVERYRetry the right snapshot04 / STARTUPReturn useful refusals05 / ADOPTIONOne shared verb table
ARCHITECTURE / USE THE FRAMEWORK

The API is a Surface contract.

Odu defines CI semantics once.
Framework adapters expose them.

Browsersurface-app + surface/solid

Typed live values over the framework WebSocket.

CLI agentsurface-cli

Generated verbs and readers; an Odu endpoint resolver dials the web daemon.

MCP / HTTP agentsurface-mcp

Same verbs as tools; reads as resources. HTTP transport at /mcp.

↓         ↓         ↓
odu web · service Surface runtimeRun registry · command handlers · durable attention journal · shared exposure / verb definitions127.0.0.1:18440 serves the app, framework WebSocket, and MCP transport
↓   @odu/run-client   ↓
Coordinator A

checkout A · immutable run identity

Coordinator B

checkout B · independent lifecycle

Coordinator C

checkout C · independent lifecycle

↓   existing lane Surface + surface-remote   ↓
local / SSH workers · leases · shards · logs · GitHub statuses
Package responsibilities · checked against actual source
PackageReuseOdu supplies
@kolu/surfaceEffect RPC contracts, implementation, projection, transport clients, reactive subscriptionsService schemas and handlers; bounded registry views
@kolu/surface-appHTTP shell + WebSocket serving, origin gate, heartbeat, stale-tab/reconnect lifecycleDashboard, loopback port, allowed origins, fault UI
@kolu/surface-mcpTools/resources, schema projection, default-deny exposure, injectable MCP transportShared verb metadata; HTTP transport and request/session lifecycle integration
@kolu/surface-cliVerbs + get/keys/watch, JSON output, help, declared exit disciplineEndpoint resolver, concise examples, client-side forwarding only
@kolu/surface-daemonSingleton gate, control identity/drain, lifetime, daemon entryDaemon home, fixed HTTP listener, registry recovery
@kolu/surface-daemon-supervisorConvergence, identity checks, survivable spawn, controlled recycleReuse/upgrade policy, environment, binary identity
@kolu/surface-remoteExisting worker sessions, provisioning and reconnectExisting 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 watch is valid on this streaming endpoint.
  • HTTP agents: MCP tools/call and resources/read at /mcp. surface-mcp accepts an injected transport; mounting Streamable HTTP on Odu’s listener is integration work, not an existing one-call HTTP helper.
  • Stdio MCP hosts: odu mcp becomes 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_wait calls; 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.
COMPOSITION / PACKAGES ARE DEPENDENCY BOUNDARIES

Seven packages. Three processes.

Split by authority, dependency cost,
and runtime. File count is irrelevant.

PROPOSED WORKSPACEone release / one Nix entry
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
IMPORT DIRECTION · A → B MEANS A DEPENDS ON B
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 + execution

Service 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.

PackageOwns / public boundaryWhy a package
@odu/run-client
existing
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-historyRun/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/executionStrict 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/serviceMulti-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-uiRuns 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/cliGenerated 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.
Package manifest cost matters. Keep the two client manifests lean; separate browser-safe exports from platform dials. Hydrate Kolu packages through the existing Nix convention with one Effect instance. Workspace packages ship together; no independent publishing or version machinery is introduced.
Composition roots · who constructs and closes what
ProcessRoot composesTeardown boundary
odu clientCLI tree + shared verbs + endpoint resolver + supervisor policy; native launch port where neededCloses its connections and observation only; explicit domain cancel is a separate request
odu web daemonDaemon gate/control + RunHistory reader + service runtime + process-launch port + surface-app listener + UI assets + MCP transportCloses web/RPC clients and service runtime. Independently owned coordinators remain live; restart reattaches.
run coordinatorExecution runtime + writable run history + display/event sink + local/SSH lane sessionsOwns only its run: settle/finalize, status debt, worker teardown and lease release.
remote workerExecution worker entry + private lane Surface over stdioRuns 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
FactSingle writerReaders
Accepted service request + deduplication receiptServiceAll service clients; startup recovery
Run identity, snapshot, node attempts, logs, execution events, terminal outcome, posting debtThat run's coordinatorService, native CLI, live/durable client projections
Multi-run board / attention summaryDerived by service from coordinator state and durable historyBrowser, CLI, MCP
Delivered cursor / focused runEach clientThat client; does not clear shared failures
Proven process death with unfinished recordRecovery authority after ownership verificationReaders 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: RunLauncher takes 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 locationDestination / treatment
packages/run-clientKeep its role and public compatibility; do not expand it into a service SDK.
src/coordinator, src/runner, src/just, private lane protocolExecution, separating durable storage into RunHistory and CLI display from domain state.
src/common/runRecord, coordinator ledger / log persistenceRunHistory; migrate existing consumers in the same PR, with legacy record reads.
src/mcp/*Tool business logicRun-level operations go to execution/history; cross-run orchestration goes to service. MCP becomes forwarding/projection.
src/mcp/agentSurfaceRun-scoped data becomes service projections with explicit IDs; reusable wire schemas go to service-client.
src/cli + TUICLI package; original commands stay complete while generated service commands are added.
New daemon, UI, Nix packagingRoot composes service/execution/CLI; web-ui builds assets; Nix bundles the graph. Website remains documentation.
DOMAIN / ONE SERVICE SURFACE

Declare what a run means.

Run IDs are global within the service.
No resource depends on the MCP server’s cwd.

CONCEPTUAL SHAPEschema sketch, not copy-paste API
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 nameClient operation
run_startCreate from explicit checkout; idempotent request ID
run_waitReturn new red with evidence, settlement, or deadline
run_retryLive attempt or linked retry from recorded inputs
run_cancelExplicit run/node/lane cancellation
log_readRead 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.

Do not expose the worker surface wholesale. The service projects coordinator state into run-addressed views. Browser/MCP/CLI exposure is default-deny; internal run.configure, worker leases, and private control verbs stay behind the appropriate boundary.
INTERACTIVE / SAME CALL, THREE FACES

Change the client. Keep the meaning.

Select a client and an outcome.
The domain response remains the same.

SURFACE CLIproposed command
CLI exit 0 · call answered
DECODED DOMAIN RESPONSEfailure ≠ RPC failure
Important correction to v0.1 · preserve Surface CLI exits
ExitSurface meaningCI interpretation
0Call completed successfullyRead reason, settled, passed, scope and SHA
1Declared domain refusalWrong run, unavailable snapshot, invalid host pool…
2Local usage/input errorFix arguments; request was not dispatched
3Endpoint unavailable / transport failedEnsure daemon, then reconnect; reconcile mutations by request ID
130Client interruptedObserver 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.

01 / RESUMABLE ATTENTION

Fix while the rest keeps running.

Live subscriptions are a framework feature.
Durable replay is Odu’s responsibility.

t + 0srun_start

Receipt r01 / c0.
Agent calls run_wait.

t + 8s / c17Unit fails

Return red + diagnostic.
e2e keeps running.

t + 35s / c21e2e fails during edit

Resume after c17.
Return unseen c21.

t + 90s / c25Run settles failed

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.
02 / DURABLE LOGS

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.

03 / RECOVERY

The server owns the retry.

Retrying a snapshot and testing an edit
remain explicit, different requests.

LIVE RUN / SAME SHA

New node attempt

unit #1unit #2

Reset node + affected dependants. Preserve independent sibling execution.

FINALIZED RUN / SAME SHA

Linked selection run

r01 failedr04 selection

Reacquire capacity; replay required closure from recorded inputs. Parent remains failed.

NEW COMMIT

New full run

SHA ASHA B

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.
LIFECYCLE / THE DAEMON SPINE

Ensure, reuse, recover.

Use the framework’s singleton and spawn
mechanisms for the service and run owners.

odu webconverge(endpoint)ready URL

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.

web serviceindependent run owners

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 survivableSpawnDriver already uses systemd-run --user when 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.
04 / ACTIONABLE STARTUP

Refuse with the next valid move.

All faces see the same declared error.
Provisioning stays visible in the dashboard.

State / refusalResponse containsNext move
invalid host poolConfig source, platform, allowed scoped alternativesExplicit per-run host; no global edit
checkout already runningExisting run ID and tested SHAObserve it or explicitly supersede
waiting for capacityAccepted run ID, phase, lease/queue stateWait on that run; no process polling
expected SHA differsRequested and observed SHAResolve checkout state; never test a silent substitute
daemon/port unavailableIdentity or bind errorEnsure 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.

05 / ADOPTION

The skill teaches one loop.

CLI and MCP share schemas, verb names,
descriptions, defaults, and refusals.

PROPOSED CLI SKILLunversioned upstream entry
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 answerAgent action
failureUse included evidence; fix or retry; resume cursor
still_runningIssue the next bounded wait; host may notify on completion
settled + passedVerify SHA, scope, posting debt; finish
settled + failedRecover; stop waiting on that attempt
declared refusalFollow 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.

PR PLAN / ARCHITECTURAL RELEASE UNITS

Every merge stands on its own.

Two PRs recommended.
One cohesive PR is equally valid.

The boundary is execution authority → service orchestration. Each PR has an independently useful release and a complete consumer path. PR size is not a criterion. A third PR would currently split clients or defer recovery without adding a stronger architectural boundary, so it is not recommended.
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
FixtureRequired evidence
Same start/wait/retry through browser, CLI, MCPSame identities, transitions, typed outcomes and declared refusals
unit fails at 8s; e2e ends at 90sWait returns finalized diagnosis before e2e settles; board agrees
Failure during disconnect / two independent agentsReplay without lost red or shared destructive acknowledgement
Deadline / CI failure / malformed input / wrong runDomain outcomes and Surface CLI/MCP error contracts stay distinct
Simultaneous bootstrap / port occupied / daemon upgradeOne verified singleton or actionable refusal; surviving runs reattach
Native run launched before web startsBoard discovers it through the canonical run catalog, with exact identity
Service crash between launch and receiptRequest ID reconciles to one accepted execution, never duplicate work
Late log bytes / same-SHA rerun / deleted checkoutCompleteness is honest; old attempt evidence remains accessible
Sharded retry / partial scope / unposted statusPartition and dependency correctness; no false full pass or hidden debt
MCP HTTP wait, cancellation, resource reads, teardownActual transport/session behavior verified, no imaginary push support
Browser and client package closureNo filesystem/engine dependency leaked into browser; no UI dependency in execution
New feature packaging on supported platformsNix 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.

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.