Live CI for just · humans + agents

CI you can watch together.

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.

odu — live run
$ nix run github:juspay/odu -- run --host x86_64-linux=localhost --no-post
web · attach · MCP — same service
Batch

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

odu

One local service tracks every run. Open the web board, attach from a terminal, or use MCP — diagnose failures while other jobs continue.

Justfile

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

Every repository, one board

See the failure while the rest keeps running.

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

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

Web, agents and remote access →

Recorded from real runs

One service. Browser, terminal, agent.

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.

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

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.

Human

Retry a flake without restarting CI

Choose Retry on the failed node. A live run gets a new attempt; a finalized run gets a linked replay of the selected scope.

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.

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.

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
run_startStart a run through the shared service; keep its run ID and cursor.
02
run_waitReturn on settlement or the first red node — don’t wait out a doomed build.
03
log_readRead the log key returned with the failure, paging by byte offset.
04
Retry or start a new commitUse run_retry for unchanged inputs. Commit a source fix and run_start at the new SHA; verify scope and reporting debt.

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 -- 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 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; \
      }'
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

This starts the local service automatically. Use your Nix system in the host pin; this example is x86_64 Linux.

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

Open the board

# another terminalhttp://127.0.0.1:18440# or let an agent drive$ nix run github:juspay/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.