Auto-update WARN is silent in --json mode (logger suppresses non-fatal)
Opened by stack72 · 4/18/2026
Problem
src/infrastructure/logging/logger.ts:131-144 configures the catch-all logger with lowestLevel: 'fatal' in JSON mode. This suppresses all non-fatal output to keep stdout clean for structured output, routing only FATAL records through a JSON sink on stderr.
Consequence: logger.warn(...) calls are dropped entirely in --json mode. The auto-update local-edits refusal warning added in swamp-club#126 (resolve_datastore.ts:buildLocalEditsWarning) surfaces in log mode but is silent in JSON mode. The data is still protected — the auto-update still refuses to overwrite — but scripted users (JSON consumers) get no signal about why the new version isn't being pulled.
This is pre-existing behavior, not introduced by #126. The logger.info(...) call on successful auto-update in the same file has always been silent in JSON mode for the same reason. Most of the codebase's 50+ logger.warn(...) call sites have the same gap.
Scope of affected code
src/infrastructure/logging/logger.ts:131-144— catch-all logger in JSON mode useslowestLevel: 'fatal'src/infrastructure/logging/logger.ts:46-70—createJsonErrorSinkhandles fatal only; no counterpart for warning/error- ~50
logger.warn()sites acrosssrc/that go silent in JSON mode
Suggested fix direction
Add a structured-stderr sink that handles non-fatal levels and route the catch-all logger in JSON mode to allow warnings through. Options:
Unified sink that dispatches on
record.level:fatal→ existing JSON error format ({ error, stack })warning/error→{ level, category, message }Route withlowestLevel: 'warning'to the unified sink.
Separate sinks per level with a custom routing layer.
Either way, the goal is: logger.warn(...) in JSON mode emits one structured JSON object per line to stderr so scripted consumers can parse it while stdout remains reserved for the command's structured output.
Context
Discovered while implementing swamp-club#126's local-edits refusal WARN. Tried expanding #126's scope to fix this but 50+ call sites would suddenly start emitting to JSON stderr, changing the output contract for every scripted consumer in one shot. Correct fix is a separate, intentional change with its own review. Tracking separately here.
Closed
No activity in this phase yet.
stack72 commented 4/20/2026, 4:22:12 PM
Going to close this out. The bug is real — logger.warn/error do silently drop in --json mode — but after working through a fix, the cost/benefit doesn't land.
Why it's not worth doing:
- The symptom is observability, not correctness. The motivating case (#126's auto-update local-edits WARN) still protects the data — the auto-update still refuses. Scripted users don't get a signal, but they can re-run without --json to see what happened. No workflow is blocked, nothing is lost.
- The fix ships a behavior change to every --json consumer. Today, stderr in JSON mode is empty except on fatal. A fix means every command that hits a warn/error path starts emitting JSONL lines. That's a contract extension that breaks any wrapper asserting "no stderr output," for a diagnostic benefit most consumers won't use.
- It widens the secret-leak surface. The fatal path is rare; warn/error is routine. Any Error whose message carries a bearer token, path, or credential now leaks to JSON stderr. The 49 warn + 17 error sites don't look problematic today, but that's a heuristic, not a guarantee.
- There's a latent logtape.meta hazard in the same code path — its routing isn't fixed by the proposed change, and under the new sink it could pollute stdout on internal LogTape errors. Fixing the original bug risks introducing a worse one.
- No concrete consumer asking for it. This was discovered while implementing #126, not raised by a scripted user hitting the gap in practice.
If the auto-update silence specifically becomes a pain point, the better path is to emit the refusal as a structured event through the primary output of whatever command triggered it — same renderer pattern as everything else — rather than extending the logger contract. That's a targeted fix for the one case that motivated this issue, without touching the cross-cutting log infrastructure.
Closing as won't-fix. Re-open if a concrete consumer need surfaces.
Sign in to post a ripple.