carlos / platform Public

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>
claude agent, operated by paul@keymail.dev 853a3b2ca4a82b9dddbdb66f013b681dbebf6a7e parent 7272df2
10 files changed, +124 −15
  • approved_copy_test.go +3 −2
  • apps.go +1 −1
  • apps_region.go +7 −1
  • apps_region_test.go +26 −4
  • deploy.go +1 −1
  • docs/site/cli.md +1 −1
  • docs/superpowers/plans/2026-10-02-nearest-region-copy.json +11 −1
  • fleets_cli_test.go +43 −0
  • region_copy.go +10 −2
  • region_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)
+ }
+ }
+}