Skip to content

task-monitor 4/6: ratatui front end (repo view + all-repos overview, real-time): the task-monitor binary in monitor/ - #282

Merged
martin-conur merged 3 commits into
mainfrom
task/monitor-tui
Oct 20, 2026
Merged

martin-conur merged 3 commits into
mainfrom
task/monitor-tui

Conversation

@martin-conur

@martin-conur martin-conur commented Oct 20, 2026 •

Copy link
Copy Markdown
Owner

Summary

This adds task-monitor, a live terminal board built in Rust with ratatui. For the repo you're in it shows what is in flight, what is next and what has landed, and an overview shows which repo needs you and why. It refreshes from disk the moment radio or a worker changes something, asks GitHub every 45 seconds, and keeps the last good screen up, marked stale, when a refresh fails.

Closes #276

What

  • monitor/: a new crate that builds the task-monitor binary. It reads task-monitor-snapshot --json --all [--local] and nothing else. Every field is optional, unknown fields are ignored, and a different schema is refused.

  • Screens: the repo view (in flight with status, PR, activity icon and age, reviewers; next, from the queue or the backlog, with notes; done with PR, SHA, age and a two-line summary) and the overview (attention light, signal text, in-flight count, zellij session). ⏎ expands an item in place, with a done item showing its full PR body, scrollable.

  • Refresh: filesystem events (notify) on $TASK_FORCE_HOME/radio (excluding log), $TASK_FORCE_HOME/monitor and each worktree base (.<slug>.info only) run a --local snapshot. Events are debounced 400ms on the trailing edge and capped at 2s, and only one local run goes at a time, with a rerun if an event lands mid-run. The full run starts at launch and then 45s after the previous one finished (--poll 30-600). Timeouts are 20s for a local run and 180s for a full one, and a hung collector is killed. A result never replaces a newer run's.

  • t: uses the sidecar TAB_ID, else the session's, or the PM's for queue/done items and overview rows. It refuses a tab in another zellij session by naming it. Otherwise it checks with list-tabs that the tab at that id still bears the item's name (the aw_tab_name_is_slug rule from task-done closes a stale TAB_ID without checking the tab is its own, killing an unrelated worker after a zellij restart #260) before go-to-tab-by-id.

  • Snapshot: one new field, worktree_base, so the watcher doesn't re-derive task-work's path rule. It is in the schema doc and example, with a bats test.

  • Docs: a README section, a task-monitor paragraph in .claude/gh-workflow.md and the seven steering templates, and a CHANGELOG entry.

  • CI (the PM moved this here from task-monitor 5/6: install.sh cargo build + CI Rust job #277): a rust job on ubuntu-latest and macos-latest runs from monitor/ on stable: cargo fmt --check, cargo build --locked, cargo test --locked, cargo clippy --all-targets --locked -- -D warnings, with Swatinem/rust-cache (keyed on monitor/Cargo.lock). A rust-msrv job builds and tests on 1.88, the rust-version in Cargo.toml and ratatui 0.30's floor. I checked that pin locally with cargo +1.88 test --locked (45/45).

Branch protection: main requires four checks by name: ShellCheck, Bats tests (ubuntu-latest), Bats tests (macos-latest) and Loadout drift check. The new Rust (ubuntu-latest), Rust (macos-latest) and Rust MSRV (1.88) checks are not required. Add them in the branch settings if they should gate merges. I did not change protection.

install.sh stays with #277, and the queue is read only through the snapshot (#275 owns task-monitor-queue).

Verification

  • cargo test: 45 pass, cargo clippy --all-targets: clean. I broke eight targets on purpose (tab-name rule, session check, run ordering, local-run coalescing, log filter, narrow fit, error reset, timeout kill), plus the symlink canonicalization, and each turned a test red.
  • PTY end to end with a fake collector under a scratch TASK_FORCE_HOME: it renders, a session-file write and a new sidecar each trigger a --local run, a radio/log append does not, and q exits 0.
  • I also rendered a real --all snapshot from this machine (4 repos) on a TestBackend to check the layout.
  • ./run_tests.sh: 1436/1436. tools/check-drift.sh: clean. shellcheck -x bin/task-monitor-snapshot: clean. CI runs the Rust tests as of the second commit.

🤖 Generated with Claude Code

…real-time): the task-monitor binary in monitor/

