Skip to content

odu: a CI runner you attach to — replaces justci, then graduates - #1252

Merged
srid merged 34 commits into
masterfrom
odu
Jun 10, 2026
Merged

odu: a CI runner you attach to — replaces justci, then graduates#1252
srid merged 34 commits into
masterfrom
odu

Conversation

@srid

@srid srid commented Jun 9, 2026

Copy link
Copy Markdown
Member

kolu's CI is now run by odu (Tamil ஓடு — "run") — the live-service CI runner grown from the mini-ci example — replacing github.com/juspay/justci. This PR carries the whole Phase-1 arc of the accepted Atlas plan (mini-ci-vs-justci): odu was built here, dogfooded on this very PR, and then graduated to juspay/odu — so the final diff deletes the runner's source from kolu and consumes it upstream, the way remote-process-monitor became drishti. Same status contexts, same per-SHA log layout, same strict-mode flag table as justci, so branch protection and the /do muscle memory carry over unchanged — but the run is state you attach to, not a batch process you scrape.

Dogfooded on this PR throughout: the full pipeline (26/26 nodes, linux on leased pool box kolu-ci-5, darwin on rasam) went green with every required check posted by odu itself; the final head re-verifies the linux lane (e2e deliberately skipped — maintainer's call). The ## Evidence comment has a live odu monitor dashboard captured mid-run.

How a run works

odu run  (coordinator — one process, parented by ci/pu/run.sh's lease)
 ├─ strict gate: dirty-tree refuse · HEAD pin via git worktree
 ├─ just --dump → [metadata("ci")] DAG  (odu dump / graph to inspect)
 ├─ per platform lane (hosts.json):
 │    nix copy odu-runner.drv → realise on host → ssh host odu-runner --stdio
 │    └─ run.configure over the typed surface → host git-fetches the pushed
 │       SHA into a writable per-SHA workspace → just --no-deps <recipe> each
 ├─ fan-in: lane state cells merge → served on .ci/odu.sock
 │    (odu status / logs -f / monitor — a live TUI dashboard — attach here)
 ├─ .ci/<sha7>/<plat>/<recipe>.log + timings.jsonl  (durable past runner death)
 └─ gh: ci::<recipe>@<platform> statuses on transitions read from the cell

On a TTY, odu run paints a live colour recipes × lanes matrix — spinners, per-cell ticking elapsed times, a log-tail footer, the commit as a clickable OSC-8 link (@ 3cbac86, or @ 3cbac86+dirty in live-tree mode) — justci-UX mimicry deliberately dropped. Piped, it emits quiet transition lines with a once-a-minute heartbeat; --progress json is the byte-stable NDJSON contract /do consumes.

What kolu ends up with

kolu consumes via
the odu / odu-runner binaries npins pin of juspay/odu, re-exported through kolu's flakenix run .#odu, ci/pu/run.sh (lease machinery untouched), and the coordinator's runner-drv eval all unchanged; npins update odu to bump
the /ci skill juspay/odu as an apm dependency (juspay/odu#2) — the runner documents itself, like justci did
@kolu/surface libraries (inside odu) odu's own npins pin of kolu — two deliberate pins, never a build-time loop

The justci excision is total in live paths: apm.yml drops the dependency, .agency/do.md / ci/mod.just / the README CI section describe odu, the justci-era ralph reports carry historical notes, and the workflow rule justci incidentally shipped (Keep README.md in sync…) is re-authored repo-locally so it survives the dependency removal. odu's 40-test suite now runs in juspay/odu's own GitHub Actions (odu-on-odu, per push).

The road here (review + production)

Codex ⇄ Claude debate (consensus, 2 rounds): NUL-byte escapes, per-invocation worktree suffix, dirty-live-tree remote-lane guard, the timings.jsonl sidecar report.sh now prefers. Lowy ⇄ Hickey lens debate (consensus, 11 applied): one nodeId owner, one STATUS_META projection, shared log-tail store, dead ODU_RUNNER_DRV wire deleted. Code-police: 3 polish fixes. Production then caught what review couldn't: node's 'pipe' stdio is an AF_UNIX socketpair Linux can't open() by path (cucumber's pretty:/dev/stderr died where process-compose's real pipes worked → recipes now run behind a | cat interposition, regression-tested), and a pinned nix client older than the host daemon corrupts CA-derivation handling (the wrappers no longer pin nix — the host that provides the daemon provides the client).

Byte-parity, verified against live data

justci posted odu posts
ci::e2e@x86_64-linux contexts (+ _ci-setup@<plat>) identical strings
Running: / Succeeded (3m26s): / Failed (8s): + log path identical, incl. Go's 1m0s zero-pad
logs at .ci/<sha7>/<plat>/ci::e2e.log identical layout
skipped nodes: no post (absent context blocks merge) identical

odu adds one state justci never defined: infrastructure death posts error / Errored (<dur>) — a lane's ssh link dying mid-run fails the run instead of wedging it.

Deliberate divergences (documented in the odu README/skill): remote lanes git fetch the pushed SHA (no git-bundle transport); live-tree mode is localhost-only on a dirty tree; lanes are one-shot; dump replaces dump-yaml; --tui is absorbed by odu monitor; idle-attach and the (vacuous — contexts are byte-identical) protection flip are follow-ups. No website changelog entry — contributor tooling, invisible to kolu app users.

Try it

nix run github:juspay/odu -- --help          # the graduated tool itself
nix run github:juspay/kolu/odu#odu -- --help # kolu's re-export of the pin

Generated by /be on Claude Code (model claude-fable-5).

srid added 5 commits June 9, 2026 15:47
…or/CLI + tests

Phase 1 of the accepted Atlas plan (mini-ci-vs-justci): a fresh package on
@kolu/surface that ingests the just [metadata("ci")] DAG, fans lanes out per
platform over HostSession (drv copy → remote realise → odu-runner --stdio),
prepares writable per-SHA workspaces on lane hosts, posts justci-byte-
compatible commit statuses from the fan-in state cell, writes the per-SHA
log layout, and serves the run on .ci/odu.sock for in-band
status/logs/monitor. nix run .#odu works via the root composer.
Replaces the justci apm dependency with a repo-local .apm/skills/ci (the odu
reference) and re-authors the workflow instruction justci used to provide.
ci/pu/run.sh keeps its lease machinery byte-for-byte; only the wrapped
runner ref changes (KOLU_JUSTCI → KOLU_CI_RUNNER, default .#odu).
.agency/do.md's CI section, ci/mod.just's preamble, and the README CI
section now describe odu; the three justci-era ralph reports carry a
historical note.
CODEX raised 4 findings on the odu CI-runner branch; I agree with all four and fixed each in the working tree. Verified with `pnpm typecheck` (clean), `pnpm test:unit` (34/34 pass), `biome check` on touched files (clean), and end-to-end dry-runs of `ci/pu/report.sh` against both the new odu timing sidecar and the legacy pc.log fallback. F1: two literal NUL bytes in statuses.ts (which made git treat the .ts as binary and hid it from review) replaced with `\0` escapes — same NUL byte at runtime, file is now plain text (git --no-index now classifies it as 181 text lines). F2: per-run worktree path was `${sha7}-${pid}`, which collides on a second prepareWorkspace() in the same process (rerun(_ci-setup) / same-SHA retry) and makes `git worktree add` fail; added a random suffix so every call yields a fresh path, matching the function's advertised idempotence. F3: in `--no-strict`/`--no-snapshot` (live-tree) mode on a dirty tree, remote lanes silently fetched committed HEAD instead of the user's edits; now compute dirtiness unconditionally and reject non-local lanes in live mode on a dirty tree with a clear error, plus documented the localhost-only rule in the README. F4: `ci/pu/report.sh` parsed justci's `.ci/pc.log` for per-recipe timings, which odu never writes; odu now emits a `.ci/<sha7>/timings.jsonl` sidecar from its NodeState (node/recipe/platform/status/startedAt/durationMs/exitCode) and report.sh prefers it, falling back to pc.log only for replaying old justci runs. do.md prose updated to match. No disputes — CODEX was right on all four.

codex (round 1) findings:
- [F1 · major] This source file contains literal NUL bytes in the status dedupe key. Git now treats the TypeScript file as binary, so normal diffs/reviews hide the entire file; other text tooling can also misbehave. (packages/odu/src/coordinator/statuses.ts:103)
- [F2 · major] Remote workspace paths are only `${sha7}-${process.pid}`. A second `prepareWorkspace()` call for the same SHA in the same runner process, which is exactly what `rerun(_ci-setup)` does, reuses the existing directory and `git worktree add` fails. This breaks the advertised idempotence and setup reruns. (packages/odu/src/runner/workspace.ts:84)
- [F3 · major] `--no-strict` / `--no-snapshot` are documented as live-working-tree modes, but non-local lanes still receive `origin + HEAD sha` and fetch the committed SHA. On a dirty tree, remote lanes silently test stale HEAD instead of the user's changes, producing misleading dev-iteration results. (packages/odu/src/coordinator/run.ts:408)
- [F4 · minor] The migration leaves the reporting path pointed at justci/process-compose artifacts: `ci/pu/report.sh` still parses `.ci/pc.log`, but odu does not write that file. The instructed post-CI metrics comment will degrade to missing per-recipe timing and a zero lane wall. (.agency/do.md:37)

claude:
- F1 fixed: Confirmed two literal NUL bytes in packages/odu/src/coordinator/statuses.ts (byte 3493 line 103, byte 3899 line 112) — they were the dedupe-key delimiter (`${state}\0${description}`) and the `startsWith("pending\0")` check, and they made git mark the .ts as binary (numstat showed `-	-`, diff said "Binary files differ"). Replaced both literal NULs with `\0` string escapes, which the TS compiler emits as the identical NUL byte at runtime — so the dedupe behavior is byte-for-byte unchanged while the source is now plain text. Verified: no NUL bytes remain in the file (or anywhere under packages/odu/src), and `git diff --no-index` against an empty file now classifies it as 181 text lines instead of binary. Once committed (side `a` is still the old binary blob), the rendered diff will be plain text.
- F2 fixed: Confirmed the bug in packages/odu/src/runner/workspace.ts:84-88: workdir was `.../{slug}/{sha7}-{pid}`. rerun(SETUP_NODE_ID) (runner.ts:344 bumps setupGeneration, ticks, re-enters runSetup → prepareWorkspace) and same-SHA retries re-derive the identical path in the same process; `git worktree prune` (line ~130) only deregisters worktrees whose dir is *gone*, so the live dir survives and `git worktree add --detach` fails on the existing directory. This also contradicted the function's own docstring promising 'every call yields a new worktree'. Fixed by appending `randomBytes(4).toString('hex')` to the worktree name (imported node:crypto), so each invocation gets a brand-new path. The prior worktree leaks on rerun, but that is consistent with the documented 'tmpdir reaper owns the long tail' lifecycle for crash debris, and dispose() still cleans the final worktree on green. Typecheck + 34 tests pass.
- F3 fixed: Confirmed in packages/odu/src/coordinator/run.ts: when snapshotMode is false (--no-strict/--no-snapshot), specSource is the live tree and per-lane config is `workspace: local ? specSource : null`, `origin/sha` set for non-local lanes — so remote lanes fetch committed HEAD while local lanes test live edits, with no guard or warning. The README's 'Honest notes' documented pushed-SHAs-only but not this dirty-tree divergence. Fixed: dirty is now computed unconditionally in runCommand and threaded through RunContext; orchestrate's existing lane-validation loop now rejects non-local lanes when (!snapshotMode && dirty) with a clear error naming the platform, host, and the HEAD sha7 it would have tested, and suggesting --platform or commit+push. Added a matching 'Live-tree mode is localhost-only' bullet to packages/odu/README.md Honest notes. Typecheck + tests pass; no test asserted the old silent behavior.
- F4 fixed: Confirmed: ci/pu/report.sh's recipe_rows/lane_table scrape `.ci/pc.log` (process-compose's log = justci's transport) for per-recipe Started/Exited timestamps, but odu writes no pc.log — so the post-CI metrics comment degrades to '(no per-recipe timing)' and a 0s lane wall, and .agency/do.md line 37 still pointed report.sh at .ci/pc.log. Fixed both: (1) odu now writes a `.ci/<sha7>/timings.jsonl` sidecar in run.ts's verdict section from finalState (one JSON line per node: node/recipe/platform/status/startedAt/durationMs/exitCode), best-effort. (2) report.sh prefers that sidecar (resolved from PU_SHA via jq, emitting node/startMs/endMs/code), and falls back to pc.log only when the sidecar is absent (so old justci runs still report); the legacy awk parser is preserved (now epoch-converting in-awk to the same uniform ms shape). Footer and do.md prose updated to name the dynamic source. Verified end-to-end: dry-run against a synthetic sidecar renders the per-recipe table (ci::nix 3m0s/ci::biome 25s, ci::default root excluded), a correct 3m5s lane wall, and the sidecar path in the footer; the pc.log fallback also verified against a realistic process-last log line.

Committed by the codex<->claude debate (round 1); not pushed or merged.
@srid

srid commented Jun 9, 2026

Copy link
Copy Markdown
Member Author

Codex ⇄ Claude debate

Consensus after 2 round(s) · codex reviewed at xhigh reasoning effort · base 712a97649842

Round 1

codex — approved: false

The branch is a substantial odu CI runner addition with generally coherent architecture, but I found several issues that should be fixed before approval. I could not run the package typecheck/tests because pnpm is not available on this PATH, so this is based on read-only inspection plus lightweight git/node checks.

Findings:

  • F1 · major · open — This source file contains literal NUL bytes in the status dedupe key. Git now treats the TypeScript file as binary, so normal diffs/reviews hide the entire file; other text tooling can also misbehave. (packages/odu/src/coordinator/statuses.ts:103)
  • F2 · major · open — Remote workspace paths are only ${sha7}-${process.pid}. A second prepareWorkspace() call for the same SHA in the same runner process, which is exactly what rerun(_ci-setup) does, reuses the existing directory and git worktree add fails. This breaks the advertised idempotence and setup reruns. (packages/odu/src/runner/workspace.ts:84)
  • F3 · major · open — --no-strict / --no-snapshot are documented as live-working-tree modes, but non-local lanes still receive origin + HEAD sha and fetch the committed SHA. On a dirty tree, remote lanes silently test stale HEAD instead of the user's changes, producing misleading dev-iteration results. (packages/odu/src/coordinator/run.ts:408)
  • F4 · minor · open — The migration leaves the reporting path pointed at justci/process-compose artifacts: ci/pu/report.sh still parses .ci/pc.log, but odu does not write that file. The instructed post-CI metrics comment will degrade to missing per-recipe timing and a zero lane wall. (.agency/do.md:37)

claude — CODEX raised 4 findings on the odu CI-runner branch; I agree with all four and fixed each in the working tree. Verified with pnpm typecheck (clean), pnpm test:unit (34/34 pass), biome check on touched files (clean), and end-to-end dry-runs of ci/pu/report.sh against both the new odu timing sidecar and the legacy pc.log fallback. F1: two literal NUL bytes in statuses.ts (which made git treat the .ts as binary and hid it from review) replaced with \0 escapes — same NUL byte at runtime, file is now plain text (git --no-index now classifies it as 181 text lines). F2: per-run worktree path was ${sha7}-${pid}, which collides on a second prepareWorkspace() in the same process (rerun(_ci-setup) / same-SHA retry) and makes git worktree add fail; added a random suffix so every call yields a fresh path, matching the function's advertised idempotence. F3: in --no-strict/--no-snapshot (live-tree) mode on a dirty tree, remote lanes silently fetched committed HEAD instead of the user's edits; now compute dirtiness unconditionally and reject non-local lanes in live mode on a dirty tree with a clear error, plus documented the localhost-only rule in the README. F4: ci/pu/report.sh parsed justci's .ci/pc.log for per-recipe timings, which odu never writes; odu now emits a .ci/<sha7>/timings.jsonl sidecar from its NodeState (node/recipe/platform/status/startedAt/durationMs/exitCode) and report.sh prefers it, falling back to pc.log only for replaying old justci runs. do.md prose updated to match. No disputes — CODEX was right on all four.

  • F1 fixed — Confirmed two literal NUL bytes in packages/odu/src/coordinator/statuses.ts (byte 3493 line 103, byte 3899 line 112) — they were the dedupe-key delimiter (${state}\0${description}) and the startsWith("pending\0") check, and they made git mark the .ts as binary (numstat showed - -, diff said "Binary files differ"). Replaced both literal NULs with \0 string escapes, which the TS compiler emits as the identical NUL byte at runtime — so the dedupe behavior is byte-for-byte unchanged while the source is now plain text. Verified: no NUL bytes remain in the file (or anywhere under packages/odu/src), and git diff --no-index against an empty file now classifies it as 181 text lines instead of binary. Once committed (side a is still the old binary blob), the rendered diff will be plain text.
  • F2 fixed — Confirmed the bug in packages/odu/src/runner/workspace.ts:84-88: workdir was .../{slug}/{sha7}-{pid}. rerun(SETUP_NODE_ID) (runner.ts:344 bumps setupGeneration, ticks, re-enters runSetup → prepareWorkspace) and same-SHA retries re-derive the identical path in the same process; git worktree prune (line ~130) only deregisters worktrees whose dir is gone, so the live dir survives and git worktree add --detach fails on the existing directory. This also contradicted the function's own docstring promising 'every call yields a new worktree'. Fixed by appending randomBytes(4).toString('hex') to the worktree name (imported node:crypto), so each invocation gets a brand-new path. The prior worktree leaks on rerun, but that is consistent with the documented 'tmpdir reaper owns the long tail' lifecycle for crash debris, and dispose() still cleans the final worktree on green. Typecheck + 34 tests pass.
  • F3 fixed — Confirmed in packages/odu/src/coordinator/run.ts: when snapshotMode is false (--no-strict/--no-snapshot), specSource is the live tree and per-lane config is workspace: local ? specSource : null, origin/sha set for non-local lanes — so remote lanes fetch committed HEAD while local lanes test live edits, with no guard or warning. The README's 'Honest notes' documented pushed-SHAs-only but not this dirty-tree divergence. Fixed: dirty is now computed unconditionally in runCommand and threaded through RunContext; orchestrate's existing lane-validation loop now rejects non-local lanes when (!snapshotMode && dirty) with a clear error naming the platform, host, and the HEAD sha7 it would have tested, and suggesting --platform or commit+push. Added a matching 'Live-tree mode is localhost-only' bullet to packages/odu/README.md Honest notes. Typecheck + tests pass; no test asserted the old silent behavior.
  • F4 fixed — Confirmed: ci/pu/report.sh's recipe_rows/lane_table scrape .ci/pc.log (process-compose's log = justci's transport) for per-recipe Started/Exited timestamps, but odu writes no pc.log — so the post-CI metrics comment degrades to '(no per-recipe timing)' and a 0s lane wall, and .agency/do.md line 37 still pointed report.sh at .ci/pc.log. Fixed both: (1) odu now writes a .ci/<sha7>/timings.jsonl sidecar in run.ts's verdict section from finalState (one JSON line per node: node/recipe/platform/status/startedAt/durationMs/exitCode), best-effort. (2) report.sh prefers that sidecar (resolved from PU_SHA via jq, emitting node/startMs/endMs/code), and falls back to pc.log only when the sidecar is absent (so old justci runs still report); the legacy awk parser is preserved (now epoch-converting in-awk to the same uniform ms shape). Footer and do.md prose updated to name the dynamic source. Verified end-to-end: dry-run against a synthetic sidecar renders the per-recipe table (ci::nix 3m0s/ci::biome 25s, ci::default root excluded), a correct 3m5s lane wall, and the sidecar path in the footer; the pc.log fallback also verified against a realistic process-last log line.

commit: f068cc65908b82c8f6b3a8e8f66828d64a38e62d

Round 2

codex — approved: true

Verified the current tree only against the four existing findings. CLAUDE's fixes address all of them, and I did not find a new regression introduced by those fix patches.

Findings:

  • F1 · major · resolved — Literal NUL bytes in the TypeScript source made Git treat the file as binary, hiding source-level diffs and review context. (packages/odu/src/coordinator/statuses.ts:103)
  • F2 · major · resolved — Remote workspaces were keyed only by sha7 and process id, so a second prepareWorkspace() in the same runner process collided with the previous live worktree during _ci-setup rerun or same-SHA retry. (packages/odu/src/runner/workspace.ts:93)
  • F3 · major · resolved — In live-tree mode, local lanes used dirty working-tree contents while remote lanes fetched committed HEAD, silently testing different source contents. (packages/odu/src/coordinator/run.ts:219)
  • F4 · minor · resolved — The pool metrics reporter parsed justci's process-compose .ci/pc.log, which odu never writes, so odu runs lost per-recipe timing metrics. (ci/pu/report.sh:43)

codex on the rebuttal: No disputes to answer. CLAUDE agreed with all four findings; I verified the corresponding fixes in statuses.ts, workspace.ts, run.ts, packages/odu/README.md, ci/pu/report.sh, and .agency/do.md.

claude(no author turn this round)

(no author turn — the debate ended this round)

srid added 9 commits June 9, 2026 16:31
… no receptacle — its parse/compose logic is sprayed across 6 modules

Added packages/odu/src/common/nodeId.ts (fanId/splitFanId/onPlatform) as the receptacle for the <namepath>@<platform> fan-in node id, and routed all composition/decomposition sites in run.ts, statuses.ts, and protect.ts through it.

Agreed by the lowy ⇄ hickey lens debate (finding lowy-1, raised by lowy). Not pushed or merged.
… NodeStatus → external representation (PROGRESS_STATUS table vs statusFor switch)

Unified the NodeStatus→external-representation projection into one exhaustively-typed STATUS_META table in common/surface.ts that render.ts, run.ts, and statuses.ts all read.

Agreed by the lowy ⇄ hickey lens debate (finding lowy-2, raised by lowy). Not pushed or merged.
…dinator never plugs into — dead wire

Removed the dead ODU_RUNNER_DRV wire from the odu wrapper: deleted the --set-default line and rewrote the header comment to say every lane's drvPath is resolved at run time via nix eval.

Agreed by the lowy ⇄ hickey lens debate (finding lowy-3, raised by lowy). Not pushed or merged.
…g is an unstated invariant that the missing nodeId receptacle would enforce

Routed the coordinator rerun's bare-id (lane-local) case explicitly through splitFanId's "unknown" platform sentinel instead of relying on lanes.get("unknown") incidentally returning undefined, completing finding lowy-6.

Agreed by the lowy ⇄ hickey lens debate (finding lowy-6, raised by lowy). Not pushed or merged.
… duplicated verbatim across runner and coordinator

Extracted the duplicated per-node log tail (buffer + bus + lazy logFor + nodeLog source) into common/logTail.ts; runner and coordinator now compose createLogTail(), coordinator layering its file sink on top.

Agreed by the lowy ⇄ hickey lens debate (finding hickey-1, raised by hickey). Not pushed or merged.
…ending seed) is written out four times

Added pendingNode factory in common/surface.ts and replaced the four duplicated pending-NodeState literals in coordinator/run.ts and runner/runner.ts with calls to it.

Agreed by the lowy ⇄ hickey lens debate (finding hickey-3, raised by hickey). Not pushed or merged.
…ecial-casing one branch inside the generic onNodes projection

Split _ci-setup timing out of the generic onNodes mirror into a coordinator-owned finishSetup() helper, removing the per-id branch from the projection loop (fix hickey-7).

Agreed by the lowy ⇄ hickey lens debate (finding hickey-7, raised by hickey). Not pushed or merged.
…ane.ts purely as a pass-through

Removed the destroyAllSessions pass-through re-export from lane.ts; run.ts now imports it directly from @kolu/surface-nix-host alongside isLocalHost.

Agreed by the lowy ⇄ hickey lens debate (finding hickey-9, raised by hickey). Not pushed or merged.
…ored` though the runner never produces it

Added a one-line comment on the `errored` arm of blocked() in runner.ts documenting it as coordinator-only and unreachable in lane state, per agreed plan option (a).

Agreed by the lowy ⇄ hickey lens debate (finding hickey-10, raised by hickey). Not pushed or merged.
@srid

srid commented Jun 9, 2026

Copy link
Copy Markdown
Member Author

⚖️ Lowy ⇄ Hickey lens debate

Consensus after 1 round(s) · lowy + hickey · base 712a97649842

Independent findings: lowy=6, hickey=12

Applied (11)

  • lowy-1 The @ composite node-id has no receptacle — its parse/compose logic is sprayed across 6 modules — commit 4de7c9747
  • lowy-2 Two parallel encapsulations of the same axis: NodeStatus → external representation (PROGRESS_STATUS table vs statusFor switch) — commit 2b44d4563
  • lowy-3 ODU_RUNNER_DRV is a baked receptacle the coordinator never plugs into — dead wire — commit dd48aaa18
  • lowy-6 The at > 0 vs at >= 0 guard on @-splitting is an unstated invariant that the missing nodeId receptacle would enforce — commit b327658e4
  • hickey-1 Per-node log store (buffer + bus + logFor) is duplicated verbatim across runner and coordinator — commit 8d11c0f66
  • hickey-3 NodeState initial construction (the 6-field pending seed) is written out four times — commit 786a507d1
  • hickey-4 The <recipe>@<platform> node id is a fragmented compound key, split and rejoined by ad-hoc string surgery at ~10 sites — (uncommitted)
  • hickey-5 Status derivation forks into three parallel lookup structures over one NodeStatus enum — (uncommitted)
  • hickey-7 _ci-setup duration/status is reconciled by special-casing one branch inside the generic onNodes projection — commit 4a266ba6b
  • hickey-9 destroyAllSessions is re-exported through lane.ts purely as a pass-through — commit 9e6143429
  • hickey-10 The runner's blocked predicate accepts errored though the runner never produces it — commit 8c6b5dc58

Agreed — no change (7)

  • lowy-4 Lane-state→fan-in projection re-encodes the NodeState shape rather than transforming through one mapper (packages/odu/src/coordinator/run.ts:438-465 (onNodes), run.ts:243-268 (initial fan-in node construction), packages/odu/src/runner/runner.ts:156-182 (configure node construction))
  • lowy-5 fetchUrlFor silently downgrades non-GitHub origins to the raw URL — a forge-volatility boundary that leaks (packages/odu/src/coordinator/statuses.ts:177-181 (fetchUrlFor), run.ts:201-207 (github null guard))
  • hickey-2 DAG scheduling (runnable/blocked/tick fixed-point + skip-cascade) is reimplemented in the coordinator's verdict/skip handling (packages/odu/src/runner/runner.ts:193-221 and packages/odu/src/coordinator/run.ts:478-494)
  • hickey-6 failedOverall / done / red-count is computed three independent ways (packages/odu/src/cli/render.ts:38-59 (summarize), coordinator/run.ts:530-549 (verdict counts + red), cli/introspect.ts:119,122 (summarize reuse) )
  • hickey-8 parseGithubRemote regex hand-rolls URL parsing for two consumers (packages/odu/src/coordinator/statuses.ts:163-181)
  • hickey-11 logPathFor and resolveNodeId re-derive the same id-grammar two more times (packages/odu/src/coordinator/statuses.ts:29-33 and cli/introspect.ts:27-39 and just/ingest.ts:196-202)
  • hickey-12 report.sh re-derives the verdict glyph and duration format already owned by odu (ci/pu/report.sh:52,100-102 and packages/odu/src/common/duration.ts)

@srid

srid commented Jun 9, 2026

Copy link
Copy Markdown
Member Author

👮 Code-police

Ran after the codex and lens debates, on the settled tree. Findings actioned (commit fix(police): …):

  • One glyph source of truth — removed the STATUS_GLYPH constant derived from STATUS_META; both render paths now read STATUS_META[status].glyph directly, so the table the lens debate introduced has no shadow copy to drift.
  • runner.ts hot loopstick() and rerun() now snapshot getState() once per pass instead of re-reading the store per field access.
  • Dead fieldPipelineSummary.total was computed but never read; dropped.

Deliberately skipped: unifying resolveNodeId/resolveRecipe (different types, two call sites — a generic abstraction would cost more than it saves) and the ci::default guard in report.sh (defensive for legacy pc.log replays).

Typecheck + 34 unit tests green after the fixes.

srid added 2 commits June 9, 2026 17:02
First dogfood run failed ci::e2e@x86_64-linux: cucumber's ui profile
formats pretty:/dev/stderr, and Linux cannot open() a socket by path —
node's 'pipe' stdio is an AF_UNIX socketpair, where process-compose gave
justci's recipes real pipes. Recipe commands now run as
bash -o pipefail -c '{ cmd } 2>&1 | cat' so fd 1/2 are genuine pipe(2)s.
Regression-tested over the loopback harness.
@srid

srid commented Jun 9, 2026

Copy link
Copy Markdown
Member Author

🧪 CI metrics — leased pool box

The x86_64-linux lane ran on kolu-ci-5 (idliv2-02) — commit 3cbac86f, exit 0

  • Lane wall (pipeline): 9m13s
  • Wrapper wall (incl. lease + nix-run startup): 28m37s
recipe duration
_ci-setup 9m13s
ci::e2e 8m20s
ci::home-manager 44s
ci::smoke 31s
ci::nix 30s
ci::atlas-sync 26s
ci::unit 21s
ci::pnpm-hash-fresh 19s
ci::surface-example-build 12s
ci::surface-app-example-build 12s
ci::install 11s
ci::biome 11s
ci::fmt 10s

Pool status (8 boxes)

box location state
kolu-ci-1 dev-x86-64-linux-04 ✓ idle
kolu-ci-2 dev-x86-64-linux-04 ✓ idle
kolu-ci-3 dev-x86-64-linux-03 ✓ idle
kolu-ci-4 dev-x86-64-linux-08 ✓ idle
kolu-ci-5 idliv2-02 ✓ idle
kolu-ci-6 dev-x86-64-linux-03 ✓ idle
kolu-ci-7 dev-x86-64-linux-05 ✓ idle
kolu-ci-8 dev-x86-64-linux-08 ✓ idle

Posted by ci/pu/report.sh. Lane timings from .ci/3cbac86/timings.jsonl; pool state is a live flock probe.

@srid

srid commented Jun 9, 2026

Copy link
Copy Markdown
Member Author

Evidence

The dogfood is the evidence: this PR's required checks were posted by odu itself — full [metadata("ci")] DAG, strict mode, linux lane on leased pool box kolu-ci-5, darwin lane on rasam, 26/26 nodes green on 3cbac86 (gh pr checks above is all odu-posted contexts; lane wall 28m38s, bounded by ci::e2e@aarch64-darwin at 26m23s).

The headline feature, captured live mid-runnix run .#odu -- monitor attached to the running pipeline from a second terminal over .ci/odu.sock, painting both lanes' state plus the attached node's streaming cucumber output (this is the in-band introspection justci could not do without a separately-versioned process-compose client):

pipeline: ci::default
  ✔ _ci-setup@aarch64-darwin                     ok (3m27s)
  ✔ ci::atlas-sync@aarch64-darwin                ok (1m35s)
  ✔ ci::home-manager@aarch64-darwin              ok (2m37s)
  ✔ ci::install@aarch64-darwin                   ok (1m16s)
  ✔ ci::nix@aarch64-darwin                       ok (2m36s)
  ✔ ci::pnpm-hash-fresh@aarch64-darwin           ok (2m9s)
  ✔ ci::smoke@aarch64-darwin                     ok (2m42s)
  ✔ ci::surface-app-example-build@aarch64-darwin ok (5s)
  ✔ ci::surface-example-build@aarch64-darwin     ok (5s)
  ✔ ci::unit@aarch64-darwin                      ok (48s)
  ✔ ci::biome@aarch64-darwin                     ok (3s)
› ▶ ci::e2e@aarch64-darwin                       running
  ✔ ci::fmt@aarch64-darwin                       ok (2s)
  ✔ _ci-setup@x86_64-linux                       ok (1m26s)
  ✔ ci::atlas-sync@x86_64-linux                  ok (26s)
  ✔ ci::home-manager@x86_64-linux                ok (44s)
  ✔ ci::install@x86_64-linux                     ok (11s)
  ✔ ci::nix@x86_64-linux                         ok (30s)
  ✔ ci::pnpm-hash-fresh@x86_64-linux             ok (19s)
  ✔ ci::smoke@x86_64-linux                       ok (31s)
  ✔ ci::surface-app-example-build@x86_64-linux   ok (12s)
  ✔ ci::surface-example-build@x86_64-linux       ok (12s)
  ✔ ci::unit@x86_64-linux                        ok (21s)
  ✔ ci::biome@x86_64-linux                       ok (11s)
  ▶ ci::e2e@x86_64-linux                         running
  ✔ ci::fmt@x86_64-linux                         ok (10s)
────────────────────────────────────────────────────────────
$ just --no-deps ci::e2e
.    When I click the chrome-bar maximize toggle
.    Then canvas tile 1 should be maximized
.    When I click the chrome-bar maximize toggle
.    Then no canvas tile should be maximized
.    And. there should be no page errors
..
  Scenario: Keyboard shortcut toggles canvas maximize # features/canvas.feature:366
.    Given the terminal is ready
.    Given I create a terminal
.    Then there should be 2 canvas tiles
.    When I press the maximize toggle shortcut

────────────────────────────────────────────────────────────
● 2 running · 24 ok · 0 pending

[digits] attach · [n/p] cycle · [r] rerun · [q] quit

The first dogfood run earned its keep: it caught a real environment-parity gap — cucumber's pretty:/dev/stderr dies with ENXIO when fd 2 is node's socketpair instead of process-compose's real pipe — fixed in 3cbac86 (recipes now get genuine pipe(2)s via a | cat interposition) with a loopback regression test, then re-run to full green.

Known cosmetic follow-up: the end-of-run verdict table recomputes _ci-setup's duration from the last lane frame instead of freezing it at the terminal transition, so it over-reports there (the posted GitHub status descriptions are correct — they fire once, on the transition).

@srid
srid marked this pull request as ready for review June 9, 2026 21:45
srid added 7 commits June 9, 2026 17:51
# Conflicts:
#	apm.lock.yaml
#	default.nix
PR #1084 (master) added serveOverUnixSocket / unixSocketLink — the
hardened version of what coordinator/socket.ts hand-rolled (probe, stale
reclaim, per-connection serveOverStdio). odu now consumes them, keeping
its checkout-scoped .ci/odu.sock path and translating the library's
outcomes (already-served = the one-run-per-checkout lock; a dial failure
= no run in progress). .ci is tightened to 0700 — the library refuses to
serve a full-control router from a world-readable directory. Also adopts
serveOverStdio's new settled ServeOverStdioEnd result in the runner.
…o dead air

justci-UX mimicry dropped (explicitly sanctioned): odu run now picks its
face by where stdout points. On a TTY: an in-place recipes × lanes matrix
with spinners, per-cell ticking elapsed times, a one-line tail of whatever
the busiest node just printed, and persistent scrollback lines for
failures — a 30-minute e2e node reads as alive, not hung. Piped: quiet
transition lines plus a once-a-minute '… still running' heartbeat.
--progress json is byte-identical (the /do contract). surface-nix-host's
[host:…] stderr chatter no longer shreds the terminal — it lives in
_ci-setup's log and the live footer. Monitor/status share the colour
treatment via render's STATUS_COLOR.
… client

A pinned nixpkgs nix (2.31) older than the host daemon (2.34) corrupts
CA-derivation handling the moment a recipe re-enters the devshell:
'derivation has incorrect deferred output … kolu-shell-env'. The lane
host's nix realised the runner closure, so it provides the client too;
the pool boxes never hit this only because their client and daemon were
the same version.
finishSetup re-fired on every later lane frame, re-deriving now() − start
and inflating the settled number (6s mid-run → 25s in the final table;
the posted GitHub description was always correct — it fires on the
transition). Closes the cosmetic follow-up noted on the PR.
Proposed, not built: OpenTUI (Anomaly's Zig-cored, MIT terminal framework
powering opencode) has a first-class SolidJS reconciler, which would let
the terminal dashboard consume @kolu/surface's Solid-only hooks — the
same view code the PWA face will use. Costs named: native core in the
nix closure, bun-first runtime. Plan: prototype odu monitor first.
Phase-2 milestone updated.
…'s claims

Run output now carries '@ <sha7>' (or '@ <sha7>+dirty', yellow, for a
live-tree run on uncommitted changes) in the live header, the plain
header, and the verdict banner — the verdict always says which code it
is about. The Atlas note's OpenTUI paragraph replaces its two hedges
with verified facts: prebuilt natives for all four kolu systems ship as
optionalDependencies (no Zig for consumers; fetchPnpmDeps-compatible),
and 0.4.0's FFI is bun-only — node 24 throws 'native FFI is not
available for this runtime yet' (reproduced; bun renders the same probe)
— so adoption is gated on upstream node support. @opentui/solid's exact
solid-js 1.9.12 peer pin noted.
srid added 3 commits June 9, 2026 18:54
The '@ <sha7>' label in the live header and the verdict banner is now an
OSC 8 hyperlink to the GitHub commit page when origin is a GitHub remote
(kolu's own xterm.js panes render these; plain passthrough off-TTY and
on non-GitHub origins).
Assuming PR #1252 lands: status accepted → implemented; Phase-1
milestone → done (exit criterion met by the PR's own odu-posted checks);
the three work-order cells the implementation outgrew now state what
shipped — fetch-pushed-SHA workspaces (no git bundles), dump/graph
instead of dump-yaml, .ci/odu.sock serving the same typed surface with
idle-attach moved to Phase 2, the vacuous protect flip, errored-state
posts, and the de-justci'd run UX.
srid added a commit to juspay/odu that referenced this pull request Jun 9, 2026
* feat: odu — a CI runner you attach to, graduated from juspay/kolu

The Phase-1 implementation built and dogfooded in juspay/kolu#1252
(packages/odu), upstreamed the drishti way: the @kolu/surface libraries
are consumed via an npins pin of kolu (extracted by nix/overlay.nix,
hydrated into node_modules as raw TypeScript by
scripts/hydrate-kolu-packages.sh — no vendoring, no build step), the
toolchain is node/pnpm/tsx, and nix run .#odu / github:juspay/odu works
via the zero-input flake. The repo runs its own CI with itself:
ci/mod.just carries the [metadata("ci")] DAG and
`nix run .#odu -- run` executes it (verified: typecheck + 40 unit
tests + fmt green, plus a localhost-lane dogfood run of ci::fmt).

* ci: odu runs odu on GitHub Actions

Two platform jobs (ubuntu = x86_64-linux, macos = aarch64-darwin), each a
localhost lane executing the repo's own [metadata("ci")] DAG with the odu
built from the commit under test — strict mode, head-sha checkout, posting
ci::<recipe>@<platform> statuses alongside the Actions checks (fork PRs:
posts fail soft, the run still gates).
srid added a commit to juspay/odu that referenced this pull request Jun 9, 2026
**odu becomes an exporting apm package**: justci shipped its `/ci` skill
via `.apm/skills/ci`, and consumers got the runner reference deployed
into their agent runtimes by declaring the dependency. odu now does the
same — `apm.yml` is already `type: hybrid`, so this PR only adds the
skill source.

Consumers wire it with one line in their `apm.yml`:

```yaml
dependencies:
  apm:
    - juspay/odu
```

…and `just ai::apm` (or however the repo runs apm) deploys it as
`.claude/skills/ci` / `.agents/skills/ci`. The content is the
consumer-generic runner reference: invocation (`nix run
github:juspay/odu`, or a consumer's re-exported `.#odu`), the
strict-mode flag table, selectors, `--progress json`'s NDJSON contract,
in-band `status`/`logs`/`monitor` over `.ci/odu.sock`, hosts config with
the justci-fallback migration path, and the one-shot-lane /
skipped-posts-nothing / runner-drv-resolution semantics a consumer must
know.

*kolu will switch from its repo-local `.apm/skills/ci` to this
dependency in juspay/kolu#1252 once this merges — closing the loop: odu
documents itself, downstream.*

_Generated by Claude Code (model `claude-fable-5`)._
srid added 3 commits June 9, 2026 19:51
odu graduated to github.com/juspay/odu (juspay/odu#1, merged), so kolu no
longer hosts its source. The flip:

- packages/odu is deleted; pnpm-lock.yaml drops the importer.
- npins gains an `odu` pin (`npins update odu` to bump).
- Root default.nix imports the pinned repo (threading only the system into
  odu's own pinned nixpkgs) and RE-EXPORTS `{ odu, odu-runner }`, so every
  existing consumer is unchanged: `nix run .#odu` (run.sh, do.md, the /ci
  skill) and the coordinator's
  `nix eval .#packages.<platform>.odu-runner.drvPath` keep working verbatim.
- README / ci/mod.just / .agency/do.md / ci/pu/run.sh repointed at the
  upstream repo; the mini-ci-vs-justci Atlas note records the dependency
  flip in its Phase-3 milestone (historical packages/odu prose stays).

odu's unit tests no longer run in this repo — they run in juspay/odu's own
GitHub Actions (odu-on-odu, per push).
…nts itself

juspay/odu#2 made odu an exporting apm package (the wiring justci had);
kolu's repo-local .apm/skills/ci is deleted and apm.yml depends on
juspay/odu instead, closing the graduation loop: kolu consumes both the
binary (npins, re-exported flake outputs) and its documentation (apm)
from upstream. odu pin freshened to the same commit.
# Conflicts:
#	docs/atlas/dist/index.html
@srid srid changed the title odu: a CI runner you attach to — Phase 1, replacing justci odu: a CI runner you attach to — replaces justci, then graduates Jun 10, 2026
@srid

srid commented Jun 10, 2026

Copy link
Copy Markdown
Member Author

🧪 CI metrics — leased pool box

The x86_64-linux lane ran on kolu-ci-5 (idliv2-02) — commit 1acc855b, exit 0

  • Lane wall (pipeline): 1m23s
  • Wrapper wall (incl. lease + nix-run startup): 1m26s
recipe duration
ci::home-manager 52s
ci::nix 35s
ci::atlas-sync 34s
ci::smoke 33s
_ci-setup 31s
ci::pnpm-hash-fresh 23s
ci::unit 20s
ci::install 19s
ci::surface-example-build 11s
ci::surface-app-example-build 11s
ci::fmt 10s
ci::biome 10s

Pool status (8 boxes)

box location state
kolu-ci-1 dev-x86-64-linux-04 ✓ idle
kolu-ci-2 dev-x86-64-linux-04 ✓ idle
kolu-ci-3 dev-x86-64-linux-03 ✓ idle
kolu-ci-4 dev-x86-64-linux-08 ✓ idle
kolu-ci-5 idliv2-02 ✓ idle
kolu-ci-6 dev-x86-64-linux-03 ✓ idle
kolu-ci-7 dev-x86-64-linux-05 ✓ idle
kolu-ci-8 dev-x86-64-linux-08 🔒 leased

Posted by ci/pu/report.sh. Lane timings from .ci/1acc855/timings.jsonl; pool state is a live flock probe.

Maintainer's call: the rule isn't worth carrying repo-locally now that
the justci package that bundled it is gone. .claude/rules/workflow.md
and its AGENTS.md section go with it.
@srid
srid merged commit dafee84 into master Jun 10, 2026
2 checks passed
@srid
srid deleted the odu branch June 10, 2026 00:22
srid added a commit that referenced this pull request Jun 15, 2026
F4 (minor, fixed): the round-1 F2 edit added an unquoted '(PR #1379)' to the Atlas frontmatter description. YAML treats '#' after whitespace as a comment, so the generated meta description and index copy truncated at '(PR'. Single-quoted the description (matching the existing convention in odu.mdx, which carries 'PR #1252' the same way) and regenerated docs/atlas/dist — the full sentence now renders in both dock-repo-identity.html meta and index.html copy.

F1, F2, F3: already resolved in round 1 (codex confirmed); no changes this round.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant