Commit 2b641dd
`--json` has exactly one audience — a program — and every subcommand that
boots a kernel was handing that program a stream it could not parse.
`ObjectLogger` routes `debug`/`info`/`warn` to stdout and only `error`/`fatal`
to stderr, so `os migrate recorded-by --json` produced "~60 INFO lines +
payload + 2 shutdown lines" on stdout while stderr stayed empty. The only
recourse was a "find the last lone `{` and its matching `}`" extractor — #4873
was forced to write one to assert its own payload — and that heuristic
silently picks the wrong text as soon as a log line looks like JSON.
This takes route 1 from the issue (redirect to stderr), not route 2 (drop the
kernel to `logLevel: 'silent'`): route 2 throws the operator's diagnostics
away, warnings included, and could not quiet the one line that never goes
through the logger at all (`console.log` in `loadArtifactBundle`:
`[StandaloneStack] no compiled artifact …`). Route 3 (make the logger default
to stderr in `packages/core`) would change `os serve` / `os dev` output for
every existing user and is a maintainer call — untouched here.
The fix lands on the family's shared boot seam: `bootSchemaStack` gains a
REQUIRED `jsonOutput` option. Before the boot can print its first byte it
takes over `process.stdout.write` and forwards everything the kernel and its
plugins write to stderr — nothing is discarded — while the payload goes out
through `writeStdoutDirect` on the real stdout. `shutdown()` gives stdout back
only after the kernel is fully down, so the two shutdown lines cannot land
under the payload either. A failed boot deliberately keeps the reservation:
the command's next act on that path is to emit its error payload, and a
half-started kernel can still log. The option is required so a family member
added later has to decide at compile time instead of inheriting the bug.
All nine members covered: `os migrate plan` / `apply` / `resume` /
`recorded-by` / `summary-nulls` / `value-shapes` / `files-to-references`,
`os migrate meta --stored`, and `os meta resync`. Human-mode output is
unchanged (verified: stdout identical, stderr still empty).
Tests:
- `packages/cli/test/json-stdout-purity.e2e.test.ts` — the shared family
expectation. Discovers the members from source (calls `bootSchemaStack` AND
declares a `--json` flag), reconciles that set against the driven list, then
runs each as a real child process asserting a bare `JSON.parse(stdout)`, no
logger record anywhere on stdout, and the boot diagnostics still present on
stderr (so a regression toward silencing goes red too). A new member that is
not driven goes red.
- `packages/cli/test/migrate-exit-code.e2e.test.ts` — the `jsonPayload()`
heuristic extractor #4873 wrote under duress is deleted; it is now a bare
`JSON.parse(stdout)`.
- `packages/cli/src/utils/json-stdout.test.ts` — the mechanism at unit level.
Reverse verification (predicted red, and red): with the reservation disabled,
27 of the family e2e's 28 cases fail — the one that stays green is the
member-inventory case, which asserts a set rather than behaviour — and the
exit-code pin fails with `SyntaxError: Unexpected token 'S', "[Standalone"...`,
exactly the error recorded in the issue.
Fixes #6217
Claude-Session: https://claude.ai/code/session_017uFVNMmTxLpmfQYiuKM1Yx
Co-authored-by: Claude <noreply@anthropic.com>
1 parent 8e13ca8 commit 2b641dd
20 files changed
Lines changed: 637 additions & 39 deletions
File tree
- .changeset
- packages/cli
- src
- commands
- meta
- migrate
- utils
- test
| 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 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
83 | 83 | | |
84 | 84 | | |
85 | 85 | | |
86 | | - | |
| 86 | + | |
87 | 87 | | |
88 | 88 | | |
89 | 89 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
126 | 126 | | |
127 | 127 | | |
128 | 128 | | |
129 | | - | |
| 129 | + | |
130 | 130 | | |
131 | 131 | | |
132 | 132 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
161 | 161 | | |
162 | 162 | | |
163 | 163 | | |
| 164 | + | |
164 | 165 | | |
165 | 166 | | |
166 | 167 | | |
| |||
Lines changed: 3 additions & 0 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
94 | 94 | | |
95 | 95 | | |
96 | 96 | | |
| 97 | + | |
97 | 98 | | |
98 | 99 | | |
99 | 100 | | |
| |||
163 | 164 | | |
164 | 165 | | |
165 | 166 | | |
| 167 | + | |
166 | 168 | | |
167 | 169 | | |
168 | 170 | | |
| |||
223 | 225 | | |
224 | 226 | | |
225 | 227 | | |
| 228 | + | |
226 | 229 | | |
227 | 230 | | |
228 | 231 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
518 | 518 | | |
519 | 519 | | |
520 | 520 | | |
| 521 | + | |
521 | 522 | | |
522 | 523 | | |
523 | 524 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
77 | 77 | | |
78 | 78 | | |
79 | 79 | | |
80 | | - | |
| 80 | + | |
81 | 81 | | |
82 | 82 | | |
83 | 83 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
95 | 95 | | |
96 | 96 | | |
97 | 97 | | |
| 98 | + | |
98 | 99 | | |
99 | 100 | | |
100 | 101 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
105 | 105 | | |
106 | 106 | | |
107 | 107 | | |
| 108 | + | |
108 | 109 | | |
109 | 110 | | |
110 | 111 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
167 | 167 | | |
168 | 168 | | |
169 | 169 | | |
| 170 | + | |
170 | 171 | | |
171 | 172 | | |
172 | 173 | | |
| |||
0 commit comments