carlos / platform Public

Clone
git clone https://amadan.net/carlos/platform

Plain git — no account needed to clone.

Download

Download this file

1# Plans 3b: Never-Sleep and Schedule Refusals Implementation Plan
2
3> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
4
5**Goal:** Under a plan, the console refuses the three things that would keep an app awake beyond its plan: shipping a release that declares `daemon`, enabling sidecars with `hibernate: false`, and declaring a schedule more frequent than the plan's minimum.
6
7**Architecture:** Three checks join `internal/console/plancaps.go`, each nil when plans are off, each a `*planCapError` with approved copy, answered 409 `"code":"cap"` by its JSON door. Cron schedules need a minimum-gap helper in `internal/schedule`.
8
9**Tech Stack:** Go; `internal/console`, `internal/schedule`.
10
11**Spec:** `docs/superpowers/specs/2026-10-20-plans-caps-packs-design.md` §5.2 (never sleeping is refused for plan-governed accounts; schedules: free minimum 15 minutes, paid 1 minute, validated where `ValidEvery` runs). Paul (2026-10-20): all apps hibernate; no plan offers always-on. Builds on Plans 1–3a.
12
13## Own boxes (Paul, 2026-10-24)
14
15Asked "Should we not just have plans that include daemons and sidecars? Or do we limit those to folks bringing their own boxes?", the answer taken (author's recommendation, matching the rootfs-apps ruling "own boxes only, always-on by default"): always-on is allowed when the app is placed on the account's own fleet (paid plans include fleets), and refused only on shared boxes. All three checks therefore pass when the app's placement is one of the account's fleets (`registry.FleetPlacement` of `appPlacement`); sidecars of a placed app inherit its fleet region. An always-on pack for shared boxes is a later pack (Plan 6), when someone asks.
16
17## Global Constraints
18
19- No catalog, no change: every check returns nil when `EffectiveLimits` is nil.
20- Only new writes are refused. A sidecar manifest or schedule written before plans were switched on keeps working; lapses and over-limit leftovers are Plan 4's.
21- Address-proxy routes and instance hibernation are already operator-only (`refuseMemberUpstreams`, `apiinstances.go:1874`; no member field sets `Record.Hibernate`); nothing to do.
22- Operators' direct-bucket `carlos ship` bypasses the console and is out of scope (operator-only).
23- Copy goes through copy review before it lands; strings join `capCopy` in `plancaps_copy.go`.
24- Touched packages only, with `-run`; CI runs the full suite; census entries in the last task.
25
26## Review Focus
27
281. **Plans off.** Daemon ships, always-on sidecars and one-minute schedules all work exactly as today. Pinned per task.
292. **A re-ship of an already-shipped daemon release** is refused under a plan too (the identical-reship short-circuit must not skip the check). Pinned in Task 1 (`TestShipRefusesDaemonEvenOnReship`).
303. **Cron that fires faster than the minimum** (`*/1 * * * *`, `0,5 * * * *`) is refused, and a cron whose shortest gap meets the minimum (`*/15 * * * *` on free) is allowed. Pinned in Task 3 (`TestCronMinGap`, `TestScheduleRefusedBelowThePlanMinimum`).
314. **Enabling sidecars without a `hibernate` field** on an existing always-on manifest is not a new always-on write and is allowed. Pinned in Task 2 (`TestSidecarEnableKeepsAnExistingSettingWhenOmitted`).
32
33---
34
35### Task 1: Daemon releases
36
37**Files:** Modify `internal/console/plancaps.go` (`checkDaemon(acct *Account) error`), `internal/console/apicli.go` (`handleAPIShip`, right after the capability block is validated at ~`:2010`, before the identical-reship short-circuit at ~`:2203`); tests in `internal/console/plancaps_doors_test.go`.
38
39- [ ] **Step 1: Failing tests:** `TestShipRefusesDaemonUnderAPlan` (ship with `{"daemon": true}` capabilities: 409 `"code":"cap"`, no release written); `TestShipRefusesDaemonEvenOnReship`; `TestShipDaemonWithoutACatalogUnchanged`; `TestShipWithoutDaemonUnderAPlanUnchanged`.
40- [ ] **Step 2:** Run `go test ./internal/console/ -run 'ShipRefusesDaemon|ShipDaemonWithout|ShipWithoutDaemon'` → FAIL.
41- [ ] **Step 3:** Implement `checkDaemon` (nil when plans off; otherwise `&planCapError{msg: capMessage("cli.cap.daemon")}`) and call it when `declared != nil && declared.Daemon`.
42- [ ] **Step 4:** Run → PASS. **Step 5:** Commit `console: no daemon releases under a plan`.
43
44### Task 2: Always-on sidecars
45
46**Files:** Modify `plancaps.go` (`checkAlwaysOn(acct *Account) error`), `internal/console/apisidecars.go` (`handleAPISidecarEnable`, after body validation, before the manifest read); tests in `plancaps_doors_test.go`.
47
48- [ ] **Step 1: Failing tests:** `TestSidecarEnableRefusesHibernateFalseUnderAPlan` (409 cap, manifest unchanged); `TestSidecarEnableKeepsAnExistingSettingWhenOmitted` (an existing `hibernate: false` manifest, enable without the field: allowed, unchanged); `TestSidecarEnableHibernateFalseWithoutACatalogUnchanged`.
49- [ ] **Step 2:** Run → FAIL. **Step 3:** Refuse only when `req.Hibernate != nil && !*req.Hibernate`. **Step 4:** Run → PASS. **Step 5:** Commit `console: no always-on sidecars under a plan`.
50
51### Task 3: Schedule minimum
52
53**Files:** Create `internal/schedule/mingap.go`, `internal/schedule/mingap_test.go`; modify `plancaps.go` (`checkSchedule(acct *Account, sc schedule.Schedule) error`), `internal/console/apischedule.go` (`handleAPIScheduleSet`, right after `sc.Validate()`); tests in `plancaps_doors_test.go`.
54
55**Interfaces:** `func (s Schedule) ShortestInterval() (time.Duration, error)` in `internal/schedule`: `Every`'s duration, or for cron the smallest gap between consecutive `Next` firings over a 7-day window from a fixed start (covers hourly, daily and day-of-week patterns).
56
57- [ ] **Step 1: Failing tests:** `TestCronMinGap` (`*/1 * * * *` → 1m; `*/15 * * * *` → 15m; `0,5 * * * *` → 5m; `0 3 * * *` → 24h; `every: 10m` → 10m); `TestScheduleRefusedBelowThePlanMinimum` (free: `every: 5m` and `*/1 * * * *` → 409 cap, nothing stored; `every: 15m` and `*/15 * * * *` → stored); `TestScheduleMinimumOnAPaidPlan` (personal: `every: 1m` stored); `TestScheduleWithoutACatalogUnchanged`.
58- [ ] **Step 2:** Run `go test ./internal/schedule/ -run CronMinGap` and the console tests → FAIL.
59- [ ] **Step 3:** Implement `ShortestInterval` and `checkSchedule` (refuse when `ShortestInterval < MinScheduleSeconds`, message `cli.cap.schedule` with the minimum in minutes).
60- [ ] **Step 4:** Run → PASS. **Step 5:** Commit `console: schedules meet the plan's minimum interval`.
61
62### Task 4: Census and gates
63
64- [ ] Re-pin the #280 root-read overlay (`~/.cache/tmp/claude-1001/census-gen/gen.sh PROPOSED-plans-3b-never-sleep <basis>`), check the record-write census, run the full claim invariant once, merge main, push, CI green, ask for the ruling on #280.
65
66## Done when
67
68Every task's tests pass, Review Focus 1–4 are pinned, CI is green after merging current main, and Astra's whole-branch review has no open Critical or Important finding.
69
70## Astra review (2026-10-25)
71
72Findings in `docs/superpowers/specs/CODEX-FINDINGS-PLANS-3B-CODE.md`. All six were fixed:
73
741. Cron gap: `ShortestInterval` is now a conservative bound over the minute and hour fields. It ignores the day fields, so seasonal and leap-day crons can't slip through.
752. Promotion: CLI promote, browser promote, new canaries and pending promote all refuse a daemon release on shared boxes under a plan (`checkDaemonRelease`). The pending check runs at execution, against the plan as it stands then.
763. Sidecar create: refused on shared boxes when the app's manifest says `hibernate: false` (`checkAlwaysOnCreate`).
774. Always-on enable on a fleet: every existing sidecar record must already be on a fleet the account owns.
785. Fleet ownership: `ownsFleet` checks that the fleet exists and that `Account == acct.Sqid`. Lookup failures are errors.
796. Census card: names the new indirect reads, and records `ownsFleet` as a hand-written decision.
80
81Rulings taken in the fix pass:
82
83- ~~**Rollback and fast track are not checked.**~~ Rollback reversed in round 2 (below). Fast track and retry still stand. They only re-serve a version that is already on one of this app's channels. Every way onto a channel is now checked: ship, promote, canary and pending promote. So a daemon version can only be there if it got there before plans were on, or while the app was on its fleet. Those leftovers belong to Plan 4, the same as an always-on app that moves from its fleet back to shared boxes. Pending rollbacks and retries are exempt for the same reason. The box's converge step still refuses a daemon whose hibernation is on.
84- **No new copy.** The promote doors reuse the approved `cli.cap.daemon` string ("…or ship it without daemon" still applies). The browser doors show it through `Browser()`, which falls back to the CLI text the same way `cli.cap.sidecars` does. A failed plan check shows `planCheckFailedCopy`.
85
86## Astra review, round 2 (2026-10-25)
87
88Findings in `docs/superpowers/specs/CODEX-FINDINGS-PLANS-3B-CODE-R2.md`. Round 1's findings 1, 3, 5 and 6 are confirmed fixed. Three new Important findings, all fixed:
89
901. **Sidecar listing.** `checkAlwaysOn` now uses `provision.ListSidecarRecordsStrict`. It has no cap, and it fails on any record it can't read, so a skipped worker can no longer earn the own-fleet exemption. A record that disappears between the list and the read is skipped. Covered by `TestAlwaysOnRefusedPastAnUnreadableSidecarRecord`.
912. **Rollback is checked.** A rollback target need not be on any channel now, so CLI rollback, browser rollback and pending rollback all apply the daemon rule to it. The direct doors check the target they read. They also pass `rollbackDaemonGuard` as Rollback's authorize hook. That hook re-reads the pointer after Rollback's own read and before its conditional write, so the version actually written is the one checked. Fast track and retry stay unchecked: they keep the version the channel already serves. Covered by `TestRollbackToADaemonReleaseRefusedUnderAPlan`, `TestRollbackDaemonGuardReadsThePointerAtWriteTime` and `TestPendingRollbackToADaemonReleaseRefusedUnderAPlan`.
923. **Exact cron gap.** `ShortestInterval` now walks a whole 400-year Gregorian cycle, which is a whole number of weeks, to find the closest pair of matching days. Weekly and monthly lines get their real gaps (`0,55 0,23 * * 1` is 55 minutes), and seasonal and leap-day bursts are still caught. Cron runs in UTC, so there is no daylight-saving shift.
93
94## Astra review, round 3 (2026-10-25)
95
96Findings in `docs/superpowers/specs/CODEX-FINDINGS-PLANS-3B-CODE-R3.md`. All three round-2 findings are confirmed fixed. Two new findings, both fixed:
97
981. **Important: with plans off, rollback read a manifest.** `checkDaemonVersion` now checks the plan and the placement before it reads anything. Pending execution goes through it too. So with plans off, neither rollback nor a pending change reads a manifest it didn't read before. Covered by `TestRollbackWithoutACatalogNeedsNoManifest` (mutation-checked).
992. **Minor: guard read failures looked like deploy refusals.** A guard that can't read now returns `*planGuardError`. Both rollback doors answer it as a failed plan check: a 502 with `planCheckFailedCopy` on the API and the same text as a browser flash. Covered by `TestRollbackGuardReadFailureIsAPlanCheckFailure`.
100
101## Astra review, round 4 (2026-10-25)
102
103Verdict: accept (`docs/superpowers/specs/CODEX-FINDINGS-PLANS-3B-CODE-R4.md`). Both round-3 findings are confirmed fixed, there are no new findings, and with plans off nothing differs from main.
104