# ADR-0002: SF Schedule System is Pull-Based, Not Daemon-Based **Date:** 2026-05-05 **Status:** Accepted **Deciders:** SF core team (M010) **Related:** M010 S01 (schedule store), M010 S02 (schedule CLI), M010 S03 (milestone YAML integration), M010 S05 (this slice) --- ## Context The SF schedule system requires time-bound reminders that surface at a future date. Several design options were considered: 1. **Daemon-based (cron/launchd)** — A background process fires items at their due time using the OS scheduler. 2. **Daemon-based (in-process timer)** — SF itself runs as a long-lived process with in-process timers. 3. **Pull-based (on-demand query)** — Items are stored durably and queried at integration points (launch, auto-mode boundaries, explicit CLI query). Option 1 was explicitly ruled out early: platform-specific (cron on Unix, launchd on macOS, Task Scheduler on Windows), requires daemon installation, and cannot fire items when SF is not running. Option 2 was ruled out because SF is designed to be a session-based tool — agents run in fresh contexts per unit, state does not accumulate across sessions, and there is no persistent long-lived process in the happy path. Option 3 (pull-based) is what we adopted. --- ## Decision The SF schedule system is **pull-based**: - Schedule entries are stored in SQLite (`schedule_entries`). Legacy `.sf/schedule.jsonl` rows are import-only compatibility input, and rows without `schemaVersion` are treated as legacy version 1 by the current reader. - There is no background daemon or timer process. - Entries are queried ("pulled") at defined integration points: 1. **Launch** — `loader.ts` calls `findDue()` and prints a banner if items are overdue 2. **Auto-mode boundaries** — `sf headless query` populates a machine snapshot `schedule` field with `due` and `upcoming` entries 3. **CLI** — `sf schedule list --due` for explicit human query 4. **TUI status overlay** — displays due/upcoming schedule entries in the dashboard --- ## Consequences ### Positive - **Portable** — works identically on Linux, macOS, and Windows without platform-specific code - **Simple** — no process management, no signal handlers, no daemon lifecycle - **Auditable** — the DB ledger preserves append-style schedule operations - **Resilient** — no fire-and-forget timer that might miss if the process is restarted - **Stateless** — fits SF's session model: fresh context per unit, no in-memory state ### Negative / Explicitly Deferred - **No fire-at-exact-time** — items are not delivered at their exact `due_at`; they surface at the next pull query. If an item is due at 3 AM and the user opens SF at 9 AM, the item appears as overdue. - **No background notification** — SF cannot send a system notification when an item becomes due unless SF is open and the user is interacting with it. - **No recurring fire precision** — `kind: recurring` entries are stored but the recurring fire mechanism is deferred to a future iteration. These limitations are accepted trade-offs for the portability and simplicity benefits. A future iteration could add an optional lightweight notification helper (e.g. a separate binary that reads the schedule and posts system notifications) without changing the core design. --- ## Implementation Notes - `schedule-store.js` — DB-primary store with `findDue()` and `findUpcoming()` queries plus legacy JSONL import - `loader.ts` — calls `findDue()` on both scopes at startup; prints banner if any items are due - `headless-query.ts` — populates `schedule: { due, upcoming }` in `QuerySnapshot` - `sf schedule` CLI — add, list, done, cancel, snooze, run subcommands - `sf_plan_milestone` YAML — supports `schedule[]` array with `in` and `on_complete` duration fields --- ## Alternatives Considered ### In-Process Timer (Rejected) A long-lived SF process could maintain a timer queue and fire items at their due time. Rejected because it conflicts with SF's session architecture — each unit runs in isolation with no shared timer state across dispatch cycles. ### External Cron Wrapper (Rejected) A `sf-schedule-daemon` sidecar process managed by the user. Rejected because it adds an installation and运维 burden that conflicts with the "install and use immediately" experience goal. ### Third-Party Scheduling Service (Rejected) Using a hosted service (e.g. cron-job.org, AWS EventBridge) to fire webhook calls. Rejected because it introduces an external dependency and network requirement that does not fit SF's self-contained model.