Skip to content

feat(studio): schedules — capability-gated inventory, spec mapping, actions, fires - #1758

Merged
samuv merged 1 commit into
plan/studio-schedulesfrom
studio/schedules
Sep 23, 2026
Merged

samuv merged 1 commit into
plan/studio-schedulesfrom
studio/schedules

Conversation

@samuv

@samuv samuv commented Sep 22, 2026 •

Copy link
Copy Markdown
Contributor

Summary

What lands

  • BFF: /api/v1/schedules… list with the supported flag and reason read live from the negotiated snapshot, create and update mapping mode / profile / cron-or-once trigger onto the SDK spec, fire / pause / resume, delete, newest-first fire history with a derived in-flight flag; status derived from enabled / in-flight / pending-claim state.
  • Web: schedules workspace, form with cron builder and phrase parser, detail page; "Scheduled" joins the nav.

Amended after review

Implements the amended contract and fixes the two implementation findings:

  • Daylight-saving round-trip. The form carries the schedule's original one-shot instant and re-sends it unchanged while the displayed wall clock is unedited, so saving an untouched schedule can no longer move its fire. An edited wall clock resolves with the documented rule, and the form now shows the absolute time it will save. Three proofs pin it in a fixed zone (Europe/Rome, which falls back on 2026-10-25 and springs forward on 2026-03-29); they pass under UTC, America/New_York, and Asia/Tokyo as well as the local zone.
  • Terminal status. A schedule the daemon reports with no next fire after at least one fire is completed.
  • Numeric bounds. Both counts are bounded to 0..2147483647 in the contract schema, so an oversized or non-safe value is a 400 before any SDK call.
  • next <weekday> at <time> now parses: the pattern accepts the optional at token, with coverage for the bare form, the at form, and odd spacing and case.
  • ?schedule=<name> now opens that schedule's editor once the inventory has loaded, and a ref keeps closing the editor from reopening it.

Verification

69 server and 209 web Vitest proofs (17 ACs, 0 missing); task studio:check, task docs pass; the unsupported path smoke-tested against mecated --mock, which runs with scheduling off. A manual run against a deployment with scheduling enabled is still owed.

Further amendments

Implements the amended schedules contract (see the plan layer below):

  • The adapter refuses a changed trigger before calling update. It validates the timezone through Intl and derives completed ahead of paused.

  • The form sends the stored mode and trigger unchanged, and locks trigger editing for an existing schedule.

  • Run now, Pause, and Resume are gated on status, and Run now refreshes that schedule's fire history.

  • An empty timezone reads UTC.

  • Trigger lock. The form now routes every trigger edit through withTriggerPatch and renders from displayedValue. An existing schedule can no longer show one trigger while sending another.

  • Plan status. This layer's diff moves its acceptance plan to landed (header and index), per the plan lifecycle; the transition is authoritative only when this PR merges, and task ac-trace-strict now gates the plan.

Re-verified on the rebased stack: task studio:check passes (106 server + 248 web Vitest), and task ac-trace-strict passes with every Studio plan gated as landed (141 ACs, 0 failures).

Development stage

  • Plan / Interface
  • Implementation
  • Combined
  • Spike / Routine / Cleanup

Contract linkage

  • Work classification: Bounded
  • Classification rationale: adds the schedules product surface (/api/v1/schedules…) inside the boundary ADR 0351 fixed
  • Decision record: None — see the plan layer
  • Human waiver of spine: Yes — the directing human chose stacked (gh stack) delivery for the whole Studio series: nothing merges until every layer is reviewed, so the plan's approval-by-merge event is deferred to the end (recorded in each plan's Delivery line), and each implementation layer records the exact plan commit it built against.
  • Acceptance plan: docs/acceptance/studio-schedules.md
  • Human decisions resolved and recorded: Yes
  • Plan / Interface PR: docs(plan): Mecatl Studio schedules acceptance plan #1757
  • Approved commit baseline: d3300a6aaaec9744f7744eaf9b26f24a39d642e9 (the plan layer below; not yet merged — stacking exception above)
  • Combined/exemption rationale: N/A

Interface conformance

  • Matches the plan's contract.

