296 lines
9 KiB
Markdown
296 lines
9 KiB
Markdown
# SF Schedule System — Specification
|
|
|
|
> **Spec version:** 1.0.0
|
|
> **Status:** Implemented (M010 S02)
|
|
> **Owner:** M010 S05
|
|
|
|
---
|
|
|
|
## Overview
|
|
|
|
The SF schedule system provides time-based reminders and deferred work items that surface at a future date. Entries are stored in SQLite (`schedule_entries`) and queried on demand (pull-based), not fired by a daemon or cron job. This makes the system portable, auditable, and free of background processes.
|
|
|
|
Use `sf schedule` when something needs to happen at a specific future time but cannot (or should not) happen immediately:
|
|
|
|
- **Schedule** — time-bound items that must surface on a date, even if SF is not running continuously
|
|
- **Backlog** — priority-ordered items with no specific timing (SF's standard milestone/slice queue)
|
|
|
|
---
|
|
|
|
## Design Rationale
|
|
|
|
### Pull-Based, Not Daemon-Based
|
|
|
|
SF has no long-running daemon. Entries are not "fired" by a timer. Instead, the schedule store is queried at specific integration points:
|
|
|
|
1. **On launch** — `loader.ts` calls `findDue()` and prints a banner if items are due
|
|
2. **Autonomous mode boundaries** — `sf headless query` (machine snapshot) and the TUI status overlay include due/upcoming entries in their output
|
|
3. **CLI query** — `sf schedule list --due` shows items whose `due_at <= now`
|
|
|
|
This means: if an item is scheduled for 3 AM and you open SF at 9 AM, you will see the item as overdue. There is no fire-at-exact-time guarantee. This is an explicit trade-off — see the [pull-based ADR](../adr/0002-sf-schedule-pull-based.md) for the full decision record.
|
|
|
|
### ULID Identifiers
|
|
|
|
Schedule entries use [ULID](https://github.com/ulid/spec) (Universally Unique Lexicographically Sortable Identifier) instead of UUID. ULIDs are:
|
|
|
|
- 28 characters, Crockford Base32 encoded
|
|
- Lexicographically sortable by creation time (useful for schedule ordering)
|
|
- Unique enough to avoid collisions across concurrent appends
|
|
- Monotonic within millisecond precision via sub-millisecond counter
|
|
|
|
The `generateULID()` function in `schedule-ulid.js` is used for all new entries.
|
|
|
|
### DB-Primary Ledger
|
|
|
|
Each write appends a row to `schedule_entries`. The latest row per ID wins on read. This means status transitions (`pending` → `done`, `cancelled`, `snoozed`) are implemented as ledger entries, not in-place mutations.
|
|
|
|
Legacy `schedule.jsonl` files are import-only compatibility inputs. Rows without `schemaVersion` are treated as legacy version 1. Unsupported future schema versions are ignored by the current reader. Corrupt lines are skipped with a warning, never fatal.
|
|
|
|
---
|
|
|
|
## Storage Format
|
|
|
|
### Storage Locations
|
|
|
|
| Scope | Path |
|
|
|-------|------|
|
|
| `project` | `<basePath>/.sf/sf.db` |
|
|
| `global` | `~/.sf/sf.db` with `scope = 'global'` |
|
|
| legacy import | `<basePath>/.sf/schedule.jsonl` or `~/.sf/schedule.jsonl` |
|
|
|
|
### Schema
|
|
|
|
```json
|
|
{
|
|
"schemaVersion": 1,
|
|
"id": "01ARZ3NDEKTSV4RRFFQ69G5FAV", // ULID — 28 chars
|
|
"kind": "reminder", // ScheduleKind enum
|
|
"status": "pending", // pending | done | cancelled | snoozed
|
|
"due_at": "2026-06-15T09:00:00.000Z", // ISO-8601 timestamp
|
|
"created_at": "2026-05-15T09:00:00.000Z",
|
|
"snoozed_at": "2026-06-01T09:00:00.000Z", // ISO-8601 — set on each snooze
|
|
"payload": { "message": "Review adoption metrics" }, // kind-specific
|
|
"created_by": "user", // user | agent | system
|
|
"autonomous_dispatch": false // if true + kind=prompt/command, consume from autonomous mode
|
|
}
|
|
```
|
|
|
|
### Legacy JSONL Line Example
|
|
|
|
```
|
|
{"schemaVersion":1,"id":"01ARZ3NDEKTSV4RRFFQ69G5FAV","kind":"reminder","status":"pending","due_at":"2026-06-15T09:00:00.000Z","created_at":"2026-05-15T09:00:00.000Z","payload":{"message":"Review adoption metrics"},"created_by":"user","autonomous_dispatch":false}
|
|
```
|
|
|
|
---
|
|
|
|
## Schedule Kinds
|
|
|
|
| Kind | Description | Payload fields |
|
|
|------|-------------|----------------|
|
|
| `reminder` | General time-based reminder | `message`, `unitId?`, `milestoneId?` |
|
|
| `milestone_check` | Milestone health check | `milestoneId`, `checkType?` |
|
|
| `review_due` | Review prompt surfaced at next planning turn | `prUrl?`, `reviewer?`, `unitId?` |
|
|
| `recurring` | Cron-based recurring entry (future) | `cron`, `unitId?`, `milestoneId?` |
|
|
| `review` | Alias for `review_due` — same behaviour | — |
|
|
| `audit` | Audit surfaced at next planning turn | `unitId?` |
|
|
| `command` | Shell command run by explicit `sf schedule run <id>` | `command`, `capture?` |
|
|
|
|
`review` and `audit` kinds are surfaced to the next autonomous planning turn (TBD: integration point in `sf_plan_slice` / `sf_plan_task` / autonomous dispatch). They are stored but not autonomous dispatched without a consumer.
|
|
|
|
---
|
|
|
|
## CLI Reference
|
|
|
|
All commands are invoked as `/schedule <subcommand>` in the TUI or `sf schedule <subcommand>` from the shell.
|
|
|
|
### `sf schedule add`
|
|
|
|
```
|
|
sf schedule add --in <duration> [--kind <kind>] [--scope <scope>] <title>
|
|
sf schedule add --at <ISO-date> [--kind <kind>] [--scope <scope>] <title>
|
|
```
|
|
|
|
Schedule a new item.
|
|
|
|
**Flags:**
|
|
- `--in <duration>` — Relative time from now (e.g. `2w`, `30m`, `1d`, `4h`)
|
|
- `--at <ISO-date>` — Absolute ISO-8601 date
|
|
- `--kind <kind>` — Entry kind (default: `reminder`). Valid: `reminder`, `milestone_check`, `review_due`, `review`, `audit`, `recurring`, `command`
|
|
- `--scope <scope>` — `project` (default) or `global`
|
|
|
|
**Examples:**
|
|
```
|
|
sf schedule add --in 2w "Review feature adoption metrics"
|
|
sf schedule add --in 30m --kind milestone_check "Check M003 validation"
|
|
sf schedule add --at 2026-06-01T09:00:00Z --scope global "Team sync"
|
|
```
|
|
|
|
### `sf schedule list`
|
|
|
|
```
|
|
sf schedule list [--due] [--all] [--json] [--scope <scope>]
|
|
```
|
|
|
|
List scheduled items.
|
|
|
|
**Flags:**
|
|
- `--due`, `-d` — Show only items whose `due_at <= now` (overdue + just-due)
|
|
- `--all`, `-a` — Show all entries including `done` and `cancelled`
|
|
- `--json`, `-j` — Raw JSON output
|
|
- `--scope <scope>` — `project` (default) or `global`
|
|
|
|
**Output columns:** ID (8-char prefix), Title, Due (relative), Status, Kind
|
|
|
|
**Examples:**
|
|
```
|
|
sf schedule list
|
|
sf schedule list --due
|
|
sf schedule list --all --json
|
|
sf schedule list --scope global
|
|
```
|
|
|
|
### `sf schedule done`
|
|
|
|
```
|
|
sf schedule done <id>
|
|
```
|
|
|
|
Mark a pending item as done. ID can be a prefix (ULID prefix match).
|
|
|
|
```
|
|
sf schedule done 01ARZ3ND
|
|
```
|
|
|
|
### `sf schedule cancel`
|
|
|
|
```
|
|
sf schedule cancel <id>
|
|
```
|
|
|
|
Cancel a scheduled item. ID can be a prefix.
|
|
|
|
```
|
|
sf schedule cancel 01ARZ3ND
|
|
```
|
|
|
|
### `sf schedule snooze`
|
|
|
|
```
|
|
sf schedule snooze <id> --by <duration>
|
|
```
|
|
|
|
Postpone a scheduled item by a relative duration. Updates `due_at` and sets `snoozed_at`.
|
|
|
|
```
|
|
sf schedule snooze 01ARZ3ND --by 1d
|
|
sf schedule snooze 01ARZ3ND --by 30m
|
|
```
|
|
|
|
### `sf schedule run`
|
|
|
|
```
|
|
sf schedule run <id>
|
|
```
|
|
|
|
Execute a scheduled item. For `reminder`, `milestone_check`, `review_due` kinds: displays the title and marks done. For `command` kind: executes the stored shell command and captures output.
|
|
|
|
```
|
|
sf schedule run 01ARZ3ND
|
|
```
|
|
|
|
---
|
|
|
|
## Integration Points
|
|
|
|
### Loader Banner (`loader.ts`)
|
|
|
|
On every SF startup, `loader.ts` calls `findDue()` for both project and global scopes. If any items are due, it prints:
|
|
|
|
```
|
|
[forge] N scheduled item(s) due now. Manage: /schedule list
|
|
```
|
|
|
|
### Machine Snapshot (`sf headless query`)
|
|
|
|
`headless-query.ts` populates a `schedule` field in `QuerySnapshot`:
|
|
|
|
```ts
|
|
schedule: {
|
|
due: ScheduleEntry[], // due_at <= now
|
|
upcoming: ScheduleEntry[] // due_at within 7 days
|
|
}
|
|
```
|
|
|
|
This feeds the `sf status` dashboard and autonomous dispatch context.
|
|
|
|
### Milestone YAML Schedule (`sf_plan_milestone`)
|
|
|
|
The milestone plan schema supports a `schedule[]` array in the YAML spec:
|
|
|
|
```yaml
|
|
schedule:
|
|
- in: 2w
|
|
kind: review
|
|
title: "Review adoption after shipping feature"
|
|
```
|
|
|
|
These entries are created at milestone creation time. The `in` field is relative to `now`. The `on_complete` variant fires a duration after milestone completion.
|
|
|
|
### Autonomous Dispatch
|
|
|
|
When `autonomous_dispatch: true` and `kind: "prompt"` or `kind: "command"`, the item is consumed by autonomous mode when `due_at <= now`. This is the mechanism for time-bound autonomous repo work.
|
|
|
|
---
|
|
|
|
## Duration Format
|
|
|
|
All duration strings follow the format `<number><unit>`:
|
|
|
|
| Unit | Meaning |
|
|
|------|---------|
|
|
| `w` | weeks |
|
|
| `d` | days |
|
|
| `h` | hours |
|
|
| `m` | minutes |
|
|
|
|
Examples: `30m`, `4h`, `2d`, `1w`
|
|
|
|
---
|
|
|
|
## Examples
|
|
|
|
### Reminder (2 weeks out)
|
|
|
|
```
|
|
sf schedule add --in 2w "Review feature adoption metrics"
|
|
```
|
|
|
|
### Milestone Check (at milestone creation via plan YAML)
|
|
|
|
```yaml
|
|
# In milestone spec
|
|
schedule:
|
|
- in: 2w
|
|
kind: milestone_check
|
|
title: "Validate M003 success criteria"
|
|
```
|
|
|
|
### Audit (surfaced at next planning turn)
|
|
|
|
```
|
|
sf schedule add --in 1mo --kind audit "Audit ADR-007 decision implementation"
|
|
```
|
|
|
|
### Command (shell command execution)
|
|
|
|
```
|
|
sf schedule add --in 30m --kind command "Reminder: run integration tests"
|
|
# Note: kind=command requires payload.command field — use the CLI directly
|
|
# to set kind=reminder for simple reminders
|
|
```
|
|
|
|
### Global Scope (across all projects)
|
|
|
|
```
|
|
sf schedule add --in 1w --scope global "Review all open milestones"
|
|
```
|