# Seeded Instances, Plan 8: Sweeps, Move Refusals and Legacy Route Assignments 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:** Close the series: the console's deadline sweep that abandons stale reservations and imports, finishes every rolled-back seed and deletes only snapshots no live record needs; moves refused until a seed is placed or an import committed, in both move doors and in the move worker; and `carlos system legacy-routes assign`, the operator's mapping that lets an import take over a route filed under the operator account.

**Architecture:** `StartSeedsPass` runs every five minutes from `cmd/console/main.go` through the console's worker registry. It reads every record strictly (refusing to act on a partial set, and treating a host whose record body is unreadable as referenced), abandons stale `reserving` and `awaiting-upload`/`uploaded` seeds by CAS to `aborted`, calls Plan 6's `finishSeed` for every `rolled-back` seed, and walks `seeds/` with last-modified times. The move refusals live in `provision` (the transitions, under the record CAS) and in a console helper that both move doors call before their transition, so each door answers the deck's sentence; the worker skips a blocked record before its first effect. The assignment is a console door that writes `control/legacy-route-owners.json` by CAS, refusing to re-assign a name while an import under its current entry is between `ready` and `committed`.

**Tech Stack:** Go 1.25, `release.ModLister`, `provision.ListAllRecords`, `seeds.ReadLegacyOwners`.

**Spec:** `docs/superpowers/specs/2026-10-08-seeded-instances-design.md` rev 7: §3.1 (the sweep's three cases, codex R2-5, R3-2, R4-1), §5.6 (the sweep as the console finish's fallback, codex R6-1), §5.8 (codex R4-2), §6.4 (codex 9), §7 (`legacy-route-assign` audit row, the move errors). Index: `2026-10-08-seeded-instances-0-index.md`. Copy deck rows SD-N100 to SD-N110 and SD-N116.

## Global Constraints

- Every user-facing string (CLI output, errors, console messages, docs prose) is copy-reviewed by Paul before it is written into code; implementation tasks use only strings from the copy deck. Copy voice: no em dashes; full stops and new sentences instead.
- CI cross-compiles darwin and windows: linux-only code needs `//go:build linux` plus a `!linux` stub.
- Local test commands: targeted tests (`go test ./internal/<pkg>/ -run TestX`), not the full suite in a loop. Inside the sandbox the default Go build cache may be read-only; if so use `GOCACHE=$TMPDIR/gocache` re-exported per command. Socket tests need a short TMPDIR (`TMPDIR=/tmp/g`). The local gate may need `GOFLAGS=-buildvcs=false`.
- Heavy loops run under `nice`; no -race full suite until the end of a plan.
- Production use needs nested boxes; the claim-armed path is refused here and left to #250 (spec §3.4).
- Commits end with `Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>`.
- No new file in this series imports `internal/moves`. New sentences go in new files (`internal/console/seedsweep.go`, `seedmove.go`, `apilegacyroutes.go`, `system_legacy_routes.go`) or in `internal/provision/seed.go`; files listed in `movesScanFiles` (`apiinstancemove.go`, `movespass.go`, `movesphase1.go`, `provision.go`, `authcli.go`, `cmd/console/main.go`) gain only calls and routes.
- `TestClaimAuthoritySourceCensus` inventories console and `cmd/console` calls in its `selected` set; add each new site to `internal/console/testdata/claim-authority-sites.txt`.
- When a task wires a deck row, append its id to `seededWiredIDs` in `approved_seeded_copy_test.go` in the same commit.
- Write each deck sentence as exactly one string literal or one format string holding the row's text, with `%s` or `%d` at each `<placeholder>` and nothing else: no prefix, no trailing `\n`, no leading spaces. The gate's matcher (`movesRowSpoken`, `approved_moves_copy_test.go:811`) anchors both ends. Print with `fmt.Fprintln(w, sentence)`.
- The sweep deletes a snapshot only on positive knowledge: a complete record set, and, for "no record names it", a last-modified time more than an hour old (spec §3.1).

## Review Focus

1. A record whose body cannot be decoded must count as naming whatever snapshots exist under its host: the sweep must never delete a snapshot because a record was unreadable for a pass. Test in Task 2 (`TestSweepKeepsSnapshotsOfAnUnreadableRecord`).
2. A `--from` record in `reserving` for less than an hour, with its snapshot already uploaded, must keep both: the door is between upload and publish. Test in Task 2 (`TestSweepLeavesALiveReservation`).
3. A stale import is abandoned by CAS first, and if the CAS loses (the app committed or uploaded that second), nothing is deleted. Test in Task 2 (`TestSweepAbandonsOnlyByCAS`).
4. A move request on a `--from` instance whose seed is `ready` is refused in both the legacy door and the Phase 1 door with the deck's sentence, and the record's `move` stays absent. Test in Task 3 (`TestMoveDoorsRefuseAnUnplacedSeed`).
5. Re-assigning a legacy name while an import under the old mapping is `swapped` is refused, and the file is unchanged. Test in Task 5 (`TestAssignRefusedWhileAnImportIsInFlight`).

---

## File structure

| path | responsibility |
|---|---|
| `internal/provision/reconcile.go` | `ListAllRecordsStrict` beside `ListAllRecords` |
| `internal/console/seedsweep.go` | `StartSeedsPass`, `seedsPass` |
| `internal/console/claim_workers.go`, `cmd/console/main.go` | the worker registered and started |
| `internal/provision/seed.go` | `SeedMoveSentence`; the move transitions' refusal |
| `internal/provision/provision.go` | `MoveRequest` and `MoveStartTransition` call `seedMoveRefusal` |
| `internal/console/seedmove.go` | `refuseSeedMove`, called by both move doors |
| `internal/console/apiinstancemove.go` | two calls |
| `internal/host/movespass.go` | the worker's skip |
| `internal/console/apilegacyroutes.go` | `POST /api/cli/system/legacy-routes` |
| `system_legacy_routes.go`, `system.go` | `carlos system legacy-routes assign` |
| `docs/site/cli.md` | SD-N110 |

---

### Task 1: A strict record listing

**Files:**
- Modify: `internal/provision/reconcile.go:1732` (`ListAllRecords`)
- Test: `internal/provision/reconcile_test.go`

**Interfaces:**
- Produces: `ListAllRecordsStrict(ctx context.Context, s release.Store) (recs []Record, unreadable map[string]bool, err error)`: `ListAllRecords`' set, plus the hosts whose record key parsed but whose body did not decode or disagreed with its path. `ListAllRecords` becomes a wrapper that drops the second value.

