rootfs apps: Astra R1 fix round (all 14 accepted), R11 in plans, index updates
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
docs/superpowers/plans/2026-10-24-rootfs-apps-0-index.md+32 −0docs/superpowers/plans/2026-10-24-rootfs-apps-1-ship.md+97 −50docs/superpowers/plans/2026-10-24-rootfs-apps-2-box-isolation.md+660 −46docs/superpowers/plans/2026-10-24-rootfs-apps-3-lifecycle.md+203 −57docs/superpowers/plans/2026-10-24-rootfs-apps-4-console-and-docs.md+41 −13docs/superpowers/plans/2026-10-24-rootfs-apps-5-egress-allow.md+169 −59docs/superpowers/plans/2026-10-24-rootfs-apps-6-interactive.md+90 −25
CI cancelled — superseded by a newer push — run details
CI log
=== gate === .amadan/test-gate.sh amadan: job cancelled — a newer push superseded it
Intent
<task-notification> <task-id>a6ee10db289c97455</task-id> <tool-use-id>toolu_01JVDgMtWzymX3QE2q4MaZpi</tool-use-id> <output-file>/home/paulca/.cache/tmp/claude-1001/-home-paulca-amadan-net-carlos-platform/622e3b2d-1bba-40c2-bd40-b65d02622c4c/tasks/a6ee10db289c97455.output</output-file> <status>completed</status> <summary>Agent "Fix round 1: plan 3" finished</summary> <note>A task-notification fires each time this agent stops with no live background children of its own. The user can send it another message and resume it, so the same task-id may notify more than once.</note> <result>I've edited the plan for findings 1, 2 and 8. I checked each one against the code at 3dff2214 first, and all three hold, so none was rejected. The only file touched is `docs/superpowers/plans/2026-10-24-rootfs-apps-3-lifecycle.md`. Nothing is committed, no test was run (this round only changes the plan), and my one scratch file in `$TMPDIR` is deleted. The new "Review dispositions (Astra R1)" section sits after the plan header, with "Cross-plan changes" under it. ## Dispositions - **1. Accepted (Plan 3 side).** The bug is real. Polling a command reads the binary cmd unit (`internal/instancectl/cmd.go:129`), and stopping one stops and waits on the binary unit too (`cmdunit.go:256`, `backend_exec.go:1656`). But `CmdRootfs` starts the rootfs cmd unit, so a running migration reads as gone and its stop proves the wrong unit. - **Changed:** new assumption A8 uses Plan 2's runtime-aware cmd unit. Task 10 (release) and Task 12 (`carlos run`) return it for every rootfs command, including after a failed start. - **Tests:** the scripted `CmdStatus` fake is gone. A new test harness, `rootfsCtlHarness`, runs the real `instancectl.Server` handler and client with a running rootfs cmd unit and no binary one. New tests: `TestReleasePollsTheRootfsCmdUnit`, `TestReleaseStopProvesTheRootfsCmdUnitGone` and `TestRootfsRunPollsAndStopsTheRootfsUnit`. - **2. Accepted.** The bug is real. - On a cold wake the entry is `actWaking`, which the sweep's lease check skips (`activator.go:4563`), and a lost lease does not cancel the wake. - During a swap the sweep only notices at the safety deadline. It then allows up to 2 minutes to stop, but the lease leaves only a 5-second margin. - **Changed:** `runRelease` now takes the lease it runs under (the wake's lease, or the incumbent's replica during a swap) and the calling site's existing ownership gate. - The gate runs before the start and again just before root is asked to start the command. - A supervisor stops the release and proves it gone when the lease reports loss, or 2 minutes before its safety deadline. Normally that deadline is 10 to 15 minutes ahead, because the lease is renewed every 5. - Losing the lease is treated as losing ownership, never as the build failing: a swap is abandoned, and a cold wake starts nothing after it. If the stop can't be proven, the lease is left to expire on its own rather than released. - In the swap, the release now runs after the step 3a ownership checks rather than before them, so it passes the same gate as the new process. - **Tests:** lease loss during a cold-wake migration, during a swap's migration, stalled renewals, an unproven stop, and two race tests. - **New string:** RFS-3-C29. - **8. Accepted.** The bug is real. litestream does nothing for a database file that doesn't exist (`db.go:966` in v0.5.14). So an app that never opens `CARLOS_DB` has no file and no replica, its next wake reads as a fresh boot, and the "served but no data" refusal blocks every restart. - **Changed:** new deviation D7 and Task 7. On a rootfs first boot that has never served, the agent writes an empty SQLite file at `CARLOS_DB` under the first-wake lease, before `release` and `web`. It writes through `instancefs`, the same way seeds are placed, and the SQLite driver never opens a tenant-writable path. The file is synced to the replica once replication starts. - **The refusal is unchanged:** it runs before the placeholder, and no placeholder is ever written for a row that has served. - **Tests:** `TestRootfsAppWithoutSQLiteRestarts` covers restart on the same box, restart from the replica, and the refusal still firing when both the file and the replica are gone. It also has negative tests. ## Cross-plan changes (what Plan 3 needs from Plan 2) Plan 2's file was being edited while I worked. Its fixer already names these, and my plan uses them: - `RootfsControl.CmdStatusRootfs(ctx, host, invocation string) (instancectl.CmdStatus, error)`: the status read with `runtime=rootfs`. Its handler reads `RootfsUnitName(RoleCmd, host)` and takes no `image`. - `cmdUnit.Poll` and `cmdUnit.StopAndSettle` choosing the unit from `rt.Runtime` themselves. Stopping goes through `StopRootfs(ctx, host, RoleCmd, 0, "")`, then `waitRoleCgroupEmpty(ctx, rt, RoleCmd)` checks the right unit via `unitNameFor`. Plan 3 then builds `&cmdUnit{b, rt, inv, out, errs}` the same way for both runtimes. - Nothing from Plan 2 is needed for findings 2 or 8. Plan 3 adds `SafeUntil` and `Lost` to `execLease` itself, and `*lease.Replicator` already has both. ## New copy row (needs Paul's review) - **RFS-3-C29** (`internal/rolloutwords/words.go`, `EventReleaseLeaseLost`): "the release command of build %s was stopped because this box no longer holds the instance. it runs again on the next attempt" Task 1 now expects 29 rows. Because C29 was added after the series copy review, Task 1 Step 2 sends it to Paul on its own.</result> <usage><subagent_tokens>230591</subagent_tokens><tool_uses>65</tool_uses><duration_ms>710744</duration_ms></usage> </task-notification>
<task-notification> <task-id>aa94dd2623027cbc5</task-id> <tool-use-id>toolu_01C8beB62M6LrFHv7YTQ1V6n</tool-use-id> <output-file>/home/paulca/.cache/tmp/claude-1001/-home-paulca-amadan-net-carlos-platform/622e3b2d-1bba-40c2-bd40-b65d02622c4c/tasks/aa94dd2623027cbc5.output</output-file> <status>completed</status> <summary>Agent "Fix round 1: plans 1, 4, 6" finished</summary> <note>A task-notification fires each time this agent stops with no live background children of its own. The user can send it another message and resume it, so the same task-id may notify more than once.</note> <result>I accepted all five findings after checking each against the code, and took in R11 in Plans 1 and 4, with a test-only note in Plan 6. Each of the three plans now opens with a "Review dispositions (Astra R1)" section, a list of string IDs and "Cross-plan changes". Nothing was committed, nothing was run, and I touched only the three plan files. My scratch scripts in `$TMPDIR` are deleted. **Plan 1** (`docs/superpowers/plans/2026-10-24-rootfs-apps-1-ship.md`) - **Finding 9, accepted.** Nothing serialises two first ships today. `shipSem` lets two in at once, the direct-mode ship (`cli.go:2365`) never reaches the console, and `WithAppMutation` is a plain call when host claims are off. A console lock can't cover direct mode, so I used the store's create-only put, the pattern the trash marker already uses (`claimRawTrashApp`): - The app's first ship writes a marker, `apps/<acct>/<app>/kind.json` (`AppKindMarkerKey`, `AppKindMarker`). - Both publication paths, `claimRawShip` and `claimRawShipSpooled`, re-check the kind and set up the marker in `establishShipKind` before their first artifact write. The ship that loses the race reads the winner's marker, is refused, and writes no bytes. - The door maps that refusal to 409, not the retry-flavoured 502. - The key is already covered by the existing app-scoped `apps` classifier family and is removed with the app's prefix on purge, so no new family is needed. - The concurrent mixed-kind tests are `TestFirstShipsOfTwoKindsRaceOneWinner` (release layer, looped 50 times, run under `-race` in the gate) and `TestAPIShipFirstShipsOfTwoKindsAtOnce` (through the real door). The "known race, accepted" bullet is gone. - One limit: if a first ship writes its marker and then fails before its manifest, the app's kind is still fixed. The new refusals call it the app's "first ship", never a release, and shipping that kind again works. - **R11.** `Manifest.JIT` is valid only on rootfs releases, set by a `--jit` flag, carried in the door's meta, and backed by a `--check` warning (`rootfsimage.JITProgram` spots Node, Bun or Deno). I chose a flag rather than a processes-file key because `--hibernate` is the same kind of release-wide switch, and the processes file is keyed by process names, so a `jit` key would read as a valid worker called `jit`. `jit` is per release, so it is not part of the kind check. - **Strings:** added RFS-1-C80 to C85; changed RFS-1-C13, C29, C34. **Plan 4** (`docs/superpowers/plans/2026-10-24-rootfs-apps-4-console-and-docs.md`) - **Finding 13, accepted.** Today `fleets.go:217` answers "that name is taken" to any retry, so a failed owner-record write can't be repaired. Now, when the fleet row already exists: - **Caller's own fleet:** the door writes the record first. An exact retry then gets 200, and a request with a different bucket still gets the existing 409. - **Another account's fleet:** the existing 409, with no read or write of the record. - **Why no periodic reconciler:** it would be a new worker with its own claim census, and the retry is what the door's own 500 message asks for. - **Test:** `TestFleetCreateRetryRepairsAFailedOwnerRecord` fails one write, then recovers on the retry without a console restart. Two more tests cover the other-bucket and other-account cases. - **R11.** The contract page's "No JIT" section becomes "JIT compilers", with a short Node note. The Rails YJIT row now offers `--jit`. The sample app stays without JIT. - **Strings:** added RFS-4-C136 to C138; changed RFS-4-C54, C55, C81, C112. Nothing new for finding 13. **Plan 6** (`docs/superpowers/plans/2026-10-24-rootfs-apps-6-interactive.md`) - **Finding 10, accepted.** The session is reserved in the hub before the grant and the admission, and a reserved session counts as live. A failed admission drops the reservation and writes no audit row; registration follows a successful one. New tests: `TestAdmissionHoldsItsReservationUntilRegistered`, which pauses between record publication and registration, and `TestAFailedAdmissionReleasesItsReservation`. - **Finding 11, accepted.** Instead of fragmenting, the run relay gets its own bounded frame size, `RunMaxFrame` (one kind byte, a 12-byte header and one 262,144-byte chunk: 262,157 bytes), set at both ends through `DialFrame`/`SendFrame`. Sessions keep the 64 KiB `MaxFrame`. Tests carry a full 262,144-byte chunk and refuse one byte more. - **Finding 12, accepted.** The plan never defined the "hello" it relied on, and took the incarnation where `RunPick` and `launchable` need the agent's boot ID. Now: - The relay's first frame is `RunHello{Schema, Run, Box, Boot}`, and the console's pickup uses that box and boot. The incarnation stays out of it and is checked as today. - The console sends back `RunPicked` (picked or not-picked). The box starts nothing until it gets "picked" and reads the record back naming its own box and boot. - If the box drops after pickup, the console closes the run as lost. - **R11.** No new code. The test `TestRootfsTTYStartFollowsTheReleasesJIT` checks that interactive sessions get Plan 2's JIT handling. - **Strings:** none added or changed. **Cross-plan changes** - **Plan 2:** read `release.Manifest.JIT` (JSON `jit`). Put the JIT drop-in and the effective `MemoryDenyWriteExecute` check in the shared rootfs start path, so they also cover the tty role. Plan 4's docs promise this for every process of a JIT release, including `carlos run` and interactive sessions. - **Plan 4 (from Plan 1):** `KindConflictError` and `HibernateConflictError` gain a `FirstShip` field. Any door must use `Error()`, never a copy of the sentence. `AppKind` is unchanged. - **Index:** add the bucket key `release.AppKindMarkerKey(acct, app)` (`apps/<acct>/<app>/kind.json`) to the shared contract. Note `interactive.RunMaxFrame` and `RunHello` as Plan 6's names. - **Copy deck** (`docs/superpowers/plans/2026-10-24-rootfs-apps-copy.md`, which I did not edit): it needs the added and changed rows above, RFS-1-C80 to C85 and RFS-4-C136 to C138. The index file changed on disk while I worked; I left it as it is.</result> <usage><subagent_tokens>277394</subagent_tokens><tool_uses>74</tool_uses><duration_ms>920819</duration_ms></usage> </task-notification>
docs/superpowers/plans/2026-10-24-rootfs-apps-0-index.mdLoading diff…
docs/superpowers/plans/2026-10-24-rootfs-apps-1-ship.mdLoading diff…
docs/superpowers/plans/2026-10-24-rootfs-apps-2-box-isolation.mdLoading diff…
docs/superpowers/plans/2026-10-24-rootfs-apps-3-lifecycle.mdLoading diff…
docs/superpowers/plans/2026-10-24-rootfs-apps-4-console-and-docs.mdLoading diff…
docs/superpowers/plans/2026-10-24-rootfs-apps-5-egress-allow.mdLoading diff…
docs/superpowers/plans/2026-10-24-rootfs-apps-6-interactive.mdLoading diff…