singularity-forge/docs/specs/sf-schedule.md
2026-05-08 01:34:07 +02:00

9 KiB

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 launchloader.ts calls findDue() and prints a banner if items are due
  2. Autonomous mode boundariessf headless query (machine snapshot) and the TUI status overlay include due/upcoming entries in their output
  3. CLI querysf 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 for the full decision record.

ULID Identifiers

Schedule entries use ULID (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 (pendingdone, 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

{
  "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:

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:

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)

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