| Earlier finding | Disposition at `abd90b9f` |
|---|---|
| R1.1 — Expiry signature accepted as production-off | **Fixed:** no signed expiry object. |
| R1.2 — Existing data passes creation checks | **Fixed:** fresh row, empty-prefix checks, and rename/move restrictions. |
| R1.3 — Keep/TTL races expiry | **Fixed:** settle, lock, and recheck `expires_at > 0`. |
| R1.4 — Purge misses current deadline | **Declined-with-reason acceptable:** §11 explicitly defers the pre-existing box race; console early purge checks the marker timestamp. |
| R1.5 — Expiry replay after keep | **Fixed:** no bucket expiry authority. |
| R1.6 — Restore bypasses cap | **Fixed:** console restores reserve capacity; direct-mode exception explicitly accepted. |
| R1.7 — Restrictions enforced only in CLI | **Fixed for the reported paths:** server enforcement specified. |
| R1.8 — Browser/headless restore misses renewal | **Fixed:** shared restore helper and catch-up renewal. |
| R1.9 — Interrupted purge loses short deadline | **Fixed:** deadline remains in the row. |
| R1.10 — Migration disables expiry | **Declined-with-reason acceptable:** §4 describes direct migration and accepts its outcome. |
| R2.1 — Rename brings permanent data under expiry | **Fixed:** shared rename operation refuses temporary apps. |
| R2.2 — Early purge skips email teardown | **Fixed:** provisioning refusals and purge backstop. |
| R2.3 — Failed restore cancels purge deadline | **Fixed:** deadline survives until successful row restore. |
| R2.4 — Missing marker mistaken for restore | **Fixed for the reported completed-purge case:** empty prefix finalizes deletion. A separate safeguard regression remains below. |
| R2.5 — Materialization omits deadline | **Fixed:** marker-bound deadline reconstruction specified. |
| R2.6 — Instance provisioning bypasses store prohibition | **Fixed:** manifest and provisioning paths covered. |
| R2.7 — Old console silently creates permanent app | **Fixed:** distinct endpoint and response verification; §7 now agrees. |
| R2.8 — Expiry bypasses control-plane protection | **Fixed:** creation/setter restrictions and pre-trash check. |
| R3.1 — Expiry condition matches kept app | **Fixed:** positive-expiry check under mutex. |
| R3.2 — Account move brings permanent data under expiry | **Fixed:** shared `MoveApp` operation refuses temporary apps. |
| R3.3 — `expiring` survives restore | **Fixed:** flag removed; settle-first recovery specified. |
| R3.4 — Failed restore frees capacity | **Fixed:** §6 counts durable `restoring` reservations. |
| R3.5 — Temporary app can become control plane | **Fixed:** setter restriction and expiry backstop. |
| R3.6 — Listing command/API shape nonexistent | **Fixed:** new command and additive endpoint specified. |

1. **P1 — §§2, 5–6: absence-based recovery drops the existing empty-bucket safeguard.**  
   Both recovery branches prescribe `PurgeAppRow` when the marker and app objects are absent. But existing materialization deliberately refuses that conclusion when the entire `apps/` listing is empty: a valid but misconfigured deployment bucket otherwise looks exactly like completed deletion ([materialize.go:296](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/temporary-sites/internal/console/materialize.go:296), [materialize.go:388](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/temporary-sites/internal/console/materialize.go:388)). `PurgeAppRow` actually deletes the claim row ([store.go:1491](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/temporary-sites/internal/console/store.go:1491)).

   Start the console against an incorrect empty bucket with temporary apps awaiting purge. Startup materialization preserves their rows, but the proposed early-purge recovery then deletes them. Correcting the bucket leaves the real objects without their console claims or three-day purge authority.

   **Fix:** carry the existing empty-bucket safeguard into `settleApp` and every absent-marker finalization path. Require a successful deployment-wide listing and preserve rows when it is empty. Test startup materialization followed by the expiry pass against an empty replacement bucket.

2. **P2 — §§5, 7: the existing Trash page promises recovery for 27 days after automatic destruction.**  
   The spec changes the CLI deletion output and adds a new listing, but leaves the existing browser deadline calculation unspecified. `TrashedApps` always calculates `PurgesAt` using the 30-day `TrashCooloff` ([view.go:492](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/temporary-sites/internal/console/view.go:492)). The template displays that date and explicitly promises 30 days ([trash.html:10](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/temporary-sites/internal/console/templates/trash.html:10), [trash.html:21](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/temporary-sites/internal/console/templates/trash.html:21)). The trash API likewise returns the 30-day date as `restorable_until` ([apicli.go:1295](/home/paulca/amadan.net/carlos/platform/.claude/worktrees/temporary-sites/internal/console/apicli.go:1295)).

   A temporary app can therefore be destroyed on day three while its recovery screen advertises day thirty.

   **Fix:** specify one effective purge-deadline calculation using the persisted `purge_after` for temporary trash, with the existing 30-day calculation for permanent trash. Use it in the Trash view, deletion confirmation, API response, and CLI output; update the unconditional recovery promises. Test both app classes together.

Verdict: revise.