carlos / platform Public

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

Plain git — no account needed to clone.

Download

Download this file

Plans 3b: Never-Sleep and Schedule Refusals Implementation Plan

> 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.

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.

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.

Tech Stack: Go; internal/console, internal/schedule.

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.

Own boxes (Paul, 2026-10-24)

Asked "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.

Global Constraints

  • No catalog, no change: every check returns nil when EffectiveLimits is nil.
  • 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.
  • Address-proxy routes and instance hibernation are already operator-only (refuseMemberUpstreams, apiinstances.go:1874; no member field sets Record.Hibernate); nothing to do.
  • Operators' direct-bucket carlos ship bypasses the console and is out of scope (operator-only).
  • Copy goes through copy review before it lands; strings join capCopy in plancaps_copy.go.
  • Touched packages only, with -run; CI runs the full suite; census entries in the last task.

Review Focus

  1. Plans off. Daemon ships, always-on sidecars and one-minute schedules all work exactly as today. Pinned per task.
  2. 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).
  3. 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).
  4. 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).

---

Task 1: Daemon releases

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.

  • [ ] Step 1: Failing tests: TestShipRefusesDaemonUnderAPlan (ship with {"daemon": true} capabilities: 409 "code":"cap", no release written); TestShipRefusesDaemonEvenOnReship; TestShipDaemonWithoutACatalogUnchanged; TestShipWithoutDaemonUnderAPlanUnchanged.
  • [ ] Step 2: Run go test ./internal/console/ -run 'ShipRefusesDaemon|ShipDaemonWithout|ShipWithoutDaemon' → FAIL.
  • [ ] Step 3: Implement checkDaemon (nil when plans off; otherwise &planCapError{msg: capMessage("cli.cap.daemon")}) and call it when declared != nil && declared.Daemon.
  • [ ] Step 4: Run → PASS. Step 5: Commit console: no daemon releases under a plan.

Task 2: Always-on sidecars

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.

  • [ ] Step 1: Failing tests: TestSidecarEnableRefusesHibernateFalseUnderAPlan (409 cap, manifest unchanged); TestSidecarEnableKeepsAnExistingSettingWhenOmitted (an existing hibernate: false manifest, enable without the field: allowed, unchanged); TestSidecarEnableHibernateFalseWithoutACatalogUnchanged.
  • [ ] 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.

Task 3: Schedule minimum

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.

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).

  • [ ] 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.
  • [ ] Step 2: Run go test ./internal/schedule/ -run CronMinGap and the console tests → FAIL.
  • [ ] Step 3: Implement ShortestInterval and checkSchedule (refuse when ShortestInterval < MinScheduleSeconds, message cli.cap.schedule with the minimum in minutes).
  • [ ] Step 4: Run → PASS. Step 5: Commit console: schedules meet the plan's minimum interval.

Task 4: Census and gates

  • [ ] 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.

Done when

Every 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.

Astra review (2026-10-25)

Findings in docs/superpowers/specs/CODEX-FINDINGS-PLANS-3B-CODE.md. All six were fixed:

  1. 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.
  2. 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.
  3. Sidecar create: refused on shared boxes when the app's manifest says hibernate: false (checkAlwaysOnCreate).
  4. Always-on enable on a fleet: every existing sidecar record must already be on a fleet the account owns.
  5. Fleet ownership: ownsFleet checks that the fleet exists and that Account == acct.Sqid. Lookup failures are errors.
  6. Census card: names the new indirect reads, and records ownsFleet as a hand-written decision.

Rulings taken in the fix pass:

  • ~~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.
  • 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.

Astra review, round 2 (2026-10-25)

Findings 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:

  1. 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.
  2. 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.
  3. 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.

Astra review, round 3 (2026-10-25)

Findings in docs/superpowers/specs/CODEX-FINDINGS-PLANS-3B-CODE-R3.md. All three round-2 findings are confirmed fixed. Two new findings, both fixed:

  1. 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).
  2. 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.

Astra review, round 4 (2026-10-25)

Verdict: 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.