Skip to content

Epic: task-monitor, a real-time view of in-flight, next and done work per repo #272

Open
9 of 9 issues completed

Description

@martin-conur

Epic. The sub-issues below are the delivery plan; this body is the agreed design. It came out of a PM/user design discussion on 2026-10-20 and a builder read-through.

Why

With task-force, a ticket takes minutes rather than a day. A normal session runs
ten or more tickets, all referred to by number. The PM and the workers keep up.
The human driving the PM does not: "#262" no longer reliably means anything to
them, and nothing shows the shape of the work as a whole. They need to see
what is in flight, what is next, and what landed, each as a title plus a short
human description. Done items also need a short account of what the solution
actually was.

There is a second job. The user runs one PM per project, in several repos at
once, on both claude and kiro. They need to see at a glance which project needs
them now and which can be left alone.

Goals

  • A per-repo view in three sections: In flight, Next, Done. Each
    item shows its number, title, and a one- or two-line plain description.
  • Done items also carry a high-level summary of the solution, expandable in
    place to the full PR body.
  • An all-repos overview with one row per repo and an attention light.
  • Real-time: local events (radio mail, worker state, tab icons) show up
    immediately. Forge state (PR and CI) is polled.
  • No model tokens spent by the monitor. Every value it shows is read
    deterministically from disk, the tracker, or the forge.
  • Works for claude and kiro PMs, and on every tracker task-force supports (gh,
    jira, notion, local). The exceptions are stated: the ❓ signal is claude-only
    in v1, and jira/notion show a slug before the PR exists (see Data sources).

Non-goals

  • Not a project-management view. The tracker owns the backlog as a whole. The
    monitor's "Next" is the short-term queue only.
  • It does not replace the PM or drive work. It is read-only, except for jumping
    to a tab and opening URLs.
  • It does not need a global PM. The overview reads each repo's state directly.
    A cross-repo PM may come later, and the monitor must not depend on one.

Anti-goals (a failure even if it technically works)

  • Calling a model to produce or summarise anything shown.
  • A screen that shows GitHub's slowness: it hangs, or goes blank while it waits.
  • Task-force logic duplicated in Rust, such as tracker parsing, role naming, or
    radio file formats, which would drift from the bash that owns it.
  • The PM spending meaningful tokens maintaining the board.
  • Light noise: flagging a repo for anything other than the three signals below.

Architecture (decided)

Two parts.

1. task-monitor snapshot --json (bash)

