# 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.
