Skip to content

scheduler: file-driven scheduled tasks - #103

Merged
max-tet merged 21 commits into
mainfrom
feat/scheduled-tasks
Sep 21, 2026
Merged

max-tet merged 21 commits into
mainfrom
feat/scheduled-tasks

Conversation

@ClaydeCode

Copy link
Copy Markdown
Owner

Summary

Adds a file-driven scheduler to Clayde: markdown task files in a host-mounted ~/clayde-tasks/ directory run their body as a prompt on a schedule (one-off timestamp or recurring cron), independent of any interactive session or the Pebble watch. Due tasks flow through the existing job pipeline (fresh Claude CLI session, skills available, cwd = KB).

Motivating case: a recurring keep-warm task whose CLI run refreshes the container's OAuth token, so the CLI login stops lapsing from disuse.

Design doc: docs/superpowers/specs/2026-09-20-scheduled-tasks-design.md · Plan: docs/superpowers/plans/2026-09-21-scheduled-tasks.md.

What's in it

  • Neutral service/ package. The job-execution core (queue, worker, runner, notify, skills) moved out of webhook/ into src/clayde/service/; PebbleJobJob with an origin field. webhook/ is now just the HTTP producer; scheduler/ is the second producer. Behaviour-preserving move.
  • scheduler/ package. Task-file parsing (tasks.py), recurring-task run-state (state.py), pure due-ness/lateness helpers + the tick loop (loop.py). scheduler_loop joins the orchestrator's asyncio.gather, gated by CLAYDE_SCHEDULER_ENABLED.
  • Auto permission mode. The shared runner now uses --permission-mode auto --permission-prompts none instead of --dangerously-skip-permissions, so headless runs auto-deny dangerous actions. Applies to Pebble jobs too (a hardening).
  • Whole KB skill library mounted at /skills/kb; discovery is now SKILL.md-aware (ignores reference markdown).
  • Per-task timeout. Optional timeout: frontmatter (4h / 90m / 45s / bare seconds), defaulting to CLAYDE_SCHEDULER_TIMEOUT (300s), hard-capped at 4h. Pebble jobs keep 300s. The budget is surfaced in the system prompt so long tasks scope their work.

Task file format

---
cron: "0 8 * * *"        # recurring, OR:  at: 2026-09-21T08:00  (one-off)
tz: Europe/Berlin         # optional, default from CLAYDE_SCHEDULER_TZ
enabled: true             # optional
timeout: 4h               # optional, default 300s, capped at 4h
---
The prompt to run.

Fired one-offs are moved to ~/clayde-tasks/done/. Recurring dedup lives in /data/scheduler_state.json. Overdue tasks fire once on the next tick after downtime (no backfill), with a lateness note prepended to the prompt.

Notifications

Scheduler jobs are silent on success; the framework still fires an ntfy on failure (timeout / usage limit / CLI / auth / crash). Intentional success notifications are prompt-driven (the task asks the agent to use the ntfy-ping skill). Pebble jobs are unchanged (notify on every outcome).

Testing

uv run pytest416 passing, output pristine. Every task was built TDD with an independent review gate; a final whole-branch review surfaced one dedup-persistence edge (a failing task could drop an earlier cron task's dedup) which is fixed and covered by a test.

Known limitations (v1, by design)

  • In-memory job queue: a restart between enqueue and execution loses that one run.
  • Single serial worker: a long scheduled run holds the worker (and queues Pebble requests) until it finishes — sequential by design; long tasks are meant for off-hours.
  • Single attempt, no auto-resume: a run that hits its timeout or a usage limit fails once. Long tasks should be told, in their prompt, to persist interim progress as they go.

Deployment (not done here)

The running stack (~/clayde/docker-compose.yml, data/config.env) is separate from this repo's compose and untouched. To enable: add the ~/clayde-tasks:/tasks and ~/knowledge_base/skills:/skills/kb:ro mounts, set CLAYDE_SCHEDULER_ENABLED=true, rebuild the image, create ~/clayde-tasks/ with a keep-warm.md, and ensure a CLI login exists (and doesn't lapse before the first keep-warm tick).

Wires scheduler_loop (Task 11) into _run_with_pebble, gated by
settings.scheduler_enabled, mirroring the existing freeshard gating.
Adds _scheduler_state_path() for the state file location.

Scheduler jobs share the worker's pebble_timeout; settings.scheduler_timeout
is intentionally not wired into the worker in v1 (per-origin timeout is a
deferred follow-up).

Also sets scheduler_enabled=False explicitly on the freeshard gather test's
mock settings, since the new gate would otherwise default to truthy on a
bare MagicMock and hang that test in an infinite scheduler loop.
@max-tet
max-tet merged commit b3d275c into main Sep 21, 2026
3 checks passed
@max-tet
max-tet deleted the feat/scheduled-tasks branch September 21, 2026 11:22
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants