Repository navigation
Commit a8adf66
fix(cli): on a first run, os start's compile step writes to stderr, so the stdio MCP transport's stdout carries only JSON-RPC (#24214)
Fixes #24192
Clause-②: no
With `OS_MCP_STDIO_ENABLED=true`, a first-run `os start` (no
`dist/objectstack.json`) now writes only JSON-RPC frames to stdout. The
automatic compile step's progress now goes to stderr, next to the rest
of `os start`'s own output.
## What was wrong (measured on `f792290df`, built CLI through
`bin/run.js`, `NODE_ENV` unset)
- **H1 holds.** `start.ts` calls `redirectStdoutToStderr()`
(`utils/json-stdout.ts`) at the top of `run()`. That call patches
`process.stdout.write` in this process only. The compile `spawnSync`
gave its child `stdio: ['ignore', 'inherit', 'inherit']`, so the child
wrote to the raw fd 1. That is the same fd the `serve` child's stdio
transport writes JSON-RPC frames to.
- **Re-measured once, with stdio enabled:** the new pin, run against the
unfixed build, went RED with `stdout carries 24 line(s) a JSON-RPC
client would fail to parse`. Those 24 lines are 19 text lines and 5
blank lines: `◆ Compile`, the rule, the `→ …` steps, `✓ Build complete`,
the `Data:`/`UI:`/`Logic:`/`Security:`/`Runtime:` summary and `Artifact:
…`. All of them came ahead of the `initialize` answer frame. The card's
27 is the #24002 dev's count at `fd16e46f6` on another fixture. A
host-plugin variant of this fixture also measured 24 here. The producer
and the mechanism are the same; only the count differs.
- **H2 is falsified: the plain `os start` gets the same treatment.** H2
asked whether the compile lines on stdout were the intended human output
of a plain `os start` (stdio not enabled). Measured on the same
first-run fixture with stdio OFF, unfixed build:
- stdout carried exactly 19 lines, and every one came from the compile
child.
- Everything else `os start` prints was already on stderr: its own
header (`◆ ObjectStack`, `→ Compiling objectstack.config.ts →
dist/objectstack.json...`, `Config:`, `Home:`, `Artifact:`, `Database:`,
`Environment:`, `→ Starting server...`) and the whole `serve` banner.
- The compile lines were the one part of `os start`'s output that was
not on stderr. `start` decides nothing by stdio mode.
`redirectStdoutToStderr()` is UNCONDITIONAL there and in `serve`
(#7915), and `json-stdout.ts` gives the reason: a conditional needs a
reliable signal at the moment each line prints, and it fails silently in
the worse direction. So always-stderr is already the contract, and the
fix does not branch on stdio mode.
## The fix (`packages/cli/src/commands/start.ts`, the compile
`spawnSync` only)
`stdio: ['ignore', 'inherit', 'inherit']` becomes `stdio: ['ignore', 2,
'inherit']`, so the compile child's stdout is this process's fd 2.
- stdin stays `'ignore'` (#24002), and stderr stays inherited.
- `--compile` on a later run takes the same spawn, so it is covered too.
- The `serve` child keeps `stdio: 'inherit'`, because its transport owns
fd 1.
- Nothing is dropped: the same lines still print, on stderr.
- `os compile`'s own output when you run it directly is unchanged (held
off by the claim).
Measured after the fix, plain `os start`, same fixture: stdout carries 0
lines, and `✓ Build complete` is on stderr.
## Pin:
`packages/cli/test/start-stdio-stdout-purity.integration.test.ts`
The pin sits beside `serve-stdio-stdout-purity.e2e.test.ts`, which pins
the same invariant for `os serve`.
**Fixture and boots:**
- One minting `serve --dev` boot (stdio auto-start is fail-closed,
ADR-0101), then one first-run `os start`.
- The `os start` boot runs with `OS_MCP_STDIO_ENABLED=true` and the
minted key, with `initialize` written at spawn.
- Both are bounded children: a detached process group, SIGTERM then
SIGKILL, and a 120 s answer deadline.
**On that one boot, the pin asserts:**
1. Every complete stdout line, through the `initialize` answer and a 750
ms settle window after it, parses as a JSON-RPC 2.0 frame. Blank lines
count.
2. The answer carries the request's own `id` and no `error`, so the
channel is live and not merely quiet.
3. The compile child's `◆ Compile` and `✓ Build complete` are on stderr:
the output moved and was not deleted.
4. Positive control: `dist/objectstack.json` exists afterwards, so the
boot really took the auto-compile leg.
**Tier and budget:**
- The file is in the `integration` tier: the `vitest-tiers.ts` predicate
classifies it because it spawns `bin/run.js`. The `.integration` name
keeps it on the queue run.
- `test-budget: 20`: 15.0 s, the slowest of three local runs (15.01 /
13.41 / 13.85 s), x 1.25. There is no CI reading yet.
- It spawns `bin/run.js` with `NODE_ENV: undefined`, so
`scripts/check-cli-test-child-env.mjs`'s pinned built-entrypoint
population gains it ("eleven" becomes "twelve"). It carries the same
`[#N] Joined the population deliberately` note as the #24002 pin. This
is the edit shape PR #24184 used.
**Reverse verification.** The fix was committed first (`45a7b8667`). One
leg, run under a `trap … EXIT INT TERM` restore:
- `scripts/ablation-replace.mjs` swapped `stdio: ['ignore', 2,
'inherit']` back to `stdio: ['ignore', 'inherit', 'inherit']`. Anchor 1
became 0, and the blob went from `733683e27b67` to `1dc9de46b023`.
- The CLI was rebuilt. `ablation-dist-preflight.mjs` found the mutant
marker in `dist/commands/start.js` (exit 0).
- The pin went **RED**: `stdout carries 24 line(s) a JSON-RPC client
would fail to parse (#24192)`.
- Restored: the blob equals HEAD (`733683e27b67`), `git diff HEAD` is
empty and `git status` is clean. After the rebuild, the mutant marker is
absent from `dist/` (`--absent`, exit 0) and the fix marker is present
(exit 0).
## Local verification
- Pin at `8319dd69b` (after merging `main`), run together with the
#24002 pin: `pnpm --filter @objectstack/cli exec vitest run --project
integration --maxWorkers=1
test/start-stdio-stdout-purity.integration.test.ts
test/start-mcp-stdio-early-initialize.integration.test.ts` gave 2 files
and 2 tests passed. The rest of the `integration` tier is declared to
CI.
- Unit tier at `81612d923`: `pnpm --filter @objectstack/cli exec vitest
run --project unit --maxWorkers=2` gave 310 files and 4599 tests passed.
The merge brought no `packages/cli` path.
- `pnpm --filter @objectstack/cli typecheck` exited 0, and
`check:test-typecheck` was OK. `tsc -p tsconfig.test.json --noEmit
--listFiles` lists the new pin once, and it has no errors.
- `pnpm exec eslint --no-inline-config` over the three changed source
files exited 0 with no output. The repo-wide `pnpm lint` is left to CI.
- `node scripts/check-cli-test-child-env.mjs --self-test` passed 152
cases. Before the population edit, it failed on exactly that case.
- Derived gate union at `8319dd69b`: `node scripts/pm/dispatch-gates.mjs
--commands --repo objectstack-ai/objectstack` named 85 commands. All 85
exited 0, each exit code captured before any pipe. `--ran` reconciled
them: `85 derived, 85 run, 0 NOT-MEASURED, 0 UNRUN`, exit 0. The union
includes `check:cli-test-child-env` (14 built-CLI spawns, all out of
development/test), `check:pm-dispatch-gates` (fast tier, 1884 cases),
`check:nul-bytes`, `check:dual-build-cjs-loads` and
`check:i18n-coverage`. The derivation names `origin/main` as 3 commits
ahead after the merge, with one changed derivation input,
`scripts/check-route-envelope.mjs`. That edit moves `packages/rest`
MODULES counts only, and this diff touches no `packages/rest` path. CI
runs on the merge ref.
## Acceptance notes
- **A stale sentence in the #24002 pin's header.**
`start-mcp-stdio-early-initialize.integration.test.ts` says "⛔ Stdout
PURITY is not asserted: the compile child's progress lines reach the
same stdout. That is a separate defect, not this card's". After this PR
the first half is no longer true, and the purity half is pinned here.
The file is outside this claim's surface, so it is left as is. Noted
only (carrier: none).
- **The 24 vs 27 count.** Both fixtures here measured 24 lines: one
object, and the same with a host plugin. The card's 27 came from another
build and fixture. The mechanism and the producer are identical.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01QyoWKK9RwUNxBxmevy9Neb)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 4f11af5 commit a8adf66
4 files changed
Lines changed: 378 additions & 2 deletions
File tree
- .changeset
- packages/cli
- src/commands
- test
- scripts
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
249 | 249 | | |
250 | 250 | | |
251 | 251 | | |
| 252 | + | |
| 253 | + | |
| 254 | + | |
| 255 | + | |
| 256 | + | |
| 257 | + | |
| 258 | + | |
| 259 | + | |
| 260 | + | |
| 261 | + | |
| 262 | + | |
| 263 | + | |
252 | 264 | | |
253 | 265 | | |
254 | 266 | | |
255 | | - | |
| 267 | + | |
256 | 268 | | |
257 | 269 | | |
258 | 270 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
| 31 | + | |
| 32 | + | |
| 33 | + | |
| 34 | + | |
| 35 | + | |
| 36 | + | |
| 37 | + | |
| 38 | + | |
| 39 | + | |
| 40 | + | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
| 50 | + | |
| 51 | + | |
| 52 | + | |
| 53 | + | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
| 57 | + | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
| 62 | + | |
| 63 | + | |
| 64 | + | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
| 68 | + | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
| 72 | + | |
| 73 | + | |
| 74 | + | |
| 75 | + | |
| 76 | + | |
| 77 | + | |
| 78 | + | |
| 79 | + | |
| 80 | + | |
| 81 | + | |
| 82 | + | |
| 83 | + | |
| 84 | + | |
| 85 | + | |
| 86 | + | |
| 87 | + | |
| 88 | + | |
| 89 | + | |
| 90 | + | |
| 91 | + | |
| 92 | + | |
| 93 | + | |
| 94 | + | |
| 95 | + | |
| 96 | + | |
| 97 | + | |
| 98 | + | |
| 99 | + | |
| 100 | + | |
| 101 | + | |
| 102 | + | |
| 103 | + | |
| 104 | + | |
| 105 | + | |
| 106 | + | |
| 107 | + | |
| 108 | + | |
| 109 | + | |
| 110 | + | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
| 125 | + | |
| 126 | + | |
| 127 | + | |
| 128 | + | |
| 129 | + | |
| 130 | + | |
| 131 | + | |
| 132 | + | |
| 133 | + | |
| 134 | + | |
| 135 | + | |
| 136 | + | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
| 141 | + | |
| 142 | + | |
| 143 | + | |
| 144 | + | |
| 145 | + | |
| 146 | + | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
| 150 | + | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
| 155 | + | |
| 156 | + | |
| 157 | + | |
| 158 | + | |
| 159 | + | |
| 160 | + | |
| 161 | + | |
| 162 | + | |
| 163 | + | |
| 164 | + | |
| 165 | + | |
| 166 | + | |
| 167 | + | |
| 168 | + | |
| 169 | + | |
| 170 | + | |
| 171 | + | |
| 172 | + | |
| 173 | + | |
| 174 | + | |
| 175 | + | |
| 176 | + | |
| 177 | + | |
| 178 | + | |
| 179 | + | |
| 180 | + | |
| 181 | + | |
| 182 | + | |
| 183 | + | |
| 184 | + | |
| 185 | + | |
| 186 | + | |
| 187 | + | |
| 188 | + | |
| 189 | + | |
| 190 | + | |
| 191 | + | |
| 192 | + | |
| 193 | + | |
| 194 | + | |
| 195 | + | |
| 196 | + | |
| 197 | + | |
| 198 | + | |
| 199 | + | |
| 200 | + | |
| 201 | + | |
| 202 | + | |
| 203 | + | |
| 204 | + | |
| 205 | + | |
| 206 | + | |
| 207 | + | |
| 208 | + | |
| 209 | + | |
| 210 | + | |
| 211 | + | |
| 212 | + | |
| 213 | + | |
| 214 | + | |
| 215 | + | |
| 216 | + | |
| 217 | + | |
| 218 | + | |
| 219 | + | |
| 220 | + | |
| 221 | + | |
| 222 | + | |
| 223 | + | |
| 224 | + | |
| 225 | + | |
| 226 | + | |
| 227 | + | |
| 228 | + | |
| 229 | + | |
| 230 | + | |
| 231 | + | |
| 232 | + | |
| 233 | + | |
| 234 | + | |
| 235 | + | |
| 236 | + | |
| 237 | + | |
| 238 | + | |
| 239 | + | |
| 240 | + | |
| 241 | + | |
| 242 | + | |
| 243 | + | |
| 244 | + | |
| 245 | + | |
| 246 | + | |
| 247 | + | |
| 248 | + | |
| 249 | + | |
| 250 | + | |
| 251 | + | |
| 252 | + | |
| 253 | + | |
| 254 | + | |
| 255 | + | |
| 256 | + | |
| 257 | + | |
| 258 | + | |
| 259 | + | |
| 260 | + | |
| 261 | + | |
| 262 | + | |
| 263 | + | |
| 264 | + | |
| 265 | + | |
| 266 | + | |
| 267 | + | |
| 268 | + | |
| 269 | + | |
| 270 | + | |
| 271 | + | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
| 275 | + | |
| 276 | + | |
| 277 | + | |
| 278 | + | |
| 279 | + | |
| 280 | + | |
| 281 | + | |
| 282 | + | |
| 283 | + | |
| 284 | + | |
| 285 | + | |
| 286 | + | |
| 287 | + | |
| 288 | + | |
| 289 | + | |
| 290 | + | |
| 291 | + | |
| 292 | + | |
| 293 | + | |
| 294 | + | |
| 295 | + | |
| 296 | + | |
| 297 | + | |
| 298 | + | |
| 299 | + | |
| 300 | + | |
| 301 | + | |
| 302 | + | |
| 303 | + | |
| 304 | + | |
| 305 | + | |
| 306 | + | |
| 307 | + | |
| 308 | + | |
| 309 | + | |
| 310 | + | |
| 311 | + | |
| 312 | + | |
| 313 | + | |
| 314 | + | |
| 315 | + | |
| 316 | + | |
| 317 | + | |
| 318 | + | |
| 319 | + | |
| 320 | + | |
| 321 | + | |
| 322 | + | |
| 323 | + | |
| 324 | + | |
| 325 | + | |
| 326 | + | |
| 327 | + | |
| 328 | + | |
| 329 | + | |
| 330 | + | |
| 331 | + | |
| 332 | + | |
| 333 | + | |
| 334 | + | |
| 335 | + | |
| 336 | + | |
| 337 | + | |
| 338 | + | |
| 339 | + | |
| 340 | + | |
| 341 | + | |
| 342 | + | |
| 343 | + | |
| 344 | + | |
| 345 | + | |
| 346 | + | |
| 347 | + | |
0 commit comments