Conversation
…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.
Codex ⇄ Claude debate✅ Consensus after 2 round(s) · codex reviewed at Round 1codex — approved: 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 Findings:
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
commit: Round 2codex — approved: 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:
codex on the rebuttal: No disputes to answer. CLAUDE agreed with all four findings; I verified the corresponding fixes in claude — (no author turn this round) (no author turn — the debate ended this round) |
… 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.
⚖️ Lowy ⇄ Hickey lens debate✅ Consensus after 1 round(s) · lowy + hickey · base Independent findings: lowy=6, hickey=12 Applied (11)
Agreed — no change (7)
|
…rop dead summary field
👮 Code-policeRan after the codex and lens debates, on the settled tree. Findings actioned (commit
Deliberately skipped: unifying Typecheck + 34 unit tests green after the fixes. |
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.
🧪 CI metrics — leased pool boxThe x86_64-linux lane ran on
Pool status (8 boxes)
Posted by |
EvidenceThe dogfood is the evidence: this PR's required checks were posted by odu itself — full The headline feature, captured live mid-run — The first dogfood run earned its keep: it caught a real environment-parity gap — cucumber's Known cosmetic follow-up: the end-of-run verdict table recomputes |
# 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.
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.
…estone and the odu README
* 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).
# Conflicts: # apm.lock.yaml # docs/atlas/dist/index.html
**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`)._
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
🧪 CI metrics — leased pool boxThe x86_64-linux lane ran on
Pool status (8 boxes)
Posted by |
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.
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.
kolu's CI is now run by odu (Tamil ஓடு — "run") — the live-service CI runner grown from the
mini-ciexample — replacinggithub.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/domuscle memory carry over unchanged — but the run is state you attach to, not a batch process you scrape.How a run works
On a TTY,
odu runpaints 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+dirtyin live-tree mode) — justci-UX mimicry deliberately dropped. Piped, it emits quiet transition lines with a once-a-minute heartbeat;--progress jsonis the byte-stable NDJSON contract/doconsumes.What kolu ends up with
odu/odu-runnerbinariesnix run .#odu,ci/pu/run.sh(lease machinery untouched), and the coordinator's runner-drv eval all unchanged;npins update oduto bump/ciskilljuspay/oduas an apm dependency (juspay/odu#2) — the runner documents itself, like justci did@kolu/surfacelibraries (inside odu)The justci excision is total in live paths:
apm.ymldrops 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.jsonlsidecarreport.shnow prefers. Lowy ⇄ Hickey lens debate (consensus, 11 applied): onenodeIdowner, oneSTATUS_METAprojection, shared log-tail store, deadODU_RUNNER_DRVwire deleted. Code-police: 3 polish fixes. Production then caught what review couldn't: node's'pipe'stdio is an AF_UNIX socketpair Linux can'topen()by path (cucumber'spretty:/dev/stderrdied where process-compose's real pipes worked → recipes now run behind a| catinterposition, 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
ci::e2e@x86_64-linuxcontexts (+_ci-setup@<plat>)Running:/Succeeded (3m26s):/Failed (8s):+ log path1m0szero-pad.ci/<sha7>/<plat>/ci::e2e.logodu 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.Try it
Generated by
/beon Claude Code (modelclaude-fable-5).