A Rust + ratatui front end over `task-monitor-snapshot --json` (#276,
epic #272). It renders the repo view (in flight / next / done) and the
all-repos overview with each repo's attention light, its signals in
words and its zellij session.

- Real time: filesystem events under `$TASK_FORCE_HOME/radio` (not its
  log), `$TASK_FORCE_HOME/monitor` and each repo's worktree base
  (`.<slug>.info` only) trigger a debounced `--local` snapshot; a full
  snapshot runs every 45s (`--poll`). Both run off the UI thread with
  timeouts, and an older run never replaces a newer one.
- Never blank: a failed or slow snapshot keeps the last good one up,
  marked stale, with the collector's reason in the footer.
- Keys: arrows, enter (expand / enter repo), o, t, a, q. `t` stays in
  the monitor's zellij session and checks that the tab at the recorded
  id still bears the item's name (the #260 rule) before going to it.
- No task-force logic in Rust: the snapshot gains `worktree_base` so
  the watcher does not re-derive task-work's path rule. Schema doc,
  example and a bats test cover it.

45 Rust tests (fixture rendering, keys, scheduler timing, watcher
filter, collector failure/timeout, tab check). README section, workflow
doc + steering templates, CHANGELOG. install.sh and CI stay with #277.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@martin-conur

Copy link
Copy Markdown
Owner Author

Spec compliance (#276)

Every scope bullet is delivered:

  • Screens: the repo view (In flight / Next / Done) and the all-repos overview. The overview shows the attention light, the signal in words and the zellij session.
  • Keys: ↑↓, ⏎ (in-place expand, scrollable full PR body for done items), o, t, a, q.
  • t: it is limited to the monitor's own zellij session and names a target in another session instead of attempting it. It checks the tab at the recorded id against the item's name before go-to-tab-by-id, matching aw_tab_name_is_slug from task-done closes a stale TAB_ID without checking the tab is its own, killing an unrelated worker after a zellij restart #260.
  • Real-time: notify watches radio (excluding log), monitor and each worktree base (.<slug>.info only). Events are debounced 400ms with a 2s cap, and only one local run goes at a time. The forge run is on a 45s timer (--poll 30-600).
  • Never blank: the last good snapshot stays up with a stale marker.
  • Rust tests: rendering from fixtures on a TestBackend, key handling, scheduler timing, the watcher filter and the fake-zellij tab checks.
  • No task-force logic in Rust: the one addition is worktree_base in the snapshot. It is documented in the schema and the example, and it is the right way to avoid re-deriving task-work's path rule in Rust.

The PR body opens with ## Summary. CI ran for the head SHA and passed (bats on macOS and Ubuntu, drift check, ShellCheck). I also ran cargo test locally: 45 passed. check-drift is clean.

Code-review findings

No blockers. Two points are worth fixing in this PR; both are small.

  1. A full run's result is dropped whenever a local run started after it lands first (App::offer, monitor/src/app.rs; the local-run trigger is in Scheduler::due, monitor/src/refresh.rs).

    • A full run can take up to 180s. The radio dir sees constant writes (session heartbeats), so a local run will almost always start and finish during a slow full run.
    • Because the full run started earlier, offer rejects it. It is dropped silently: no error is cleared, and freshness is not updated.
    • The data is not lost, since the collector's full-run cache is picked up by the next local run (the monitor/ dir is watched). But the screen can show pre-full-run forge state for one extra cycle, and the "forge " marker lags.
    • Fix: let a full result replace a local one even when it started earlier. This is safe because the full result is a superset (it includes the forge data and disk state read at its start). Or at minimum, when a full result is rejected, trigger a local rerun immediately so the cache lands. Add a test for "local lands during full".
  2. A stale failure from an older run can mark a fresh screen stale (App::failed).

    • failed sets freshness.error unconditionally. If a full run times out at 180s after a newer local run succeeded, the screen is marked stale and shows the error until the next success, even though the data on screen is current.
    • Fix: give failed the run's start time and ignore it when snapshot_started is later, mirroring offer.

Nits (non-blocking)

Verdict

clean-with-nits. I would fix findings 1 and 2 in this PR, since they are small and in code the PR already owns. PM decides; neither blocks the merge.

martin-conur and others added 2 commits October 20, 2026 15:39
…real-time): CI builds, tests and lints the Rust crate

The PM moved the CI Rust job from #277 into this PR, so #282 is
verified before it merges. A `rust` job on ubuntu-latest and
macos-latest runs from `monitor/` on stable: fmt --check, build
--locked, test --locked, clippy -D warnings, with Swatinem/rust-cache.
A `rust-msrv` job builds and tests on 1.88, the declared rust-version
(ratatui 0.30's floor), and checked locally with `cargo +1.88 test`.

CHANGELOG and README say so; neither job is a required check yet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…real-time): keep full results that land after a local run

Review round 1 (PR #282):

- `App::offer` dropped a full run's result whenever a local run that
  started later landed first. Heartbeats rewrite session files all the
  time, so that was the normal case and the forge data never reached
  the screen. A full result now replaces local data whatever their start
  times. When it does, its disk state is older than what it replaced, so
  offer returns `TakenNeedsLocal` and main schedules a local rerun.
- `App::failed` marked a fresh screen stale over an older run's failure.
  It now takes the run's start and mode and applies the same rule as
  offer (`App::superseded`). A full run's failure still counts against
  local data, because the forge state on screen is what failed to
  refresh.
- `to_key` gets a test: Ctrl-C is Quit, a bare c is a key, a release is
  ignored.

48 Rust tests. Breaking the full-run exception, the failure guard, the
rerun signal and the Ctrl-C mapping each turned a test red. Also
builds and tests on 1.88.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@martin-conur
martin-conur merged commit 93d3c95 into main Oct 20, 2026
7 checks passed
martin-conur added a commit that referenced this pull request Oct 20, 2026
…k-monitor-queue in the task-monitor docs

After rebasing onto #282: the README's snapshot and `task-monitor`
sections and the `task-monitor` block in all seven workflow templates and
this repo's workflow doc now say the Next section is the queue the PM
writes with `task-monitor-queue`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@martin-conur
martin-conur deleted the task/monitor-tui branch October 20, 2026 19:47
martin-conur added a commit that referenced this pull request Oct 20, 2026
…-monitor-queue (#281)

* task-monitor 3/6: PM short-term queue (task-monitor next …): add task-monitor-queue

New canonical root binary `bin/task-monitor-queue` (named by the PM: the
user-facing `task-monitor` is the #276 front end). The PM writes the board's
Next section with `set <ref>... [--note <ref>=<text>]`, `add`, `rm`,
`clear`, or no subcommand to show it. Each write answers with one line, so
the PM's board cost is one short call. It owns the queue file format that
`task-monitor-snapshot` already reads:
`$TASK_FORCE_HOME/monitor/<repo>/queue`, one ref per line with an optional
tab-separated note.

Refs are compared by their last path segment, so `267`, `#267` and an issue
URL are one item. `task-work` uses that to drop the ref it dispatches, best
effort, so a dispatched item never returns as next once it leaves In flight.

Every PM prompt (four claude `commands/pm.md`, this repo's copy, three kiro
`agents/pm.json`) gains one paragraph on when to call it. All seven
installers link it; README, workflow templates, schema doc and CHANGELOG
updated. New tests/task_monitor_queue.bats (21 tests under /bin/bash,
mutation-checked) and a symlink test.

Closes #275

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* task-monitor 3/6: PM short-term queue (task-monitor next …): address review round 1

- Every leading `#` of a ref is stripped, so `##5` is stored as `5`
  instead of `#5`, which the reader skipped as a comment.
- `--` ends the options, so a ref starting with `-` can be passed.
- A URL ref's query string and fragment are dropped from its key: a Notion
  page URL with and without `?pvs=4` is one item.
- Documented that `--note` splits on the first `=`, so a ref containing
  `=` cannot take a note.
- One comment naming the accepted race between a PM `set` and the
  task-work `rm`.

Three new tests, each mutation-checked.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* task-monitor 3/6: PM short-term queue (task-monitor next …): name task-monitor-queue in the task-monitor docs

After rebasing onto #282: the README's snapshot and `task-monitor`
sections and the `task-monitor` block in all seven workflow templates and
this repo's workflow doc now say the Next section is the queue the PM
writes with `task-monitor-queue`.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
martin-conur added a commit that referenced this pull request Oct 20, 2026
…k task-monitor from every loadout installer

New `lib/monitor-install.sh` (`aw_install_monitor`), called from the
drift-guarded install-shared-symlinks stanza of all seven loadout
installers. With cargo on PATH it runs `cargo build --release` on
monitor/ and links ~/.local/bin/task-monitor to the executable cargo's
JSON output reports, so CARGO_TARGET_DIR is honoured; a binary built
outside the checkout gets a TASK_MONITOR_SNAPSHOT hint. Without cargo it
prints one skip line; a failed build warns. Neither fails the install.

tests/install_symlinks.bats stubs cargo for every installer test and
adds five #277 cases. README, the workflow doc, the seven steering
templates and CHANGELOG updated. The CI Rust job moved to #282.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
martin-conur added a commit that referenced this pull request Oct 20, 2026
…k task-monitor from every loadout installer (#285)

* task-monitor 5/6: install.sh cargo build + CI Rust job: build and link task-monitor from every loadout installer

New `lib/monitor-install.sh` (`aw_install_monitor`), called from the
drift-guarded install-shared-symlinks stanza of all seven loadout
installers. With cargo on PATH it runs `cargo build --release` on
monitor/ and links ~/.local/bin/task-monitor to the executable cargo's
JSON output reports, so CARGO_TARGET_DIR is honoured; a binary built
outside the checkout gets a TASK_MONITOR_SNAPSHOT hint. Without cargo it
prints one skip line; a failed build warns. Neither fails the install.

tests/install_symlinks.bats stubs cargo for every installer test and
adds five #277 cases. README, the workflow doc, the seven steering
templates and CHANGELOG updated. The CI Rust job moved to #282.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* task-monitor 5/6: install.sh cargo build + CI Rust job: build with --locked

The installer's `cargo build` now passes `--locked`, as CI does, so a
user install builds exactly the committed Cargo.lock instead of silently
updating a stale one in the checkout. The retry hint says the same. The
stub-cargo test pinning the build args is updated; dropping `--locked`
turns it red. README, CHANGELOG and the helper's header comment follow.

Review nit on PR #285.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
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.

task-monitor 4/6: ratatui front end (repo view + all-repos overview, real-time)

1 participant