carlos / platform Public

plan: region HA 1a rev 4, answering Astra plan round 3 with dry-run code

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Paul Campbell pushed by claude agent, operated by paul@keymail.dev 6e13c775098091e76f2434c578b343f8177c9fb4 parent 065bb9e
2 files changed, +305 −84
  • docs/superpowers/plans/2026-10-05-region-ha-1a-foundations.md +276 −84
  • docs/superpowers/plans/CODEX-PLAN-REGION-HA-1A-BRIEF-4.md +29 −0

CI passed — run details

diff --git a/docs/superpowers/plans/2026-10-05-region-ha-1a-foundations.md b/docs/superpowers/plans/2026-10-05-region-ha-1a-foundations.md
index 69a101363..49ef606a4 100644
--- a/docs/superpowers/plans/2026-10-05-region-ha-1a-foundations.md
+++ b/docs/superpowers/plans/2026-10-05-region-ha-1a-foundations.md
@@ -2307,6 +2307,82 @@ func TestRestoreSyncsEveryDirectoryItChangedBeforeReturning(t *testing.T) {
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**
@@ -2687,7 +2763,17 @@ func Restore(ctx context.Context, s release.Store, prefix, dir string) (Manifest
// Tombstones first, deepest first, so a directory that became a file
// is emptied before the file is written over it.
- touched := map[string]bool{} // directories whose entries changed
+ // 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 {
@@ -2696,13 +2782,13 @@ func Restore(ctx context.Context, s release.Store, prefix, dir string) (Manifest
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++
- touched[path.Dir(p)] = true
}
}
sort.Strings(live)
@@ -2712,6 +2798,7 @@ func Restore(ctx context.Context, s release.Store, prefix, dir string) (Manifest
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)
@@ -2720,35 +2807,33 @@ func Restore(ctx context.Context, s release.Store, prefix, dir string) (Manifest
return Manifest{}, st, fmt.Errorf("hostfiles: write %s: %w", p, err)
}
st.Written++
- // The file's own directory, and every directory above it up to
- // the root: any of them may have been created, or had something
- // in the way removed, on the way to this file.
- for d := path.Dir(p); ; d = path.Dir(d) {
- touched[d] = true
- if d == "." {
- break
- }
- }
}
- // Deepest first, so a directory is durable before the entry that
- // names it is.
+ // 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 {
- di, dj := strings.Count(dirs[i], "/"), strings.Count(dirs[j], "/")
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 _, err := root.Lstat(filepath.FromSlash(d)); errors.Is(err, fs.ErrNotExist) {
- continue // emptied by a tombstone and then replaced by a file
+ 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)
@@ -2909,7 +2994,7 @@ Expected: PASS.
`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 directory in which a name was created, replaced or removed, deepest first, and fails if any of those syncs fails. 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.
+**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**
@@ -3153,7 +3238,7 @@ func TestSyncDirDoesNotTombstoneWhatASkippedDirectoryHolds(t *testing.T) {
}
```
-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.
+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
@@ -3334,49 +3419,9 @@ func TestSyncDirDoesNotBlockOnADirectorySwappedForAFIFO(t *testing.T) {
}
}
-// 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)
- }
- })
- }
-}
-
-// Finding 2 of plan round 2: a file that still exists is never tombstoned
+// Plan round 2, finding 2: a file that still exists is never tombstoned
// just because it could not be read.
-func TestAnUnreadableFileKeepsItsPublishedEntry(t *testing.T) {
+func TestSyncDirKeepsAnUnreadableFilesPublishedEntry(t *testing.T) {
if os.Getuid() == 0 {
t.Skip("root can read anything")
}
@@ -3397,17 +3442,35 @@ func TestAnUnreadableFileKeepsItsPublishedEntry(t *testing.T) {
if st.Deleted != 0 || p.Dirty() || !reflect.DeepEqual(st.Unreadable, []string{"locked"}) {
t.Fatalf("stats = %+v dirty = %v", st, p.Dirty())
}
- if _, err := publishOne(ctx, p, tree(t, src), "locked", nil); err == nil {
- t.Fatal("publishOne reported success for a file it could not read")
+}
+
+// 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)
}
- if p.Dirty() {
- t.Fatal("publishOne staged a change for a file it could not read")
+ 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)
}
}
-// A regular file replaced by another regular file mid-look is read again
-// later, not tombstoned and not read as if it were the first.
-func TestARegularFileReplacedMidLookIsRetriedNotDeleted(t *testing.T) {
+// 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)
@@ -3415,7 +3478,7 @@ func TestARegularFileReplacedMidLookIsRetriedNotDeleted(t *testing.T) {
t.Fatal(err)
}
commit(t, p)
- replace := func() {
+ swapAt(t, 1, func() {
tmp := filepath.Join(src, "f.new")
if err := os.WriteFile(tmp, []byte("v2"), 0o640); err != nil {
t.Error(err)
@@ -3423,19 +3486,13 @@ func TestARegularFileReplacedMidLookIsRetriedNotDeleted(t *testing.T) {
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)
}
- swapAt(t, 1, replace)
- 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")
+ 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)
}
}
```
@@ -3891,7 +3948,7 @@ Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>"
**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`, `internal/hostfiles/watch_linux_test.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`.
@@ -4284,9 +4341,123 @@ func TestRunIgnoresAnEventForAPathThatEscapes(t *testing.T) {
}
```
+`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 -count=1`
+Run: `go test ./internal/hostfiles/ -run 'Run|PublishOne' -count=1`
Expected: FAIL to build, `undefined: run`.
- [ ] **Step 6: Write `run.go`**
@@ -4565,6 +4736,15 @@ func TestWatcherPublishesAFileStillHeldOpen(t *testing.T) {
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) {
@@ -4689,7 +4869,7 @@ func TestWatcherOverflowMarksCoverageUncertainUntilRepaired(t *testing.T) {
// 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 TestARepairDoesNotEraseAFailureThatArrivedDuringItsWalk(t *testing.T) {
+func TestWatcherRepairDoesNotEraseAFailureThatArrivedDuringItsWalk(t *testing.T) {
w, _ := unwatched(t)
w.handle(0, unix.IN_Q_OVERFLOW, "")
orig := afterRepairWalk
@@ -4964,7 +5144,7 @@ Expected: no gofmt output, PASS, both cross-builds succeed.
CARLOS_REQUIRE_INOTIFY=1 go test ./internal/hostfiles/ -run Watcher -count=1 -race -v
-Expected: seven tests `PASS`, none `SKIP`. 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.
+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.
@@ -6498,3 +6678,15 @@ Findings are in `CODEX-PLAN-REGION-HA-1A-R2.md`. Round 2 marked twelve of round
| 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 |
diff --git a/docs/superpowers/plans/CODEX-PLAN-REGION-HA-1A-BRIEF-4.md b/docs/superpowers/plans/CODEX-PLAN-REGION-HA-1A-BRIEF-4.md
new file mode 100644
index 000000000..cc8f3c7c3
--- /dev/null
+++ b/docs/superpowers/plans/CODEX-PLAN-REGION-HA-1A-BRIEF-4.md
@@ -0,0 +1,29 @@
+# Review brief: region HA plan 1a (foundations), round 4
+
+Read `CODEX-PLAN-REGION-HA-1A-BRIEF-1.md` first: its "Fixed: do not
+re-argue" and "What to check" sections still apply, to the plan at the
+commit named in the prompt.
+
+## Round 4
+
+This revision answers your round 3 (`CODEX-PLAN-REGION-HA-1A-R3.md`); the
+plan's last section is its dispositions table. The fixes were dry-run:
+the host-files package was written into the tree, built for linux,
+darwin and windows and tested with the race detector; Task 5's files
+alone, and Tasks 5 and 6's alone, were also built and tested, to check
+each task's gate passes in order. Then the code was removed.
+
+Give a disposition for the remaining R2-3 issue and your four round 3
+findings: resolved, partly resolved (say what is left), or not resolved.
+Then report only new findings, including any these fixes introduce:
+`Restore`'s `touched` set and its sync loop, the split of the publishOne
+tests into Task 7, and the held-open test's second write.
+
+Check task order once more for every task, not only 6 and 7: does any
+task's test or code use an identifier that a later task defines?
+
+Do not re-raise anything you have marked resolved in any round.
+
+## Output
+
+As round 1. End with a verdict: ready to build, or not ready.