| Round 1 finding | Disposition |
|---|---|
| 1. Expiry signature accepted as production-off | **Fixed:** §2 removes the signed expiry record. |
| 2. Existing data passes temporary creation checks | **Fixed at the create door:** §3 requires an empty prefix and a fresh row. A separate rename bypass is described below. |
| 3. Keep/TTL races expiry | **Fixed:** §5 serializes these console transitions and records expiry in progress. |
| 4. Purge re-read misses the destruction deadline | **Not fixed across all consumers:** the unchanged box pass checks an old tombstone’s age, then calls unguarded `PurgeApp` ([reconcile.go:228](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/temporary-sites/internal/domains/reconcile.go:228)). Restore→keep→trash can still substitute a fresh permanent tombstone. Update or gate that automatic purge path too; the console mutex cannot protect it. |
| 5. Deleted expiry record remains replayable | **Fixed:** no bucket expiry authority remains. |
| 6. Restore bypasses the cap | **Fixed for console restores:** §6 applies §3’s admission check. The explicit direct-mode exception is acceptable. |
| 7. Static restrictions exist only in the CLI | **Fixed for the reported kind/host paths:** §4 requires server enforcement. |
| 8. Browser/headless restore retains stale expiry | **Fixed in the specified behavior:** shared console restore and explicit catch-up renewal. Catch-up’s interpretation of missing markers has a separate flaw below. |
| 9. Interrupted purge loses the short deadline | **Fixed for an interrupted payload sweep:** the deadline survives in the row. Completion recovery remains incomplete below. |
| 10. Migration disables expiry | **Declined-with-reason acceptable:** §4 explicitly accepts direct operator migration. However, its API-mode refusal describes a mode that does not currently exist: migration opens the registry and bucket directly ([accounts.go:330](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/temporary-sites/accounts.go:330), [accounts.go:346](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/temporary-sites/accounts.go:346)). |

1. **P1 — §§3, 5: renaming a temporary row can put existing permanent data under expiry.**  
   A fresh claim can succeed despite failure to write its default pipeline, leaving an empty prefix ([server.go:3571](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/temporary-sites/internal/console/server.go:3571)). Rename checks the **source** prefix for emptiness ([appsettings.go:785](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/temporary-sites/internal/console/appsettings.go:785)); its destination checks do not reject shipped-but-unclaimed releases. `RenameAppIn` changes only the name, preserving the proposed expiry columns ([store.go:1787](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/temporary-sites/internal/console/store.go:1787)). Rename that empty temporary app onto a name with permanent releases but no row or routes, and those releases become eligible for destruction. **Fix:** require keep before renaming a temporary app, or apply the full fresh-prefix reservation checks to the destination and serialize rename with lifecycle transitions.

2. **P1 — §§4–5, 8: early purge can erase the records needed to revoke email infrastructure.**  
   Contrary to §8’s premise, email provisioning paths exist: domain creation calls `mail.EnsureDomain` ([apimail.go:1016](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/temporary-sites/internal/console/apimail.go:1016)), and standalone credential creation calls `mail.CreateStandalone` ([apimail.go:1391](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/temporary-sites/internal/console/apimail.go:1391)). Neither is prohibited by §4. Ordinary console purge runs `mail.Teardown` before sweeping the prefix specifically to avoid orphaned credentials and unreleasable domain claims ([ownership.go:410](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/temporary-sites/internal/console/ownership.go:410)). The proposed early purge skips that teardown and deletes the records ([trash.go:257](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/temporary-sites/internal/release/trash.go:257)). **Fix:** require keep before creating email resources, and refuse automatic purge if any email resources or claims nevertheless exist.

3. **P2 — §6: a failed restore permanently cancels the three-day purge.**  
   Step 3 clears `purge_after` and `purge_for` before step 4 deletes the marker. That deletion can fail; `RestoreApp` directly returns the storage error ([trash.go:98](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/temporary-sites/internal/release/trash.go:98)). After failure or a crash between those steps, the app remains trashed with no early-purge deadline. Neither §5 query selects it, so cleanup falls back to 30 days. **Fix:** retain the purge deadline until restore completes, using a durable restoring state to make crash recovery unambiguous. Clear the deadline atomically with the successful row restore.

4. **P2 — §5: `ErrNotTrashed` does not prove that somebody restored the app.**  
   Purge itself deletes the marker last ([trash.go:265](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/temporary-sites/internal/release/trash.go:265)). If the sweep succeeds but `PurgeAppRow` fails, the next pass receives `ErrNotTrashed` and follows the proposed restore path: it renews an empty app, retains its name, and counts it against the live cap. An independent box purge produces the same ambiguity. Existing materialization distinguishes absent-marker-with-objects from absent-marker-without-objects ([materialize.go:376](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/temporary-sites/internal/console/materialize.go:376)). **Fix:** record purge progress durably and finalize completed purges; do not infer restore from marker absence alone.

5. **P2 — §§2, 5–6: materializing a trash marker can omit the early-purge deadline.**  
   Manual trash writes the bucket marker before updating the row; row failure explicitly relies on materialization for recovery ([apicli.go:1279](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/temporary-sites/internal/console/apicli.go:1279)). Startup materialization calls `TrashAppRow` ([materialize.go:367](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/temporary-sites/internal/console/materialize.go:367)), which updates only `deleted_at` ([store.go:1460](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/temporary-sites/internal/console/store.go:1460)). The spec extends the two manual handlers but does not specify equivalent trash recovery here. A temporary app recovered this way has `deleted_at > 0` and `purge_after = 0`, excluding it from both new passes. **Fix:** include materialization in the lifecycle protocol and recover the original marker-bound deadline for temporary trash transitions.

6. **P2 — §4: instance creation bypasses the object-store prohibition.**  
   A caller can enable `private_storage` through the instance-manifest endpoint ([apiinstances.go:1751](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/temporary-sites/internal/console/apiinstances.go:1751)), then create a static instance at the permitted platform host. Instance creation provisions its store directly ([apiinstances.go:995](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/temporary-sites/internal/console/apiinstances.go:995)), without entering the object-store create handler. All listed temporary-app checks pass, but automatic purge subsequently refuses the provisioned store ([trash.go:248](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/temporary-sites/internal/release/trash.go:248)). **Fix:** prohibit private-storage enablement and implicit provisioning while temporary; enforce the restriction in the shared provisioning path.

7. **P2 — §§3, 7: an older console silently creates a permanent app for `--temporary`.**  
   The existing create request has no `temporary` field ([apicli.go:359](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/temporary-sites/internal/console/apicli.go:359)), and its JSON decoder accepts unknown fields ([authcli.go:940](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/temporary-sites/internal/console/authcli.go:940)). Sending the proposed request to that console succeeds as an ordinary permanent creation, after which the specified CLI flow ships and promotes. **Fix:** use a distinct temporary-create endpoint, or require capability confirmation before creation and verify the returned expiry before shipping. Missing support must produce an explicit refusal.

8. **P2 — §§3–5: expiry bypasses the control-plane deletion protection.**  
   The existing create endpoint accepts `control_plane: true` from an owner ([apicli.go:373](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/temporary-sites/internal/console/apicli.go:373)); the proposed temporary field is not declared incompatible with it. Ordinary trash and purge refuse control-plane apps ([apicli.go:1209](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/temporary-sites/internal/console/apicli.go:1209), [ownership.go:335](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/temporary-sites/internal/console/ownership.go:335)). The new loop calls release helpers directly, bypassing that database-backed protection. **Fix:** reject temporary/control-plane combinations and recheck the control-plane flag before automatic trash or purge.

Verdict: revise.