Skip to content

feat: persistent watcher daemon — supervision survives walking away (PRD #10)#16

Merged
Catafal merged 6 commits into
mainfrom
feat/watcher-daemon
Jun 13, 2026
Merged

feat: persistent watcher daemon — supervision survives walking away (PRD #10)#16
Catafal merged 6 commits into
mainfrom
feat/watcher-daemon

Conversation

@Catafal

@Catafal Catafal commented Jun 13, 2026

Copy link
Copy Markdown
Owner

Implements PRD #10 — a persistent supervisor daemon so the observer/judge loop keeps running after you close the terminal or drop your SSH session. Supervision survives walking away; status is pulled (over SSH via bach sessions list / /board / bach daemon status), not pushed — no notification channel, by design.

What shipped (issues #11#15)

Key architectural decision

Supervision state is derived from the OS process table, not a bookkeeping file. "Is session X supervised?" = "does a live bach internal session-watch <artifact> process exist?" This auto-dedupes against watch_on_launch, survives daemon restarts without orphaning/doubling watchers, and keeps reconcile a pure function of two observable sets. Recorded in ADR-017.

Quality

  • Built TDD across 5 sonnet agents (4 implementation + 1 review + 1 docs), each verified with make gate between slices.
  • Review pass (/review + /backend-taste + /qa, no HITL) fixed an fd leak + 3 imports-at-top violations and added 3 coverage gaps; the duplicate-watcher invariant, backoff, single-instance guard, and launchctl fallback all reviewed "ship it".
  • make gate green: 1137 passed, 1 skipped (ruff + mypy + pytest).

Closes #10, #11, #12, #13, #14, #15

🤖 Generated with Claude Code

Catafal and others added 6 commits June 13, 2026 10:40
… scan (#11)

Extract the detached watcher Popen from task_service into a reusable
runtimes/watcher_processes.spawn_watcher; task_service delegates to it
with no launch-path behavior change. Add list_supervised_artifacts()
which derives the supervised set from the OS process table (ps scan for
'internal session-watch <artifact>'), the ground truth that lets the
daemon dedupe against watch_on_launch and survive restarts.

Closes #11

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Pure reconcile(desired, supervised, backoff, now) -> SpawnPlan upholds
the duplicate-watcher invariant (never spawn an already-supervised
artifact) with bounded exponential crash backoff. run_supervisor is a
single-threaded synchronous loop with every side-effect injected as a
seam. list_supervisable_artifacts filters the session/task join to live,
task-linked, non-terminal artifacts. New daemon_reconcile_seconds config
knob. bach daemon run wires the seams with SIGTERM/SIGINT clean shutdown.

Closes #12

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…13)

acquire() writes our pid unless a live daemon is recorded (refuse),
reclaiming a stale pidfile when the recorded pid is dead. bach daemon run
acquires on start and clears the pidfile on clean shutdown. bach daemon
status reports running state + the supervised artifact set (ps truth);
bach daemon stop SIGTERMs the recorded pid. All side effects (kill,
liveness probe, read) injected for temp-dir + fake testing.

Closes #13

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…llback (#14)

render_launch_agent emits a valid plist (plistlib) for com.bach.daemon
invoking 'bach daemon run' with RunAtLoad + KeepAlive so supervision is
on at login and self-restarts on crash. install/uninstall are idempotent
(unload-before-overwrite) and drive launchctl through an injected runner.
Any launchctl failure/unavailability surfaces a manual 'bach daemon run'
fallback the CLI prints — honoring the iTerm-fallback non-negotiable.

Closes #14

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Close the parent's log file handle after Popen in spawn_watcher (fd leak
under high-volume spawning). Move three deferred imports (errno,
subprocess, daemon_cmd's enumerate closure) to module top, honoring the
imports-at-top rule. Add tests: log_fh closed in parent, unreadable
artifact stays supervisable, run_supervisor backoff prevents immediate
respawn. /review + /backend-taste + /qa otherwise ship-it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…15)

ADR-017 records the supervisor-of-subprocesses architecture, the
process-table-as-truth decision, and three accepted trade-offs (pidfile
TOCTOU, ps truncation, in-memory backoff). New how-to documents the
install + SSH check-in (pull-not-push) workflow. CLAUDE.md gains the
bach daemon command block. Surgical /code-comments pass adds WHY intent
to the five daemon modules.

Closes #15

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
@Catafal Catafal merged commit 257c092 into main Jun 13, 2026
2 checks passed
@Catafal Catafal deleted the feat/watcher-daemon branch June 13, 2026 09:15
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.

PRD: Persistent watcher daemon — supervision survives walking away (Gap B, no notification)

1 participant