carlos / platform Public

provision: the landing's rollback sentence for a files directory it did not write (MV-N180, final-review fix 1)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
claude agent, operated by paul@keymail.dev d86852fc67944d3d90c80e6daa0f245624fdcea5 parent 590a443
4 files changed, +11 −3
  • approved_moves_copy_test.go +1 −1
  • docs/superpowers/plans/2026-10-06-moves-phase1-copy.md +2 −1
  • internal/provision/move_test.go +1 −1
  • internal/provision/provision.go +7 −0
diff --git a/approved_moves_copy_test.go b/approved_moves_copy_test.go
index bdb100929..aa34c8d7b 100644
--- a/approved_moves_copy_test.go
+++ b/approved_moves_copy_test.go
@@ -69,7 +69,7 @@ var movesCopyIDs = []string{
"MV-N157", "MV-N158", "MV-N159", "MV-N160", "MV-N161", "MV-N162",
"MV-N163", "MV-N164", "MV-N165", "MV-N166", "MV-N167", "MV-N168",
"MV-N169", "MV-N170", "MV-N171", "MV-N172", "MV-N173", "MV-N174",
- "MV-N175", "MV-N176", "MV-N177", "MV-N178", "MV-N179",
+ "MV-N175", "MV-N176", "MV-N177", "MV-N178", "MV-N179", "MV-N180",
}
// copyRow is one row of the record. The File cell is the file the sentence is
diff --git a/docs/superpowers/plans/2026-10-06-moves-phase1-copy.md b/docs/superpowers/plans/2026-10-06-moves-phase1-copy.md
index 18ac421a3..7292d1f8b 100644
--- a/docs/superpowers/plans/2026-10-06-moves-phase1-copy.md
+++ b/docs/superpowers/plans/2026-10-06-moves-phase1-copy.md
@@ -210,7 +210,7 @@ Columns: id, file, text, status.
## Plan 2
-(Task 10 rows, then task E-switch's MV-N173 to MV-N179; the rest appended by Plan 2 Task 11)
+(Task 10 rows, then task E-switch's MV-N173 to MV-N179, then the final-review fix's MV-N180; the rest appended by Plan 2 Task 11)
| id | file | text | status |
|---|---|---|---|
@@ -226,6 +226,7 @@ Columns: id, file, text, status.
| MV-N177 | internal/console/apiinstancemove.go | the host roster does not allow this move. the source and target must be active boxes in <from> and <to> | pending |
| MV-N178 | internal/console/apiinstancemove.go | the instance or its boxes changed under this request. read its state and try again | pending |
| MV-N179 | internal/console/apiinstancemove.go | the move could not be recorded and is held until an operator checks the record | pending |
+| MV-N180 | internal/provision/provision.go | the files directory already holds files this move did not write | pending |
## Plan 3
diff --git a/internal/provision/move_test.go b/internal/provision/move_test.go
index e55036c30..529935a77 100644
--- a/internal/provision/move_test.go
+++ b/internal/provision/move_test.go
@@ -596,7 +596,7 @@ func TestMarkRollbackRecordsASentenceOnALandingRecord(t *testing.T) {
if err := MoveMarkRollback(g, "some error text")(rec); err == nil || errors.Is(err, ErrMoveConflict) || rec.Move.Reason != "" {
t.Fatalf("an unrecorded reason: %v %+v", err, rec.Move)
}
- for _, reason := range []string{RollbackConfigChanged, RollbackLandingDeadline, RollbackClaimWired, RollbackLandingLayout, RollbackInstallMismatch} {
+ for _, reason := range []string{RollbackConfigChanged, RollbackLandingDeadline, RollbackClaimWired, RollbackLandingLayout, RollbackInstallMismatch, RollbackFilesOccupied} {
rec := &Record{Region: "uk", Move: &MoveState{ID: g.ID, Incarnation: g.Incarnation, State: MoveLanding, LandingPhase: LandChecking}}
if err := MoveMarkRollback(g, reason)(rec); err != nil || rec.Move.State != MoveLanding || rec.Move.LandingPhase != LandChecking || rec.Move.Reason != reason {
t.Fatalf("%q: %v %+v", reason, err, rec.Move)
diff --git a/internal/provision/provision.go b/internal/provision/provision.go
index dc2d7ecc0..1dba6ebb3 100644
--- a/internal/provision/provision.go
+++ b/internal/provision/provision.go
@@ -1561,6 +1561,11 @@ const (
// RollbackInstallMismatch: the built database does not hash to the
// install manifest's sha256, or does not pass its integrity check.
RollbackInstallMismatch = "the installed database does not match the build"
+ // RollbackFilesOccupied: the files directory holds files, or <host>.env
+ // exists, and this landing did not write them (no earlier stay of this
+ // installation registered them, so nothing retired them). They are not
+ // the landing's to clear (final review, finding 1).
+ RollbackFilesOccupied = "the files directory already holds files this move did not write"
)
// MoveMarkRollback records on a landing record why the landing stopped
@@ -1593,6 +1598,8 @@ func MoveMarkRollback(g MoveGuard, reason string) func(*Record) error {
rec.Move.Reason = RollbackLandingLayout
case RollbackInstallMismatch:
rec.Move.Reason = RollbackInstallMismatch
+ case RollbackFilesOccupied:
+ rec.Move.Reason = RollbackFilesOccupied
default:
return fmt.Errorf("provision: rollback reason %q has no recorded sentence", reason)
}