From 9c350c190b099271ed828ebbccfbf3806194f7d3 Mon Sep 17 00:00:00 2001 From: Martin Contreras Date: Tue, 20 Oct 2026 17:15:34 -0300 Subject: [PATCH 1/2] 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 --- .claude/gh-workflow.md | 6 +- CHANGELOG.md | 2 + README.md | 4 +- claude-gh/install.sh | 5 + claude-gh/steering/gh-workflow.example.md | 6 +- claude-jira/install.sh | 5 + claude-jira/steering/jira-workflow.example.md | 6 +- claude-local/install.sh | 5 + .../steering/local-workflow.example.md | 6 +- claude-notion/install.sh | 5 + .../steering/notion-workflow.example.md | 6 +- kiro-gh/install.sh | 5 + kiro-gh/steering/gh-workflow.example.md | 6 +- kiro-local/install.sh | 5 + kiro-local/steering/local-workflow.example.md | 6 +- kiro-notion/install.sh | 5 + .../steering/notion-workflow.example.md | 6 +- lib/monitor-install.sh | 45 ++++++++ tests/install_symlinks.bats | 101 ++++++++++++++++++ 19 files changed, 210 insertions(+), 25 deletions(-) create mode 100644 lib/monitor-install.sh diff --git a/.claude/gh-workflow.md b/.claude/gh-workflow.md index 80da836..1ce6ac1 100644 --- a/.claude/gh-workflow.md +++ b/.claude/gh-workflow.md @@ -308,9 +308,9 @@ jump to the item's tab (the worker's, or the PM's for a queued or done item), `a` toggle the overview, `q` quit. `t` stays inside the monitor's own zellij session, and goes to a tab only while the tab at the recorded id still bears the item's name, the check `task-done` makes before closing one (#260). It holds no -task-force logic: every value is a snapshot field. Build it from the task-force -checkout with `cargo build --release --manifest-path monitor/Cargo.toml` -(`install.sh` does not build it yet; #277). Its Next section is the queue the +task-force logic: every value is a snapshot field. A loadout's +`install.sh` builds and links it when `cargo` is on PATH, and skips it with one +line when not (#277). Its Next section is the queue the PM writes with `task-monitor-queue`, below, else the tracker's backlog. `task-monitor-queue` — the PM's short-term queue, the board's **Next** (#275) diff --git a/CHANGELOG.md b/CHANGELOG.md index b6fec72..a129d70 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -199,6 +199,8 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), ### Added +- **`install.sh` builds and links `task-monitor` (#277, part of #272).** Every loadout's installer now runs `cargo build --release` on `monitor/` when `cargo` is on `PATH` and links `~/.local/bin/task-monitor`. Without cargo it prints one line, `Skipped: task-monitor (no cargo on PATH; install Rust >= 1.88 from https://rustup.rs and re-run this installer)`, and carries on: `task-monitor-snapshot` and the other bash commands install either way. A failed build warns with the command to retry and does not fail the install. The binary's path is read off cargo's JSON output rather than assumed, so a `CARGO_TARGET_DIR` or `build.target-dir` is honoured; when that puts the binary outside the checkout, where it cannot find `bin/task-monitor-snapshot` beside itself, the installer says to export `TASK_MONITOR_SNAPSHOT`. The logic is `lib/monitor-install.sh`, called from the drift-guarded shared install stanza, so all seven loadouts get it. `install.sh all` runs the build seven times; after the first, cargo finds it up to date. Prebuilt binaries remain out of scope. Docs: the README's `task-monitor` section, `.claude/gh-workflow.md` and the seven steering templates no longer say `install.sh` does not build it. `tests/install_symlinks.bats` puts a stub `cargo` on `PATH` for every installer test, so none runs a real build, and adds five tests: the build arguments and the link to the reported executable, the one-line skip with no cargo anywhere on `PATH`, a failed build, a reported executable that is missing, and the outside-the-checkout hint. Six mutations of the helper each turned at least one of them red. **Upgrading: re-run the loadout's `install.sh`** to build and link the monitor, and again after a `git pull` that changes `monitor/`. No `task-init` re-run is needed; the steering template edit only touches the description, so re-run `task-init ` only if you want the refreshed workflow doc. + - **`task-monitor`: the board, live in the terminal (#276, epic #272 4/6).** A Rust + ratatui front end in `monitor/` over `task-monitor-snapshot --json`, with the two screens the epic drew. The **repo view** shows In flight (number, title, status word with its PR, the plain description, the `▶` / `⏸` / `❓` activity icon with the time since the session was last heard from, and how many reviewers are on it), Next (the PM queue or the tracker backlog, with notes) and Done (PR, short merge SHA, age, and the two-line summary). The **overview** (`a`, or `--all`, and the default outside a repo) has one row per repo: the attention light, the signals in words, the in-flight count, and the zellij session it runs in. Keys: `↑↓` move, `⏎` expands an item in place (a done item to its full PR body, scrollable) or enters a repo, `o` opens the PR or issue, `t` jumps to the item's tab, `a` toggles the overview, `q` quits. - **Real time, and never waiting on GitHub.** Filesystem events under `$TASK_FORCE_HOME/radio` (except its `log`), `$TASK_FORCE_HOME/monitor` and each repo's worktree base (`..info` sidecars only) trigger a `--local` snapshot, debounced on the trailing edge at 400ms and capped at 2s. One local run at a time; an event during a run reruns it afterwards. A full run, the only one that calls `gh`, runs at start and then 45s after the previous one finished (`--poll SECS`, 30-600). Both run off the UI thread, with timeouts (20s / 180s) that kill a hung collector. A result never replaces one from a run that started later, with one exception: a full result replaces local data even when the local run started after it, since a local run cannot ask the forge. Heartbeats rewrite session files constantly, so a local run landing mid-full-run is the normal case, and without the exception every full result would be dropped. The full result's disk state is then older than what it replaced, so a local rerun is scheduled at once. A failed run marks the screen stale under the same rule, so an old run's timeout cannot mark a fresh screen stale. - **Never blank.** A failed or timed-out snapshot keeps the last good one on screen; the header turns to `stale ` and the footer gives the collector's own last stderr line. `forge ` marks a repo whose PR state is from the last full run. diff --git a/README.md b/README.md index 5e46442..0b91fcf 100644 --- a/README.md +++ b/README.md @@ -450,7 +450,9 @@ A ref is whatever the tracker uses: an issue number, a Jira key, a Notion page U - **`t` stays in this zellij session.** Each overview row names its repo's zellij session; `zellij action` cannot switch sessions, so a tab elsewhere is named, not attempted. Within the session, `t` goes to the recorded `TAB_ID` only while that tab still bears the item's name, the check `task-done` makes before it closes a tab (#260). - **No task-force logic in Rust.** Every word comes from the snapshot; status words are a lookup of its `status` field. -Build it from the task-force checkout with `cargo build --release --manifest-path monitor/Cargo.toml` and run `monitor/target/release/task-monitor`; it finds `bin/task-monitor-snapshot` beside it, and `$TASK_MONITOR_SNAPSHOT` overrides that. `install.sh` does not build or link it yet (#277). It needs Rust 1.88 or newer; CI builds, tests and lints it on Linux and macOS. +**Installing.** Every loadout's `install.sh` builds it (`cargo build --release`) and links `~/.local/bin/task-monitor` when `cargo` is on `PATH`. Without cargo it prints one line saying so and skips the monitor; `task-monitor-snapshot` and the rest of the bash commands install either way. A failed build warns, leaves the monitor unlinked and lets the install finish. It needs Rust 1.88 or newer ([rustup.rs](https://rustup.rs)); install Rust and re-run the installer to add the monitor later. Re-running the installer is also how to pick up a newer monitor after a `git pull`, since the link points at the built binary, not at source. There are no prebuilt binaries yet. + +The binary finds `bin/task-monitor-snapshot` beside its real path in the checkout, and `$TASK_MONITOR_SNAPSHOT` overrides that. A `CARGO_TARGET_DIR` (or `build.target-dir`) outside the checkout breaks that lookup, so the installer reads the binary's path from cargo and, when it lands elsewhere, says to export `TASK_MONITOR_SNAPSHOT`. To build by hand: `cargo build --release --manifest-path monitor/Cargo.toml`, then run `monitor/target/release/task-monitor`. CI builds, tests and lints it on Linux and macOS. --- diff --git a/claude-gh/install.sh b/claude-gh/install.sh index f427e2e..b800cc0 100755 --- a/claude-gh/install.sh +++ b/claude-gh/install.sh @@ -47,6 +47,11 @@ ln -sf "$SCRIPT_DIR/../bin/task-monitor-snapshot" ~/.local/bin/task-monitor-snap echo " ✓ Script: task-monitor-snapshot (canonical)"; sleep 0.05 ln -sf "$SCRIPT_DIR/../bin/task-monitor-queue" ~/.local/bin/task-monitor-queue echo " ✓ Script: task-monitor-queue (canonical)"; sleep 0.05 +# task-monitor is the one compiled command (#277): built here when cargo is on +# PATH, skipped with one line when it is not. +# shellcheck source=lib/monitor-install.sh +source "$SCRIPT_DIR/../lib/monitor-install.sh" +aw_install_monitor "$SCRIPT_DIR/.." # endregion:install-shared-symlinks ln -sf "$SCRIPT_DIR/../bin/task-reviewer" ~/.local/bin/task-reviewer echo " ✓ Script: task-reviewer (canonical + kiro routing)"; sleep 0.05 diff --git a/claude-gh/steering/gh-workflow.example.md b/claude-gh/steering/gh-workflow.example.md index 757e881..694d72d 100644 --- a/claude-gh/steering/gh-workflow.example.md +++ b/claude-gh/steering/gh-workflow.example.md @@ -304,9 +304,9 @@ jump to the item's tab (the worker's, or the PM's for a queued or done item), `a` toggle the overview, `q` quit. `t` stays inside the monitor's own zellij session, and goes to a tab only while the tab at the recorded id still bears the item's name, the check `task-done` makes before closing one (#260). It holds no -task-force logic: every value is a snapshot field. Build it from the task-force -checkout with `cargo build --release --manifest-path monitor/Cargo.toml` -(`install.sh` does not build it yet; #277). Its Next section is the queue the +task-force logic: every value is a snapshot field. A loadout's +`install.sh` builds and links it when `cargo` is on PATH, and skips it with one +line when not (#277). Its Next section is the queue the PM writes with `task-monitor-queue`, below, else the tracker's backlog. `task-monitor-queue` — the PM's short-term queue, the board's **Next** (#275) diff --git a/claude-jira/install.sh b/claude-jira/install.sh index 569ea52..93833bd 100755 --- a/claude-jira/install.sh +++ b/claude-jira/install.sh @@ -47,6 +47,11 @@ ln -sf "$SCRIPT_DIR/../bin/task-monitor-snapshot" ~/.local/bin/task-monitor-snap echo " ✓ Script: task-monitor-snapshot (canonical)"; sleep 0.05 ln -sf "$SCRIPT_DIR/../bin/task-monitor-queue" ~/.local/bin/task-monitor-queue echo " ✓ Script: task-monitor-queue (canonical)"; sleep 0.05 +# task-monitor is the one compiled command (#277): built here when cargo is on +# PATH, skipped with one line when it is not. +# shellcheck source=lib/monitor-install.sh +source "$SCRIPT_DIR/../lib/monitor-install.sh" +aw_install_monitor "$SCRIPT_DIR/.." # endregion:install-shared-symlinks ln -sf "$SCRIPT_DIR/../bin/task-reviewer" ~/.local/bin/task-reviewer echo " ✓ Script: task-reviewer (canonical + kiro routing)"; sleep 0.05 diff --git a/claude-jira/steering/jira-workflow.example.md b/claude-jira/steering/jira-workflow.example.md index 110509f..1011759 100644 --- a/claude-jira/steering/jira-workflow.example.md +++ b/claude-jira/steering/jira-workflow.example.md @@ -304,9 +304,9 @@ jump to the item's tab (the worker's, or the PM's for a queued or done item), `a` toggle the overview, `q` quit. `t` stays inside the monitor's own zellij session, and goes to a tab only while the tab at the recorded id still bears the item's name, the check `task-done` makes before closing one (#260). It holds no -task-force logic: every value is a snapshot field. Build it from the task-force -checkout with `cargo build --release --manifest-path monitor/Cargo.toml` -(`install.sh` does not build it yet; #277). Its Next section is the queue the +task-force logic: every value is a snapshot field. A loadout's +`install.sh` builds and links it when `cargo` is on PATH, and skips it with one +line when not (#277). Its Next section is the queue the PM writes with `task-monitor-queue`, below, else the tracker's backlog. `task-monitor-queue` — the PM's short-term queue, the board's **Next** (#275) diff --git a/claude-local/install.sh b/claude-local/install.sh index c101d5c..13077f3 100755 --- a/claude-local/install.sh +++ b/claude-local/install.sh @@ -47,6 +47,11 @@ ln -sf "$SCRIPT_DIR/../bin/task-monitor-snapshot" ~/.local/bin/task-monitor-snap echo " ✓ Script: task-monitor-snapshot (canonical)"; sleep 0.05 ln -sf "$SCRIPT_DIR/../bin/task-monitor-queue" ~/.local/bin/task-monitor-queue echo " ✓ Script: task-monitor-queue (canonical)"; sleep 0.05 +# task-monitor is the one compiled command (#277): built here when cargo is on +# PATH, skipped with one line when it is not. +# shellcheck source=lib/monitor-install.sh +source "$SCRIPT_DIR/../lib/monitor-install.sh" +aw_install_monitor "$SCRIPT_DIR/.." # endregion:install-shared-symlinks ln -sf "$SCRIPT_DIR/../bin/task-reviewer" ~/.local/bin/task-reviewer echo " ✓ Script: task-reviewer (canonical + kiro routing)"; sleep 0.05 diff --git a/claude-local/steering/local-workflow.example.md b/claude-local/steering/local-workflow.example.md index 28ecbbc..a1344dc 100644 --- a/claude-local/steering/local-workflow.example.md +++ b/claude-local/steering/local-workflow.example.md @@ -326,9 +326,9 @@ jump to the item's tab (the worker's, or the PM's for a queued or done item), `a` toggle the overview, `q` quit. `t` stays inside the monitor's own zellij session, and goes to a tab only while the tab at the recorded id still bears the item's name, the check `task-done` makes before closing one (#260). It holds no -task-force logic: every value is a snapshot field. Build it from the task-force -checkout with `cargo build --release --manifest-path monitor/Cargo.toml` -(`install.sh` does not build it yet; #277). Its Next section is the queue the +task-force logic: every value is a snapshot field. A loadout's +`install.sh` builds and links it when `cargo` is on PATH, and skips it with one +line when not (#277). Its Next section is the queue the PM writes with `task-monitor-queue`, below, else the tracker's backlog. `task-monitor-queue` — the PM's short-term queue, the board's **Next** (#275) diff --git a/claude-notion/install.sh b/claude-notion/install.sh index 3a1c163..d4282e2 100755 --- a/claude-notion/install.sh +++ b/claude-notion/install.sh @@ -47,6 +47,11 @@ ln -sf "$SCRIPT_DIR/../bin/task-monitor-snapshot" ~/.local/bin/task-monitor-snap echo " ✓ Script: task-monitor-snapshot (canonical)"; sleep 0.05 ln -sf "$SCRIPT_DIR/../bin/task-monitor-queue" ~/.local/bin/task-monitor-queue echo " ✓ Script: task-monitor-queue (canonical)"; sleep 0.05 +# task-monitor is the one compiled command (#277): built here when cargo is on +# PATH, skipped with one line when it is not. +# shellcheck source=lib/monitor-install.sh +source "$SCRIPT_DIR/../lib/monitor-install.sh" +aw_install_monitor "$SCRIPT_DIR/.." # endregion:install-shared-symlinks ln -sf "$SCRIPT_DIR/../bin/task-reviewer" ~/.local/bin/task-reviewer echo " ✓ Script: task-reviewer (canonical + kiro routing)"; sleep 0.05 diff --git a/claude-notion/steering/notion-workflow.example.md b/claude-notion/steering/notion-workflow.example.md index e1cf38e..13d91ce 100644 --- a/claude-notion/steering/notion-workflow.example.md +++ b/claude-notion/steering/notion-workflow.example.md @@ -321,9 +321,9 @@ jump to the item's tab (the worker's, or the PM's for a queued or done item), `a` toggle the overview, `q` quit. `t` stays inside the monitor's own zellij session, and goes to a tab only while the tab at the recorded id still bears the item's name, the check `task-done` makes before closing one (#260). It holds no -task-force logic: every value is a snapshot field. Build it from the task-force -checkout with `cargo build --release --manifest-path monitor/Cargo.toml` -(`install.sh` does not build it yet; #277). Its Next section is the queue the +task-force logic: every value is a snapshot field. A loadout's +`install.sh` builds and links it when `cargo` is on PATH, and skips it with one +line when not (#277). Its Next section is the queue the PM writes with `task-monitor-queue`, below, else the tracker's backlog. `task-monitor-queue` — the PM's short-term queue, the board's **Next** (#275) diff --git a/kiro-gh/install.sh b/kiro-gh/install.sh index dbd0ea4..a722bcc 100755 --- a/kiro-gh/install.sh +++ b/kiro-gh/install.sh @@ -47,6 +47,11 @@ ln -sf "$SCRIPT_DIR/../bin/task-monitor-snapshot" ~/.local/bin/task-monitor-snap echo " ✓ Script: task-monitor-snapshot (canonical)"; sleep 0.05 ln -sf "$SCRIPT_DIR/../bin/task-monitor-queue" ~/.local/bin/task-monitor-queue echo " ✓ Script: task-monitor-queue (canonical)"; sleep 0.05 +# task-monitor is the one compiled command (#277): built here when cargo is on +# PATH, skipped with one line when it is not. +# shellcheck source=lib/monitor-install.sh +source "$SCRIPT_DIR/../lib/monitor-install.sh" +aw_install_monitor "$SCRIPT_DIR/.." # endregion:install-shared-symlinks ln -sf "$SCRIPT_DIR/../bin/task-reviewer" ~/.local/bin/task-reviewer echo " ✓ Script: task-reviewer (canonical + kiro routing)"; sleep 0.05 diff --git a/kiro-gh/steering/gh-workflow.example.md b/kiro-gh/steering/gh-workflow.example.md index 4c91bdd..6d3dbf7 100644 --- a/kiro-gh/steering/gh-workflow.example.md +++ b/kiro-gh/steering/gh-workflow.example.md @@ -302,9 +302,9 @@ jump to the item's tab (the worker's, or the PM's for a queued or done item), `a` toggle the overview, `q` quit. `t` stays inside the monitor's own zellij session, and goes to a tab only while the tab at the recorded id still bears the item's name, the check `task-done` makes before closing one (#260). It holds no -task-force logic: every value is a snapshot field. Build it from the task-force -checkout with `cargo build --release --manifest-path monitor/Cargo.toml` -(`install.sh` does not build it yet; #277). Its Next section is the queue the +task-force logic: every value is a snapshot field. A loadout's +`install.sh` builds and links it when `cargo` is on PATH, and skips it with one +line when not (#277). Its Next section is the queue the PM writes with `task-monitor-queue`, below, else the tracker's backlog. `task-monitor-queue` — the PM's short-term queue, the board's **Next** (#275) diff --git a/kiro-local/install.sh b/kiro-local/install.sh index f57ef1b..1966ac6 100755 --- a/kiro-local/install.sh +++ b/kiro-local/install.sh @@ -47,6 +47,11 @@ ln -sf "$SCRIPT_DIR/../bin/task-monitor-snapshot" ~/.local/bin/task-monitor-snap echo " ✓ Script: task-monitor-snapshot (canonical)"; sleep 0.05 ln -sf "$SCRIPT_DIR/../bin/task-monitor-queue" ~/.local/bin/task-monitor-queue echo " ✓ Script: task-monitor-queue (canonical)"; sleep 0.05 +# task-monitor is the one compiled command (#277): built here when cargo is on +# PATH, skipped with one line when it is not. +# shellcheck source=lib/monitor-install.sh +source "$SCRIPT_DIR/../lib/monitor-install.sh" +aw_install_monitor "$SCRIPT_DIR/.." # endregion:install-shared-symlinks # Ensure ~/.local/bin is on PATH diff --git a/kiro-local/steering/local-workflow.example.md b/kiro-local/steering/local-workflow.example.md index 98124b1..32fd612 100644 --- a/kiro-local/steering/local-workflow.example.md +++ b/kiro-local/steering/local-workflow.example.md @@ -324,9 +324,9 @@ jump to the item's tab (the worker's, or the PM's for a queued or done item), `a` toggle the overview, `q` quit. `t` stays inside the monitor's own zellij session, and goes to a tab only while the tab at the recorded id still bears the item's name, the check `task-done` makes before closing one (#260). It holds no -task-force logic: every value is a snapshot field. Build it from the task-force -checkout with `cargo build --release --manifest-path monitor/Cargo.toml` -(`install.sh` does not build it yet; #277). Its Next section is the queue the +task-force logic: every value is a snapshot field. A loadout's +`install.sh` builds and links it when `cargo` is on PATH, and skips it with one +line when not (#277). Its Next section is the queue the PM writes with `task-monitor-queue`, below, else the tracker's backlog. `task-monitor-queue` — the PM's short-term queue, the board's **Next** (#275) diff --git a/kiro-notion/install.sh b/kiro-notion/install.sh index 035b499..8e54d0f 100755 --- a/kiro-notion/install.sh +++ b/kiro-notion/install.sh @@ -47,6 +47,11 @@ ln -sf "$SCRIPT_DIR/../bin/task-monitor-snapshot" ~/.local/bin/task-monitor-snap echo " ✓ Script: task-monitor-snapshot (canonical)"; sleep 0.05 ln -sf "$SCRIPT_DIR/../bin/task-monitor-queue" ~/.local/bin/task-monitor-queue echo " ✓ Script: task-monitor-queue (canonical)"; sleep 0.05 +# task-monitor is the one compiled command (#277): built here when cargo is on +# PATH, skipped with one line when it is not. +# shellcheck source=lib/monitor-install.sh +source "$SCRIPT_DIR/../lib/monitor-install.sh" +aw_install_monitor "$SCRIPT_DIR/.." # endregion:install-shared-symlinks # Ensure ~/.local/bin is on PATH diff --git a/kiro-notion/steering/notion-workflow.example.md b/kiro-notion/steering/notion-workflow.example.md index 66633a8..81e2db4 100644 --- a/kiro-notion/steering/notion-workflow.example.md +++ b/kiro-notion/steering/notion-workflow.example.md @@ -299,9 +299,9 @@ jump to the item's tab (the worker's, or the PM's for a queued or done item), `a` toggle the overview, `q` quit. `t` stays inside the monitor's own zellij session, and goes to a tab only while the tab at the recorded id still bears the item's name, the check `task-done` makes before closing one (#260). It holds no -task-force logic: every value is a snapshot field. Build it from the task-force -checkout with `cargo build --release --manifest-path monitor/Cargo.toml` -(`install.sh` does not build it yet; #277). Its Next section is the queue the +task-force logic: every value is a snapshot field. A loadout's +`install.sh` builds and links it when `cargo` is on PATH, and skips it with one +line when not (#277). Its Next section is the queue the PM writes with `task-monitor-queue`, below, else the tracker's backlog. `task-monitor-queue` — the PM's short-term queue, the board's **Next** (#275) diff --git a/lib/monitor-install.sh b/lib/monitor-install.sh new file mode 100644 index 0000000..bede009 --- /dev/null +++ b/lib/monitor-install.sh @@ -0,0 +1,45 @@ +#!/usr/bin/env bash +# Build and link the task-monitor TUI (#277). Sourced by every loadout's +# install.sh from inside its drift-guarded install-shared-symlinks stanza. +# +# Exports: +# aw_install_monitor +# +# With `cargo` on PATH it runs `cargo build --release` on /monitor and +# links the binary into ~/.local/bin/task-monitor. Without cargo it prints one +# line saying how to get the monitor and links nothing. A failed build warns +# and links nothing. Neither case fails the install: the bash commands +# (task-monitor-snapshot included) are what the rest of task-force runs on, and +# the monitor is a view over them. +# +# The binary's path is read off cargo's own JSON (the compiler-artifact +# message's "executable"), not assumed to be monitor/target/release: a +# CARGO_TARGET_DIR or a build.target-dir in ~/.cargo/config.toml moves it. The +# binary finds bin/task-monitor-snapshot through its canonicalized current_exe, +# which only works while it stays inside the checkout, so a target dir outside +# it needs $TASK_MONITOR_SNAPSHOT; the warning below says so. + +aw_install_monitor() { + local root + root=$(cd "$1" && pwd) + local manifest="$root/monitor/Cargo.toml" + local binary + if ! command -v cargo >/dev/null 2>&1; then + echo " - Skipped: task-monitor (no cargo on PATH; install Rust >= 1.88 from https://rustup.rs and re-run this installer)" + return 0 + fi + echo " … Building task-monitor (cargo build --release; the first build takes a minute or two)" + binary=$(cargo build --release --quiet --message-format=json-render-diagnostics \ + --manifest-path "$manifest" \ + | sed -n 's/.*"executable":"\([^"]*\)".*/\1/p' | tail -n 1) || binary="" + if [[ -z "$binary" || ! -x "$binary" ]]; then + echo " ! task-monitor build failed; not linked. Retry with: cargo build --release --manifest-path $manifest" >&2 + return 0 + fi + ln -sf "$binary" ~/.local/bin/task-monitor + echo " ✓ Binary: task-monitor (Rust TUI, built from monitor/)" + case "$(cd "$(dirname "$binary")" && pwd -P)/" in + "$(cd "$root" && pwd -P)/"*) ;; + *) echo " ! task-monitor was built outside the checkout ($binary); export TASK_MONITOR_SNAPSHOT=$root/bin/task-monitor-snapshot so it finds its data" >&2 ;; + esac +} diff --git a/tests/install_symlinks.bats b/tests/install_symlinks.bats index 9772544..337f236 100644 --- a/tests/install_symlinks.bats +++ b/tests/install_symlinks.bats @@ -11,6 +11,41 @@ load helpers/common setup() { FAKE_HOME=$(mktemp -d) export HOME="$FAKE_HOME" + # Every installer builds task-monitor when cargo is on PATH (#277). A stub + # stands in for it so no test here runs a real release build; see + # stub_cargo for what it reports. + STUB_BIN="$FAKE_HOME/stub-bin" + mkdir -p "$STUB_BIN" + stub_cargo "$REPO_ROOT_REAL/bin/radio" + export PATH="$STUB_BIN:$PATH" +} + +# Write a fake `cargo` that logs its argv to $FAKE_HOME/cargo.args and answers +# the way `cargo build --message-format=json-render-diagnostics` does: one +# compiler-artifact line naming $1 as the built executable. The default points +# at an executable already in the checkout, so the build is never faked into +# the real monitor/target. With $1 = "fail" it prints nothing and exits 101. +stub_cargo() { + local exe="$1" + if [[ "$exe" == fail ]]; then + printf '#!/bin/sh\nprintf "%%s\\n" "$*" > "%s/cargo.args"\necho "error: could not compile" >&2\nexit 101\n' \ + "$FAKE_HOME" > "$STUB_BIN/cargo" + else + printf '#!/bin/sh\nprintf "%%s\\n" "$*" > "%s/cargo.args"\necho '"'"'{"reason":"compiler-artifact","executable":"%s","fresh":true}'"'"'\necho '"'"'{"reason":"build-finished","success":true}'"'"'\n' \ + "$FAKE_HOME" "$exe" > "$STUB_BIN/cargo" + fi + chmod +x "$STUB_BIN/cargo" +} + +# $PATH minus every directory holding a `cargo`, the stub's included. +path_without_cargo() { + local dir out="" + local IFS=: + for dir in $PATH; do + [[ -x "$dir/cargo" ]] && continue + out="${out:+$out:}$dir" + done + printf '%s' "$out" } teardown() { @@ -136,3 +171,69 @@ resolve_link() { esac done } + +# task-monitor (#277). The stanza is drift-guarded, so one loadout per agent +# proves all seven, as for task-monitor-snapshot above. +@test "install builds task-monitor with cargo and links the executable cargo reports (#277)" { + local impl + for impl in claude-gh kiro-local; do + rm -rf "$HOME/.local/bin" "$FAKE_HOME/cargo.args" + run bash "$REPO_ROOT_REAL/$impl/install.sh" + assert_success + assert_output --partial "✓ Binary: task-monitor" + assert_equal "$(cat "$FAKE_HOME/cargo.args")" \ + "build --release --quiet --message-format=json-render-diagnostics --manifest-path $REPO_ROOT_REAL/monitor/Cargo.toml" + assert [ -L "$HOME/.local/bin/task-monitor" ] + assert_equal "$(readlink "$HOME/.local/bin/task-monitor")" "$REPO_ROOT_REAL/bin/radio" + refute_output --partial "TASK_MONITOR_SNAPSHOT" + done +} + +@test "install without cargo skips task-monitor in one line and still links the bash commands (#277)" { + local impl + for impl in claude-gh kiro-local; do + rm -rf "$HOME/.local/bin" + PATH=$(path_without_cargo) run bash "$REPO_ROOT_REAL/$impl/install.sh" + assert_success + assert_equal "$(grep -c 'task-monitor (' <<<"$output" | tr -d ' ')" "1" + assert_line --partial "Skipped: task-monitor (no cargo on PATH; install Rust >= 1.88 from https://rustup.rs" + assert [ ! -e "$HOME/.local/bin/task-monitor" ] + assert [ ! -L "$HOME/.local/bin/task-monitor" ] + assert [ ! -e "$FAKE_HOME/cargo.args" ] + assert [ -L "$HOME/.local/bin/task-monitor-snapshot" ] + assert [ -L "$HOME/.local/bin/task-monitor-queue" ] + done +} + +@test "a failed task-monitor build warns, links nothing and does not fail the install (#277)" { + stub_cargo fail + run bash "$REPO_ROOT_REAL/claude-gh/install.sh" + assert_success + assert_output --partial "task-monitor build failed; not linked. Retry with: cargo build --release --manifest-path" + assert [ ! -L "$HOME/.local/bin/task-monitor" ] + # The rest of the install ran past it. + assert [ -L "$HOME/.local/bin/task-monitor-snapshot" ] + assert [ -L "$HOME/.local/bin/task-reviewer" ] + assert_output --partial "Done. Next steps:" +} + +@test "a cargo-reported executable that is not there is a failed build, not a dangling link (#277)" { + stub_cargo "$FAKE_HOME/nowhere/task-monitor" + run bash "$REPO_ROOT_REAL/claude-gh/install.sh" + assert_success + assert_output --partial "task-monitor build failed" + assert [ ! -L "$HOME/.local/bin/task-monitor" ] +} + +@test "task-monitor built outside the checkout is linked with a TASK_MONITOR_SNAPSHOT hint (#277)" { + # A CARGO_TARGET_DIR elsewhere: the binary can no longer find + # bin/task-monitor-snapshot beside its canonical path. + mkdir -p "$FAKE_HOME/target/release" + printf '#!/bin/sh\n' > "$FAKE_HOME/target/release/task-monitor" + chmod +x "$FAKE_HOME/target/release/task-monitor" + stub_cargo "$FAKE_HOME/target/release/task-monitor" + run bash "$REPO_ROOT_REAL/claude-gh/install.sh" + assert_success + assert_equal "$(readlink "$HOME/.local/bin/task-monitor")" "$FAKE_HOME/target/release/task-monitor" + assert_output --partial "export TASK_MONITOR_SNAPSHOT=$REPO_ROOT_REAL/bin/task-monitor-snapshot" +} From da3e1d0a4d9a71da97d00b4c3f94270d22692a9e Mon Sep 17 00:00:00 2001 From: Martin Contreras Date: Tue, 20 Oct 2026 18:02:44 -0300 Subject: [PATCH 2/2] 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 --- CHANGELOG.md | 2 +- README.md | 2 +- lib/monitor-install.sh | 9 +++++---- tests/install_symlinks.bats | 4 ++-- 4 files changed, 9 insertions(+), 8 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a129d70..4d2d0cf 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -199,7 +199,7 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/), ### Added -- **`install.sh` builds and links `task-monitor` (#277, part of #272).** Every loadout's installer now runs `cargo build --release` on `monitor/` when `cargo` is on `PATH` and links `~/.local/bin/task-monitor`. Without cargo it prints one line, `Skipped: task-monitor (no cargo on PATH; install Rust >= 1.88 from https://rustup.rs and re-run this installer)`, and carries on: `task-monitor-snapshot` and the other bash commands install either way. A failed build warns with the command to retry and does not fail the install. The binary's path is read off cargo's JSON output rather than assumed, so a `CARGO_TARGET_DIR` or `build.target-dir` is honoured; when that puts the binary outside the checkout, where it cannot find `bin/task-monitor-snapshot` beside itself, the installer says to export `TASK_MONITOR_SNAPSHOT`. The logic is `lib/monitor-install.sh`, called from the drift-guarded shared install stanza, so all seven loadouts get it. `install.sh all` runs the build seven times; after the first, cargo finds it up to date. Prebuilt binaries remain out of scope. Docs: the README's `task-monitor` section, `.claude/gh-workflow.md` and the seven steering templates no longer say `install.sh` does not build it. `tests/install_symlinks.bats` puts a stub `cargo` on `PATH` for every installer test, so none runs a real build, and adds five tests: the build arguments and the link to the reported executable, the one-line skip with no cargo anywhere on `PATH`, a failed build, a reported executable that is missing, and the outside-the-checkout hint. Six mutations of the helper each turned at least one of them red. **Upgrading: re-run the loadout's `install.sh`** to build and link the monitor, and again after a `git pull` that changes `monitor/`. No `task-init` re-run is needed; the steering template edit only touches the description, so re-run `task-init ` only if you want the refreshed workflow doc. +- **`install.sh` builds and links `task-monitor` (#277, part of #272).** Every loadout's installer now runs `cargo build --release --locked` on `monitor/`, so it builds exactly the committed `Cargo.lock` as CI does, when `cargo` is on `PATH` and links `~/.local/bin/task-monitor`. Without cargo it prints one line, `Skipped: task-monitor (no cargo on PATH; install Rust >= 1.88 from https://rustup.rs and re-run this installer)`, and carries on: `task-monitor-snapshot` and the other bash commands install either way. A failed build warns with the command to retry and does not fail the install. The binary's path is read off cargo's JSON output rather than assumed, so a `CARGO_TARGET_DIR` or `build.target-dir` is honoured; when that puts the binary outside the checkout, where it cannot find `bin/task-monitor-snapshot` beside itself, the installer says to export `TASK_MONITOR_SNAPSHOT`. The logic is `lib/monitor-install.sh`, called from the drift-guarded shared install stanza, so all seven loadouts get it. `install.sh all` runs the build seven times; after the first, cargo finds it up to date. Prebuilt binaries remain out of scope. Docs: the README's `task-monitor` section, `.claude/gh-workflow.md` and the seven steering templates no longer say `install.sh` does not build it. `tests/install_symlinks.bats` puts a stub `cargo` on `PATH` for every installer test, so none runs a real build, and adds five tests: the build arguments and the link to the reported executable, the one-line skip with no cargo anywhere on `PATH`, a failed build, a reported executable that is missing, and the outside-the-checkout hint. Six mutations of the helper each turned at least one of them red. **Upgrading: re-run the loadout's `install.sh`** to build and link the monitor, and again after a `git pull` that changes `monitor/`. No `task-init` re-run is needed; the steering template edit only touches the description, so re-run `task-init ` only if you want the refreshed workflow doc. - **`task-monitor`: the board, live in the terminal (#276, epic #272 4/6).** A Rust + ratatui front end in `monitor/` over `task-monitor-snapshot --json`, with the two screens the epic drew. The **repo view** shows In flight (number, title, status word with its PR, the plain description, the `▶` / `⏸` / `❓` activity icon with the time since the session was last heard from, and how many reviewers are on it), Next (the PM queue or the tracker backlog, with notes) and Done (PR, short merge SHA, age, and the two-line summary). The **overview** (`a`, or `--all`, and the default outside a repo) has one row per repo: the attention light, the signals in words, the in-flight count, and the zellij session it runs in. Keys: `↑↓` move, `⏎` expands an item in place (a done item to its full PR body, scrollable) or enters a repo, `o` opens the PR or issue, `t` jumps to the item's tab, `a` toggles the overview, `q` quits. - **Real time, and never waiting on GitHub.** Filesystem events under `$TASK_FORCE_HOME/radio` (except its `log`), `$TASK_FORCE_HOME/monitor` and each repo's worktree base (`..info` sidecars only) trigger a `--local` snapshot, debounced on the trailing edge at 400ms and capped at 2s. One local run at a time; an event during a run reruns it afterwards. A full run, the only one that calls `gh`, runs at start and then 45s after the previous one finished (`--poll SECS`, 30-600). Both run off the UI thread, with timeouts (20s / 180s) that kill a hung collector. A result never replaces one from a run that started later, with one exception: a full result replaces local data even when the local run started after it, since a local run cannot ask the forge. Heartbeats rewrite session files constantly, so a local run landing mid-full-run is the normal case, and without the exception every full result would be dropped. The full result's disk state is then older than what it replaced, so a local rerun is scheduled at once. A failed run marks the screen stale under the same rule, so an old run's timeout cannot mark a fresh screen stale. diff --git a/README.md b/README.md index 0b91fcf..4d81391 100644 --- a/README.md +++ b/README.md @@ -450,7 +450,7 @@ A ref is whatever the tracker uses: an issue number, a Jira key, a Notion page U - **`t` stays in this zellij session.** Each overview row names its repo's zellij session; `zellij action` cannot switch sessions, so a tab elsewhere is named, not attempted. Within the session, `t` goes to the recorded `TAB_ID` only while that tab still bears the item's name, the check `task-done` makes before it closes a tab (#260). - **No task-force logic in Rust.** Every word comes from the snapshot; status words are a lookup of its `status` field. -**Installing.** Every loadout's `install.sh` builds it (`cargo build --release`) and links `~/.local/bin/task-monitor` when `cargo` is on `PATH`. Without cargo it prints one line saying so and skips the monitor; `task-monitor-snapshot` and the rest of the bash commands install either way. A failed build warns, leaves the monitor unlinked and lets the install finish. It needs Rust 1.88 or newer ([rustup.rs](https://rustup.rs)); install Rust and re-run the installer to add the monitor later. Re-running the installer is also how to pick up a newer monitor after a `git pull`, since the link points at the built binary, not at source. There are no prebuilt binaries yet. +**Installing.** Every loadout's `install.sh` builds it (`cargo build --release --locked`, so exactly the committed `Cargo.lock`) and links `~/.local/bin/task-monitor` when `cargo` is on `PATH`. Without cargo it prints one line saying so and skips the monitor; `task-monitor-snapshot` and the rest of the bash commands install either way. A failed build warns, leaves the monitor unlinked and lets the install finish. It needs Rust 1.88 or newer ([rustup.rs](https://rustup.rs)); install Rust and re-run the installer to add the monitor later. Re-running the installer is also how to pick up a newer monitor after a `git pull`, since the link points at the built binary, not at source. There are no prebuilt binaries yet. The binary finds `bin/task-monitor-snapshot` beside its real path in the checkout, and `$TASK_MONITOR_SNAPSHOT` overrides that. A `CARGO_TARGET_DIR` (or `build.target-dir`) outside the checkout breaks that lookup, so the installer reads the binary's path from cargo and, when it lands elsewhere, says to export `TASK_MONITOR_SNAPSHOT`. To build by hand: `cargo build --release --manifest-path monitor/Cargo.toml`, then run `monitor/target/release/task-monitor`. CI builds, tests and lints it on Linux and macOS. diff --git a/lib/monitor-install.sh b/lib/monitor-install.sh index bede009..454aeef 100644 --- a/lib/monitor-install.sh +++ b/lib/monitor-install.sh @@ -5,8 +5,9 @@ # Exports: # aw_install_monitor # -# With `cargo` on PATH it runs `cargo build --release` on /monitor and -# links the binary into ~/.local/bin/task-monitor. Without cargo it prints one +# With `cargo` on PATH it runs `cargo build --release --locked` on /monitor +# (the committed Cargo.lock, as CI builds it) and links the binary into +# ~/.local/bin/task-monitor. Without cargo it prints one # line saying how to get the monitor and links nothing. A failed build warns # and links nothing. Neither case fails the install: the bash commands # (task-monitor-snapshot included) are what the rest of task-force runs on, and @@ -29,11 +30,11 @@ aw_install_monitor() { return 0 fi echo " … Building task-monitor (cargo build --release; the first build takes a minute or two)" - binary=$(cargo build --release --quiet --message-format=json-render-diagnostics \ + binary=$(cargo build --release --locked --quiet --message-format=json-render-diagnostics \ --manifest-path "$manifest" \ | sed -n 's/.*"executable":"\([^"]*\)".*/\1/p' | tail -n 1) || binary="" if [[ -z "$binary" || ! -x "$binary" ]]; then - echo " ! task-monitor build failed; not linked. Retry with: cargo build --release --manifest-path $manifest" >&2 + echo " ! task-monitor build failed; not linked. Retry with: cargo build --release --locked --manifest-path $manifest" >&2 return 0 fi ln -sf "$binary" ~/.local/bin/task-monitor diff --git a/tests/install_symlinks.bats b/tests/install_symlinks.bats index 337f236..b6d48b9 100644 --- a/tests/install_symlinks.bats +++ b/tests/install_symlinks.bats @@ -182,7 +182,7 @@ resolve_link() { assert_success assert_output --partial "✓ Binary: task-monitor" assert_equal "$(cat "$FAKE_HOME/cargo.args")" \ - "build --release --quiet --message-format=json-render-diagnostics --manifest-path $REPO_ROOT_REAL/monitor/Cargo.toml" + "build --release --locked --quiet --message-format=json-render-diagnostics --manifest-path $REPO_ROOT_REAL/monitor/Cargo.toml" assert [ -L "$HOME/.local/bin/task-monitor" ] assert_equal "$(readlink "$HOME/.local/bin/task-monitor")" "$REPO_ROOT_REAL/bin/radio" refute_output --partial "TASK_MONITOR_SNAPSHOT" @@ -209,7 +209,7 @@ resolve_link() { stub_cargo fail run bash "$REPO_ROOT_REAL/claude-gh/install.sh" assert_success - assert_output --partial "task-monitor build failed; not linked. Retry with: cargo build --release --manifest-path" + assert_output --partial "task-monitor build failed; not linked. Retry with: cargo build --release --locked --manifest-path" assert [ ! -L "$HOME/.local/bin/task-monitor" ] # The rest of the install ran past it. assert [ -L "$HOME/.local/bin/task-monitor-snapshot" ]