fix(cli): os dev -a and os start --artifact serve the named artifact beside a cwd objectstack.config.ts — one artifact precedence for start, dev and the serve child - #21549
Conversation
Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016GiHYRmLSNWTfbX9gVQkpz
Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016GiHYRmLSNWTfbX9gVQkpz
Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016GiHYRmLSNWTfbX9gVQkpz
Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016GiHYRmLSNWTfbX9gVQkpz
Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016GiHYRmLSNWTfbX9gVQkpz
…tifact-flag-precedence
📓 Docs Drift CheckThis PR changes 1 package(s): 27 hand-written doc(s) name something this change touched — list omitted above 15 rows. Re-derive on the tree named below: ⛔ 8 release-owned page(s) also affected — read-only, see AGENTS.md Documentation Guardrails. What this run could not see
Coarse fallback — 27 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): Which tree this was computed onThis run read A worktree cut from an older # while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 899ff6d63045e5ca752894718f21e27161a1cc25 && git checkout 899ff6d63045e5ca752894718f21e27161a1cc25
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin 5dbcee8a6d4f3a3feac3ab0a64e5555a1f45cf7c f634c5bf09327f47cfb2f260c9262d741e84bea7 && git checkout -B drift-repro 5dbcee8a6d4f3a3feac3ab0a64e5555a1f45cf7c && git merge --no-ff f634c5bf09327f47cfb2f260c9262d741e84bea7
node scripts/docs-audit/affected-docs.mjs --json 5dbcee8a6d4f3a3feac3ab0a64e5555a1f45cf7c
|
Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016GiHYRmLSNWTfbX9gVQkpz
…cs row Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016GiHYRmLSNWTfbX9gVQkpz
Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016GiHYRmLSNWTfbX9gVQkpz
Co-Authored-By: Claude <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016GiHYRmLSNWTfbX9gVQkpz
Fixes #21501
Clause-②: no
What changed
The ruling on the card is one precedence, written once: explicit flag > env (
OS_ARTIFACT_URL/OS_ARTIFACT_PATH) >dist/objectstack.json> a cwdobjectstack.config.ts. Triage amended it in5966519064: a cwd config joins the boot when the resolved artifact is its own compiled output, because a host config's compiled file cannot carry its code plugins. This PR builds that order in five parts.packages/cli/src/utils/artifact-precedence.ts.resolveArtifactBootSource()holds the ladder:--artifact>OS_ARTIFACT_URL>OS_ARTIFACT_PATH>CWD/dist/objectstack.json>HOME/dist/objectstack.json(os startonly) > unresolved, which is the cwd config.os startandos devboth resolve through it.cwdConfigJoinsBoot()answers the last rung, as amended: the config joins only when the resolved artifact is its own compiled output. The supervisors'Config:row and theservechild both read this one predicate.start.tsdrops its privateresolveArtifactSourceand its inlineOS_ARTIFACT_URLhandling.dev.tsdrops its inline ladder, which had noOS_ARTIFACT_URLrung.OS_ARTIFACT_PATHorOS_ARTIFACT_URLinstart.tsordev.ts.servechild boots the supervisor's answer beside a config (serve.ts).OS_INTERNAL_ARTIFACT_PATHonly when the cwd held no config. With a config present, the config boot ran instead, and its standalone stack re-derived the artifact from the environment.artifact-precedence.ts,internal-artifact-channel.ts,dev.ts,serve.ts).CONFIGDIR/dist/objectstack.jsonstill counts. So does the path the supervising command itself compiles the config to.os devunder a localOS_ARTIFACT_PATHcompiles the cwd config INTO that path, so it declares that path to the child on a second private variable of the same channel,OS_INTERNAL_CONFIG_OUTPUT_PATH. That variable is set only when declared and is owned by the parent.isConfigCompiledArtifact(path, configPath, compiledTo)recognises either place, so a host config compiled to a named path still composes its plugins.serve.ts,utils/format.ts). On a config boot, the child's ready banner names what actually loaded:Artifact: dist/objectstack.jsonwhen a non-host config's standalone stack served the app from a compiled bundle. The proof is the stack's own AppPlugin over that bundle, and the path comes from the runtime's ownresolveDefaultArtifactPathover the same explicit input;Config: objectstack.config.tsfor a host config (itspluginshold code) or a config with no bundle loaded.os startstale-artifact warning is added, per triage.os devflag over env (internal-artifact-channel.ts). Aresolvedchannel decision removesOS_ARTIFACT_URLfrom the child env. Without a flag,devtreats a reference the waystartdoes: it hands no channel down, prints a redactedArtifact: ... (OS_ARTIFACT_URL)row, and does not compile, watch or run the staleness check.content/docs/deployment/cli.mdx, theos devoptions row lists-a's env equivalents asOS_ARTIFACT_URL/OS_ARTIFACT_PATH, as the resolver's ladder says. That is the only docs edit. The PM declares this docs path todomain:devx.What does not change:
os dev, a bareos startin a project, and the documentedos start --artifact ./dist/objectstack.jsonall name the config's own compiled output, so the config still joins on those paths. The showcase is a host config, and it still boots itself.os serveis untouched, since no supervisor channel is involved.packages/runtime's own fallback ladder is not edited.Measured at the public door
Two artifacts differ in one served value, the label of object
fx_widget. The label was read back throughGET /api/v1/meta/object/fx_widget, booted through the built entrybin/run.js.550f4cc2fd)os dev -a ALPHA, beside a config whosedist/holds BRAVOos start --artifact ALPHA, same directoryos start --artifact ALPHA, config but nodist/os start --artifact ALPHA, no configos start --artifact ALPHA, beside a host config (plugin instance inplugins)OS_ARTIFACT_URL=file://.../BRAVO.json os dev -a ALPHAOS_ARTIFACT_PATH=ALPHA os start --artifact ./dist/objectstack.json(dist = BRAVO)os start --artifact ./dist/objectstack.jsonbeside its config (documented path)os startbeside a host config,dist/= BRAVO: ready-banner rowConfig: objectstack.config.ts(served CONFIG; the supervisor row saidArtifact: dist/objectstack.json)Config: objectstack.config.ts, served CONFIGos startbeside a non-host config,dist/= BRAVO: ready-banner rowConfig: objectstack.config.ts(served BRAVO)Artifact: dist/objectstack.json, served BRAVOOS_ARTIFACT_PATH=build/named.json os devbeside a host config: plugin rosterConfig: objectstack.config.tsArtifact:banner. Before the fix, the supervisor printedArtifact:from its own resolution before spawning. The child then printedLoading objectstack.config.ts...and a ready-bannerConfig:row, so one screen named two sources. After the fix:Config:only whencwdConfigJoinsBootsays the config takes part;Env leg.
OS_ARTIFACT_URLalready outranked a cwd config. Two flag-over-env violations were in scope and are now fixed and pinned: theos devreference case and the twin-plus-OS_ARTIFACT_PATHcase.OS_ARTIFACT_PATHbeside a config now boots that artifact alone underos start. Underos dev, it is the path dev compiles the config to, so the config joins.Raise rule. No deploy was measured serving a different stack this way. The shipped runtime image and the scaffolded
Dockerfilecopy only the artifact into/srv/app, with no config beside it.Pins
packages/cli/test/artifact-flag-precedence.integration.test.ts(integration tier) runs 10 cases over the source entry. All boots happen inbeforeAll.dist/, and the leg 2 no-config control;NODE_ENV=development, where the dev metadata door serves the channel's artifact even with the config loaded;os startbeside a host config with a differingdist/: the ready banner saysConfig:, and the roster marker is present (its positive control);os startbeside a non-host config: the ready banner saysArtifact: dist/objectstack.json;os devunderOS_ARTIFACT_PATH=build/named.jsonbeside a host config: the config is compiled there and still composes its plugins;dev -aunderOS_ARTIFACT_URL;OS_ARTIFACT_PATH.packages/cli/src/commands/artifact-child-env.pin.test.ts:resolveArtifactBootSource;cwdConfigJoinsBootandisConfigCompiledArtifact, including the command's own compile path;OS_ARTIFACT_URL;serve-banner-config-row.test.tsandformat.config-artifact-row.test.tsunit pins cover the new bundle row.Reverse verification
All mutations ran through
scripts/ablation-replace.mjsin WRAP mode on the committed tree, with literal anchors. The subject runs fromsrc/throughbin/run-dev.js, so there is nodist/leg.configJoinsignores the channelOS_ARTIFACT_PATHOS_ARTIFACT_URLEvery run ended with
ok restored: blob == HEADand an emptygit diff HEAD. Ablations A to B ran at round 1 (f5c0a890b7), and C1 to E at round 2.Verification (final commit
f634c5bf09)pnpm --filter @objectstack/cli exec vitest run --project unit: 251 files, 3690 tests passed.pnpm --filter @objectstack/cli typecheckpassed, includingcheck:test-typecheck: OKwith the debt ledger unchanged.artifact-flag-precedence.integration.test.tsanddev-no-watch.pin.test.ts, 18 of 18 passed;artifact-child-env.pin.test.ts, 33 of 33.dispatch-gates --commands --repo objectstack-ai/objectstackderived 97 families atf634c5bf09. All 97 ran with exit 0, after a fullturbo run build.--ranreconciliation: 97 derived, 97 run, 0 NOT-MEASURED, 0 UNRUN, with the zero derived from recorded exit codes.pnpm lint: a proven narrowing, not the repo-wide run.eslint.config.mjs's own globs, and none of the 10 changed.tsfiles is ignored.eslint --no-inline-config --format jsonover the 10 files reports 10 results, 0 errors, 0 warnings.parserOptions.projectand reads only two untouched baseline JSONs, so untouched files' verdicts cannot move.c4528fad62(10454b3afa). Main has not moved underserve.ts,dev.tsorstart.tssince.Acceptance notes
The supervisor's pre-boot
Artifact:row on the config-joins path is unchanged. On a bareos startbeside a host config,os startstill prints📦 Artifact: dist/objectstack.jsonbefore it spawns, and that file is not what boots (non-dev). The child's ready banner after the boot now saysConfig: objectstack.config.ts, which is the row triage named for this. The supervisor cannot tell a host config from a non-host one without loading the config. Dropping or rewording its row would change every bareos dev/os startbanner, so this round does not do it, and the seat decides.os dev's handling ofOS_ARTIFACT_PATHis pre-existing. It compiles into that path when the file is missing (or under--compile), and its watch loop rebuilds there. This PR only makes the child recognise that file as the config's output.The round-1 merge commit
f5c0a890b7carries no trailer pair. Every other commit carries the model-free pair.Generated by Claude Code