Skip to content

refactor(monitor): one progress contract for run + monitor (#4) - #5

Merged
srid merged 4 commits into
masterfrom
run-monitor-dedup
Jun 10, 2026
Merged

refactor(monitor): one progress contract for run + monitor (#4)#5
srid merged 4 commits into
masterfrom
run-monitor-dedup

Conversation

@srid

@srid srid commented Jun 10, 2026

Copy link
Copy Markdown
Member

run and monitor: one progress contract — fixes #4

odu run and odu monitor rendered the same fan-in state through two
separate, hand-rolled output paths that had drifted (#4).
This routes monitor's non-interactive stream through run's own renderer, so
there is now a single source of truth for the json/plain contract.

The seam

  • Shared progressEvent(sha7, id, node) in display.ts — the one projection
    of a node's state into a ProgressEvent. Both run (its own run sha7) and
    monitor (the surface's sha7) build transitions through it, so the two faces
    emit byte-identical output by construction instead of re-deriving it.
  • monitorStream now drives createDisplay(json ? "json" : "plain") — the
    same JsonDisplay/PlainDisplay run uses. Collapses three divergences at
    once:

Carry sha7/dirty on the surface

monitor needs the run's commit to name the durable log path
(.ci/<sha7>/…). Rather than re-read git HEAD (and risk drift if HEAD moved
mid-run), PipelineState now carries sha7/dirty: the coordinator stamps the
authoritative values onto the fan-in surface (.ci/odu.sock). The lane's copy
is advisory — documented in runner.ts — since commit identity is a fan-in
concern.

The one deliberate difference (#1)

The interactive threshold stays stdin && stdout for the dashboard (it reads
keystrokes) vs run's stdout-only live matrix (output only). That difference
is 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 the
old poor cousin.

Tests

  • progressEvent unit tests: recipe/platform/log, the NodeStatus → ProgressStatus
    mapping, exit_code only when present, null for pending.
  • introspect.test.ts: monitorStream over a real served surface — the json
    carries the full contract; the plain lines use run's glyph + wording + log
    ref. pnpm typecheck clean, 68 unit tests green.

🤖 Generated with Claude Code

srid added 4 commits June 10, 2026 09:30
… 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).
@srid

srid commented Jun 10, 2026

Copy link
Copy Markdown
Member Author

Codex ⇄ Claude debate

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

Round 1

codex — approved: false

The shared progressEvent/monitorStream implementation looks internally consistent, and I did not find a correctness bug in the changed source path itself. I could not run the project typecheck because pnpm is not installed in this environment. The branch is not approvable as-is because the working tree includes an untracked nested checkout, and the README wording misstates the default non-TTY monitor output.

Findings:

  • F1 · major · open — 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 · open — 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 — 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.

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

commit: e8ced9d79a787666b9fc30a18c5a23f17e5e00c3

Round 2

codex — approved: true

Both previously raised findings have been addressed in the current tree. .worktrees/ is now ignored and no longer appears in git status --short; the README now distinguishes monitor -o json from the default piped plain transition stream. No new regression was introduced by CLAUDE's follow-up changes.

Findings:

  • F1 · major · resolved — Previously, the untracked nested .worktrees/lanky-mind/ checkout made the tree dirty and risked being staged accidentally. The current .gitignore now ignores /.worktrees/, and git status --short is clean. (.gitignore:2)
  • F2 · nit · resolved — Previously, the README implied default piped odu monitor output matched run --progress json. The current text correctly says -o json matches run --progress json, while the default piped mode emits the plain transition stream. (README.md:125)

codex on the rebuttal: F1: Agreed with CLAUDE's fix. Ignoring /.worktrees/ is appropriate for an intentional local worktree convention and removes the dirty-tree/staging risk. Verified git status --short is empty. F2: Agreed with CLAUDE's fix. The README wording now matches the implementation's createDisplay(json ? "json" : "plain") behavior for non-interactive monitor output.

claude(no author turn this round)

(no author turn — the debate ended this round)

@srid

srid commented Jun 10, 2026

Copy link
Copy Markdown
Member Author

⚖️ Lowy ⇄ Hickey lens debate

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

Independent findings: lowy=5, hickey=4

Agreed fixes — handed off to the caller (3)

  • lowy-1 Shared-surface sha7 justification names a consumer that bypasses the receptacle (MCP tools re-derive the sha from git HEAD) (src/common/surface.ts:113-118 (claim) vs. src/mcp/tools.ts:154 + src/mcp/git.ts:19-25 (reality))
  • hickey-2 status renders the same fan-in state through a third, drifted plain wording (src/cli/introspect.ts:57-64)
  • hickey-4 seen-map status type is widened to string, losing the NodeStatus invariant (src/cli/introspect.ts:124,136-137)

Agreed — no change (6)

  • lowy-2 sha7/dirty on the shared schema is variable-not-volatile for the lane, encapsulated as a half-live field (src/common/surface.ts:111-127; src/runner/runner.ts:145-151)
  • lowy-3 progressEvent receptacle correctly deduplicates the node→ProgressEvent projection across run and monitor (src/coordinator/display.ts:106-122; src/coordinator/run.ts:308-312; src/cli/introspect.ts:138)
  • lowy-4 Display receptacle now serves both faces — the duplicated rendering encapsulation is removed (src/cli/introspect.ts:118-145 vs. prior inline json/plain branch)
  • lowy-5 monitor's plain transition relies on PlainDisplay re-deriving glyph from NodeState instead of from the ProgressEvent (src/coordinator/display.ts:183-192)
  • hickey-1 Plain transition line derives glyph and word from two parallel status mappings (src/coordinator/display.ts:183-191)
  • hickey-3 monitorHeader builds a RunHeader in the consumer, not where RunHeader lives (src/cli/introspect.ts:97-111)

Applied by /be-review

Re-validated each agreed fix against the post-codex tree, then applied:

  • lowy-1applied in 20df31a (comment-only: name monitor as the real sha7 consumer in surface.ts/runner.ts; note the MCP tail_log git-HEAD fallback is deliberate).
  • hickey-2applied in 9c62804 (odu status plain now uses STATUS_META's success/… wording, pad-7, matching run/monitor; + a plain-status test; json/nodeRow left untouched).
  • hickey-4applied in 9c62804 (seen map restored to Map<string, NodeState["status"]>).

The 6 agreed — no change findings stand, including lowy-2 (sha7/dirty on the shared PipelineStateSchema): the lenses agreed the shared-primitives design is worth keeping over forking the schema for two scalars, with the lane copy documented as advisory.

@srid

srid commented Jun 10, 2026

Copy link
Copy Markdown
Member Author

🚔 Code-police review

Read-only passes over the diff (aa0af7c...HEAD): rule checklist, fact-check, elegance.

Pass Issues Notes
Rules 0 The change removes duplication (progressEvent extracted, one source for both faces); no dead code, no swallowed errors, no untyped escape hatches, non-obvious decisions all commented.
Fact-check 0 Verified the log-path format (.ci/<sha7>/<platform>/<namepath>.log), the STATUS_META mapping (ok→success, …), the exit-code logic, the hostsSource: string | null widening (only reader is null-guarded), and loop termination. Every added comment matches the code.
Elegance 0 Net simplification; nothing further to extract or collapse.

All clear — the deliberate decisions (advisory lane copy, progressEvent home, the stdin+stdout dashboard threshold, the observer's empty-lane header) were checked and correctly implemented.

@srid

srid commented Jun 10, 2026

Copy link
Copy Markdown
Member Author

CI + evidence

odu-on-odu CI green on the final HEAD (9c62804), linux lane via the warm pool (kolu-ci-6), 73s:

── ci run summary @ 9c62804 ──
  ✔ _ci-setup@x86_64-linux       ok 44s
  ✔ ci::fmt@x86_64-linux         ok 4s
  ✔ ci::install@x86_64-linux     ok 13s
  ✔ ci::nix@x86_64-linux         ok 1s
  ✔ ci::typecheck@x86_64-linux   ok 5s
  ✔ ci::unit@x86_64-linux        ok 4s
6 ok · 0 failed · 0 errored · 0 skipped — OK

ci::unit runs the new parity tests (69 total). The change is pure rendering/CLI logic with no platform-specific surface, so the linux lane exercises the whole recipe (typecheck + unit + fmt-check + nix build .#odu .#odu-runner); macos would re-run the identical recipe.

Evidence — the fix in action. The --progress json line run emitted for each node in that CI run is exactly the shape monitor -o json now produces (the contract this PR unifies), e.g.:

{"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, odu monitor -o json dropped recipe/platform/log and emitted "ok" instead of "success". src/cli/introspect.test.ts asserts the two faces now match over a real served socket.

@srid
srid marked this pull request as ready for review June 10, 2026 13:54
@srid
srid merged commit ad0c3a3 into master Jun 10, 2026
16 checks passed
@srid
srid deleted the run-monitor-dedup branch June 10, 2026 14:01
srid added a commit that referenced this pull request Jun 10, 2026
## 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)
srid added a commit to juspay/kolu that referenced this pull request Jun 10, 2026
## 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)
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.

run and monitor produce inconsistent output (duplicated, drifted render paths)

1 participant