- [ ] **Step 1: Write the failing test**

```go
func TestListAllRecordsStrictNamesUnreadableHosts(t *testing.T) {
	s, sqid := setup(t)
	if err := CreateRecord(ctx, s, acmeRecord(sqid)); err != nil {
		t.Fatal(err)
	}
	bad := acmeRecord(sqid)
	bad.Host, bad.Slug = "bad.go.tito.io", "bad"
	if err := CreateRecord(ctx, s, bad); err != nil {
		t.Fatal(err)
	}
	putReplace(t, s, RecordKey(sqid, "tito", "bad.go.tito.io"), []byte("{not json"))
	recs, unreadable, err := ListAllRecordsStrict(ctx, s)
	if err != nil || len(recs) != 1 || recs[0].Host != "acme.go.tito.io" || !unreadable["bad.go.tito.io"] {
		t.Fatalf("recs %v unreadable %v err %v", recs, unreadable, err)
	}
}
```

- [ ] **Step 2: Run it to verify it fails**

Run: `go test ./internal/provision/ -run TestListAllRecordsStrict -count=1`
Expected: FAIL to compile.

- [ ] **Step 3: Split the function**

Rename `ListAllRecords`' body to `ListAllRecordsStrict`, add `unreadable := map[string]bool{}` and set `unreadable[host] = true` in each of its two `continue` branches (malformed, disagrees with its path), and return `recs, unreadable, nil`. Then:

```go
// ListAllRecords is ListAllRecordsStrict without the unreadable hosts.
func ListAllRecords(ctx context.Context, s release.Store) ([]Record, error) {
	recs, _, err := ListAllRecordsStrict(ctx, s)
	return recs, err
}
```

- [ ] **Step 4: Run the package**

Run: `go test ./internal/provision/ -count=1`
Expected: PASS.

- [ ] **Step 5: Commit**

```bash
git add internal/provision/reconcile.go internal/provision/reconcile_test.go
git commit -m "provision: ListAllRecordsStrict names the hosts it could not read (Plan 8 Task 1)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
```

### Task 2: The seeds sweep

**Files:**
- Create: `internal/console/seedsweep.go`, `internal/console/seedsweep_test.go`
- Modify: `internal/console/claim_workers.go` (`"StartSeedsPass": s.StartSeedsPass`), `cmd/console/main.go` (`go srv.RunClaimWorker(context.Background(), "StartSeedsPass")` beside `StartExpiryPass`)

**Interfaces:**
- Consumes: `provision.ListAllRecordsStrict`, `provision.SetRecordSeed`, `provision.SeedAbort`, `s.finishSeed(ctx, sqid, app, host, id)` (Plan 6), `release.ModLister`, `seeds.ParseSnapshotKey`, `seeds.Prefix`.
- Produces: `(s *Server) StartSeedsPass(ctx context.Context)`; `(s *Server) seedsPass(ctx context.Context, now time.Time)`; `seedsPassInterval = 5 * time.Minute`, `seedReserveTTL = time.Hour`, `seedImportTTL = 7 * 24 * time.Hour`, `seedOrphanAge = time.Hour`.

One pass (spec §3.1, §5.6):

1. Records, strictly; an error ends the pass (nothing is deleted on a partial set).
2. For each seeded record: `reserving` and older than an hour, or an import `awaiting-upload`/`uploaded` older than seven days, is CASed to `aborted` (`bumpSerial` after; the box finishes its part). `rolled-back` is finished (`finishSeed`).
3. Snapshots under `seeds/` with their last-modified times (`ListMod`; a store without it skips the walk). For each `seeds/<host>/<id>/snapshot.db`:
   - the host is unreadable this pass: keep;
   - the host's record names this `id`: keep, unless its seed is `placed` or `committed` (the host's own replica exists, so the box's delete was lost), or the record was just abandoned by this pass (step 2) and its CAS landed;
   - the host's record names another `id` and is `placed` or `committed`: delete;
   - otherwise (no record, or a record naming another `id` in any other state): delete when the object is more than an hour old.
   A delete names the exact key, so it never reaches another seed's snapshot.

The "placed or committed with this `id`" case is not one of spec §3.1's three; it is the same safety argument (placed means the host's own replica exists, §3.4 step 5) applied to a snapshot whose delete the box lost, which otherwise would never be collected.

- [ ] **Step 1: Write the failing tests**

```go
package console

import (
	"context"
	"errors"
	"os"
	"strings"
	"testing"
	"time"

	"github.com/carlosframework/platform/internal/provision"
	"github.com/carlosframework/platform/internal/release"
	"github.com/carlosframework/platform/internal/seeds"
)

var sweepDataRoot string // the DirStore's root, to age its files

func sweepFixture(t *testing.T) (*consoleFixture, release.Store) {
	t.Helper()
	fx := newConsoleFixture(t)
	sweepDataRoot = t.TempDir()
	data := release.NewDirStore(sweepDataRoot)
	fx.srv.cfg.SeedData = data
	return fx, data
}

func seededRec(t *testing.T, fx *consoleFixture, host, kind, state string, at time.Time) string {
	t.Helper()
	id, _ := seeds.NewID()
	rec := provision.Record{Schema: 1, Host: host, Slug: strings.Split(host, ".")[0], App: "titogo", Account: "bab",
		Channel: "stable", CreatedAt: at, Seed: &provision.SeedSource{ID: id, Kind: kind, State: state, By: "t", At: at}}
	if err := provision.CreateRecord(context.Background(), fx.rel, rec); err != nil {
		t.Fatal(err)
	}
	return id
}

func putSnapshotAged(t *testing.T, data release.Store, host, id string, age time.Duration) string {
	t.Helper()
	key := seeds.SnapshotKey(host, id)
	if _, err := data.Put(context.Background(), key, []byte("db"), ""); err != nil {
		t.Fatal(err)
	}
	// DirStore keeps a key at root/key and its ListMod reports the file's
	// mtime (store.go:338, :509): age the file.
	path := filepath.Join(sweepDataRoot, filepath.FromSlash(key))
	when := time.Now().Add(-age)
	if err := os.Chtimes(path, when, when); err != nil {
		t.Fatal(err)
	}
	return key
}

func exists(t *testing.T, data release.Store, key string) bool {
	t.Helper()
	_, _, err := data.Get(context.Background(), key)
	return !errors.Is(err, os.ErrNotExist)
}

func TestSweepLeavesALiveReservation(t *testing.T) {
	fx, data := sweepFixture(t)
	now := time.Now()
	id := seededRec(t, fx, "canary.go.tito.io", seeds.KindReplica, seeds.StateReserving, now.Add(-10*time.Minute))
	key := putSnapshotAged(t, data, "canary.go.tito.io", id, 10*time.Minute)
	fx.srv.seedsPass(context.Background(), now)
	rec, _ := provision.GetRecord(context.Background(), fx.rel, "bab", "titogo", "canary.go.tito.io")
	if rec.Seed.State != seeds.StateReserving || !exists(t, data, key) {
		t.Fatalf("a live reservation was touched: %s, snapshot kept %v", rec.Seed.State, exists(t, data, key))
	}
	// An hour later it is abandoned by CAS.
	fx.srv.seedsPass(context.Background(), now.Add(2*time.Hour))
	rec, _ = provision.GetRecord(context.Background(), fx.rel, "bab", "titogo", "canary.go.tito.io")
	if rec.Seed.State != seeds.StateAborted {
		t.Fatalf("a stale reservation = %s, want aborted", rec.Seed.State)
	}
}

func TestSweepDeletesOnlyUnreferencedOrSupersededSnapshots(t *testing.T) {
	fx, data := sweepFixture(t)
	now := time.Now()
	live := seededRec(t, fx, "a.go.tito.io", seeds.KindReplica, seeds.StateReady, now)
	liveKey := putSnapshotAged(t, data, "a.go.tito.io", live, 3*time.Hour)
	stale, _ := seeds.NewID()
	staleKey := putSnapshotAged(t, data, "a.go.tito.io", stale, 3*time.Hour) // another id, record not placed: an orphan
	fresh, _ := seeds.NewID()
	freshKey := putSnapshotAged(t, data, "nobody.go.tito.io", fresh, 10*time.Minute) // no record yet, too young
	seededRec(t, fx, "b.go.tito.io", seeds.KindReplica, seeds.StatePlaced, now)
	old, _ := seeds.NewID()
	supersededKey := putSnapshotAged(t, data, "b.go.tito.io", old, 5*time.Minute) // another id, record placed
	fx.srv.seedsPass(context.Background(), now)
	if !exists(t, data, liveKey) {
		t.Error("a snapshot a live record needs was deleted")
	}
	if exists(t, data, staleKey) {
		t.Error("an orphan over an hour old survived")
	}
	if !exists(t, data, freshKey) {
		t.Error("a young unreferenced snapshot was deleted (its door may be between record and upload)")
	}
	if exists(t, data, supersededKey) {
		t.Error("a snapshot superseded by a placed seed survived")
	}
}

func TestSweepKeepsSnapshotsOfAnUnreadableRecord(t *testing.T) {
	fx, data := sweepFixture(t)
	id := seededRec(t, fx, "a.go.tito.io", seeds.KindReplica, seeds.StateReady, time.Now())
	key := putSnapshotAged(t, data, "a.go.tito.io", id, 3*time.Hour)
	_, etag, _ := fx.rel.Get(context.Background(), provision.RecordKey("bab", "titogo", "a.go.tito.io"))
	fx.rel.Put(context.Background(), provision.RecordKey("bab", "titogo", "a.go.tito.io"), []byte("{not json"), etag)
	fx.srv.seedsPass(context.Background(), time.Now())
	if !exists(t, data, key) {
		t.Fatal("an unreadable record's snapshot was deleted")
	}
}

func TestSweepAbandonsOnlyByCAS(t *testing.T) {
	fx, data := sweepFixture(t)
	old := time.Now().Add(-8 * 24 * time.Hour)
	id := seededRec(t, fx, "imp.go.tito.io", seeds.KindImport, seeds.StateUploaded, old)
	key := putSnapshotAged(t, data, "imp.go.tito.io", id, 8*24*time.Hour)
	// The app marks it stopped in the same instant: the CAS the sweep makes
	// must lose and nothing be deleted.
	seedsPassBeforeAbort = func() {
		provision.SetRecordSeed(context.Background(), fx.rel, "bab", "titogo", "imp.go.tito.io", provision.SeedStopped(id))
	}
	defer func() { seedsPassBeforeAbort = nil }()
	fx.srv.seedsPass(context.Background(), time.Now())
	rec, _ := provision.GetRecord(context.Background(), fx.rel, "bab", "titogo", "imp.go.tito.io")
	if rec.Seed.State != seeds.StateReady || !exists(t, data, key) {
		t.Fatalf("state %s, snapshot kept %v; want the app's stop to win and nothing deleted", rec.Seed.State, exists(t, data, key))
	}
}

func TestSweepFinishesARolledBackSeed(t *testing.T) {
	fx, _ := sweepFixture(t)
	seededRec(t, fx, "gone.go.tito.io", seeds.KindImport, seeds.StateRolledBack, time.Now())
	fx.srv.seedsPass(context.Background(), time.Now())
	if _, err := provision.GetRecord(context.Background(), fx.rel, "bab", "titogo", "gone.go.tito.io"); !errors.Is(err, os.ErrNotExist) {
		t.Fatalf("a rolled-back record after the sweep: %v", err)
	}
}
```

`seedsPassBeforeAbort` is a test seam in `seedsweep.go`: a `func()` called just before each abandonment CAS, nil in production. Because `SetRecordSeed` retries once on a lost etag and re-applies `SeedAbort`, which accepts `ready` for an import, the test as written would see the abort land on the retry; make the sweep's abandonment use a guarded mutation that re-checks staleness on the fresh record (`rec.Seed.State` still `awaiting-upload`/`uploaded`/`reserving` and still older than its limit), so the retry refuses once the app has moved it on.

- [ ] **Step 2: Run the tests to verify they fail**

Run: `go test ./internal/console/ -run 'TestSweep' -count=1`
Expected: FAIL to compile.

- [ ] **Step 3: Write `seedsweep.go`**