Issue relationship

Relates to #1748 (parent #1736; stack #1766). Non-closing: the step completes when both of its layers merge.

Type of change

  • Behavioral/interface plan
  • Bug fix
  • New feature
  • Refactoring (no behavior change)
  • Dependency update
  • Documentation/process

Test plan

Baseline checks

  • Acceptance-plan checker
  • Linting (task lint)
  • Offline test suite (task test)
  • Offline demo (go run ./cmd/mecademo)
  • Markdown changed: docs generation/link checks (task docs)

Studio gates (this workspace is not covered by task lint / task test, which are Go-only): task studio:check (Biome, typecheck, Vitest, generated-drift), task ac-trace — all pass; see Verification above.

User-facing change

Studio gains the Scheduled page: create, edit, pause, resume, fire, and inspect schedules.

Special notes for reviewers

Part of a fourteen-layer stack; review bottom-up. Nothing merges until the whole series is reviewed.

@samuv
samuv added this pull request to stack #1766 September 22, 2026 08:01
@samuv samuv self-assigned this Sep 22, 2026
@samuv

samuv commented Sep 22, 2026

Copy link
Copy Markdown
Contributor Author

Review findings addressed: the at token and ?schedule=

Both confirmed and fixed here, alongside the contract changes from #1757 (daylight-saving-safe one-shot instants, the completed status, and the int32 bounds).

next <weekday> at <time> was rejected. The pattern required the clock immediately after the weekday, so the documented phrase only worked without at. It now accepts the optional token, with coverage for the bare form, the at form, and odd spacing and case.

?schedule=<name> did not open the editor. The effect only cleared filters, scrolled, focused, and expanded history. It now sets the matching schedule as the editor once the inventory query has succeeded, and a ref records which name has been opened so closing the editor does not reopen it on the next render.

Verification: 97 server and 240 web Vitest proofs, 8 integration tests against a spawned mecated --mock, and task ac-trace resolves every named proof for the schedules plan with none missing.

…ctions, fires

Implementation layer of docs/acceptance/studio-schedules.md, built
against plan commit d3300a6.

BFF: /api/v1/schedules list (supported flag and reason read live from
the negotiated snapshot's scheduling capability; 501 schedule_unsupported
on mutations when off), create and update mapping the product mode /
profile / cron-or-once trigger vocabulary onto the SDK spec while
carrying the daemon's unexposed spec fields over untouched, fire / pause
/ resume actions, delete, and newest-first fire history with a derived
in-flight flag; status derived from enabled / in-flight / pending-claim
state. Mutations sit behind the bootstrap's CSRF and session gates.

Web: the schedules workspace, form with cron builder and phrase parser,
and detail page; "Scheduled" joins the nav.

Answers four review findings. A one-shot instant no longer round-trips
through an offset-less datetime-local value: the form carries the
schedule's original instant and re-sends it unchanged while the displayed
wall clock is unedited, so saving an untouched schedule whose instant is
the second occurrence of an ambiguous local time can no longer move the
fire an hour; an edited wall clock CANONICALISES with a fixed rule
rather than restoring the previous instant (the earlier occurrence at a
fall-back transition, the first instant after the gap at a spring-forward
one), and the form shows the absolute time it will save. A proof states
the canonicalisation explicitly, so the preservation rule is not read as
a general round-trip. A schedule the daemon reports with no next fire after at least one
fire is completed rather than scheduled. maxFires and oneShotMaxRetries
are bounded to 0..2147483647 before any SDK call, so no value reaches a
signed 32-bit daemon field that cannot hold it. The phrase parser accepts
"next <weekday> at <time>", not only the bare form, and ?schedule=<name>
opens that schedule editor once, without reopening it after close.

77 server and 213 web Vitest proofs; the unsupported path smoke-tested
against a spawned mecated --mock (which runs with scheduling off).

Tracking: #1736

Signed-off-by: Samuele Verzi <samu@stacklok.com>
@samuv
samuv merged commit 0abf09e into main Sep 23, 2026
33 checks passed
@samuv
samuv deleted the studio/schedules branch September 23, 2026 13:15
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.

1 participant