Skip to content

PRD: Board × Radar fusion + observer dogfooding enablement #8

Description

@Catafal

Problem Statement

Phase 16 gave Bach two halves of mission control that don't talk to each other. The board knows what each task should be doing (its kanban stage); the radar knows what each session is doing (running / idle / ended, live previews). To answer the only question that matters during a working day — "which of my agents needs me right now?" — the user has to run /sessions, mentally join session IDs against tasks, then go back to the board. The observer/judge pipeline that should be doing this joining automatically is built but dormant: hooks aren't installed on any real project, watch_on_launch defaults off, and nothing in the board or task views ever surfaces what the judge concluded. Phase 16 shipped as machinery; this PRD makes it the daily driver.

Solution

Two threads, one outcome — the board becomes live mission control:

Board × Radar fusion. Every board card and dashboard row whose task has a linked runtime session gets a liveness indicator (● running / ◐ idle / ✓ ended) computed by joining the task artifact's runtime session id against session discovery. Cards in the executing and qa columns additionally show the judge's last one-line summary ("running make gate, 2 failures left"), so the board tells you what each agent is doing, not just that it exists. The join is near-live: a short (~5s) in-memory TTL cache makes dots fresh on every render without re-scanning disk and processes on every prompt loop, and discovery is scoped to registered projects' session directories rather than the whole machine.

Observer dogfooding enablement. The friction that keeps Phase 16 dormant gets removed: bach hooks install learns --project <name> so wiring each registered project is one command; bach config set (allowlisted to the observer knobs, typed validation) makes watch-on-launch on a one-liner instead of hand-editing YAML; the task detail view surfaces the last observer summary and the needs-human flag; and the launch→hooks→spool→watcher→judge→board-move loop is exercised end-to-end on a real task as an acceptance gate, with whatever wiring friction that surfaces fixed in this PRD rather than deferred.

User Stories

Liveness on the board and dashboard

  1. As a Bach user, I want each board card with a linked session to show a liveness dot (running / idle / ended), so that I can see which tasks have live agents without leaving the board.
  2. As a Bach user, I want the daily dashboard task list to carry the same liveness indicator, so that the first screen I see in the REPL already answers "what's alive?".
  3. As a Bach user, I want the idle state visually distinct (warning color) from running (success) and ended (muted), so that "an agent is probably waiting on me" jumps out at a glance.
  4. As a Bach user, I want tasks without a linked session to render exactly as they do today (no dot, no placeholder noise), so that the board stays clean for non-session work.
  5. As a Bach user, I want liveness computed by joining the artifact's runtime session id against session discovery, so that the board and the radar can never disagree about the same session.
  6. As a Bach user, I want the join cached for a few seconds in memory, so that dashboard re-renders stay instant while dots remain near-live.
  7. As a Bach user, I want discovery for the board join scoped to my registered projects' session directories, so that board rendering cost doesn't grow with unrelated machine-wide session history.
  8. As a Bach user, I want a liveness scan failure to degrade to "no dots" with a logged warning, so that a discovery hiccup can never break my board.
  9. As a Bach user, I want the board legend/help to explain the dot symbols, so that the indicators are self-describing.

Judge output where I look

  1. As a Bach user, I want executing and qa cards to show the judge's last one-line summary truncated under the title, so that the board tells me what each agent is actually doing.
  2. As a Bach user, I want cards whose latest observer verdict flagged needs-human to show an explicit marker, so that blocked agents are impossible to miss.
  3. As a Bach user, I want the task detail view to show the latest observer summary, confidence, and needs-human flag with their timestamp, so that I can audit what the judge has been saying about a task.
  4. As a Bach user, I want the summary read from the artifact's log events (the observer already writes them), so that no new state or schema is introduced for display.
  5. As a Bach user, I want cards with no observer history to render without an empty summary line, so that unwatched tasks don't pay a visual tax.
  6. As a Bach user, I want the summary line to be the most recent observer event regardless of whether it produced a board move, so that log-only (below-threshold) judgments are still visible.

Enablement — hooks and config

  1. As a Bach user, I want bach hooks install <runtime> --project <name> to resolve a registered project by name and install into that project's settings, so that wiring each of my projects is one command from anywhere.
  2. As a Bach user, I want hooks install without --project to keep today's behavior (current directory), so that nothing breaks for existing usage.
  3. As a Bach user, I want bach hooks status to accept the same --project flag, so that I can verify wiring per project.
  4. As a Bach user, I want bach config set <key> <value> for the observer knobs (watch-on-launch, observer-moves, judge-confidence-threshold, judge-interval-seconds, liveness thresholds, afk bounds), so that enabling the observer doesn't require hand-editing YAML.
  5. As a Bach user, I want the setter to validate types and ranges per key (bools accept on/off/true/false; numbers must parse and be positive; threshold within 0–1) and reject unknown keys with the list of valid ones, so that a typo cannot corrupt my config.
  6. As a Bach user, I want bach config show to include the observer knobs with their current values, so that I can see the observer's configuration at a glance.
  7. As a REPL user, I want the config setter reachable from the REPL like other commands, so that enablement doesn't force a context switch to a separate shell.

