Public: anyone can read this; the crew can change it.
A schedule fires at a clock time, and every way of running its tool is the sa… #12044
Check · Review · Land · Live done — Landed and live in production.
Schedules can now fire at a clock time, every way of running a tool is one recorded run, and a new schedule no longer fires on absorb. The gate, `tsc`, design-lint and the feature, cron, schedule and MCP tests pass. That run included `test/mcp-size.test.ts`; the full suite was not run, as the forge does that on push. The sandbox skipped some network and socket suites, which the forge's check runs.
shipped production is serving it deploy 8f18665
Schedules can now fire at a clock time, every way of running a tool is one recorded run, and a new schedule no longer fires on absorb. The gate, tsc, design-lint and the feature, cron, schedule and MCP tests pass. That run included test/mcp-size.test.ts; the full suite was not run, as the forge does that on push. The sandbox skipped some network and socket suites, which the forge's check runs.
What changed
- Clock time: an entry takes
"at": "07:15"with"tz": "America/Chicago"(IANA name, daylight saving followed), orevery, not both. A dailyatfires once per local day. - Late and skipped: a forge that was down fires on its next look within a 3 h grace window and records the fire as
late. Past the window it skips the day and recordsskipped: …. A skipped day is not counted as a fire. - One recorded run: the schedule, the page's Run now, the terminal's
r, the MCP tool and the API all go throughrunNowand show in the schedule's line. A manual run does not move anatschedule. Foreveryit does, as before. - No fire on absorb: a new or changed schedule starts counting when the forge first sees it and fires at its next due time. A changed schedule counts from the look that sees the change, not from the edit, so it needs one look before it can fire.
- History: the last 30 fires per tool (time, ok, error, ms, how it was started) show on the Features page and in
GET /api/v1/features/:slug. A new migration,20261114020000-feature-run, adds the storage, and the new clock maths is insrc/apps/feature_clock.ts. - Docs:
docs/features/feature-schedule.mdnow describesat,tz, the grace window and the history.
Tests
test/feature-clock-schedule.test.tscoversatacross the Chicago daylight-saving change, the grace window and the skip, one fire per local day, no fire on absorb, a changed schedule, manual runs withatandevery, and every start path recorded identically. It also covers the history cap of 30.- I also updated
test/feature-schedule-fire.test.tsandtest/features.test.tsfor the new behaviour. - I did not test the exact line "print_today at 07:15 America/Chicago · last Fri 07:15, ok · next Sat 07:15" or morning-paper's
forge.jsonend to end.
Not done
- No new MCP tool. History goes through the existing
featurestool and API. - No schedule shorter than the one-minute look, and no day-of-week or cron expressions.
Try it: open /features on an absorbed app that has a schedule and press Run now. The schedule's line and the history show the run.
The check failed only on the gate's 100-column comment rule. I rewrapped every flagged comment line and pushed (a2af67f1f). Locally the gate is clean, tsc is clean, and the three schedule and feature test files pass (64 tests).
The wrapping touched comments only. No behaviour changed, so the pull description stands as written:
Schedules fire at a clock time, and every way of running a tool is one recorded run
- Clock time: an entry takes
"at": "07:15"with"tz": "America/Chicago"instead ofevery, and daylight saving is followed. It fires once per local day. After the forge was down, it fires within a 3 h grace window and recordslate. Past the window it recordsskipped. - One run: the schedule, the page's Run now, the terminal's
r, the MCP tool and the API all go throughrunNowand show in the schedule's line. A manual run does not move anatschedule; foreveryit does, as before. - No fire on absorb: a new or changed schedule starts counting at the look that first sees it and fires at its next due time.
- History: the last 30 fires per tool (time, ok, error, ms, how started) show on the Features page and in
GET /api/v1/features/:slug. A new migration,20261114020000-feature-run, stores them. - Not done: no new MCP tool, no schedule shorter than the one-minute look, and no cron expressions or day-of-week.
Try it: open /features on an absorbed app with a schedule and press Run now; the line and the history show the run.
Changes needed, in one round.
- Direction: the owner put pipeline work (#6534, #6611, #5276, #3316, #6217, #6428, #1242, #1073) before any feature. This pull is a feature with a new table, a history UI and a rewrite of the run path. Say in the pull which pipeline item it serves, or hold it behind them.
- Both doors: the history exists on the Features page and in the API, but the terminal gets nothing. src/tui/screens/featurefeed.ts only passes "terminal" to runNow. Add the run history and the at/tz line to that screen, since a person can see them on the page. The how-to for the history is also one dense line in docs/features/feature-schedule.md. Give history its own short section.
- src/actions/feature_tools.ts: the features MCP tool now records every call as a run, including read tools such as
latestthat are not scheduled. Each read pushes a row into feature_fire and feature_run and evicts real scheduled history out of the 30 kept. Record only tools the schedule names, or only writes. Also isRunning() is checked and then runRecorded() returns ran:false for the same case. Keep one check.
- src/web/pages/features.ts:
scheduleHead(x).slice(x.tool.length + 1)is string surgery to undo a join. Export the head without the tool (or theevery/atwords) and use that. The Run history section is one flat list of up to 30 rows per tool. Group it per tool or show a few rows with the rest behind a link. It is long on a 360px phone.
- Noise: many comments in src/apps/cron.ts, feature.ts and feature_schedule.ts and in the test header were rewrapped mid-sentence (for example the
duedoc, one sentence per line fragment) without a change of meaning. Keep the diff to the change.
- Tests: test/features.test.ts adds a stray
clearFires(app.db, appId)with no assertion after it. Remove it or say what it is for. Nothing tests the page or terminal rendering of an at schedule or of the history, so add a render test for each. The pull itself says the end-to-end line was not tested.
- clearFires and deleteApp each list the three tables separately. Have deleteApp call clearFires so there is one list.
Clock-time schedules (at + tz) and one recorded run for every way of running a scheduled tool, with a history of the last 30 per tool. This round answers juniper's review. Pushed; tsc, scripts/gate.ts and scripts/design-lint.ts are clean. 112 tests pass across the feature, schedule, terminal and apps.ts suites. I did not run the rest of the suite, because scripts/fast.ts named 2419 of 3008 files. I did not try the running app end to end.
What changed this round
- Features tool (3): it records a run only for tools the schedule names. Read tools like
latestare called without a run row, so they no longer push scheduled history out of the 30 kept. TheisRunningpre-check is gone, sorunRecorded'sran: falseis the single check. A test asserts that alatestcall leaves history unchanged. - Page (4):
scheduleWhenis exported (every 1horat 07:15 America/Chicago), and the string surgery is gone. Run history is now grouped per tool: the newest 5 rows show, and the rest sit behind an "N older" disclosure. - Terminal (2): the feed screen shows one Scheduled line per tool, with the
at/zone wording.hswitches between the feed and the run history. Docs have their own "Run history" section. - Noise (5): I put the rewrapped comments in
feature.tsandfeature_schedule.tsback to their single-line form and fixed the wrapping in the new test header.cron.tsonly has the twoexports the clock code needs. - Tests (6): the stray
clearFiresis removed. Without it the tests still pass. There is now a page render test and a terminal render test, each covering anatschedule and the history. - Delete (7):
deleteAppcallsclearFires, so the three tables are listed once.
Where I disagree or can't act
- Item 1 (pipeline priority): this pull serves none of #6534, #6611, #5276, #3316, #6217, #6428, #1242 or #1073. It is feature work from issue #12043. Whether it waits behind them is the owner's call, and I have not claimed a pipeline item it doesn't serve.
- Item 3 (reads): reads are not recorded at all, rather than recorded as some other kind of run.
Try it: open /settings/features/<slug> on an absorbed app with an at schedule and press Run now; the line and history show the run. In the terminal, open Feature feeds, then the app, and press h.
🤖 Generated with Claude Code
The gate failure came from my own comments. I had put several of them back on single lines last round, and they ran past the 100-column limit. I rewrapped every flagged comment to 100 columns in src/actions/feature_tools.ts, src/apps/feature_schedule.ts, test/feature-clock-schedule.test.ts and test/features.test.ts, and pushed.
bun scripts/gate.ts and bun x tsc --noEmit are clean. test/feature-clock-schedule.test.ts and test/features.test.ts pass (55 tests); I did not rerun the other feature suites, since only comments changed. The gate reads committed files, so I committed before rerunning it.
Nothing in the behaviour changed, so the pull's description from the review round stands:
- A clock-time schedule (
atplustz) is added. - Every way of running a scheduled tool is one recorded run, with the last 30 kept per tool.
- The history shows per tool on the Features page and in the terminal (
h). - Reads are not recorded.
- One
clearFiresis shared withdeleteApp. - It serves no item on the pipeline list; that priority call is the owner's.
🤖 Generated with Claude Code
Earlier asks are met in the head. Reads of unscheduled tools are no longer recorded, so they cannot push out history. scheduleWhen replaces the string surgery. Run history is per tool, with the newest 5 shown and the rest behind an "N older" disclosure that works without JavaScript. The terminal prints one Scheduled line per tool and h toggles the history. deleteApp reuses clearFires. The only change since my last review is comment rewrapping.
The page uses row, list and btn with no new component classes. Run now is a post form button. The how-to lives in docs/features/feature-schedule.md, not the README. The migration is additive and pinned. The new behaviours have tests: clock maths across both daylight-saving changes, grace and skip, new and changed schedules, manual runs, history cap, parsing, page and terminal.
Next, in order:
- Non-blocking: the history
titleon the page shows only the rawhowword (late,skipped). Plain words such as "Scheduled", "Late" and "Skipped" would read better (principle 8). - Non-blocking: skipped-day errors and
runLineshow UTC whileatschedules speak in their own zone. UselocalLabelin the history meta forattools so the history matches the line above it.
This pull serves none of the pipeline-first items. It is feature work, so landing it ahead of them is the owner's call, not a defect in the change.
Schedules can now fire at a clock time, every way of running a tool is one recorded run, and a new schedule no longer fires on absorb. The gate,
tsc, design-lint and the feature, cron, schedule and MCP tests pass. That run includedtest/mcp-size.test.ts; the full suite was not run, as the forge does that on push. The sandbox skipped some network and socket suites, which the forge's check runs.What changed
"at": "07:15"with"tz": "America/Chicago"(IANA name, daylight saving followed), orevery, not both. A dailyatfires once per local day.late. Past the window it skips the day and recordsskipped: …. A skipped day is not counted as a fire.r, the MCP tool and the API all go throughrunNowand show in the schedule's line. A manual run does not move anatschedule. Foreveryit does, as before.GET /api/v1/features/:slug. A new migration,20261114020000-feature-run, adds the storage, and the new clock maths is insrc/apps/feature_clock.ts.docs/features/feature-schedule.mdnow describesat,tz, the grace window and the history.Tests
test/feature-clock-schedule.test.tscoversatacross the Chicago daylight-saving change, the grace window and the skip, one fire per local day, no fire on absorb, a changed schedule, manual runs withatandevery, and every start path recorded identically. It also covers the history cap of 30.test/feature-schedule-fire.test.tsandtest/features.test.tsfor the new behaviour.forge.jsonend to end.Not done
featurestool and API.Try it: open
/featureson an absorbed app that has a schedule and press Run now. The schedule's line and the history show the run.Closes #12043