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 — attach from another terminal, or let a coding agent drive the same run 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.
The runner owns live state. attach for humans, mcp for agents — same run, fail-fast and rerun only what broke.
One [metadata("ci")] tag. No second YAML describing the graph you already wrote.
Recorded from real runs
Not a staged dashboard. The terminal and the agent are thin adapters over the same live pipeline.
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.
wait_for_settle returns the instant a node fails. Read that log, fix the source, node_rerun only the failed node and its dependents.
Keep the coordinator with --linger, hit rerun on the flaky node. No full pipeline restart for one bad leaf.
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
No CI service, no server-side YAML. The 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.
Tools: run, wait_for_settle, node_rerun, node_cancel, lane_cancel, cancel, runs. Pipeline state and logs are subscribable resources, not scraped text.
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's own CI pipeline — consumed by odu itself (`nix run .#odu -- run`).
# The runner discovers the recipe tagged [metadata("ci")], expands its
# reachable dependency subgraph, and runs each leaf via
# `just --no-deps <namepath>`.
set working-directory := '..'
nix_shell := if env('IN_NIX_SHELL', '') != '' { '' } else { 'nix develop --accept-flake-config -c' }
[parallel]
[metadata("ci")]
default: typecheck unit fmt nix e2e 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_REMOTE" @kolu/surface-remote \
"$ODU_KOLU_SHELL_QUOTE" @kolu/shell-quote \
"$ODU_KOLU_SURFACE_MAP" @kolu/surface-map \
"$ODU_KOLU_LOG" @kolu/log'
typecheck: install
{{ nix_shell }} bun run typecheck
unit: install
{{ nix_shell }} bun run test:unit
# Black-box e2e: builds the odu binary with nix and drives it against a
# throwaway fixture repo on a localhost lane (tests/e2e/README.md). Needs the
# dev shell on PATH (nix, git, just, bun) plus the node_modules from `install`.
e2e: install
{{ nix_shell }} bun run test:e2e
fmt:
{{ nix_shell }} nixpkgs-fmt --check *.nix nix/*.nix nix/packages/*.nix
nix:
nix build .#odu .#odu-runner --no-link
# bun.nix freshness — regenerate from bun.lock into a tmpdir and diff
# against the committed file. Fails the pipeline if bun.lock has moved
# without `just regenerate-bun-nix` being run.
bun-nix-fresh:
{{ 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.
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.