| 1 | | Earlier finding | Disposition at `9a11decc` | |
| 2 | |---|---| |
| 3 | | R1.1 — Signature accepted as production-off | **Fixed:** no signed expiry object remains. | |
| 4 | | R1.2 — Existing data passes creation checks | **Fixed at creation:** fresh row and empty-prefix checks. A separate account-move bypass remains below. | |
| 5 | | R1.3 — Keep/TTL races expiry | **Not fixed:** the specified conditional update still matches after keep; finding 1. | |
| 6 | | R1.4 — Purge misses the current deadline | **Declined-with-reason acceptable:** §11 explicitly defers the pre-existing box race. The unchanged [reconcile.go:228](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/temporary-sites/internal/domains/reconcile.go:228) remains vulnerable. | |
| 7 | | R1.5 — Expiry replay after keep | **Fixed:** no bucket expiry authority remains. | |
| 8 | | R1.6 — Restore bypasses cap | **Not fixed for failed console restores:** finding 4. The direct-mode exception is acceptable. | |
| 9 | | R1.7 — Restrictions enforced only in CLI | **Fixed for the reported paths:** §4 requires server enforcement. | |
| 10 | | R1.8 — Browser/headless restore misses renewal | **Fixed in the specified shared restore and catch-up behavior.** | |
| 11 | | R1.9 — Interrupted purge loses short deadline | **Fixed:** the deadline survives in the row. | |
| 12 | | R1.10 — Migration disables expiry | **Declined-with-reason acceptable:** §4 accurately describes direct migration and accepts its outcome. | |
| 13 | | R2.1 — Rename brings permanent data under expiry | **Fixed:** temporary-app rename is refused. | |
| 14 | | R2.2 — Early purge skips email teardown | **Fixed for the reported paths:** provisioning is refused and early purge checks for email resources. | |
| 15 | | R2.3 — Failed restore cancels purge deadline | **Fixed:** the deadline remains until the row restore succeeds. | |
| 16 | | R2.4 — Missing marker mistaken for restore | **Fixed for the reported completed-purge case:** an empty prefix finalizes deletion. | |
| 17 | | R2.5 — Materialization omits deadline | **Fixed:** §5 explicitly reconstructs the marker-bound deadline. | |
| 18 | | R2.6 — Instance provisioning bypasses store prohibition | **Fixed:** both manifest enablement and implicit provisioning are covered. | |
| 19 | | R2.7 — Old console silently creates permanent app | **Fixed in §3:** distinct endpoint and response verification. §7 still contains the obsolete `/new` instruction and should agree with §3. | |
| 20 | | R2.8 — Expiry bypasses control-plane protection | **Not fixed:** post-creation flag setting remains available, and the backstop covers purge only; finding 5. | |
| 21 | |
| 22 | 1. **P1 — §5: the expiry update accepts a kept, permanent app.** |
| 23 | The pass selects an expired candidate, then keep acquires the mutex and sets `expires_at = 0`. When the pass subsequently acquires it, the specified `UPDATE … WHERE expires_at <= now AND deleted_at = 0 AND expiring = 0` still matches: zero satisfies `<= now`. Step 2 then calls `TrashApp`, which writes the marker without checking lifecycle state ([trash.go:43](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/temporary-sites/internal/release/trash.go:43)), and step 3 assigns the three-day deadline. The mutex prevents simultaneous transitions but does not invalidate an earlier candidate listing. An in-memory SQLite check confirms the specified update affects one row after keep. **Fix:** require `expires_at > 0` in the conditional update and revalidate eligibility under the mutex. Test keep between candidate selection and lock acquisition. |
| 24 | |
| 25 | 2. **P1 — §§3–4: moving the console record can put a permanent site under expiry.** |
| 26 | Create temporary `A/site` without deploying it. Suppose account B already has permanent `site` releases and a non-platform route, but no console app row. That is an eligible account-repair target: the review groups accessible same-named routes whose account has no app row ([accountreview.go:65](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/temporary-sites/internal/console/accountreview.go:65)). The browser move calls `MoveApp` ([appsettings.go:1350](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/temporary-sites/internal/console/appsettings.go:1350)), whose update changes only `account_id`, preserving the proposed expiry fields ([store.go:1705](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/temporary-sites/internal/console/store.go:1705)). B’s existing permanent releases now fall under automatic expiry and three-day purge. This is separate from the accepted bucket-copy migration outcome. **Fix:** require keep before moving a temporary row, enforce it in the shared move operation, and serialize the check with lifecycle transitions. |
| 27 | |
| 28 | 3. **P2 — §§5–6: recovered expiry leaves `expiring = 1` across restore.** |
| 29 | Crash after writing the expiry tombstone but before completing step 3. Startup materialization can mark the row trashed ([materialize.go:367](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/temporary-sites/internal/console/materialize.go:367)); the proposed extension sets the purge fields but never clears `expiring`. Restore likewise clears only `deleted_at`, `purge_after` and `purge_for`. The existing row helper provides no additional transition handling ([store.go:1469](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/temporary-sites/internal/console/store.go:1469)). The restored app therefore retains an outstanding instruction to retry expiry from step 2, allowing it to be trashed again despite its renewed deadline; keep and TTL also remain refused. **Fix:** settle `expiring` when materialization completes the trash transition, clear it atomically during restore, and ensure retry eligibility cannot survive a completed restore. |
| 30 | |
| 31 | 4. **P2 — §§3, 5–6: failed console restore frees a capacity slot before recovery.** |
| 32 | With 19 live temporary apps, restore a twentieth. Tombstone deletion succeeds but the final row update fails—a failure explicitly handled by the existing API ([apicli.go:1338](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/temporary-sites/internal/console/apicli.go:1338)). The restored app serves again, but its row remains excluded from the cap. Create another temporary app, bringing the counted total to 20. Later, §5’s missing-marker catch-up restores the first row **without a cap check**, producing 21 live apps. No direct-mode operation or simultaneous create is required. **Fix:** reserve capacity durably before removing the marker, count incomplete restores against the cap, and retain that reservation through recovery. Merely adding a late refusal cannot undo the already-serving restore. |
| 33 | |
| 34 | 5. **P2 — §§3–5: an existing temporary app can still become the control plane.** |
| 35 | Removing `control_plane` from the create request does not close the existing owner-facing setter: `handleAppControlPlaneSet` calls `SetAppControlPlane(..., true)` on an existing app ([controlplane.go:288](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/temporary-sites/internal/console/controlplane.go:288)). §4 does not restrict that path. §5 checks the flag only before early purge, so expiry still trashes an app that ordinary deletion explicitly protects ([apicli.go:1209](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/temporary-sites/internal/console/apicli.go:1209)). **Fix:** require keep before setting the control-plane flag and recheck the flag before automatic trash as well as purge, under the lifecycle mutex. |
| 36 | |
| 37 | 6. **P3 — §7: the proposed listing assumes an API shape and command that do not exist.** |
| 38 | `carlos apps` currently has no `list` subcommand ([apps.go:129](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/temporary-sites/apps.go:129)). The existing app enumeration returns account entries containing app-name strings ([apicli.go:548](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/temporary-sites/internal/console/apicli.go:548)), and clients decode them as `[]string` ([account_choose.go:27](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/temporary-sites/account_choose.go:27)). Its store query excludes trashed apps ([store.go:1362](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/temporary-sites/internal/console/store.go:1362)), so it cannot supply the advertised purge countdown. **Fix:** specify the new list command and an additive metadata endpoint or response field, including trash visibility and service-principal filtering; preserve the existing string-array contract. |
| 39 | |
| 40 | Verdict: revise. |