Live CI for just · humans + agents

CI you can attach to.

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.

odu — live run
$ nix run github:juspay/odu -- run --host x86_64-linux=localhost
attach · mcp · rerun — same live state
Batch

Compile the graph, run once, scrape logs. Mid-run you poll a process; agents get a wall of text.

odu

The runner owns live state. attach for humans, mcp for agents — same run, fail-fast and rerun only what broke.

Justfile

One [metadata("ci")] tag. No second YAML describing the graph you already wrote.

Recorded from real runs

One state. Two readers.

Not a staged dashboard. The terminal and the agent are thin adapters over the same live pipeline.

Human faceodu run / odu attach — one live pipeline, late attach with buffer replay.
Agent faceodu mcp — agent drives the same state while the terminal watches.

What you actually get

Usefulness, not feature chips.

Moments that batch CI makes painful — and what odu does instead.

Human

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.

Agent

Stop on first red, not after 20 minutes

wait_for_settle returns the instant a node fails. Read that log, fix the source, node_rerun only the failed node and its dependents.

Human

Retry a flake without restarting CI

Keep the coordinator with --linger, hit rerun on the flaky node. No full pipeline restart for one bad leaf.

Team

Linux + macOS in one command

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

Runs from your machine. Executes anywhere.

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.

your machineodu runcoordinator · holds live state · posts GitHub statuses
x86_64-linux
ci-1 leasedci-2localhost
aarch64-darwin
mac-mini leased

Point a platform at a pool

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.

One run per machine

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.

localhost is opt-in

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

The fail-fast loop.

odu mcp exposes the live run over MCP. Structured state in, targeted fixes out — no scraping a terminal.

01
runStart the coordinator in the background.
02
wait_for_settleReturn on settlement or the first red node — don’t wait out a doomed build.
03
inspectSubscribe to that node’s log resource and read the failure.
04
fix → node_rerunPatch source, then reset only the failed node and its dependents.

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 -- mcp

Three steps

Keep your justfile.

No CI-flavored YAML. Tag one recipe; its dependency closure is the pipeline. Hosts are always explicit — never a silent localhost surprise.

ci/mod.justodu’s own CI — this reposource ↗
# 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; \
      }'
Exactly one recipe carries [metadata("ci")]. Everything reachable from it is the pipeline — here typecheck, unit, fmt, nix, and e2e.
01

Tag the root

Add [metadata("ci")] to one recipe. That dependency closure is what odu runs.

02

Run a lane

$ nix run github:juspay/odu -- run--host x86_64-linux=localhost typecheck unit e2e
03

Attach or agent

# another terminal$ odu attach# or let an agent drive$ odu mcp

Shape

Batch job vs live service.

Typical local CIodu
Mid-runLogs / process pollLive attach + typed snapshot
Agent faceScraped terminal / ad-hoc scriptsMCP tools + resources, fail-fast wait
RetryRestart the whole graphRerun node + dependents only
Pipeline configOften a second graph formatYour just DAG is the pipeline
Remote hostsAgents / daemons / portsssh + Nix closure, no preinstall

Already have a justfile?

Tag one recipe, pick a host, attach. Full reference lives in the docs.