refactor(monitor): one progress contract for run + monitor (#4) - #5
Conversation
… the surface `odu run` and `odu monitor` rendered the same fan-in state through two hand-rolled, drifted paths (#4). Route monitor's non-interactive stream through run's `createDisplay`, building each transition with a new shared `progressEvent`, so the json shape (now with recipe/platform/log), the plain line format, and the 60s heartbeat are byte-identical to run instead of re-derived. Carry `sha7`/`dirty` on PipelineState so monitor sources the durable log path from the surface: the coordinator stamps the authoritative values onto the fan-in (the lane copy is advisory). The interactive threshold stays stdin+stdout (the dashboard reads keys) — the one deliberate difference from run's output-only matrix — now documented in code.
Addressed both CODEX findings in /home/srid/code/odu. F1 (untracked nested worktree making the tree dirty): agreed and fixed by adding /.worktrees/ to .gitignore — the better of CODEX's two suggestions, since .worktrees/lanky-mind is a real git worktree of this repo (an intentional local convention) and .gitignore already ignores sibling tooling dirs (.be-review/, .codex-debate/, .lens-debate/). The tree is now clean (git status --short no longer lists .worktrees/), so strict `odu run` won't refuse it and a broad git add can't stage a nested checkout. F2 (README misstates non-TTY monitor output): agreed and fixed — reworded the CLI block so `-o json` matches `run --progress json` while the default piped output is described as run's plain transition stream, not JSON. The implementation confirms this: monitorStream uses createDisplay("plain") (PlainDisplay: banner + glyph/status lines + heartbeat) unless -o json is passed. Only *.nix files have a formatter; the two files I changed need none.
codex (round 1) findings:
- [F1 · major] The branch has an untracked nested Git worktree under `.worktrees/lanky-mind/`. Because `.worktrees/` is not ignored, this makes the checkout dirty; in this project that is not just cosmetic because strict `odu run` refuses dirty trees. If someone later stages untracked files with a broad add, it would also include an entire nested checkout with a machine-local `.git` pointer. (.worktrees/lanky-mind/.git:1)
- [F2 · nit] The CLI docs say non-TTY `odu monitor` emits "the same transition stream as run --progress json", but the implementation only emits NDJSON when `-o json` is passed. A piped monitor without `-o json` uses the plain display, including the banner and glyph/status lines, so this wording can mislead users into piping default output to JSON consumers. (README.md:125)
claude:
- F1 fixed: Agreed. Added `/.worktrees/` to /home/srid/code/odu/.gitignore (line 2). Verified .worktrees/lanky-mind/.git is `gitdir: /home/srid/code/odu/.git/worktrees/lanky-mind` — a genuine git worktree of this repo, i.e. the intentional local convention CODEX's second suggestion targets, not stray files. Chose the gitignore route over deleting the dir because (a) deletion would recur every time the worktree convention is used, and (b) .gitignore already ignores analogous tooling dirs (.be-review/, .codex-debate/, .lens-debate/ at lines 9-11), so this fits the existing pattern. After the edit, `git -C /home/srid/code/odu status --short` shows only the two intended file modifications and no longer lists .worktrees/ — the tree is clean, so strict `odu run` won't refuse it and a broad git add can't pull in the nested checkout. Note: the diff under review never touched .gitignore, so this untracked dir was a workspace artifact, but ignoring it is the durable fix.
- F2 fixed: Agreed. Reworded README.md:125-127 from 'live dashboard (tty); else the same transition stream as run --progress json' to distinguish the two non-TTY modes: '-o json matches run --progress json, else run's plain transition stream'. Confirmed against src/cli/introspect.ts:90-93 and :118-146: monitorCommand sets interactive only when !json && stdin/stdout both TTY; otherwise monitorStream runs createDisplay(json ? "json" : "plain"). PlainDisplay (src/coordinator/display.ts:156-222) emits a banner via start(), per-transition glyph/status lines, and a 60s heartbeat — that is run's plain stream, not NDJSON. Only `-o json` yields JsonDisplay's NDJSON matching `run --progress json`. The code comment at introspect.ts:88-89 already stated this correctly ('shares run's json/plain rendering'); the README was the imprecise one. Verified no line in the CLI code block exceeds the column budget after the edit.
Committed by the codex<->claude debate (round 1); not pushed or merged.
…wy-1) The sha7 doc comment claimed "every attached face (monitor, the MCP tools)" derives the durable log path from surface state "instead of re-deriving the sha from git" — but the MCP tail_log durable-file fallback uses headSha7(git HEAD), never state.sha7 (no live socket exists in that branch). So the receptacle has exactly one real consumer: monitor. Correct both comments (surface.ts, runner.ts) to say so and note the MCP fallback's git-HEAD derivation is deliberate. Comment-only; no behavior change. Lens: lowy ⇄ hickey consensus (#4 review gauntlet).
…type (hickey-2, hickey-4)
hickey-2: `odu status` (plain) was a third drifted rendering of the same fan-in
state — `${node.status.padEnd(8)}` emitted the raw NodeStatus (`ok`) where
run/monitor emit the external ProgressStatus (`success`). Route it through the
same STATUS_META wording (`progress ?? status`, pad-7), so a green node reads
`success` in every plain face. The json branch (nodeRow) is left alone — a
snapshot row and a transition event are legitimately different shapes.
hickey-4: monitorStream's `seen` map was widened to Map<string,string>, losing
the NodeStatus invariant; restore Map<string, NodeState["status"]>.
statusCommand gains an optional socketPath (defaulting to the live socket) so it
is testable over the same served-surface harness; added a plain-status test.
Lens: lowy ⇄ hickey consensus (#4 review gauntlet).
Codex ⇄ Claude debate✅ Consensus after 2 round(s) · codex reviewed at Round 1codex — approved: The shared Findings:
claude — Addressed both CODEX findings in /home/srid/code/odu. F1 (untracked nested worktree making the tree dirty): agreed and fixed by adding /.worktrees/ to .gitignore — the better of CODEX's two suggestions, since .worktrees/lanky-mind is a real git worktree of this repo (an intentional local convention) and .gitignore already ignores sibling tooling dirs (.be-review/, .codex-debate/, .lens-debate/). The tree is now clean (git status --short no longer lists .worktrees/), so strict
commit: Round 2codex — approved: Both previously raised findings have been addressed in the current tree. Findings:
codex on the rebuttal: F1: Agreed with CLAUDE's fix. Ignoring claude — (no author turn this round) (no author turn — the debate ended this round) |
⚖️ Lowy ⇄ Hickey lens debate✅ Consensus after 1 round(s) · lowy + hickey · base Independent findings: lowy=5, hickey=4 Agreed fixes — handed off to the caller (3)
Agreed — no change (6)
Applied by /be-reviewRe-validated each agreed fix against the post-codex tree, then applied:
The 6 agreed — no change findings stand, including |
🚔 Code-police reviewRead-only passes over the diff (
All clear — the deliberate decisions (advisory lane copy, |
CI + evidenceodu-on-odu CI green on the final HEAD (
Evidence — the fix in action. The {"node":"ci::unit@x86_64-linux","recipe":"ci::unit","platform":"x86_64-linux","status":"success","exit_code":0,"log":".ci/9c62804/x86_64-linux/ci::unit.log"}Before this PR, |
## Rename `odu monitor` → `odu attach` odu's whole pitch is **"a CI runner you attach to"** — it's the README's first line, the code's own architecture comment (*"status / logs / attach dial the fan-in surface"*), and the roadmap's *"idle attach"* phase. Yet the interactive command was `monitor`, which (a) sounds passive for a face that **reruns nodes** (`r`), focuses, and quits, and (b) fights the tmux `attach` mental model everyone already has. This renames it: `odu monitor` → **`odu attach`**. - Command + `USAGE` + the `attachCommand` / `attachStream` / `attachDashboard` / `attachHeader` handlers + every doc/comment reference. - `status` / `logs` stay as the one-shot, non-interactive primitives (the byte contract #4/#5 unified). - **Clean break — `monitor` is removed, no deprecated alias.** odu is pre-1.0 with no external callers (`ci/pu/run.sh` and the MCP tools drive `odu run`), so a lingering alias would just be dead surface. ``` odu run [recipe[@platform]…] run … odu status [-o json] snapshot a live run odu logs [-f] <node> replay (+ follow) one node's log odu attach [-o json] live dashboard (tty); piped/-o json → run's contract odu dump | graph | protect | mcp … ``` Precursor to **#6** (*Unify the live view: one interactive renderer for run + attach*) — once the verb is `attach`, the model is one live view reached two ways (`run` = start+attach, `attach` = join a live run). `pnpm typecheck` clean, 69 unit tests green. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
## Re-pin odu to master — `attach` rename + unified live view Bumps kolu's [odu](https://github.com/juspay/odu) pin from `aa0af7c` (the MCP merge, #1258) to master **`7fa40f3`**, picking up four merged odu PRs: - **[juspay/odu#5](juspay/odu#5 — one progress contract for `run` + `monitor` (the `-o json` / piped-plain dedup; `monitor` json regained `recipe`/`platform`/`log`). - **[juspay/odu#7](juspay/odu#7 + **[#8](juspay/odu#8 — renamed the interactive command **`monitor` → `attach`** (clean break, no alias), matching odu's "a CI runner you attach to" thesis. - **[juspay/odu#9](juspay/odu#9 — **unified the live view**: `attach` now paints `run`'s recipes×platforms matrix (one shared renderer) with a focused-node log pane, reading the lane→host map off a new fan-in `header` cell. ### What changed here - **npins** (`npins/sources.json`) → odu `7fa40f3` + new hash. - **apm** (`apm.lock.yaml` + `just ai apm-update juspay/odu`) → redeployed odu's `ci` skill, which now documents `odu attach` (`.claude/skills/ci`, `.agents/skills/ci`). The `odu-mcp` launcher and `.mcp.json` MCP-server entry are **unchanged** (the `odu mcp` command didn't move). - **Docs synced** for the rename + unification: `README.md`, `.agency/do.md`, and the `mini-ci-vs-justci` Atlas note (+ rebuilt `dist/`). Carefully left the *example node* named `monitor`, *justci's* `monitor` subcommand, and the `remote-process-monitor` lineage untouched. ### Validation `nix build .#odu .#odu-runner` green on the new pin; `nix run .#odu` usage now lists `attach`. CI will confirm end-to-end (kolu's own pipeline runs through this odu). 🤖 Generated with [Claude Code](https://claude.com/claude-code)
runandmonitor: one progress contract — fixes #4odu runandodu monitorrendered the same fan-in state through twoseparate, hand-rolled output paths that had drifted (#4).
This routes
monitor's non-interactive stream throughrun's own renderer, sothere is now a single source of truth for the json/plain contract.
The seam
progressEvent(sha7, id, node)indisplay.ts— the one projectionof a node's state into a
ProgressEvent. Bothrun(its own runsha7) andmonitor(the surface'ssha7) build transitions through it, so the two facesemit byte-identical output by construction instead of re-deriving it.
monitorStreamnow drivescreateDisplay(json ? "json" : "plain")— thesame
JsonDisplay/PlainDisplayrunuses. Collapses three divergences atonce:
recipe/platform/logand uses the
--progress jsonwording (success, not the rawok) — so aconsumer of
run's contract no longer breaks againstmonitor.ProgressStatus+ duration + a→ logpathon failures, matching
run(was a barestatus id).runandmonitorproduce inconsistent output (duplicated, drifted render paths) #4 (heartbeat): the 60s… still running:line now appears inmonitor's stream too (it was silent between transitions).
Carry
sha7/dirtyon the surfacemonitorneeds the run's commit to name the durable log path(
.ci/<sha7>/…). Rather than re-read git HEAD (and risk drift if HEAD movedmid-run),
PipelineStatenow carriessha7/dirty: the coordinator stamps theauthoritative values onto the fan-in surface (
.ci/odu.sock). The lane's copyis advisory — documented in
runner.ts— since commit identity is a fan-inconcern.
The one deliberate difference (#1)
The interactive threshold stays
stdin && stdoutfor the dashboard (it readskeystrokes) vs
run'sstdout-only live matrix (output only). That differenceis inherent to interactivity, now commented as deliberate. The win: when monitor
does fall back to the stream, that stream is now
run-quality rather than theold poor cousin.
Tests
progressEventunit tests: recipe/platform/log, theNodeStatus → ProgressStatusmapping,
exit_codeonly when present,nullforpending.introspect.test.ts:monitorStreamover a real served surface — the jsoncarries the full contract; the plain lines use
run's glyph + wording + logref.
pnpm typecheckclean, 68 unit tests green.🤖 Generated with Claude Code