Skip to content

Add model selector to scheduled-task frontmatter (#107) - #108

Merged
max-tet merged 4 commits into
mainfrom
feat/scheduled-task-model-selector
Oct 15, 2026
Merged

max-tet merged 4 commits into
mainfrom
feat/scheduled-task-model-selector

Conversation

@ClaydeCode

Copy link
Copy Markdown
Owner

What changed

Scheduled tasks can now request a specific model via an optional model: frontmatter key, validated at parse time the same way cron/tz/timeout already are:

  • src/clayde/scheduler/tasks.py: ScheduledTask gains a model: str | None field. A new _parse_model() returns None when the key is absent, and raises ValueError(f"bad model {value!r}") for anything not in the new KNOWN_MODELS allowlist (seeded with the current- and prior-generation model ids already referenced in this codebase; carries the same hand-maintained-allowlist caveat as _MODEL_PRICING in claude.py).
  • src/clayde/service/queue.py: Job gains model: str | None = None.
  • src/clayde/scheduler/loop.py: run_tick() passes model=task.model into the enqueued Job.
  • src/clayde/service/runner.py: invoke_claude_job() takes an optional model kwarg and, only when set, appends "--model", model as list elements to the cmd argv passed to asyncio.create_subprocess_exec — never shell-interpolated, so the task-file-controlled value can't reach a shell.
  • src/clayde/service/worker.py: process_job() forwards model=job.model.

When model is absent, behavior is byte-for-byte unchanged from before this PR (no --model flag added at all) — existing scheduled tasks are unaffected.

Out of scope (per the issue, being handled separately in the knowledge_base vault repo): the scheduled-task skill's frontmatter docs, the design doc, and check-task.sh. Also untouched: claude.py's ApiBackend/CliBackend — a separate invocation path used by the issue-working flow, not the scheduler.

Closes #107

Testing

Added tests mirroring the existing timeout test style:

  • tests/scheduler/test_tasks.py: model absent → None, known id accepted, unrecognized id rejected (and skipped by discover_tasks the same way a malformed timeout is).
  • tests/scheduler/test_loop.py: run_tick() carries task.model into the enqueued Job, and leaves it None when absent.
  • tests/service/test_runner.py: --model <id> appears in the built CLI command when model is set, and is absent entirely when it isn't.
  • tests/service/test_worker.py: process_job() forwards job.model into invoke_claude_job(), both when set and when absent.

uv run pytest — 428 passed.

Review panel

Dispatched one adversarial-reviewer subagent against git diff origin/main...HEAD plus the issue text (not this PR's own commit messages or reasoning), instructed to try to refute the change rather than approve it. It traced every call site of Job(...), ScheduledTask(...), and invoke_claude_job(...) across the repo, confirmed the subprocess argv-list safety (no shell interpolation), confirmed backward compatibility (all construction sites use keyword args, model defaults to None), and confirmed the Pebble webhook path can't smuggle an unvalidated model value through (no model field on PebblePayload).

It raised one non-blocking finding: the worker→runner model forwarding (process_job passing model=job.model) had no direct test — the scheduler→Job and runner→--model-flag links were each covered, but not that middle hop. Fixed by adding test_process_job_forwards_model_to_invoke and test_process_job_forwards_none_model_when_absent to tests/service/test_worker.py; re-ran uv run pytest green (428 passed) after the fix.

No blocking findings.

🤖 Generated with Claude Code

ClaydeCode and others added 4 commits October 15, 2026 13:51
Scheduled tasks had no way to request a non-default model — every run
used whatever the CLI defaults to. Parse an optional `model:` key the
same way `tz`/`timeout` are already validated, rejecting unrecognized
ids at parse time (so a typo surfaces in the skipped-file warning,
not silently at run time).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Plumb ScheduledTask.model through to the Job the scheduler enqueues,
so the worker has it available at invocation time. Jobs with no model
set keep the existing default of None.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
invoke_claude_job() now accepts an optional model kwarg and, when set,
appends ["--model", model] to the argv list passed to
create_subprocess_exec — still a list element, never shell-interpolated,
so a task-file-controlled value can't reach a shell. When absent, the
command is byte-for-byte what it was before (no --model flag at all).

process_job() wires job.model through from the queue.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
The adversarial review of this change flagged that process_job's new
model=job.model kwarg had no test exercising it directly — the
scheduler-to-Job link and the runner's --model-flag link were each
covered, but the middle hop (worker passing job.model through) was not,
so a future refactor could silently drop it. Add two tests covering
both the set and absent cases.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
@max-tet
max-tet merged commit fde0647 into main Oct 15, 2026
3 checks passed
@max-tet
max-tet deleted the feat/scheduled-task-model-selector branch October 15, 2026 14:08
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.

Scheduled-task frontmatter has no model selector — every run uses the scheduler's default

2 participants