```go
package console

import (
	"context"
	"errors"
	"log"
	"time"

	"github.com/carlosframework/platform/internal/provision"
	"github.com/carlosframework/platform/internal/release"
	"github.com/carlosframework/platform/internal/seeds"
)

const (
	seedsPassInterval = 5 * time.Minute
	seedReserveTTL    = time.Hour
	seedImportTTL     = 7 * 24 * time.Hour
	seedOrphanAge     = time.Hour
)

// seedsPassBeforeAbort is the tests' pause before an abandonment CAS.
var seedsPassBeforeAbort func()

var errNotStale = errors.New("console: the seed moved on")

// StartSeedsPass runs the seeds sweep now and every five minutes
// (seeded-instances spec §3.1, §5.6), StartExpiryPass's shape.
func (s *Server) StartSeedsPass(ctx context.Context) {
	if s.cfg.Releases == nil || s.cfg.SeedData == nil {
		return
	}
	t := time.NewTicker(seedsPassInterval)
	defer t.Stop()
	for {
		s.seedsPass(ctx, time.Now())
		select {
		case <-ctx.Done():
			return
		case <-t.C:
		}
	}
}

func stale(seed *provision.SeedSource, now time.Time) bool {
	switch {
	case seed.State == seeds.StateReserving:
		return now.Sub(seed.At) > seedReserveTTL
	case seed.Import() && (seed.State == seeds.StateAwaitingUpload || seed.State == seeds.StateUploaded):
		return now.Sub(seed.At) > seedImportTTL
	}
	return false
}

func (s *Server) seedsPass(ctx context.Context, now time.Time) {
	recs, unreadable, err := provision.ListAllRecordsStrict(ctx, s.cfg.Releases)
	if err != nil {
		log.Printf("console: seeds pass: %v (nothing swept this pass)", err)
		return
	}
	byHost := map[string]provision.Record{}
	abandoned := map[string]string{} // host -> seed id aborted by this pass
	for _, rec := range recs {
		byHost[rec.Host] = rec
		seed := rec.Seed
		if seed == nil {
			continue
		}
		switch {
		case stale(seed, now):
			if seedsPassBeforeAbort != nil {
				seedsPassBeforeAbort()
			}
			_, err := provision.SetRecordSeed(ctx, s.cfg.Releases, rec.Account, rec.App, rec.Host, func(r *provision.Record) error {
				if r.Seed == nil || r.Seed.ID != seed.ID || !stale(r.Seed, now) {
					return errNotStale
				}
				return provision.SeedAbort(seed.ID)(r)
			})
			if err != nil {
				if !errors.Is(err, errNotStale) && !errors.Is(err, provision.ErrSeedConflict) {
					log.Printf("console: seeds pass: abandon %s: %v", rec.Host, err)
				}
				continue
			}
			abandoned[rec.Host] = seed.ID
			s.bumpSerial(ctx)
			log.Printf("console: seeds pass: abandoned seed %s of %s (%s since %s)", seed.ID, rec.Host, seed.State, seed.At.Format(time.RFC3339))
		case seed.State == seeds.StateRolledBack:
			if err := s.finishSeed(ctx, rec.Account, rec.App, rec.Host, seed.ID); err != nil {
				log.Printf("console: seeds pass: finish %s: %v", rec.Host, err)
			}
		}
	}
	ml, ok := s.cfg.SeedData.(release.ModLister)
	if !ok {
		return
	}
	objs, err := ml.ListMod(ctx, seeds.Prefix)
	if err != nil {
		log.Printf("console: seeds pass: list snapshots: %v", err)
		return
	}
	for _, o := range objs {
		host, id, ok := seeds.ParseSnapshotKey(o.Key)
		if !ok || unreadable[host] {
			continue
		}
		rec, has := byHost[host]
		del := false
		switch {
		case has && rec.Seed != nil && rec.Seed.ID == id:
			placed := rec.Seed.State == seeds.StatePlaced || rec.Seed.State == seeds.StateCommitted
			del = placed || abandoned[host] == id
		case has && rec.Seed != nil && (rec.Seed.State == seeds.StatePlaced || rec.Seed.State == seeds.StateCommitted):
			del = true
		default:
			del = now.Sub(o.Mod) > seedOrphanAge
		}
		if !del {
			continue
		}
		if err := s.cfg.SeedData.Delete(ctx, o.Key); err != nil {
			log.Printf("console: seeds pass: delete %s: %v", o.Key, err)
			continue
		}
		log.Printf("console: seeds pass: deleted %s", o.Key)
	}
}
```

Register the worker in `claim_workers.go`'s map (`"StartSeedsPass": s.StartSeedsPass,`) and start it in `cmd/console/main.go` beside `StartExpiryPass`:

```go
	// The seeds sweep (seeded-instances spec §3.1, §5.6): stale
	// reservations and imports abandoned, rolled-back seeds finished,
	// unreferenced snapshots deleted. Same placement rule as every pass
	// above.
	go srv.RunClaimWorker(context.Background(), "StartSeedsPass")
```

If a test pins the worker names (`grep -rn '"StartExpiryPass"' internal/console/*_test.go`), add `StartSeedsPass` there.

- [ ] **Step 4: Run the tests to verify they pass**

Run: `go test ./internal/console/ -run 'TestSweep|TestClaimWorker|TestClaimAuthoritySourceCensus' -count=1`
Expected: PASS.

- [ ] **Step 5: Commit**

```bash
git add internal/console/seedsweep.go internal/console/seedsweep_test.go internal/console/claim_workers.go cmd/console/main.go internal/console/testdata/claim-authority-sites.txt
git commit -m "console: the seeds sweep (Plan 8 Task 2)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
```

### Task 3: Moves wait for placement, in both doors

**Files:**
- Modify: `internal/provision/seed.go` (`SeedMoveSentence`, `seedMoveRefusal`), `internal/provision/provision.go:1146` (`MoveRequest`), `:1177` (`MoveStartTransition`)
- Create: `internal/console/seedmove.go`
- Modify: `internal/console/apiinstancemove.go` (`legacyHandleAPIInstanceMoveRequest` `:260`, after `moveGate`; `handleAPIInstanceMoveRequest` `:396`, after `moveOperatorGate`)
- Test: `internal/provision/seed_test.go`, `internal/console/seedmove_test.go`

**Interfaces:**
- Produces: `provision.SeedMoveSentence(block string) string` (SD-N100 for `SeedMoveUnplaced`, SD-N101 for `SeedMoveUncommitted`, `""` otherwise); `seedMoveRefusal(rec *Record) error` (an `ErrMoveConflict` carrying the sentence, or nil); `(s *Server) refuseSeedMove(w http.ResponseWriter, rec *provision.Record) bool`.

The refusal sits in two places (spec §5.8): inside the two move transitions, under the record CAS; and in a console helper both doors call after their gate and before their transition, so each door answers the deck's sentence (the Phase 1 door's `phase1Conflict` maps unknown provision refusals to one fixed sentence, and the legacy door prints the wrapped error). The record has already been read by both gates.

- [ ] **Step 1: Write the failing tests**

In `internal/provision/seed_test.go`:

```go
func TestMoveTransitionsRefuseAnUnplacedSeed(t *testing.T) {
	ctx := context.Background()
	for _, c := range []struct {
		kind, state, want string
	}{
		{seeds.KindReplica, seeds.StateReady, "wake the instance once before moving it"},
		{seeds.KindImport, seeds.StatePlaced, "commit or abort the import first"},
	} {
		s := release.NewDirStore(t.TempDir())
		rec := seedTestRecord(t, s, testSeed(t, c.kind, c.state))
		_, err := SetRecordMove(ctx, s, rec.Account, rec.App, rec.Host, MoveRequest("uk", time.Now()))
		if !errors.Is(err, ErrMoveConflict) || !strings.Contains(err.Error(), c.want) {
			t.Fatalf("%s %s: err = %v", c.kind, c.state, err)
		}
		_, err = SetRecordMove(ctx, s, rec.Account, rec.App, rec.Host, MoveStartTransition(MoveStart{ID: "mv_aaaaaaaaaaaaaaaaaaaaaaaaaa", Incarnation: "i", From: "", To: "uk", Cut: "auto", By: "op", Now: time.Now()}))
		if !errors.Is(err, ErrMoveConflict) || !strings.Contains(err.Error(), c.want) {
			t.Fatalf("%s %s start: err = %v", c.kind, c.state, err)
		}
	}
	s := release.NewDirStore(t.TempDir())
	rec := seedTestRecord(t, s, testSeed(t, seeds.KindReplica, seeds.StatePlaced))
	if _, err := SetRecordMove(ctx, s, rec.Account, rec.App, rec.Host, MoveRequest("uk", time.Now())); err != nil {
		t.Fatalf("a placed seed: %v", err)
	}
}
```

`internal/console/seedmove_test.go` (the legacy door is the one a default console runs; the Phase 1 door needs `MovesEnabled` and an operator, which `apiinstancemove_test.go` sets up with a helper: read it and use the same helper for the second half):

```go
package console

import (
	"context"
	"net/http"
	"strings"
	"testing"
	"time"

	"github.com/carlosframework/platform/internal/provision"
	"github.com/carlosframework/platform/internal/seeds"
)

func TestMoveDoorsRefuseAnUnplacedSeed(t *testing.T) {
	fx, token := instanceFixture(t, "code-seed-move")
	m := titoManifest()
	m.Pool = ""
	enableInstances(t, fx, "titogo", m)
	id, _ := seeds.NewID()
	rec := provision.Record{Schema: 1, Host: "canary.go.tito.io", Slug: "canary", App: "titogo", Account: "bab",
		Region: "ie", Channel: "stable", CreatedAt: time.Now().UTC(),
		Seed: &provision.SeedSource{ID: id, Kind: seeds.KindReplica, State: seeds.StateReady, By: "t", At: time.Now().UTC()}}
	if err := provision.CreateRecord(context.Background(), fx.rel, rec); err != nil {
		t.Fatal(err)
	}
	code, _, raw := rawReq(t, http.MethodPost, instancesURL(fx, "bab", "titogo")+"/canary.go.tito.io/move", token, `{"region":"uk"}`)
	if code != http.StatusConflict || !strings.Contains(raw, "wake the instance once before moving it") {
		t.Fatalf("legacy door = %d %s", code, raw)
	}
	got, _ := provision.GetRecord(context.Background(), fx.rel, "bab", "titogo", "canary.go.tito.io")
	if got.Move != nil {
		t.Fatalf("a refused move left a journal: %+v", got.Move)
	}
}
```

Add the Phase 1 half with `apiinstancemove_test.go`'s operator fixture (it sets `fx.srv.cfg.MovesEnabled = true` and an operator token); the assertion is the same 409 and sentence.

- [ ] **Step 2: Run the tests to verify they fail**

Run: `go test ./internal/provision/ -run TestMoveTransitionsRefuseAnUnplacedSeed -count=1; go test ./internal/console/ -run TestMoveDoorsRefuseAnUnplacedSeed -count=1`
Expected: FAIL: the moves are accepted.

- [ ] **Step 3: Write the refusal**

Append to `internal/provision/seed.go`:

```go
// SeedMoveSentence is a move refusal's sentence (spec §5.8).
func SeedMoveSentence(block string) string {
	switch block {
	case SeedMoveUnplaced:
		return "wake the instance once before moving it"
	case SeedMoveUncommitted:
		return "commit or abort the import first"
	}
	return ""
}

// seedMoveRefusal is the move transitions' seed rule, under the record CAS.
func seedMoveRefusal(rec *Record) error {
	if b := rec.Seed.MoveBlock(); b != "" {
		return fmt.Errorf("%w: %s", ErrMoveConflict, SeedMoveSentence(b))
	}
	return nil
}
```

In `MoveRequest`'s and `MoveStartTransition`'s closures, first statement: `if err := seedMoveRefusal(rec); err != nil { return err }`.

`internal/console/seedmove.go`:

```go
package console

import (
	"net/http"

	"github.com/carlosframework/platform/internal/provision"
)

// refuseSeedMove answers a move of a seed that is not placed (--from) or
// not committed (import) with the deck's sentence (spec §5.8). The
// transitions refuse it again under the record CAS.
func (s *Server) refuseSeedMove(w http.ResponseWriter, rec *provision.Record) bool {
	if b := rec.Seed.MoveBlock(); b != "" {
		jsonError(w, http.StatusConflict, provision.SeedMoveSentence(b))
		return true
	}
	return false
}
```

In `apiinstancemove.go`: after `moveGate` in `legacyHandleAPIInstanceMoveRequest`, `if s.refuseSeedMove(w, rec) { return }`; after `moveOperatorGate` in `handleAPIInstanceMoveRequest`, the same. Both gates return the record they read as a `*provision.Record` (`moveGate` `:44`, `moveOperatorGate` `:357`).

- [ ] **Step 4: Run the tests to verify they pass**

Run: `go test ./internal/provision/ -run 'TestMove|TestSeed' -count=1 && go test ./internal/console/ -run 'TestMove|TestAPIInstanceMove' -count=1 && go test . -run TestMoves -count=1`
Expected: PASS, the moves copy gate included (the doors gained one call each).

