Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions .claude/gh-workflow.md
Original file line number Diff line number Diff line change
Expand Up @@ -293,6 +293,24 @@ task-monitor-snapshot --all # every repo radio knows about
task-monitor-snapshot --local # no forge calls: disk + the last full run's cache
```

`task-monitor` — the board, live, in the terminal (#276)

The ratatui front end over that JSON. It opens on the repo view (In flight /
Next / Done) inside a repo, and on the all-repos overview (`a`, or `--all`)
outside one: one row per repo with its attention light, the signal in words,
and the zellij session it runs in. A change under radio's state or a worktree
sidecar refreshes it from disk at once (`--local`); forge state refreshes every
45s (`--poll SECS`). A failed or slow refresh never blanks the screen: the last
good snapshot stays up, marked `stale`. Keys: `↑↓` move, `⏎` expand (a done
item's full PR body, scrollable) or enter a repo, `o` open the PR or issue, `t`
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).

### PM ↔ worker messaging (radio)

Radio is the **canonical** coordination channel between the PM and workers — every
Expand Down
56 changes: 56 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -44,3 +44,59 @@ jobs:

- name: Check loadout drift
run: bash tools/check-drift.sh

# The task-monitor front end (#276). Stable on both OSes; the MSRV job keeps
# `rust-version` in monitor/Cargo.toml honest (ratatui 0.30 needs 1.88).
rust:
name: Rust (${{ matrix.os }})
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
os: [ubuntu-latest, macos-latest]
defaults:
run:
working-directory: monitor
steps:
- uses: actions/checkout@v4

- uses: dtolnay/rust-toolchain@stable
with:
components: clippy, rustfmt

- uses: Swatinem/rust-cache@v2
with:
workspaces: monitor

- name: Format
run: cargo fmt --check

- name: Build
run: cargo build --locked

- name: Test
run: cargo test --locked

- name: Clippy
run: cargo clippy --all-targets --locked -- -D warnings

rust-msrv:
name: Rust MSRV (1.88)
runs-on: ubuntu-latest
defaults:
run:
working-directory: monitor
steps:
- uses: actions/checkout@v4

- uses: dtolnay/rust-toolchain@1.88

- uses: Swatinem/rust-cache@v2
with:
workspaces: monitor

- name: Build and test on the declared rust-version
run: |
grep -qx 'rust-version = "1.88"' Cargo.toml
cargo build --locked
cargo test --locked
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,15 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.1.0/),

### Added

- **`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 (`.<slug>.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 <age>` and the footer gives the collector's own last stderr line. `forge <age>` marks a repo whose PR state is from the last full run.
- **`t` stays in the monitor's zellij session and verifies before it moves.** It uses the worker sidecar's `TAB_ID` (else the session's), or the PM session's for a queued or done item and an overview row. A target in another zellij session is named, not attempted. Otherwise it lists tabs and runs `zellij action go-to-tab-by-id` only when the tab at that id still bears the item's name, by the rule `aw_tab_name_is_slug` applies in `task-done` (#260). A stale id now held by another worker's tab is refused with that tab's name.
- **No task-force logic in Rust.** Every value is a snapshot field, and every field is optional, so a v1 snapshot that grows fields or drops one still renders; a different `schema` is refused, not guessed at. To keep it that way the snapshot gains one field, `worktree_base` (where task-work puts the repo's sidecars), so the watcher does not re-derive task-work's path rule. `docs/task-monitor-schema.md` and its example list it, and a new bats test pins it to the directory holding the sidecars.
- 48 Rust tests: rendering both screens, the expanded view and the stale states from fixture snapshots (including the documented example) on a `TestBackend`; every key on every screen, and Ctrl-C at the key-mapping level; the run-ordering rule, including a local run landing during a full one and a failure older than the screen; the debounce and poll timing; the watcher's path filter, including a `TASK_FORCE_HOME` behind a symlink (FSEvents reports the real path); the collector's flags, failure text, timeout and a snapshot larger than a pipe buffer; and the tab check against a fake zellij. Breaking the tab-name rule, the session check, the run ordering, the local-run coalescing, the log filter, the narrow-terminal fit, the error reset and the timeout kill each turned a test red.

Build it with `cargo build --release --manifest-path monitor/Cargo.toml` and run `monitor/target/release/task-monitor`, which finds the checkout's `bin/task-monitor-snapshot` (`$TASK_MONITOR_SNAPSHOT` overrides). `install.sh` does not build or link it yet (#277). **CI gains a Rust job** on ubuntu-latest and macos-latest, run from `monitor/` on stable: `cargo fmt --check`, `cargo build --locked`, `cargo test --locked` and `cargo clippy --all-targets --locked -- -D warnings`, cached with `Swatinem/rust-cache` (keyed on `monitor/Cargo.lock`). A second job builds and tests on Rust 1.88, the `rust-version` in `monitor/Cargo.toml` and ratatui 0.30's floor, so that pin stays true. Neither job is a required check on `main` yet; branch protection names only the four existing ones. Docs: a README section, and a `task-monitor` paragraph in `.claude/gh-workflow.md` and all seven steering templates. **Upgrading: re-run `task-init <loadout>`** only to refresh the workflow doc; nothing else installer-written changed.

- **`task-monitor-snapshot`: the board as one JSON document — in flight, next, done, and whether a repo needs you (#273, epic #272 1/6).** A new canonical root binary, the data layer the task-monitor front end will render. It prints `"schema": 1` for the current repo, or with `--all` for every repo a radio session file names, and every value in it is read deterministically from radio's state, task-work's `.<slug>.info` sidecars, the tracker and the forge. No model is asked to write or summarise anything. The schema is documented in `docs/task-monitor-schema.md`, with an example taken from this repo.
- **In flight** is every worktree sidecar, joined to its radio session (`STATE=`, heartbeat), its PR (found by the `task/<slug>` head branch on every tracker) and a status word derived from the latest review intent about that PR. Reviewers attach to their PR's item rather than being items.
- **Titles and descriptions** come from three new tracker hooks with `_default.sh` bodies: `aw_tracker_item_describe` (gh: the issue's title and first paragraph; local: the task file; jira / notion: `ref · slug`, since the shell cannot read them), `aw_tracker_pr_item` (gh: `Closes #N`, else the head branch) and `aw_tracker_next_fallback` (gh: open issues on the configured project in none of the three configured statuses, leaving out drafts, archived items and issues with sub-issues, ordered by the project's `Priority` field when there is one; local: the `tasks/` backlog; none on jira / notion). The gh fallback is one paginated GraphQL query, because `gh project item-list` carries neither sub-issue counts nor issue state. Once a PR exists, its title and `## Summary` take over, falling back to the first paragraph after `Closes`. `tests/loadout_modules.bats` checks that all three resolve on every loadout.
Expand Down
22 changes: 22 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -416,6 +416,28 @@ task-monitor-snapshot --local # disk only: no forge calls, merged with

`--local` exists so a front end can refresh on every filesystem event without waiting on GitHub: it shadows `gh` with a function that fails, so every forge read falls back to the cache under `$TASK_FORCE_HOME/monitor/<repo>/`. The schema (`"schema": 1`) is documented in [`docs/task-monitor-schema.md`](docs/task-monitor-schema.md), with a full example beside it.

## `task-monitor` — the board, live

`task-monitor` is the terminal front end over that JSON (Rust + ratatui, in [`monitor/`](monitor/)). Inside a repo it opens on that repo's view; outside one, or with `--all`, on the overview of every repo radio knows about.

```
task-force · PM claude · ✉ 1 unread 12:04 · live
─────────────────────────────────────────────────────────────────────────────
IN FLIGHT (2)
▸ #267 kiro upgraded agent-config breaks task-init/remove PR #270 re-review
Teach the hook merge and strip the new array hook shape ▶ 14m
…
↑↓ move ⏎ expand o open PR/issue t jump to tab a all repos q quit
```

- **Real time without waiting on GitHub.** A change under `$TASK_FORCE_HOME/radio` (sessions, mailboxes), `$TASK_FORCE_HOME/monitor` (the PM queue, the forge cache) or a repo's worktree base (a `.<slug>.info` sidecar appearing or going) triggers a `--local` snapshot, debounced. Forge state comes from a full snapshot every 45 seconds (`--poll SECS`, 30-600).
- **Never blank.** A snapshot that fails or times out leaves the last good one on screen; the header says `stale <age>` and the footer says why. `forge <age>` means disk state is current but PR state is from the last full run.
- **Keys.** `↑↓` move, `⏎` expand a done item to its full PR body (scrollable) or enter a repo from the overview, `o` open the PR or issue, `t` jump to the item's zellij tab (the worker's; the PM's for a queued or done item, or for an overview row), `a` toggle the overview, `q` quit.
- **`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.

---

## Per-combo setup
Expand Down
3 changes: 2 additions & 1 deletion bin/task-monitor-snapshot
Original file line number Diff line number Diff line change
Expand Up @@ -455,7 +455,8 @@ _snapshot_repo() {

# --- assemble.
jq -n \
--arg name "$name" --arg root "$root" --arg impl "$impl" --arg tracker "$tracker" \
--arg name "$name" --arg root "$root" --arg worktree_base "$wtbase" \
--arg impl "$impl" --arg tracker "$tracker" \
--arg agent "$agent" --arg now "$NOW" --arg fetched_at "$fetched_at" \
--arg next_source "$next_source" --argjson next_limit "$NEXT_LIMIT" \
--argjson forge_ok "$forge_ok" --argjson names "$names_json" \
Expand Down
18 changes: 18 additions & 0 deletions claude-gh/steering/gh-workflow.example.md
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,24 @@ task-monitor-snapshot --all # every repo radio knows about
task-monitor-snapshot --local # no forge calls: disk + the last full run's cache
```

`task-monitor` — the board, live, in the terminal (#276)

The ratatui front end over that JSON. It opens on the repo view (In flight /
Next / Done) inside a repo, and on the all-repos overview (`a`, or `--all`)
outside one: one row per repo with its attention light, the signal in words,
and the zellij session it runs in. A change under radio's state or a worktree
sidecar refreshes it from disk at once (`--local`); forge state refreshes every
45s (`--poll SECS`). A failed or slow refresh never blanks the screen: the last
good snapshot stays up, marked `stale`. Keys: `↑↓` move, `⏎` expand (a done
item's full PR body, scrollable) or enter a repo, `o` open the PR or issue, `t`
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).

### PM ↔ worker messaging (radio)

Radio is the **canonical** coordination channel between the PM and workers — every
Expand Down
18 changes: 18 additions & 0 deletions claude-jira/steering/jira-workflow.example.md
Original file line number Diff line number Diff line change
Expand Up @@ -289,6 +289,24 @@ task-monitor-snapshot --all # every repo radio knows about
task-monitor-snapshot --local # no forge calls: disk + the last full run's cache
```

`task-monitor` — the board, live, in the terminal (#276)

The ratatui front end over that JSON. It opens on the repo view (In flight /
Next / Done) inside a repo, and on the all-repos overview (`a`, or `--all`)
outside one: one row per repo with its attention light, the signal in words,
and the zellij session it runs in. A change under radio's state or a worktree
sidecar refreshes it from disk at once (`--local`); forge state refreshes every
45s (`--poll SECS`). A failed or slow refresh never blanks the screen: the last
good snapshot stays up, marked `stale`. Keys: `↑↓` move, `⏎` expand (a done
item's full PR body, scrollable) or enter a repo, `o` open the PR or issue, `t`
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).

### PM ↔ worker messaging (radio)

Radio is the **canonical** coordination channel between the PM and workers — every
Expand Down
18 changes: 18 additions & 0 deletions claude-local/steering/local-workflow.example.md
Original file line number Diff line number Diff line change
Expand Up @@ -311,6 +311,24 @@ task-monitor-snapshot --all # every repo radio knows about
task-monitor-snapshot --local # no forge calls: disk + the last full run's cache
```

`task-monitor` — the board, live, in the terminal (#276)

The ratatui front end over that JSON. It opens on the repo view (In flight /
Next / Done) inside a repo, and on the all-repos overview (`a`, or `--all`)
outside one: one row per repo with its attention light, the signal in words,
and the zellij session it runs in. A change under radio's state or a worktree
sidecar refreshes it from disk at once (`--local`); forge state refreshes every
45s (`--poll SECS`). A failed or slow refresh never blanks the screen: the last
good snapshot stays up, marked `stale`. Keys: `↑↓` move, `⏎` expand (a done
item's full PR body, scrollable) or enter a repo, `o` open the PR or issue, `t`
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).

### PM ↔ worker messaging (radio)

Radio is the **canonical** coordination channel between the PM and workers — every
Expand Down
18 changes: 18 additions & 0 deletions claude-notion/steering/notion-workflow.example.md
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,24 @@ task-monitor-snapshot --all # every repo radio knows about
task-monitor-snapshot --local # no forge calls: disk + the last full run's cache
```

`task-monitor` — the board, live, in the terminal (#276)

The ratatui front end over that JSON. It opens on the repo view (In flight /
Next / Done) inside a repo, and on the all-repos overview (`a`, or `--all`)
outside one: one row per repo with its attention light, the signal in words,
and the zellij session it runs in. A change under radio's state or a worktree
sidecar refreshes it from disk at once (`--local`); forge state refreshes every
45s (`--poll SECS`). A failed or slow refresh never blanks the screen: the last
good snapshot stays up, marked `stale`. Keys: `↑↓` move, `⏎` expand (a done
item's full PR body, scrollable) or enter a repo, `o` open the PR or issue, `t`
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).

### PM ↔ worker messaging (radio)

Radio is the **canonical** coordination channel between the PM and workers — every
Expand Down
1 change: 1 addition & 0 deletions docs/task-monitor-schema.example.json
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
{
"name": "task-force",
"root": "/home/you/src/task-force",
"worktree_base": "/home/you/src/task-force-worktrees",
"loadout": "claude-gh",
"tracker": "gh",
"agent": "claude",
Expand Down
Loading
Loading