feat(scheduler): scheduled jobs inside skills (.volcanic/skills/<skill>/jobs/<job>.md)
Skill directories may now declare prompt or watcher jobs as files (cron or one-off at). The file is the source of truth: the scheduler re-reads every chat's jobs/ every minute, around agent runs and before /jobs and /skills, and schedules, re-plans or removes them. Ids are <skill>/<job>; run state lives in .volcanic/scheduler/skill-jobs-state.json, never in jobs.json. Watchers run from the skill directory with VOLCANIC_WORKSPACE and VOLCANIC_SKILL_DIR. Broken files are shown with the reason in /jobs, /skills, schedule list and the new <skill_jobs> prompt section; cancel answers with the file to edit. Every part of a skill directory is optional; file reading and frontmatter parsing move to Abstractions.
This commit is contained in:
49
docs/superpowers/specs/2026-10-14-skill-jobs-design.md
Normal file
49
docs/superpowers/specs/2026-10-14-skill-jobs-design.md
Normal file
@@ -0,0 +1,49 @@
|
||||
# Skill jobs — design
|
||||
|
||||
## Goal
|
||||
|
||||
Let a skill declare scheduled jobs as files: `<workspace>/.volcanic/skills/<skill>/jobs/<job>.md`, prompt or watcher,
|
||||
cron or one-off `at`. Every part of a skill directory (SKILL.md, INSTRUCTIONS.md, scripts, `jobs/`) is optional, in any
|
||||
combination. The owner's spec (2026-10-14) was accepted with the eight amendments below.
|
||||
|
||||
## Decisions
|
||||
|
||||
- **One scheduler, separate source.** Skill jobs run in the process-wide `JobScheduler`: one timer, the same script
|
||||
runner, script environment (`GOOGLE_ACCESS_TOKEN`), redaction, failure alerts without repeats, and the skip of an
|
||||
occurrence while the previous run is in flight. They are never written to `jobs.json`: the file is the source of
|
||||
truth. `SyncSkillJobsAsync(target)` re-reads the target's files and adds, re-plans (definition changed) or removes
|
||||
(file gone, `enabled: false`, broken) entries; an unchanged definition keeps its next run. Entries carry
|
||||
`ScheduledJob.Skill` (`[JsonIgnore]`), and their table key includes the target because `<skill>/<job>` is only unique
|
||||
per chat.
|
||||
- **Sync points**: on start (before overdue one-offs fire), every `Scheduler:SkillJobSyncInterval` (1 min) for every
|
||||
deliverable known target, in `SchedulerExtension` on `before_agent_start` and `agent_end` (a file the agent just
|
||||
wrote is in `/jobs` at once), and before `/jobs`, `/skills` and `schedule list`.
|
||||
- **State** per workspace in `.volcanic/scheduler/skill-jobs-state.json`: last run and result, the `at` text that fired
|
||||
(`doneAt`), and the `at` text last seen with when it was first seen. State of deleted files is dropped.
|
||||
- **Amendments (accepted)**:
|
||||
1. The id comes from paths only: `<skill dir>/<file name without .md>`. `name` is a label.
|
||||
2. The header carries the file: `[Scheduled job "<skill>/<job>" (skill job, .volcanic/skills/<skill>/jobs/<job>.md)]`;
|
||||
the failure alert too.
|
||||
3. An `at` already past (more than 60 s) when the bot first saw that `at` never fires and is shown as not
|
||||
scheduled; an `at` seen before it was due and missed while the bot was down fires late on start.
|
||||
4. Sync every minute plus before and after each run (above).
|
||||
5. File reading and the simplified frontmatter parser live in Abstractions (`SkillFiles`, `Frontmatter`), shared by
|
||||
the core's `SkillCatalog` and the scheduler. Job files additionally cut YAML comments (`#` after whitespace).
|
||||
6. The format and the chat's skill jobs go into the system prompt from `SchedulerExtension` (`<skill_jobs>`), so
|
||||
sessions without the scheduler never advertise them.
|
||||
7. Prompt text up to 16 KB.
|
||||
8. `enabled: false` unschedules the job; it stays listed as `disabled`.
|
||||
- **No frequency guard**: cron has 5 fields (no seconds), so once a minute is the floor, and overlapping runs are
|
||||
skipped already.
|
||||
- **Limit**: `Scheduler:MaxSkillJobsPerTarget` (50), separate from `MaxJobsPerTarget`; beyond it, files are listed as
|
||||
over the limit. At most 200 job files are read per workspace.
|
||||
- **Watchers** run from the skill directory with `VOLCANIC_WORKSPACE` and `VOLCANIC_SKILL_DIR` added after the
|
||||
environment filter; those paths are not redacted (only the host's extra values are).
|
||||
- **Control is by files only**: `/cancel` and `schedule cancel` on a skill job id answer with the file to edit.
|
||||
- **Errors aren't silent**: missing `kind`, neither or both of `cron`/`at`, a watcher without `command`, an invalid
|
||||
cron, `at`, `timeoutSeconds` or `enabled` → not scheduled, shown with the reason in `/jobs`, `/skills`, `schedule
|
||||
list` and the prompt section.
|
||||
|
||||
## User docs
|
||||
|
||||
`docs/telegram-bot.md#skill-jobs`, and the skill directory rules under `#skills`.
|
||||
@@ -40,7 +40,9 @@ settings use the `GoogleCalendar` section, the scheduler the `Scheduler` section
|
||||
| `Scheduler:Enabled` | `true` | Scheduled prompts and watcher scripts (see [Scheduling](#scheduling)) |
|
||||
| `Scheduler:LegacyStorePath` | `<agent dir>/scheduler/jobs.json` | The retired shared job file. Jobs live in each chat's `<workspace>/.volcanic/scheduler/jobs.json`; on start the jobs found here move into their chats' files and this file is renamed `jobs.json.migrated-<timestamp>` |
|
||||
| `Scheduler:DefaultScriptTimeout` | `00:05:00` | Watcher timeout when a job sets none (1 s to 1 h) |
|
||||
| `Scheduler:MaxJobsPerTarget` | `50` | Jobs per chat |
|
||||
| `Scheduler:MaxJobsPerTarget` | `50` | Jobs per chat (the `schedule` tool's) |
|
||||
| `Scheduler:MaxSkillJobsPerTarget` | `50` | Skill jobs per chat, counted apart (see [Skill jobs](#skill-jobs)) |
|
||||
| `Scheduler:SkillJobSyncInterval` | `00:01:00` | How often every chat's skill job files are re-read |
|
||||
| `Scheduler:Shell` | `/bin/sh -c` (`cmd.exe /d /s /c` on Windows) | The watcher shell as a list, command appended; same as `bash` by default |
|
||||
| `OPENAI_API_KEY` / `Telegram:Speech:ApiKey` | — | Turns voice transcription and the `voice` tool on. Hidden from `bash` and redacted from tool output |
|
||||
| `STT_MODEL` / `Telegram:Speech:Model` | `gpt-4o-transcribe` | `gpt-4o-mini-transcribe` and `whisper-1` also work |
|
||||
@@ -195,8 +197,12 @@ blocks in directory order. It is re-read on every run, so an edit counts from th
|
||||
SKILL.md `name`, else the directory name. A directory may hold only an `INSTRUCTIONS.md`; it is then not listed among
|
||||
the skills, but its rules still apply. Limits: 16 KB (16,384 characters) per file and 64 KB (65,536) for all
|
||||
files together; a file over its share is cut and the prompt says `[…truncated, read the full file at <path>]`, and
|
||||
once the total is used up, later files appear with only that note. Empty and unreadable files are skipped. `/skills`
|
||||
marks skills with `(+ INSTRUCTIONS.md)` and lists instruction-only directories as `INSTRUCTIONS.md only`.
|
||||
once the total is used up, later files appear with only that note. Empty and unreadable files are skipped.
|
||||
|
||||
Every part of a skill directory is optional: a directory is a skill when it holds a `SKILL.md`, an `INSTRUCTIONS.md`
|
||||
or a `jobs/` directory ([skill jobs](#skill-jobs)), in any combination. `/skills` lists every such directory in
|
||||
directory order: a skill with a SKILL.md as `• name – description (+ INSTRUCTIONS.md, 2 jobs)`, any other as
|
||||
`• dir – INSTRUCTIONS.md, 2 jobs`, and a broken job file under its skill as `⚠️ <file>: <problem>`.
|
||||
|
||||
```markdown
|
||||
---
|
||||
@@ -248,6 +254,53 @@ bot is stopped; a job in a chat's file always belongs to that chat. Watcher fail
|
||||
conversation, so the agent can't see them later. With
|
||||
`Scheduler:Enabled=false`, `/jobs` and `/cancel` reply that scheduling is turned off, and `/timezone` still works.
|
||||
|
||||
#### Skill jobs
|
||||
|
||||
A skill can declare scheduled jobs as files, one per job: `<workspace>/.volcanic/skills/<skill>/jobs/<job>.md`
|
||||
(subdirectories of `jobs/` aren't read). The skill needs nothing else: `jobs/` alone is enough. The job's id is
|
||||
`<skill>/<job>` (directory name and file name without `.md`, whatever `name` says), so a restart or an edit never
|
||||
duplicates it.
|
||||
|
||||
```markdown
|
||||
---
|
||||
kind: prompt # prompt | watcher, required
|
||||
cron: "0 8 * * 1-5" # recurring, 5 fields, chat's zone; or instead
|
||||
at: 2026-10-20T09:00 # one-off, ISO 8601; without an offset in the chat's zone
|
||||
command: python3 scripts/check.py # watcher only, required there
|
||||
timeoutSeconds: 60 # watcher only, 1–3600, default 300
|
||||
enabled: true # false: the job is off
|
||||
name: Morning digest # optional label (default: the file name)
|
||||
description: Weekday summary # optional, shown in /jobs
|
||||
---
|
||||
For a prompt job: what the agent should check and write when it fires (up to 16 KB).
|
||||
```
|
||||
|
||||
Exactly one of `cron` and `at`. The frontmatter is the same simplified YAML as SKILL.md, and `#` after a space starts a
|
||||
comment. A watcher's body is ignored (use it for notes).
|
||||
|
||||
- **The file is the source of truth.** The bot re-reads every chat's `jobs/` every minute, before and after each agent
|
||||
run, and when `/jobs` or `/skills` is used: a new file is scheduled, an edit re-plans the job, and a deleted file or
|
||||
`enabled: false` takes it off. No restart is needed. The agent can create jobs itself by writing these files; its
|
||||
system prompt has the format and this chat's skill jobs in a `<skill_jobs>` section.
|
||||
- **prompt**: the body reaches the agent as `[Scheduled job "<skill>/<job>" (skill job, .volcanic/skills/<skill>/jobs/<job>.md)]`
|
||||
followed by the text, like a `schedule` prompt.
|
||||
- **watcher**: the bot runs `command` itself, from the **skill directory** (so `scripts/check.py` is relative to the
|
||||
skill), with `VOLCANIC_WORKSPACE` (the chat's workspace) and `VOLCANIC_SKILL_DIR` (the skill directory) added to the
|
||||
usual watcher environment (filtered like `bash`, `TZ`, `GOOGLE_ACCESS_TOKEN`). Output, exit codes and failure alerts
|
||||
work as for other watchers; the header carries the file path.
|
||||
- **One-off** (`at`): fires once, then it is done (recorded by the bot, not in the file) until `at` changes. One that
|
||||
came due while the bot was down fires after the start with the "bot was offline" note. An `at` that was already past
|
||||
when the bot first saw the file (for example a copied skill) never fires and is shown as not scheduled. Missed
|
||||
`cron` occurrences aren't caught up.
|
||||
- **Errors aren't silent**: a file without `kind`, with neither or both of `cron`/`at`, a watcher without `command`, a
|
||||
bad cron, `at` or `timeoutSeconds` isn't scheduled and appears in `/jobs` and `/skills` with the reason.
|
||||
- **Managed through files only.** `/jobs` and the `schedule` tool's `list` show skill jobs in their own block (id, kind,
|
||||
schedule, next run, `disabled`, `done` or the error); `/cancel <skill>/<job>` and `schedule cancel` answer with the
|
||||
file to edit (`enabled: false`). Up to 50 skill jobs per chat (`Scheduler:MaxSkillJobsPerTarget`), separate from the
|
||||
50 `schedule` jobs; the rest are listed as over the limit.
|
||||
- The bot's own record (last run and result, fired one-offs, when it first saw each `at`) is
|
||||
`.volcanic/scheduler/skill-jobs-state.json` in the workspace; skill jobs never go into `jobs.json`.
|
||||
|
||||
#### Importing VolcanicSecretary jobs
|
||||
|
||||
Copy VolcanicSecretary's `.secretary/jobs.json` to the chat's `.volcanic/scheduler/jobs.json`
|
||||
|
||||
Reference in New Issue
Block a user