Skip to content

fix(cli): a direct os serve --dev compiles the config before its dev artifact door serves it - #26289

Merged
objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/project-thread-kcwm7i
Oct 26, 2026
Merged

objectstack-fleet[bot] merged 4 commits into
mainfrom
claude/project-thread-kcwm7i

Conversation

@objectstack-fleet

Copy link
Copy Markdown
Contributor

Requested by Leehom · project thread

Fixes #26242

Clause-②: no
Metadata-protocol: none

What was wrong

A host config's dev boot holds two copies of one stack. The config module, evaluated in the serving process, feeds the AppPlugin wrap, and provider-bound connectors are materialized from that copy, so it is what dispatch calls. The compiled dist/objectstack.json feeds the dev artifact door, and its copy is what /api/v1/meta/connector/NAME serves. #26130 (PR #26240) made os dev compile before it boots. A direct os serve --dev still composed the door over whatever artifact was on disk, so an env-derived value differed between the two copies.

Red first on main at 81bba6c3 (before #26240) and re-read on this branch's base 3d50a43b: examples/app-showcase, artifact compiled with OS_PORT/PORT unset (all three baseUrl values http://127.0.0.1:3000), OS_PORT=41302, nothing listening on 3000. "dispatch" is POST /api/v1/automation/showcase_declarative_connector_ping/trigger.

boot /meta/connector/showcase_status_api → providerConfig.baseUrl before after dispatch
os serve objectstack.config.ts --dev -p 41302 http://127.0.0.1:3000 http://127.0.0.1:41302 200, status: ok (reached 41302) both
os serve objectstack.config.ts -p 41302 (production) http://127.0.0.1:41302 http://127.0.0.1:41302, artifact md5 unchanged 200 both
os dev --no-compile --no-watch -p 41302 http://127.0.0.1:3000 http://127.0.0.1:3000 (unchanged, the operator's shortcut) 200
os dev --no-watch -p 41302 (:41302 since #26240) http://127.0.0.1:41302 200
os serve --dev with OS_ARTIFACT_URL=file://… (pinned) boots the pinned artifact same, no compile line, pinned file md5 unchanged

The fix

Route (b), the one #26240 took: the dev boot compiles the config into the artifact it is about to serve, in its own environment, so both copies are one evaluation's answer. Nothing restates which variables a config reads or in what order.

  • packages/cli/src/utils/dev-compile-at-boot.ts (new): devCompilesAtBoot, moved verbatim from commands/dev.ts so both doors read the one decision (dev.ts imports serve.ts, so serve.ts cannot import dev.ts). dev.ts now imports it; the decision and its os dev behaviour are unchanged. The #26130 pin's import moves with it.
  • packages/cli/src/commands/serve.ts: in the dev artifact door's existing-artifact branch, before the door is constructed, a boot with no supervising os dev (its channel OS_INTERNAL_ARTIFACT_PATH unset) asks devCompilesAtBoot and compiles. Under os dev the child keeps its parent's decision, so os dev --no-compile still serves the artifact as built and a default os dev does not compile twice. A boot with no artifact on disk never reaches this branch: no door composes, the wrap is the one copy, and nothing is compiled (so test fixtures and fresh checkouts without dist/ are untouched). A failed compile refuses the boot with the compile's own output, as under os dev. Production boots never reach the branch (it is inside the isDev door block).
  • compileForDevBoot (same util) spawns os compile CONFIG --output ARTIFACT with the serving process's environment, including the .env files serve loaded, but with NODE_ENV put back to its launch value. serve --dev sets NODE_ENV=development on itself; if the child inherited that, it would run under oclif's source loader, which child-env-source-loader.pin.test.ts forbids. The child therefore sees what os dev's compile child sees.
  • Cost: one compile per direct dev boot over an existing artifact. On the showcase, ready went from 5 s to 8 s.

Tests

Gates

Local subset at head 6f7edd28c: node scripts/pm/dispatch-gates.mjs --commands derived 91 families, and each was run with its exit code captured before any pipe. --ran reconciliation: 91 run, 0 NOT-MEASURED, all exit 0.

  • First sweep, at ca2e4216:
    • pnpm check:doc-authoring went red on a real finding: the compile refusal's runtime text carried the tracker id. It was stripped (751f30aa) and the gate is green.
    • check-plugin-teardown-shape --self-test exited 3 on the shallow clone. After git fetch origin 621a4876 it exited 0 (48 cases).
    • check:dual-build-cjs-loads exited 3 there, with its prerequisite not met. It exited 0 on the final sweep.
  • pnpm check:cli-test-child-env: green with the new census entry, and its self-test passed 152 cases. The first attempt was refused because the leg env's spread came after NODE_ENV; NODE_ENV: undefined now comes last.
  • eslint --no-inline-config on the six touched source and test files: exit 0. node scripts/check-nul-bytes.mjs: exit 0.
  • CI check-runs: in progress at PR open. The seat reads convergence.

Acceptance notes

🤖 Generated with Claude Code

https://claude.ai/code/session_019rDzaJmEJAYLUNJ4kwG2vN

…artifact door serves it

The dev artifact door served dist/objectstack.json as found while dispatch
read the evaluated config module, so an env-derived connector baseUrl
differed between /meta and what runs. The compile decision os dev reads
(devCompilesAtBoot) moves to utils/ so serve reads it too; a supervised
serve child keeps its parent's decision.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019rDzaJmEJAYLUNJ4kwG2vN
…rve --dev pin gets its own file

serve sets NODE_ENV=development on itself, so a compile child spawned
from serve.ts inherited it (child-env-source-loader pin). The spawn moves
to utils/dev-compile-at-boot.ts as compileForDevBoot, which hands the
child the serving environment with the launch NODE_ENV put back, as os
dev's compile child sees it. The serve --dev legs leave
dev-connector-env-parity (the os dev half, also being reworked for the
port flag) for serve-dev-connector-env-parity, with its census entry;
the verdicts compare the two copies rather than a port number.

Co-authored-by: Claude <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019rDzaJmEJAYLUNJ4kwG2vN
@github-actions

Copy link
Copy Markdown
Contributor

📓 Docs Drift Check

This PR changes 1 package(s): @objectstack/cli, touching 6 documentable anchor(s).

2 hand-written doc(s) NAME something this change touched and may need an implementation-accuracy re-verification:

  • content/docs/api/client-sdk.mdx (via getTypes (sdk, the bare tail of client method meta.getTypes, bound to GET /api/v1/meta), meta.getTypes (sdk, the route ledger binds it to GET /api/v1/meta))
  • content/docs/deployment/cli.mdx (via child_process (literal, a string literal on a changed line))
What this run could not see
  • 3 anchor(s) matched too much of the corpus to be a work list: os dev (command, 39 pages), os serve (command, 40 pages), /api/v1/meta (route, 42 pages)
  • 1 name(s) were too generic to anchor anything (single lowercase words)
  • the SDK route bridge reached 54 of 206 client-bound route-ledger rows — the other 152 have no registrar path: tail to select them, so pages documenting THEIR client methods cannot appear above, on this or any run. Of those 152: 0 are remediable by widening that discovery convention (an in-repo file declares the path; the convention did not scan it); 55 are structural — on a ledger where NOT ONE row is declared in-repo, so no discovery change reaches them at any price; 97 are undecided (no in-repo declaration, on a ledger that has other in-repo registrars — absence and an unreadable spelling are not distinguishable here). The rows themselves: node scripts/docs-audit/affected-docs.mjs --bridge-coverage
  • a page that states a rule by its inputs shares no identifier with the emitter that implements the rule, so an emitter-only diff cannot list it — not on this run and not on any run. Measured on fix(driver-sql): emit varchar(maxLength) for a text field a declared index keys on #11430: content/docs/protocol/objectql/types.mdx documents the text-family column mapping by the ObjectQL type names it maps FROM (text / textarea / html) while the diff changed createColumn; it went unlisted, and it was the page that diff falsified, in four places. No shared token exists to detect this on, so a rule your change carries has to be re-read by hand in the pages that restate it.
  • a key NAME is not a key, so the hand re-read the line above prescribes can land on the wrong schema. The same spelling is authorable on one governed type and a [REMOVED] tombstone on another for each of active, aria, joins, objects, template, tools and version (censused on [finding] tools is a key on BOTH AgentSchema (tombstoned, dead) and SkillSchema (live, cloud-attested), so a name-based search attributes skill examples to the agent key — it produced a false stop-the-line alarm on PR #19059 #19093 over the liveness ledger's governed types, top-level keys); nothing in a search result distinguishes the two, so a grep hit on a LIVE example reads as evidence about the DEAD key. Measured on fix(spec): the agent.tools liveness row says dead — it claimed live on a key the schema tombstoned #19059: content/docs/ai/agents.mdx was reported as contradicting the agent.tools tombstone over its tools: example at :161, which is inside the defineSkill({ block opened at :155 — the page was already correct. Settle ownership by PARSING the value against both schemas, never by the name: that literal PASSES SkillSchema, and as an AgentSchema it FAILS at tools with the tombstone prescription. ⛔ These names are not the whole class — a key retired through a .strict() guidance map leaves no tombstone in the walked shape and none of them here (tool.category, live as AIToolDefinition.category).

Coarse fallback — 31 page(s) merely mention a changed package (the pre-#9192 predicate, kept for the deliberately-wide backstop): node scripts/docs-audit/affected-docs.mjs --json be531f685e7d73ec33aedc2ed67fe997cfc4784d → packageMentionDocs.

Which tree this was computed on

This run read content/docs from 0cecbddca1b897a820c318e7bb27487fca267f0e — the merge of head 6f7edd28c330b29e25946d13f28aa3d69435a00f into base be531f685e7d73ec33aedc2ed67fe997cfc4784d, which is what actions/checkout gives a pull_request run. Not the PR head.

A worktree cut from an older main holds a different content/docs, so re-deriving there can legitimately return a different list — that is a different tree, not a wrong row. To answer on the same tree:

# while this PR is open — GitHub drops the merge commit once it closes
git fetch origin 0cecbddca1b897a820c318e7bb27487fca267f0e && git checkout 0cecbddca1b897a820c318e7bb27487fca267f0e
# afterwards, rebuild it from the two parents, which stay fetchable
git fetch origin be531f685e7d73ec33aedc2ed67fe997cfc4784d 6f7edd28c330b29e25946d13f28aa3d69435a00f && git checkout -B drift-repro be531f685e7d73ec33aedc2ed67fe997cfc4784d && git merge --no-ff 6f7edd28c330b29e25946d13f28aa3d69435a00f

node scripts/docs-audit/affected-docs.mjs --json be531f685e7d73ec33aedc2ed67fe997cfc4784d

⚠️ That checkout carried uncommitted changes, so the commit above does not fully identify what was read.

Advisory only, and a precision-first one (#9192): a page is listed because it names a
symbol, wire route or SDK method this diff touched — not because it mentions a changed
package. Each row says which anchor put it there, so a wrong row is reportable rather than
merely annoying. To re-verify, run the docs-accuracy-audit workflow scoped to these files:
node scripts/docs-audit/affected-docs.mjs be531f685e7d73ec33aedc2ed67fe997cfc4784d → pass the list as
args.docs, on the commit named under Which tree this was computed on.

@objectstack-fleet

Copy link
Copy Markdown
Contributor Author

席位核对

ACCEPT · domain:server seat 2 · os-project-manager · session_0186vfjHHMsRXC9EUT9hTFMo · 2026-10-26T19:43Z · head 6f7edd28c330b29e25946d13f28aa3d69435a00f · #26242

The maintainer's standing authorisation for landing reviewed PRs, verbatim: 「按照 pm-dispatch 规则复核过的都授权」.

  1. Shape: a draft against main with Fixes #26242. Clause-②: no and Metadata-protocol: none are line-initial. One patch changeset on @objectstack/cli. check-governed-merges --pr 26289: NOT governed, 580 changed lines.
  2. Premise and review: the claim 6422605280 asked for the os serve --dev half of the cli/dev: on an os dev boot with OS_PORT set, served connector metadata reads the build-time baseUrl (http://127.0.0.1:3000) while dispatch reaches the live port, so /meta disagrees with what runs #26130 family. The seat read the diff. devCompilesAtBoot moves unchanged from commands/dev.ts into utils/dev-compile-at-boot.ts, because dev.ts imports serve.ts and the second door has to read the same decision without restating it. Its #26130 pin follows the import. serve.ts asks that decision only inside the dev artifact door's existing-artifact branch, and only when no supervising os dev answered the internal channel. So os dev --no-compile keeps its shortcut, a default os dev does not compile twice, a pinned artifact is never rebuilt, and production os serve never reaches the branch. compileForDevBoot spawns os compile in the serving process's environment with NODE_ENV put back to its launch value. Node drops an undefined env entry, so an unset launch value stays unset, and the child is never put under the source loader. The new spawning test joins check-cli-test-child-env's population with its reason written beside it. The dev reports red on 81bba6c3/3d50a43b and green after on the showcase (/meta serves :41302, ping 200), both ablations red on exactly their own leg, the cli unit tier at 4,878 of 4,878 and the integration tier at 173 files.
  3. Gates: at this stamp the head's 47 check-runs read 36 success and 11 skipped (path-filtered UI, docs-build, dogfood matrix placeholders and opt-in harnesses), with none failing or in progress; mergeable, clean. Locally the dev reports 91 of 91 derived gate families run and exit 0, reconciled with --ran.
  4. Accepted deviations: a direct os serve --dev over an existing artifact now costs one compile (about 3 s on the showcase), and a config that fails to compile refuses that boot with the compile's own output, where it used to serve the stale artifact. That is os dev's behaviour since fix(cli): os dev compiles the config before it boots, so served connector metadata matches dispatch #26240, so Clause-②: no holds: a dev boot that failed to compile was already refused on the supervised door, and nothing a deployment or a metadata author sends is refused.
  5. Out of scope: none filed. The busy-port shift is cli/dev: when the requested port is busy, os dev serves on N+1 but the config already read N, so the showcase self URL and /meta point at a port nothing serves and the self-ping fails #26270, dispatched separately.
  6. Landing: this seat runs pr_ready and then automerge_enable through the relay, confirms MERGED on both readings, and records the landing on cli/serve: os serve --dev with OS_PORT set still serves the build-time connector baseUrl from /meta while dispatch reaches the live port (the #26130 split on the serve door) #26242, which the merge closes. PR fix(cli): os dev -p and os serve --port export the port as OS_PORT/PORT, so a config reads the port the server listens on #26268 (in the queue) touches serve.ts's port resolution, another region; the dev trial-merged it clean with both parity pins passing, and whichever lands second merges main.

Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

documentation Improvements or additions to documentation size/l tests tooling

Projects

None yet

2 participants