A canonical root binary (bin/task-monitor), installed like radio and
task-pm. It gathers everything for one repo, or with --all for every repo
radio knows about, into a single JSON document on stdout. It reuses the
existing modules rather than re-deriving anything:

  • tracker modules in lib/trackers/{gh,jira,notion,local}.sh, through new
    tracker hooks with _default.sh bodies, covering issue title and
    description, the Todo fallback, and issue URLs;
  • lib/agents/* where agent-specific state matters;
  • radio's on-disk state under ${TASK_FORCE_HOME:-~/.task-force}/radio/.

It is testable with bats and usable on its own, by a human or by a PM that wants
to read the board. A versioned schema ("schema": 1) is the contract with the
front end.

2. The ratatui front end (Rust)

It lives in the repo (location is the builder's call, e.g. monitor/) and is
built with cargo build --release by install.sh when cargo is on PATH. When
cargo is missing, the installer skips it and prints one line saying how to get
it. install.sh symlinks the built binary into ~/.local/bin as the user-facing
command. The user-facing name is the builder's call, but it must not collide
with the bash task-monitor snapshot command; one option is that the Rust
binary is task-monitor and the bash one is task-monitor-snapshot, called by
it. Prebuilt release binaries are a possible later step and out of scope here.

It owns the screen, the keys, and change detection, and nothing else:

  • it watches the radio directory and worktree sidecars with filesystem events
    and re-runs the snapshot on change, debounced;
  • local refresh and forge calls are split: a filesystem event never waits on
    gh. The snapshot takes a flag (or the front end runs two collectors) so the
    fast path reads disk only and merges with the last forge result;
  • it re-runs the snapshot on a slow timer (30-60s) for forge state;
  • it renders the last good snapshot. A snapshot that fails or times out never
    blanks the screen; it shows a small stale indicator instead.

Both machines the user works on have a Rust toolchain.

Data sources (decided unless marked)

In flight

  • What counts as in flight: a worktree whose <worktree-base>/.<slug>.info
    sidecar exists, not a fresh heartbeat. A resumed or roleless session is still
    in flight.

  • Workers: radio sessions whose role is worker-<repo>-<slug>, joined to
    <worktree-base>/.<slug>.info for the issue ref and branch. Reviewers
    (reviewer-<repo>-pr<N>) attach to their PR's item, not to their own row.

  • Status, as a short word: implementing / PR open / under review / changes
    requested / re-review / waiting merge. It is derived from the PR's existing
    state plus the latest radio intent exchanged with the PM about that PR.

  • Activity icon: ▶ / ⏸ / ❓, from the session file's STATE=
    (busy / idle / awaiting) rather than by asking zellij. It also shows time since
    the last state change.

  • Title and description (decided, no PM writing):

    gh / local jira / notion
    before the PR exists issue title + first paragraph of the body (gh via gh; local from the tasks/ file) the ref + worker slug, e.g. PROJ-42 · kiro-universal-config. The shell has no read access to these trackers (agent-only MCP), and we deliberately add no API credentials
    once the PR exists PR title + ## Summary PR title + ## Summary

    A non-gh PR is linked to its item through its task/<slug> head branch.
    Later, only if the jira/notion pre-PR gap proves too weak in use: an optional
    task-monitor describe <ref> "<line>" the PM may run when dispatching. Not
    part of v1, and only acceptable if it costs the PM's context next to nothing.

Next

  • The PM's queue: a small per-repo file under ~/.task-force/ that the PM
    writes during its normal conversation with the user, through a subcommand such
    as task-monitor next 267 266 --note 267="kiro hooks first", or an equivalent
    set / add / rm / clear form. It holds an ordered list of tracker refs
    with optional one-line notes. Writing it is the PM's only board cost. The PM
    prompts for every loadout (commands/pm.md, the kiro PM agent prompt) learn
    this command and when to use it: when the user and PM agree on what comes next.
  • Fallback when the queue is empty: on gh, open project items in none of the
    three configured statuses (in progress / in review / done), in project order,
    top N. On local, the tasks/ backlog in its own order. On jira/notion there
    is no fallback: the section shows the queue, or says it is empty. Items
    already in flight are excluded. No new tracker settings are introduced for
    this.

Done

  • Source: merged PRs linked to a tracker item (Closes #N or the tracker
    equivalent), newest first, over a recent window. The builder's default: the
    last 24 hours, or the last 10 items, whichever is larger.
  • Summary: the PR body's ## Summary section. The worker prompt for every
    loadout is changed to require that section first in every PR body: two or
    three plain sentences, written for the user rather than a reviewer, about
    what changed and why. For PRs without it, use the first paragraph after the
    Closes line.
  • Expand: pressing ⏎ shows the full PR body, scrollable, in place.
  • Cache: merged PRs never change, so their title, summary, body, merge SHA
    and time are cached on disk and fetched once. Only new merges hit the forge.

Attention light (overview), exactly three signals

  1. Unread PM inbox. mailbox/pm-<repo>/inbox/ is non-empty. --also alias
    inboxes count towards the primary PM's repo.
  2. Review waiting merge. A review-complete-clean or
    review-complete-with-findings for a PR that is still open, with no later
    changes-requested or approved-and-merged from the PM for that PR. Rebuilt
    from message frontmatter (intent:, pr:) in the mailboxes, since the radio
    log's send lines carry no pr= field.
  3. A worker stuck on a question. A worker session of that repo in the
    awaiting state (the state behind the tab's ❓︎ prefix). claude-only in
    v1:
    only the claude loadouts wire radio awaiting (PermissionRequest,
    AskUserQuestion, ExitPlanMode); kiro installers wire busy/ready only. The gap
    is documented, and a sub-issue investigates a kiro equivalent.

The overview row says which signal fired, in words.

Screens (decided by the user)

The repo view (default when run inside a repo):

 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
   #262  Launched tabs ignore TASK_FORCE_HOME                 implementing
         A scratch-home probe registers in the real radio state          ❓ 3m
─────────────────────────────────────────────────────────────────────────────
 NEXT  (PM queue)
   1  #180  stale TAB_ID / ambiguous tab / .info sourcing      "after #267"
   2  #241  task-done unregister undone one turn later
─────────────────────────────────────────────────────────────────────────────
 DONE  (last 24h)
   #266  task-remove hangs under bash 3.2         PR #269 · 1415b86 · 2h ago
         Both whitespace checks use a grep helper instead of a bash pattern
         substitution that never finished on large UTF-8 files.
─────────────────────────────────────────────────────────────────────────────
 ↑↓ move  ⏎ expand  o open PR/issue  t jump to tab  a all repos  q quit

The all-repos overview (a, or --all). ⏎ on a row opens that repo's view:

 task-monitor · all repos                                    12:04
 ● task-force            ✉ 1 unread · 1 review waiting merge   2 in flight
 ● recommender-systems   ❓ rerank-sasrec-gate is asking        1 in flight
 ○ other-repo                                                  idle

Keys: ↑↓ move, ⏎ expand or enter, o open the PR or issue URL, t jump to
the item's zellij tab (worker or PM), a toggle overview, q quit.

t works only within the monitor's own zellij session (decided). Each repo
normally runs in its own zellij session (ZELLIJ_SESSION= in its session
files), and zellij action go-to-tab cannot cross sessions. So each overview
row names its zellij session, and the user switches with zellij's own session
manager. Cross-session switching (a zellij plugin) is out of scope. The exact
keymap and styling are the builder's call within this shape.

Constraints

  • macOS with /bin/bash 3.2 is a first-class target. The snapshot must run
    there, and must not use bash 4+ features or slow pattern substitutions over
    large strings (see task-remove hangs at 100% CPU in _la_strip_line under bash 3.2 with a UTF-8 locale #266).
  • The repo's norms apply: tools/check-drift.sh for shared regions, shellcheck -x, bats tests, CHANGELOG, docs (README, the steering templates, the loadout
    workflow docs), and the pre-PR checklist in .claude/gh-workflow.md.
  • The CI matrix gains a Rust job (build plus tests) on macOS and Linux.
  • TASK_FORCE_HOME is respected everywhere. Tests never touch the real radio
    state.
  • Repos are discovered from radio session files (the REPO= field) plus the
    current repo. No separate registry.

Delivery: the epic and its sub-issues (proposed order)

  1. Snapshot command and schema (bash). Covers in-flight, next and done, the
    attention signals, the tracker hooks, the done-cache, --all, bats tests,
    and the schema documented.
  2. PR ## Summary convention. Worker prompts on every loadout, the PR-body
    parse with its fallback, and docs. It can land in parallel with 1, since it
    is only a prompt change and starts paying off immediately.
  3. PM queue. The task-monitor next … subcommand, the file format, the
    Todo fallback, and PM prompt guidance on every loadout.
  4. ratatui front end. Both screens, the keys, the watcher, the slow poll, the
    stale handling, and its Rust tests.
  5. Install and CI. install.sh cargo build and link with a graceful skip,
    the CI Rust job, README section, and an upgrading note.
  6. kiro awaiting signal (investigation). Does kiro-cli expose a hook event
    that can drive radio awaiting? If so, wire it into the three kiro installers
    so ❓ and attention signal 3 work for kiro workers. If not, document why.

Left to the builder

Decisions taken in discussion

  • Real-time ratatui front end over a bash snapshot --json. Built with cargo at
    install, since both of the user's machines have Rust.
  • No model tokens spent by the monitor. The PM's only cost is the queue command.
  • Per-repo view first, with an all-repos overview. Attention means exactly: an
    unread PM inbox, a review waiting on a merge decision, or a worker that is
    waiting on the user.
  • jira/notion show the slug before the PR exists, with no PM-written
    descriptions in v1 (see Data sources).
  • t works only within the monitor's own zellij session.
  • No ❓ for kiro in v1.

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions