# Region HA 1a: Foundations 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:** Build every part of region HA spec 1 that does not need the host lifecycle claim: box identity records, box evidence and choice, stamp-ordered home map, the ordered host-files publisher, wake-state export, termination-fence verification, the installation-complete marker, and the infrastructure they need.

**Architecture:** Eleven small tasks, each inert on a one-box region. Nothing here changes which box runs an instance: that is plan 1b, which wires these into create, reconcile, the activator and the `relocating` operation once the claim (`internal/claim`, not yet on main) lands. Every piece is a pure function or a store-backed type with its own tests.

**Tech Stack:** Go 1.26, module `github.com/carlosframework/platform`; `release.Store` (create-only and ETag compare-and-swap puts); SQLite registry; `golang.org/x/sys/unix` inotify; OpenTofu modules under `infra/modules/`.

**Spec:** `docs/superpowers/specs/2026-10-04-region-ha-box-placement-design.md` (rev 4.2 plus rulings H7 to H9, at 2cb9d12e). Executors read it first: §2 words, §3 invariants, §5.1, §5.4 to §5.7, §5.9, §6.

## Why this is plan 1a and not the whole of spec 1

Spec 1 sits on the host lifecycle claim, which exists only as a spec (rev 6.12 on `design-replica-ownership`; spec 1 was reviewed against rev 6.3) and on move-any-app's install contract (rev 7.12, also unbuilt). There is no `internal/claim` or `internal/hostguard` on main. So spec 1 splits:

- **1a (this plan):** everything with no claim dependency. Mergeable to main now, dormant.
- **1b (written when the claim's code lands):** create names a box (§5.2), reconcile's three row cases (§5.3), the activator's warm-copy check wired in (§5.5), relocation and the release sequence (§5.8), the multi-box gate (§8), `carlos ops box bind`, the manifest's `files_durability` projection onto routes, and starting the publisher from the agent. Before 1b is written, spec 1 is re-read against the claim's then-current revision.

## Global Constraints

- Nothing in this plan may change behaviour on a one-box region. Every new field is additive and `omitempty`; every reader tolerates its absence (AGENTS.md: "Wire-shape changes are additive").
- When the edges-file shape changes, every parser changes in the same task: `internal/host`, `internal/edge`, `internal/console`, and check `internal/edgesfile` and `internal/steering` (AGENTS.md: "the consumer list is everything that PARSES the shape").
- Linux-only syscalls live in `*_linux.go` with `//go:build linux` and a `*_other.go` stub with `//go:build !linux`. CI cross-compiles darwin and windows.
- Do not add a method to `release.Store` (eleven implementations). Use the existing `Get`/`Put`/`List`/`Delete`; `Put(ctx, key, body, "")` is create-only, a non-empty `ifMatch` is replace-only, and a lost race is `release.ErrCASConflict`.
- Tests use `release.NewDirStore(t.TempDir())`; inject failures with a struct embedding `release.Store` that overrides one method.
- No new Go module dependencies. `service/ec2` and `x/sys` are already direct. Do **not** add `service/sqs`, `service/autoscaling` or a PKCS7 library in this plan.
- No user-facing copy is added here. Log lines are operator-facing and follow the surrounding `agent: …` / `edges: …` style. If a task finds it must add console or CLI copy, stop: that needs the copy-review gate.
- Commit messages: lowercase `area:` prefix and a sentence stating the behaviour, for example `boxid: a box record verifies only under AWS's signature`. End each with `Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>`.
- Go test environment on this machine: use the default Go build cache. Do not set `GOCACHE`, `GOMODCACHE` or `GOTMPDIR` under `/tmp`. Scratch files go under `$TMPDIR`. Unix-socket tests need a short `TMPDIR`.
- The branch already contains `origin/main` as of 2026-10-05 (merge 014b0ce1). Before that merge `internal/console`'s tests did not build on this branch; if they fail to build again, merge main first.
- Local test runs need `GOFLAGS=-buildvcs=false` (some console tests build a binary and fail on VCS stamping in a worktree) and, inside the Claude sandbox, `GOCACHE=$TMPDIR/gocache`. Tests that open Unix sockets (`internal/activator`, `internal/edge`) fail inside the sandbox with `socket: operation not permitted`: that is the sandbox, not the change. Run those packages with the sandbox off, or rely on CI.
- This plan's code was dry-run on 2026-10-05: every new package and both patch tasks were built, cross-built for darwin and windows, and tested, then removed. Two things the dry run could not do are called out where they matter: the real-inotify tests (Task 7, step 10) and the IMDS signature check (Task 1, step 7).
- Gate before each commit: `go build ./... && go vet ./... && gofmt -l . && go test ./internal/<pkg>/... -count=1`. Full gate before the branch lands: `make test`.
- Exact values from the spec: continuous deadline **1 second** from the first unpublished modification; repair scan **10 minutes**; polling fallback **1 second**; snapshot interval default **15 minutes**; `files_durability` default **`snapshot`** (H8); capability string **`region-ha-1`**; termination fence accepts **only** state `terminated`.

## Three deliberate departures from the spec's wording

Each keeps the spec's property and is recorded in Task 11 as a spec note.

1. **Exact versions by content hash, not S3 version id.** Spec §5.6 says restore fetches "by the exact versions it lists". `release.Store` has no version-id read and the box role has no `s3:GetObjectVersion`. Objects here are immutable, written create-only under a key that contains their SHA-256, and the manifest records each SHA-256. A reader verifies the hash. That pins content exactly without version ids.
2. **Identity signature endpoint.** Spec §5.1 says "PKCS7 signature". IMDS also serves `/latest/dynamic/instance-identity/signature`, a base64 RSA-SHA256 signature over the identity document, verifiable with `crypto/x509` and no PKCS7 library. This plan uses that endpoint. Task 1's last step checks it against a real box before the branch lands.
3. **`snapshot` is a periodic full publish, not a sealed archive.** Spec §5.6 gives the manifest a sealed-archive `base`. Here `hostfiles.SyncDir` stages every difference between the directory and the manifest; run on the snapshot interval it is the `snapshot` setting, with no second format and no whole-directory archive held in memory. The manifest has no `base`.

## Review Focus

1. **A restore path that escapes its directory.** A manifest entry named `../x`, `/etc/x` or `a/../../x` must be refused, never written (Task 5).
2. **Non-regular files in the files directory.** Symlinks, sockets, FIFOs and devices must be skipped and counted, never followed or opened (Tasks 5, 6, 7).
3. **A file larger than memory allows.** `release.Store.Put` takes a whole `[]byte`. A file over the 64 MiB cap is skipped and reported, never read whole, and never silently dropped from the manifest (Tasks 5, 7).
4. **A lookalike termination.** An event or describe result for the right instance id in the wrong account or region, or with state `shutting-down`, `stopped` or missing, must not verify (Task 9).
5. **A box record whose id is someone else's.** A validly signed identity document for instance A, stored under box id B, must not verify (Task 1).

## File Structure

| Path | Responsibility |
|---|---|
| `internal/boxid/record.go` | The box record type, `Publish` (create-only), `Load` |
| `internal/boxid/verify.go` | `Verifier`: signature, account, region, id binding, legacy bind |
| `internal/boxid/certs.go`, `certs/*.pem` | AWS regional signing certificates, embedded |
| `internal/host/edgesfile.go` | New edges-file fields; `Stamp` on `edgeHost` |
| `internal/host/headroom_linux.go`, `headroom_other.go` | Reading memory, disk and awake counts |
| `internal/edge/homemap.go` | `Stamp` in the edge's view; stamp-ordered fold |
| `internal/console/instancesplacement.go` | New fields in the console's view |
| `internal/console/boxchoice.go` | `eligibleBoxes`, `chooseBox` |
| `internal/hostfiles/manifest.go` | Manifest and entry types, keys, path validation |
| `internal/hostfiles/publisher.go` | Stage, commit (CAS), tombstones |
| `internal/hostfiles/restore.go` | Restore a directory from a manifest |
| `internal/hostfiles/sync.go` | `SyncDir`: stage a whole directory's differences; `readStable` |
| `internal/hostfiles/run.go`, `deadline.go`, `watch_linux.go`, `watch_other.go` | The publish loop, deadline queue, inotify watcher, polling fallback |
| `internal/registry/timers.go` | `ReplaceTimers` |
| `internal/hostfiles/wake.go` | Wake state: timers, `NextDue`, `Replication` |
| `internal/termfence/fence.go` | Fence type, the three evidence sources, `Verify` |
| `internal/activator/installed.go` | Installation-complete marker and `warmCopyTrusted` |
| `infra/modules/region-host/*.tf`, `infra/modules/deployment-buckets/*.tf` | Volume flag, termination queue, IAM, bucket policies |

---

### Task 1: Box records and their verification (`internal/boxid`)

**Files:**
- Create: `internal/boxid/record.go`, `internal/boxid/verify.go`, `internal/boxid/certs.go`, `internal/boxid/certs/README.md`
- Test: `internal/boxid/boxid_test.go`

**Interfaces:**
- Consumes: `release.Store`.
- Produces:
  ```go
  package boxid
  const Prefix = "control/boxes/"
  func Key(box string) string      // Prefix + box + ".json"
  func BindKey(box string) string  // Prefix + box + ".bind.json"
  type Record struct {
      V                 int    `json:"v"`
      Box               string `json:"box"`
      IdentityDocument  string `json:"identity_document"`  // base64 of the raw IMDS document bytes
      IdentitySignature string `json:"identity_signature"` // base64 RSA-SHA256, as IMDS serves it
      WrittenAt         string `json:"written_at"`
  }
  type Identity struct{ AccountID, Region, InstanceID string }
  type Bind struct {
      RecordSHA256   string `json:"record_sha256"`
      InstanceID     string `json:"instance_id"`
      TofuInstanceID string `json:"tofu_instance_id"`
      Operator       string `json:"operator"`
      At             string `json:"at"`
  }
  type Verifier struct {
      Account string                       // the deployment's AWS account id
      Certs   map[string]*x509.Certificate // by AWS region
  }
  func (v Verifier) Verify(rec Record, raw []byte, bind *Bind) (Identity, error)
  func Publish(ctx context.Context, s release.Store, rec Record) error // create-only; an identical existing record is success
  func Load(ctx context.Context, s release.Store, box string) (Record, []byte, *Bind, error) // ErrUnverified if the stored record names another box
  func EmbeddedCerts() (map[string]*x509.Certificate, error)
  var ErrUnverified = errors.New("boxid: record does not verify")
  ```
  `Bind.RecordSHA256` replaces the spec's `record_version_id` (see "Three deliberate departures").

**Two bindings, both needed.** `Verify` binds the record's `box` field to the signed instance. `Load` binds the storage key to the record's `box` field. Without the second, box A's own valid record copied to `control/boxes/B.json` would load as B and verify as A. `Verify` does not check the region against an expectation: it proves which region the box is in, and the caller compares (Task 3's `eligibleBoxes`, Task 9's `termfence.Verify`).

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

`internal/boxid/boxid_test.go`:

```go
package boxid

import (
	"context"
	"crypto"
	"crypto/rand"
	"crypto/rsa"
	"crypto/sha256"
	"crypto/x509"
	"crypto/x509/pkix"
	"encoding/base64"
	"encoding/hex"
	"encoding/json"
	"errors"
	"math/big"
	"testing"
	"time"

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

// signer stands in for AWS: one key and certificate per test.
type signer struct {
	key  *rsa.PrivateKey
	cert *x509.Certificate
}

func newSigner(t *testing.T) signer {
	t.Helper()
	key, err := rsa.GenerateKey(rand.Reader, 2048)
	if err != nil {
		t.Fatal(err)
	}
	tmpl := &x509.Certificate{SerialNumber: big.NewInt(1), Subject: pkix.Name{CommonName: "test"},
		NotBefore: time.Now().Add(-time.Hour), NotAfter: time.Now().Add(time.Hour)}
	der, err := x509.CreateCertificate(rand.Reader, tmpl, tmpl, &key.PublicKey, key)
	if err != nil {
		t.Fatal(err)
	}
	cert, err := x509.ParseCertificate(der)
	if err != nil {
		t.Fatal(err)
	}
	return signer{key, cert}
}

func (s signer) record(t *testing.T, box, account, region, instance string) (Record, []byte) {
	t.Helper()
	doc, _ := json.Marshal(map[string]string{"accountId": account, "region": region, "instanceId": instance})
	sum := sha256.Sum256(doc)
	sig, err := rsa.SignPKCS1v15(rand.Reader, s.key, crypto.SHA256, sum[:])
	if err != nil {
		t.Fatal(err)
	}
	rec := Record{V: 1, Box: box, IdentityDocument: base64.StdEncoding.EncodeToString(doc),
		IdentitySignature: base64.StdEncoding.EncodeToString(sig), WrittenAt: "2026-10-05T00:00:00Z"}
	raw, _ := json.Marshal(rec)
	return rec, raw
}

func (s signer) verifier() Verifier {
	return Verifier{Account: "111122223333", Certs: map[string]*x509.Certificate{"eu-west-1": s.cert}}
}

func TestAutoScalingBoxVerifiesWhenIDIsItsInstance(t *testing.T) {
	s := newSigner(t)
	rec, raw := s.record(t, "i-0abc", "111122223333", "eu-west-1", "i-0abc")
	id, err := s.verifier().Verify(rec, raw, nil)
	if err != nil {
		t.Fatal(err)
	}
	if id.InstanceID != "i-0abc" || id.Region != "eu-west-1" || id.AccountID != "111122223333" {
		t.Fatalf("identity = %+v", id)
	}
}

// Review Focus 5: a valid document for instance A under box id B.
func TestRecordUnderAnotherInstancesIDIsRefused(t *testing.T) {
	s := newSigner(t)
	rec, raw := s.record(t, "i-0other", "111122223333", "eu-west-1", "i-0abc")
	if _, err := s.verifier().Verify(rec, raw, nil); !errors.Is(err, ErrUnverified) {
		t.Fatalf("err = %v, want ErrUnverified", err)
	}
}

func TestRefusals(t *testing.T) {
	s, other := newSigner(t), newSigner(t)
	cases := map[string]func() (Record, []byte, Verifier){
		"wrong account": func() (Record, []byte, Verifier) {
			r, raw := s.record(t, "i-0abc", "999999999999", "eu-west-1", "i-0abc")
			return r, raw, s.verifier()
		},
		"region with no certificate": func() (Record, []byte, Verifier) {
			r, raw := s.record(t, "i-0abc", "111122223333", "ap-south-1", "i-0abc")
			return r, raw, s.verifier()
		},
		"signed by someone else": func() (Record, []byte, Verifier) {
			r, raw := other.record(t, "i-0abc", "111122223333", "eu-west-1", "i-0abc")
			return r, raw, s.verifier()
		},
		"document altered after signing": func() (Record, []byte, Verifier) {
			r, _ := s.record(t, "i-0abc", "111122223333", "eu-west-1", "i-0abc")
			doc, _ := json.Marshal(map[string]string{"accountId": "111122223333", "region": "eu-west-1", "instanceId": "i-0zzz"})
			r.IdentityDocument, r.Box = base64.StdEncoding.EncodeToString(doc), "i-0zzz"
			raw, _ := json.Marshal(r)
			return r, raw, s.verifier()
		},
		"empty box": func() (Record, []byte, Verifier) {
			r, raw := s.record(t, "", "111122223333", "eu-west-1", "i-0abc")
			return r, raw, s.verifier()
		},
	}
	for name, mk := range cases {
		t.Run(name, func(t *testing.T) {
			r, raw, v := mk()
			if _, err := v.Verify(r, raw, nil); !errors.Is(err, ErrUnverified) {
				t.Fatalf("err = %v, want ErrUnverified", err)
			}
		})
	}
}

func TestLegacyLabelNeedsABindNamingThisExactRecord(t *testing.T) {
	s := newSigner(t)
	rec, raw := s.record(t, "annie-welding", "111122223333", "eu-west-1", "i-0abc")
	if _, err := s.verifier().Verify(rec, raw, nil); !errors.Is(err, ErrUnverified) {
		t.Fatalf("no bind: err = %v, want ErrUnverified", err)
	}
	sum := sha256.Sum256(raw)
	good := &Bind{RecordSHA256: hex.EncodeToString(sum[:]), InstanceID: "i-0abc", TofuInstanceID: "i-0abc", Operator: "paul", At: "2026-10-05T00:00:00Z"}
	if _, err := s.verifier().Verify(rec, raw, good); err != nil {
		t.Fatalf("good bind: %v", err)
	}
	for name, b := range map[string]Bind{
		"another record's hash":  {RecordSHA256: "00", InstanceID: "i-0abc", TofuInstanceID: "i-0abc", Operator: "paul"},
		"tofu names another box": {RecordSHA256: good.RecordSHA256, InstanceID: "i-0abc", TofuInstanceID: "i-0zzz", Operator: "paul"},
		"bind names another box": {RecordSHA256: good.RecordSHA256, InstanceID: "i-0zzz", TofuInstanceID: "i-0zzz", Operator: "paul"},
		"no operator":            {RecordSHA256: good.RecordSHA256, InstanceID: "i-0abc", TofuInstanceID: "i-0abc"},
	} {
		b := b
		t.Run(name, func(t *testing.T) {
			if _, err := s.verifier().Verify(rec, raw, &b); !errors.Is(err, ErrUnverified) {
				t.Fatalf("err = %v, want ErrUnverified", err)
			}
		})
	}
}

func TestPublishIsCreateOnlyAndIdempotent(t *testing.T) {
	ctx, st, s := context.Background(), release.NewDirStore(t.TempDir()), newSigner(t)
	rec, _ := s.record(t, "i-0abc", "111122223333", "eu-west-1", "i-0abc")
	if err := Publish(ctx, st, rec); err != nil {
		t.Fatal(err)
	}
	if err := Publish(ctx, st, rec); err != nil {
		t.Fatalf("republishing the same record: %v", err)
	}
	other, _ := s.record(t, "i-0abc", "111122223333", "eu-west-1", "i-0abc")
	other.WrittenAt = "2027-01-01T00:00:00Z"
	if err := Publish(ctx, st, other); !errors.Is(err, release.ErrCASConflict) {
		t.Fatalf("a different record for the same box: err = %v, want ErrCASConflict", err)
	}
	got, raw, bind, err := Load(ctx, st, "i-0abc")
	if err != nil || got.WrittenAt != rec.WrittenAt || len(raw) == 0 || bind != nil {
		t.Fatalf("Load = %+v, %d bytes, bind %v, err %v", got, len(raw), bind, err)
	}
}

func TestLoadReadsABindWhenThereIsOne(t *testing.T) {
	ctx, st, s := context.Background(), release.NewDirStore(t.TempDir()), newSigner(t)
	rec, _ := s.record(t, "annie-welding", "111122223333", "eu-west-1", "i-0abc")
	if err := Publish(ctx, st, rec); err != nil {
		t.Fatal(err)
	}
	body, _ := json.Marshal(Bind{RecordSHA256: "aa", InstanceID: "i-0abc", TofuInstanceID: "i-0abc", Operator: "paul"})
	if _, err := st.Put(ctx, BindKey("annie-welding"), body, ""); err != nil {
		t.Fatal(err)
	}
	_, _, bind, err := Load(ctx, st, "annie-welding")
	if err != nil || bind == nil || bind.Operator != "paul" {
		t.Fatalf("bind = %+v, err %v", bind, err)
	}
}

// Review Focus 5, the storage half: box A's own record, unchanged and
// validly signed, stored under box B's key must not load as B.
func TestLoadRefusesAnotherBoxesRecordUnderThisKey(t *testing.T) {
	ctx, st, s := context.Background(), release.NewDirStore(t.TempDir()), newSigner(t)
	_, raw := s.record(t, "i-0aaa", "111122223333", "eu-west-1", "i-0aaa")
	if _, err := st.Put(ctx, Key("i-0bbb"), raw, ""); err != nil {
		t.Fatal(err)
	}
	if _, _, _, err := Load(ctx, st, "i-0bbb"); !errors.Is(err, ErrUnverified) {
		t.Fatalf("err = %v, want ErrUnverified", err)
	}
}
```

- [ ] **Step 2: Run the tests to see them fail**

Run: `go test ./internal/boxid/ -count=1`
Expected: FAIL to build, `undefined: Record`.

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

```go
// Package boxid binds the name a claim uses for a box to one AWS instance
// (region HA spec 1 §5.1). The binding is the box's own AWS-signed identity
// document, written once. Nothing else is evidence of a box's identity: not
// an edges file, not a status snapshot.
package boxid

import (
	"bytes"
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"os"

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

const Prefix = "control/boxes/"

func Key(box string) string     { return Prefix + box + ".json" }
func BindKey(box string) string { return Prefix + box + ".bind.json" }

type Record struct {
	V                 int    `json:"v"`
	Box               string `json:"box"`
	IdentityDocument  string `json:"identity_document"`
	IdentitySignature string `json:"identity_signature"`
	WrittenAt         string `json:"written_at"`
}

// Bind is an operator's statement that a legacy label belongs to one
// instance. It names the exact record bytes it vouches for.
type Bind struct {
	RecordSHA256   string `json:"record_sha256"`
	InstanceID     string `json:"instance_id"`
	TofuInstanceID string `json:"tofu_instance_id"`
	Operator       string `json:"operator"`
	At             string `json:"at"`
}

// Publish writes a box's record create-only. Writing the same bytes again
// succeeds; a different record for the same box is ErrCASConflict, because
// a record is never replaced.
func Publish(ctx context.Context, s release.Store, rec Record) error {
	body, err := json.Marshal(rec)
	if err != nil {
		return fmt.Errorf("boxid: marshal: %w", err)
	}
	_, err = s.Put(ctx, Key(rec.Box), body, "")
	if !errors.Is(err, release.ErrCASConflict) {
		return err
	}
	cur, _, gerr := s.Get(ctx, Key(rec.Box))
	if gerr != nil {
		return fmt.Errorf("boxid: %s exists and cannot be read: %w", rec.Box, gerr)
	}
	if bytes.Equal(cur, body) {
		return nil
	}
	return fmt.Errorf("boxid: %s already has a different record: %w", rec.Box, release.ErrCASConflict)
}

// Load returns a box's record, its exact bytes (which a Bind's hash is over)
// and its bind when one exists. A missing record is os.ErrNotExist-wrapped.
// A record that names a different box than the key it is stored under is
// ErrUnverified: the key is part of the binding.
func Load(ctx context.Context, s release.Store, box string) (Record, []byte, *Bind, error) {
	raw, _, err := s.Get(ctx, Key(box))
	if err != nil {
		return Record{}, nil, nil, err
	}
	var rec Record
	if err := json.Unmarshal(raw, &rec); err != nil {
		return Record{}, nil, nil, fmt.Errorf("boxid: %s: %w", box, err)
	}
	if rec.Box != box {
		// Another box's record, intact and validly signed, copied under
		// this name. Verify would accept it as that other box.
		return Record{}, nil, nil, fmt.Errorf("%w: the record stored for %q names %q", ErrUnverified, box, rec.Box)
	}
	braw, _, err := s.Get(ctx, BindKey(box))
	if errors.Is(err, os.ErrNotExist) {
		return rec, raw, nil, nil
	}
	if err != nil {
		return Record{}, nil, nil, err
	}
	var b Bind
	if err := json.Unmarshal(braw, &b); err != nil {
		return Record{}, nil, nil, fmt.Errorf("boxid: %s bind: %w", box, err)
	}
	return rec, raw, &b, nil
}
```

- [ ] **Step 4: Write `verify.go`**

```go
package boxid

import (
	"crypto/sha256"
	"crypto/x509"
	"encoding/base64"
	"encoding/hex"
	"encoding/json"
	"errors"
	"fmt"
)

// ErrUnverified wraps every reason a record is not evidence of identity.
var ErrUnverified = errors.New("boxid: record does not verify")

type Identity struct{ AccountID, Region, InstanceID string }

type Verifier struct {
	Account string
	Certs   map[string]*x509.Certificate
}

func refuse(format string, a ...any) error {
	return fmt.Errorf("%w: %s", ErrUnverified, fmt.Sprintf(format, a...))
}

// Verify checks, in order: the signature under the certificate for the
// document's own region, the deployment account, and the binding of the
// record's box id to the signed instance id. A box whose id is its instance
// id needs nothing more. Any other id is a legacy label and needs a bind
// that names these exact record bytes and agrees on the instance.
func (v Verifier) Verify(rec Record, raw []byte, bind *Bind) (Identity, error) {
	if rec.Box == "" {
		return Identity{}, refuse("no box id")
	}
	doc, err := base64.StdEncoding.DecodeString(rec.IdentityDocument)
	if err != nil {
		return Identity{}, refuse("document is not base64")
	}
	sig, err := base64.StdEncoding.DecodeString(rec.IdentitySignature)
	if err != nil {
		return Identity{}, refuse("signature is not base64")
	}
	var d struct {
		AccountID  string `json:"accountId"`
		Region     string `json:"region"`
		InstanceID string `json:"instanceId"`
	}
	if err := json.Unmarshal(doc, &d); err != nil {
		return Identity{}, refuse("document is not JSON")
	}
	cert := v.Certs[d.Region]
	if cert == nil {
		return Identity{}, refuse("no signing certificate for region %q", d.Region)
	}
	if err := cert.CheckSignature(x509.SHA256WithRSA, doc, sig); err != nil {
		return Identity{}, refuse("signature: %v", err)
	}
	if d.InstanceID == "" {
		return Identity{}, refuse("document names no instance")
	}
	if v.Account == "" || d.AccountID != v.Account {
		return Identity{}, refuse("account %q is not the deployment's", d.AccountID)
	}
	id := Identity{AccountID: d.AccountID, Region: d.Region, InstanceID: d.InstanceID}
	if rec.Box == d.InstanceID {
		return id, nil
	}
	if bind == nil {
		return Identity{}, refuse("%q is a legacy label with no operator bind", rec.Box)
	}
	sum := sha256.Sum256(raw)
	switch {
	case bind.RecordSHA256 != hex.EncodeToString(sum[:]):
		return Identity{}, refuse("bind is for a different record")
	case bind.InstanceID != d.InstanceID:
		return Identity{}, refuse("bind names instance %q, the record %q", bind.InstanceID, d.InstanceID)
	case bind.TofuInstanceID != d.InstanceID:
		return Identity{}, refuse("tofu records instance %q, the record %q", bind.TofuInstanceID, d.InstanceID)
	case bind.Operator == "":
		return Identity{}, refuse("bind names no operator")
	}
	return id, nil
}
```

- [ ] **Step 5: Write `certs.go` and add the certificates**

```go
package boxid

import (
	"crypto/x509"
	"embed"
	"encoding/pem"
	"fmt"
	"strings"
)

//go:embed certs/*.pem
var certFS embed.FS

// EmbeddedCerts returns AWS's instance-identity signing certificates by
// region, from certs/<aws-region>.pem. A region with no file has no
// certificate, so its boxes never verify: that is the safe default.
func EmbeddedCerts() (map[string]*x509.Certificate, error) {
	entries, err := certFS.ReadDir("certs")
	if err != nil {
		return nil, err
	}
	out := map[string]*x509.Certificate{}
	for _, e := range entries {
		region, ok := strings.CutSuffix(e.Name(), ".pem")
		if !ok {
			continue
		}
		raw, err := certFS.ReadFile("certs/" + e.Name())
		if err != nil {
			return nil, err
		}
		block, _ := pem.Decode(raw)
		if block == nil {
			return nil, fmt.Errorf("boxid: certs/%s is not PEM", e.Name())
		}
		cert, err := x509.ParseCertificate(block.Bytes)
		if err != nil {
			return nil, fmt.Errorf("boxid: certs/%s: %w", e.Name(), err)
		}
		out[region] = cert
	}
	return out, nil
}
```

Download the four regional certificates the flagship uses. They are on the AWS page "AWS public certificates for instance identity signatures", https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/regions-certs.html . For each of `eu-west-1`, `eu-west-2`, `us-east-1` and `ap-southeast-2`, open the region, take the certificate under the **"Base64-encoded signature"** tab (not the PKCS7 or RSA-2048 tabs), and save it verbatim, including the `BEGIN`/`END CERTIFICATE` lines, as `internal/boxid/certs/<region>.pem`.

Write `internal/boxid/certs/README.md`:

```markdown
AWS's public certificates for the base64-encoded instance identity
signature (IMDS `/latest/dynamic/instance-identity/signature`), one per
region, from
https://docs.aws.amazon.com/AWSEC2/latest/UserGuide/regions-certs.html
(the "Base64-encoded signature" tab). A region with no file here has no
certificate, and a box there never verifies. Add a region's file before
running a second box in it.
```

Append to `boxid_test.go`:

```go
func TestEmbeddedCertsCoverTheFlagshipRegions(t *testing.T) {
	certs, err := EmbeddedCerts()
	if err != nil {
		t.Fatal(err)
	}
	for _, region := range []string{"eu-west-1", "eu-west-2", "us-east-1", "ap-southeast-2"} {
		c := certs[region]
		if c == nil {
			t.Errorf("no certificate for %s", region)
			continue
		}
		if c.PublicKeyAlgorithm != x509.RSA {
			t.Errorf("%s: %v key; the base64-signature certificate is RSA (the PKCS7 one is DSA: wrong tab)", region, c.PublicKeyAlgorithm)
		}
	}
}
```

- [ ] **Step 6: Run the tests**

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

- [ ] **Step 7: Check the signature endpoint against a real box**

This is the one step that needs AWS. On the dev box (ask Paul for the session; do not run it against a production box unprompted), run:

```bash
TOKEN=$(curl -sX PUT http://169.254.169.254/latest/api/token -H 'X-aws-ec2-metadata-token-ttl-seconds: 60')
curl -sH "X-aws-ec2-metadata-token: $TOKEN" http://169.254.169.254/latest/dynamic/instance-identity/document > doc.json
curl -sH "X-aws-ec2-metadata-token: $TOKEN" http://169.254.169.254/latest/dynamic/instance-identity/signature | base64 -d > sig.bin
openssl x509 -in <region>.pem -pubkey -noout > pub.pem
openssl dgst -sha256 -verify pub.pem -signature sig.bin doc.json
```

Expected: `Verified OK`. If it prints `Verification Failure`, stop and report: the certificate tab or the digest is wrong, and `Verify` must not ship until this passes. Record the result in the task's commit message.

- [ ] **Step 8: Commit**

```bash
git add internal/boxid
git commit -m "boxid: a box record verifies only under AWS's signature, for its own instance

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

---

### Task 2: Box evidence in the edges file

**Files:**
- Modify: `internal/host/edgesfile.go` (struct at `:156-232`, ident at `:638-650`), `internal/console/instancesplacement.go:38`
- Create: `internal/host/headroom_linux.go`, `internal/host/headroom_other.go`
- Test: `internal/host/edgesfile_test.go`, `internal/host/headroom_test.go`, `internal/console/regioncandidates_test.go`

**Interfaces:**
- Consumes: `status.InstanceID() string`; `edgesFilePass(ctx, s, reg, ident, eip, &last)`.
- Produces, in `internal/host`:
  ```go
  type edgeHeadroom struct {
      MemFreeMiB  int64 `json:"mem_free_mib"`
      DiskFreeMiB int64 `json:"disk_free_mib"`
      Awake       int   `json:"awake"`
  }
  // new edgesFile fields:
  BoxID        string        `json:"box_id,omitempty"`
  Capabilities []string      `json:"capabilities,omitempty"`
  Draining     bool          `json:"draining,omitempty"`
  Headroom     *edgeHeadroom `json:"headroom,omitempty"`
  const capRegionHA1 = "region-ha-1"
  func edgesFileBoxID(instanceID, label string) string
  func edgesFileCapabilities(getenv func(string) string) []string
  func readHeadroom(dataDir string, awake int) *edgeHeadroom
  func awakeCount(reg *registry.Registry) int
  // edgesFilePass keeps its signature and calls this with refresh false:
  func edgesFileWrite(ctx context.Context, s release.Store, reg *registry.Registry, ident edgesFile, eip string, last *[]byte, refresh bool) error
  ```
  and in `internal/console`, on `edgesFilePlacementView`:
  ```go
  BoxID        string   `json:"box_id"`
  Capabilities []string `json:"capabilities"`
  Draining     bool     `json:"draining"`
  Headroom     *struct {
      MemFreeMiB  int64 `json:"mem_free_mib"`
      DiskFreeMiB int64 `json:"disk_free_mib"`
      Awake       int   `json:"awake"`
  } `json:"headroom"`
  ```

Headroom changes constantly, so it must not defeat the pass's "nothing changed, skip the write" gate. The gate compares a marshalled file with `Generation` blanked; this task blanks `Headroom` in the gate copy too. That alone would leave headroom stale for ever on a quiet box, because the loop's timed pass goes through the same gate. So the pass gains one input, `refresh`, true only on the timed pass: a file that carries headroom is then written even if nothing else changed. A box that publishes no headroom behaves exactly as before.

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

Append to `internal/host/edgesfile_test.go`:

```go
func TestEdgesFileBoxIDIsTheInstanceIDWhenThereIsOne(t *testing.T) {
	if got := edgesFileBoxID("i-0abc", "annie-welding"); got != "i-0abc" {
		t.Fatalf("on EC2: %q", got)
	}
	if got := edgesFileBoxID("", "annie-welding"); got != "annie-welding" {
		t.Fatalf("off EC2: %q", got)
	}
}

func TestEdgesFileCapabilityFollowsTheFlag(t *testing.T) {
	env := func(v string) func(string) string {
		return func(k string) string {
			if k == "CARLOS_REGION_HA" {
				return v
			}
			return ""
		}
	}
	if got := edgesFileCapabilities(env("")); len(got) != 0 {
		t.Fatalf("unset: %v", got)
	}
	if got := edgesFileCapabilities(env("1")); len(got) != 1 || got[0] != "region-ha-1" {
		t.Fatalf("set: %v", got)
	}
	if got := edgesFileCapabilities(env("0")); len(got) != 0 {
		t.Fatalf("\"0\" is off: %v", got)
	}
}

func TestEdgesFileRawWireBodyCarriesBoxEvidence(t *testing.T) {
	s, reg := release.NewDirStore(t.TempDir()), edgesTestRegistry(t)
	seedRoute(t, reg, "app.example.com")
	var last []byte
	ident := edgesFile{Box: "annie", BoxID: "i-0abc", Capabilities: []string{"region-ha-1"},
		Headroom: &edgeHeadroom{MemFreeMiB: 2210, DiskFreeMiB: 31200, Awake: 14}}
	if err := edgesFilePass(context.Background(), s, reg, ident, "1.2.3.4", &last); err != nil {
		t.Fatal(err)
	}
	body, _, err := s.Get(context.Background(), "control/edges/annie.json")
	if err != nil {
		t.Fatal(err)
	}
	var raw map[string]json.RawMessage
	if err := json.Unmarshal(body, &raw); err != nil {
		t.Fatal(err)
	}
	for key, want := range map[string]string{
		"box_id":       `"i-0abc"`,
		"capabilities": `["region-ha-1"]`,
		"headroom":     `{"mem_free_mib":2210,"disk_free_mib":31200,"awake":14}`,
	} {
		if string(raw[key]) != want {
			t.Errorf("%s = %s, want %s", key, raw[key], want)
		}
	}
	if _, ok := raw["draining"]; ok {
		t.Error("draining:false must be omitted")
	}
}

func TestEdgesFileOmitsBoxEvidenceWhenAbsent(t *testing.T) {
	s, reg := release.NewDirStore(t.TempDir()), edgesTestRegistry(t)
	var last []byte
	if err := edgesFilePass(context.Background(), s, reg, edgesFile{Box: "annie"}, "1.2.3.4", &last); err != nil {
		t.Fatal(err)
	}
	body, _, _ := s.Get(context.Background(), "control/edges/annie.json")
	for _, key := range []string{"box_id", "capabilities", "draining", "headroom"} {
		if bytes.Contains(body, []byte(`"`+key+`"`)) {
			t.Errorf("%s present in %s", key, body)
		}
	}
}

func TestEdgesFileHeadroomAloneDoesNotRewriteTheFile(t *testing.T) {
	st := &putCountingStore{Store: release.NewDirStore(t.TempDir())}
	reg := edgesTestRegistry(t)
	seedRoute(t, reg, "app.example.com")
	var last []byte
	ident := edgesFile{Box: "annie", Headroom: &edgeHeadroom{MemFreeMiB: 100}}
	if err := edgesFilePass(context.Background(), st, reg, ident, "1.2.3.4", &last); err != nil {
		t.Fatal(err)
	}
	ident.Headroom = &edgeHeadroom{MemFreeMiB: 99}
	if err := edgesFilePass(context.Background(), st, reg, ident, "1.2.3.4", &last); err != nil {
		t.Fatal(err)
	}
	if st.puts != 1 {
		t.Fatalf("puts = %d, want 1: a headroom change alone is not a reason to write", st.puts)
	}
}

// The loop's timed pass refreshes headroom even when nothing else moved.
func TestEdgesFileTimedPassRefreshesHeadroom(t *testing.T) {
	st := &putCountingStore{Store: release.NewDirStore(t.TempDir())}
	reg := edgesTestRegistry(t)
	seedRoute(t, reg, "app.example.com")
	var last []byte
	ident := edgesFile{Box: "annie", Headroom: &edgeHeadroom{MemFreeMiB: 100}}
	if err := edgesFileWrite(context.Background(), st, reg, ident, "1.2.3.4", &last, false); err != nil {
		t.Fatal(err)
	}
	ident.Headroom = &edgeHeadroom{MemFreeMiB: 42}
	if err := edgesFileWrite(context.Background(), st, reg, ident, "1.2.3.4", &last, true); err != nil {
		t.Fatal(err)
	}
	if st.puts != 2 {
		t.Fatalf("puts = %d, want 2: the timed pass must publish the new reading", st.puts)
	}
	body, _, err := st.Get(context.Background(), "control/edges/annie.json")
	if err != nil {
		t.Fatal(err)
	}
	var f edgesFile
	if err := json.Unmarshal(body, &f); err != nil || f.Headroom == nil || f.Headroom.MemFreeMiB != 42 {
		t.Fatalf("published headroom = %+v, err %v", f.Headroom, err)
	}
	// A box that publishes no headroom keeps today's behaviour exactly:
	// a timed pass with nothing changed writes nothing.
	plain := &putCountingStore{Store: release.NewDirStore(t.TempDir())}
	var plainLast []byte
	for i := 0; i < 2; i++ {
		if err := edgesFileWrite(context.Background(), plain, reg, edgesFile{Box: "betty"}, "1.2.3.4", &plainLast, true); err != nil {
			t.Fatal(err)
		}
	}
	if plain.puts != 1 {
		t.Fatalf("puts = %d, want 1 for a box with no headroom", plain.puts)
	}
}

// A restart must not rewrite an unchanged file just because the stored
// copy carries a headroom reading.
func TestEdgesFileSeededGateIgnoresStoredHeadroom(t *testing.T) {
	st := &putCountingStore{Store: release.NewDirStore(t.TempDir())}
	reg := edgesTestRegistry(t)
	seedRoute(t, reg, "app.example.com")
	var last []byte
	ident := edgesFile{Box: "annie", Headroom: &edgeHeadroom{MemFreeMiB: 100}}
	if err := edgesFilePass(context.Background(), st, reg, ident, "1.2.3.4", &last); err != nil {
		t.Fatal(err)
	}
	seeded := seedEdgesFileGate(context.Background(), st, "annie")
	ident.Headroom = &edgeHeadroom{MemFreeMiB: 7}
	if err := edgesFilePass(context.Background(), st, reg, ident, "1.2.3.4", &seeded); err != nil {
		t.Fatal(err)
	}
	if st.puts != 1 {
		t.Fatalf("puts = %d, want 1", st.puts)
	}
}
```

Create `internal/host/headroom_test.go`:

```go
package host

import "testing"

func TestParseMemAvailable(t *testing.T) {
	const meminfo = "MemTotal:       16000000 kB\nMemFree:          500000 kB\nMemAvailable:    2263040 kB\n"
	if got := parseMemAvailableMiB(meminfo); got != 2210 {
		t.Fatalf("got %d, want 2210", got)
	}
	if got := parseMemAvailableMiB("MemTotal: 1 kB\n"); got != 0 {
		t.Fatalf("no MemAvailable line: got %d, want 0", got)
	}
	if got := parseMemAvailableMiB("MemAvailable: lots kB\n"); got != 0 {
		t.Fatalf("unparseable: got %d, want 0", got)
	}
}
```

Append to `internal/console/regioncandidates_test.go`:

```go
func TestEdgesViewParsesBoxEvidenceAndToleratesItsAbsence(t *testing.T) {
	ctx, s := context.Background(), release.NewDirStore(t.TempDir())
	with := `{"v":1,"box":"i-0abc","eip":"1.2.3.4","region":"eu-west-2","placement":"uk","hosts":{},"box_id":"i-0abc","capabilities":["region-ha-1"],"draining":true,"headroom":{"mem_free_mib":2210,"disk_free_mib":31200,"awake":14}}`
	without := `{"v":1,"box":"old-box","eip":"5.6.7.8","region":"eu-west-1","hosts":{}}`
	for k, b := range map[string]string{"i-0abc": with, "old-box": without} {
		if _, err := s.Put(ctx, "control/edges/"+k+".json", []byte(b), ""); err != nil {
			t.Fatal(err)
		}
	}
	snap, err := readEdgesSnapshot(ctx, s)
	if err != nil || !snap.Complete {
		t.Fatalf("snap complete=%v err=%v", snap.Complete, err)
	}
	by := map[string]edgesFilePlacementView{}
	for _, f := range snap.Files {
		by[f.Box] = f
	}
	n := by["i-0abc"]
	if n.BoxID != "i-0abc" || len(n.Capabilities) != 1 || !n.Draining || n.Headroom == nil || n.Headroom.Awake != 14 {
		t.Fatalf("new box parsed as %+v", n)
	}
	o := by["old-box"]
	if o.BoxID != "" || o.Capabilities != nil || o.Draining || o.Headroom != nil {
		t.Fatalf("old box parsed as %+v", o)
	}
}
```

- [ ] **Step 2: Run them to see them fail**

Run: `go test ./internal/host/ -run 'EdgesFile|ParseMemAvailable' -count=1`
Expected: FAIL to build, `undefined: edgesFileBoxID`.

- [ ] **Step 3: Add the fields and helpers to `internal/host/edgesfile.go`**

Add the import (the file does not have it):

```go
	"github.com/carlosframework/platform/internal/status"
```

After `SecretsKeyFP` in `edgesFile`:

```go
	// BoxID is the name claims use for this box (region HA spec 1 §2): the
	// EC2 instance id, or the label off EC2. It is NOT evidence of identity:
	// that is the box record (internal/boxid). A reader compares the two and
	// treats a mismatch as an ineligible box.
	BoxID string `json:"box_id,omitempty"`
	// Capabilities gates multi-box regions (spec §8): "region-ha-1" once
	// CARLOS_REGION_HA is on.
	Capabilities []string `json:"capabilities,omitempty"`
	// Draining is set while the placer empties this box (spec 4).
	Draining bool `json:"draining,omitempty"`
	// Headroom is what placement chooses by (spec 2). It is refreshed when
	// the file is written for another reason and on the loop's own tick; a
	// headroom change alone never forces a write (see edgesFilePass).
	Headroom *edgeHeadroom `json:"headroom,omitempty"`
```

Above `edgesFile`'s doc comment:

```go
// edgeHeadroom is this box's spare capacity at the time the file was
// written. Zero values mean "could not read", never "none left": a chooser
// ranks an unreadable box last, it does not exclude it.
type edgeHeadroom struct {
	MemFreeMiB  int64 `json:"mem_free_mib"`
	DiskFreeMiB int64 `json:"disk_free_mib"`
	Awake       int   `json:"awake"`
}

const capRegionHA1 = "region-ha-1"

// edgesFileBoxID is the instance id on EC2 and the label everywhere else.
func edgesFileBoxID(instanceID, label string) string {
	if instanceID != "" {
		return instanceID
	}
	return label
}

// edgesFileCapabilities publishes region-ha-1 only when CARLOS_REGION_HA is
// set to something other than "" or "0".
func edgesFileCapabilities(getenv func(string) string) []string {
	switch strings.TrimSpace(getenv("CARLOS_REGION_HA")) {
	case "", "0":
		return nil
	}
	return []string{capRegionHA1}
}

// awakeCount is how many routes this box has running now. An error reads
// as zero: headroom is advisory.
func awakeCount(reg *registry.Registry) int {
	routes, err := reg.List()
	if err != nil {
		return 0
	}
	n := 0
	for _, r := range routes {
		if r.Status == "ready" && !r.Hibernate {
			n++
		}
	}
	return n
}
```

`Hibernate` true means the route sleeps between requests, so a ready route with `Hibernate` false is always running. This under-counts hibernating routes that happen to be awake. That is acceptable here: the field is advisory and spec 2 decides what the chooser really needs.

Split the pass in two. Keep `edgesFilePass`'s signature, so none of its many callers in tests change, and move its body into `edgesFileWrite`:

```go
func edgesFilePass(ctx context.Context, s release.Store, reg *registry.Registry, ident edgesFile, eip string, last *[]byte) error {
	return edgesFileWrite(ctx, s, reg, ident, eip, last, false)
}

// edgesFileWrite is edgesFilePass with one more input. refresh is true on
// the loop's own timed pass: the file is then written even if nothing but
// its headroom reading changed, so headroom is never more than one timer
// period stale. On every other pass a headroom change alone is not a
// reason to write.
func edgesFileWrite(ctx context.Context, s release.Store, reg *registry.Registry, ident edgesFile, eip string, last *[]byte, refresh bool) error {
	// ... the existing body of edgesFilePass, unchanged except for the gate below
```

In that body, replace the gate:

```go
	f := ident
	f.V, f.EIP, f.Hosts, f.Generation = 1, eip, hosts, ""
	gate, err := json.Marshal(f)
	if err != nil {
		return fmt.Errorf("edges: marshal: %w", err)
	}
	if bytes.Equal(gate, *last) {
		return nil
	}
```

with:

```go
	f := ident
	f.V, f.EIP, f.Hosts, f.Generation = 1, eip, hosts, ""
	gateCopy := f
	gateCopy.Headroom = nil // a changing reading is not, by itself, a reason to write
	gate, err := json.Marshal(gateCopy)
	if err != nil {
		return fmt.Errorf("edges: marshal: %w", err)
	}
	if bytes.Equal(gate, *last) && !(refresh && f.Headroom != nil) {
		return nil
	}
```

Leave the rest as it is: `body` is still marshalled from `f`, which keeps its headroom, and `*last = gate` still stores the headroom-free gate.

In `seedEdgesFileGate`, beside the line that clears `Generation`:

```go
	prior.Generation = ""
	prior.Headroom = nil // the gate never includes headroom: see edgesFileWrite
```

In `edgesFileLoop`, add two fields to the `ident` literal and read the data directory once after it:

```go
		SecretsKeyFP:          edgesFileSecretsFP(os.Getenv),
		BoxID:                 edgesFileBoxID(status.InstanceID(), box),
		Capabilities:          edgesFileCapabilities(os.Getenv),
	}
	dataDir := provisionTickConfig(os.Getenv).DataDir
```

and replace the loop's one call, `err = edgesFilePass(ctx, store, reg, ident, eip, &last)`, with:

```go
		ident.Headroom = readHeadroom(dataDir, awakeCount(reg))
		err = edgesFileWrite(ctx, store, reg, ident, eip, &last, sched.timed)
```

`sched.timed` is the schedule's own flag for "this pass is the timer's" (`edgesFileSchedule`, set in `due`). The loop already reads it two lines above to decide whether to re-read the public IP.

- [ ] **Step 4: Write the headroom readers**

`internal/host/headroom_linux.go`:

```go
//go:build linux

package host

import (
	"os"
	"strconv"
	"strings"

	"golang.org/x/sys/unix"
)

// readHeadroom reads spare memory from /proc/meminfo and spare disk from
// the data directory's filesystem. A reading that fails is left at zero.
func readHeadroom(dataDir string, awake int) *edgeHeadroom {
	h := &edgeHeadroom{Awake: awake}
	if b, err := os.ReadFile("/proc/meminfo"); err == nil {
		h.MemFreeMiB = parseMemAvailableMiB(string(b))
	}
	var st unix.Statfs_t
	if err := unix.Statfs(dataDir, &st); err == nil {
		h.DiskFreeMiB = int64(st.Bavail) * int64(st.Bsize) / (1 << 20)
	}
	return h
}

// parseMemAvailableMiB returns the MemAvailable line in MiB, or 0.
func parseMemAvailableMiB(meminfo string) int64 {
	for _, line := range strings.Split(meminfo, "\n") {
		rest, ok := strings.CutPrefix(line, "MemAvailable:")
		if !ok {
			continue
		}
		fields := strings.Fields(rest)
		if len(fields) == 0 {
			return 0
		}
		kb, err := strconv.ParseInt(fields[0], 10, 64)
		if err != nil {
			return 0
		}
		return kb / 1024
	}
	return 0
}
```

`internal/host/headroom_other.go`:

```go
//go:build !linux

package host

import (
	"strconv"
	"strings"
)

// readHeadroom has no readings off Linux: only the awake count.
func readHeadroom(dataDir string, awake int) *edgeHeadroom { return &edgeHeadroom{Awake: awake} }

func parseMemAvailableMiB(meminfo string) int64 {
	for _, line := range strings.Split(meminfo, "\n") {
		rest, ok := strings.CutPrefix(line, "MemAvailable:")
		if !ok {
			continue
		}
		fields := strings.Fields(rest)
		if len(fields) == 0 {
			return 0
		}
		kb, err := strconv.ParseInt(fields[0], 10, 64)
		if err != nil {
			return 0
		}
		return kb / 1024
	}
	return 0
}
```

The parser is duplicated so `headroom_test.go` builds on every platform without a third file. Keep the two bodies identical.

- [ ] **Step 5: Add the fields to the console's view**

In `internal/console/instancesplacement.go`, after `SecretsKeyFP` in `edgesFilePlacementView`:

```go
	// Box evidence (region HA spec 1 §5.1). All absent on a writer that
	// predates it: BoxID "", Capabilities nil, Draining false, Headroom nil.
	BoxID        string   `json:"box_id"`
	Capabilities []string `json:"capabilities"`
	Draining     bool     `json:"draining"`
	Headroom     *struct {
		MemFreeMiB  int64 `json:"mem_free_mib"`
		DiskFreeMiB int64 `json:"disk_free_mib"`
		Awake       int   `json:"awake"`
	} `json:"headroom"`
```

Run `gofmt -w internal/console/instancesplacement.go`: the new fields change the struct's column alignment, and the gate checks formatting.

- [ ] **Step 6: Check the other parsers**

Run: `grep -rn "control/edges\|edgesFileView\|edgesFilePrefix" --include=*.go internal/edgesfile internal/steering internal/edge | grep -v _test`

For each struct that unmarshals an edges file, confirm it ignores unknown fields (Go's default) and has no `DisallowUnknownFields`. Run `grep -rn "DisallowUnknownFields" internal/edgesfile internal/steering internal/edge internal/console`. Expected: no hit on an edges-file decoder. If there is one, add the four fields to that struct in this task.

- [ ] **Step 7: Run the tests**

Run: `go test ./internal/host/ ./internal/console/ ./internal/edge/ ./internal/edgesfile/ -count=1 && GOOS=darwin go build ./... && GOOS=windows go build ./...`
Expected: PASS, and both cross-builds succeed.

- [ ] **Step 8: Commit**

```bash
git add internal/host internal/console
git commit -m "edges: a box publishes its id, the HA capability, draining and headroom

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

---

### Task 3: Which boxes are eligible, and which one to choose (`internal/console/boxchoice.go`)

**Files:**
- Create: `internal/console/boxchoice.go`
- Test: `internal/console/boxchoice_test.go`

**Interfaces:**
- Consumes: `edgesSnapshot`, `edgesFilePlacementView` (with Task 2's fields), `isTrue`, `provision.ValidRegion`, `boxid.Identity`.
- Produces:
  ```go
  type boxCandidate struct {
      BoxID     string
      Region    string // AWS region, from the verified box record
      MemFreeMiB, DiskFreeMiB int64
      Awake     int
  }
  // verified maps box id to its verified identity; a box absent from it is never eligible.
  // awsRegion is the AWS region the caller expects for this placement; "" offers nothing.
  // lost is the set of box ids a claim has quarantined.
  func eligibleBoxes(snap edgesSnapshot, verified map[string]boxid.Identity, placement, awsRegion, homeName, expectedFP string, lost map[string]bool) []boxCandidate
  // running is, per box id, how many hosts claims place there and which apps ("acct/app") they belong to.
  type boxLoad struct {
      Hosts int
      Apps  map[string]int
  }
  func chooseBox(cands []boxCandidate, running map[string]boxLoad, app string) (boxCandidate, bool)
  ```
  `placement` is the instance's region code (`""` for home). Pools are not an input yet: nearest-region's predicate covers the shared pool only (`serves_shared_pool`), and pool-aware candidates are its stated non-goal. Plan 1b decides pooled records.

The eligibility predicate is nearest-region's, applied to one box, plus spec 1's additions: a verified record whose instance matches `box_id`, the capability, not draining, not lost.

**The region is checked against the signature, not the file.** A box's placement code (`uk`) is its own configuration. Its AWS region is in its signed identity. So the caller passes the AWS region it expects for the placement, and a box is eligible only if its signed region and its file's `region` both equal it. Plan 1b decides where the expected region comes from (nearest-region's `regionCandidate.AWSRegion` is a majority vote over the same unsigned files, which is not enough on its own).

**`chooseBox` follows spec §5.2 exactly:** a box that runs no instance of the app is preferred; among boxes equal on that, the fewest claimed hosts; then the lowest box id. It compares whether a box runs the app at all, not how many of it.

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

`internal/console/boxchoice_test.go`:

```go
package console

import (
	"encoding/json"
	"testing"

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

// haBox is an eligible box's edges file in placement "uk".
func haBox(id string, mut func(m map[string]any)) string {
	m := map[string]any{
		"v": 1, "box": id, "box_id": id, "eip": "1.2.3.4", "region": "eu-west-2", "placement": "uk",
		"hosts": map[string]any{}, "hibernation_configured": true, "serves_shared_pool": true,
		"accepts_inbound": true, "secrets_key_fp": testKeyFP, "capabilities": []string{"region-ha-1"},
		"headroom": map[string]any{"mem_free_mib": 1000, "disk_free_mib": 1000, "awake": 1},
	}
	if mut != nil {
		mut(m)
	}
	b, _ := json.Marshal(m)
	return string(b)
}

func ids(cs []boxCandidate) []string {
	out := make([]string, len(cs))
	for i, c := range cs {
		out[i] = c.BoxID
	}
	return out
}

func verifiedFor(boxes ...string) map[string]boxid.Identity {
	out := map[string]boxid.Identity{}
	for _, b := range boxes {
		out[b] = boxid.Identity{AccountID: "1", Region: "eu-west-2", InstanceID: b}
	}
	return out
}

func TestEligibleBoxesAppliesThePredicateOneBoxAtATime(t *testing.T) {
	del := func(k string) func(map[string]any) { return func(m map[string]any) { delete(m, k) } }
	set := func(k string, v any) func(map[string]any) { return func(m map[string]any) { m[k] = v } }
	cases := []struct {
		name string
		mut  func(map[string]any)
		want bool
	}{
		{"all present", nil, true},
		{"no dial address", set("eip", ""), false},
		{"hibernation unconfigured", set("hibernation_configured", false), false},
		{"hibernation unknown", del("hibernation_configured"), false},
		{"refuses the shared pool", set("serves_shared_pool", false), false},
		{"refuses inbound", set("accepts_inbound", false), false},
		{"wrong secrets key", set("secrets_key_fp", "sha256:other"), false},
		{"no capability", del("capabilities"), false},
		{"draining", set("draining", true), false},
		{"another placement", set("placement", "us"), false},
		{"box_id disagrees with its record", set("box_id", "i-0zzz"), false},
	}
	for _, tc := range cases {
		t.Run(tc.name, func(t *testing.T) {
			snap := views(t, haBox("i-0aaa", tc.mut))
			got := eligibleBoxes(snap, verifiedFor("i-0aaa"), "uk", "eu-west-2", "ie", testKeyFP, nil)
			if (len(got) == 1) != tc.want {
				t.Fatalf("eligible = %v, want %v", ids(got), tc.want)
			}
		})
	}
}

func TestEligibleBoxesNeedsAVerifiedRecordFreshnessAndNotLost(t *testing.T) {
	snap := views(t, haBox("i-0aaa", nil), haBox("i-0bbb", nil), haBox("i-0ccc", nil))
	snap = markStale(snap, "i-0bbb")
	got := eligibleBoxes(snap, verifiedFor("i-0aaa", "i-0bbb"), "uk", "eu-west-2", "ie", testKeyFP, nil)
	if len(got) != 1 || got[0].BoxID != "i-0aaa" {
		t.Fatalf("got %v, want only i-0aaa (i-0bbb is stale, i-0ccc has no verified record)", ids(got))
	}
	got = eligibleBoxes(snap, verifiedFor("i-0aaa"), "uk", "eu-west-2", "ie", testKeyFP, map[string]bool{"i-0aaa": true})
	if len(got) != 0 {
		t.Fatalf("a lost box is eligible: %v", ids(got))
	}
}

// TestEligibilityNeverCombinesBoxes is nearest-region's rule for boxes: one
// box holding the key and another accepting inbound do not add up to one.
func TestEligibleBoxesNeverCombinesTwoHalfBoxes(t *testing.T) {
	snap := views(t,
		haBox("i-0aaa", func(m map[string]any) { m["secrets_key_fp"] = "" }),
		haBox("i-0bbb", func(m map[string]any) { m["accepts_inbound"] = false }))
	if got := eligibleBoxes(snap, verifiedFor("i-0aaa", "i-0bbb"), "uk", "eu-west-2", "ie", testKeyFP, nil); len(got) != 0 {
		t.Fatalf("got %v, want none", ids(got))
	}
}

func TestEligibleBoxesAnIncompleteSnapshotOffersNothing(t *testing.T) {
	snap := views(t, haBox("i-0aaa", nil))
	snap.Complete = false
	if got := eligibleBoxes(snap, verifiedFor("i-0aaa"), "uk", "eu-west-2", "ie", testKeyFP, nil); len(got) != 0 {
		t.Fatalf("got %v from an incomplete snapshot", ids(got))
	}
}

func TestEligibleBoxesHomePlacementMatchesTheEmptyCode(t *testing.T) {
	home := haBox("i-0aaa", func(m map[string]any) { m["placement"] = "" })
	named := haBox("i-0bbb", func(m map[string]any) { m["placement"] = "ie" })
	snap := views(t, home, named)
	got := eligibleBoxes(snap, verifiedFor("i-0aaa", "i-0bbb"), "", "eu-west-2", "ie", testKeyFP, nil)
	if len(got) != 2 {
		t.Fatalf("got %v, want both home boxes", ids(got))
	}
}

func TestChooseBox(t *testing.T) {
	a, b, c := boxCandidate{BoxID: "i-0aaa"}, boxCandidate{BoxID: "i-0bbb"}, boxCandidate{BoxID: "i-0ccc"}
	load := func(hosts int, apps ...string) boxLoad {
		l := boxLoad{Hosts: hosts, Apps: map[string]int{}}
		for _, x := range apps {
			l.Apps[x]++
		}
		return l
	}
	if _, ok := chooseBox(nil, nil, "bab/jam"); ok {
		t.Fatal("chose from nothing")
	}
	got, _ := chooseBox([]boxCandidate{a, b}, map[string]boxLoad{"i-0aaa": load(5), "i-0bbb": load(2)}, "bab/jam")
	if got.BoxID != "i-0bbb" {
		t.Fatalf("fewest hosts: got %s", got.BoxID)
	}
	got, _ = chooseBox([]boxCandidate{a, b}, map[string]boxLoad{"i-0aaa": load(5), "i-0bbb": load(2, "bab/jam")}, "bab/jam")
	if got.BoxID != "i-0aaa" {
		t.Fatalf("spread beats load: got %s", got.BoxID)
	}
	got, ok := chooseBox([]boxCandidate{a, b}, map[string]boxLoad{"i-0aaa": load(5, "bab/jam"), "i-0bbb": load(2, "bab/jam", "bab/jam")}, "bab/jam")
	if !ok || got.BoxID != "i-0bbb" {
		t.Fatalf("every box runs the app: fewest hosts overall, never a refusal; got %s ok=%v", got.BoxID, ok)
	}
	got, _ = chooseBox([]boxCandidate{c, b, a}, nil, "bab/jam")
	if got.BoxID != "i-0aaa" {
		t.Fatalf("a tie breaks by box id: got %s", got.BoxID)
	}
}

// A box whose SIGNED region is not the placement's AWS region is not in
// that placement, even when its own file says it is and both regions have
// certificates.
func TestEligibleBoxesRefusesABoxSignedInAnotherRegion(t *testing.T) {
	snap := views(t, haBox("i-0aaa", nil), haBox("i-0bbb", nil))
	verified := verifiedFor("i-0aaa", "i-0bbb")
	verified["i-0bbb"] = boxid.Identity{AccountID: "1", Region: "us-east-1", InstanceID: "i-0bbb"}
	got := eligibleBoxes(snap, verified, "uk", "eu-west-2", "ie", testKeyFP, nil)
	if len(got) != 1 || got[0].BoxID != "i-0aaa" {
		t.Fatalf("got %v, want only i-0aaa", ids(got))
	}
	// And a file that advertises another AWS region than the one expected.
	snap = views(t, haBox("i-0aaa", func(m map[string]any) { m["region"] = "us-east-1" }))
	if got := eligibleBoxes(snap, verifiedFor("i-0aaa"), "uk", "eu-west-2", "ie", testKeyFP, nil); len(got) != 0 {
		t.Fatalf("got %v, want none", ids(got))
	}
	if got := eligibleBoxes(views(t, haBox("i-0aaa", nil)), verifiedFor("i-0aaa"), "uk", "", "ie", testKeyFP, nil); len(got) != 0 {
		t.Fatalf("got %v with no expected region", ids(got))
	}
}
```

- [ ] **Step 2: Run them to see them fail**

Run: `go test ./internal/console/ -run 'EligibleBoxes|ChooseBox' -count=1`
Expected: FAIL to build, `undefined: eligibleBoxes`.

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

```go
package console

import (
	"slices"
	"sort"

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

// boxCandidate is one box that may be named as a host's executor (region HA
// spec 1 §2, "eligible box").
type boxCandidate struct {
	BoxID                   string
	Region                  string
	MemFreeMiB, DiskFreeMiB int64
	Awake                   int
}

// eligibleBoxes is regionCandidates' predicate applied to one box at a time,
// plus what a multi-box region adds: a verified box record that agrees with
// the file's box_id and whose signed region is awsRegion (the AWS region
// the caller expects for this placement), the HA capability, not draining,
// not lost. Two boxes
// that each satisfy half of it never add up to one.
//
// An incomplete snapshot offers nothing: an unread file could be the one
// box that would change the answer.
func eligibleBoxes(snap edgesSnapshot, verified map[string]boxid.Identity, placement, awsRegion, homeName, expectedFP string, lost map[string]bool) []boxCandidate {
	if !snap.complete() || expectedFP == "" || awsRegion == "" {
		return nil
	}
	var out []boxCandidate
	for _, f := range snap.Files {
		code := f.Placement
		if code == "" || (homeName != "" && code == homeName) {
			code = ""
		}
		want := placement
		if homeName != "" && want == homeName {
			want = ""
		}
		id, ok := verified[f.BoxID]
		switch {
		case code != want:
		case !f.Fresh, f.EIP == "", f.BoxID == "":
		case !ok || id.InstanceID == "":
		case id.Region != awsRegion, f.Region != awsRegion:
			// The placement code is the box's own word. The AWS region is
			// signed: a box whose identity is in another region is not in
			// this placement, whatever its file says.
		case !isTrue(f.HibernationConfigured), !isTrue(f.ServesSharedPool), !isTrue(f.AcceptsInbound):
		case f.SecretsKeyFP != expectedFP:
		case !slices.Contains(f.Capabilities, "region-ha-1"):
		case f.Draining, lost[f.BoxID]:
		default:
			c := boxCandidate{BoxID: f.BoxID, Region: id.Region}
			if f.Headroom != nil {
				c.MemFreeMiB, c.DiskFreeMiB, c.Awake = f.Headroom.MemFreeMiB, f.Headroom.DiskFreeMiB, f.Headroom.Awake
			}
			out = append(out, c)
		}
	}
	sort.Slice(out, func(i, j int) bool { return out[i].BoxID < out[j].BoxID })
	return out
}

// boxLoad is what claims already place on a box.
type boxLoad struct {
	Hosts int
	Apps  map[string]int // "acct/app" -> hosts of that app
}

// chooseBox is spec 1 §5.2's interim rule until spec 2: prefer a box that
// runs no instance of this app (soft spread: it never refuses), then the
// fewest hosts overall, then the lowest box id.
func chooseBox(cands []boxCandidate, running map[string]boxLoad, app string) (boxCandidate, bool) {
	if len(cands) == 0 {
		return boxCandidate{}, false
	}
	best := cands[0]
	for _, c := range cands[1:] {
		cl, bl := running[c.BoxID], running[best.BoxID]
		cRuns, bRuns := cl.Apps[app] > 0, bl.Apps[app] > 0
		switch {
		case cRuns != bRuns:
			if !cRuns {
				best = c
			}
		case cl.Hosts != bl.Hosts:
			if cl.Hosts < bl.Hosts {
				best = c
			}
		case c.BoxID < best.BoxID:
			best = c
		}
	}
	return best, true
}
```

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

Run: `go test ./internal/console/ -run 'EligibleBoxes|ChooseBox' -count=1`
Expected: PASS.

If "box_id disagrees with its record" fails: the test changes the file's `box_id` to `i-0zzz` while `verified` holds only `i-0aaa`, so the lookup misses. That is the intended path; check `verified[f.BoxID]` is keyed by the file's `box_id`, not its `box`.

- [ ] **Step 5: Commit**

```bash
git add internal/console/boxchoice.go internal/console/boxchoice_test.go
git commit -m "console: eligible boxes, one at a time, and a choice that spreads an app

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

---

### Task 4: The home map follows the newest installation

**Files:**
- Modify: `internal/host/edgesfile.go` (`edgeHost`, `:41-83`), `internal/edge/homemap.go` (`edgesHostView` `:139`, `homeEntry` `:161`, `foldHomes` `:538-602`)
- Test: `internal/edge/homemap_test.go`, `internal/host/edgesfile_test.go`

**Interfaces:**
- Produces: `edgeHost.Stamp int64 \`json:"stamp,omitempty"\`` (writer), `edgesHostView.Stamp int64 \`json:"stamp,omitempty"\`` (reader), `homeEntry.stamp int64`, and a changed signature: `foldHomes(files map[string]edgesFileView, self string) (map[string]homeEntry, []string)`, where the second result is the sorted list of hosts that two boxes publish as home at the same positive stamp, found over every file including this box's own (`tiedStamps`). Its one caller is `(*homeMap).Refresh`.
- Nothing sets `Stamp` yet: plan 1b sets it from the claim's `holder.stamp`. Until then every entry is unstamped and today's rule applies unchanged. That is what makes this task safe to land alone.

The rule (spec HA4, §5.4), applied when two files claim one host as a dial target:
1. A stamped entry beats an unstamped one.
2. Between two stamped entries, the higher stamp wins.
3. Between two unstamped entries, today's rule: a route beats an alias, any dial target beats none, otherwise the first box in sorted order.
4. Equal non-zero stamps cannot happen for two different boxes (a stamp is one claim revision naming one executor). Treat it as rule 3, and log it once per refresh: `foldHomes` returns the hosts it happened to.

**No alert for two unstamped entries.** Spec §5.4 asks for one, on the reasoning that it means the multi-box gate was bypassed. That is wrong for today's files: a static site or a public address route runs on every box, so every box publishes it as `home`, unstamped, and that is normal. The edge cannot tell those from relocatable hosts. Alerting here would fire for every static site on every refresh. This plan reports only equal positive stamps; Task 11 records the correction in the spec, and plan 1b decides whether the publisher should mark relocatable entries so the edge can alert on those alone.

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

Append to `internal/edge/homemap_test.go`:

```go
func TestHomeMapHigherStampWinsWhateverTheSortOrder(t *testing.T) {
	s := meshTestStore(t)
	// "annie" sorts first and is the dead box; "betty" holds the newer installation.
	putJSON(t, s, "control/edges/annie.json", `{"v":1,"box":"annie","eip":"1.1.1.1","hosts":{"shop.test":{"role":"home","stamp":41}}}`)
	putJSON(t, s, "control/edges/betty.json", `{"v":1,"box":"betty","eip":"2.2.2.2","hosts":{"shop.test":{"role":"home","stamp":57}}}`)
	m := newHomeMap(s, "carol")
	refreshHomeMap(t, m)
	if eip, box, ok := m.Home("shop.test"); !ok || box != "betty" || eip != "2.2.2.2" {
		t.Fatalf("Home = %q %q %v, want betty", eip, box, ok)
	}
	// And the other way round, so it is the stamp and not the name.
	putJSON(t, s, "control/edges/annie.json", `{"v":1,"box":"annie","eip":"1.1.1.1","hosts":{"shop.test":{"role":"home","stamp":90}}}`)
	refreshHomeMap(t, m)
	if _, box, _ := m.Home("shop.test"); box != "annie" {
		t.Fatalf("Home box = %q, want annie", box)
	}
}

func TestHomeMapStampedBeatsUnstamped(t *testing.T) {
	s := meshTestStore(t)
	putJSON(t, s, "control/edges/annie.json", `{"v":1,"box":"annie","eip":"1.1.1.1","hosts":{"shop.test":{"role":"home"}}}`)
	putJSON(t, s, "control/edges/betty.json", `{"v":1,"box":"betty","eip":"2.2.2.2","hosts":{"shop.test":{"role":"home","stamp":3}}}`)
	m := newHomeMap(s, "carol")
	refreshHomeMap(t, m)
	if _, box, _ := m.Home("shop.test"); box != "betty" {
		t.Fatalf("Home box = %q, want betty", box)
	}
}

func TestHomeMapUnstampedKeepsTodaysRule(t *testing.T) {
	s := meshTestStore(t)
	putJSON(t, s, "control/edges/annie.json", `{"v":1,"box":"annie","eip":"1.1.1.1","hosts":{"shop.test":{"role":"home","alias":true}}}`)
	putJSON(t, s, "control/edges/betty.json", `{"v":1,"box":"betty","eip":"2.2.2.2","hosts":{"shop.test":{"role":"home"}}}`)
	m := newHomeMap(s, "carol")
	refreshHomeMap(t, m)
	if _, box, _ := m.Home("shop.test"); box != "betty" {
		t.Fatalf("a route still beats an alias: got %q", box)
	}
	putJSON(t, s, "control/edges/annie.json", `{"v":1,"box":"annie","eip":"1.1.1.1","hosts":{"shop.test":{"role":"home"}}}`)
	refreshHomeMap(t, m)
	if _, box, _ := m.Home("shop.test"); box != "annie" {
		t.Fatalf("two plain routes: first box in sorted order; got %q", box)
	}
}

func TestHomeMapAStampedAliasDoesNotBeatAnUnstampedRoute(t *testing.T) {
	s := meshTestStore(t)
	putJSON(t, s, "control/edges/annie.json", `{"v":1,"box":"annie","eip":"1.1.1.1","hosts":{"shop.test":{"role":"home"}}}`)
	putJSON(t, s, "control/edges/betty.json", `{"v":1,"box":"betty","eip":"2.2.2.2","hosts":{"shop.test":{"role":"home","alias":true,"stamp":9}}}`)
	m := newHomeMap(s, "carol")
	refreshHomeMap(t, m)
	if _, box, _ := m.Home("shop.test"); box != "annie" {
		t.Fatalf("got %q: #287's rule (a route beats an alias) comes before stamps", box)
	}
}

func TestHomeMapAStampedEntryWithNoDialAddressDoesNotWin(t *testing.T) {
	s := meshTestStore(t)
	putJSON(t, s, "control/edges/annie.json", `{"v":1,"box":"annie","eip":"1.1.1.1","hosts":{"shop.test":{"role":"home","stamp":2}}}`)
	putJSON(t, s, "control/edges/betty.json", `{"v":1,"box":"betty","eip":"","hosts":{"shop.test":{"role":"home","stamp":99}}}`)
	m := newHomeMap(s, "carol")
	refreshHomeMap(t, m)
	if eip, box, ok := m.Home("shop.test"); !ok || box != "annie" || eip != "1.1.1.1" {
		t.Fatalf("Home = %q %q %v, want annie: a box nothing can dial is skipped today and stays skipped", eip, box, ok)
	}
}

func file(box, eip string, hosts map[string]edgesHostView) edgesFileView {
	return edgesFileView{V: 1, Box: box, EIP: eip, Hosts: hosts}
}

func TestHomeMapReportsTwoBoxesAtTheSameStamp(t *testing.T) {
	files := map[string]edgesFileView{
		"control/edges/annie.json": file("annie", "1.1.1.1", map[string]edgesHostView{
			"tied.test": {Role: "home", Stamp: 7}, "fine.test": {Role: "home", Stamp: 7}, "plain.test": {Role: "home"}}),
		"control/edges/betty.json": file("betty", "2.2.2.2", map[string]edgesHostView{
			"tied.test": {Role: "home", Stamp: 7}, "fine.test": {Role: "home", Stamp: 9}, "plain.test": {Role: "home"}}),
	}
	hosts, tied := foldHomes(files, "carol")
	if len(tied) != 1 || tied[0] != "tied.test" {
		t.Fatalf("tied = %v, want [tied.test]: unstamped duplicates are normal (static sites run on every box) and are not reported", tied)
	}
	if hosts["tied.test"].box != "annie" {
		t.Fatalf("a tie keeps the first box in sorted order; got %q", hosts["tied.test"].box)
	}
	if hosts["fine.test"].box != "betty" {
		t.Fatalf("fine.test box = %q", hosts["fine.test"].box)
	}
}

// A tie between two boxes is reported even when a third box holds a higher
// stamp and wins the dial: the report does not depend on the winner.
func TestHomeMapReportsATieBelowTheWinner(t *testing.T) {
	files := map[string]edgesFileView{
		"control/edges/annie.json": file("annie", "1.1.1.1", map[string]edgesHostView{"h.test": {Role: "home", Stamp: 9}}),
		"control/edges/betty.json": file("betty", "2.2.2.2", map[string]edgesHostView{"h.test": {Role: "home", Stamp: 7}}),
		"control/edges/carol.json": file("carol", "3.3.3.3", map[string]edgesHostView{"h.test": {Role: "home", Stamp: 7}}),
	}
	hosts, tied := foldHomes(files, "dave")
	if len(tied) != 1 || tied[0] != "h.test" {
		t.Fatalf("tied = %v, want [h.test]", tied)
	}
	if hosts["h.test"].box != "annie" {
		t.Fatalf("the highest stamp still wins the dial; got %q", hosts["h.test"].box)
	}
}

// In a two-box region each box excludes itself from routing. It must
// still see that it and its neighbour publish one host at one stamp.
func TestHomeMapReportsATieWithThisBoxItself(t *testing.T) {
	files := map[string]edgesFileView{
		"control/edges/annie.json": file("annie", "1.1.1.1", map[string]edgesHostView{"h.test": {Role: "home", Stamp: 7}}),
		"control/edges/betty.json": file("betty", "2.2.2.2", map[string]edgesHostView{"h.test": {Role: "home", Stamp: 7}}),
	}
	hosts, tied := foldHomes(files, "annie")
	if len(tied) != 1 || tied[0] != "h.test" {
		t.Fatalf("tied = %v, want [h.test]", tied)
	}
	if hosts["h.test"].box != "betty" {
		t.Fatalf("this box is still excluded from its own routing; got %q", hosts["h.test"].box)
	}
}
```

Append to `internal/host/edgesfile_test.go`:

```go
func TestEdgesFileStampIsOmittedWhenZero(t *testing.T) {
	b, _ := json.Marshal(edgeHost{Role: "home"})
	if bytes.Contains(b, []byte("stamp")) {
		t.Fatalf("zero stamp on the wire: %s", b)
	}
	b, _ = json.Marshal(edgeHost{Role: "home", Stamp: 41})
	if !bytes.Contains(b, []byte(`"stamp":41`)) {
		t.Fatalf("stamp missing: %s", b)
	}
}
```

- [ ] **Step 2: Run them to see them fail**

Run: `go test ./internal/edge/ -run 'HomeMap' -count=1`
Expected: FAIL to build (`TestHomeMapReportsTwoBoxesAtTheSameStamp` uses `Stamp` and `foldHomes`'s second result). Once the field exists and before the fold changes, `TestHomeMapHigherStampWinsWhateverTheSortOrder` and `TestHomeMapStampedBeatsUnstamped` fail (annie wins by sort order) and the other three stamp tests already pass. `go test ./internal/host/ -run StampIsOmitted` fails to build, `unknown field Stamp`.

- [ ] **Step 3: Add the field on both sides**

`internal/host/edgesfile.go`, in `edgeHost` after `App`:

```go
	// Stamp is the claim's holder.stamp for the installation this box is
	// serving (region HA spec 1 HA4). 0 = unstamped: no claim, or a writer
	// that predates it. Two files claiming one host are settled by the
	// higher stamp on the edge (homemap.go foldHomes).
	Stamp int64 `json:"stamp,omitempty"`
```

`internal/edge/homemap.go`, in `edgesHostView` after `App`:

```go
	// Stamp orders two boxes' claims on one host: see foldHomes.
	Stamp int64 `json:"stamp,omitempty"`
```

and in `homeEntry` after `account, app`:

```go
	// stamp is the dial winner's installation stamp, 0 when unstamped.
	stamp int64
```

- [ ] **Step 4: Change the fold**

`foldHomes` gains a second result. Change its signature:

```go
func foldHomes(files map[string]edgesFileView, self string) (map[string]homeEntry, []string) {
```

Set the stamp on the incoming entry. Change:

```go
			in := homeEntry{eip: f.EIP, box: f.Box, platform: h.Platform && !h.Alias && !h.Wildcard, alias: h.Alias, wildcard: h.Wildcard, dial: h.Role == edgeRoleHome,
				account: h.Account, app: h.App}
```

to:

```go
			in := homeEntry{eip: f.EIP, box: f.Box, platform: h.Platform && !h.Alias && !h.Wildcard, alias: h.Alias, wildcard: h.Wildcard, dial: h.Role == edgeRoleHome,
				account: h.Account, app: h.App, stamp: h.Stamp}
```

Replace the merge condition:

```go
			if in.dial && (!prev.dial || (prev.alias && !in.alias)) {
				merged.eip, merged.box, merged.alias, merged.dial = in.eip, in.box, in.alias, true
				merged.account, merged.app = in.account, in.app
			}
```

with:

```go
			if dialBeats(in, prev) {
				merged.eip, merged.box, merged.alias, merged.dial = in.eip, in.box, in.alias, true
				merged.account, merged.app, merged.stamp = in.account, in.app, in.stamp
			}
```

Replace the function's final `return hosts` with `return hosts, tiedStamps(files)`, and add below it:

```go
// tiedStamps lists, sorted, the hosts that two boxes publish as home at
// the same positive stamp. It looks at every file, this box's own
// included, and at every pair, not only at whichever entry won the dial:
// in a two-box region each box must be able to see that it and its
// neighbour disagree, and a third box's higher stamp must not hide a tie
// between two others.
//
// Unstamped duplicates are not reported. They are normal: a static site
// or a public address route runs on every box, and every box publishes it.
func tiedStamps(files map[string]edgesFileView) []string {
	type key struct {
		host  string
		stamp int64
	}
	boxes := map[key]map[string]bool{}
	for _, f := range files {
		if f.Box == "" {
			continue
		}
		for host, h := range f.Hosts {
			if host == "" || h.Role != edgeRoleHome || h.Stamp <= 0 {
				continue
			}
			k := key{host, h.Stamp}
			if boxes[k] == nil {
				boxes[k] = map[string]bool{}
			}
			boxes[k][f.Box] = true
		}
	}
	seen := map[string]bool{}
	var tied []string
	for k, set := range boxes {
		if len(set) > 1 && !seen[k.host] {
			seen[k.host] = true
			tied = append(tied, k.host)
		}
	}
	slices.Sort(tied)
	return tied
}

// dialBeats reports whether a later file's entry takes the dial from the
// one already folded. In order:
//
//   - only a dial target can take it, and any dial target beats none;
//   - a ROUTE beats an ALIAS whatever else is true (#287);
//   - between two of the same kind, the newer installation wins: a stamped
//     entry beats an unstamped one, and a higher stamp beats a lower one
//     (region HA spec 1 HA4). A dead box's file lingers; its successor's
//     stamp is always higher, so the stale one loses without any liveness
//     rule;
//   - otherwise the earlier file keeps it, which is the first box in
//     sorted order, as before.
func dialBeats(in, prev homeEntry) bool {
	if !in.dial {
		return false
	}
	if !prev.dial {
		return true
	}
	if prev.alias != in.alias {
		return prev.alias && !in.alias
	}
	return in.stamp > prev.stamp
}
```

Ties are found by `tiedStamps` in a pass of its own, over every file, rather than inside the fold. The fold skips this box's own file and compares each entry only with the current winner, so it cannot see a tie that involves this box, or one between two boxes that both lose to a third.

In `(*homeMap).Refresh`, replace the one call:

```go
	snap := &homeSnapshot{files: next, hosts: foldHomes(next, m.self)}
```

with:

```go
	hosts, tied := foldHomes(next, m.self)
	if len(tied) > 0 {
		// One claim revision names one executor, so two boxes publishing
		// one host at the same stamp is a fault upstream. The fold stays
		// deterministic; say so once a refresh.
		log.Printf("edge: home map: %d host(s) published by two boxes at the same installation stamp, e.g. %s", len(tied), tied[0])
	}
	snap := &homeSnapshot{files: next, hosts: hosts}
```

`homemap.go` already imports `log` and `slices`. An entry with no dial address never reaches `dialBeats` as a dial target: the existing `case edgeRoleHome: if f.EIP == "" { … continue }` skips it first. That is what `TestHomeMapAStampedEntryWithNoDialAddressDoesNotWin` pins.

- [ ] **Step 5: Run the tests**

Run: `go test ./internal/edge/ ./internal/host/ -count=1`
Expected: PASS, including every pre-existing home-map test.

- [ ] **Step 6: Commit**

```bash
git add internal/edge/homemap.go internal/edge/homemap_test.go internal/host/edgesfile.go internal/host/edgesfile_test.go
git commit -m "edge: two boxes claiming one host are settled by the newer installation's stamp

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

---
### Task 5: The ordered host-files manifest (`internal/hostfiles`)

**Files:**
- Create: `internal/hostfiles/manifest.go`, `internal/hostfiles/publisher.go`, `internal/hostfiles/restore.go`
- Test: `internal/hostfiles/hostfiles_test.go`

**Interfaces:**
- Consumes: `release.Store`, `release.ErrCASConflict`.
- Produces:
  ```go
  package hostfiles
  const MaxFileBytes = 64 << 20
  type Entry struct {
      Object  string `json:"object,omitempty"`  // key relative to the prefix
      SHA256  string `json:"sha256,omitempty"`
      Size    int64  `json:"size,omitempty"`
      Mode    uint32 `json:"mode,omitempty"`    // permission bits only
      Seq     int64  `json:"seq"`
      Deleted bool   `json:"deleted,omitempty"`
      Torn    bool   `json:"torn,omitempty"`    // the file changed while it was read
      Skipped string `json:"skipped,omitempty"` // "too-large": present on disk, not published
  }
  type Manifest struct {
      V        int              `json:"v"`
      Seq      int64            `json:"seq"`
      Entries  map[string]Entry `json:"entries"`  // files directory, by clean relative path
      Platform map[string]Entry `json:"platform"` // platform-managed items, by name; never written into the files directory
  }
  func ManifestKey(prefix string) string
  func CleanPath(p string) (string, error)
  var ErrSuperseded = errors.New("hostfiles: another writer published this manifest")
  type Publisher struct{ /* unexported */ }
  func Open(ctx context.Context, s release.Store, prefix string) (*Publisher, error)
  func (p *Publisher) Stage(ctx context.Context, path string, body []byte, mode uint32, torn bool) error
  func (p *Publisher) StageSkipped(path, why string) error
  func (p *Publisher) StageDelete(path string) error
  func (p *Publisher) StagePlatform(ctx context.Context, name string, body []byte) error
  func (p *Publisher) Commit(ctx context.Context) (seq int64, err error)
  func (p *Publisher) Dirty() bool
  func (p *Publisher) Seq() int64            // the last committed seq
  func (p *Publisher) Paths() []string       // live (not deleted) entry paths, sorted
  type RestoreStats struct{ Written, Removed int; Skipped []string }
  func Load(ctx context.Context, s release.Store, prefix string) (Manifest, bool, error)
  func Restore(ctx context.Context, s release.Store, prefix, dir string) (Manifest, RestoreStats, error)
  func ReadPlatform(ctx context.Context, s release.Store, prefix string, m Manifest, name string) ([]byte, error)
  ```
  `prefix` ends in `/`. Plan 1b supplies `<holder.location>/files/<generation>/`.

**How ordering works (spec §5.6).** Every staged change takes the next sequence number. Its bytes go to a new key, `objects/<seq, 16 digits>-<sha256>`, written create-only, so nothing is ever overwritten. Only the manifest says which object is a path's current content, and the manifest is replaced by compare-and-swap. An upload that stalls and lands late creates an object no manifest names. A second writer's commit fails with `ErrSuperseded` instead of overwriting.

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

`internal/hostfiles/hostfiles_test.go`:

```go
package hostfiles

import (
	"context"
	"errors"
	"os"
	"path/filepath"
	"reflect"
	"strings"
	"testing"

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

const pfx = "instances/shop.test/abc/files/g1/"

func open(t *testing.T, s release.Store) *Publisher {
	t.Helper()
	p, err := Open(context.Background(), s, pfx)
	if err != nil {
		t.Fatal(err)
	}
	return p
}

func stage(t *testing.T, p *Publisher, path, body string) {
	t.Helper()
	if err := p.Stage(context.Background(), path, []byte(body), 0o600, false); err != nil {
		t.Fatal(err)
	}
}

func commit(t *testing.T, p *Publisher) int64 {
	t.Helper()
	seq, err := p.Commit(context.Background())
	if err != nil {
		t.Fatal(err)
	}
	return seq
}

func read(t *testing.T, dir, rel string) string {
	t.Helper()
	b, err := os.ReadFile(filepath.Join(dir, rel))
	if err != nil {
		t.Fatal(err)
	}
	return string(b)
}

func TestPublishThenRestoreRoundTrips(t *testing.T) {
	ctx, s := context.Background(), release.NewDirStore(t.TempDir())
	p := open(t, s)
	stage(t, p, "instance.key", "k1")
	stage(t, p, "uploads/a/b.txt", "hello")
	if seq := commit(t, p); seq != 2 {
		t.Fatalf("seq = %d, want 2", seq)
	}
	dir := t.TempDir()
	m, st, err := Restore(ctx, s, pfx, dir)
	if err != nil {
		t.Fatal(err)
	}
	if m.Seq != 2 || st.Written != 2 {
		t.Fatalf("manifest seq %d, written %d", m.Seq, st.Written)
	}
	if read(t, dir, "instance.key") != "k1" || read(t, dir, "uploads/a/b.txt") != "hello" {
		t.Fatal("content differs")
	}
	fi, _ := os.Stat(filepath.Join(dir, "instance.key"))
	if fi.Mode().Perm() != 0o600 {
		t.Fatalf("mode = %v, want 0600", fi.Mode().Perm())
	}
}

func TestALaterWriteWinsAndAnUnchangedOneTakesNoSequence(t *testing.T) {
	ctx, s := context.Background(), release.NewDirStore(t.TempDir())
	p := open(t, s)
	stage(t, p, "instance.key", "old")
	commit(t, p)
	stage(t, p, "instance.key", "new")
	stage(t, p, "instance.key", "new") // unchanged: no new sequence number
	if seq := commit(t, p); seq != 2 {
		t.Fatalf("seq = %d, want 2", seq)
	}
	dir := t.TempDir()
	if _, _, err := Restore(ctx, s, pfx, dir); err != nil {
		t.Fatal(err)
	}
	if got := read(t, dir, "instance.key"); got != "new" {
		t.Fatalf("restored %q, want new", got)
	}
}

// A1-4: a stalled upload of the OLD content that lands after the new
// content was published must not come back.
func TestALateUploadCannotResurrectOldContent(t *testing.T) {
	ctx, s := context.Background(), release.NewDirStore(t.TempDir())
	p := open(t, s)
	stage(t, p, "instance.key", "new")
	commit(t, p)
	// The late arrival: any object at all under objects/, with a higher number.
	if _, err := s.Put(ctx, pfx+"objects/0000000000000099-deadbeef", []byte("old"), ""); err != nil {
		t.Fatal(err)
	}
	dir := t.TempDir()
	if _, _, err := Restore(ctx, s, pfx, dir); err != nil {
		t.Fatal(err)
	}
	if got := read(t, dir, "instance.key"); got != "new" {
		t.Fatalf("restored %q, want new", got)
	}
}

func TestADeleteIsATombstoneAndRestoreRemovesTheFile(t *testing.T) {
	ctx, s := context.Background(), release.NewDirStore(t.TempDir())
	p := open(t, s)
	stage(t, p, "a.txt", "a")
	stage(t, p, "b.txt", "b")
	commit(t, p)
	dir := t.TempDir()
	if _, _, err := Restore(ctx, s, pfx, dir); err != nil {
		t.Fatal(err)
	}
	if err := p.StageDelete("a.txt"); err != nil {
		t.Fatal(err)
	}
	commit(t, p)
	_, st, err := Restore(ctx, s, pfx, dir)
	if err != nil {
		t.Fatal(err)
	}
	if _, err := os.Stat(filepath.Join(dir, "a.txt")); !os.IsNotExist(err) {
		t.Fatalf("a.txt still there: %v", err)
	}
	if st.Removed != 1 || read(t, dir, "b.txt") != "b" {
		t.Fatalf("removed %d", st.Removed)
	}
	if got := p.Paths(); len(got) != 1 || got[0] != "b.txt" {
		t.Fatalf("Paths = %v", got)
	}
}

func TestASecondWriterIsSupersededNotMerged(t *testing.T) {
	s := release.NewDirStore(t.TempDir())
	a, b := open(t, s), open(t, s)
	stage(t, a, "x", "from a")
	commit(t, a)
	stage(t, b, "x", "from b")
	if _, err := b.Commit(context.Background()); !errors.Is(err, ErrSuperseded) {
		t.Fatalf("err = %v, want ErrSuperseded", err)
	}
	dir := t.TempDir()
	if _, _, err := Restore(context.Background(), s, pfx, dir); err != nil {
		t.Fatal(err)
	}
	if got := read(t, dir, "x"); got != "from a" {
		t.Fatalf("restored %q, want from a", got)
	}
}

func TestAReopenedPublisherContinuesTheSequence(t *testing.T) {
	s := release.NewDirStore(t.TempDir())
	p := open(t, s)
	stage(t, p, "x", "1")
	commit(t, p)
	q := open(t, s)
	stage(t, q, "y", "2")
	if seq := commit(t, q); seq != 2 {
		t.Fatalf("seq = %d, want 2", seq)
	}
}

// Review Focus 1.
func TestCleanPathRefusesEscapes(t *testing.T) {
	for _, bad := range []string{"", "/etc/passwd", "../x", "a/../../x", "a/../b", "./a", "a//b", "a/", ".", "..", "a\x00b"} {
		if _, err := CleanPath(bad); err == nil {
			t.Errorf("CleanPath(%q) accepted", bad)
		}
	}
	for _, ok := range []string{"a", "a/b.txt", ".hidden", "a b/c", `back\slash`} {
		if got, err := CleanPath(ok); err != nil || got != ok {
			t.Errorf("CleanPath(%q) = %q, %v", ok, got, err)
		}
	}
}

func TestStageAndRestoreRefuseAnEscapingPath(t *testing.T) {
	ctx, s := context.Background(), release.NewDirStore(t.TempDir())
	p := open(t, s)
	if err := p.Stage(ctx, "../evil", []byte("x"), 0o600, false); err == nil {
		t.Fatal("Stage accepted ../evil")
	}
	// A manifest written by something else with a hostile path.
	body := `{"v":1,"seq":1,"entries":{"../evil":{"object":"objects/0000000000000001-aa","sha256":"aa","seq":1}},"platform":{}}`
	hostile := "instances/h/i/files/g/"
	if _, err := s.Put(ctx, ManifestKey(hostile), []byte(body), ""); err != nil {
		t.Fatal(err)
	}
	parent := t.TempDir()
	dir := filepath.Join(parent, "files")
	if err := os.Mkdir(dir, 0o755); err != nil {
		t.Fatal(err)
	}
	if _, _, err := Restore(ctx, s, hostile, dir); err == nil {
		t.Fatal("Restore accepted a manifest with ../evil")
	}
	if _, err := os.Stat(filepath.Join(parent, "evil")); !os.IsNotExist(err) {
		t.Fatal("../evil was written")
	}
}

func TestRestoreRefusesContentThatDoesNotMatchItsHash(t *testing.T) {
	ctx, s := context.Background(), release.NewDirStore(t.TempDir())
	p := open(t, s)
	stage(t, p, "x", "good")
	commit(t, p)
	m, _, err := Load(ctx, s, pfx)
	if err != nil {
		t.Fatal(err)
	}
	key := pfx + m.Entries["x"].Object
	_, etag, _ := s.Get(ctx, key)
	if _, err := s.Put(ctx, key, []byte("tampered"), etag); err != nil {
		t.Fatal(err)
	}
	if _, _, err := Restore(ctx, s, pfx, t.TempDir()); err == nil || !strings.Contains(err.Error(), "sha256") {
		t.Fatalf("err = %v, want a sha256 mismatch", err)
	}
}

func TestAnAbsentManifestRestoresNothing(t *testing.T) {
	m, st, err := Restore(context.Background(), release.NewDirStore(t.TempDir()), pfx, t.TempDir())
	if err != nil || m.Seq != 0 || st.Written != 0 {
		t.Fatalf("m=%+v st=%+v err=%v", m, st, err)
	}
}

func TestPlatformItemsAreNotWrittenIntoTheFilesDirectory(t *testing.T) {
	ctx, s := context.Background(), release.NewDirStore(t.TempDir())
	p := open(t, s)
	if err := p.StagePlatform(ctx, "wake.json", []byte(`{"v":1}`)); err != nil {
		t.Fatal(err)
	}
	commit(t, p)
	dir := t.TempDir()
	m, _, err := Restore(ctx, s, pfx, dir)
	if err != nil {
		t.Fatal(err)
	}
	if entries, _ := os.ReadDir(dir); len(entries) != 0 {
		t.Fatalf("files directory has %d entries, want 0", len(entries))
	}
	got, err := ReadPlatform(ctx, s, pfx, m, "wake.json")
	if err != nil || string(got) != `{"v":1}` {
		t.Fatalf("ReadPlatform = %q, %v", got, err)
	}
	if _, err := ReadPlatform(ctx, s, pfx, m, "absent"); !errors.Is(err, os.ErrNotExist) {
		t.Fatalf("absent item: err = %v, want os.ErrNotExist", err)
	}
}

// Review Focus 3: a skipped file stays in the manifest, visibly.
func TestASkippedFileIsRecordedAndReportedOnRestore(t *testing.T) {
	ctx, s := context.Background(), release.NewDirStore(t.TempDir())
	p := open(t, s)
	if err := p.StageSkipped("huge.bin", "too-large"); err != nil {
		t.Fatal(err)
	}
	commit(t, p)
	_, st, err := Restore(ctx, s, pfx, t.TempDir())
	if err != nil {
		t.Fatal(err)
	}
	if len(st.Skipped) != 1 || st.Skipped[0] != "huge.bin" {
		t.Fatalf("Skipped = %v", st.Skipped)
	}
}

// A destination that already holds a symlink pointing elsewhere must not
// let a write or a delete reach through it.
func TestRestoreNeverWritesOrDeletesThroughASymlink(t *testing.T) {
	ctx, s := context.Background(), release.NewDirStore(t.TempDir())
	p := open(t, s)
	stage(t, p, "sub/x", "ours")
	stage(t, p, "gone/y", "temp")
	commit(t, p)
	if err := p.StageDelete("gone/y"); err != nil {
		t.Fatal(err)
	}
	commit(t, p)

	outside, dir := t.TempDir(), t.TempDir()
	if err := os.WriteFile(filepath.Join(outside, "y"), []byte("not ours"), 0o600); err != nil {
		t.Fatal(err)
	}
	for _, link := range []string{"sub", "gone"} {
		if err := os.Symlink(outside, filepath.Join(dir, link)); err != nil {
			t.Fatal(err)
		}
	}
	if _, _, err := Restore(ctx, s, pfx, dir); err != nil {
		t.Fatal(err)
	}
	if _, err := os.Stat(filepath.Join(outside, "x")); !os.IsNotExist(err) {
		t.Fatal("a file was written through the symlink")
	}
	if got := read(t, outside, "y"); got != "not ours" {
		t.Fatalf("a file outside was changed to %q", got)
	}
	if fi, err := os.Lstat(filepath.Join(dir, "sub")); err != nil || !fi.IsDir() {
		t.Fatalf("sub is not a real directory: %v %v", fi, err)
	}
	if got := read(t, dir, "sub/x"); got != "ours" {
		t.Fatalf("sub/x = %q", got)
	}
}

func TestRestoreHandlesADirectoryBecomingAFileAndBack(t *testing.T) {
	ctx, s := context.Background(), release.NewDirStore(t.TempDir())
	p := open(t, s)
	stage(t, p, "a/b", "child")
	commit(t, p)
	prior := t.TempDir() // a copy restored while "a" was a directory
	if _, _, err := Restore(ctx, s, pfx, prior); err != nil {
		t.Fatal(err)
	}

	// "a" becomes a file.
	if err := p.StageDelete("a/b"); err != nil {
		t.Fatal(err)
	}
	stage(t, p, "a", "now a file")
	commit(t, p)
	for name, dir := range map[string]string{"an empty destination": t.TempDir(), "a previously restored one": prior} {
		if _, _, err := Restore(ctx, s, pfx, dir); err != nil {
			t.Fatalf("%s: %v", name, err)
		}
		if got := read(t, dir, "a"); got != "now a file" {
			t.Fatalf("%s: a = %q", name, got)
		}
	}

	// And back: "a" becomes a directory again.
	if err := p.StageDelete("a"); err != nil {
		t.Fatal(err)
	}
	stage(t, p, "a/c", "child again")
	commit(t, p)
	for name, dir := range map[string]string{"an empty destination": t.TempDir(), "a previously restored one": prior} {
		if _, _, err := Restore(ctx, s, pfx, dir); err != nil {
			t.Fatalf("%s: %v", name, err)
		}
		if got := read(t, dir, "a/c"); got != "child again" {
			t.Fatalf("%s: a/c = %q", name, got)
		}
	}
}

func TestRestoreIntoAMissingDirectoryIsAnError(t *testing.T) {
	ctx, s := context.Background(), release.NewDirStore(t.TempDir())
	p := open(t, s)
	stage(t, p, "x", "x")
	commit(t, p)
	if _, _, err := Restore(ctx, s, pfx, filepath.Join(t.TempDir(), "absent")); err == nil {
		t.Fatal("restored into a directory that does not exist")
	}
}

// Plan round 2, finding 3: a restore that returns nil has made its names
// durable, and one that could not must say so.
func TestRestoreSyncsEveryDirectoryItChangedBeforeReturning(t *testing.T) {
	ctx, s := context.Background(), release.NewDirStore(t.TempDir())
	p := open(t, s)
	stage(t, p, "top.txt", "t")
	stage(t, p, "a/b/deep.txt", "d")
	stage(t, p, "gone/old.txt", "o")
	commit(t, p)
	dir := t.TempDir()
	if _, _, err := Restore(ctx, s, pfx, dir); err != nil {
		t.Fatal(err)
	}
	if err := p.StageDelete("gone/old.txt"); err != nil {
		t.Fatal(err)
	}
	commit(t, p)

	var synced []string
	orig := syncDir
	t.Cleanup(func() { syncDir = orig })
	syncDir = func(root *os.Root, rel string) error {
		// Everything must already be in place when the first sync runs.
		if len(synced) == 0 {
			if _, err := os.Stat(filepath.Join(dir, "gone", "old.txt")); !os.IsNotExist(err) {
				t.Error("a directory was synced before the tombstone was applied")
			}
		}
		synced = append(synced, rel)
		return orig(root, rel)
	}
	if _, _, err := Restore(ctx, s, pfx, dir); err != nil {
		t.Fatal(err)
	}
	want := []string{"a/b", "a", "gone", "."}
	if !reflect.DeepEqual(synced, want) {
		t.Fatalf("synced %v, want %v (deepest first, the root last)", synced, want)
	}

	syncDir = func(*os.Root, string) error { return errors.New("disk says no") }
	if _, _, err := Restore(ctx, s, pfx, dir); err == nil || !strings.Contains(err.Error(), "disk says no") {
		t.Fatalf("err = %v; a failed directory sync must fail the restore", err)
	}
}

// Plan round 3: a restore that failed at its directory sync has already
// removed the file. The retry finds nothing to remove, and must still
// make that removal durable before it reports success.
func TestARetriedRestoreStillSyncsADeletionAnEarlierAttemptMade(t *testing.T) {
	ctx, s := context.Background(), release.NewDirStore(t.TempDir())
	p := open(t, s)
	stage(t, p, "gone/old.txt", "o")
	commit(t, p)
	dir := t.TempDir()
	if _, _, err := Restore(ctx, s, pfx, dir); err != nil {
		t.Fatal(err)
	}
	if err := p.StageDelete("gone/old.txt"); err != nil {
		t.Fatal(err)
	}
	commit(t, p)

	orig := syncDir
	t.Cleanup(func() { syncDir = orig })
	syncDir = func(*os.Root, string) error { return errors.New("disk says no") }
	if _, _, err := Restore(ctx, s, pfx, dir); err == nil {
		t.Fatal("the first attempt must fail")
	}
	if _, err := os.Stat(filepath.Join(dir, "gone", "old.txt")); !os.IsNotExist(err) {
		t.Fatal("the first attempt should have removed the file before failing")
	}
	var synced []string
	syncDir = func(root *os.Root, rel string) error { synced = append(synced, rel); return orig(root, rel) }
	_, st, err := Restore(ctx, s, pfx, dir)
	if err != nil {
		t.Fatal(err)
	}
	if st.Removed != 0 {
		t.Fatalf("Removed = %d; the retry had nothing left to remove", st.Removed)
	}
	if want := []string{"gone", "."}; !reflect.DeepEqual(synced, want) {
		t.Fatalf("the retry synced %v, want %v", synced, want)
	}
}

// A deeper directory replaced by a file: the directories that vanish
// under the new file are not synced (they do not exist), and the restore
// still succeeds and syncs what survives.
func TestRestoreHandlesADeepDirectoryBecomingAFile(t *testing.T) {
	ctx, s := context.Background(), release.NewDirStore(t.TempDir())
	p := open(t, s)
	stage(t, p, "a/b/c", "leaf")
	commit(t, p)
	dir := t.TempDir()
	if _, _, err := Restore(ctx, s, pfx, dir); err != nil {
		t.Fatal(err)
	}
	if err := p.StageDelete("a/b/c"); err != nil {
		t.Fatal(err)
	}
	stage(t, p, "a", "now a file")
	commit(t, p)

	var synced []string
	orig := syncDir
	t.Cleanup(func() { syncDir = orig })
	syncDir = func(root *os.Root, rel string) error { synced = append(synced, rel); return orig(root, rel) }
	for name, d := range map[string]string{"a previously restored destination": dir, "an empty one": t.TempDir()} {
		synced = nil
		if _, _, err := Restore(ctx, s, pfx, d); err != nil {
			t.Fatalf("%s: %v", name, err)
		}
		if got := read(t, d, "a"); got != "now a file" {
			t.Fatalf("%s: a = %q", name, got)
		}
		if want := []string{"."}; !reflect.DeepEqual(synced, want) {
			t.Fatalf("%s: synced %v, want %v: a/b and a are gone, the root records it", name, synced, want)
		}
	}
}
```

- [ ] **Step 2: Run them to see them fail**

Run: `go test ./internal/hostfiles/ -count=1`
Expected: FAIL to build, `undefined: Open`.

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

```go
// Package hostfiles publishes an instance's files directory, and the small
// platform-managed items that travel with it, to object storage in an order
// that survives late and duplicate uploads (region HA spec 1 §5.6).
//
// Nothing is overwritten: each change is a new immutable object, and one
// manifest, replaced by compare-and-swap, says which object is current.
package hostfiles

import (
	"context"
	"encoding/json"
	"errors"
	"fmt"
	"os"
	"path"
	"strings"

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

// MaxFileBytes is the largest file published as one object. release.Store
// takes a whole body in memory, so a larger file is recorded as skipped
// rather than read. Plan 1b lifts this with a streaming put.
const MaxFileBytes = 64 << 20

type Entry struct {
	Object  string `json:"object,omitempty"`
	SHA256  string `json:"sha256,omitempty"`
	Size    int64  `json:"size,omitempty"`
	Mode    uint32 `json:"mode,omitempty"`
	Seq     int64  `json:"seq"`
	Deleted bool   `json:"deleted,omitempty"`
	Torn    bool   `json:"torn,omitempty"`
	Skipped string `json:"skipped,omitempty"`
}

type Manifest struct {
	V        int              `json:"v"`
	Seq      int64            `json:"seq"`
	Entries  map[string]Entry `json:"entries"`
	Platform map[string]Entry `json:"platform"`
}

func ManifestKey(prefix string) string { return prefix + "manifest.json" }

func objectKey(seq int64, sha string) string { return fmt.Sprintf("objects/%016d-%s", seq, sha) }

// CleanPath accepts only a relative, already-clean, slash-separated path
// that stays inside its root. It never repairs a path: a manifest is
// written by this package, so anything else is damage or an attack.
func CleanPath(p string) (string, error) {
	if p == "" || strings.ContainsRune(p, 0) || strings.HasPrefix(p, "/") {
		return "", fmt.Errorf("hostfiles: path %q is not a relative path", p)
	}
	c := path.Clean(p)
	if c != p || c == "." || c == ".." || strings.HasPrefix(c, "../") {
		return "", fmt.Errorf("hostfiles: path %q is not clean or leaves its root", p)
	}
	return c, nil
}

// Load reads the manifest. The bool is false when there is none yet.
func Load(ctx context.Context, s release.Store, prefix string) (Manifest, bool, error) {
	m, _, found, err := load(ctx, s, prefix)
	return m, found, err
}

func load(ctx context.Context, s release.Store, prefix string) (Manifest, string, bool, error) {
	empty := Manifest{V: 1, Entries: map[string]Entry{}, Platform: map[string]Entry{}}
	raw, etag, err := s.Get(ctx, ManifestKey(prefix))
	if errors.Is(err, os.ErrNotExist) {
		return empty, "", false, nil
	}
	if err != nil {
		return Manifest{}, "", false, err
	}
	var m Manifest
	if err := json.Unmarshal(raw, &m); err != nil {
		return Manifest{}, "", false, fmt.Errorf("hostfiles: manifest %s: %w", prefix, err)
	}
	if m.V != 1 {
		return Manifest{}, "", false, fmt.Errorf("hostfiles: manifest %s is version %d; this binary reads 1", prefix, m.V)
	}
	if m.Entries == nil {
		m.Entries = map[string]Entry{}
	}
	if m.Platform == nil {
		m.Platform = map[string]Entry{}
	}
	return m, etag, true, nil
}
```

- [ ] **Step 4: Write `publisher.go`**

```go
package hostfiles

import (
	"context"
	"crypto/sha256"
	"encoding/hex"
	"encoding/json"
	"errors"
	"fmt"
	"sort"
	"strings"
	"sync"

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

// ErrSuperseded means another writer replaced the manifest. The caller
// stops publishing: it is no longer this host's executor, or two
// publishers were started for one host.
var ErrSuperseded = errors.New("hostfiles: another writer published this manifest")

// Publisher stages changes and commits them as one manifest revision. It
// is safe for concurrent use.
type Publisher struct {
	s      release.Store
	prefix string

	mu    sync.Mutex
	man   Manifest
	etag  string
	next  int64 // the last sequence number handed out; man.Seq is the last committed
	dirty bool
}

func Open(ctx context.Context, s release.Store, prefix string) (*Publisher, error) {
	if !strings.HasSuffix(prefix, "/") {
		return nil, fmt.Errorf("hostfiles: prefix %q must end in /", prefix)
	}
	m, etag, _, err := load(ctx, s, prefix)
	if err != nil {
		return nil, err
	}
	return &Publisher{s: s, prefix: prefix, man: m, etag: etag, next: m.Seq}, nil
}

func sum(body []byte) string {
	h := sha256.Sum256(body)
	return hex.EncodeToString(h[:])
}

// put writes body as the next immutable object and returns its entry.
// Callers hold p.mu.
func (p *Publisher) put(ctx context.Context, body []byte, mode uint32, torn bool) (Entry, error) {
	sha := sum(body)
	seq := p.next + 1
	key := objectKey(seq, sha)
	if _, err := p.s.Put(ctx, p.prefix+key, body, ""); err != nil && !errors.Is(err, release.ErrCASConflict) {
		// A conflict means this exact key exists: the same sequence number
		// and the same hash, left by an attempt that crashed before its
		// commit. The bytes are the ones we were about to write.
		return Entry{}, err
	}
	p.next = seq
	return Entry{Object: key, SHA256: sha, Size: int64(len(body)), Mode: mode & 0o777, Seq: seq, Torn: torn}, nil
}

// Stage records path's content. Content identical to what is already
// staged or committed takes no sequence number and writes nothing.
func (p *Publisher) Stage(ctx context.Context, path string, body []byte, mode uint32, torn bool) error {
	clean, err := CleanPath(path)
	if err != nil {
		return err
	}
	p.mu.Lock()
	defer p.mu.Unlock()
	if cur, ok := p.man.Entries[clean]; ok && !cur.Deleted && cur.Skipped == "" &&
		cur.SHA256 == sum(body) && cur.Mode == mode&0o777 && cur.Torn == torn {
		return nil
	}
	e, err := p.put(ctx, body, mode, torn)
	if err != nil {
		return err
	}
	p.man.Entries[clean], p.dirty = e, true
	return nil
}

// StageSkipped records that path exists but was not published, and why.
func (p *Publisher) StageSkipped(path, why string) error {
	clean, err := CleanPath(path)
	if err != nil {
		return err
	}
	p.mu.Lock()
	defer p.mu.Unlock()
	if cur, ok := p.man.Entries[clean]; ok && cur.Skipped == why {
		return nil
	}
	p.next++
	p.man.Entries[clean], p.dirty = Entry{Seq: p.next, Skipped: why}, true
	return nil
}

// StageDelete records that path is gone. Deleting a path the manifest never
// had, or already has as deleted, is a no-op.
func (p *Publisher) StageDelete(path string) error {
	clean, err := CleanPath(path)
	if err != nil {
		return err
	}
	p.mu.Lock()
	defer p.mu.Unlock()
	if cur, ok := p.man.Entries[clean]; !ok || cur.Deleted {
		return nil
	}
	p.next++
	p.man.Entries[clean], p.dirty = Entry{Seq: p.next, Deleted: true}, true
	return nil
}

// StagePlatform records a platform-managed item (wake state, an env file).
// It shares the manifest's ordering but is never written into the files
// directory by Restore.
func (p *Publisher) StagePlatform(ctx context.Context, name string, body []byte) error {
	if name == "" || strings.ContainsAny(name, "/\x00") {
		return fmt.Errorf("hostfiles: platform item %q is not a plain name", name)
	}
	p.mu.Lock()
	defer p.mu.Unlock()
	if cur, ok := p.man.Platform[name]; ok && cur.SHA256 == sum(body) {
		return nil
	}
	e, err := p.put(ctx, body, 0o600, false)
	if err != nil {
		return err
	}
	p.man.Platform[name], p.dirty = e, true
	return nil
}

// Commit publishes everything staged as one manifest revision and returns
// its sequence number. With nothing staged it returns the current one. A
// failed commit leaves the staged changes in place, so the caller retries;
// ErrSuperseded is final.
func (p *Publisher) Commit(ctx context.Context) (int64, error) {
	p.mu.Lock()
	defer p.mu.Unlock()
	if !p.dirty {
		return p.man.Seq, nil
	}
	out := p.man
	out.Seq = p.next
	body, err := json.Marshal(out)
	if err != nil {
		return 0, fmt.Errorf("hostfiles: marshal manifest: %w", err)
	}
	etag, err := p.s.Put(ctx, ManifestKey(p.prefix), body, p.etag)
	if errors.Is(err, release.ErrCASConflict) {
		return 0, fmt.Errorf("%w: %s", ErrSuperseded, p.prefix)
	}
	if err != nil {
		return 0, err
	}
	p.man.Seq, p.etag, p.dirty = out.Seq, etag, false
	return out.Seq, nil
}

func (p *Publisher) Dirty() bool { p.mu.Lock(); defer p.mu.Unlock(); return p.dirty }

func (p *Publisher) Seq() int64 { p.mu.Lock(); defer p.mu.Unlock(); return p.man.Seq }

// Paths lists every path the manifest holds as present: published or
// skipped, not deleted.
func (p *Publisher) Paths() []string {
	p.mu.Lock()
	defer p.mu.Unlock()
	var out []string
	for path, e := range p.man.Entries {
		if !e.Deleted {
			out = append(out, path)
		}
	}
	sort.Strings(out)
	return out
}

// pending is the last sequence number handed out, committed or not.
func (p *Publisher) pending() int64 {
	p.mu.Lock()
	defer p.mu.Unlock()
	return p.next
}
```

`gofmt` will expand the three one-line methods; let it.

- [ ] **Step 5: Write `restore.go`**

```go
package hostfiles

import (
	"context"
	"crypto/rand"
	"encoding/hex"
	"errors"
	"fmt"
	"io/fs"
	"os"
	"path"
	"path/filepath"
	"sort"
	"strings"

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

// syncDir makes a directory's entries durable. A test seam.
var syncDir = func(root *os.Root, rel string) error {
	d, err := root.Open(filepath.FromSlash(rel))
	if err != nil {
		return err
	}
	defer d.Close()
	return d.Sync()
}

type RestoreStats struct {
	Written, Removed int
	Skipped          []string // paths the manifest holds as present but unpublished
}

// Restore makes dir match the manifest under prefix: it removes every
// tombstoned entry, writes every published one, and reports the skipped
// ones. It reads nothing but the manifest and the objects the manifest
// names, and verifies each object against the hash the manifest records.
//
// Every path is validated before anything is touched, so a manifest with
// one bad path changes nothing. Every file operation goes through an
// os.Root opened on dir, so nothing is written or removed outside it even
// if dir already holds a symlink pointing elsewhere.
//
// When Restore returns nil, what it did is durable: each file was synced
// before its rename, and every directory in which a name was created,
// replaced or removed has been synced since. A caller may record the
// installation as complete on that basis, and on no weaker one.
func Restore(ctx context.Context, s release.Store, prefix, dir string) (Manifest, RestoreStats, error) {
	var st RestoreStats
	m, _, _, err := load(ctx, s, prefix)
	if err != nil {
		return Manifest{}, st, err
	}
	var live, dead []string
	for p, e := range m.Entries {
		if _, err := CleanPath(p); err != nil {
			return Manifest{}, st, err
		}
		if e.Deleted {
			dead = append(dead, p)
		} else {
			live = append(live, p)
		}
	}
	if len(live)+len(dead) == 0 {
		return m, st, nil
	}
	root, err := os.OpenRoot(dir)
	if err != nil {
		return Manifest{}, st, fmt.Errorf("hostfiles: restore into %s: %w", dir, err)
	}
	defer root.Close()

	// Tombstones first, deepest first, so a directory that became a file
	// is emptied before the file is written over it.
	// Every directory on the way to a path the manifest speaks of, live or
	// tombstoned, is a directory whose entries this restore may have
	// changed: now, or in an earlier attempt that removed or created the
	// name and then failed before its sync. So the set does not depend on
	// what this call happened to find already done.
	touched := map[string]bool{".": true}
	mark := func(p string) {
		for d := path.Dir(p); d != "."; d = path.Dir(d) {
			touched[d] = true
		}
	}
	sort.Slice(dead, func(i, j int) bool {
		di, dj := strings.Count(dead[i], "/"), strings.Count(dead[j], "/")
		if di != dj {
			return di > dj
		}
		return dead[i] < dead[j]
	})
	for _, p := range dead {
		mark(p)
		removed, err := removeInside(root, p)
		if err != nil {
			return Manifest{}, st, fmt.Errorf("hostfiles: remove %s: %w", p, err)
		}
		if removed {
			st.Removed++
		}
	}
	sort.Strings(live)
	for _, p := range live {
		e := m.Entries[p]
		if e.Skipped != "" {
			st.Skipped = append(st.Skipped, p)
			continue
		}
		mark(p)
		body, err := fetch(ctx, s, prefix, e)
		if err != nil {
			return Manifest{}, st, fmt.Errorf("hostfiles: %s: %w", p, err)
		}
		if err := writeInside(root, p, body, os.FileMode(e.Mode)); err != nil {
			return Manifest{}, st, fmt.Errorf("hostfiles: write %s: %w", p, err)
		}
		st.Written++
	}
	// Sync the directories that survive, deepest first and the root last,
	// so a directory is durable before the entry that names it is. One
	// that no longer exists as a real directory (it was emptied and then
	// replaced by a file, or it sat under a path that became a file) has
	// nothing to sync: the directory above it records its going.
	dirs := make([]string, 0, len(touched))
	for d := range touched {
		dirs = append(dirs, d)
	}
	sort.Slice(dirs, func(i, j int) bool {
		if (dirs[i] == ".") != (dirs[j] == ".") {
			return dirs[j] == "."
		}
		di, dj := strings.Count(dirs[i], "/"), strings.Count(dirs[j], "/")
		if di != dj {
			return di > dj
		}
		return dirs[i] < dirs[j]
	})
	for _, d := range dirs {
		if d != "." {
			if ok, err := realParents(root, d+"/x"); err != nil {
				return Manifest{}, st, fmt.Errorf("hostfiles: sync directory %s: %w", d, err)
			} else if !ok {
				continue
			}
		}
		if err := syncDir(root, d); err != nil {
			return Manifest{}, st, fmt.Errorf("hostfiles: sync directory %s: %w", d, err)
		}
	}
	return m, st, nil
}

// ReadPlatform returns a platform-managed item's bytes, verified. A name
// the manifest does not hold is os.ErrNotExist.
func ReadPlatform(ctx context.Context, s release.Store, prefix string, m Manifest, name string) ([]byte, error) {
	e, ok := m.Platform[name]
	if !ok {
		return nil, fmt.Errorf("hostfiles: platform item %q: %w", name, os.ErrNotExist)
	}
	return fetch(ctx, s, prefix, e)
}

func fetch(ctx context.Context, s release.Store, prefix string, e Entry) ([]byte, error) {
	body, _, err := s.Get(ctx, prefix+e.Object)
	if err != nil {
		return nil, err
	}
	if got := sum(body); got != e.SHA256 {
		return nil, fmt.Errorf("object %s has sha256 %s, the manifest says %s", e.Object, got, e.SHA256)
	}
	return body, nil
}

// realParents reports whether every ancestor of rel inside root is a real
// directory: not a symlink, not a file, not missing.
func realParents(root *os.Root, rel string) (bool, error) {
	parts := strings.Split(rel, "/")
	cur := ""
	for _, part := range parts[:len(parts)-1] {
		cur = path.Join(cur, part)
		fi, err := root.Lstat(filepath.FromSlash(cur))
		if errors.Is(err, fs.ErrNotExist) {
			return false, nil
		}
		if err != nil {
			return false, err
		}
		if !fi.IsDir() {
			return false, nil
		}
	}
	return true, nil
}

// removeInside removes rel if it is a file of ours. A path whose ancestor
// is a symlink, a file or missing is not ours to remove and is left alone:
// that is how a tombstone for "a/b" behaves once "a" has become a file,
// and why a symlinked ancestor never lets a delete reach outside root.
func removeInside(root *os.Root, rel string) (bool, error) {
	ok, err := realParents(root, rel)
	if err != nil || !ok {
		return false, err
	}
	name := filepath.FromSlash(rel)
	fi, err := root.Lstat(name)
	if errors.Is(err, fs.ErrNotExist) {
		return false, nil
	}
	if err != nil {
		return false, err
	}
	if fi.IsDir() {
		// The path is a directory now: a later entry lives under it, and
		// the tombstone is for a file that no longer exists.
		return false, nil
	}
	if err := root.Remove(name); err != nil && !errors.Is(err, fs.ErrNotExist) {
		return false, err
	}
	return true, nil
}

// writeInside writes rel atomically inside root. Anything in the way that
// is not a real directory (a file where a directory must be, a symlink)
// is removed first, and a directory where the file must be is removed
// with its contents: the manifest is the truth about this tree.
func writeInside(root *os.Root, rel string, body []byte, mode os.FileMode) error {
	if mode == 0 {
		mode = 0o600
	}
	parent := path.Dir(rel)
	if parent != "." {
		cur := ""
		for _, part := range strings.Split(parent, "/") {
			cur = path.Join(cur, part)
			fi, err := root.Lstat(filepath.FromSlash(cur))
			if errors.Is(err, fs.ErrNotExist) {
				break
			}
			if err != nil {
				return err
			}
			if !fi.IsDir() {
				if err := root.Remove(filepath.FromSlash(cur)); err != nil {
					return err
				}
				break
			}
		}
		if err := root.MkdirAll(filepath.FromSlash(parent), 0o755); err != nil {
			return err
		}
	}
	name := filepath.FromSlash(rel)
	if fi, err := root.Lstat(name); err == nil && fi.IsDir() {
		if err := root.RemoveAll(name); err != nil {
			return err
		}
	}
	var rnd [8]byte
	if _, err := rand.Read(rnd[:]); err != nil {
		return err
	}
	tmp := filepath.FromSlash(path.Join(parent, ".carlos-restore-"+hex.EncodeToString(rnd[:])))
	f, err := root.OpenFile(tmp, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0o600)
	if err != nil {
		return err
	}
	done := false
	defer func() {
		if !done {
			f.Close()
			root.Remove(tmp)
		}
	}()
	if _, err := f.Write(body); err != nil {
		return err
	}
	if err := f.Chmod(mode); err != nil {
		return err
	}
	if err := f.Sync(); err != nil {
		return err
	}
	if err := f.Close(); err != nil {
		return err
	}
	if err := root.Rename(tmp, name); err != nil {
		return err
	}
	done = true
	return nil
}
```

- [ ] **Step 6: Run the tests**

Run: `gofmt -w internal/hostfiles && go test ./internal/hostfiles/ -count=1`
Expected: PASS.

`TestRestoreRefusesContentThatDoesNotMatchItsHash` overwrites an object through the test store with its etag. That is a test-only tamper. In production the publisher only ever creates objects; the bucket policy that makes storage refuse anything else is plan 1b's, because the files prefix depends on the claim's location layout, and it must be in place before the publisher is switched on for any host.

`Restore` does every file operation through an `os.Root` opened on the destination (Go 1.24+), so a symlink already sitting in the destination cannot carry a write or a delete outside it. The destination must exist.

**A restore that returns nil is durable.** Each file is synced before its rename, and before returning `Restore` syncs every surviving directory on the way to any path the manifest speaks of, live or tombstoned, deepest first and the root last, and fails if any of those syncs fails. The set comes from the manifest, not from what this call happened to change: an earlier attempt may have removed a file and then failed before its sync, and the retry, finding nothing to remove, must still make that removal durable. A directory that no longer exists as a real directory (it sat under a path that became a file) is skipped; the directory above it records its going. Plan 1b writes the installation-complete marker (Task 10) only after `Restore` returns nil; without the directory syncs that marker could outlive the names it vouches for across a crash. `TestRestoreSyncsEveryDirectoryItChangedBeforeReturning` pins the set, the order and the failure; `TestARetriedRestoreStillSyncsADeletionAnEarlierAttemptMade` the retry; `TestRestoreHandlesADeepDirectoryBecomingAFile` the `a/b/c` to `a` case.

- [ ] **Step 7: Commit**

```bash
git add internal/hostfiles
git commit -m "hostfiles: immutable objects and one manifest decide which file content is current

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

---

### Task 6: Publishing a whole directory (`hostfiles.SyncDir`)

**Files:**
- Create: `internal/hostfiles/sync.go`, `internal/hostfiles/dir_unix.go`, `internal/hostfiles/walk_unix.go`, `internal/hostfiles/dir_other.go`
- Test: `internal/hostfiles/sync_test.go`, `internal/hostfiles/sync_unix_test.go` (both `//go:build unix`)

**Interfaces:**
- Consumes: Task 5's `Publisher`.
- Produces:
  ```go
  type SyncStats struct {
      Published, Deleted   int
      TooLarge, NotRegular []string // relative paths
  }
  // SyncDir stages every difference between dir and the manifest: changed
  // and new regular files, and a tombstone for each manifest path no longer
  // on disk. It does not commit. skip, when non-nil, excludes a relative
  // path (and, for a directory, everything under it).
  func SyncDir(ctx context.Context, p *Publisher, dir string, skip func(rel string, isDir bool) bool) (SyncStats, error)
  // A directory held open by descriptor; nothing beneath it is reached through a symlink.
  type dirHandle struct{ /* unexported */ }
  func openTree(path string) (*dirHandle, error)
  func (d *dirHandle) descend(rel string) (sub *dirHandle, gone bool, err error)
  func (d *dirHandle) Close() error
  // readStable reads the file name inside d for publishing.
  func readStable(d *dirHandle, name string) (body []byte, mode uint32, state fileState, err error)
  func syncRoot(ctx context.Context, p *Publisher, root *dirHandle, skip func(rel string, isDir bool) bool) (SyncStats, error)
  func skipped(skip func(string, bool) bool, rel string, isDir bool) bool // rel or any directory above it is excluded
  // fileState gains: fileUnreadable (exists, could not be read) and fileRetry (replaced mid-look).
  // SyncStats gains: Unreadable []string.
  ```

This one function serves three callers in plan 1b: the `snapshot` setting (run every `files_snapshot_interval`), the continuous publisher's ten-minute repair scan, and the release sequence's "publish every pending mutation" step. That is why this plan has no separate sealed-archive snapshot: a periodic full `SyncDir` gives the `snapshot` setting the same result through the same manifest (recorded as a spec note in Task 11).

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

`internal/hostfiles/sync_test.go`:

```go
//go:build unix

package hostfiles

import (
	"context"
	"io"
	"os"
	"path/filepath"
	"reflect"
	"testing"

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

func write(t *testing.T, dir, rel, body string) {
	t.Helper()
	full := filepath.Join(dir, rel)
	if err := os.MkdirAll(filepath.Dir(full), 0o755); err != nil {
		t.Fatal(err)
	}
	if err := os.WriteFile(full, []byte(body), 0o640); err != nil {
		t.Fatal(err)
	}
}

func TestSyncDirPublishesNewChangedAndDeleted(t *testing.T) {
	ctx, s, src := context.Background(), release.NewDirStore(t.TempDir()), t.TempDir()
	p := open(t, s)
	write(t, src, "instance.key", "k")
	write(t, src, "uploads/a.txt", "a")
	st, err := SyncDir(ctx, p, src, nil)
	if err != nil || st.Published != 2 {
		t.Fatalf("first sync: %+v, %v", st, err)
	}
	commit(t, p)

	write(t, src, "uploads/a.txt", "a2")
	if err := os.Remove(filepath.Join(src, "instance.key")); err != nil {
		t.Fatal(err)
	}
	write(t, src, "new.txt", "n")
	st, err = SyncDir(ctx, p, src, nil)
	if err != nil || st.Published != 2 || st.Deleted != 1 {
		t.Fatalf("second sync: %+v, %v", st, err)
	}
	commit(t, p)

	dst := t.TempDir()
	if _, _, err := Restore(ctx, s, pfx, dst); err != nil {
		t.Fatal(err)
	}
	if read(t, dst, "uploads/a.txt") != "a2" || read(t, dst, "new.txt") != "n" {
		t.Fatal("content differs")
	}
	if _, err := os.Stat(filepath.Join(dst, "instance.key")); !os.IsNotExist(err) {
		t.Fatal("deleted file restored")
	}
	fi, _ := os.Stat(filepath.Join(dst, "new.txt"))
	if fi.Mode().Perm() != 0o640 {
		t.Fatalf("mode = %v, want 0640", fi.Mode().Perm())
	}
}

func TestSyncDirOfAnUnchangedDirectoryStagesNothing(t *testing.T) {
	ctx, s, src := context.Background(), release.NewDirStore(t.TempDir()), t.TempDir()
	p := open(t, s)
	write(t, src, "a", "a")
	if _, err := SyncDir(ctx, p, src, nil); err != nil {
		t.Fatal(err)
	}
	commit(t, p)
	st, err := SyncDir(ctx, p, src, nil)
	if err != nil || st.Published != 0 || st.Deleted != 0 || p.Dirty() {
		t.Fatalf("second sync: %+v dirty=%v err=%v", st, p.Dirty(), err)
	}
}

// Review Focus 2: never follow or open what is not a regular file.
func TestSyncDirSkipsWhatIsNotARegularFile(t *testing.T) {
	ctx, s, src := context.Background(), release.NewDirStore(t.TempDir()), t.TempDir()
	outside := filepath.Join(t.TempDir(), "secret")
	if err := os.WriteFile(outside, []byte("do not publish"), 0o600); err != nil {
		t.Fatal(err)
	}
	write(t, src, "real.txt", "r")
	if err := os.Symlink(outside, filepath.Join(src, "link")); err != nil {
		t.Fatal(err)
	}
	if err := os.Symlink(filepath.Dir(outside), filepath.Join(src, "linkdir")); err != nil {
		t.Fatal(err)
	}
	p := open(t, s)
	st, err := SyncDir(ctx, p, src, nil)
	if err != nil {
		t.Fatal(err)
	}
	if st.Published != 1 || !reflect.DeepEqual(st.NotRegular, []string{"link", "linkdir"}) {
		t.Fatalf("stats = %+v", st)
	}
	if got := p.Paths(); !reflect.DeepEqual(got, []string{"real.txt"}) {
		t.Fatalf("Paths = %v", got)
	}
}

// Review Focus 3: too large is recorded, never read whole, never dropped.
func TestSyncDirRecordsAFileOverTheCapWithoutReadingIt(t *testing.T) {
	ctx, s, src := context.Background(), release.NewDirStore(t.TempDir()), t.TempDir()
	big := filepath.Join(src, "huge.bin")
	f, err := os.Create(big)
	if err != nil {
		t.Fatal(err)
	}
	if err := f.Truncate(MaxFileBytes + 1); err != nil { // sparse: costs no disk
		t.Fatal(err)
	}
	f.Close()
	reads := 0
	orig := readAll
	readAll = func(r io.Reader) ([]byte, error) { reads++; return orig(r) }
	t.Cleanup(func() { readAll = orig })
	p := open(t, s)
	st, err := SyncDir(ctx, p, src, nil)
	if err != nil {
		t.Fatal(err)
	}
	if !reflect.DeepEqual(st.TooLarge, []string{"huge.bin"}) || st.Published != 0 {
		t.Fatalf("stats = %+v", st)
	}
	if reads != 0 {
		t.Fatalf("the oversized file was read %d times; it must be refused on its size alone", reads)
	}
	commit(t, p)
	_, rs, err := Restore(ctx, s, pfx, t.TempDir())
	if err != nil || !reflect.DeepEqual(rs.Skipped, []string{"huge.bin"}) {
		t.Fatalf("restore skipped %v, err %v", rs.Skipped, err)
	}
}

func TestSyncDirHonoursSkip(t *testing.T) {
	ctx, s, src := context.Background(), release.NewDirStore(t.TempDir()), t.TempDir()
	write(t, src, "keep.txt", "k")
	write(t, src, ".files-gen", "marker")
	write(t, src, "cache/x", "x")
	p := open(t, s)
	skip := func(rel string, isDir bool) bool { return rel == ".files-gen" || (isDir && rel == "cache") }
	if _, err := SyncDir(ctx, p, src, skip); err != nil {
		t.Fatal(err)
	}
	if got := p.Paths(); !reflect.DeepEqual(got, []string{"keep.txt"}) {
		t.Fatalf("Paths = %v", got)
	}
}

func TestSyncDirOfAMissingDirectoryTombstonesNothing(t *testing.T) {
	ctx, s, src := context.Background(), release.NewDirStore(t.TempDir()), t.TempDir()
	p := open(t, s)
	write(t, src, "a", "a")
	if _, err := SyncDir(ctx, p, src, nil); err != nil {
		t.Fatal(err)
	}
	commit(t, p)
	if _, err := SyncDir(ctx, p, filepath.Join(src, "does-not-exist"), nil); err == nil {
		t.Fatal("a missing root must be an error, not an empty directory")
	}
	if p.Dirty() {
		t.Fatal("a missing root staged tombstones")
	}
}

// An excluded directory's files, published before the exclusion applied,
// are left as the manifest has them: not republished, not tombstoned.
func TestSyncDirDoesNotTombstoneWhatASkippedDirectoryHolds(t *testing.T) {
	ctx, s, src := context.Background(), release.NewDirStore(t.TempDir()), t.TempDir()
	write(t, src, "cache/x", "x")
	write(t, src, "keep.txt", "k")
	p := open(t, s)
	if _, err := SyncDir(ctx, p, src, nil); err != nil {
		t.Fatal(err)
	}
	commit(t, p)
	skip := func(rel string, isDir bool) bool { return isDir && rel == "cache" }
	st, err := SyncDir(ctx, p, src, skip)
	if err != nil || st.Deleted != 0 || p.Dirty() {
		t.Fatalf("stats = %+v dirty=%v err=%v", st, p.Dirty(), err)
	}
	if got := p.Paths(); !reflect.DeepEqual(got, []string{"cache/x", "keep.txt"}) {
		t.Fatalf("Paths = %v", got)
	}
}
```

Then `internal/hostfiles/sync_unix_test.go`, for special files, substitution races and unreadable files. `swapAt` uses the seam `betweenLookAndOpen` to swap a file or directory in the gap between its inspection and its open, which makes each race deterministic. Two tests skip where they cannot run: the socket test inside a sandbox that forbids sockets, the device test without root. Every test in this file uses only what Tasks 5 and 6 define; the tests of `publishOne` are Task 7's.

```go
//go:build unix

package hostfiles

import (
	"context"
	"net"
	"os"
	"path/filepath"
	"reflect"
	"syscall"
	"testing"
	"time"

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

// within fails the test if fn has not returned in two seconds: the way a
// blocked open shows up.
func within(t *testing.T, what string, fn func()) {
	t.Helper()
	done := make(chan struct{})
	go func() { defer close(done); fn() }()
	select {
	case <-done:
	case <-time.After(2 * time.Second):
		t.Fatalf("%s did not return: something blocked", what)
	}
}

// swapAt arranges for fn to run once, at the nth pause between a look and
// an open (1 is the first).
func swapAt(t *testing.T, n int, fn func()) {
	t.Helper()
	orig, calls := betweenLookAndOpen, 0
	t.Cleanup(func() { betweenLookAndOpen = orig })
	betweenLookAndOpen = func() {
		calls++
		if calls == n {
			fn()
		}
	}
}

func tree(t *testing.T, dir string) *dirHandle {
	t.Helper()
	d, err := openTree(dir)
	if err != nil {
		t.Fatal(err)
	}
	t.Cleanup(func() { d.Close() })
	return d
}

func fifo(t *testing.T, path string) {
	t.Helper()
	if err := syscall.Mkfifo(path, 0o600); err != nil {
		t.Skipf("no FIFOs here: %v", err)
	}
}

// Review Focus 2: special files are skipped, never opened for reading.
func TestSyncDirSkipsAFIFO(t *testing.T) {
	ctx, s, src := context.Background(), release.NewDirStore(t.TempDir()), t.TempDir()
	write(t, src, "real.txt", "r")
	fifo(t, filepath.Join(src, "pipe"))
	p := open(t, s)
	var st SyncStats
	within(t, "SyncDir", func() { st, _ = SyncDir(ctx, p, src, nil) })
	if st.Published != 1 || !reflect.DeepEqual(st.NotRegular, []string{"pipe"}) {
		t.Fatalf("stats = %+v", st)
	}
}

func TestSyncDirSkipsASocket(t *testing.T) {
	ctx, s, src := context.Background(), release.NewDirStore(t.TempDir()), t.TempDir()
	l, err := net.Listen("unix", filepath.Join(src, "sock"))
	if err != nil {
		t.Skipf("no Unix sockets here: %v", err)
	}
	defer l.Close()
	p := open(t, s)
	var st SyncStats
	within(t, "SyncDir", func() { st, _ = SyncDir(ctx, p, src, nil) })
	if !reflect.DeepEqual(st.NotRegular, []string{"sock"}) || len(p.Paths()) != 0 {
		t.Fatalf("stats = %+v, paths = %v", st, p.Paths())
	}
}

func TestSyncDirSkipsADevice(t *testing.T) {
	if os.Getuid() != 0 {
		t.Skip("making a device node needs root")
	}
	ctx, s, src := context.Background(), release.NewDirStore(t.TempDir()), t.TempDir()
	if err := syscall.Mknod(filepath.Join(src, "null"), syscall.S_IFCHR|0o600, 1<<8|3); err != nil {
		t.Skipf("mknod: %v", err)
	}
	p := open(t, s)
	st, err := SyncDir(ctx, p, src, nil)
	if err != nil || !reflect.DeepEqual(st.NotRegular, []string{"null"}) {
		t.Fatalf("stats = %+v, err = %v", st, err)
	}
}

// A symlinked directory is never descended into, wherever it points.
func TestSyncDirDoesNotDescendASymlinkedDirectory(t *testing.T) {
	ctx, s, src, outside := context.Background(), release.NewDirStore(t.TempDir()), t.TempDir(), t.TempDir()
	if err := os.WriteFile(filepath.Join(outside, "secret"), []byte("s"), 0o600); err != nil {
		t.Fatal(err)
	}
	write(t, src, "inside/own", "o")
	for link, target := range map[string]string{"out": outside, "in": "inside"} {
		if err := os.Symlink(target, filepath.Join(src, link)); err != nil {
			t.Fatal(err)
		}
	}
	p := open(t, s)
	st, err := SyncDir(ctx, p, src, nil)
	if err != nil {
		t.Fatal(err)
	}
	if got := p.Paths(); !reflect.DeepEqual(got, []string{"inside/own"}) {
		t.Fatalf("Paths = %v", got)
	}
	if !reflect.DeepEqual(st.NotRegular, []string{"in", "out"}) {
		t.Fatalf("NotRegular = %v", st.NotRegular)
	}
}

// A regular file swapped for a FIFO after it was inspected must neither
// block the open nor be read.
func TestReadStableDoesNotBlockOnAFIFOSwappedIn(t *testing.T) {
	dir := t.TempDir()
	write(t, dir, "f", "regular")
	d := tree(t, dir)
	swapAt(t, 1, func() {
		full := filepath.Join(dir, "f")
		if err := os.Remove(full); err != nil {
			t.Error(err)
		}
		if err := syscall.Mkfifo(full, 0o600); err != nil {
			t.Error(err)
		}
	})
	var body []byte
	var state fileState
	within(t, "readStable", func() { body, _, state, _ = readStable(d, "f") })
	if state != fileNotRegular || body != nil {
		t.Fatalf("state = %v, %d bytes; want fileNotRegular and nothing read", state, len(body))
	}
}

// A directory swapped for a FIFO between being listed and being entered
// must not hang the scan.
func TestSyncDirDoesNotBlockOnADirectorySwappedForAFIFO(t *testing.T) {
	ctx, s, src := context.Background(), release.NewDirStore(t.TempDir()), t.TempDir()
	write(t, src, "d/child", "c")
	write(t, src, "z.txt", "z")
	swapAt(t, 1, func() { // the first pause is before entering "d"
		if err := os.RemoveAll(filepath.Join(src, "d")); err != nil {
			t.Error(err)
		}
		if err := syscall.Mkfifo(filepath.Join(src, "d"), 0o600); err != nil {
			t.Error(err)
		}
	})
	p := open(t, s)
	var st SyncStats
	var err error
	within(t, "SyncDir", func() { st, err = SyncDir(ctx, p, src, nil) })
	if err != nil {
		t.Fatal(err)
	}
	if !reflect.DeepEqual(st.NotRegular, []string{"d"}) || !reflect.DeepEqual(p.Paths(), []string{"z.txt"}) {
		t.Fatalf("stats = %+v, paths = %v", st, p.Paths())
	}
}

// Plan round 2, finding 2: a file that still exists is never tombstoned
// just because it could not be read.
func TestSyncDirKeepsAnUnreadableFilesPublishedEntry(t *testing.T) {
	if os.Getuid() == 0 {
		t.Skip("root can read anything")
	}
	ctx, s, src := context.Background(), release.NewDirStore(t.TempDir()), t.TempDir()
	write(t, src, "locked", "v1")
	p := open(t, s)
	if _, err := SyncDir(ctx, p, src, nil); err != nil {
		t.Fatal(err)
	}
	commit(t, p)
	if err := os.Chmod(filepath.Join(src, "locked"), 0); err != nil {
		t.Fatal(err)
	}
	st, err := SyncDir(ctx, p, src, nil)
	if err != nil {
		t.Fatal(err)
	}
	if st.Deleted != 0 || p.Dirty() || !reflect.DeepEqual(st.Unreadable, []string{"locked"}) {
		t.Fatalf("stats = %+v dirty = %v", st, p.Dirty())
	}
}

// A regular file replaced by another regular file between the look and
// the open is neither read nor declared gone: the caller asks again.
func TestReadStableAsksAgainWhenARegularFileIsReplacedMidLook(t *testing.T) {
	dir := t.TempDir()
	write(t, dir, "f", "v1")
	d := tree(t, dir)
	swapAt(t, 1, func() {
		tmp := filepath.Join(dir, "f.new")
		if err := os.WriteFile(tmp, []byte("v2"), 0o640); err != nil {
			t.Error(err)
		}
		if err := os.Rename(tmp, filepath.Join(dir, "f")); err != nil {
			t.Error(err)
		}
	})
	body, _, state, err := readStable(d, "f")
	if err != nil || state != fileRetry || body != nil {
		t.Fatalf("state = %v, body = %q, err = %v; want fileRetry and nothing read", state, body, err)
	}
	body, _, state, err = readStable(d, "f")
	if err != nil || state != fileOK || string(body) != "v2" {
		t.Fatalf("second look: state = %v, body = %q, err = %v", state, body, err)
	}
}

// And through a scan: the entry the manifest already has is kept.
func TestSyncDirKeepsAnEntryWhoseFileWasReplacedMidLook(t *testing.T) {
	ctx, s, src := context.Background(), release.NewDirStore(t.TempDir()), t.TempDir()
	write(t, src, "f", "v1")
	p := open(t, s)
	if _, err := SyncDir(ctx, p, src, nil); err != nil {
		t.Fatal(err)
	}
	commit(t, p)
	swapAt(t, 1, func() {
		tmp := filepath.Join(src, "f.new")
		if err := os.WriteFile(tmp, []byte("v2"), 0o640); err != nil {
			t.Error(err)
		}
		if err := os.Rename(tmp, filepath.Join(src, "f")); err != nil {
			t.Error(err)
		}
	})
	st, err := SyncDir(ctx, p, src, nil)
	if err != nil || st.Deleted != 0 || p.Dirty() {
		t.Fatalf("stats = %+v dirty = %v err = %v; the replaced file must be left for the next scan", st, p.Dirty(), err)
	}
	if _, err := SyncDir(ctx, p, src, nil); err != nil || !p.Dirty() {
		t.Fatalf("the next scan did not pick the replacement up: dirty = %v err = %v", p.Dirty(), err)
	}
}
```

- [ ] **Step 2: Run them to see them fail**

Run: `go test ./internal/hostfiles/ -run 'SyncDir|ReadStable' -count=1`
Expected: FAIL to build, `undefined: SyncDir`.

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

```go
package hostfiles

import (
	"context"
	"fmt"
	"io"
	"path"
	"sort"
)

type fileState int

const (
	fileOK fileState = iota
	fileTorn
	fileGone
	fileNotRegular
	fileTooLarge
	fileUnreadable
	fileRetry
)

// Test seams. betweenLookAndOpen runs after something has been inspected
// and before it is opened; readAll is the one place file contents are read.
var (
	betweenLookAndOpen = func() {}
	readAll            = io.ReadAll
)

type SyncStats struct {
	Published, Deleted               int
	TooLarge, NotRegular, Unreadable []string
}

// skipped reports whether rel, or any directory above it, is excluded.
func skipped(skip func(string, bool) bool, rel string, isDir bool) bool {
	if skip == nil {
		return false
	}
	if skip(rel, isDir) {
		return true
	}
	for dir := path.Dir(rel); dir != "."; dir = path.Dir(dir) {
		if skip(dir, true) {
			return true
		}
	}
	return false
}

// SyncDir stages every difference between dir and the manifest: changed
// and new regular files, and a tombstone for each manifest path that is
// no longer there. It does not commit. skip, when non-nil, excludes a
// relative path and, for a directory, everything under it; an excluded
// path is left exactly as the manifest has it.
//
// A tombstone needs proof of absence. A file that exists but could not be
// read keeps its manifest entry and is reported in Unreadable. A root
// that cannot be opened is an error and stages nothing: an unreadable
// directory is not an empty one.
func SyncDir(ctx context.Context, p *Publisher, dir string, skip func(rel string, isDir bool) bool) (SyncStats, error) {
	root, err := openTree(dir)
	if err != nil {
		return SyncStats{}, fmt.Errorf("hostfiles: sync %s: %w", dir, err)
	}
	defer root.Close()
	return syncRoot(ctx, p, root, skip)
}

func syncRoot(ctx context.Context, p *Publisher, root *dirHandle, skip func(rel string, isDir bool) bool) (SyncStats, error) {
	var st SyncStats
	seen := map[string]bool{}
	err := syncTree(root, p, skip, func(d *dirHandle, name, rel string) error {
		if err := ctx.Err(); err != nil {
			return err
		}
		body, mode, state, err := readStable(d, name)
		if err != nil {
			return err
		}
		switch state {
		case fileGone:
			return nil
		case fileNotRegular:
			st.NotRegular = append(st.NotRegular, rel)
			return nil
		case fileUnreadable:
			seen[rel] = true // it exists: keep what the manifest has
			st.Unreadable = append(st.Unreadable, rel)
			return nil
		case fileRetry:
			seen[rel] = true // replaced mid-look: the next scan reads it
			return nil
		case fileTooLarge:
			seen[rel] = true
			st.TooLarge = append(st.TooLarge, rel)
			return p.StageSkipped(rel, "too-large")
		}
		seen[rel] = true
		before := p.pending()
		if err := p.Stage(ctx, rel, body, mode, state == fileTorn); err != nil {
			return err
		}
		if p.pending() != before {
			st.Published++
		}
		return nil
	})
	if err != nil {
		return st, fmt.Errorf("hostfiles: sync %s: %w", root.Name(), err)
	}
	for _, rel := range p.Paths() {
		if seen[rel] || skipped(skip, rel, false) {
			continue
		}
		if err := p.StageDelete(rel); err != nil {
			return st, err
		}
		st.Deleted++
	}
	sort.Strings(st.TooLarge)
	sort.Strings(st.NotRegular)
	sort.Strings(st.Unreadable)
	return st, nil
}
```

`Publisher.pending()` (in Task 5's `publisher.go`) is the last sequence number handed out; `SyncDir` uses it to count what a `Stage` call really published.

`internal/hostfiles/dir_unix.go`:

```go
//go:build unix

package hostfiles

import (
	"errors"
	"io"
	"os"
	"sort"
	"strings"

	"golang.org/x/sys/unix"
)

// dirHandle is a directory held open by descriptor. Everything beneath it
// is reached one path component at a time with openat and O_NOFOLLOW, so
// no symlink is ever followed, inside the tree or out of it, and a
// directory swapped for something else after it was looked at is refused
// rather than entered. Every open is non-blocking, so a FIFO swapped in
// for a file or a directory cannot hang the publisher.
type dirHandle struct{ f *os.File }

// openTree opens the root of a files directory. The path to the root is
// the platform's own and may contain symlinks; nothing beneath it may.
func openTree(path string) (*dirHandle, error) {
	fd, err := unix.Open(path, unix.O_RDONLY|unix.O_DIRECTORY|unix.O_CLOEXEC|unix.O_NONBLOCK, 0)
	if err != nil {
		return nil, &os.PathError{Op: "open", Path: path, Err: err}
	}
	return &dirHandle{os.NewFile(uintptr(fd), path)}, nil
}

func (d *dirHandle) Close() error { return d.f.Close() }
func (d *dirHandle) Name() string { return d.f.Name() }

// openDir opens the directory name inside d. A name that is not a real
// directory (a symlink, a file, a FIFO) fails with ENOTDIR or ELOOP
// without blocking; a missing one with ENOENT.
func (d *dirHandle) openDir(name string) (*dirHandle, error) {
	fd, err := unix.Openat(int(d.f.Fd()), name, unix.O_RDONLY|unix.O_DIRECTORY|unix.O_NOFOLLOW|unix.O_CLOEXEC|unix.O_NONBLOCK, 0)
	if err != nil {
		return nil, &os.PathError{Op: "openat", Path: name, Err: err}
	}
	return &dirHandle{os.NewFile(uintptr(fd), name)}, nil
}

// names lists d's entries, sorted.
func (d *dirHandle) names() ([]string, error) {
	if _, err := d.f.Seek(0, io.SeekStart); err != nil {
		return nil, err
	}
	names, err := d.f.Readdirnames(-1)
	if err != nil {
		return nil, err
	}
	sort.Strings(names)
	return names, nil
}

// look inspects name inside d without following a symlink.
func (d *dirHandle) look(name string) (unix.Stat_t, error) {
	var st unix.Stat_t
	err := unix.Fstatat(int(d.f.Fd()), name, &st, unix.AT_SYMLINK_NOFOLLOW)
	return st, err
}

func isDir(st unix.Stat_t) bool     { return st.Mode&unix.S_IFMT == unix.S_IFDIR }
func isRegular(st unix.Stat_t) bool { return st.Mode&unix.S_IFMT == unix.S_IFREG }

func sameFile(a, b unix.Stat_t) bool { return a.Dev == b.Dev && a.Ino == b.Ino }

func sameImage(a, b unix.Stat_t) bool {
	return sameFile(a, b) && a.Size == b.Size && a.Mtim == b.Mtim
}

// notADirectory reports the errors openDir gives for a name that exists
// but is not a real directory.
func notADirectory(err error) bool {
	return errors.Is(err, unix.ENOTDIR) || errors.Is(err, unix.ELOOP)
}

// descend opens the directory rel ("" or "a/b") beneath d, one component
// at a time. gone is true when some component is missing or is not a real
// directory: nothing of ours lives at a path under it.
func (d *dirHandle) descend(rel string) (sub *dirHandle, gone bool, err error) {
	cur, owned := d, false
	if rel == "" || rel == "." {
		return d, false, nil
	}
	for _, part := range strings.Split(rel, "/") {
		betweenLookAndOpen()
		next, err := cur.openDir(part)
		if owned {
			cur.Close()
		}
		if err != nil {
			if errors.Is(err, unix.ENOENT) || notADirectory(err) {
				return nil, true, nil
			}
			return nil, false, err
		}
		cur, owned = next, true
	}
	return cur, false, nil
}

// readStable reads the file name inside d for publishing.
//
// It reads only what it opened without following a link and found to be
// the same regular file it had just inspected. A file over MaxFileBytes is
// refused on its size alone, before any read. What it reports:
//
//   - fileGone: nothing by that name.
//   - fileNotRegular: something by that name that is not a regular file.
//   - fileUnreadable: a regular file that could not be opened or read. It
//     still exists, so the caller must keep what the manifest has for it.
//   - fileRetry: the regular file was replaced by another regular file
//     between the look and the open. Nothing was read; ask again.
//   - fileTorn: read, but its size, mtime or identity changed meanwhile.
//     The bytes are a possibly inconsistent image.
func readStable(d *dirHandle, name string) ([]byte, uint32, fileState, error) {
	before, err := d.look(name)
	if errors.Is(err, unix.ENOENT) {
		return nil, 0, fileGone, nil
	}
	if err != nil {
		return nil, 0, fileUnreadable, nil
	}
	if !isRegular(before) {
		return nil, 0, fileNotRegular, nil
	}
	if before.Size > MaxFileBytes {
		return nil, 0, fileTooLarge, nil
	}
	betweenLookAndOpen()
	fd, err := unix.Openat(int(d.f.Fd()), name, unix.O_RDONLY|unix.O_NOFOLLOW|unix.O_CLOEXEC|unix.O_NONBLOCK, 0)
	switch {
	case errors.Is(err, unix.ENOENT):
		return nil, 0, fileGone, nil
	case errors.Is(err, unix.ELOOP):
		return nil, 0, fileNotRegular, nil // it became a symlink
	case err != nil:
		return nil, 0, fileUnreadable, nil
	}
	f := os.NewFile(uintptr(fd), name)
	defer f.Close()
	var opened unix.Stat_t
	if err := unix.Fstat(fd, &opened); err != nil {
		return nil, 0, fileUnreadable, nil
	}
	if !isRegular(opened) {
		return nil, 0, fileNotRegular, nil // swapped for a FIFO, a device, a directory
	}
	if !sameFile(before, opened) {
		return nil, 0, fileRetry, nil
	}
	if opened.Size > MaxFileBytes {
		return nil, 0, fileTooLarge, nil
	}
	body, err := readAll(io.LimitReader(f, MaxFileBytes+1))
	if err != nil {
		return nil, 0, fileUnreadable, nil
	}
	if int64(len(body)) > MaxFileBytes {
		return nil, 0, fileTooLarge, nil // it grew past the cap while being read
	}
	state := fileOK
	if after, err := d.look(name); err != nil || !sameImage(before, after) {
		state = fileTorn
	}
	return body, uint32(before.Mode & 0o777), state, nil
}
```

`internal/hostfiles/walk_unix.go`:

```go
//go:build unix

package hostfiles

import (
	"errors"

	"golang.org/x/sys/unix"
)

// syncTree walks the tree beneath root by descriptor and calls visit for
// every entry that is not a directory, with the open directory that holds
// it, its name there, and its path relative to root. It never follows a
// symlink and never blocks on a FIFO: a directory is entered only through
// openDir.
func syncTree(root *dirHandle, p *Publisher, skip func(string, bool) bool, visit func(d *dirHandle, name, rel string) error) error {
	return walkDir(root, "", skip, visit)
}

func walkDir(d *dirHandle, prefix string, skip func(string, bool) bool, visit func(d *dirHandle, name, rel string) error) error {
	names, err := d.names()
	if err != nil {
		return err
	}
	for _, name := range names {
		rel := name
		if prefix != "" {
			rel = prefix + "/" + name
		}
		st, err := d.look(name)
		if errors.Is(err, unix.ENOENT) {
			continue
		}
		if err != nil {
			return err
		}
		if !isDir(st) {
			if skipped(skip, rel, false) {
				continue
			}
			if err := visit(d, name, rel); err != nil {
				return err
			}
			continue
		}
		if skipped(skip, rel, true) {
			continue
		}
		betweenLookAndOpen()
		sub, err := d.openDir(name)
		if err != nil {
			if errors.Is(err, unix.ENOENT) {
				continue
			}
			if notADirectory(err) {
				// It was a directory a moment ago and is something else
				// now. Let readStable say what: it will not follow or
				// block on it either.
				if err := visit(d, name, rel); err != nil {
					return err
				}
				continue
			}
			return err
		}
		err = walkDir(sub, rel, skip, visit)
		sub.Close()
		if err != nil {
			return err
		}
	}
	return nil
}
```

`internal/hostfiles/dir_other.go`, so the package still builds on Windows, where it restores and reads manifests but does not publish:

```go
//go:build !unix

package hostfiles

import "errors"

// The publisher reads a files directory by descriptor, without following
// links, which needs openat. The agent runs on Linux; elsewhere this
// package builds, restores and reads manifests, and does not publish.
var errNoPublish = errors.New("hostfiles: publishing a directory is not supported on this OS")

type dirHandle struct{}

func openTree(path string) (*dirHandle, error) { return nil, errNoPublish }
func (d *dirHandle) Close() error              { return nil }
func (d *dirHandle) Name() string              { return "" }

func syncTree(d *dirHandle, p *Publisher, skip func(string, bool) bool, visit func(*dirHandle, string, string) error) error {
	return errNoPublish
}

func (d *dirHandle) descend(rel string) (*dirHandle, bool, error) { return nil, false, errNoPublish }

func readStable(d *dirHandle, name string) ([]byte, uint32, fileState, error) {
	return nil, 0, fileUnreadable, errNoPublish
}
```

Why it is built this way:

- **Everything is reached by descriptor, one component at a time.** `openat` with `O_NOFOLLOW` never follows a symlink, whether it points outside the directory or to another part of it (an excluded directory, say). `O_DIRECTORY` makes an open of something that is not a real directory fail at once, and `O_NONBLOCK` stops any open from hanging on a FIFO. So a directory or file swapped for something else after it was looked at is refused, not entered and not waited on. The scan walks with these handles too: `fs.WalkDir` would re-open each directory by name with a plain blocking open.
- **A tombstone needs proof of absence.** `readStable` distinguishes five outcomes that are not "here are the bytes", and only two of them stage a delete: the name is gone, or something that is not a regular file stands there. A file that exists but cannot be read (`fileUnreadable`) keeps its manifest entry and is reported. A regular file replaced by another regular file mid-look (`fileRetry`) is read again later.
- **Size is checked before any read**, from the first look and again from the opened file. `TestSyncDirRecordsAFileOverTheCapWithoutReadingIt` counts calls to `readAll` and expects none.
- **`skipped`** applies an exclusion to a path and to every directory above it, in the walk and in the tombstone pass alike.
- **`betweenLookAndOpen`** is a test seam that runs in every gap between inspecting a name and opening it. The substitution tests use it to make each race happen exactly once, on demand.

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

Run: `go test ./internal/hostfiles/ -count=1 && GOOS=windows go build ./internal/hostfiles/ && GOOS=darwin go build ./internal/hostfiles/`
Expected: PASS, and both cross-builds succeed. Then `go test ./internal/hostfiles/ -run 'Socket|Device' -count=1 -v`: on a machine that allows Unix sockets the socket test must `PASS`, not `SKIP`.

- [ ] **Step 5: Commit**

```bash
git add internal/hostfiles
git commit -m "hostfiles: SyncDir stages a directory's differences, and never follows a link

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

---

### Task 7: Continuous publishing with a deadline (`hostfiles.Run`)

**Files:**
- Create: `internal/hostfiles/deadline.go`, `internal/hostfiles/run.go`, `internal/hostfiles/watch_linux.go`, `internal/hostfiles/watch_other.go`
- Test: `internal/hostfiles/deadline_test.go`, `internal/hostfiles/run_test.go` and `internal/hostfiles/publish_unix_test.go` (both `//go:build unix`), `internal/hostfiles/watch_linux_test.go`

**Interfaces:**
- Consumes: `Publisher`, `SyncDir`, `readStable`.
- Produces:
  ```go
  type Options struct {
      Deadline time.Duration // first unpublished modification to publish; spec: 1s
      Scan     time.Duration // repair scan; spec: 10m
      Poll     time.Duration // full-scan period while a watch is missing; spec: 1s
      Skip     func(rel string, isDir bool) bool
      OnStats  func(SyncStats) // called after each full scan; may be nil
  }
  func DefaultOptions() Options
  // Run publishes dir's changes until ctx ends or the manifest is superseded.
  func Run(ctx context.Context, p *Publisher, dir string, o Options) error
  // internal:
  type event struct{ rel string; rescan bool }
  type watcher interface {
      Events() <-chan event // closes when the watcher can report nothing more
      Degraded() bool       // some directory may have no working watch
      Repair() error        // restore full coverage, or say why not
      Close() error
  }
  func newWatcher(root string, skip func(string, bool) bool) (watcher, error) // per-OS
  ```

Spec §5.6, continuous mode: a file is published within one second of its **first** unpublished modification, whether or not it has been closed; a file still being written is published again at the next deadline; an unstable read is published anyway, flagged; a missing watch means polling every second until coverage returns.

Three failure rules, each with its own test:

- **Overflow makes coverage uncertain.** A dropped event may have been a directory's creation, which leaves that directory unwatched. So an overflow marks the watcher degraded as well as asking for a scan; the loop then polls and calls `Repair` on every pass until `Repair` succeeds, and scans once more after it does.
- **A repair clears only what it could have repaired.** Coverage is two counters, not a flag: `failures` rises on every overflow or failed watch, and `repaired` is set to the value `failures` had when a successful walk began. A failure that arrives during the walk may concern a directory the walk has already passed, so it leaves the two unequal and the watcher still degraded.
- **A dead watcher means polling for good.** If the reader stops, its channel closes. The loop sets the channel and the watcher to nil and polls from then on. It must not spin on the closed channel, and `Repair` on a dead watcher must fail.
- **The first full scan is part of start-up.** Tests wait for it (`startRun` returns after the first `OnStats`) before they change anything, and the event tests run with scans and polling an hour apart, so only the mechanism under test can satisfy them.

- [ ] **Step 1: Write the deadline queue's failing test**

`internal/hostfiles/deadline_test.go`:

```go
package hostfiles

import (
	"reflect"
	"testing"
	"time"
)

func TestQueueArmsOnTheFirstTouchAndLaterTouchesDoNotPushItBack(t *testing.T) {
	q := newQueue(time.Second)
	t0 := time.Unix(1000, 0)
	q.Touch("a", t0)
	q.Touch("a", t0.Add(900*time.Millisecond)) // still being written
	if got := q.Due(t0.Add(999 * time.Millisecond)); len(got) != 0 {
		t.Fatalf("due early: %v", got)
	}
	if got := q.Due(t0.Add(time.Second)); !reflect.DeepEqual(got, []string{"a"}) {
		t.Fatalf("due = %v, want [a]", got)
	}
	if got := q.Due(t0.Add(2 * time.Second)); len(got) != 0 {
		t.Fatalf("due twice: %v", got)
	}
	q.Touch("a", t0.Add(3*time.Second)) // a new modification arms a new deadline
	if got := q.Due(t0.Add(4 * time.Second)); !reflect.DeepEqual(got, []string{"a"}) {
		t.Fatalf("second deadline: %v", got)
	}
}

func TestQueueReturnsDuePathsSorted(t *testing.T) {
	q := newQueue(time.Second)
	t0 := time.Unix(1000, 0)
	for _, p := range []string{"c", "a", "b"} {
		q.Touch(p, t0)
	}
	if got := q.Due(t0.Add(time.Second)); !reflect.DeepEqual(got, []string{"a", "b", "c"}) {
		t.Fatalf("due = %v", got)
	}
}
```

- [ ] **Step 2: Run it to see it fail**

Run: `go test ./internal/hostfiles/ -run Queue -count=1`
Expected: FAIL to build, `undefined: newQueue`.

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

```go
package hostfiles

import (
	"sort"
	"time"
)

// queue holds, per path, the time by which its first unpublished
// modification must be published. A later modification of a path already
// waiting does not move its deadline, so a file written without pause is
// still published every period.
type queue struct {
	d   time.Duration
	due map[string]time.Time
}

func newQueue(d time.Duration) *queue { return &queue{d: d, due: map[string]time.Time{}} }

func (q *queue) Touch(rel string, now time.Time) {
	if _, waiting := q.due[rel]; !waiting {
		q.due[rel] = now.Add(q.d)
	}
}

// Due removes and returns every path whose deadline has arrived.
func (q *queue) Due(now time.Time) []string {
	var out []string
	for rel, at := range q.due {
		if !at.After(now) {
			out = append(out, rel)
			delete(q.due, rel)
		}
	}
	sort.Strings(out)
	return out
}
```

Run: `go test ./internal/hostfiles/ -run Queue -count=1`. Expected: PASS.

- [ ] **Step 4: Write the run loop's failing tests, with a fake watcher**

`internal/hostfiles/run_test.go`:

```go
//go:build unix

package hostfiles

import (
	"context"
	"errors"
	"os"
	"path/filepath"
	"sync"
	"testing"
	"time"

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

type fakeWatcher struct {
	ch        chan event
	mu        sync.Mutex
	degraded  bool
	repairs   int
	repairErr error // when nil, Repair succeeds and clears degraded
}

func newFake() *fakeWatcher { return &fakeWatcher{ch: make(chan event, 64)} }

func (f *fakeWatcher) Events() <-chan event { return f.ch }
func (f *fakeWatcher) Degraded() bool       { f.mu.Lock(); defer f.mu.Unlock(); return f.degraded }
func (f *fakeWatcher) Close() error         { return nil }
func (f *fakeWatcher) Repair() error {
	f.mu.Lock()
	defer f.mu.Unlock()
	f.repairs++
	if f.repairErr == nil {
		f.degraded = false
	}
	return f.repairErr
}
func (f *fakeWatcher) setDegraded(v bool) { f.mu.Lock(); f.degraded = v; f.mu.Unlock() }

// eventsOnly publishes on events and nothing else: no repair scan and no
// polling can satisfy a test that uses it. polled is the opposite.
var (
	eventsOnly = Options{Deadline: 40 * time.Millisecond, Scan: time.Hour, Poll: time.Hour}
	polled     = Options{Deadline: 40 * time.Millisecond, Scan: time.Hour, Poll: 40 * time.Millisecond}
)

// startRun runs the loop and returns once its first full scan has
// finished, so nothing a test does afterwards can be found by that scan.
func startRun(t *testing.T, p *Publisher, dir string, w watcher, o Options) (stop func() error) {
	t.Helper()
	first := make(chan struct{})
	var once sync.Once
	o.OnStats = func(SyncStats) { once.Do(func() { close(first) }) }
	ctx, cancel := context.WithCancel(context.Background())
	done := make(chan error, 1)
	go func() { done <- run(ctx, p, dir, o, w) }()
	select {
	case <-first:
	case err := <-done:
		t.Fatalf("run ended before its first scan: %v", err)
	case <-time.After(2 * time.Second):
		t.Fatal("run never finished its first scan")
	}
	return func() error { cancel(); return <-done }
}

// startRunCounting is startRun with a callback on every full scan.
func startRunCounting(t *testing.T, p *Publisher, dir string, w watcher, o Options, onScan func()) (stop func() error) {
	t.Helper()
	first := make(chan struct{})
	var once sync.Once
	o.OnStats = func(SyncStats) { onScan(); once.Do(func() { close(first) }) }
	ctx, cancel := context.WithCancel(context.Background())
	done := make(chan error, 1)
	go func() { done <- run(ctx, p, dir, o, w) }()
	select {
	case <-first:
	case <-time.After(2 * time.Second):
		t.Fatal("run never finished its first scan")
	}
	return func() error { cancel(); return <-done }
}

// eventually polls until ok returns true or two seconds pass.
func eventually(t *testing.T, what string, ok func() bool) {
	t.Helper()
	deadline := time.Now().Add(2 * time.Second)
	for time.Now().Before(deadline) {
		if ok() {
			return
		}
		time.Sleep(10 * time.Millisecond)
	}
	t.Fatalf("timed out waiting for: %s", what)
}

func manifestHas(s release.Store, path, sha string) bool {
	m, found, err := Load(context.Background(), s, pfx)
	if err != nil || !found {
		return false
	}
	e, ok := m.Entries[path]
	return ok && !e.Deleted && e.SHA256 == sha
}

func TestRunPublishesATouchedFileAfterTheDeadline(t *testing.T) {
	s, dir, w := release.NewDirStore(t.TempDir()), t.TempDir(), newFake()
	p := open(t, s)
	stop := startRun(t, p, dir, w, eventsOnly)
	defer stop()
	write(t, dir, "instance.key", "k1")
	time.Sleep(3 * eventsOnly.Deadline)
	if manifestHas(s, "instance.key", sum([]byte("k1"))) {
		t.Fatal("published with no event: something other than the event path found it")
	}
	touched := time.Now()
	w.ch <- event{rel: "instance.key"}
	eventually(t, "instance.key published", func() bool { return manifestHas(s, "instance.key", sum([]byte("k1"))) })
	if took := time.Since(touched); took < eventsOnly.Deadline/2 {
		t.Fatalf("published %s after the event; the deadline is %s", took, eventsOnly.Deadline)
	}
}

func TestRunPublishesADeleteAsATombstone(t *testing.T) {
	s, dir, w := release.NewDirStore(t.TempDir()), t.TempDir(), newFake()
	p := open(t, s)
	write(t, dir, "a", "a")
	stop := startRun(t, p, dir, w, eventsOnly) // the first scan publishes "a"
	defer stop()
	eventually(t, "a published", func() bool { return manifestHas(s, "a", sum([]byte("a"))) })
	if err := os.Remove(filepath.Join(dir, "a")); err != nil {
		t.Fatal(err)
	}
	w.ch <- event{rel: "a"}
	eventually(t, "a tombstoned", func() bool {
		m, _, _ := Load(context.Background(), s, pfx)
		return m.Entries["a"].Deleted
	})
}

func TestRunRescansOnOverflow(t *testing.T) {
	s, dir, w := release.NewDirStore(t.TempDir()), t.TempDir(), newFake()
	p := open(t, s)
	stop := startRun(t, p, dir, w, eventsOnly)
	defer stop()
	write(t, dir, "missed.txt", "m") // no event for it: the queue overflowed
	time.Sleep(3 * eventsOnly.Deadline)
	if manifestHas(s, "missed.txt", sum([]byte("m"))) {
		t.Fatal("found before the rescan was asked for")
	}
	w.ch <- event{rescan: true}
	eventually(t, "missed.txt found by the rescan", func() bool { return manifestHas(s, "missed.txt", sum([]byte("m"))) })
}

// A2-5 residual: with a watch missing, changes are found by polling, not
// by waiting for the ten-minute scan.
func TestRunPollsWhileTheWatcherIsDegradedAndKeepsTryingToRepair(t *testing.T) {
	s, dir, w := release.NewDirStore(t.TempDir()), t.TempDir(), newFake()
	w.setDegraded(true)
	w.repairErr = errors.New("watch limit reached")
	p := open(t, s)
	stop := startRun(t, p, dir, w, polled)
	defer stop()
	write(t, dir, "quiet.txt", "q") // no event at all
	eventually(t, "quiet.txt found by polling", func() bool { return manifestHas(s, "quiet.txt", sum([]byte("q"))) })
	eventually(t, "more than one repair attempt", func() bool { w.mu.Lock(); defer w.mu.Unlock(); return w.repairs > 1 })
}

// The other half: once repair succeeds, polling stops. The loop runs
// with a short polling period, so if it kept polling the scan count would
// keep climbing; it must scan once more for what changed meanwhile and
// then go quiet.
func TestRunScansOnceAfterARepairThenStopsPolling(t *testing.T) {
	s, dir, w := release.NewDirStore(t.TempDir()), t.TempDir(), newFake()
	w.setDegraded(true)
	w.repairErr = errors.New("not yet")
	p := open(t, s)
	scans := 0
	var mu sync.Mutex
	count := func() int { mu.Lock(); defer mu.Unlock(); return scans }
	stop := startRunCounting(t, p, dir, w, polled, func() { mu.Lock(); scans++; mu.Unlock() })
	defer stop()

	// While repair fails, it polls: the count climbs.
	n := count()
	eventually(t, "polling while degraded", func() bool { return count() >= n+3 })

	write(t, dir, "during.txt", "d")
	w.mu.Lock()
	w.repairErr = nil // the next Repair succeeds and clears degraded
	w.mu.Unlock()
	eventually(t, "the repair", func() bool { return !w.Degraded() })
	eventually(t, "during.txt found by the post-repair scan", func() bool { return manifestHas(s, "during.txt", sum([]byte("d"))) })

	time.Sleep(2 * polled.Poll) // let any scan already under way finish
	n = count()
	time.Sleep(8 * polled.Poll)
	if got := count(); got != n {
		t.Fatalf("%d more scans in eight polling periods after the repair; polling should have stopped", got-n)
	}
}

// A watcher whose reader dies closes its channel. The loop must fall back
// to polling, not spin and not stop publishing.
func TestRunPollsAfterTheWatcherStops(t *testing.T) {
	s, dir, w := release.NewDirStore(t.TempDir()), t.TempDir(), newFake()
	p := open(t, s)
	stop := startRun(t, p, dir, w, polled)
	defer stop()
	close(w.ch)
	write(t, dir, "after.txt", "a")
	eventually(t, "after.txt found by polling", func() bool { return manifestHas(s, "after.txt", sum([]byte("a"))) })
}

func TestRunWithNoWatcherPolls(t *testing.T) {
	s, dir := release.NewDirStore(t.TempDir()), t.TempDir()
	p := open(t, s)
	stop := startRun(t, p, dir, nil, polled)
	defer stop()
	write(t, dir, "x", "x")
	eventually(t, "x found by polling", func() bool { return manifestHas(s, "x", sum([]byte("x"))) })
}

func TestRunStopsWhenSuperseded(t *testing.T) {
	s, dir, w := release.NewDirStore(t.TempDir()), t.TempDir(), newFake()
	p, other := open(t, s), open(t, s)
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()
	done := make(chan error, 1)
	go func() { done <- run(ctx, p, dir, eventsOnly, w) }()
	stage(t, other, "theirs", "t")
	commit(t, other)
	write(t, dir, "mine", "m")
	w.ch <- event{rel: "mine"}
	select {
	case err := <-done:
		if !errors.Is(err, ErrSuperseded) {
			t.Fatalf("err = %v, want ErrSuperseded", err)
		}
	case <-time.After(2 * time.Second):
		t.Fatal("run did not stop after another writer published")
	}
}

func TestRunIgnoresAnEventForAPathThatEscapes(t *testing.T) {
	s, dir, w := release.NewDirStore(t.TempDir()), t.TempDir(), newFake()
	p := open(t, s)
	stop := startRun(t, p, dir, w, eventsOnly)
	w.ch <- event{rel: "../outside"}
	write(t, dir, "ok", "ok")
	w.ch <- event{rel: "ok"}
	eventually(t, "ok published", func() bool { return manifestHas(s, "ok", sum([]byte("ok"))) })
	if err := stop(); !errors.Is(err, context.Canceled) {
		t.Fatalf("run ended with %v; a bad path must be skipped, not fatal", err)
	}
}
```

`internal/hostfiles/publish_unix_test.go`, for `publishOne`, the function that handles one queued path. These need Task 6's `swapAt` and `tree` helpers and this task's `manifestHas`, which is why they live here and not in Task 6:

```go
//go:build unix

package hostfiles

import (
	"context"
	"os"
	"path/filepath"
	"testing"

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

// A parent directory swapped for a symlink, to outside or to an excluded
// directory inside, must not let a queued path be read through it.
func TestPublishOneNeverReadsThroughASwappedAncestor(t *testing.T) {
	for name, target := range map[string]string{"a link to outside": "OUTSIDE", "a link to an excluded directory inside": "cache"} {
		t.Run(name, func(t *testing.T) {
			ctx, s, src, outside := context.Background(), release.NewDirStore(t.TempDir()), t.TempDir(), t.TempDir()
			write(t, src, "sub/x", "ours")
			write(t, src, "cache/x", "excluded")
			if err := os.WriteFile(filepath.Join(outside, "x"), []byte("outside secret"), 0o600); err != nil {
				t.Fatal(err)
			}
			skip := func(rel string, isDir bool) bool { return isDir && rel == "cache" }
			p := open(t, s)
			if _, err := SyncDir(ctx, p, src, skip); err != nil {
				t.Fatal(err)
			}
			commit(t, p)
			if target == "OUTSIDE" {
				target = outside
			}
			swapAt(t, 1, func() { // before "sub" is opened
				if err := os.RemoveAll(filepath.Join(src, "sub")); err != nil {
					t.Error(err)
				}
				if err := os.Symlink(target, filepath.Join(src, "sub")); err != nil {
					t.Error(err)
				}
			})
			if _, err := publishOne(ctx, p, tree(t, src), "sub/x", skip); err != nil {
				t.Fatal(err)
			}
			commit(t, p)
			m, _, _ := Load(ctx, s, pfx)
			if e := m.Entries["sub/x"]; !e.Deleted {
				t.Fatalf("sub/x = %+v; the directory is gone, so the file is: want a tombstone, and no content read through the link", e)
			}
		})
	}
}

// A queued path whose file exists but cannot be read is an error, and
// what is published for it stays.
func TestPublishOneKeepsAnUnreadableFilesPublishedEntry(t *testing.T) {
	if os.Getuid() == 0 {
		t.Skip("root can read anything")
	}
	ctx, s, src := context.Background(), release.NewDirStore(t.TempDir()), t.TempDir()
	write(t, src, "locked", "v1")
	p := open(t, s)
	if _, err := SyncDir(ctx, p, src, nil); err != nil {
		t.Fatal(err)
	}
	commit(t, p)
	if err := os.Chmod(filepath.Join(src, "locked"), 0); err != nil {
		t.Fatal(err)
	}
	if _, err := publishOne(ctx, p, tree(t, src), "locked", nil); err == nil {
		t.Fatal("publishOne reported success for a file it could not read")
	}
	if p.Dirty() {
		t.Fatal("publishOne staged a change for a file it could not read")
	}
}

// A regular file replaced by another regular file mid-look is read again
// at the next deadline, not tombstoned.
func TestPublishOneRetriesARegularFileReplacedMidLook(t *testing.T) {
	ctx, s, src := context.Background(), release.NewDirStore(t.TempDir()), t.TempDir()
	write(t, src, "f", "v1")
	p := open(t, s)
	if _, err := SyncDir(ctx, p, src, nil); err != nil {
		t.Fatal(err)
	}
	commit(t, p)
	swapAt(t, 1, func() {
		tmp := filepath.Join(src, "f.new")
		if err := os.WriteFile(tmp, []byte("v2"), 0o640); err != nil {
			t.Error(err)
		}
		if err := os.Rename(tmp, filepath.Join(src, "f")); err != nil {
			t.Error(err)
		}
	})
	again, err := publishOne(ctx, p, tree(t, src), "f", nil)
	if err != nil || !again || p.Dirty() {
		t.Fatalf("again = %v, err = %v, dirty = %v; want a retry and nothing staged", again, err, p.Dirty())
	}
	again, err = publishOne(ctx, p, tree(t, src), "f", nil)
	if err != nil || again {
		t.Fatalf("second look: again = %v, err = %v", again, err)
	}
	commit(t, p)
	if !manifestHas(s, "f", sum([]byte("v2"))) {
		t.Fatal("the replacement was not published")
	}
}
```

- [ ] **Step 5: Run them to see them fail**

Run: `go test ./internal/hostfiles/ -run 'Run|PublishOne' -count=1`
Expected: FAIL to build, `undefined: run`.

- [ ] **Step 6: Write `run.go`**

```go
package hostfiles

import (
	"context"
	"errors"
	"fmt"
	"log"
	"path"
	"strings"
	"time"
)

type Options struct {
	Deadline time.Duration // first unpublished modification to publish
	Scan     time.Duration // repair scan while every directory is watched
	Poll     time.Duration // full-scan period while watch coverage is missing
	Skip     func(rel string, isDir bool) bool
	OnStats  func(SyncStats) // called after each full scan; may be nil
}

// DefaultOptions are region HA spec 1 §5.6's values.
func DefaultOptions() Options {
	return Options{Deadline: time.Second, Scan: 10 * time.Minute, Poll: time.Second}
}

type event struct {
	rel    string
	rescan bool
}

// watcher reports changes under one directory. Its event channel closes
// when it can report nothing more.
type watcher interface {
	Events() <-chan event
	Degraded() bool // some directory may have no working watch
	Repair() error  // restore full coverage, or say why not
	Close() error
}

// Run publishes dir's changes until ctx ends (it returns ctx.Err()) or
// another writer supersedes the manifest (ErrSuperseded). Where there is
// no watcher, or the watcher fails, it polls.
func Run(ctx context.Context, p *Publisher, dir string, o Options) error {
	w, err := newWatcher(dir, o.Skip)
	if err != nil {
		log.Printf("hostfiles: no watcher for %s (%v): polling every %s", dir, err, o.Poll)
		return run(ctx, p, dir, o, nil)
	}
	defer w.Close()
	return run(ctx, p, dir, o, w)
}

func run(ctx context.Context, p *Publisher, dir string, o Options, w watcher) error {
	root, err := openTree(dir)
	if err != nil {
		return fmt.Errorf("hostfiles: %s: %w", dir, err)
	}
	defer root.Close()

	var events <-chan event
	if w != nil {
		events = w.Events()
	}
	q := newQueue(o.Deadline)
	tick := time.NewTicker(o.Deadline / 4)
	defer tick.Stop()

	needScan := true // the first pass is a full scan: changes made while nothing watched
	var lastScan time.Time
	for {
		select {
		case <-ctx.Done():
			return ctx.Err()
		case ev, open := <-events:
			if !open {
				// The watcher has stopped for good. A nil channel never
				// fires, and with no watcher every pass below polls.
				log.Printf("hostfiles: watcher for %s stopped: polling every %s", dir, o.Poll)
				events, w = nil, nil
				continue
			}
			if ev.rescan {
				needScan = true
			} else {
				q.Touch(ev.rel, time.Now())
			}
			continue
		case <-tick.C:
		}
		now := time.Now()
		if w != nil && w.Degraded() {
			if err := w.Repair(); err != nil {
				log.Printf("hostfiles: %s: watch coverage still missing: %v", dir, err)
			} else {
				needScan = true // whatever changed while coverage was missing
			}
		}
		polling := w == nil || w.Degraded()
		switch {
		case polling && now.Sub(lastScan) >= o.Poll:
			needScan = true
		case !polling && now.Sub(lastScan) >= o.Scan:
			needScan = true
		}
		if needScan {
			st, err := syncRoot(ctx, p, root, o.Skip)
			if err != nil {
				if ctx.Err() != nil {
					return ctx.Err()
				}
				log.Printf("hostfiles: %v", err)
			} else {
				needScan, lastScan = false, now
				if o.OnStats != nil {
					o.OnStats(st)
				}
			}
		}
		for _, rel := range q.Due(now) {
			again, err := publishOne(ctx, p, root, rel, o.Skip)
			if err != nil {
				log.Printf("hostfiles: %s: %v", rel, err)
				q.Touch(rel, now) // try again at the next deadline
				continue
			}
			if again {
				q.Touch(rel, now)
			}
		}
		if p.Dirty() {
			if _, err := p.Commit(ctx); errors.Is(err, ErrSuperseded) {
				return err
			} else if err != nil {
				log.Printf("hostfiles: commit: %v", err) // staged changes stay; the next tick retries
			}
		}
	}
}

// publishOne stages one path's current state. again is true when the
// caller should read the path again at the next deadline: the file changed
// while it was read (its image is published, flagged), or it was replaced
// mid-look and nothing was read.
//
// A tombstone is staged only on proof: the name is gone, or something
// that is not a regular file now stands there or above it. A file that
// exists but could not be read is an error, and its manifest entry stays.
func publishOne(ctx context.Context, p *Publisher, root *dirHandle, rel string, skip func(string, bool) bool) (again bool, err error) {
	clean, err := CleanPath(rel)
	if err != nil {
		return false, nil // a path the watcher should never have produced: skip it
	}
	if skipped(skip, clean, false) {
		return false, nil
	}
	parent, name := path.Split(clean)
	dir, gone, err := root.descend(strings.TrimSuffix(parent, "/"))
	if err != nil {
		return false, err
	}
	if gone {
		return false, p.StageDelete(clean)
	}
	if dir != root {
		defer dir.Close()
	}
	body, mode, state, err := readStable(dir, name)
	if err != nil {
		return false, err
	}
	switch state {
	case fileGone, fileNotRegular:
		// StageDelete is a no-op for a path the manifest never had, which
		// is what a directory's own event is.
		return false, p.StageDelete(clean)
	case fileUnreadable:
		return false, fmt.Errorf("%s exists but could not be read; keeping what is published", clean)
	case fileRetry:
		return true, nil
	case fileTooLarge:
		return false, p.StageSkipped(clean, "too-large")
	}
	return state == fileTorn, p.Stage(ctx, clean, body, mode, state == fileTorn)
}
```

A path that turns out to be a directory arrives as `fileNotRegular` and `StageDelete` is a no-op for a path the manifest never had, so directory events need no special case.

- [ ] **Step 7: Write the non-Linux stub**

`internal/hostfiles/watch_other.go`:

```go
//go:build !linux

package hostfiles

import "errors"

// newWatcher has no implementation off Linux; Run polls instead.
func newWatcher(root string, skip func(string, bool) bool) (watcher, error) {
	return nil, errors.New("no file watcher on this OS")
}
```

- [ ] **Step 8: Run the loop tests**

Run: `go test ./internal/hostfiles/ -run 'Run|Queue' -count=1` (on Linux this needs Step 9's file to build; if so, do Step 9 first and run both).
Expected: PASS.

- [ ] **Step 9: Write the Linux watcher and its test**

`internal/hostfiles/watch_linux_test.go`:

```go
//go:build linux

package hostfiles

import (
	"context"
	"os"
	"path/filepath"
	"sync/atomic"
	"testing"
	"time"

	"golang.org/x/sys/unix"

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

// realWatcher returns a working inotify watcher, or skips the test where
// the kernel will not give one (a shared machine at its per-user limit).
// CI must not skip: see the plan's Task 7, step 10.
func realWatcher(t *testing.T, root string, skip func(string, bool) bool) watcher {
	t.Helper()
	w, err := newWatcher(root, skip)
	if err != nil {
		if os.Getenv("CARLOS_REQUIRE_INOTIFY") != "" {
			t.Fatalf("inotify is required here: %v", err)
		}
		t.Skipf("no inotify instance available: %v", err)
	}
	t.Cleanup(func() { w.Close() })
	return w
}

// startReal runs the loop on a real watcher with scans and polling off,
// and returns once the first full scan is done: everything after that is
// found by an inotify event or not at all.
func startReal(t *testing.T, s release.Store, dir string) {
	t.Helper()
	w := realWatcher(t, dir, nil)
	p := open(t, s)
	stop := startRun(t, p, dir, w, Options{Deadline: 50 * time.Millisecond, Scan: time.Hour, Poll: time.Hour})
	t.Cleanup(func() { stop() })
}

func TestWatcherPublishesAFileStillHeldOpen(t *testing.T) {
	s, dir := release.NewDirStore(t.TempDir()), t.TempDir()
	startReal(t, s, dir)
	f, err := os.Create(filepath.Join(dir, "open.log"))
	if err != nil {
		t.Fatal(err)
	}
	defer f.Close() // deliberately not closed before the assertion
	if _, err := f.WriteString("line one\n"); err != nil {
		t.Fatal(err)
	}
	eventually(t, "open.log published while still open", func() bool {
		return manifestHas(s, "open.log", sum([]byte("line one\n")))
	})
	// The first publication could have come from the file's creation. A
	// second write through the same descriptor produces no create, no
	// close and no rename: only IN_MODIFY can announce it.
	if _, err := f.WriteString("line two\n"); err != nil {
		t.Fatal(err)
	}
	eventually(t, "the second write published, still open", func() bool {
		return manifestHas(s, "open.log", sum([]byte("line one\nline two\n")))
	})
}

func TestWatcherSeesAFileInADirectoryCreatedAfterItStarted(t *testing.T) {
	s, dir := release.NewDirStore(t.TempDir()), t.TempDir()
	startReal(t, s, dir)
	write(t, dir, "new/deep/a.txt", "a")
	eventually(t, "new/deep/a.txt published", func() bool {
		return manifestHas(s, "new/deep/a.txt", sum([]byte("a")))
	})
	// And a later write inside the new directory is an event, not a scan.
	time.Sleep(150 * time.Millisecond)
	write(t, dir, "new/deep/b.txt", "b")
	eventually(t, "new/deep/b.txt published", func() bool {
		return manifestHas(s, "new/deep/b.txt", sum([]byte("b")))
	})
}

func TestWatcherPublishesARenameAsDeleteAndCreate(t *testing.T) {
	s, dir := release.NewDirStore(t.TempDir()), t.TempDir()
	write(t, dir, "tmp", "v")
	startReal(t, s, dir)
	if err := os.Rename(filepath.Join(dir, "tmp"), filepath.Join(dir, "final")); err != nil {
		t.Fatal(err)
	}
	eventually(t, "final present and tmp tombstoned", func() bool {
		m, _, _ := Load(context.Background(), s, pfx)
		return m.Entries["tmp"].Deleted && m.Entries["final"].SHA256 == sum([]byte("v"))
	})
}

func TestWatcherDoesNotWatchASkippedDirectory(t *testing.T) {
	root := t.TempDir()
	if err := os.MkdirAll(filepath.Join(root, "cache"), 0o755); err != nil {
		t.Fatal(err)
	}
	w := realWatcher(t, root, func(rel string, isDir bool) bool { return isDir && rel == "cache" })
	write(t, root, "cache/x", "x")
	select {
	case ev := <-w.Events():
		t.Fatalf("event from a skipped directory: %+v", ev)
	case <-time.After(150 * time.Millisecond):
	}
}

// unwatched builds a watcher with one directory that has no watch and no
// event on its way: the state an overflow leaves when it swallows that
// directory's creation. The directory exists before the watcher starts
// and is excluded while the watcher is built, so the kernel never queues
// an event that could install the watch later behind the test's back.
func unwatched(t *testing.T) (w *inotifyWatcher, root string) {
	t.Helper()
	root = t.TempDir()
	if err := os.MkdirAll(filepath.Join(root, "lost"), 0o755); err != nil {
		t.Fatal(err)
	}
	var building atomic.Bool
	building.Store(true)
	w = realWatcher(t, root, func(rel string, isDir bool) bool {
		return building.Load() && isDir && rel == "lost"
	}).(*inotifyWatcher)
	building.Store(false)
	return w, root
}

// noEventFor fails if an event for rel arrives within 200ms.
func noEventFor(t *testing.T, w *inotifyWatcher, rel string) {
	t.Helper()
	deadline := time.After(200 * time.Millisecond)
	for {
		select {
		case ev := <-w.Events():
			if ev.rel == rel {
				t.Fatalf("an event for %s, which has no watch", rel)
			}
		case <-deadline:
			return
		}
	}
}

func eventFor(t *testing.T, w *inotifyWatcher, rel string) {
	t.Helper()
	deadline := time.After(2 * time.Second)
	for {
		select {
		case ev := <-w.Events():
			if ev.rel == rel {
				return
			}
		case <-deadline:
			t.Fatalf("no event for %s", rel)
		}
	}
}

// Plan round 1 finding 8: a kernel overflow may have swallowed a
// directory's creation, so coverage is uncertain until Repair has walked
// the tree again. Only Repair can restore it here: remove the walk from
// Repair and the last assertion fails.
func TestWatcherOverflowMarksCoverageUncertainUntilRepaired(t *testing.T) {
	w, root := unwatched(t)
	if w.Degraded() {
		t.Fatal("degraded before anything went wrong")
	}
	write(t, root, "lost/before.txt", "b")
	noEventFor(t, w, "lost/before.txt") // proves the directory really is unwatched

	w.handle(0, unix.IN_Q_OVERFLOW, "")
	if !w.Degraded() {
		t.Fatal("an overflow left coverage marked complete")
	}
	if err := w.Repair(); err != nil {
		t.Fatal(err)
	}
	if w.Degraded() {
		t.Fatal("still degraded after a successful repair")
	}
	write(t, root, "lost/after.txt", "a")
	eventFor(t, w, "lost/after.txt")
}

// Plan round 2 finding 5: a failure that arrives while Repair is walking
// may concern a directory the walk has already passed. That Repair must
// not clear it.
func TestWatcherRepairDoesNotEraseAFailureThatArrivedDuringItsWalk(t *testing.T) {
	w, _ := unwatched(t)
	w.handle(0, unix.IN_Q_OVERFLOW, "")
	orig := afterRepairWalk
	t.Cleanup(func() { afterRepairWalk = orig })
	afterRepairWalk = func() {
		afterRepairWalk = func() {}
		w.handle(0, unix.IN_Q_OVERFLOW, "") // a second overflow, after the walk
	}
	if err := w.Repair(); err != nil {
		t.Fatal(err)
	}
	if !w.Degraded() {
		t.Fatal("the repair cleared a failure newer than its walk")
	}
	if err := w.Repair(); err != nil {
		t.Fatal(err)
	}
	if w.Degraded() {
		t.Fatal("a repair with nothing new during its walk left the watcher degraded")
	}
}

func TestAStoppedWatcherCannotBeRepairedAndClosesItsChannel(t *testing.T) {
	w := realWatcher(t, t.TempDir(), nil).(*inotifyWatcher)
	w.Close()
	select {
	case _, open := <-w.Events():
		if open {
			t.Fatal("an event after Close")
		}
	case <-time.After(2 * time.Second):
		t.Fatal("the channel did not close")
	}
	if err := w.Repair(); err == nil {
		t.Fatal("a stopped watcher reported a successful repair")
	}
	if !w.Degraded() {
		t.Fatal("a stopped watcher is not degraded")
	}
}
```

`internal/hostfiles/watch_linux.go`:

```go
//go:build linux

package hostfiles

import (
	"errors"
	"fmt"
	"io/fs"
	"os"
	"path/filepath"
	"sync"
	"sync/atomic"
	"unsafe"

	"golang.org/x/sys/unix"
)

const watchMask = unix.IN_MODIFY | unix.IN_CLOSE_WRITE | unix.IN_MOVED_FROM | unix.IN_MOVED_TO |
	unix.IN_CREATE | unix.IN_DELETE | unix.IN_ATTRIB

type inotifyWatcher struct {
	root string
	skip func(string, bool) bool
	f    *os.File // the inotify descriptor, non-blocking, so Close unblocks Read
	ch   chan event

	mu  sync.Mutex
	wds map[int32]string // watch descriptor -> directory, relative to root ("" is root)

	// Coverage is tracked by counting failures, not with a flag. failures
	// goes up whenever a directory may have been left unwatched; repaired
	// is the count Repair had seen when it started the walk that
	// succeeded. A failure that arrives while Repair is walking is
	// therefore never erased by that Repair: the two differ again.
	failures atomic.Int64
	repaired atomic.Int64
	dead     atomic.Bool // the reader has stopped; no event will ever arrive
}

// afterRepairWalk runs between Repair's walk and its bookkeeping. A test seam.
var afterRepairWalk = func() {}

func newWatcher(root string, skip func(string, bool) bool) (watcher, error) {
	fd, err := unix.InotifyInit1(unix.IN_CLOEXEC | unix.IN_NONBLOCK)
	if err != nil {
		return nil, fmt.Errorf("inotify init: %w", err)
	}
	w := &inotifyWatcher{root: root, skip: skip, f: os.NewFile(uintptr(fd), "inotify"),
		ch: make(chan event, 4096), wds: map[int32]string{}}
	if err := w.addTree(""); err != nil {
		w.failures.Add(1) // partial coverage: Run polls and calls Repair
	}
	go w.loop()
	return w, nil
}

func (w *inotifyWatcher) Events() <-chan event { return w.ch }
func (w *inotifyWatcher) Degraded() bool {
	return w.dead.Load() || w.failures.Load() != w.repaired.Load()
}
func (w *inotifyWatcher) Close() error { return w.f.Close() }

// Repair re-adds a watch on every directory. InotifyAddWatch on a path
// already watched returns the same descriptor, so this is safe to repeat.
// A watcher whose reader has stopped cannot be repaired.
func (w *inotifyWatcher) Repair() error {
	if w.dead.Load() {
		return errors.New("the inotify reader has stopped")
	}
	seen := w.failures.Load()
	if err := w.addTree(""); err != nil {
		return err
	}
	afterRepairWalk()
	// Only the failures this walk could have covered are cleared. One that
	// arrived during the walk may concern a directory the walk had already
	// passed, so it leaves the watcher degraded and Repair runs again.
	w.repaired.Store(seen)
	return nil
}

// addTree watches rel and every directory under it. It keeps going after a
// failure, so one unreadable directory does not cost the rest their
// watches, and returns the first error.
func (w *inotifyWatcher) addTree(rel string) error {
	var first error
	start := filepath.Join(w.root, filepath.FromSlash(rel))
	err := filepath.WalkDir(start, func(full string, d fs.DirEntry, err error) error {
		if err != nil {
			if first == nil {
				first = err
			}
			return nil
		}
		if !d.IsDir() {
			return nil // WalkDir does not follow symlinks, so a linked directory is never watched
		}
		r, rerr := filepath.Rel(w.root, full)
		if rerr != nil {
			return nil
		}
		r = filepath.ToSlash(r)
		if r == "." {
			r = ""
		}
		if r != "" && w.skip != nil && w.skip(r, true) {
			return filepath.SkipDir
		}
		wd, aerr := unix.InotifyAddWatch(int(w.f.Fd()), full, watchMask|unix.IN_DONT_FOLLOW|unix.IN_ONLYDIR)
		if aerr != nil {
			if first == nil {
				first = fmt.Errorf("watch %s: %w", full, aerr)
			}
			return nil
		}
		w.mu.Lock()
		w.wds[int32(wd)] = r
		w.mu.Unlock()
		return nil
	})
	if err != nil && first == nil {
		first = err
	}
	return first
}

func (w *inotifyWatcher) send(ev event) {
	select {
	case w.ch <- ev:
	default:
		// Our own buffer is full: the same remedy as a kernel overflow.
		// A path was dropped, so only a scan finds it now; and it may
		// have been a new directory, so coverage is uncertain too.
		w.failures.Add(1)
		select {
		case <-w.ch:
		default:
		}
		select {
		case w.ch <- event{rescan: true}:
		default:
		}
	}
}

func (w *inotifyWatcher) loop() {
	defer close(w.ch)
	buf := make([]byte, 64<<10)
	for {
		n, err := w.f.Read(buf)
		if err != nil {
			// Closed by us, or broken. Either way nothing more will be
			// read: the closed channel tells Run to poll from here on.
			w.dead.Store(true)
			return
		}
		for off := 0; off+unix.SizeofInotifyEvent <= n; {
			raw := (*unix.InotifyEvent)(unsafe.Pointer(&buf[off]))
			nameLen := int(raw.Len)
			name := ""
			if nameLen > 0 {
				b := buf[off+unix.SizeofInotifyEvent : off+unix.SizeofInotifyEvent+nameLen]
				for i, c := range b {
					if c == 0 {
						b = b[:i]
						break
					}
				}
				name = string(b)
			}
			off += unix.SizeofInotifyEvent + nameLen
			w.handle(raw.Wd, raw.Mask, name)
		}
	}
}

func (w *inotifyWatcher) handle(wd int32, mask uint32, name string) {
	if mask&unix.IN_Q_OVERFLOW != 0 {
		// Events were dropped, and one of them may have been a new
		// directory, which is then unwatched. Coverage is uncertain until
		// Repair has walked the tree again.
		w.failures.Add(1)
		w.send(event{rescan: true})
		return
	}
	w.mu.Lock()
	dir, known := w.wds[wd]
	if mask&unix.IN_IGNORED != 0 {
		delete(w.wds, wd)
	}
	w.mu.Unlock()
	if !known || name == "" {
		return
	}
	rel := name
	if dir != "" {
		rel = dir + "/" + name
	}
	if mask&unix.IN_ISDIR != 0 {
		if mask&(unix.IN_CREATE|unix.IN_MOVED_TO) != 0 {
			if w.skip != nil && w.skip(rel, true) {
				return
			}
			if err := w.addTree(rel); err != nil {
				w.failures.Add(1)
			}
		}
		// Files may have appeared in the new directory before its watch
		// did, and a directory moved away takes its files with it: only a
		// scan finds either.
		w.send(event{rescan: true})
		return
	}
	if w.skip != nil && w.skip(rel, false) {
		return
	}
	w.send(event{rel: rel})
}
```

- [ ] **Step 10: Run everything, and the cross-builds**

Run: `gofmt -l internal/hostfiles; go vet ./internal/hostfiles/ && go test ./internal/hostfiles/ -count=1 -race && GOOS=darwin go build ./internal/hostfiles/ && GOOS=windows go build ./internal/hostfiles/`
Expected: no gofmt output, PASS, both cross-builds succeed.

**The inotify tests skip where the kernel refuses an inotify instance**, and that hides them. Check they ran: `go test ./internal/hostfiles/ -run Watcher -count=1 -v | grep -c SKIP` must print `0`. On the shared development machine it will not: `fs.inotify.max_user_instances` is 128 and the many agent sessions there use them all (this is how the plan's own dry run went: every test here passed except the real-inotify ones, which skipped). So run them once somewhere with a free instance, with skipping turned off, before this task is called done:

    CARLOS_REQUIRE_INOTIFY=1 go test ./internal/hostfiles/ -run Watcher -count=1 -race -v

Expected: seven tests `PASS`, none `SKIP` (every real-inotify test has `Watcher` in its name; `grep -c 'realWatcher\|unwatched(t)' internal/hostfiles/watch_linux_test.go` shows where they get their watcher). The dev box is the place; ask Paul for the session. Until that run is green, say in the task's report that `watch_linux.go` is unverified against a real kernel. Do not raise the machine's inotify limit yourself: that is Paul's call.

If `TestWatcherPublishesAFileStillHeldOpen` is flaky under `-race`, do not raise `eventually`'s two-second bound past five seconds: look for a lost event first.

- [ ] **Step 11: Commit**

```bash
git add internal/hostfiles
git commit -m "hostfiles: a changed file is published a second after its first change, open or not

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

---

### Task 8: Wake state travels with the files (`registry.ReplaceTimers`, `hostfiles/wake.go`)

**Files:**
- Modify: `internal/registry/timers.go`
- Create: `internal/hostfiles/wake.go`
- Test: `internal/registry/timers_test.go`, `internal/hostfiles/wake_test.go`

**Interfaces:**
- Consumes: `registry.Timer` (its `Registration` field is `json:"-"`), `(*Registry).TimersFor(host) ([]Timer, error)`, `(*Registry).Get(host) (*Route, error)`, `SetNextDue(host string, due int64) error`, `SetReplication(host, class string) error`, `registry.ValidReplication`, `Publisher.StagePlatform`, `ReadPlatform`.
- Produces:
  ```go
  // registry
  func (r *Registry) ReplaceTimers(host string, rows []Timer) error
  // hostfiles
  const WakeItem = "wake.json"
  type WakeTimer struct {
      Name, Kind, Spec, Path string
      NextAt, LastAt         int64
      LastStatus             string
      LastMs                 int64
      Attempts               int
      CreatedAt, DueAt       int64
      Registration           string
  }
  type WakeState struct {
      V           int         `json:"v"`
      Host        string      `json:"host"`
      Timers      []WakeTimer `json:"timers"`
      NextDue     int64       `json:"next_due"`
      Replication string      `json:"replication"`
  }
  func ExportWake(reg *registry.Registry, host string) (WakeState, error)
  func ImportWake(reg *registry.Registry, host string, ws WakeState) error
  func StageWake(ctx context.Context, p *Publisher, ws WakeState) error
  func LoadWake(ctx context.Context, s release.Store, prefix string, m Manifest) (WakeState, bool, error)
  ```

Spec §5.7: the wake state is the host's timer rows with their bookkeeping, the route's `NextDue`, and its `Replication` class. Completed once-timers (`kind == "once"` with `next_at == 0`) are history and are left out, as the moves spec's digest leaves them out, so the runner's daily prune never changes what is exported. An occurrence that fell due while no box ran the host fires once after import, because `DueTimers` selects by the imported `next_at`.

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

Append to `internal/registry/timers_test.go`:

```go
func TestReplaceTimersSwapsAHostsRowsExactly(t *testing.T) {
	r := openTimersReg(t)
	for _, tm := range []Timer{
		{Host: "a.test", Name: "old", Kind: "schedule", Spec: "@every 1m", Path: "/t", NextAt: 100, CreatedAt: 1},
		{Host: "b.test", Name: "other", Kind: "schedule", Spec: "@every 1m", Path: "/t", NextAt: 100, CreatedAt: 1},
	} {
		if err := r.UpsertTimer(tm); err != nil {
			t.Fatal(err)
		}
	}
	rows := []Timer{
		{Host: "a.test", Name: "nightly", Kind: "schedule", Spec: "0 3 * * *", Path: "/n", NextAt: 5000, LastAt: 4000,
			LastStatus: "ok", LastMs: 12, Attempts: 2, CreatedAt: 10, DueAt: 4900, Registration: "reg-nightly"},
		{Host: "a.test", Name: "once-1", Kind: "once", Spec: "2026-10-06T00:00:00Z", Path: "/o", NextAt: 6000, CreatedAt: 11, DueAt: 6000, Registration: "reg-once"},
	}
	if err := r.ReplaceTimers("a.test", rows); err != nil {
		t.Fatal(err)
	}
	got, err := r.TimersFor("a.test")
	if err != nil {
		t.Fatal(err)
	}
	if len(got) != 2 {
		t.Fatalf("a.test has %d timers, want 2: %+v", len(got), got)
	}
	n := got[0] // ordered by name: nightly, once-1
	if n.Name != "nightly" || n.NextAt != 5000 || n.LastAt != 4000 || n.LastStatus != "ok" || n.LastMs != 12 ||
		n.Attempts != 2 || n.CreatedAt != 10 || n.DueAt != 4900 || n.Registration != "reg-nightly" {
		t.Fatalf("bookkeeping not preserved: %+v", n)
	}
	if other, _ := r.TimersFor("b.test"); len(other) != 1 {
		t.Fatalf("b.test has %d timers, want 1: another host's rows were touched", len(other))
	}
}

func TestReplaceTimersIsAllOrNothing(t *testing.T) {
	r := openTimersReg(t)
	if err := r.UpsertTimer(Timer{Host: "a.test", Name: "keep", Kind: "schedule", Spec: "@every 1m", Path: "/t", NextAt: 100, CreatedAt: 1}); err != nil {
		t.Fatal(err)
	}
	// Two rows with one name: the delete and the first insert succeed, the
	// second insert breaks the primary key. Without a transaction the host
	// would be left with "dup" and without "keep".
	dup := []Timer{
		{Host: "a.test", Name: "dup", Kind: "schedule", Spec: "@every 1m", Path: "/t", NextAt: 1, CreatedAt: 1},
		{Host: "a.test", Name: "dup", Kind: "schedule", Spec: "@every 2m", Path: "/t", NextAt: 1, CreatedAt: 1},
	}
	if err := r.ReplaceTimers("a.test", dup); err == nil {
		t.Fatal("two rows with one name were accepted")
	}
	got, _ := r.TimersFor("a.test")
	if len(got) != 1 || got[0].Name != "keep" {
		t.Fatalf("a failed replace changed the rows: %+v", got)
	}
}

func TestReplaceTimersRefusesARowForAnotherHost(t *testing.T) {
	r := openTimersReg(t)
	rows := []Timer{{Host: "elsewhere.test", Name: "y", Kind: "schedule", Spec: "@every 1m", Path: "/t", NextAt: 1, CreatedAt: 1}}
	if err := r.ReplaceTimers("a.test", rows); err == nil {
		t.Fatal("a row for another host was accepted")
	}
}

func TestReplaceTimersWithNoRowsClearsTheHost(t *testing.T) {
	r := openTimersReg(t)
	if err := r.UpsertTimer(Timer{Host: "a.test", Name: "gone", Kind: "schedule", Spec: "@every 1m", Path: "/t", NextAt: 100, CreatedAt: 1}); err != nil {
		t.Fatal(err)
	}
	if err := r.ReplaceTimers("a.test", nil); err != nil {
		t.Fatal(err)
	}
	if got, _ := r.TimersFor("a.test"); len(got) != 0 {
		t.Fatalf("rows left: %+v", got)
	}
}
```

- [ ] **Step 2: Run it to see it fail**

Run: `go test ./internal/registry/ -run ReplaceTimers -count=1`
Expected: FAIL to build, `r.ReplaceTimers undefined`.

- [ ] **Step 3: Add `ReplaceTimers` to `internal/registry/timers.go`**

Add after `UpsertTimer`:

```go
// ReplaceTimers makes host's timer rows exactly rows, in one transaction,
// keeping every column as given: next_at, due_at, attempts and
// registration included. It is how an installation imports the wake state
// it was handed (region HA spec 1 §5.7, moves spec §9.4).
//
// UpsertTimer is the wrong tool for that: it mints a fresh registration
// and resets the runner's bookkeeping on a rearm.
func (r *Registry) ReplaceTimers(host string, rows []Timer) error {
	for _, t := range rows {
		if t.Host != host {
			return fmt.Errorf("registry: timer %q is for host %q, not %q", t.Name, t.Host, host)
		}
	}
	tx, err := r.sql.Begin()
	if err != nil {
		return err
	}
	defer tx.Rollback()
	if _, err := tx.Exec(`DELETE FROM timers WHERE host = ?`, host); err != nil {
		return err
	}
	for _, t := range rows {
		if _, err := tx.Exec(`INSERT INTO timers (`+timerCols+`) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)`,
			t.Host, t.Name, t.Kind, t.Spec, t.Path, t.NextAt, t.LastAt, t.LastStatus, t.LastMs, t.Attempts, t.CreatedAt, t.DueAt, t.Registration); err != nil {
			return fmt.Errorf("registry: insert timer %q: %w", t.Name, err)
		}
	}
	return tx.Commit()
}
```

If `timers.go` does not already import `fmt`, add it.

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

- [ ] **Step 4: Write the failing wake-state tests**

`internal/hostfiles/wake_test.go`:

```go
package hostfiles

import (
	"context"
	"path/filepath"
	"testing"

	"github.com/carlosframework/platform/internal/registry"
	"github.com/carlosframework/platform/internal/release"
)

func wakeReg(t *testing.T) *registry.Registry {
	t.Helper()
	r, err := registry.Open(filepath.Join(t.TempDir(), "registry.db"))
	if err != nil {
		t.Fatal(err)
	}
	t.Cleanup(func() { r.Close() })
	return r
}

func seedHost(t *testing.T, r *registry.Registry, host string) {
	t.Helper()
	if err := r.Upsert(registry.Route{Host: host, Kind: "service", Addr: "10.0.0.1:3000", Status: "ready", Hibernate: true}); err != nil {
		t.Fatal(err)
	}
}

func TestWakeStateRoundTripsThroughTheManifest(t *testing.T) {
	ctx, s := context.Background(), release.NewDirStore(t.TempDir())
	src, dst := wakeReg(t), wakeReg(t)
	seedHost(t, src, "shop.test")
	seedHost(t, dst, "shop.test")
	if err := src.ReplaceTimers("shop.test", []registry.Timer{
		{Host: "shop.test", Name: "nightly", Kind: "schedule", Spec: "0 3 * * *", Path: "/n", NextAt: 5000, LastAt: 4000,
			LastStatus: "ok", Attempts: 1, CreatedAt: 10, DueAt: 4900, Registration: "reg-n"},
		{Host: "shop.test", Name: "remind", Kind: "once", Spec: "2026-10-06T00:00:00Z", Path: "/r", NextAt: 6000, CreatedAt: 11, DueAt: 6000, Registration: "reg-r"},
		{Host: "shop.test", Name: "done", Kind: "once", Spec: "2026-10-01T00:00:00Z", Path: "/d", NextAt: 0, LastAt: 3000, CreatedAt: 9, Registration: "reg-d"},
	}); err != nil {
		t.Fatal(err)
	}
	if err := src.SetNextDue("shop.test", 7777); err != nil {
		t.Fatal(err)
	}
	if err := src.SetReplication("shop.test", registry.ReplicationBatched); err != nil {
		t.Fatal(err)
	}

	ws, err := ExportWake(src, "shop.test")
	if err != nil {
		t.Fatal(err)
	}
	if len(ws.Timers) != 2 {
		t.Fatalf("exported %d timers, want 2: a finished once-timer is history, not wake state", len(ws.Timers))
	}
	p := open(t, s)
	if err := StageWake(ctx, p, ws); err != nil {
		t.Fatal(err)
	}
	commit(t, p)

	m, _, err := Load(ctx, s, pfx)
	if err != nil {
		t.Fatal(err)
	}
	got, found, err := LoadWake(ctx, s, pfx, m)
	if err != nil || !found {
		t.Fatalf("LoadWake found=%v err=%v", found, err)
	}
	if err := ImportWake(dst, "shop.test", got); err != nil {
		t.Fatal(err)
	}
	rows, _ := dst.TimersFor("shop.test")
	if len(rows) != 2 || rows[0].Name != "nightly" || rows[0].Registration != "reg-n" || rows[0].NextAt != 5000 || rows[0].Attempts != 1 {
		t.Fatalf("imported timers: %+v", rows)
	}
	rt, err := dst.Get("shop.test")
	if err != nil || rt == nil {
		t.Fatalf("route: %v %v", rt, err)
	}
	if rt.NextDue != 7777 || rt.Replication != registry.ReplicationBatched {
		t.Fatalf("NextDue=%d Replication=%q", rt.NextDue, rt.Replication)
	}
}

func TestStageWakeOfUnchangedStateStagesNothing(t *testing.T) {
	ctx, s := context.Background(), release.NewDirStore(t.TempDir())
	p := open(t, s)
	ws := WakeState{V: 1, Host: "shop.test", NextDue: 5}
	if err := StageWake(ctx, p, ws); err != nil {
		t.Fatal(err)
	}
	commit(t, p)
	if err := StageWake(ctx, p, ws); err != nil {
		t.Fatal(err)
	}
	if p.Dirty() {
		t.Fatal("unchanged wake state was staged again")
	}
}

func TestLoadWakeWithNoneReportsNotFound(t *testing.T) {
	ctx, s := context.Background(), release.NewDirStore(t.TempDir())
	m, _, _ := Load(ctx, s, pfx)
	if _, found, err := LoadWake(ctx, s, pfx, m); found || err != nil {
		t.Fatalf("found=%v err=%v", found, err)
	}
}

// A manifest that names wake state whose object has gone is lost data,
// not a host without timers.
func TestLoadWakeWithItsObjectMissingIsAnError(t *testing.T) {
	ctx, s := context.Background(), release.NewDirStore(t.TempDir())
	p := open(t, s)
	if err := StageWake(ctx, p, WakeState{V: 1, Host: "shop.test", NextDue: 5}); err != nil {
		t.Fatal(err)
	}
	commit(t, p)
	m, _, err := Load(ctx, s, pfx)
	if err != nil {
		t.Fatal(err)
	}
	if err := s.Delete(ctx, pfx+m.Platform[WakeItem].Object); err != nil {
		t.Fatal(err)
	}
	if _, found, err := LoadWake(ctx, s, pfx, m); err == nil || found {
		t.Fatalf("found = %v, err = %v; a missing wake object must be an error", found, err)
	}
}

func TestImportWakeRefusesAnotherHostsState(t *testing.T) {
	r := wakeReg(t)
	seedHost(t, r, "shop.test")
	if err := ImportWake(r, "shop.test", WakeState{V: 1, Host: "other.test"}); err == nil {
		t.Fatal("imported another host's wake state")
	}
}

func TestImportWakeRefusesAnUnknownReplicationClassAndChangesNothing(t *testing.T) {
	r := wakeReg(t)
	seedHost(t, r, "shop.test")
	if err := r.ReplaceTimers("shop.test", []registry.Timer{{Host: "shop.test", Name: "keep", Kind: "schedule", Spec: "@every 1m", Path: "/k", NextAt: 1, CreatedAt: 1}}); err != nil {
		t.Fatal(err)
	}
	err := ImportWake(r, "shop.test", WakeState{V: 1, Host: "shop.test", Replication: "hourly"})
	if err == nil {
		t.Fatal("accepted replication class \"hourly\"")
	}
	if rows, _ := r.TimersFor("shop.test"); len(rows) != 1 {
		t.Fatalf("a refused import changed the timers: %+v", rows)
	}
}

func TestExportWakeOfAnUnknownHostIsAnError(t *testing.T) {
	if _, err := ExportWake(wakeReg(t), "nobody.test"); err == nil {
		t.Fatal("exported wake state for a host with no route")
	}
}

func TestImportWakeOfTheDefaultClassReplacesAnotherClass(t *testing.T) {
	r := wakeReg(t)
	seedHost(t, r, "shop.test")
	if err := r.SetReplication("shop.test", registry.ReplicationDaily); err != nil {
		t.Fatal(err)
	}
	if err := ImportWake(r, "shop.test", WakeState{V: 1, Host: "shop.test", Replication: ""}); err != nil {
		t.Fatal(err)
	}
	rt, err := r.Get("shop.test")
	if err != nil || rt == nil {
		t.Fatalf("route: %v %v", rt, err)
	}
	if rt.Replication != "" {
		t.Fatalf("Replication = %q, want the default", rt.Replication)
	}
}
```

- [ ] **Step 5: Run them to see them fail**

Run: `go test ./internal/hostfiles/ -run Wake -count=1`
Expected: FAIL to build, `undefined: ExportWake`.

- [ ] **Step 6: Write `wake.go`**

```go
package hostfiles

import (
	"context"
	"encoding/json"
	"fmt"

	"github.com/carlosframework/platform/internal/registry"
	"github.com/carlosframework/platform/internal/release"
)

// WakeItem is the wake state's name among the manifest's platform items.
const WakeItem = "wake.json"

// WakeTimer is registry.Timer on the wire. It exists because
// registry.Timer hides Registration from JSON, and an import must keep it:
// the runner's delivery bookkeeping is keyed by it.
type WakeTimer struct {
	Name         string `json:"name"`
	Kind         string `json:"kind"`
	Spec         string `json:"spec"`
	Path         string `json:"path"`
	NextAt       int64  `json:"next_at"`
	LastAt       int64  `json:"last_at"`
	LastStatus   string `json:"last_status"`
	LastMs       int64  `json:"last_ms"`
	Attempts     int    `json:"attempts"`
	CreatedAt    int64  `json:"created_at"`
	DueAt        int64  `json:"due_at"`
	Registration string `json:"registration"`
}

// WakeState is everything that decides when a host next runs without a
// request: its timers, the app's own next wake, and its replication class
// (region HA spec 1 §5.7).
type WakeState struct {
	V           int         `json:"v"`
	Host        string      `json:"host"`
	Timers      []WakeTimer `json:"timers"`
	NextDue     int64       `json:"next_due"`
	Replication string      `json:"replication"`
}

// ExportWake reads host's wake state. A finished once-timer (next_at 0) is
// history: it is left out, so the runner's daily prune of such rows never
// changes what is exported.
func ExportWake(reg *registry.Registry, host string) (WakeState, error) {
	rt, err := reg.Get(host)
	if err != nil {
		return WakeState{}, err
	}
	if rt == nil {
		return WakeState{}, fmt.Errorf("hostfiles: wake state for %s: no route", host)
	}
	rows, err := reg.TimersFor(host)
	if err != nil {
		return WakeState{}, err
	}
	ws := WakeState{V: 1, Host: host, Timers: []WakeTimer{}, NextDue: rt.NextDue, Replication: rt.Replication}
	for _, t := range rows {
		if t.Kind == "once" && t.NextAt == 0 {
			continue
		}
		ws.Timers = append(ws.Timers, WakeTimer{Name: t.Name, Kind: t.Kind, Spec: t.Spec, Path: t.Path,
			NextAt: t.NextAt, LastAt: t.LastAt, LastStatus: t.LastStatus, LastMs: t.LastMs, Attempts: t.Attempts,
			CreatedAt: t.CreatedAt, DueAt: t.DueAt, Registration: t.Registration})
	}
	return ws, nil
}

// ImportWake makes host's wake state exactly ws. It validates everything
// before it writes anything.
func ImportWake(reg *registry.Registry, host string, ws WakeState) error {
	if ws.V != 1 {
		return fmt.Errorf("hostfiles: wake state version %d; this binary reads 1", ws.V)
	}
	if ws.Host != host {
		return fmt.Errorf("hostfiles: wake state is for %q, not %q", ws.Host, host)
	}
	if !registry.ValidReplication(ws.Replication) {
		return fmt.Errorf("hostfiles: wake state for %s names replication class %q", host, ws.Replication)
	}
	rows := make([]registry.Timer, 0, len(ws.Timers))
	for _, t := range ws.Timers {
		rows = append(rows, registry.Timer{Host: host, Name: t.Name, Kind: t.Kind, Spec: t.Spec, Path: t.Path,
			NextAt: t.NextAt, LastAt: t.LastAt, LastStatus: t.LastStatus, LastMs: t.LastMs, Attempts: t.Attempts,
			CreatedAt: t.CreatedAt, DueAt: t.DueAt, Registration: t.Registration})
	}
	if err := reg.ReplaceTimers(host, rows); err != nil {
		return err
	}
	if err := reg.SetNextDue(host, ws.NextDue); err != nil {
		return err
	}
	// Always, the empty class included: "" is the default rung, and a
	// destination left on its previous class would not be the state that
	// was exported.
	return reg.SetReplication(host, ws.Replication)
}

// StageWake stages the wake state as a platform item. Unchanged state
// stages nothing.
func StageWake(ctx context.Context, p *Publisher, ws WakeState) error {
	body, err := json.Marshal(ws)
	if err != nil {
		return fmt.Errorf("hostfiles: marshal wake state: %w", err)
	}
	return p.StagePlatform(ctx, WakeItem, body)
}

// LoadWake reads the wake state a manifest carries. The bool is false only
// when the manifest has no wake entry at all. A manifest that names wake
// state whose object is missing or damaged is an error: that is lost
// timers, not a host with none, and the caller must not install as if it
// were.
func LoadWake(ctx context.Context, s release.Store, prefix string, m Manifest) (WakeState, bool, error) {
	if _, ok := m.Platform[WakeItem]; !ok {
		return WakeState{}, false, nil
	}
	body, err := ReadPlatform(ctx, s, prefix, m, WakeItem)
	if err != nil {
		return WakeState{}, false, fmt.Errorf("hostfiles: the manifest names wake state that cannot be read: %w", err)
	}
	var ws WakeState
	if err := json.Unmarshal(body, &ws); err != nil {
		return WakeState{}, false, fmt.Errorf("hostfiles: wake state: %w", err)
	}
	return ws, true, nil
}
```

- [ ] **Step 7: Check the registry facts the code relies on**

The dry run confirmed both, against main at 014b0ce1; confirm them again if `registry.go` has changed since:

- `registry.ValidReplication("")` is true: `""` is the default rung.
- `(*Registry).SetReplication(host, "")` succeeds and clears the class. `ImportWake` calls it unconditionally, so importing the default class onto a route that had `daily` leaves it on the default. `TestImportWakeOfTheDefaultClassReplacesAnotherClass` pins that.

Run: `grep -n "func ValidReplication" -A8 internal/registry/registry.go; grep -n "func (r \*Registry) SetReplication" -A14 internal/registry/registry.go`

- [ ] **Step 8: Run the tests**

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

- [ ] **Step 9: Commit**

```bash
git add internal/registry/timers.go internal/registry/timers_test.go internal/hostfiles/wake.go internal/hostfiles/wake_test.go
git commit -m "hostfiles: a host's timers, next wake and replication class travel with its files

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

---

### Task 9: Termination fences (`internal/termfence`)

**Files:**
- Create: `internal/termfence/fence.go`
- Test: `internal/termfence/fence_test.go`

**Interfaces:**
- Consumes: `boxid.Identity`; `github.com/aws/aws-sdk-go-v2/service/ec2` and its `types` (already a dependency).
- Produces:
  ```go
  package termfence
  const (
      ViaDescribe = "describe"
      ViaEvent    = "event"
  )
  type Observation struct {
      Via        string
      Found      bool   // the source said something about this instance
      InstanceID string
      Account    string
      Region     string
      State      string // EC2's state name, verbatim
      EventID    string
  }
  type Fence struct {
      Box        string `json:"box"`
      InstanceID string `json:"instance_id"`
      Account    string `json:"account"`
      AWSRegion  string `json:"aws_region"`
      State      string `json:"state"`
      ObservedAt string `json:"observed_at"`
      Via        string `json:"via"`
      EventID    string `json:"event_id,omitempty"`
  }
  var ErrNotEvidence = errors.New("termfence: not evidence of termination")
  func FromDescribe(out *ec2.DescribeInstancesOutput, instanceID, region string) Observation
  func FromEvent(body []byte) (Observation, error)
  func Verify(box string, id boxid.Identity, o Observation, now time.Time) (Fence, error)
  ```

Spec §5.9 and HA7. Only `terminated` counts. A missing instance, an error, another account or another region is never evidence. This task builds the two observation sources that exist today; the third (Auto Scaling activity history) is off until spec 4 verifies it on dev, so it is not built here. Reading the queue and calling EC2 belong to the placer (spec 3): this package is pure.

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

`internal/termfence/fence_test.go`:

```go
package termfence

import (
	"errors"
	"testing"
	"time"

	"github.com/aws/aws-sdk-go-v2/aws"
	"github.com/aws/aws-sdk-go-v2/service/ec2"
	"github.com/aws/aws-sdk-go-v2/service/ec2/types"

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

var (
	id  = boxid.Identity{AccountID: "111122223333", Region: "eu-west-1", InstanceID: "i-0abc"}
	now = time.Date(2026, 10, 5, 12, 0, 0, 0, time.UTC)
)

func describe(owner, instance string, state types.InstanceStateName) *ec2.DescribeInstancesOutput {
	return &ec2.DescribeInstancesOutput{Reservations: []types.Reservation{{
		OwnerId:   aws.String(owner),
		Instances: []types.Instance{{InstanceId: aws.String(instance), State: &types.InstanceState{Name: state}}},
	}}}
}

func TestATerminatedInstanceOfThisBoxIsAFence(t *testing.T) {
	o := FromDescribe(describe("111122223333", "i-0abc", types.InstanceStateNameTerminated), "i-0abc", "eu-west-1")
	f, err := Verify("i-0abc", id, o, now)
	if err != nil {
		t.Fatal(err)
	}
	want := Fence{Box: "i-0abc", InstanceID: "i-0abc", Account: "111122223333", AWSRegion: "eu-west-1",
		State: "terminated", ObservedAt: "2026-10-05T12:00:00Z", Via: "describe"}
	if f != want {
		t.Fatalf("fence = %+v\nwant    %+v", f, want)
	}
}

// Review Focus 4: every lookalike is refused.
func TestLookalikesAreNotEvidence(t *testing.T) {
	cases := map[string]Observation{
		"stopped":          FromDescribe(describe("111122223333", "i-0abc", types.InstanceStateNameStopped), "i-0abc", "eu-west-1"),
		"stopping":         FromDescribe(describe("111122223333", "i-0abc", types.InstanceStateNameStopping), "i-0abc", "eu-west-1"),
		"shutting-down":    FromDescribe(describe("111122223333", "i-0abc", types.InstanceStateNameShuttingDown), "i-0abc", "eu-west-1"),
		"running":          FromDescribe(describe("111122223333", "i-0abc", types.InstanceStateNameRunning), "i-0abc", "eu-west-1"),
		"another account":  FromDescribe(describe("999999999999", "i-0abc", types.InstanceStateNameTerminated), "i-0abc", "eu-west-1"),
		"another region":   FromDescribe(describe("111122223333", "i-0abc", types.InstanceStateNameTerminated), "i-0abc", "us-east-1"),
		"another instance": FromDescribe(describe("111122223333", "i-0zzz", types.InstanceStateNameTerminated), "i-0zzz", "eu-west-1"),
		"not in the reply": FromDescribe(describe("111122223333", "i-0zzz", types.InstanceStateNameTerminated), "i-0abc", "eu-west-1"),
		"empty reply":      FromDescribe(&ec2.DescribeInstancesOutput{}, "i-0abc", "eu-west-1"),
		"nil reply":        FromDescribe(nil, "i-0abc", "eu-west-1"),
		"no state":         FromDescribe(&ec2.DescribeInstancesOutput{Reservations: []types.Reservation{{OwnerId: aws.String("111122223333"), Instances: []types.Instance{{InstanceId: aws.String("i-0abc")}}}}}, "i-0abc", "eu-west-1"),
		"unknown source":   {Via: "cloudtrail", Found: true, InstanceID: "i-0abc", Account: "111122223333", Region: "eu-west-1", State: "terminated"},
		"zero observation": {},
	}
	for name, o := range cases {
		t.Run(name, func(t *testing.T) {
			if f, err := Verify("i-0abc", id, o, now); !errors.Is(err, ErrNotEvidence) {
				t.Fatalf("fence = %+v, err = %v; want ErrNotEvidence", f, err)
			}
		})
	}
}

func TestAnUnverifiedIdentityIsNeverFenced(t *testing.T) {
	// A found, terminated observation whose every coordinate is empty, so
	// it MATCHES an empty identity field for field. Only the guard on the
	// identity itself can refuse it.
	o := Observation{Via: ViaDescribe, Found: true, State: "terminated"}
	if _, err := Verify("i-0abc", boxid.Identity{}, o, now); !errors.Is(err, ErrNotEvidence) {
		t.Fatalf("err = %v: an empty identity matched an empty observation", err)
	}
	if _, err := Verify("", id, Observation{Via: ViaDescribe, Found: true, InstanceID: "i-0abc", Account: "111122223333", Region: "eu-west-1", State: "terminated"}, now); !errors.Is(err, ErrNotEvidence) {
		t.Fatalf("err = %v: a fence with no box name", err)
	}
}

const terminatedEvent = `{"version":"0","id":"ev-123","detail-type":"EC2 Instance State-change Notification","source":"aws.ec2","account":"111122223333","time":"2026-10-05T11:59:00Z","region":"eu-west-1","resources":["arn:aws:ec2:eu-west-1:111122223333:instance/i-0abc"],"detail":{"instance-id":"i-0abc","state":"terminated"}}`

func TestATerminatedEventIsAFence(t *testing.T) {
	o, err := FromEvent([]byte(terminatedEvent))
	if err != nil {
		t.Fatal(err)
	}
	f, err := Verify("i-0abc", id, o, now)
	if err != nil {
		t.Fatal(err)
	}
	if f.Via != "event" || f.EventID != "ev-123" || f.State != "terminated" {
		t.Fatalf("fence = %+v", f)
	}
}

func TestEventsThatAreNotEC2StateChangesAreRefused(t *testing.T) {
	for name, body := range map[string]string{
		"not JSON":            `terminated`,
		"another source":      `{"id":"e","detail-type":"EC2 Instance State-change Notification","source":"my.app","account":"111122223333","region":"eu-west-1","detail":{"instance-id":"i-0abc","state":"terminated"}}`,
		"another detail-type": `{"id":"e","detail-type":"EC2 Spot Instance Interruption Warning","source":"aws.ec2","account":"111122223333","region":"eu-west-1","detail":{"instance-id":"i-0abc","state":"terminated"}}`,
		"no event id":         `{"detail-type":"EC2 Instance State-change Notification","source":"aws.ec2","account":"111122223333","region":"eu-west-1","detail":{"instance-id":"i-0abc","state":"terminated"}}`,
	} {
		t.Run(name, func(t *testing.T) {
			if _, err := FromEvent([]byte(body)); err == nil {
				t.Fatal("accepted")
			}
		})
	}
}

func TestAShuttingDownEventIsNotEvidence(t *testing.T) {
	body := `{"id":"e","detail-type":"EC2 Instance State-change Notification","source":"aws.ec2","account":"111122223333","region":"eu-west-1","detail":{"instance-id":"i-0abc","state":"shutting-down"}}`
	o, err := FromEvent([]byte(body))
	if err != nil {
		t.Fatal(err)
	}
	if _, err := Verify("i-0abc", id, o, now); !errors.Is(err, ErrNotEvidence) {
		t.Fatalf("err = %v, want ErrNotEvidence", err)
	}
}

func TestALegacyLabelFencesByItsBoundInstance(t *testing.T) {
	o := FromDescribe(describe("111122223333", "i-0abc", types.InstanceStateNameTerminated), "i-0abc", "eu-west-1")
	f, err := Verify("annie-welding", id, o, now)
	if err != nil || f.Box != "annie-welding" || f.InstanceID != "i-0abc" {
		t.Fatalf("fence = %+v, err = %v", f, err)
	}
}
```

- [ ] **Step 2: Run them to see them fail**

Run: `go test ./internal/termfence/ -count=1`
Expected: FAIL to build, `undefined: FromDescribe`.

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

```go
// Package termfence decides whether an observation proves that a box's
// machine has been terminated (region HA spec 1 §5.9, invariant HA7).
//
// It is pure: something else calls EC2 or reads the event queue and hands
// the result here. The rule is narrow on purpose. Only the state
// "terminated" counts, only for the instance, account and region that the
// box's AWS-signed record names, and silence is never evidence.
package termfence

import (
	"encoding/json"
	"errors"
	"fmt"
	"time"

	"github.com/aws/aws-sdk-go-v2/aws"
	"github.com/aws/aws-sdk-go-v2/service/ec2"

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

const (
	ViaDescribe = "describe"
	ViaEvent    = "event"

	stateTerminated = "terminated"
	eventSource     = "aws.ec2"
	eventDetailType = "EC2 Instance State-change Notification"
)

// ErrNotEvidence wraps every reason an observation does not fence a box.
var ErrNotEvidence = errors.New("termfence: not evidence of termination")

// Observation is what one source said about one instance. Found is false
// when the source said nothing about it: an empty reply, another instance.
type Observation struct {
	Via        string
	Found      bool
	InstanceID string
	Account    string
	Region     string
	State      string
	EventID    string
}

// Fence is the record a relocation journals in place of stop evidence.
type Fence struct {
	Box        string `json:"box"`
	InstanceID string `json:"instance_id"`
	Account    string `json:"account"`
	AWSRegion  string `json:"aws_region"`
	State      string `json:"state"`
	ObservedAt string `json:"observed_at"`
	Via        string `json:"via"`
	EventID    string `json:"event_id,omitempty"`
}

// FromDescribe reads one instance out of a DescribeInstances reply. region
// is the region the call was made in: the reply does not carry it. A reply
// that does not mention instanceID yields Found false, which Verify
// refuses: AWS forgets a terminated instance within about an hour, so
// absence proves nothing.
func FromDescribe(out *ec2.DescribeInstancesOutput, instanceID, region string) Observation {
	o := Observation{Via: ViaDescribe, Region: region}
	if out == nil || instanceID == "" {
		return o
	}
	for _, r := range out.Reservations {
		for _, inst := range r.Instances {
			if aws.ToString(inst.InstanceId) != instanceID {
				continue
			}
			o.Found, o.InstanceID, o.Account = true, instanceID, aws.ToString(r.OwnerId)
			if inst.State != nil {
				o.State = string(inst.State.Name)
			}
			return o
		}
	}
	return o
}

// FromEvent reads an EventBridge "EC2 Instance State-change Notification".
// It refuses anything else: the queue's policy admits only the rule that
// forwards these, and this is the second check.
func FromEvent(body []byte) (Observation, error) {
	var ev struct {
		ID         string `json:"id"`
		DetailType string `json:"detail-type"`
		Source     string `json:"source"`
		Account    string `json:"account"`
		Region     string `json:"region"`
		Detail     struct {
			InstanceID string `json:"instance-id"`
			State      string `json:"state"`
		} `json:"detail"`
	}
	if err := json.Unmarshal(body, &ev); err != nil {
		return Observation{}, fmt.Errorf("termfence: event is not JSON: %w", err)
	}
	switch {
	case ev.Source != eventSource:
		return Observation{}, fmt.Errorf("termfence: event source %q is not %s", ev.Source, eventSource)
	case ev.DetailType != eventDetailType:
		return Observation{}, fmt.Errorf("termfence: event detail-type %q is not a state change", ev.DetailType)
	case ev.ID == "":
		return Observation{}, errors.New("termfence: event has no id")
	}
	return Observation{Via: ViaEvent, Found: ev.Detail.InstanceID != "", InstanceID: ev.Detail.InstanceID,
		Account: ev.Account, Region: ev.Region, State: ev.Detail.State, EventID: ev.ID}, nil
}

func refuse(format string, a ...any) error {
	return fmt.Errorf("%w: %s", ErrNotEvidence, fmt.Sprintf(format, a...))
}

// Verify returns a fence for box when o proves that the instance in id,
// the box's verified identity, is terminated.
func Verify(box string, id boxid.Identity, o Observation, now time.Time) (Fence, error) {
	switch {
	case box == "" || id.InstanceID == "" || id.AccountID == "" || id.Region == "":
		return Fence{}, refuse("the box has no verified identity")
	case o.Via != ViaDescribe && o.Via != ViaEvent:
		return Fence{}, refuse("source %q is not one this binary accepts", o.Via)
	case !o.Found:
		return Fence{}, refuse("the source said nothing about %s; absence is not termination", id.InstanceID)
	case o.InstanceID != id.InstanceID:
		return Fence{}, refuse("observed %s, the box is %s", o.InstanceID, id.InstanceID)
	case o.Account != id.AccountID:
		return Fence{}, refuse("observed in account %q, the box is in %q", o.Account, id.AccountID)
	case o.Region != id.Region:
		return Fence{}, refuse("observed in region %q, the box is in %q", o.Region, id.Region)
	case o.State != stateTerminated:
		return Fence{}, refuse("state is %q; only %q counts", o.State, stateTerminated)
	}
	return Fence{Box: box, InstanceID: o.InstanceID, Account: o.Account, AWSRegion: o.Region, State: o.State,
		ObservedAt: now.UTC().Format(time.RFC3339), Via: o.Via, EventID: o.EventID}, nil
}
```

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

Run: `gofmt -w internal/termfence && go test ./internal/termfence/ -count=1`
Expected: PASS.

- [ ] **Step 5: Commit**

```bash
git add internal/termfence
git commit -m "termfence: only a terminated state, for the box's own verified instance, is a fence

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

---

### Task 10: The installation-complete marker (`internal/activator/installed.go`)

**Files:**
- Create: `internal/activator/installed.go`
- Test: `internal/activator/installed_test.go`

**Interfaces:**
- Produces:
  ```go
  type InstallMarker struct {
      Incarnation string `json:"incarnation"`
      Generation  string `json:"generation"`
      Stamp       int64  `json:"stamp"`
      DBMarker    string `json:"db_marker"`
      FilesSeq    int64  `json:"files_seq"`
  }
  // Installation is what the claim says is current for a host.
  type Installation struct {
      Incarnation string
      Generation  string
      Stamp       int64
  }
  func InstallMarkerPath(dataDir, host string) string // <dataDir>/.installed/<InstanceID(host)>.json
  func WriteInstallMarker(dataDir, host string, m InstallMarker) error
  func ReadInstallMarker(dataDir, host string) (InstallMarker, bool, error)
  func RemoveInstallMarker(dataDir, host string) error
  func WarmCopyTrusted(want Installation, activeStamp int64, m InstallMarker, found bool, dbMarker string) bool
  ```

Spec §5.5 and HA3: authority (the box's `active_stamp`, from the claim) and evidence (this marker) are separate, and a box skips restore only when both match. This task builds the marker and the decision. It does **not** change `ExecBackend.Restore`: wiring it in needs the claim's fresh read and host guard, which is plan 1b. Until then `WarmCopyTrusted` has no caller outside its tests.

The marker's file name uses `provision.InstanceID(host)`, as the nested layout does, so a long host name cannot exceed the file-name limit.

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

`internal/activator/installed_test.go`:

```go
package activator

import (
	"os"
	"path/filepath"
	"strings"
	"testing"
)

func TestInstallMarkerRoundTrips(t *testing.T) {
	dir := t.TempDir()
	if _, found, err := ReadInstallMarker(dir, "shop.test"); found || err != nil {
		t.Fatalf("before any write: found=%v err=%v", found, err)
	}
	m := InstallMarker{Incarnation: "inc1", Generation: "g-op7", Stamp: 41, DBMarker: "dbm", FilesSeq: 1882}
	if err := WriteInstallMarker(dir, "shop.test", m); err != nil {
		t.Fatal(err)
	}
	got, found, err := ReadInstallMarker(dir, "shop.test")
	if err != nil || !found || got != m {
		t.Fatalf("got %+v found=%v err=%v", got, found, err)
	}
	m.Stamp = 57
	if err := WriteInstallMarker(dir, "shop.test", m); err != nil {
		t.Fatal(err)
	}
	if got, _, _ := ReadInstallMarker(dir, "shop.test"); got.Stamp != 57 {
		t.Fatalf("a second write did not replace the first: %+v", got)
	}
	if err := RemoveInstallMarker(dir, "shop.test"); err != nil {
		t.Fatal(err)
	}
	if err := RemoveInstallMarker(dir, "shop.test"); err != nil {
		t.Fatalf("removing an absent marker: %v", err)
	}
	if _, found, _ := ReadInstallMarker(dir, "shop.test"); found {
		t.Fatal("marker still there")
	}
}

func TestInstallMarkerLeavesNoTemporaryFile(t *testing.T) {
	dir := t.TempDir()
	if err := WriteInstallMarker(dir, "shop.test", InstallMarker{Stamp: 1}); err != nil {
		t.Fatal(err)
	}
	entries, err := os.ReadDir(filepath.Join(dir, ".installed"))
	if err != nil {
		t.Fatal(err)
	}
	if len(entries) != 1 {
		t.Fatalf("%d files in .installed, want 1", len(entries))
	}
}

func TestInstallMarkerForALongHostHasAShortFileName(t *testing.T) {
	host := strings.Repeat("a", 200) + ".example.com"
	p := InstallMarkerPath(t.TempDir(), host)
	if n := len(filepath.Base(p)); n > 64 {
		t.Fatalf("file name is %d bytes", n)
	}
}

func TestADamagedMarkerIsAnErrorNotAbsence(t *testing.T) {
	dir := t.TempDir()
	if err := WriteInstallMarker(dir, "shop.test", InstallMarker{Stamp: 1}); err != nil {
		t.Fatal(err)
	}
	if err := os.WriteFile(InstallMarkerPath(dir, "shop.test"), []byte("{half"), 0o600); err != nil {
		t.Fatal(err)
	}
	if _, found, err := ReadInstallMarker(dir, "shop.test"); err == nil || found {
		t.Fatalf("found=%v err=%v: a damaged marker must be an error", found, err)
	}
}

func TestWarmCopyTrusted(t *testing.T) {
	want := Installation{Incarnation: "inc1", Generation: "g-op7", Stamp: 41}
	good := InstallMarker{Incarnation: "inc1", Generation: "g-op7", Stamp: 41, DBMarker: "dbm", FilesSeq: 9}
	if !WarmCopyTrusted(want, 41, good, true, "dbm") {
		t.Fatal("a complete, current installation was not trusted")
	}
	for name, tc := range map[string]struct {
		active int64
		m      InstallMarker
		found  bool
		db     string
	}{
		"no marker: crashed before the install finished": {41, InstallMarker{}, false, "dbm"},
		"authority without evidence":                     {41, InstallMarker{Incarnation: "inc1", Generation: "g-op7", Stamp: 40, DBMarker: "dbm"}, true, "dbm"},
		"evidence without authority":                     {40, good, true, "dbm"},
		"no authority at all":                            {0, good, true, "dbm"},
		"another generation":                             {41, InstallMarker{Incarnation: "inc1", Generation: "g-op6", Stamp: 41, DBMarker: "dbm"}, true, "dbm"},
		"another incarnation":                            {41, InstallMarker{Incarnation: "inc0", Generation: "g-op7", Stamp: 41, DBMarker: "dbm"}, true, "dbm"},
		"the database is not the one installed":          {41, good, true, "other"},
		"the database has no marker":                     {41, good, true, ""},
	} {
		t.Run(name, func(t *testing.T) {
			if WarmCopyTrusted(want, tc.active, tc.m, tc.found, tc.db) {
				t.Fatal("trusted")
			}
		})
	}
	if WarmCopyTrusted(Installation{}, 0, InstallMarker{}, true, "") {
		t.Fatal("an empty claim matched an empty marker")
	}
}
```

- [ ] **Step 2: Run them to see them fail**

Run: `go test ./internal/activator/ -run 'InstallMarker|DamagedMarker|WarmCopyTrusted' -count=1`
Expected: FAIL to build, `undefined: ReadInstallMarker`.

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

```go
package activator

import (
	"encoding/json"
	"errors"
	"fmt"
	"os"
	"path/filepath"

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

// InstallMarker is a box's own record that it finished installing one
// generation of a host: the database restored and synced, the files
// applied, the wake state imported (region HA spec 1 §5.5). It is written
// last, so its presence vouches for all of them.
//
// It is evidence, not authority. Authority is the claim naming this box,
// which the box holds as its active stamp. A box that crashed mid-install
// has authority and no marker, and must restore.
type InstallMarker struct {
	Incarnation string `json:"incarnation"`
	Generation  string `json:"generation"`
	Stamp       int64  `json:"stamp"`
	DBMarker    string `json:"db_marker"`
	FilesSeq    int64  `json:"files_seq"`
}

// Installation is what the claim says is current for a host.
type Installation struct {
	Incarnation string
	Generation  string
	Stamp       int64
}

func InstallMarkerPath(dataDir, host string) string {
	return filepath.Join(dataDir, ".installed", provision.InstanceID(host)+".json")
}

// WriteInstallMarker replaces the marker atomically and syncs both the
// file and its directory, so a marker that is read back was fully written.
func WriteInstallMarker(dataDir, host string, m InstallMarker) error {
	path := InstallMarkerPath(dataDir, host)
	dir := filepath.Dir(path)
	if err := os.MkdirAll(dir, 0o700); err != nil {
		return err
	}
	body, err := json.Marshal(m)
	if err != nil {
		return err
	}
	tmp, err := os.CreateTemp(dir, ".marker-*")
	if err != nil {
		return err
	}
	defer os.Remove(tmp.Name())
	if _, err := tmp.Write(body); err != nil {
		tmp.Close()
		return err
	}
	if err := tmp.Sync(); err != nil {
		tmp.Close()
		return err
	}
	if err := tmp.Close(); err != nil {
		return err
	}
	if err := os.Rename(tmp.Name(), path); err != nil {
		return err
	}
	d, err := os.Open(dir)
	if err != nil {
		return err
	}
	defer d.Close()
	return d.Sync()
}

// ReadInstallMarker returns the marker and whether there is one. A marker
// that exists but cannot be parsed is an error, never "absent": the caller
// restores either way, but it must not mistake damage for a clean slate.
func ReadInstallMarker(dataDir, host string) (InstallMarker, bool, error) {
	body, err := os.ReadFile(InstallMarkerPath(dataDir, host))
	if errors.Is(err, os.ErrNotExist) {
		return InstallMarker{}, false, nil
	}
	if err != nil {
		return InstallMarker{}, false, err
	}
	var m InstallMarker
	if err := json.Unmarshal(body, &m); err != nil {
		return InstallMarker{}, false, fmt.Errorf("activator: install marker for %s: %w", host, err)
	}
	return m, true, nil
}

func RemoveInstallMarker(dataDir, host string) error {
	err := os.Remove(InstallMarkerPath(dataDir, host))
	if errors.Is(err, os.ErrNotExist) {
		return nil
	}
	return err
}

// WarmCopyTrusted is invariant HA3: a box may skip restore and serve its
// local copy only when it has authority for the current installation
// (activeStamp equals the claim's stamp) AND evidence that it finished
// installing exactly that one (the marker matches the claim, and the
// database on disk is the one the marker was written for).
//
// dbMarker is the generation marker read from beside the local database.
// Every empty value fails: nothing matches by being absent on both sides.
func WarmCopyTrusted(want Installation, activeStamp int64, m InstallMarker, found bool, dbMarker string) bool {
	if !found || want.Stamp == 0 || want.Incarnation == "" || want.Generation == "" || dbMarker == "" {
		return false
	}
	return activeStamp == want.Stamp &&
		m.Stamp == want.Stamp &&
		m.Incarnation == want.Incarnation &&
		m.Generation == want.Generation &&
		m.DBMarker == dbMarker
}
```

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

Run: `go test ./internal/activator/ -run 'InstallMarker|DamagedMarker|WarmCopyTrusted' -count=1 && GOOS=windows go build ./internal/activator/`
Expected: PASS and the cross-build succeeds.

If the Windows build fails on the directory `Sync`, it is a compile error only if the code is wrong: `(*os.File).Sync` exists on every OS. A runtime failure syncing a directory on Windows does not matter here, because the agent runs on Linux.

- [ ] **Step 5: Commit**

```bash
git add internal/activator/installed.go internal/activator/installed_test.go
git commit -m "activator: an installation-complete marker, and the rule for trusting a local copy

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

---

### Task 11: Infrastructure, the spec note, and the branch gate

**Files:**
- Modify: `infra/modules/region-host/main.tf` (instance at `:461`, role policy `host_buckets` at `:139`), `infra/modules/region-host/outputs.tf`, `infra/modules/region-host/README.md`
- Modify: `infra/modules/deployment-buckets/main.tf`, `infra/modules/deployment-buckets/variables.tf`
- Modify: `docs/superpowers/specs/2026-10-04-region-ha-box-placement-design.md`

**Interfaces:**
- Produces, from `region-host`: outputs `instance_id` (if not already there) and `termination_queue_url`. From `deployment-buckets`: variable `operator_role_arns` (list of strings, default `[]`).

Nothing here is applied. The flagship's root lives in another repository and consumes these modules at a release tag; applying is a separate, gated operation. Two cautions from past incidents, both binding on this task:

- A whole-root apply of the flagship restarts every box (user-data drift). This task changes no `user_data`.
- A Deny on these buckets once covered `s3:DeleteObject` and broke every lease release for the full 15-minute TTL. Every Deny added here names the `control/boxes/` prefix and nothing wider.

- [ ] **Step 1: Make the root volume die with the instance**

In `infra/modules/region-host/main.tf`, in `aws_instance.host`'s `root_block_device`:

```hcl
  root_block_device {
    volume_size = var.root_volume_size
    volume_type = "gp3"
    encrypted   = true
    # A terminated box must leave no disk behind: termination is the proof
    # that its instances may run elsewhere (region HA spec 1 §5.9).
    delete_on_termination = true
  }
```

`true` is AWS's default for a root volume, so this changes nothing on a box created by this module; it pins the property so it cannot be lost.

- [ ] **Step 2: Add the termination queue**

Append to `infra/modules/region-host/main.tf`:

```hcl
# --- Termination evidence (region HA spec 1 §5.9) ---------------------------
# EC2 tells EventBridge when an instance in this account and region reaches
# "terminated"; the rule forwards that to a queue the region's boxes read.
# Delivery is best-effort on AWS's side: the queue keeps what arrived, and a
# box that finds nothing here still asks EC2 directly.

data "aws_caller_identity" "current" {}

resource "aws_sqs_queue" "terminations" {
  name                      = "${var.iam_name}-terminations"
  message_retention_seconds = 1209600 # 14 days, the maximum
  sqs_managed_sse_enabled   = true
}

resource "aws_cloudwatch_event_rule" "terminations" {
  name        = "${var.iam_name}-terminations"
  description = "EC2 instances reaching terminated, for region HA failover."
  event_pattern = jsonencode({
    source        = ["aws.ec2"]
    "detail-type" = ["EC2 Instance State-change Notification"]
    detail        = { state = ["terminated"] }
  })
}

resource "aws_cloudwatch_event_target" "terminations" {
  rule = aws_cloudwatch_event_rule.terminations.name
  arn  = aws_sqs_queue.terminations.arn
}

# Only this rule may put a message on the queue. A message is evidence, so
# who can write one matters as much as who can read one.
#
# The Allow alone is not enough: a principal in this account with its own
# sqs:SendMessage grant (an admin role, a wildcard policy) could still post
# a forged event, because an identity policy and a queue policy add up.
# The Deny closes that. It matches every send whose source is not this
# rule, including one with no source at all, which is what a direct
# SendMessage from a role looks like.
resource "aws_sqs_queue_policy" "terminations" {
  queue_url = aws_sqs_queue.terminations.id
  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        Sid       = "TheTerminationRuleMaySend"
        Effect    = "Allow"
        Principal = { Service = "events.amazonaws.com" }
        Action    = "sqs:SendMessage"
        Resource  = aws_sqs_queue.terminations.arn
        Condition = { ArnEquals = { "aws:SourceArn" = aws_cloudwatch_event_rule.terminations.arn } }
      },
      {
        Sid       = "NobodyElseMaySend"
        Effect    = "Deny"
        Principal = "*"
        Action    = "sqs:SendMessage"
        Resource  = aws_sqs_queue.terminations.arn
        Condition = { ArnNotEquals = { "aws:SourceArn" = aws_cloudwatch_event_rule.terminations.arn } }
      },
    ]
  })
}

resource "aws_iam_role_policy" "host_terminations" {
  name = "${var.iam_name}-terminations"
  role = aws_iam_role.host.id
  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [{
      Effect   = "Allow"
      Action   = ["sqs:ReceiveMessage", "sqs:DeleteMessage", "sqs:GetQueueAttributes"]
      Resource = aws_sqs_queue.terminations.arn
    }]
  })
}
```

First run `grep -n 'data "aws_caller_identity"' infra/modules/region-host/*.tf`. If the module already declares one, delete the declaration above (it is not otherwise used here) rather than adding a second.

`tofu validate` cannot prove a policy's effect. Add this to the module README's new section (Step 6) as the check to run once after the first apply, from an administrator's session, and record its result on the discussion that tracks the roll:

    aws sqs send-message --queue-url <termination_queue_url> --message-body '{"forged":true}'

Expected: `AccessDenied`. If it succeeds, the queue is not evidence: stop, and do not enable the placer (spec 3) against it. Then terminate a throwaway instance in the region and confirm its event arrives with `aws sqs receive-message`.

The box role already has `ec2:DescribeInstances` on `*` for the infra inventory (`main.tf`, the read-only statement in `host_buckets`). Spec §6 asks for an `ec2:Region` condition on it. Do **not** narrow the existing statement in this task: the inventory reads other regions. Record it as an open item in the README text below; plan 1b decides it with the placer.

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

Run `grep -n 'output "instance_id"' infra/modules/region-host/outputs.tf`. Add whichever of these is missing to `outputs.tf`:

```hcl
output "instance_id" {
  description = "The box's EC2 instance id. `carlos ops box bind` checks a legacy box's record against this."
  value       = aws_instance.host.id
}

output "termination_queue_url" {
  description = "The queue that receives this region's EC2 terminated events."
  value       = aws_sqs_queue.terminations.id
}
```

- [ ] **Step 4: Protect box records in the deployment bucket**

Append to `infra/modules/deployment-buckets/variables.tf`:

```hcl
variable "operator_role_arns" {
  description = "IAM role ARNs of human operators. Only these may write a box bind (control/boxes/*.bind.json). Empty means nobody can: the safe default until operators are named."
  type        = list(string)
  default     = []
}
```

Append to `infra/modules/deployment-buckets/main.tf`:

```hcl
# --- Box records (region HA spec 1 §5.1) -----------------------------------
# control/boxes/<id>.json binds a box's name to one AWS instance, and
# control/boxes/<id>.bind.json is an operator's word for a legacy label.
# Both are written once and never changed. Storage enforces that, so no
# bug or stolen box credential can rebind a name to another machine.
#
# EVERY statement here names control/boxes/ and nothing wider. A broad Deny
# on this bucket has broken lease releases before.

resource "aws_s3_bucket_policy" "deployment_box_records" {
  bucket = aws_s3_bucket.deployment.id
  policy = jsonencode({
    Version = "2012-10-17"
    Statement = [
      {
        # A record or bind may be created, never replaced. The qualifier
        # leaves multipart preparation alone and still covers its completion.
        Sid       = "BoxRecordsAreCreateOnly"
        Effect    = "Deny"
        Principal = "*"
        Action    = "s3:PutObject"
        Resource  = "${aws_s3_bucket.deployment.arn}/control/boxes/*"
        Condition = {
          Null = { "s3:if-none-match" = "true" }
          Bool = { "s3:ObjectCreationOperation" = "true" }
        }
      },
      {
        Sid       = "BoxRecordsAreNeverDeleted"
        Effect    = "Deny"
        Principal = "*"
        Action    = ["s3:DeleteObject", "s3:DeleteObjectVersion"]
        Resource  = "${aws_s3_bucket.deployment.arn}/control/boxes/*"
      },
      {
        # With no operators named, the list is one ARN nobody has, so the
        # Deny covers everyone.
        Sid       = "OnlyOperatorsWriteBinds"
        Effect    = "Deny"
        Principal = "*"
        Action    = "s3:PutObject"
        Resource  = "${aws_s3_bucket.deployment.arn}/control/boxes/*.bind.json"
        Condition = {
          ArnNotLike = {
            "aws:PrincipalArn" = length(var.operator_role_arns) > 0 ? var.operator_role_arns : ["arn:aws:iam::000000000000:role/nobody"]
          }
        }
      },
    ]
  })
}
```

Before adding it, run `grep -rn "aws_s3_bucket_policy" infra/modules/`. The report found no bucket policy in this module. If one exists for the deployment bucket elsewhere, a bucket takes only one policy: merge these three statements into it instead of creating a second resource.

- [ ] **Step 5: Validate both modules**

Run:

```bash
(cd infra/modules/region-host && tofu fmt -check && tofu init -backend=false -input=false >/dev/null && tofu validate)
(cd infra/modules/deployment-buckets && tofu fmt -check && tofu init -backend=false -input=false >/dev/null && tofu validate)
```

Expected: `Success! The configuration is valid.` twice. `tofu init` downloads the AWS provider: it needs network access to `registry.opentofu.org` and `github.com`, and writes a `.terraform` directory in each module. Delete both `.terraform` directories and any `.terraform.lock.hcl` this created before committing (`git status` must show only the `.tf` and `.md` changes). If `tofu fmt -check` lists a file, run `tofu fmt` on it.

- [ ] **Step 6: Document the module changes**

Append to `infra/modules/region-host/README.md`:

```markdown
## Termination evidence

The module creates a queue, `<iam_name>-terminations`, that receives an
event whenever an EC2 instance in this account and region reaches
`terminated`. Region HA reads it to prove a dead box is gone before its
instances run elsewhere. Only the module's own EventBridge rule can write
to the queue, and the box role can only read and delete from it.

After the first apply, check that nobody but the rule can write to it.
From an administrator's session:

    aws sqs send-message --queue-url <termination_queue_url> --message-body '{"forged":true}'

must fail with `AccessDenied`. Then terminate a throwaway instance in the
region and confirm its event arrives.

The root volume is deleted when the instance is terminated. Region HA
depends on that: a terminated box must leave no disk behind.

Open item for region HA plan 1b: the box role's `ec2:DescribeInstances`
is unconditioned, because the infrastructure inventory reads every
region. The HA spec asks for an `ec2:Region` condition on the call the
placer makes. Decide there whether that is a second, narrower statement.
```

- [ ] **Step 7: Record this plan's three departures in the spec**

Append to `docs/superpowers/specs/2026-10-04-region-ha-box-placement-design.md`:

```markdown

## 18. Notes from plan 1a (2026-10-05)

Plan `docs/superpowers/plans/2026-10-05-region-ha-1a-foundations.md` builds
the parts of this spec that need no claim. It keeps every property above
and changes three mechanisms:

1. **Exact content by hash, not by S3 version id (§5.6, §5.1).** The store
   abstraction has no version-id read and the box role has no
   `s3:GetObjectVersion`. Objects are immutable, their key contains their
   SHA-256, the manifest records it, and a reader verifies it. A bind
   names its record by `record_sha256` in place of `record_version_id`.
2. **The identity signature (§5.1).** The record carries IMDS's
   base64 RSA-SHA256 signature (`/latest/dynamic/instance-identity/signature`),
   verified with the regional certificate, in place of the PKCS7 form.
   Same document, same signer, no new dependency.
3. **`snapshot` is a periodic full publish, not a sealed archive (§5.6).**
   `hostfiles.SyncDir` stages every difference between the directory and
   the manifest. Run on the snapshot interval it gives the `snapshot`
   setting; run every ten minutes it is the continuous setting's repair
   scan; run before stop evidence it is the release flush. The manifest
   therefore has no `base` or `base_seq`, and restore reads one manifest
   and the objects it names.

Two corrections to the spec, found while planning:

- **§5.4's alert for two unstamped entries is dropped.** Static sites and
  public address routes run on every box, so every box publishes them as
  `home`, unstamped: that is normal, and the edge cannot tell them from
  relocatable hosts. The edge reports only two boxes at the same positive
  stamp. Plan 1b decides whether the publisher marks relocatable entries
  so the edge can alert on those alone.
- **§5.1's region check has two halves.** `boxid.Verify` proves which
  region a box is in. Whoever uses that compares it with the region it
  expects: `eligibleBoxes` takes the placement's AWS region and refuses a
  box whose signed region differs, and `termfence.Verify` compares the
  observation's region with the signed one. Plan 1b must supply the
  expected region from something better than a majority vote of the
  unsigned edges files.

Two limits to lift in plan 1b: a file over 64 MiB is recorded in the
manifest as skipped and is not published (the store takes a whole body in
memory), and the third termination-evidence source (Auto Scaling activity
history) is not built until spec 4 verifies it.
```

- [ ] **Step 8: Run the whole gate**

Run: `go build ./... && make test`
Expected: `gofmt -l .: clean` and every package `ok`. `make test` takes up to thirty minutes. Then the cross-builds CI runs: `GOOS=darwin go build ./... && GOOS=windows go build ./... && GOOS=linux GOARCH=arm64 go build ./...`. Expected: all three succeed.

If a package unrelated to this plan fails, do not fix it here and do not skip it: run that package alone, confirm it also fails on `origin/main`, and report both results.

- [ ] **Step 9: Commit**

```bash
git add infra/modules/region-host infra/modules/deployment-buckets docs/superpowers/specs/2026-10-04-region-ha-box-placement-design.md
git commit -m "infra: a termination queue per region, and box records that storage will not let anyone rewrite

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

- [ ] **Step 10: Push and read CI**

```bash
git push origin design-region-ha
amadan ci status carlos/platform -branch design-region-ha
```

Expected: exit 0 (pass). Exit 2 is pending: wait and read it again. Exit 1 is a failure: read the log, fix it, and do not ask for the branch to be merged. Merging is Paul's call, through `amadan branch merge`, never a push to `main`.

---

## What plan 1b picks up

Written when `internal/claim` and `internal/hostguard` are on main, after re-reading spec 1 against the claim's then-current revision:

1. Create names a box: `chooseBox(eligibleBoxes(…))` in the claim's create step (§5.2), with `verified` built from `boxid.Load` and `Verifier.Verify`.
2. Reconcile's three row cases and the ten-minute sweep (§5.3).
3. The edges-file pass sets `edgeHost.Stamp` from the claim, and publishes `home` only for the executor (§5.4).
4. `ExecBackend.Restore` consults `WarmCopyTrusted` under the host guard (§5.5).
5. `files_durability` and `files_snapshot_interval` on the app manifest, projected onto the route as Seed is, and the agent starting `hostfiles.Run` or a periodic `SyncDir` per host (§5.6), with the files directory's location and skip list for both layouts.
6. The agent publishing its box record at start, and `carlos ops box bind` (§5.1), with `docs/site/cli.md`.
7. The `relocating` operation, the release sequence and the pinned recovery set (§5.8).
8. The multi-box gate and the HA floor (§8).
9. A streaming put, lifting the 64 MiB limit; the data bucket's conditional-write policy for the files prefix (§6), in place before the publisher is switched on for any host.
10. Where the expected AWS region for a placement comes from (Task 3), and whether relocatable entries are marked so the edge can alert on unstamped duplicates of those alone (Task 4).

## Astra plan round 1: dispositions

Findings are in `CODEX-PLAN-REGION-HA-1A-R1.md`. All nineteen are accepted. After them the plan's code was dry-run: built, cross-built and tested, then removed.

| # | Finding | Disposition |
|---|---|---|
| 1 | P1 Task 2: `status` not imported | fixed: the import is a named step |
| 2 | P1 Task 1: storage key not bound to the record | fixed: `Load` refuses a record that names another box; `TestLoadRefusesAnotherBoxesRecordUnderThisKey` |
| 3 | P1 Tasks 1, 3: a wrong but supported region accepted | fixed: `eligibleBoxes` takes the expected AWS region and compares the signed one; test with two certified regions |
| 4 | P1 Task 6: `readStable` follows a swapped ancestor, blocks on a swapped FIFO | fixed: `os.Root`, a non-blocking open, identity check after open; deterministic tests through a seam |
| 5 | P1 Task 5: restore writes or deletes through a symlink | fixed: every restore operation goes through an `os.Root`; symlinked ancestors are replaced or left alone, never followed |
| 6 | P1 Task 11: queue policy does not exclude other senders | fixed: an explicit Deny for any other source; a post-apply check in the README |
| 7 | P2 Task 5: directory and file transitions | fixed: tombstones first, deepest first; obstructions removed; tested both ways into empty and previously restored directories |
| 8 | P2 Task 7: overflow leaves a directory unwatched | fixed: overflow marks coverage degraded until `Repair` succeeds; tested |
| 9 | P2 Task 7: a dead watcher disables polling | fixed: a closed channel switches the loop to polling for good; `Repair` fails on a dead watcher; tested |
| 10 | P2 Task 2: headroom never refreshed on the timer | fixed: `edgesFileWrite(…, refresh)`; the timed pass writes when the file carries headroom |
| 11 | P2 Task 8: default replication class not applied | fixed: `SetReplication` is always called; tested |
| 12 | P2 Task 3: `chooseBox` used a different spread rule | fixed: runs-the-app or not, then total hosts, then id |
| 13 | P2 Task 6: a skipped directory's children tombstoned | fixed: `skipped` checks every ancestor in both passes; tested |
| 14 | P2 Task 7: the first scan masked the tests | fixed: `startRun` waits for the first scan; event tests run with scans and polling an hour apart and assert nothing is published before the event |
| 15 | P2 Task 6: Review Focus 2 and 3 partly tested | fixed: FIFO, swapped FIFO, swapped ancestor, symlinked directory; the oversize test counts reads and expects none |
| 16 | P2 Task 8: the all-or-nothing test never reached the transaction | fixed: duplicate names fail the second insert inside the transaction |
| 17 | P2 Task 9: a test passed for the wrong reason | fixed: a found, terminated, all-empty observation against an empty identity |
| 18 | P2 Task 4: conflict diagnostics missing | fixed for equal positive stamps (`foldHomes` returns them, `Refresh` logs once). The unstamped alert is dropped as wrong, with the reason, and recorded as a spec correction |
| 19 | P3 Task 5: storage protection attributed to Task 11 | fixed: it is plan 1b's, and must precede switching the publisher on |

**What the dry run could not check**, and the plan says so at each place: the six tests that need a real inotify instance (the development machine is at its per-user limit of 128), and the IMDS signature against a real box.

## Astra plan round 2: dispositions

Findings are in `CODEX-PLAN-REGION-HA-1A-R2.md`. Round 2 marked twelve of round 1's nineteen resolved and raised seven. All seven, and the residuals, are accepted. The fixes were dry-run again.

| # | Finding | Disposition |
|---|---|---|
| 1 | P1 Task 6: the walk can block on a directory swapped for a FIFO | fixed: the walk is by descriptor (`walk_unix.go`), entering a directory only through `openat` with `O_DIRECTORY|O_NOFOLLOW|O_NONBLOCK`; `TestSyncDirDoesNotBlockOnADirectorySwappedForAFIFO` |
| 2 | P1 Tasks 6, 7: read failures became tombstones | fixed: `fileUnreadable` keeps the manifest entry and is reported; `fileRetry` for a regular file replaced mid-look; a tombstone only on proof. Two tests |
| 3 | P1 Task 5: restore not crash-durable | fixed: every directory in which a name changed is synced before `Restore` returns, deepest first, and a failed sync fails the restore; tested through a seam |
| 4 | P2 Tasks 6, 7: in-root ancestor symlinks bypass exclusions | fixed by the same descriptor traversal: no symlink is followed anywhere. `TestPublishOneNeverReadsThroughASwappedAncestor` covers a link to outside and a link to an excluded directory, through the queued-event path |
| 5 | P2 Task 7: a repair can erase a newer failure | fixed: `failures` and `repaired` counters; tested with a failure injected after the walk |
| 6 | P2 Task 4: tie reporting depended on self and on the winner | fixed: `tiedStamps` looks at every file and every pair; tests for `9, 7, 7` and for a tie with this box itself |
| 7 | P2 Task 7: two repair tests could pass with their mechanism off | fixed: the scan-count test polls at 40 ms, proves polling while degraded, then requires no scan across eight periods after the repair; the overflow test builds an unwatched directory with no pending kernel event, proves it is unwatched, and only then repairs |
| R1-4 | residual: in-root links | finding 4 above |
| R1-8 | residual: overflow repair | findings 5 and 7 above |
| R1-15 | residual: socket and device cases | added; they skip where the sandbox forbids sockets or the user is not root. The socket test was run outside the sandbox and passes |
| R1-18 | residual: diagnostics | finding 6 above |

**Still not run:** the seven tests that need a real inotify instance (the machine is at its per-user limit), and the IMDS signature check.

## Astra plan round 3: dispositions

Findings are in `CODEX-PLAN-REGION-HA-1A-R3.md`. Round 3 marked every round 2 finding and round 1 residual resolved except one part of restore durability, and raised four. All five are accepted and were dry-run. This time the dry run also built and tested Task 5's files alone and Tasks 5 and 6's alone, to check each task's gate passes in order.

| # | Finding | Disposition |
|---|---|---|
| R2-3 | P1 Task 5: a retried restore can skip the sync for a deletion an earlier attempt made | fixed: the directories to sync come from the manifest's paths, live and tombstoned, not from what this call changed; tested by failing the first sync and requiring the retry to perform it |
| 1 | P1 Task 6: its tests used `publishOne` and `manifestHas` from Task 7 | fixed: those tests moved to Task 7's `publish_unix_test.go`; Task 6 keeps `SyncDir` and `readStable` tests of the same behaviours |
| 2 | P2 Task 5: syncing a directory that vanished under a replacement file | fixed: only directories that still exist as real directories are synced; tested with `a/b/c` becoming `a` |
| 3 | P2 Task 7: the held-open test did not need `IN_MODIFY` | fixed: a second write through the same open descriptor must be published |
| 4 | P2 Task 7: `-run Watcher` missed one real-inotify test | fixed: the test is renamed so all seven match |

## Astra plan round 4: dispositions

Findings are in `CODEX-PLAN-REGION-HA-1A-R4.md`. Round 4 marked every outstanding finding resolved, found no task that uses a later task's identifier, and raised one.

| # | Finding | Disposition |
|---|---|---|
| 1 | P2 Task 8: wake state named by the manifest but missing its object read as "no wake state" | fixed: `LoadWake` reports absence only when the manifest has no wake entry; any failure to read a named entry is an error; `TestLoadWakeWithItsObjectMissingIsAnError` |

## After the build (2026-10-06)

The plan was built task by task with a review after each, then a whole-branch review and one fix wave. Where the code now differs from the text above, the code is right:

- **`box_id` is the box's label on every box** (final review, I2). `edgesFileBoxID` and the instance-id read at loop arm were removed. Spec §2 keeps labels for boxes that predate region HA; plan 1b adds instance ids for Auto Scaling boxes from a positive signal, and must write box records under the label.
- **Headroom is published only when `CARLOS_REGION_HA` is on** (`headroomFor`), so a one-box region never rewrites its edges file for headroom alone. `parseMemAvailableMiB` lives once, in `headroom.go`.
- **`Publisher.Commit` remembers every failed attempt since the last success** and adopts whichever one storage holds, so a lost response is never a false `ErrSuperseded`, even with stages in between or two failures in a row.
- **`Restore`** masks modes to permission bits, resolves an entry that is both a live path and the ancestor of another by keeping the higher seq (`RestoreStats.Superseded`), and refuses any entry whose object key, hash or size is not one this package could have written.
- **`descend`** refuses `""`, `.` and `..` components. The run loop retries repair and failing scans at most once per poll and logs only transitions; zero `Options` take the defaults.
- **`ImportWake`** checks the route exists before it writes.

**Add to "What plan 1b picks up":**

11. One inotify instance per agent rather than per host, and no re-hash of a file whose size, mtime and inode match what was last staged (final review, I4). As built, a box with many continuous hosts exhausts `max_user_instances` and falls back to polling full scans.
12. A `MaxFileBytes` cap in `StagePlatform`, matching `Restore`'s refusal; a rate limit on `Commit` during a manifest outage.
13. The box-record writer (`boxid.Publish` at agent start) and `carlos ops box bind`, keyed by label.
14. Box ids must not contain `.` (`Key("x.bind") == BindKey("x")`); the termination queue is one per box (per `region-host` instance), not one per region.
15. Before the first apply of the deployment-buckets module: check each deployment bucket for an existing bucket policy (`aws s3api get-bucket-policy`) and the consuming root for another `aws_s3_bucket_policy`. A bucket holds one policy, and this resource replaces it whole.
