feat: persistent watcher daemon — supervision survives walking away (PRD #10)#16
Merged
Conversation
… 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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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)
watcher_processes— extracted the detachedspawn_watcherfromtask_service(launch path delegates, no behavior change) +list_supervised_artifacts()deriving the supervised set from the OS process table (psscan).daemon_supervisor— purereconcile(desired, supervised, backoff, now)upholding the duplicate-watcher invariant with bounded crash backoff;run_supervisorsingle-threaded synchronous loop (no threads/asyncio);bach daemon runforeground entry;daemon_reconcile_secondsconfig knob.daemon_lifecycle— pidfile single-instance guard (stale reclaim) +bach daemon status/stop.launchd_service—com.bach.daemonLaunchAgent (RunAtLoad + KeepAlive) viabach daemon install/uninstall, idempotent, with a printedbach daemon runfallback on launchctl failure (honors the iTerm-fallback non-negotiable).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 againstwatch_on_launch, survives daemon restarts without orphaning/doubling watchers, and keepsreconcilea pure function of two observable sets. Recorded in ADR-017.Quality
make gatebetween slices./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 gategreen: 1137 passed, 1 skipped (ruff + mypy + pytest).Closes #10, #11, #12, #13, #14, #15
🤖 Generated with Claude Code