Observation found while fixing #7915 (PR #8065). Filed rather than folded in — nothing a user
hits today, recorded so the boundary is deliberate rather than accidental.
What is true after #7915
os serve and os start install redirectStdoutToStderr() at the top of run(): every human
line those two commands print — and everything the kernel or any package writes to
process.stdout — goes to stderr, unconditionally, because their stdout may be the MCP stdio
transport's JSON-RPC channel.
os dev does not. Its own supervisor lines (packages/cli/src/commands/dev.ts — the rebuild
notices, ↪ server bound to port …, ✓ server restarted — the new build is live, the MCP
connect hint) still go to stdout, and it spawns the serve child with stdio: ['inherit', 'inherit', 'inherit', 'ipc'] — so the parent's stdout IS the fd the child's transport would
write frames to.
Why it was left out of #7915
os dev is not a viable stdio MCP host with or without this: its restart supervisor tears the
child down on every successful rebuild, which drops any live stdio session. So there is no
configuration in which the parent's lines corrupt a session that would otherwise have worked —
the inconsistency is real, the user-visible defect is not.
What remains is a smaller thing worth a decision rather than a silent divergence:
- three server-shaped commands, two of which treat their output as diagnostics and one of which
does not — the next person to read dev.ts has no way to tell whether that is a decision; os dev | grep and os serve | grep behave differently for the same-looking lines.
Options
- Install the same one-line redirect in
dev.run() — consistent, and cheap. Costs: os dev's
output moves to stderr for everyone who pipes it today. - Leave it, and say so in
dev.ts with a comment naming this issue.
No recommendation attached; both are defensible, and option 1 is a behaviour change for a
command people watch interactively all day.
Observation found while fixing #7915 (PR #8065). Filed rather than folded in — nothing a user
hits today, recorded so the boundary is deliberate rather than accidental.
What is true after #7915
os serveandos startinstallredirectStdoutToStderr()at the top ofrun(): every humanline those two commands print — and everything the kernel or any package writes to
process.stdout— goes to stderr, unconditionally, because their stdout may be the MCP stdiotransport's JSON-RPC channel.
os devdoes not. Its own supervisor lines (packages/cli/src/commands/dev.ts— the rebuildnotices,
↪ server bound to port …,✓ server restarted — the new build is live, the MCPconnect hint) still go to stdout, and it spawns the serve child with
stdio: ['inherit', 'inherit', 'inherit', 'ipc']— so the parent's stdout IS the fd the child's transport wouldwrite frames to.
Why it was left out of #7915
os devis not a viable stdio MCP host with or without this: its restart supervisor tears thechild down on every successful rebuild, which drops any live stdio session. So there is no
configuration in which the parent's lines corrupt a session that would otherwise have worked —
the inconsistency is real, the user-visible defect is not.
What remains is a smaller thing worth a decision rather than a silent divergence:
does not — the next person to read
dev.tshas no way to tell whether that is a decision;os dev | grepandos serve | grepbehave differently for the same-looking lines.Options
dev.run()— consistent, and cheap. Costs:os dev'soutput moves to stderr for everyone who pipes it today.
dev.tswith a comment naming this issue.No recommendation attached; both are defensible, and option 1 is a behaviour change for a
command people watch interactively all day.