Attach while e2e is still running
Lint already green, e2e still going. odu attach from another terminal: full matrix, focus a node, stream its log — including everything that ran before you connected.
Live CI for just · humans + agents
Batch CI turns your task graph into a process and leaves log files. odu keeps the pipeline alive as typed state — watch every run in the browser, attach from a terminal, or let a coding agent drive those same runs over MCP. Your just file is still the pipeline.
Compile the graph, run once, scrape logs. Mid-run you poll a process; agents get a wall of text.
One local service tracks every run. Open the web board, attach from a terminal, or use MCP — diagnose failures while other jobs continue.
One [metadata("ci")] tag. No second YAML describing the graph you already wrote.
Every repository, one board
Open http://127.0.0.1:18440 after starting a run. Select a run to inspect nodes, read an attempt’s logs, retry a failure or cancel work. The CLI and MCP use the same service and run IDs.
nix run github:juspay/odu -- webThis serves in the foreground. If a service is already running, open its URL; use web --upgrade to replace it in this terminal. Ctrl-C stops the web server; CI runs continue. Use web --backgroundwhen you explicitly want a daemon.
Recorded from real runs
The web board shows runs across repositories. The terminal and agent read and control those same runs. These earlier recordings show the workflow; their command and tool names predate the shared service.
odu run / odu attach — one live pipeline, late attach with buffer replay.odu mcp — agent drives the same state while the terminal watches.What you actually get
Moments that batch CI makes painful — and what odu does instead.
Lint already green, e2e still going. odu attach from another terminal: full matrix, focus a node, stream its log — including everything that ran before you connected.
run_wait reports failures before the run settles. Use log_read to diagnose them. Retry unchanged inputs with run_retry; start a new run for a source fix.
Choose Retry on the failed node. A live run gets a new attempt; a finalized run gets a linked replay of the selected scope.
A bare odu run covers every configured platform and posts a GitHub commit status per recipe@platform — a green matrix on your own machines, no hosted runner rented.
Local coordinator · remote lanes
The shared service runs locally. Each run’s coordinator lives on your machine — a bare odu run fans out across every configured platform at once, delegating each lane to a host over plain ssh. The runner travels as a Nix closure, so hosts need only ssh, Nix, and outbound HTTPS. Nothing preinstalled.
List several machines for a platform. odu leases a free one, locks it for the run, and releases when it settles — or when the holder dies.
The lock is an flock on the builder. Whole pool busy? Wait in line — it names who you're behind — or --no-wait to fail fast.
Never a silent fallback. This machine runs only when you name it: --host P=localhost, the sole lane, or an explicit pool member.
Coding agents
odu mcp exposes the live run over MCP. Structured state in, targeted fixes out — no scraping a terminal.
run_start → run_wait → log_read → run_retry. The unified Odu skill teaches this loop, including cursors, request IDs, cancellation and verifying the final result. MCP starts the service when needed.
Wire it with one stdio entry:
nix run github:juspay/odu -- mcpThree steps
No CI-flavored YAML. Tag one recipe; its dependency closure is the pipeline. Hosts are always explicit — never a silent localhost surprise.
# Odu CI: both e2e suites exercise the Nix-built application.
set working-directory := '..'
nix_shell := if env('IN_NIX_SHELL', '') != '' { '' } else { 'nix develop --accept-flake-config -c' }
# The default devshell has no browsers; use the e2e shell unless they are present.
nix_shell_e2e := if env('PLAYWRIGHT_BROWSERS_PATH', '') != '' { '' } else { 'nix develop .#e2e --accept-flake-config -c' }
[parallel]
[metadata("ci")]
default: typecheck unit fmt nix e2e-cli e2e-web bun-nix-fresh
install:
{{ nix_shell }} bun install --frozen-lockfile
{{ nix_shell }} sh -c 'sh scripts/hydrate-kolu-packages.sh \
"$ODU_KOLU_SURFACE" @kolu/surface \
"$ODU_KOLU_SURFACE_MCP" @kolu/surface-mcp \
"$ODU_KOLU_SURFACE_APP" @kolu/surface-app \
"$ODU_KOLU_SURFACE_CLI" @kolu/surface-cli \
"$ODU_KOLU_URL_SHAPE" @kolu/url-shape \
"$ODU_KOLU_SURFACE_REMOTE" @kolu/surface-remote \
"$ODU_KOLU_SHELL_QUOTE" @kolu/shell-quote \
"$ODU_KOLU_SURFACE_MAP" @kolu/surface-map \
"$ODU_KOLU_LOG" @kolu/log \
"$ODU_KOLU_SURFACE_DAEMON_SUPERVISOR" @kolu/surface-daemon-supervisor \
"$ODU_KOLU_SURFACE_DAEMON" @kolu/surface-daemon \
"$ODU_OSFACTS_CLIENT" osfacts-client'
typecheck: install
{{ nix_shell }} bun run typecheck
unit: install
{{ nix_shell }} bun run test:unit
# Build before e2e so a cold store cannot consume a test timeout.
e2e-cli: install nix
{{ nix_shell }} bun run test:e2e-cli
e2e-web: install nix
#!/usr/bin/env bash
set -euo pipefail
odu="$(nix build .#odu --no-link --print-out-paths --accept-flake-config)/bin/odu"
nix build .#odu-runner --no-link --accept-flake-config
cd packages/web-acceptance
ODU_BIN="$odu" {{ nix_shell_e2e }} bun run test
fmt:
{{ nix_shell }} nixpkgs-fmt --check *.nix nix/*.nix nix/packages/*.nix
nix:
nix build .#odu .#odu-runner .#web-ui --no-link
# Work around crates.io rejecting the fetchurl User-Agent on cold stores.
[private]
_prefetch-crates:
{{ nix_shell }} bash scripts/ci-prefetch-crates.sh .
# Verify bun.nix was regenerated after lockfile changes.
bun-nix-fresh: _prefetch-crates
{{ nix_shell }} sh -c '\
set -eu; \
tmp=$(mktemp -d); \
trap "rm -rf $tmp" EXIT; \
nix run .#bun2nix -- -l bun.lock -o "$tmp/bun.nix"; \
nixpkgs-fmt "$tmp/bun.nix" >/dev/null; \
diff -u bun.nix "$tmp/bun.nix" || { \
echo; \
echo "bun.nix is stale relative to bun.lock."; \
echo "Run: just regenerate-bun-nix && git add bun.nix"; \
exit 1; \
}'[metadata("ci")]. Everything reachable from it is the pipeline — here typecheck, unit, fmt, nix, and e2e.Add [metadata("ci")] to one recipe. That dependency closure is what odu runs.
This starts the local service automatically. Use your Nix system in the host pin; this example is x86_64 Linux.
Shape
| Typical local CI | odu | |
|---|---|---|
| Mid-run | Logs / process poll | Live attach + typed snapshot |
| Agent face | Scraped terminal / ad-hoc scripts | MCP tools + resources, fail-fast wait |
| Retry | Restart the whole graph | Rerun node + dependents only |
| Pipeline config | Often a second graph format | Your just DAG is the pipeline |
| Remote hosts | Agents / daemons / ports | ssh + Nix closure, no preinstall |
Tag one recipe, pick a host, attach. Full reference lives in the docs.