Dogfooding acceptance — the loop must actually run

  1. As a Bach user, I want a documented one-time enablement path (install hooks on a project, set watch-on-launch on, launch a task), so that turning the observer on takes two minutes, not an archaeology session.
  2. As a Bach user, I want launching a watched task to visibly confirm the watcher started (one line with the watcher's target), so that I know observation is active without checking process lists.
  3. As a Bach user, I want the watcher's status updates to appear on the board within one cache window of the judge writing them, so that the loop's effect is observable where I work.
  4. As a maintainer, I want the full loop (launch → hook events → spool → watcher → judge → artifact log/status → board dot + summary) exercised against a real session before this PRD closes, so that "built" becomes "works".
  5. As a maintainer, I want any wiring defects found during that end-to-end exercise fixed within this PRD's scope, so that dogfooding blockers don't get deferred into a backlog.
  6. As a Bach user, I want watcher spawn failures at launch to warn but never block the launch itself, so that observation problems can't cost me a session.

Implementation Decisions

  • One new deep module: the liveness join. A single service function — liveness_for_tasks(tasks) → {task_id: (liveness, summary, needs_human)} shape — owns: reading each task artifact's runtime session id, invoking session discovery scoped to registered project directories, extracting the latest observer log event, and the TTL cache. Board and dashboard renderers consume the returned mapping and know nothing about discovery, artifacts, or caching. All dependencies (discovery function, artifact reader, clock) injected for tests.
  • Cache: in-memory, per-process, ~5 seconds, fake-clock-testable. No files, no invalidation protocol — the REPL is a single process and 5 seconds of staleness is invisible at human speed. A failed scan returns the previous cached result if one exists (and logs), else an empty mapping.
  • Scoped discovery reuses the existing discovery module with the claude projects dir filtered to slugs matching registered project paths (and codex sessions matched post-scan by project dir). No new on-disk knowledge outside the existing deep module.
  • Rendering: liveness dot prefixes the task title on board cards and dashboard rows; executing/qa cards add one truncated summary line; needs-human renders as an explicit marker on the card. Theme colors map running→success, idle→warning, ended→muted (same mapping the radar uses). No layout change for tasks without sessions.
  • Judge display reads the existing artifact log events (the observer already appends suggestion/move events with summary + confidence). No schema change; display-only parsing of the most recent observer event.
  • bach config set is allowlisted, not generic: exactly the Phase 16 observer knobs, each with a typed parser/validator; unknown keys error listing valid ones. It reuses the existing single config write helper, and the matching REPL command routes through the same code path. Existing dedicated setters (set-model, set-name) stay untouched.
  • --project on hooks install/status resolves through the existing project registry; without the flag, behavior is unchanged (cwd). Installation remains parse-merge-rewrite and idempotent per the Phase 16 decisions; this PRD adds resolution only.
  • Watcher spawn confirmation: the launch path already spawns the watcher when watch-on-launch is on; it gains a printed confirmation line and a non-blocking warning on spawn failure.
  • End-to-end acceptance is part of the definition of done: hooks installed on the Bach repo itself, watch-on-launch enabled, one real task launched, judge verdict observed on the board. Defects found are in scope.
  • Respects ADR-015/ADR-016 throughout: observer authority unchanged, no daemon (cache is in-process memory, watcher remains run-and-exit), no hidden writes (hooks install stays explicit), control/observation plane split untouched (board dots are display-only and drive nothing).

Testing Decisions

A good test exercises external behavior through public interfaces with injected dependencies — never internal call order. All four module groups get tests (confirmed):

  1. Liveness join + cache (deep module, highest value): task↔session matching from artifact frontmatter, scoped discovery filtering, latest-observer-event extraction, TTL behavior with a fake clock (fresh within TTL, re-scan after, stale-result-on-failure), and degraded empty mapping on total failure. Pure fakes — no real home dirs.
  2. Board/dashboard rendering: given tasks plus a fake liveness mapping, assert dot presence/absence, summary line only on executing/qa, needs-human marker, and unchanged rendering for sessionless tasks. Prior art: the existing board render tests.
  3. Config setter: each allowlisted key accepts valid values and persists via the config writer; type/range rejection per key; unknown key error lists valid keys; round-trip through load_config. Prior art: existing settings coercion tests and the set-name CLI tests.
  4. Hooks install --project: registered-name resolution to the right settings path, unknown project error, unchanged cwd default, idempotency preserved. Prior art: existing hooks service tests.

Unit tests never touch real ~/.claude, ~/.codex, ~/.bach; integration with real CLIs stays behind the existing BACH_INTEGRATION=1 gate. The end-to-end dogfooding exercise is a manual acceptance step recorded in the PR, not an automated test.

Out of Scope

  • macOS notifications for needs-human (separate proposal, rides later).
  • bach afk drain / queue automation (deliberately held until the judge is proven in daily use).
  • Ignored-projects radar config and preview XML cleanup (separate polish PR).
  • Any change to observer authority, judge prompting/cadence, or gate semantics — this PRD surfaces and enables Phase 16, it does not retune it.
  • Auto-installing hooks at project init or launch (explicit-command rule stands).
  • A generic setter for non-observer config fields (theme, model, layout keep their dedicated commands).
  • Background refresh of the board (no daemon; the TTL cache only refreshes on render).

Further Notes

Metadata

Metadata

Assignees

No one assigned

    Labels

    ready-for-agentTriage: spec is complete, an agent can pick this up

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions