Skip to content

task-monitor 1/6: snapshot --json collector (bash) and schema - #280

Merged
martin-conur merged 4 commits into
mainfrom
task/monitor-snapshot
Oct 20, 2026
Merged

martin-conur merged 4 commits into
mainfrom
task/monitor-snapshot

Conversation

@martin-conur

Copy link
Copy Markdown
Owner

Summary

A new command, task-monitor-snapshot, prints one JSON document saying what is in flight, next and done in a repo, each with a title and a plain description, and whether the repo needs you. It reads everything from radio's state, the worktree sidecars, the tracker and GitHub, and no model writes any of it. Its --local mode never calls GitHub, so the coming front end can refresh instantly on file changes.

Closes #273

What

  • bin/task-monitor-snapshot prints "schema": 1 for the current repo, or with --all for every repo a radio session names. Repos run in parallel.
    • In flight: each .<slug>.info sidecar, joined to its radio session, its PR (by the task/<slug> head), a status word from the latest review intent, and its attached reviewers.
    • Next: the PM queue file (sub-issue 3 owns the format; this reads the expected shape), else the tracker fallback.
    • Done: merged PRs that deliver an item, the last 24h or the last 10, whichever is more. Each is cached once under $TASK_FORCE_HOME/monitor/<repo>/prs/.
    • Attention: exactly three signals, an unread PM inbox (--also aliases count toward the primary), a review waiting on merge, and a worker in awaiting.
    • --local: gh is shadowed by a failing function, and the run merges with the last full run's cache. A full run whose forge call fails keeps the last result and marks it stale.
  • Tracker hooks with _default.sh bodies: aw_tracker_item_describe, aw_tracker_pr_item and aw_tracker_next_fallback, overridden by gh, local and jira. tests/loadout_modules.bats checks that they resolve for every impl.
  • Helpers: lib/monitor-text.sh (awk paragraph and ## Summary extraction, no large-string substitutions, per task-remove hangs at 100% CPU in _la_strip_line under bash 3.2 with a UTF-8 locale #266) and lib/monitor-assemble.jq (the join).
  • Reuse: aw_task_fm moved from bin/task-board to lib/task-file.sh, and aw_main_repo_root was factored out of aw_main_repo_name.
  • Install: all seven installers link ~/.local/bin/task-monitor-snapshot (shared, drift-guarded stanza).
  • Docs: docs/task-monitor-schema.md, an example JSON from this repo, a README section, all seven workflow templates plus .claude/gh-workflow.md, and CHANGELOG.

Decisions worth a look

  • A PR counts as open only when the forge says so. With no forge data yet, review-waiting-merge stays quiet. Otherwise a cold --local run lit groppo with seven old reviews.
  • stale is an explicit "did the forge answer this run" flag. A timestamp comparison was flaky when two runs fell in the same second.
  • gh "Next" fallback order is whatever gh project item-list returns, which is not guaranteed to be board order.

Verification

  • ./run_tests.sh: 1424/1424 pass. The new tests/task_monitor_snapshot.bats has 39 tests, each run under /bin/bash (3.2 here).
  • Mutation-checked: 37 code mutations across the collector, the assembler, the hooks and the text helpers. Each turned at least one test red. The one survivor (the draft-item filter) got its own test, which now catches it.
  • tools/check-drift.sh clean, and shellcheck -x clean on the changed shell files.
  • Real data, scratch TASK_FORCE_HOME copy, /bin/bash 3.2: --all over 4 repos takes about 10s, and a single-repo --local 0.65s.

🤖 Generated with Claude Code

…sk-monitor-snapshot

New canonical root binary `bin/task-monitor-snapshot` prints one JSON
document (schema 1) per repo, or with --all for every repo radio knows:
in flight (worktree sidecars joined to radio sessions, PRs and the latest
review intent), next (PM queue, else the tracker fallback), done (merged
PRs that deliver an item, last 24h or last 10, cached once on disk), and
exactly three attention signals. --local makes no forge call: `gh` is
shadowed by a failing function and the run merges with the last full
run's cache.

Per-tracker behaviour is three new hooks with _default.sh bodies:
aw_tracker_item_describe, aw_tracker_pr_item, aw_tracker_next_fallback.
The parity suite checks them for every impl. Text extraction is awk
(lib/monitor-text.sh), assembly is jq (lib/monitor-assemble.jq), and the
whole thing runs on /bin/bash 3.2.

Also: aw_task_fm moves from bin/task-board to lib/task-file.sh for reuse,
aw_main_repo_root is factored out of aw_main_repo_name, and all seven
installers link the new binary. Schema documented in
docs/task-monitor-schema.md with an example; README, workflow templates
and CHANGELOG updated.

Closes #273

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

Copy link
Copy Markdown
Owner Author

Spec compliance (#273)

CI ran on head 0aecd6e and all four checks passed (bats ubuntu and macos, ShellCheck, drift). I also ran the new bats file, loadout_modules and install_symlinks here, plus tools/check-drift.sh and shellcheck -x on the changed shell files. All clean. A --local run against this repo under /bin/bash 3.2 takes about 0.3s.

Deliverables, checked against the issue:

  • Root binary with "schema": 1, --all, and a name that avoids the Rust binary: delivered. task-monitor-snapshot is linked by all seven installers (the shared stanza is drift-guarded).
  • In flight: delivered. It is built from .<slug>.info sidecars and joins the session STATE, the PR (found by the task/<slug> head), the latest review intent, and attached reviewers (sidecars carrying PR_NUMBER).
  • Per-tracker titles and descriptions: delivered through the new hooks aw_tracker_item_describe, aw_tracker_pr_item and aw_tracker_next_fallback. Each has a _default.sh body, and loadout_modules.bats checks that they resolve for every impl.
  • Next: delivered. The PM queue read is a stub for sub-issue 3's format, as the issue allows. The fallback covers gh, local, and none for jira/notion, and excludes in-flight items.
  • Done: delivered. It shows the last 24h or the last 10, whichever is larger, and caches each merged PR once on disk.
  • Attention, exactly three signals: delivered, with the one weakness in finding 1.
  • --local fast mode: delivered. Shadowing gh with a failing function is a neat way to guarantee no forge calls. The merge with the last forge result is explicit through forge.stale.
  • Constraints: bash 3.2 and awk/jq for bodies (task-remove hangs at 100% CPU in _la_strip_line under bash 3.2 with a UTF-8 locale #266) are respected. TASK_FORCE_HOME is respected. The schema doc and example are present.

Nothing from the spec is missing.

Code-review findings

All of these are small and fixable inside this PR.

  1. worker-awaiting has no staleness check (lib/monitor-assemble.jq, the $awaiting select).
    • It fires on STATE == "awaiting" alone. bin/radio documents that a crashed tab "leaves STATE=busy/awaiting forever" (the comment near line 2476).
    • A dead worker would therefore keep the repo lit permanently, which is the false light Epic: task-monitor, a real-time view of in-flight, next and done work per repo #272 treats as an anti-goal. The PR already avoids this for review-waiting-merge by requiring the forge to confirm the PR is open.
    • Fix: also require a heartbeat younger than the same 1h threshold radio orphans uses. The collector already has $NOW, so pass it in. Add a test with a stale awaiting session that shows no signal.
  2. Dead code in the unread count. $aliases is built from sessions whose ALIAS is the PM role, and the count then reads mailbox/<alias>/inbox. Alias sessions own no inbox (bin/radio, around line 1631), so that branch can never match.
    • What actually delivers "aliases count toward the primary" is the alias repo reporting unread: 0 with alias_of set, while the primary counts everything delivered to its inbox.
    • Drop $aliases, or add a comment saying that is the design. The same note belongs in docs/task-monitor-schema.md: an alias repo's unread mail is counted on the primary repo.
  3. session.since is the LAST_HEARTBEAT timestamp. Radio refreshes it on every state change, so the name suggests "in this state since" when it means "last heartbeat". Either rename the field to last_heartbeat, or document it as such in the schema doc. This matters because the front end will show it.
  4. Same-second ordering in the PR timeline. created_at has one-second resolution and the sort tiebreaker is .id. If the id is not monotonic, a review-complete-* and a changes-requested sent in the same second could be ordered wrongly, which would flip the waiting-merge signal. Confirm the id format sorts by time. If it does, add a one-line comment on the sort. If not, use file mtime or a sequence as the tiebreaker.
  5. Full runs on repos with no GitHub remote are stale: true permanently (local/jira/notion repos where gh pr list fails). That is probably acceptable, since the head-branch PR link needs gh anyway. Say so in the schema doc, so the front end does not show a permanent "stale" warning as an error.

Verdict: clean-with-nits

The implementation is solid and matches the spec, and the tests and CI are real. Finding 1 is the one I would not merge without. Findings 2–5 are quick doc or comment fixes. PM decides on the merge.

martin-conur and others added 3 commits October 20, 2026 13:06
…s review round 1

- worker-awaiting needs a heartbeat younger than radio orphans' 1h, so a
  crashed tab cannot keep a repo lit.
- The unread count drops the alias-inbox branch: an alias owns no inbox,
  radio delivers its mail to the primary. Documented in the schema doc.
- session.since is renamed last_heartbeat, which is what it is.
- Same-second messages are ordered by the radio log's send lines, since a
  radio id ends in a random suffix and cannot break the tie.
- forge.available: false for a repo whose forge has never answered (no
  GitHub remote), instead of a permanent stale.
- The gh Next fallback is one paginated GraphQL query: open issues only,
  no archived items, no issues with sub-issues, ordered by the project's
  Priority field when it has one. The comment says API order is not
  board order.
- A missing radio log no longer aborts the run under pipefail.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… on jq 1.7 and newer shellcheck

- lib/monitor-assemble.jq: parenthesize the `available` comparison. jq
  1.7.1 (ubuntu-latest) rejects `!=` as a bare object value; jq 1.8
  accepts it, which is why it passed locally. Reproduced with a 1.7.1
  binary, and the suite passes under it.
- bin/task-monitor-snapshot: the radio-log guard is an `if` instead of
  `A && B || true` (SC2015 on CI's shellcheck).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@martin-conur
martin-conur merged commit 30258da into main Oct 20, 2026
4 checks passed
@martin-conur
martin-conur deleted the task/monitor-snapshot branch October 20, 2026 17:52
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 1/6: snapshot --json collector (bash) and schema

1 participant