copy: D4 corrected, C15/C25 no-home fallbacks, no C13b on fleet placements (Paul's copy review 2026-10-03)
D4 now says where fleet, pooled, static and replica instances start. C15 and C25 read "the home region" when the console names no home. apps create --place fleet/<name> no longer prints C13b, since the fleet governs placement.
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
10 files changed,
+124
−15
approved_copy_test.go+3 −2apps.go+1 −1apps_region.go+7 −1apps_region_test.go+26 −4deploy.go+1 −1docs/site/cli.md+1 −1docs/superpowers/plans/2026-10-02-nearest-region-copy.json+11 −1fleets_cli_test.go+43 −0region_copy.go+10 −2region_copy_test.go+21 −2
diff --git a/approved_copy_test.go b/approved_copy_test.go| index 508daa7ab..9d6aae50c 100644 |
| --- a/approved_copy_test.go |
| +++ b/approved_copy_test.go |
| @@ -15,7 +15,7 @@ import ( |
| ) |
| // approvedCopyIDs is the copy inventory of the nearest-region plan, Task 0 |
| -// Step 1, pinned as a literal: C1 to C31, R-*, O1 to O20, U, S, G, the 34 |
| +// Step 1, pinned as a literal: C1 to C31, the C15 and C25 no-home variants, R-*, O1 to O20, U, S, G, the 34 |
| // C23 place names, D1 to D10 and F1. The record must name every one of them, |
| // once, and nothing else. A record that loses an entry fails here even though |
| // every entry left in it is applied. |
| @@ -26,7 +26,8 @@ var approvedCopyIDs = []string{ |
| "C27", "C28", "C29", "C31", "O1", "O2", "O3", "O4", "O5", "O6", |
| "O7", "O8", "O9", "O10", "O11", "O12", "O13", "O14", "O15", "O16", |
| "O17", "O18", "O19", "O20", "C9", "C10", "C11", "C12", "C13a", |
| - "C13b", "C14", "C15", "C15b", "C15c", "C25", "C30", "C30b", "C30c", |
| + "C13b", "C14", "C15", "C15.nohome", "C15b", "C15c", "C25", "C25.nohome", |
| + "C30", "C30b", "C30c", |
| "C22a", "C22b", "C22c", "C22d", "U1", "U2", "U3", "S1", "S2", |
| "S3", "G1", "G2", "G3", "G4", "G5", "G6", "C23-us-east-1", "C23-us-east-2", |
| "C23-us-west-1", "C23-us-west-2", "C23-ca-central-1", "C23-ca-west-1", |
diff --git a/apps.go b/apps.go| index 10068422e..1a1456e2f 100644 |
| --- a/apps.go |
| +++ b/apps.go |
| @@ -505,7 +505,7 @@ func runAppsCreate(args []string) { |
| fmt.Fprintf(stdout, "%s %s in account %s — it's yours\n", c.ok("created app"), res.App, res.Account) |
| fmt.Fprintf(stdout, " open: %s\n", c.link(res.URL)) |
| fmt.Fprintf(stdout, " ship: carlos ship --account %s --app %s ...\n", res.Account, res.App) |
| - printAppRegion(stdout, res, region2) |
| + printAppRegion(stdout, res, region2, place2 != "") |
| // The app WAS created (everything above already printed and is true) — |
| // what's still unverified is whether --place actually stuck. The server |
| // echoes back what it really stored (handleAPICreateApp's "placement" |
diff --git a/apps_region.go b/apps_region.go| index 126d85141..0fc9acf60 100644 |
| --- a/apps_region.go |
| +++ b/apps_region.go |
| @@ -18,7 +18,11 @@ import ( |
| // read the stored region back (it then answers the home with no source). That |
| // says nothing about whether --region was applied, so it prints nothing: not |
| // C13b, and not a C9 to C12 line that would name a source nobody knows. |
| -func printAppRegion(w io.Writer, res apiCreateAppResult, asked string) { |
| +// |
| +// onFleet is true when the create named a fleet (--place fleet/<name>). The |
| +// fleet governs where its instances run, so C13b ("--region was not applied", |
| +// "this app's instances start in <home>") would be wrong: it prints nothing. |
| +func printAppRegion(w io.Writer, res apiCreateAppResult, asked string, onFleet bool) { |
| switch { |
| case res.Region == "" && res.RegionSource == "": |
| if asked != "" { |
| @@ -26,6 +30,8 @@ func printAppRegion(w io.Writer, res apiCreateAppResult, asked string) { |
| } |
| case res.RegionSource == "": |
| // Region read-back failed on the console. Say nothing. |
| + case asked != "" && res.RegionSource != "chosen" && onFleet: |
| + // The fleet decides placement. Say nothing. |
| case asked != "" && res.RegionSource != "chosen": |
| fmt.Fprintln(w, regionNotAppliedSwitchOff(res.RegionName)) |
| case res.RegionsAvailable >= 2: |
diff --git a/apps_region_test.go b/apps_region_test.go| index c2a3390bf..854dce0f6 100644 |
| --- a/apps_region_test.go |
| +++ b/apps_region_test.go |
| @@ -88,7 +88,7 @@ func TestPrintAppRegion(t *testing.T) { |
| } |
| for _, c := range cases { |
| var b bytes.Buffer |
| - printAppRegion(&b, c.res, c.asked) |
| + printAppRegion(&b, c.res, c.asked, false) |
| if b.String() != c.want { |
| t.Errorf("%s: %q, want %q", c.name, b.String(), c.want) |
| } |
| @@ -108,7 +108,7 @@ func TestPrintAppRegionFullLines(t *testing.T) { |
| } |
| for _, c := range cases { |
| var b bytes.Buffer |
| - printAppRegion(&b, apiCreateAppResult{Region: "uk", RegionName: "UK · London", RegionSource: c.source, RegionsAvailable: 3}, c.asked) |
| + printAppRegion(&b, apiCreateAppResult{Region: "uk", RegionName: "UK · London", RegionSource: c.source, RegionsAvailable: 3}, c.asked, false) |
| if b.String() != c.want { |
| t.Errorf("source %q: %q, want %q", c.source, b.String(), c.want) |
| } |
| @@ -119,7 +119,7 @@ func TestPrintAppRegionFullLines(t *testing.T) { |
| // the home with source "default". The CLI says so (C13b), in full. |
| func TestPrintAppRegionSwitchOffFullLine(t *testing.T) { |
| var b bytes.Buffer |
| - printAppRegion(&b, apiCreateAppResult{Region: "ie", RegionName: "Ireland · Dublin", RegionSource: "default", RegionsAvailable: 1}, "uk") |
| + printAppRegion(&b, apiCreateAppResult{Region: "ie", RegionName: "Ireland · Dublin", RegionSource: "default", RegionsAvailable: 1}, "uk", false) |
| want := "This console is not assigning app regions yet, so --region was not applied. This app's instances start in Ireland · Dublin.\n" |
| if b.String() != want { |
| t.Fatalf("got %q, want %q", b.String(), want) |
| @@ -134,7 +134,7 @@ func TestPrintAppRegionReadBackFailureIsSilent(t *testing.T) { |
| for _, asked := range []string{"", "uk", "ie"} { |
| for _, avail := range []int{0, 1, 3} { |
| var b bytes.Buffer |
| - printAppRegion(&b, apiCreateAppResult{Region: "ie", RegionName: "Ireland · Dublin", RegionSource: "", RegionsAvailable: avail}, asked) |
| + printAppRegion(&b, apiCreateAppResult{Region: "ie", RegionName: "Ireland · Dublin", RegionSource: "", RegionsAvailable: avail}, asked, false) |
| if b.String() != "" { |
| t.Errorf("asked %q available %d: printed %q, want nothing", asked, avail, b.String()) |
| } |
| @@ -178,3 +178,25 @@ func TestOfferCreateAppPrintsRegionLine(t *testing.T) { |
| t.Fatalf("the timezone was printed: %q", out.String()) |
| } |
| } |
| + |
| +// A create that named a fleet is placed by the fleet: C13b would be wrong, so |
| +// nothing prints. Every other case is unchanged by onFleet. |
| +func TestPrintAppRegionOnFleet(t *testing.T) { |
| + off := apiCreateAppResult{Region: "ie", RegionName: "Ireland · Dublin", RegionSource: "default", RegionsAvailable: 2} |
| + var b bytes.Buffer |
| + printAppRegion(&b, off, "uk", true) |
| + if b.String() != "" { |
| + t.Fatalf("on a fleet: %q, want nothing", b.String()) |
| + } |
| + b.Reset() |
| + printAppRegion(&b, off, "uk", false) |
| + if b.String() != regionNotAppliedSwitchOff("Ireland · Dublin")+"\n" { |
| + t.Fatalf("off a fleet: %q", b.String()) |
| + } |
| + // A console that did apply --region still says where the app starts. |
| + b.Reset() |
| + printAppRegion(&b, apiCreateAppResult{Region: "uk", RegionName: "UK · London", RegionSource: "chosen", RegionsAvailable: 2}, "uk", true) |
| + if b.String() != regionCreatedLine("UK · London", "chosen")+"\n" { |
| + t.Fatalf("chosen on a fleet: %q", b.String()) |
| + } |
| +} |
diff --git a/deploy.go b/deploy.go| index 9213b5a02..fb820eb6e 100644 |
| --- a/deploy.go |
| +++ b/deploy.go |
| @@ -576,7 +576,7 @@ func offerCreateApp(ctx context.Context, sess *apiSession, acct apiAcct, na *noA |
| log.Fatalf("deploy: create app: %v", err) |
| } |
| fmt.Fprintf(stdout, "%s %s in account %s\n", colorFor(stdout).ok("created app"), res.App, res.Account) |
| - printAppRegion(stdout, res, region) |
| + printAppRegion(stdout, res, region, false) |
| return res.App |
| } |
diff --git a/docs/site/cli.md b/docs/site/cli.md| index 1d61da09a..a1e2ffbbe 100644 |
| --- a/docs/site/cli.md |
| +++ b/docs/site/cli.md |
| @@ -627,7 +627,7 @@ bind it here, then restart the instance to pick the values up. |
| carlos instances set-environment --app jam --host new-queue.jam.bab.oncarlos.com --environment jam-canary |
| ``` |
| -Without `--region`, a new instance starts in the app's region. |
| +Without `--region`, a new instance starts in the app's region. An instance in a fleet starts where the fleet runs, and a pooled, static or replica instance starts in the home region. |
| An instance can also be an account's **replica**: a second instance of the |
| same app, holding a one-way mirror of the live instance's data, placed near |
diff --git a/docs/superpowers/plans/2026-10-02-nearest-region-copy.json b/docs/superpowers/plans/2026-10-02-nearest-region-copy.json| index 19e4da7cf..9a15c3d8c 100644 |
| --- a/docs/superpowers/plans/2026-10-02-nearest-region-copy.json |
| +++ b/docs/superpowers/plans/2026-10-02-nearest-region-copy.json |
| @@ -274,6 +274,11 @@ |
| "file": "region_copy.go", |
| "text": "%s already runs in %s. --region only applies when deploy creates an app. To place one instance elsewhere, use carlos instances create --region." |
| }, |
| + { |
| + "id": "C15.nohome", |
| + "file": "region_copy.go", |
| + "text": "%s already runs in the home region. --region only applies when deploy creates an app. To place one instance elsewhere, use carlos instances create --region." |
| + }, |
| { |
| "id": "C15b", |
| "file": "region_copy.go", |
| @@ -289,6 +294,11 @@ |
| "file": "region_copy.go", |
| "text": "canary %s already runs in %s. --region applies only when canary creates the host. Run carlos canary rm first to start it somewhere else." |
| }, |
| + { |
| + "id": "C25.nohome", |
| + "file": "region_copy.go", |
| + "text": "canary %s already runs in the home region. --region applies only when canary creates the host. Run carlos canary rm first to start it somewhere else." |
| + }, |
| { |
| "id": "C30", |
| "file": "region_copy.go", |
| @@ -572,7 +582,7 @@ |
| { |
| "id": "D4", |
| "file": "docs/site/cli.md", |
| - "text": "Without `--region`, a new instance starts in the app's region." |
| + "text": "Without `--region`, a new instance starts in the app's region. An instance in a fleet starts where the fleet runs, and a pooled, static or replica instance starts in the home region." |
| }, |
| { |
| "id": "D5", |
diff --git a/fleets_cli_test.go b/fleets_cli_test.go| index ed43b284e..6125e3850 100644 |
| --- a/fleets_cli_test.go |
| +++ b/fleets_cli_test.go |
| @@ -792,3 +792,46 @@ func TestRunAppsCreatePlacementVerification(t *testing.T) { |
| } |
| }) |
| } |
| + |
| +// A create that names a fleet (--place fleet/<name>) is placed by the fleet, |
| +// so a switch-off console's "--region was not applied" (C13b) would be wrong |
| +// there: it prints nothing. A plain create with the same response still says |
| +// it (C13b). |
| +func TestRunAppsCreateRegionNoticeOnFleet(t *testing.T) { |
| + bin := buildCarlos(t) |
| + const c13b = "This console is not assigning app regions yet, so --region was not applied." |
| + |
| + run := func(t *testing.T, body string, extra ...string) string { |
| + t.Helper() |
| + ts := fakeCreateEcho(t, body) |
| + home := t.TempDir() |
| + creds := filepath.Join(home, "credentials") |
| + writeTestCredential(t, creds, ts.URL) |
| + args := append([]string{"apps", "create", "-app", "titogo", "-account", "bab", "-region", "uk"}, extra...) |
| + cmd := exec.Command(bin, args...) |
| + cmd.Env = append(os.Environ(), "CARLOS_CREDENTIALS="+creds, "HOME="+home, "CARLOS_CONSOLE=") |
| + cmd.Dir = home |
| + out, err := cmd.CombinedOutput() |
| + if err != nil { |
| + t.Fatalf("exited nonzero: %v\n%s", err, out) |
| + } |
| + return string(out) |
| + } |
| + const switchOff = `"region":"ie","region_name":"Ireland · Dublin","region_source":"default","regions_available":2` |
| + |
| + t.Run("fleet placement: no C13b", func(t *testing.T) { |
| + out := run(t, `{"account":"bab","app":"titogo","url":"http://x/a/bab/apps/titogo","placement":"fleet/pi",`+switchOff+`}`, "-place", "fleet/pi") |
| + if strings.Contains(out, c13b) { |
| + t.Fatalf("C13b printed for a fleet placement:\n%s", out) |
| + } |
| + if !strings.Contains(out, "placed on fleet/pi") { |
| + t.Fatalf("output missing the placed confirmation:\n%s", out) |
| + } |
| + }) |
| + t.Run("no fleet: C13b", func(t *testing.T) { |
| + out := run(t, `{"account":"bab","app":"titogo","url":"http://x/a/bab/apps/titogo",`+switchOff+`}`) |
| + if !strings.Contains(out, c13b) { |
| + t.Fatalf("C13b missing for a plain create:\n%s", out) |
| + } |
| + }) |
| +} |
diff --git a/region_copy.go b/region_copy.go| index b770924e7..6d02a3160 100644 |
| --- a/region_copy.go |
| +++ b/region_copy.go |
| @@ -34,8 +34,12 @@ func instanceCreatedSuffix(channel, place string) string { |
| return fmt.Sprintf("channel %s, %s", channel, place) |
| } |
| -// deployRegionMismatch is C15. |
| +// deployRegionMismatch is C15, or C15.nohome when the console has no named |
| +// home region to put in the place. |
| func deployRegionMismatch(app, place string) string { |
| + if place == "" { |
| + return fmt.Sprintf("%s already runs in the home region. --region only applies when deploy creates an app. To place one instance elsewhere, use carlos instances create --region.", app) |
| + } |
| return fmt.Sprintf("%s already runs in %s. --region only applies when deploy creates an app. To place one instance elsewhere, use carlos instances create --region.", app, place) |
| } |
| @@ -51,8 +55,12 @@ func deployRegionUnknown(app string) string { |
| return fmt.Sprintf("the console could not read %s's region, so deploy stopped before shipping anything. Try again, or run without --region.", app) |
| } |
| -// canaryRegionMismatch is C25. |
| +// canaryRegionMismatch is C25, or C25.nohome when the console has no named |
| +// home region to put in the place. |
| func canaryRegionMismatch(host, place string) string { |
| + if place == "" { |
| + return fmt.Sprintf("canary %s already runs in the home region. --region applies only when canary creates the host. Run carlos canary rm first to start it somewhere else.", host) |
| + } |
| return fmt.Sprintf("canary %s already runs in %s. --region applies only when canary creates the host. Run carlos canary rm first to start it somewhere else.", host, place) |
| } |
diff --git a/region_copy_test.go b/region_copy_test.go| index 632567784..051d14164 100644 |
| --- a/region_copy_test.go |
| +++ b/region_copy_test.go |
| @@ -59,8 +59,8 @@ func TestRegionCLICopyMatchesTheApprovedRecord(t *testing.T) { |
| if err != nil { |
| t.Fatal(err) |
| } |
| - want := []string{"C9", "C10", "C11", "C12", "C13a", "C13b", "C14", "C15", "C15b", "C15c", |
| - "C22a", "C22b", "C22c", "C22d", "C25", "C30", "C30b", "C30c"} |
| + want := []string{"C9", "C10", "C11", "C12", "C13a", "C13b", "C14", "C15", "C15.nohome", "C15b", "C15c", |
| + "C22a", "C22b", "C22c", "C22d", "C25", "C25.nohome", "C30", "C30b", "C30c"} |
| var got []string |
| for _, e := range rec.Entries { |
| if e.File != "region_copy.go" { |
| @@ -82,3 +82,22 @@ func TestRegionCLICopyMatchesTheApprovedRecord(t *testing.T) { |
| t.Errorf("record entries for region_copy.go = %v, want %v", got, want) |
| } |
| } |
| + |
| +// C15 and C25 with no named home region read "the home region", not an empty |
| +// place: the approved .nohome texts. With a place, the approved C15 and C25. |
| +func TestRegionMismatchWithoutAHome(t *testing.T) { |
| + for _, c := range []struct{ name, got, want string }{ |
| + {"C15.nohome", deployRegionMismatch("blog", ""), |
| + "blog already runs in the home region. --region only applies when deploy creates an app. To place one instance elsewhere, use carlos instances create --region."}, |
| + {"C15", deployRegionMismatch("blog", "Ireland"), |
| + "blog already runs in Ireland. --region only applies when deploy creates an app. To place one instance elsewhere, use carlos instances create --region."}, |
| + {"C25.nohome", canaryRegionMismatch("c.go.tito.io", ""), |
| + "canary c.go.tito.io already runs in the home region. --region applies only when canary creates the host. Run carlos canary rm first to start it somewhere else."}, |
| + {"C25", canaryRegionMismatch("c.go.tito.io", "Ireland"), |
| + "canary c.go.tito.io already runs in Ireland. --region applies only when canary creates the host. Run carlos canary rm first to start it somewhere else."}, |
| + } { |
| + if c.got != c.want { |
| + t.Errorf("%s: %q, want %q", c.name, c.got, c.want) |
| + } |
| + } |
| +} |