- [ ] **Step 5: Wire SD-N100, SD-N101 and commit**

```bash
git add internal/provision/seed.go internal/provision/seed_test.go internal/provision/provision.go internal/console/seedmove.go internal/console/seedmove_test.go internal/console/apiinstancemove.go approved_seeded_copy_test.go
git commit -m "moves wait for a seed's placement or an import's commit, in both doors (Plan 8 Task 3)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
```

### Task 4: The move worker refuses before any effect

**Files:**
- Modify: `internal/host/movespass.go:596` (`movesPass`'s record loop)
- Test: `internal/host/movespass_test.go`

**Interfaces:**
- Consumes: `(*provision.SeedSource).MoveBlock`, `provision.SeedMoveSentence`, `w.notice(key, format string, args ...any)`.

A record that slipped past an old console (a move journaled while its seed is unplaced or its import uncommitted) is skipped while its move is `preparing`, before the first mirror (legacy) or staging write (Phase 1), with one rate-limited log line. A move already past `preparing` has had its effects and is not this rule's to stop.

- [ ] **Step 1: Write the failing test**

```go
func TestMovesPassSkipsAnUnplacedSeed(t *testing.T) {
	h := newMoveHarness(t)
	host := "a.go.example.com"
	id, _ := seeds.NewID()
	rec := provision.Record{Schema: 1, Host: host, Slug: "s", App: "app", Account: "bab", Region: "ie", Channel: "stable",
		CreatedAt: time.Now().UTC(),
		Move:      &provision.MoveState{To: "uk", State: provision.MovePreparing, RequestedAt: time.Now().UTC()},
		Seed:      &provision.SeedSource{ID: id, Kind: seeds.KindReplica, State: seeds.StateReady, By: "t", At: time.Now().UTC()}}
	if err := provision.CreateRecord(context.Background(), h.s, rec); err != nil {
		t.Fatal(err)
	}
	h.f.put("data-ie", "instances/"+host+"/generations/g/wal/000", "e1")
	if err := movesPass(context.Background(), h.s, h.worker()); err != nil {
		t.Fatal(err)
	}
	if staged, _ := h.f.List(context.Background(), "data-uk", "moves/"+host+"/"); len(staged) != 0 {
		t.Fatalf("the worker mirrored an unplaced seed: %v", staged)
	}
	if got := h.read(t, host); got.Move.State != provision.MovePreparing {
		t.Fatalf("state = %s", got.Move.State)
	}
}
```

(add the `seeds` import to `movespass_test.go`).

- [ ] **Step 2: Run it to verify it fails**

Run: `go test ./internal/host/ -run TestMovesPassSkipsAnUnplacedSeed -count=1`
Expected: FAIL: the round mirrors.

- [ ] **Step 3: Add the skip**

In `movesPass`'s `for _, rec := range recs {` loop, first statement:

```go
		// A seed moves only once placed, an import only once committed
		// (seeded-instances spec §5.8): a record that slipped past an old
		// console is refused here, before the first copy.
		if rec.Move != nil && rec.Move.State == provision.MovePreparing {
			if b := rec.Seed.MoveBlock(); b != "" {
				w.notice(rec.Host+"#seed", "moves: %s: %s", rec.Host, provision.SeedMoveSentence(b))
				continue
			}
		}
```

- [ ] **Step 4: Run the tests to verify they pass**

Run: `go test ./internal/host/ -run 'TestMovesPass' -count=1 && go test . -run TestMoves -count=1`
Expected: PASS.

- [ ] **Step 5: Commit**

```bash
git add internal/host/movespass.go internal/host/movespass_test.go
git commit -m "moves worker: an unplaced seed waits before any copy (Plan 8 Task 4)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
```

### Task 5: `carlos system legacy-routes assign`

**Files:**
- Create: `internal/console/apilegacyroutes.go`, `internal/console/apilegacyroutes_test.go`, `system_legacy_routes.go`, `system_legacy_routes_test.go`
- Modify: `internal/console/authcli.go` (route beside the seeds-floor door), `system.go` (`systemVerb`, `runSystem`, `systemUsage`), `docs/site/cli.md`

**Interfaces:**
- Consumes: `s.apiSystemRoller`, `s.auditSystem`, `release.ValidApp`, `s.store.AccountBySqid(sqid) (*Account, error)`, `s.store.HasAppIn(accountID int64, app string) (bool, error)`, `provision.ListRecords`, `seeds.ReadLegacyOwners`, `seeds.LegacyOwnersKey`.
- Produces: `POST /api/cli/system/legacy-routes` `{"name": "titogo", "to": "bae/titogo"}` answering 200 `{"name":…, "account":…, "app":…}`; `runSystemLegacyRoutesVerb(rest []string)`.

The door (spec §6.4): operator-only (`apiSystemRoller`, `CARLOS_SYSTEM_OPERATORS` or an ops owner); `name` must be an app name (SD-N104); `to` must be `<account>/<app>` naming an existing app (SD-N105); the file is read with its etag; an existing entry for `name` that maps elsewhere is replaced only when no record of the old mapping's account and app holds an import between `ready` and `committed` (`ready`, `swapped`, `placed`: SD-N103); the same mapping again is a 200 no-op; the file is written by CAS (a lost race or a failed write answers SD-N116); audit SD-N106.

- [ ] **Step 1: Write the failing tests**

```go
package console

import (
	"context"
	"net/http"
	"strings"
	"testing"
	"time"

	"github.com/carlosframework/platform/internal/provision"
	"github.com/carlosframework/platform/internal/seeds"
)

func TestAssignLegacyRoutes(t *testing.T) {
	fx, _ := instanceFixture(t, "code-assign") // app titogo in bab
	owner := cliToken(t, fx, "paul@keymail.dev", "code-assign-owner")
	u := fx.ts.URL + "/api/cli/system/legacy-routes"
	if st, out := postJSON(t, u, map[string]any{"name": "Not An App!", "to": "bab/titogo"}, owner); st != http.StatusBadRequest {
		t.Fatalf("bad name = %d %v", st, out)
	}
	if st, out := postJSON(t, u, map[string]any{"name": "titogo", "to": "bab"}, owner); st != http.StatusBadRequest || !strings.Contains(str(t, out, "error"), "to must be") {
		t.Fatalf("bad to = %d %v", st, out)
	}
	if st, out := postJSON(t, u, map[string]any{"name": "titogo", "to": "bab/titogo"}, owner); st != http.StatusOK || out["account"] != "bab" {
		t.Fatalf("assign = %d %v", st, out)
	}
	o, _, err := seeds.ReadLegacyOwners(context.Background(), fx.rel)
	if got, ok := o.Lookup("titogo"); err != nil || !ok || got != (seeds.LegacyOwner{Account: "bab", App: "titogo"}) {
		t.Fatalf("file = %+v %v", o, err)
	}
}

func TestAssignRefusedWhileAnImportIsInFlight(t *testing.T) {
	fx, _ := instanceFixture(t, "code-assign-busy")
	owner := cliToken(t, fx, "paul@keymail.dev", "code-assign-busy-owner")
	u := fx.ts.URL + "/api/cli/system/legacy-routes"
	postJSON(t, u, map[string]any{"name": "titogo", "to": "bab/titogo"}, owner)
	id, _ := seeds.NewID()
	rec := provision.Record{Schema: 1, Host: "old.go.tito.io", Slug: "old", App: "titogo", Account: "bab", Channel: "stable",
		CreatedAt: time.Now().UTC(), Seed: &provision.SeedSource{ID: id, Kind: seeds.KindImport, State: seeds.StateSwapped, By: "t", At: time.Now().UTC()}}
	if err := provision.CreateRecord(context.Background(), fx.rel, rec); err != nil {
		t.Fatal(err)
	}
	createApp(t, fx, "bab", "other") // the fixture helper that adds an app to an account
	st, out := postJSON(t, u, map[string]any{"name": "titogo", "to": "bab/other"}, owner)
	if st != http.StatusConflict || !strings.Contains(str(t, out, "error"), "has an import in flight") {
		t.Fatalf("re-assign = %d %v", st, out)
	}
	o, _, _ := seeds.ReadLegacyOwners(context.Background(), fx.rel)
	if got, _ := o.Lookup("titogo"); got.App != "titogo" {
		t.Fatalf("the file changed: %+v", got)
	}
}
```

`createApp(t, fx, sqid, app)`: use the store call `instanceFixture` uses to create `titogo` (read `apiinstances_test.go:102`) for a second app. A member who is not an operator gets the bare 404 (the `apiSystemRoller` rule); add that assertion as `TestSeedsFloorDoorIsForSystemOperators` does.

`system_legacy_routes_test.go`:

```go
package main

import "testing"

func TestSystemVerbKnowsLegacyRoutes(t *testing.T) {
	verb, rest, _ := systemVerb([]string{"legacy-routes", "assign", "--name", "titogo", "--to", "bae/titogo"})
	if verb != "legacy-routes" || len(rest) != 5 || rest[0] != "assign" {
		t.Fatalf("systemVerb = %q %q", verb, rest)
	}
}
```

- [ ] **Step 2: Run the tests to verify they fail**

Run: `go test ./internal/console/ -run 'TestAssign' -count=1; go test . -run TestSystemVerbKnowsLegacyRoutes -count=1`
Expected: FAIL.

- [ ] **Step 3: Write the door, the route, the verb and the docs**

`internal/console/apilegacyroutes.go`:

```go
package console

import (
	"encoding/json"
	"errors"
	"fmt"
	"log"
	"net/http"
	"strings"

	"github.com/carlosframework/platform/internal/provision"
	"github.com/carlosframework/platform/internal/release"
	"github.com/carlosframework/platform/internal/seeds"
)

// handleAPISystemLegacyRoutes is `carlos system legacy-routes assign`
// (seeded-instances spec §6.4, codex 9): an operator maps an app name, as
// filed under the operator account, to one account and app.
func (s *Server) handleAPISystemLegacyRoutes(w http.ResponseWriter, r *http.Request) {
	addr, _, ok := s.apiSystemRoller(w, r)
	if !ok {
		return
	}
	var req struct {
		Name string `json:"name"`
		To   string `json:"to"`
	}
	if !readJSON(w, r, &req) {
		return
	}
	name := strings.TrimSpace(req.Name)
	if !release.ValidApp(name) {
		jsonError(w, http.StatusBadRequest, "name must be an app name")
		return
	}
	acctSqid, app, found := strings.Cut(strings.TrimSpace(req.To), "/")
	if !found || !release.ValidApp(app) {
		jsonError(w, http.StatusBadRequest, "to must be <account>/<app>")
		return
	}
	acct, err := s.store.AccountBySqid(acctSqid)
	if err != nil || acct == nil {
		jsonError(w, http.StatusBadRequest, "to must be <account>/<app>")
		return
	}
	if has, err := s.store.HasAppIn(acct.ID, app); err != nil || !has {
		jsonError(w, http.StatusBadRequest, "to must be <account>/<app>")
		return
	}
	ctx := r.Context()
	owners, etag, err := seeds.ReadLegacyOwners(ctx, s.cfg.Releases)
	if err != nil {
		log.Printf("console: legacy routes: %v", err)
		jsonError(w, http.StatusBadGateway, "could not read the legacy route assignments. Try again.")
		return
	}
	want := seeds.LegacyOwner{Account: acct.Sqid, App: app}
	if cur, ok := owners.Lookup(name); ok {
		if cur == want {
			writeJSON(w, http.StatusOK, map[string]string{"name": name, "account": want.Account, "app": want.App})
			return
		}
		busy, err := importInFlight(ctx, s.cfg.Releases, cur)
		if err != nil {
			log.Printf("console: legacy routes: in-flight check: %v", err)
			jsonError(w, http.StatusBadGateway, "could not read the legacy route assignments. Try again.")
			return
		}
		if busy {
			jsonError(w, http.StatusConflict, fmt.Sprintf("%s has an import in flight under its current assignment. Commit or abort it first.", name))
			return
		}
	}
	owners.Names[name] = want
	body, _ := json.Marshal(owners)
	if _, err := s.cfg.Releases.Put(ctx, seeds.LegacyOwnersKey, body, etag); err != nil {
		if !errors.Is(err, release.ErrCASConflict) {
			log.Printf("console: legacy routes: write: %v", err)
		}
		jsonError(w, http.StatusBadGateway, "could not save the legacy route assignment. Try again.")
		return
	}
	s.auditSystem(addr, "legacy-route-assign", fmt.Sprintf("assigned the legacy routes named %s to %s/%s", name, want.Account, want.App))
	s.bumpSerial(ctx)
	writeJSON(w, http.StatusOK, map[string]string{"name": name, "account": want.Account, "app": want.App})
}

// importInFlight reports an import of o's account and app between ready
// and committed (spec §6.4).
func importInFlight(ctx context.Context, rs release.Store, o seeds.LegacyOwner) (bool, error) {
	recs, truncated, err := provision.ListRecords(ctx, rs, o.Account, o.App, 10000)
	if err != nil {
		return false, err
	}
	if truncated {
		return true, nil // a listing that did not see every record never says "idle"
	}
	for _, rec := range recs {
		if rec.Seed.Import() {
			switch rec.Seed.State {
			case seeds.StateReady, seeds.StateSwapped, seeds.StatePlaced:
				return true, nil
			}
		}
	}
	return false, nil
}
```

(add `"context"` to the imports).

In `authcli.go`, beside the seeds-floor route: `s.cliRoute("POST /api/cli/system/legacy-routes", svcrole.Set{}, s.handleAPISystemLegacyRoutes)`.

`system_legacy_routes.go`:

```go
package main

import (
	"context"
	"flag"
	"fmt"
	"log"
	"net/http"
	"os"
)

// runSystemLegacyRoutesVerb is `carlos system legacy-routes assign`
// (seeded-instances spec §6.4).
func runSystemLegacyRoutesVerb(rest []string) {
	if len(rest) == 0 || rest[0] != "assign" {
		systemUsage()
		os.Exit(2)
	}
	fs := flag.NewFlagSet("system legacy-routes assign", flag.ExitOnError)
	consoleFlag := consoleOption(fs, "console origin (default: the only logged-in one)")
	name := fs.String("name", "", "the app name the legacy routes are filed under, in the operator account")
	to := fs.String("to", "", "the account and app they belong to, as <account>/<app>")
	fs.Parse(rest[1:])
	if *name == "" || *to == "" || fs.NArg() != 0 {
		fs.Usage()
		os.Exit(2)
	}
	sess := requireDeployAPISession(*consoleFlag)
	printUsing(stdout, sess)
	status, body, err := sess.postJSON(context.Background(), "/api/cli/system/legacy-routes", map[string]string{"name": *name, "to": *to})
	if err != nil {
		log.Fatalf("carlos system legacy-routes assign: %v", err)
	}
	if status != http.StatusOK {
		log.Fatalf("carlos system legacy-routes assign: %v", apiErrorFromBody(status, body))
	}
	var out struct {
		Name    string `json:"name"`
		Account string `json:"account"`
		App     string `json:"app"`
	}
	if err := json.Unmarshal(body, &out); err != nil {
		log.Fatalf("carlos system legacy-routes assign: unreadable response: %v", err)
	}
	fmt.Fprintln(stdout, fmt.Sprintf("assigned the legacy routes named %s to %s/%s", out.Name, out.Account, out.App))
}
```

(add `"encoding/json"` to the imports).

In `system.go`: `"legacy-routes"` in `systemVerb`'s case list; `case "legacy-routes": runSystemLegacyRoutesVerb(rest)` in `runSystem`; the usage line `       carlos system legacy-routes assign --name <name> --to <account>/<app>` at the top, and under "The platform's own binary (ops account owners and CARLOS_SYSTEM_OPERATORS):"

```
  legacy-routes assign --name <name> --to <account>/<app>
            the account and app they belong to, as <account>/<app>
```

In `docs/site/cli.md`, under `### carlos system`, a `#### Legacy route assignments` subsection with SD-N110's approved text and:

````markdown
```
carlos system legacy-routes assign --name titogo --to bae/titogo
```
````

- [ ] **Step 4: Run the tests to verify they pass**

Run: `go test ./internal/console/ -run 'TestAssign|TestSystemDoors' -count=1 && go test . -run 'TestSystemVerb|TestSeeded|TestMoves' -count=1`
Expected: PASS.

- [ ] **Step 5: Wire SD-N102 to SD-N110, SD-N116 and commit**

```bash
git add internal/console/apilegacyroutes.go internal/console/apilegacyroutes_test.go internal/console/authcli.go system.go system_legacy_routes.go system_legacy_routes_test.go docs/site/cli.md approved_seeded_copy_test.go
git commit -m "carlos system legacy-routes assign (Plan 8 Task 5)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
```

### Task 6: Series gate

- [ ] **Step 1: Every deck row is wired**

`seededWiredIDs` now names every id in `seededCopyIDs`. Add the test that holds it:

```go
func TestEverySeededRowIsWired(t *testing.T) {
	wired := map[string]bool{}
	for _, id := range seededWiredIDs {
		wired[id] = true
	}
	for _, id := range seededCopyIDs {
		if !wired[id] {
			t.Errorf("%s is approved and never wired", id)
		}
	}
}
```

Run: `go test . -run 'TestSeeded|TestEverySeededRowIsWired' -count=1` (PASS).

- [ ] **Step 2: Cross-compile and run the full suite under -race once**

Run:
```bash
GOOS=darwin go build ./... && GOOS=windows go build ./... && GOOS=linux GOARCH=arm64 go build ./... && \
nice go test -race ./... -count=1
```
Expected: PASS.

- [ ] **Step 3: Commit**

```bash
git add approved_seeded_copy_test.go
git commit -m "copy gate: every seeded row is wired (Plan 8 Task 6)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
```

---

## Self-review

- Spec coverage: §3.1's three sweep cases with seed-ID-scoped deletes and the CAS-first abandonment (Task 2), §5.6's sweep as the finish's fallback (Task 2 calling Plan 6's `finishSeed`), §5.8 in both doors under the CAS and in the worker before effects (Tasks 3, 4), §6.4's operator-only assignment with its in-flight rule and audit row (Task 5), §7's move errors (Task 3).
- §9's console tests: the seeds sweep deletes only unreferenced or superseded snapshots and never one a live `--from` needs (Task 2); the sweep abandons `reserving` only after an hour (Task 2); a move refused before placement or commit in both doors and the worker (Tasks 3, 4); `system legacy-routes assign` (Task 5).
- Deviation: the sweep also deletes a snapshot whose own record is `placed` or `committed` (Task 2's note); spec §3.1 lists three cases and does not cover a box's lost delete.
- Deviation: "more than an hour old" is the object's last-modified time from `ListMod`; a store without `ListMod` skips the snapshot walk.
- The dev-box checks of spec §9 and the rollout of §10 need Paul and are not tasks here.
