Tasks: poll start runs as a proper daemon
Phase 4 of the chain. Derives from
design.mdandtesting-plan.md— each task's_Test:_names a matrix row. Ticket: #191.
flowchart LR
T1["1 · state paths"] --> T3["3 · heartbeat writer"]
T1 --> T5["5 · --daemon / --logfile"]
T1 --> T6["6 · poll status"]
T2["2 · daemonize()"] --> T5
T3 --> T4["4 · poller records a cycle"]
T3 --> T6
T5 --> T7["7 · control plane + daemon_entry"]
T6 --> T7
T4 --> T8["8 · integration tests"]
T5 --> T8
T6 --> T8
T7 --> T9["9 · docs"]
T8 --> T9
T9 --> T10["10 · capability docs + decision"]Tasks
[x] 1.
StateLayoutgrows the three poller paths. Addpoll_pidfile,poll_statusandpoller_log; declare each inGENERATED_PATHSas local with its reason; replace the two ad-hoc<root>/poll.pidderivations (commands/poll.py,core/daemons.py) with the property. Requirements: R5.1, R5.2 · Test: T4[x] 2.
the_loop/daemonize.py— detach, redirect, handshake.daemonize(logfile, timeout=60)(double-fork,setsid,dup2, reap the intermediate, nochdir) andnotify_ready(). Pure stdlib; nothing poller-specific. Requirements: R1.1, R1.6, R2.1, R2.3, R3.4 · Test: T6[x] 3.
the_loop/poller/heartbeat.py— write and read.PollHeartbeat.record(summary)(atomictempfile+os.replace, warn-once onOSError) andPollHeartbeat.read(path)→Heartbeat | None. Requirements: R5.4, R4.5, R4.6 · Test: T1[x] 4. The poller records a heartbeat after every cycle. An optional
heartbeatcallable onPoller, invoked at the end ofpoll_once— so a--oncerun and a long-lived poller both leave one, and the poller core keeps no file handle of its own. Requirements: R4.5 · Test: T1, T6[x] 5.
poll startgains--daemon/--foreground/--logfile. Onedest, last-flag-wins;--daemon --oncerefused; the logfile opened and the lock probed before the fork, and the lock acquired (and a stale pidfile cleared) after it, on both the daemon and the foreground path;notify_ready()once the run loop is about to start;poller.startedcarriesdaemonandlogfile. Requirements: R1.3, R1.4, R1.5, R2.2, R2.4, R3.1, R3.2, R3.3, R3.5 · Test: T3, T6[x] 6.
poll status. New action: liveness from the lock, pid from the pidfile, the rest from the heartbeat;--format text|json; exit0running /1not; stale pidfile reported, not removed. Requirements: R4.1–R4.8 · Test: T2, T7[x] 7. The control plane starts daemons with a log, and reports the heartbeat.
core/daemons.control_daemon("start")redirectsstdout/stderrto the daemon's logfile instead ofDEVNULL;daemon_statuscarriesstartedAt/lastCycleAt;daemon_entryforcesargs.daemon = False. Requirements: R2.5, R4.7 · Test: T5[x] 8. Integration tests against a real detached process.
cli/tests/test_poll_daemon_integration.py, Gherkin-documented, one scenario per row of the T6 trace; each test kills what it spawned in afinally. Requirements: R1.1, R1.2, R2.1, R3.1, R3.3, R3.4, R3.5 · Test: T6, T7[x] 9. Documentation.
docs/cli/commands/poll.md(the two flags, thestatusaction, when to use which start mode, log rotation as the host's job),docs/cli/state.md(three classification rows, the.gitignoreline) and the repository's own.gitignore. Requirements: R5.3 · Test: T4[x] 10. Capability docs and the decision record.
docs/capabilities/cli.md(the poller's lifecycle),docs/decisions/decision-072.md(--daemonopt-in, not the default) plus its index row, and the execution log's## Documentationsection. Requirements: R1.3 (why the default did not move) · Test: T4 (docs parity)