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>
4 files changed,
+11
−3
approved_moves_copy_test.go+1 −1docs/superpowers/plans/2026-10-06-moves-phase1-copy.md+2 −1internal/provision/move_test.go+1 −1internal/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) |
| } |