Split out of #12938, which restored the composition but deliberately did not touch exit semantics (the dispatch bounded it to "only what is composed/visible", and the artifact-less run is byte-pinned to today's output, exit 0 included).
The remaining sliver
After #12938, os migrate plan can still compute a plan over a metadata set that is a fraction of the target's tables, in exactly one way: a host objectstack.config.{ts,js,mjs}exists but cannot be loaded. A missing environment variable is the ordinary cause — ObjectStack Cloud's own control-plane config throws without AUTH_SECRET, and that shape is representative rather than exotic.
Today that path:
- warns loudly on stderr, naming the config, the underlying error and the consequence ("an empty plan here is UNMEASURED, not 'in sync'");
- carries
composition.hostConfigLoaded: false in os migrate plan --json; - and exits 0, printing "Physical schema is in sync with metadata — nothing to migrate" when the reduced set happens to be clean.
Composing the platform floor still happens on that path, so managedTables rises to 9 — above the five-table artifact-less baseline that consumer-side coverage gates are calibrated against (objectstack-ai/cloud#1705's workflow uses managedTables <= 5). A count alone therefore cannot distinguish "the config blew up" from "this deployment is genuinely small"; only hostConfigLoaded can.
#12938's own body raised this class in its non-normative "Suggested direction" section: "a plan computed over a metadata set that is a fraction of the target's tables is UNMEASURED, and today it is indistinguishable from 'in sync'".
The decision, stated so it can be ruled on rather than guessed
Should os migrate plan (and apply) exit non-zero when a host config is present and could not be loaded?
No recommendation is offered here; #12938's dispatch ruled the axis out of scope rather than deciding it, so this is a fresh call.
Where to look
Generated by Claude Code
Split out of #12938, which restored the composition but deliberately did not touch exit semantics (the dispatch bounded it to "only what is composed/visible", and the artifact-less run is byte-pinned to today's output, exit 0 included).
The remaining sliver
After #12938,
os migrate plancan still compute a plan over a metadata set that is a fraction of the target's tables, in exactly one way: a hostobjectstack.config.{ts,js,mjs}exists but cannot be loaded. A missing environment variable is the ordinary cause — ObjectStack Cloud's own control-plane config throws withoutAUTH_SECRET, and that shape is representative rather than exotic.Today that path:
composition.hostConfigLoaded: falseinos migrate plan --json;Composing the platform floor still happens on that path, so
managedTablesrises to 9 — above the five-table artifact-less baseline that consumer-side coverage gates are calibrated against (objectstack-ai/cloud#1705's workflow usesmanagedTables <= 5). A count alone therefore cannot distinguish "the config blew up" from "this deployment is genuinely small"; onlyhostConfigLoadedcan.#12938's own body raised this class in its non-normative "Suggested direction" section: "a plan computed over a metadata set that is a fraction of the target's tables is UNMEASURED, and today it is indistinguishable from 'in sync'".
The decision, stated so it can be ruled on rather than guessed
Should
os migrate plan(andapply) exit non-zero when a host config is present and could not be loaded?migrateever read the config beforeos migrate plan/applycompose neither the host config nor PlatformObjectsPlugin — they diff a five-table subset and cannot apply the drift their own message names #12938) starts failing for any project whose config needs environment the operator did not supply to this one-shot run. That is a behaviour regression for a population that has nothing to do with this defect.hostConfigLoadeddiscriminator, and let consumers gate on it. Whatos migrate plan/applycompose neither the host config nor PlatformObjectsPlugin — they diff a five-table subset and cannot apply the drift their own message names #12938 shipped. Cost: a human skimming a green plan can still miss the warning; the safety depends on someone readingcomposition.hostConfigLoaded.--require-host-config/--strict-coverage) that turns B into A on demand. Cost: one more surface, and the default still reads green.No recommendation is offered here; #12938's dispatch ruled the axis out of scope rather than deciding it, so this is a fresh call.
Where to look
packages/cli/src/utils/schema-migration-plugins.ts— thecatchthat reports the unloadable config, andhostConfigLoaded.packages/cli/src/commands/migrate/plan.ts— thecompositionblock in the--jsonpayload.Generated by Claude Code