8.9 KiB
G-011 Optional per-console err channel with defined stderr semantics
Status: proposed Scope: src/modules/punk/console-999999.0a1.0.tm, src/modules/opunk/console-999999.0a1.0.tm, src/modules/punk/repl-999999.0a1.0.tm Goal: a console optionally carries an err channel as an attribute of its canonical {in out} identity - {in out err} specs accepted everywhere -console is, err resolving to process stderr for the default console and to the console's out channel elsewhere - so diagnostics and emit-to-err are first-class per-console operations instead of raw puts stderr. Acceptance: console_spec_resolve and every -console site accept an {in out err} spec (err optional; existing pair/instance-name/object spec forms unchanged); opunk::Console exposes the err channel (nullable, additive base-class change); an unset err resolves to stderr for the default console and to the console's own out channel otherwise; punk::console's own warnings/diagnostics emitted while operating on a resolvable console go to that console's err (raw puts stderr remains only where no console is in play); an emit-to-err path exists and is exercised by at least one real consumer (e.g punk::repl); the effective err is discoverable from any thread/interp via console_fact_get (fact key err, returning the effective err channel name); ownership/fact/mode-cache keys remain canonical {in out}; the existing console test suites pass unchanged.
Context
punk::console models exactly two channels per console: {in out}, defaulting to {stdin stdout}. For pure ANSI terminal control that is a faithful model - queries and responses share one terminal connection by protocol design - but the goals collectively move punk::console from "terminal control library" toward "session-wide console abstraction" (G-001 pluggable backends, G-007 location transparency, G-008 scoped state, G-009 themed profiles), and there the missing stderr stream becomes load-bearing:
- Error/diagnostic routing is ad-hoc. punk::console's own warnings (enableRaw/disableRaw
failures, query timeouts, reader-cooperation warnings, test_char_width failures - dozens of
sites) go to raw
puts stderr, bypassing the abstraction the module provides. A console backed by an ssh channel or tk widget (G-001) has no principled way to receive its own error output: backend authors must either merge stderr into stdout or bypass punk::console for errors, fragmenting the abstraction. - No console-level separation of result vs diagnostic streams. The shellrun run/runout/runerr/runx family preserves the stdout/stderr distinction at the process layer, but once output enters the console the distinction collapses into the single out channel. A subshell wanting to display a process's stderr differently (colour, prefix, separate pane) must do so outside the console model - and a G-009 theme has no stderr stream to style.
- Cross-interp discovery has no slot. The G-007 fact store and ownership registry are keyed by canonical {in out}; "what is my console's stderr?" has no fact to read, so location transparency covers stdout-shaped operations only.
- Raw-mode restore is asymmetric.
previous_stty_state_stderris tracked for stty-state restoration, but the model gives no way to address stderr as a console channel.
Approach
err is an attribute of a console, not part of its identity. Console identity - the key for the ownership registry (punk_console_owners), fact store (punk_console_facts) and mode cache (punk_console_modecache) - remains the canonical {in out} pair: two consoles differing only in err target are the same terminal, and keeping the key stable means the G-007 slices are untouched.
- Spec grammar. console_spec_resolve (and the -console option everywhere) accepts a 3-element {in out err} list. The 3-element slot is free in the existing grammar (pairs are llength 2, instance names 1, voo object values >= 7). Existing spec forms are unchanged.
- Object layer. ::opunk::Console gains a nullable err field beside o_in/o_out (additive base-class change; subclasses without a stderr-equivalent leave it unset).
- Resolution defaults. An unset err resolves to process stderr for the default console {stdin stdout} (matching where its diagnostics go today) and to the console's own out channel for any other console (single-stream backends merge, losing nothing they had).
- Diagnostics routing. punk::console warnings/diagnostics raised while operating on a
resolvable console are emitted to that console's effective err. Raw
puts stderrremains only for contexts where no console is in play (e.g module-load-time warnings). - Emit path. A first-class emit-to-err form (shape decided at implementation time - e.g an err-target variant of the opt_console_out convention) exercised by at least one real consumer such as punk::repl. The two consumer-shaped clauses divide deliberately: the internal diagnostics migration (item 4) proves the abstraction serves its own first consumer and is self-contained; the external consumer proves the public emit form is usable by subshells wanting to present stderr distinctly.
- Cross-thread discovery. The effective err is exposed through console_fact_get under
fact key
err, returning the effective err channel name, so any thread/interp of a session can answer "where do this console's diagnostics go" (G-007 location transparency extends to the err attribute).
Alternatives considered
- err in the identity key: canonical {in out err} triple - rejected: conflates routing with identity (a console does not become a different terminal when its diagnostic target changes), and churns the G-007 registry/fact/cache keying and its tests for no modelling gain.
- Scope-boundary documentation only (no third channel) - rejected by the user for subshell flexibility: G-001 backends and G-009 themes need a distinct diagnostic stream to be first-class, and a parallel unmanaged diagnostics mechanism would fragment the abstraction. (Had punk::console stayed a pure terminal-control library, this would have been the right answer - the dual-channel model is correct for ANSI control itself.)
- Uniform default err == out (including the default console) - rejected: silently changes where the default console's diagnostics land today (process stderr), breaking redirection and 2>-style capture users rely on.
- Unset err means none (error on emit-to-err) - rejected: pushes fallback burden onto every caller and makes diagnostics emission failure-prone exactly where it must be reliable.
Notes
- The "-console surface" is the choke points, not per-site edits. The module's documented convention already funnels every -console site through a small resolution set: console_spec_resolve, internal::hybrid_console_spec, internal::opt_console_out / opt_console_channels (+_var variants), internal::spec_to_channelpair, and the shared PUNKARGS fragments ::punk::console::argdoc::console_opts / console_emit_opts. Accepting the 3-element form there satisfies "every -console site" by construction; a site that bypasses these helpers is already a convention violation to be fixed on discovery, not a reason to audit the whole module up front.
- Channel-name locality. Like in/out, an err channel name is thread-local for non-std channels; the fact-store exposure inherits the owner-qualified keying that already handles this for other facts. What is discoverable cross-thread is the owner-context name (and the default-console case, where std names are process-wide).
- G-001 interaction. G-001's acceptance requires the base class unchanged by G-001's own work; the nullable err field is an additive change made by this goal. If G-011 lands first, G-001 subclasses simply inherit the field; sequencing either way is safe. Backend authors should note the default is "merge into out": a backend with a genuinely distinct diagnostic stream (e.g a separate ssh extended-data channel or a dedicated tk widget pane) must set err explicitly at construction to get separation.
- previous_stty_state_stderr stays as-is. It preserves stty state of the process stderr fd across raw-mode cycling - terminal-mode restoration, orthogonal to diagnostic output routing. This goal adds nothing to it and removes nothing from it.
- G-008/G-009 interaction. Once err exists, a per-subshell re-route of diagnostics is console state that a G-008 state set could capture; that is enrichment for those goals, not part of this goal's acceptance.
- Not in scope. Channel transforms/filtering on err (shellfilter stacks), a separate err reader, and any change to shellrun's process-layer stdout/stderr handling - this goal defines the console-side slot they can target.
- Archived-goal references in this file: G-001 achieved 2026-07-11 (goals/archive/G-001-pluggable-console-backends.md);G-007 achieved 2026-07-05 (goals/archive/G-007-console-location-transparency.md).