senkani schedule
Schedule recurring commands via launchd.
Syntax
Behavior
Generates a launchd plist per schedule. Each fire gets its own worktree + pane pair. Budget cap per run. preset install reuses the same PresetInstaller plist generator as create, so the two code paths produce byte-identical plists.
Amplification guard. create AND preset install both call AmplificationGuard.validate before any disk write. Crons whose first two fires sit at or below the 60-second amplification floor (default defaultMinIntervalSeconds) are refused with a structured error; no JSON is written under ~/.senkani/schedules/ and no plist is written under ~/Library/LaunchAgents/. The canonical reproduction case is --cron '* * * * *' (every-minute, 60s gap == floor); the guard refuses up front so the operator sees a clear rejection instead of a silent downstream clamp by the rate limiter. For preset install the guard runs AFTER cron-override resolution AND AFTER the secret-scan, so an amplifying --cron override against a shipped preset (or a malformed user-shipped preset record) is refused with a message naming the preset name + reason + floor (schedule-preset-install-amplification-guard-not-wired-2026-05-21, shipped 2026-05-21).
Counter-cadence schedules. Pass --counter-cadence "every N event_name" (mutually exclusive with --cron) to register a schedule that fires from HookRouter post-tool reactions instead of launchd. Counter cadences DO NOT get a launchd plist — only the ScheduledTask JSON is written under ~/.senkani/schedules/, with cronPattern set to the sentinel COUNTER:<event>:<N> (the JSON schema requires a non-optional cronPattern; dispatch readers filter on task.eventCounterCadence != nil or CounterCadence.isSentinel(task.cronPattern)). Validation refuses N < 2 up front (every-event fires would be clamped to one fire per minute by the rate limiter); the parser tolerates every 10 tool_calls, every 10th tool_call, and every 10 tool_call equivalently. senkani schedule list renders the SCHEDULE column with the operator's prose (e.g. every 10 tool_calls) for counter rows rather than the raw COUNTER: sentinel — cron rows continue to render via CronToLaunchd.humanReadable.
Natural-language cadence (--prose). Pass --prose "every weekday at 9am" instead of a raw cron. The three cadence flags form a three-way XOR: exactly one of --cron, --counter-cadence, or --prose is required. The prose is compiled to a 5-field cron by CompositeProseCadenceCompiler (U.8b-4 production default — rule-first / MLX-fallback) before any disk write; the compiled cron then runs through the same CronToLaunchd.convert validation and AmplificationGuard as --cron, and a launchd plist is generated (prose resolves to a real time-based schedule). The composite tries RuleBasedProseCadenceCompiler (a pure, MLX-free, English-only compiler in Core) first — deterministic phrases below hit in sub-ms — and falls through to MLXProseCadenceCompiler (Gemma 4 VLM, English-only, lazy cold-load on the first fallback call) on .unrecognizedPhrase or .unsupportedLocale from the rule arm. Every other rule-side error (.invalidCron, .unavailable, .invalidJSON, .cancelled) re-throws unchanged; MLX-side errors also re-throw unchanged so the operator sees clear messages when no Gemma tier is downloaded ("No prose-cadence model is available. Pass an explicit cron with --cron instead."). Rule arm phrases: hourly, daily, weekly, every minute, every hour, every day, every N minutes (1–59), every N hours (1–23), every day at <time>, every weekday at <time> (Mon–Fri), and every <weekday> at <time>; <time> accepts 9, 9am, 9:30, 9:30am, 14:00, 2pm. Irregular phrases like every other Tuesday at 6pm fall through to the MLX arm. There is no silent fallback beyond the rule→MLX seam — an unrecognized phrase on both arms or a non-English locale is refused with a clear error pointing at --cron or --locale en-US, and nothing is written. --locale <BCP-47> defaults to the system locale (Locale.current.identifier); both compiler arms accept only en-*. The persisted ScheduledTask records proseCadence (the phrase), compiledCadence + cronPattern (the cron), and locale. Lazy MLX invariant: constructing the composite (which the CLI does on every senkani invocation) does NOT load the Gemma model — model load happens lazily inside MLXProseCadenceCompiler.ensureModel() on the first MLX-fallback call.
Example
$ senkani schedule create --name nightly-lint --cron "0 3 * * *" --command "swift test" $ senkani schedule create --name learn-after-10 --counter-cadence "every 10 tool_calls" --command "senkani learn" $ senkani schedule create --name weekday-brief --prose "every weekday at 9am" --command "senkani learn" $ senkani schedule preset list $ senkani schedule preset install autoresearch --topic "AI workstation" # Amplification guard refuses fast-firing crons: $ senkani schedule create --name fast --cron "* * * * *" --command "echo x" Validation Error: Refused: schedule would amplify (cron fires every 60s, at or below the 60s amplification floor). The amplification floor is 60s. Pick a cron whose fire interval is above the floor. # Counter-cadence with N=1 is refused at parse time: $ senkani schedule create --name every-call --counter-cadence "every 1 tool_call" --command "echo x" Validation Error: Counter cadence N=1 is at or below the amplification floor. Pick N >= 2 so the schedule doesn't fire on every event. # Amplification guard also refuses fast-firing --cron overrides on preset install: $ senkani schedule preset install log-rotation --cron "* * * * *" Validation Error: Refused: preset `log-rotation` would amplify (cron fires every 60s, at or below the 60s amplification floor). The amplification floor is 60s. Pick a `--cron` override whose fire interval is above the floor.
Subcommands
Shipped presets
Five installable presets ship by default under Sources/Core/Presets/Defaults/:
log-rotation— shell, Sunday 03:00 sweep of old worktrees / logs / research.morning-brief— claude, daily 06:30 brief + Pushover push (budget 25¢).autoresearch— local-LLM, daily 06:00 topic scan throughsenkani_search_web+ ollama.competitive-scan— local-LLM, Monday 08:00 competitor roll-up.senkani-improve— claude, nightly 21:00 autoimprove run inside a worktree (budget 500¢).
See also
Sources/CLI/ScheduleCommand.swift