Repository navigation
feat(core): gate scheduled prompts with shell checks - #60137
Conversation
There was a problem hiding this comment.
Shell-gated schedules mostly work as described. The core schedule, tool and permission tests pass (148), the server schedule test passes, and the core type check is clean. One creation-time behaviour should change before merge (see the inline comment): a check whose condition isn't met yet can be rejected at creation, and whether that happens depends on when. I confirmed this with a temporary test using the PR's fake shell.
The tests could also be smaller. The new suites rebuild a large part of the location layer (a fake spawner, Agent, Permission and FileAccess) and add about 340 lines to schedule.test.ts. Cutting them down to the main cases (immediate match, poll then wake, expired deadline, changes baseline, 3-failure pause) would make them easier to maintain.
Public API:
- HTTP
POSTschedule andschedule_create:whenchanges from required to optional. New optionalifanduntil({ run, is?, matches?, changes? }). Creation can now return 403 when a check needs approval. Schedule.Info: addsif,untilandlastOutput.Schedule.Outcome: adds"checked"and"expired".- Core: exports
Permission.Unattended. - Database: migration adds the
check,state,checksandfailurescolumns.
These are additions, so existing callers keep working. Two exceptions: clients that switch over every Outcome need the two new values, and TypeScript code that reads Request.when or CreateInput.when must now handle undefined.
| ), | ||
| ), | ||
| ) | ||
| const shouldSeed = |
There was a problem hiding this comment.
When a check uses is, matches or changes, a non-zero exit counts as a check error. Here, that error at creation makes create fail with InvalidError (line 657). Whether the check runs at creation depends on when, so the same check gives different results. With a command exiting 1 and if: { run, is: "up" }, every: "1 minute" is rejected but a cron is accepted.
The until case hurts most, since its job is to wait for a condition that isn't true yet. For example, until: { run: "curl -sf …", is: "ready" } can't be created while the service is down. Could a failed seed count as "not yet" (like a quiet tick), with creation rejecting only syntax and permission problems? Running the check at creation for if + every without changes doesn't seem needed either.
There was a problem hiding this comment.
The new commit fixes the creation problem from my last review. A failed shell check when a schedule is created now counts as "not yet" instead of rejecting it, and if + every no longer runs the check at creation, so every and cron behave the same. Permission and syntax problems are still rejected earlier, during authorization. Core schedule and tool tests pass (40) and the core type check is clean. Nothing new to raise. The public API is unchanged since the last review.
2f21615 to
64d6f83
Compare
There was a problem hiding this comment.
This push is a rebase onto the latest dev: the only difference is that the schedule-checks migration was renamed to 20261029165540_schedule_checks and now comes after session_time_busy. The fix for my earlier point (a failed check at creation counts as "not yet") carries over unchanged. Core schedule, tool and permission tests (148) and the server schedule test pass, and the core type check is clean. Nothing new to raise; the public API is unchanged since the last review.
Schedules can now run a shell check before waking a session: wait once with
until("wake me when CI passes") or gate a recurring schedule withif, without spending a model call on quiet polls.Builds on #59401 and #59457. Everything stays behind
experimental.schedules, and plain prompt schedules behave the same.💡 Why
Waiting on a build, pull request check, or remote branch with a plain repeating schedule spends a full model call on every tick just to learn nothing changed.
🆕 Now you can
until: { run: "gh pr checks 123" }(whendefaults to every 30 seconds,ends.afterdefaults to 24 hours). If the check already passes at creation,schedule_createfinishes immediately with its output instead of queuing a wake.if: { run: "...", changes: true }(oris/matches, down to a 15-second interval). Quiet checks recordcheckedand leaveends.timesuntouched sotimescounts wakes, not silent polls.schedule_createscans subcommands and external directories up front, prompts at most once (without saving a project-wide grant), rechecks livedenyrules on every tick, pauses after 3 failed checks (waking once with the error), and wakes withexpiredif anuntildeadline passes without matching.🚦 How it ships
Off by default behind
experimental.schedules, same as #59401 and #59457.📊 Proof: 40 core schedule and tool tests (8 new shell-check suites covering immediate
t=0match, default 30s/24huntil,expireddeadline wake,changesbaseline seeding and wake-onlyends.times,is/matches, one-time approval + revoked permission 3-strike pause,Plugin.awaitActivation+ missing directory auto-pause, and input validation), 108 permission tests, 41 client tests, and 1 end-to-end server test (including immediate match and 403 on stateless HTTPask).↩️ Undo: revert isn't enough on its own once a database runs this build, because migration
20261029061045_schedule_checksadds nullablecheckandstatepluschecksandfailurescolumns toschedule. Older builds ignore the extra columns unlessexperimental.schedulesis turned on.👀 Read:
packages/core/src/schedule/check.ts(authorizeandrunShell) andfireinpackages/core/src/schedule.ts. Creation runs withoutsaveso approving a check never writes a project-wide allow, and each tick re-evaluates permissions withPermission.Unattendedafter livedenyrules. The rest is routine (schema, OpenAPI, generated client).Planned feature · no user reports · not measured
Checks run
bun run script/test.ts test/schedule.test.ts test/tool-schedule.test.ts test/permission.test.ts(core): 148 pass, 0 failbun test test/schedule.test.ts(server): 1 pass, 0 failbun test test/promise.test.ts(client): 41 pass, 0 failbun typecheckin core, server, protocol and client: 0 errorsbun run script/migration.ts --check(core) andbun run check:generated(protocol): passoxlint,ast-grepEffect patterns on changed files, andbun script/sdk-docs.ts: 0 errorsAdded by the opencode bot (AI-generated).
Public API: adds optional
if/untilto schedule create (makeswhenoptional), addslastOutput/if/untilto Schedule.Info andchecked/expiredoutcomes (additive; clients that match every outcome need the new values)