copy: MV-N317 and MV-N363 each said in one literal, so the gate can read them
MV-N317 was the path from movesfloorgate_linux.go in front of a sentinel error in movesfloorgate.go. floorGateUnsafe in movesfloorgate.go now says it in one format, wrapping errFloorGateUnsafe for errors.Is; the text is unchanged (pinned by TestFloorGateUnsafeSaysTheSameSentence). MV-N363, the usage's move line, is one literal in main.go (moveUsageLine), still printed wrapped after "abort |"; the usage output is byte-identical. Both rows stay pending; with them flipped to approved, the gate passes. No wording changed.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
5 files changed,
+42
−7
docs/superpowers/plans/2026-10-06-moves-phase1-copy.md+2 −2internal/host/movesfloorgate.go+14 −0internal/host/movesfloorgate_linux.go+2 −3internal/host/movesfloorgate_linux_test.go+14 −0main.go+10 −2
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 d63981c21..16921bf60 100644 |
| --- a/docs/superpowers/plans/2026-10-06-moves-phase1-copy.md |
| +++ b/docs/superpowers/plans/2026-10-06-moves-phase1-copy.md |
| @@ -318,7 +318,7 @@ The anomaly page (Task 9a's quiet refusals, `internal/host/movesrefusals.go`): a |
| | MV-N317 | internal/host/movesfloorgate.go | <path> is not a regular file with one link, at most 256 bytes, owned by an expected account | pending | |
| | MV-N318 | internal/host/movesfloorgate.go | floor-allows-prev takes two absolute paths: the floor file and the verdict file | pending | |
| -Plan 6 Task 7's audit: MV-N317 is said in two parts. `movesfloorgate.go` holds the text after `<path>` (`errFloorGateUnsafe`), and `movesfloorgate_linux.go` puts the path in front of it (`"%s %w"`). No one expression says the whole row, so the gate cannot pass it as approved as the code stands. Approving it needs either the code to say it in one expression, or a ruling on how the gate reads a sentence put together from a path and a sentinel error. |
| +Plan 6 Task 7's audit: MV-N317 used to be said in two parts, the path from `movesfloorgate_linux.go` in front of a sentinel error in `movesfloorgate.go`. It is now one format in `movesfloorgate.go` (`floorGateUnsafe`), which still wraps the sentinel for `errors.Is` and prints the same text, so the gate can pass it once it is approved. |
| ## Plan 6 |
| @@ -330,7 +330,7 @@ Completed by Plan 6 Task 7. |
| - Task 4 adds no sentence: the detail answer's timings reuse MV-N319 when their read fails. |
| - Task 5 took MV-N385 to MV-N390, the box's pages (internal/host/movespaging.go, preflight L8), and speaks M-C20 verbatim from movespaging.go when the cut's stop evidence finds a stray process, so M-C20 is approved. Its fix round 1 (ruling 1) keeps M-C20 to operators, as the stray page's body and the agent's log line, never the app's members, so MV-N388, the body it replaced, is withdrawn. |
| - MV-N382 to MV-N384 and MV-N391 to MV-N399 were never used. Plan 5 owns MV-N325 to MV-N349. |
| -- Task 7's audit: MV-N363 is the `move` line of the usage in main.go, which wraps it over two lines after `abort |`. The gate matches an approved row inside one literal, so the line break stops it passing as approved as the code stands. Approving it needs the usage line unwrapped, or a ruling on how the gate reads the usage's wrapped lines. |
| +- Task 7's audit: MV-N363 is the `move` line of the usage. It is now one literal in main.go (`moveUsageLine`), still printed wrapped after `abort |`, so the gate can pass it once it is approved. |
| | id | file | text | status | |
| |---|---|---|---| |
diff --git a/internal/host/movesfloorgate.go b/internal/host/movesfloorgate.go| index 8100a2848..075856a4c 100644 |
| --- a/internal/host/movesfloorgate.go |
| +++ b/internal/host/movesfloorgate.go |
| @@ -42,6 +42,20 @@ var ( |
| errFloorGateUsage = errors.New("floor-allows-prev takes two absolute paths: the floor file and the verdict file") |
| ) |
| +// floorGateUnsafe is the refusal of a file the gate will not read (MV-N317), |
| +// said whole in one format so the copy gate can read it. It wraps |
| +// errFloorGateUnsafe for errors.Is, and its text is the sentence alone. |
| +func floorGateUnsafe(path string) error { |
| + return &floorGateUnsafeError{ |
| + msg: fmt.Sprintf("%s is not a regular file with one link, at most 256 bytes, owned by an expected account", path), |
| + } |
| +} |
| + |
| +type floorGateUnsafeError struct{ msg string } |
| + |
| +func (e *floorGateUnsafeError) Error() string { return e.msg } |
| +func (e *floorGateUnsafeError) Unwrap() error { return errFloorGateUnsafe } |
| + |
| // RunSystemFloorAllowsPrev is the verb's entry point. The floor file may be |
| // owned by root (root's apply writes it) or by the service account (the |
| // agent's refresh writes it); the verdict only by root. |
diff --git a/internal/host/movesfloorgate_linux.go b/internal/host/movesfloorgate_linux.go| index badf8c23e..55329aaad 100644 |
| --- a/internal/host/movesfloorgate_linux.go |
| +++ b/internal/host/movesfloorgate_linux.go |
| @@ -4,7 +4,6 @@ package host |
| import ( |
| "bytes" |
| - "fmt" |
| "io" |
| "os" |
| "path/filepath" |
| @@ -71,7 +70,7 @@ func readFloorGateFile(path string, owners []uint32, pinDirs bool) ([]byte, erro |
| return nil, &os.PathError{Op: "fstat", Path: path, Err: err} |
| } |
| if err := checkFloorGateStat(floorGateStat{mode: st.Mode, uid: st.Uid, nlink: uint64(st.Nlink), size: st.Size}, owners); err != nil { |
| - return nil, fmt.Errorf("%s %w", path, err) |
| + return nil, floorGateUnsafe(path) |
| } |
| floorGateAfterStat(path) |
| // Read one byte past the limit, so a file that grew after the fstat is |
| @@ -81,7 +80,7 @@ func readFloorGateFile(path string, owners []uint32, pinDirs bool) ([]byte, erro |
| return nil, &os.PathError{Op: "read", Path: path, Err: err} |
| } |
| if len(b) > floorGateMaxBytes { |
| - return nil, fmt.Errorf("%s %w", path, errFloorGateUnsafe) |
| + return nil, floorGateUnsafe(path) |
| } |
| return b, nil |
| } |
diff --git a/internal/host/movesfloorgate_linux_test.go b/internal/host/movesfloorgate_linux_test.go| index 2cc7d5901..d13603510 100644 |
| --- a/internal/host/movesfloorgate_linux_test.go |
| +++ b/internal/host/movesfloorgate_linux_test.go |
| @@ -4,6 +4,7 @@ package host |
| import ( |
| "errors" |
| + "fmt" |
| "os" |
| "path/filepath" |
| "strings" |
| @@ -385,3 +386,16 @@ func TestFloorGateMainExitCodes(t *testing.T) { |
| } |
| } |
| } |
| + |
| +// MV-N317 is said whole by floorGateUnsafe, and says exactly what the old |
| +// fmt.Errorf("%s %w", path, errFloorGateUnsafe) said, with errors.Is intact. |
| +func TestFloorGateUnsafeSaysTheSameSentence(t *testing.T) { |
| + const path = "/opt/carlos/moves-floor" |
| + err := floorGateUnsafe(path) |
| + if got, want := err.Error(), fmt.Errorf("%s %w", path, errFloorGateUnsafe).Error(); got != want { |
| + t.Errorf("err = %q, want %q", got, want) |
| + } |
| + if !errors.Is(err, errFloorGateUnsafe) { |
| + t.Error("errors.Is(floorGateUnsafe(path), errFloorGateUnsafe) = false") |
| + } |
| +} |
diff --git a/main.go b/main.go| index d366e39da..2ea17257f 100644 |
| --- a/main.go |
| +++ b/main.go |
| @@ -7,6 +7,7 @@ import ( |
| "io" |
| "log" |
| "os" |
| + "strings" |
| "github.com/carlosframework/platform/internal/edge" |
| "github.com/carlosframework/platform/internal/host" |
| @@ -202,6 +203,14 @@ func main() { |
| // usage (bare, or with -h), and that is where the detail belongs. A flat |
| // alphabetical list of every verb with every caveat inline was the previous |
| // shape, and it read as a wall rather than as an offer of what to do next. |
| +// moveUsageLine is the move verb's line of the usage (MV-N363), one literal |
| +// so the copy gate reads it whole. It prints wrapped like its neighbours. |
| +const moveUsageLine = "move an instance to another region: status | cut | abort | resolve --retry (operators; off unless the console enables it)" |
| + |
| +func moveUsageWrapped() string { |
| + return strings.Replace(moveUsageLine, "abort | ", "abort |\n ", 1) |
| +} |
| + |
| func usage(w io.Writer) { |
| fmt.Fprintln(w, `usage: carlos <command> [flags] |
| @@ -295,8 +304,7 @@ Your deployment — stand up and administer boxes, routes and accounts |
| economics bill: record the month's AWS bill; backhaul: what region |
| edges cost us this period |
| status --live: what every box last reported; --dry-run: leak check |
| - move move an instance to another region: status | cut | abort | |
| - resolve --retry (operators; off unless the console enables it) |
| + move `+moveUsageWrapped()+` |
| On the host — what systemd runs; rarely typed by hand |
| edge the mesh front door (router) |