# G-113 make.tcl colour output is terminal-aware: SGR only on interactive tty, off when piped Status: active Scope: src/make.tcl (startup colour policy, define_global_ansi); src/AGENTS.md (invocation guidance); src/tests/shell/testsuites/punkexe/ (piped-output characterization if a suite proves feasible); layout make.tcl copies via established sync channels Goal: make.tcl emits ANSI SGR colour only when its output goes to an interactive terminal. Piped/redirected runs (agent harnesses, CI, log capture) produce SGR-free output with no caller action required; interactive terminal runs keep full colour by default; explicit env overrides work in both directions. Acceptance: With NO_COLOR unset, representative informational and build subcommands (at minimum check, help, workflow, and one build command such as modules) run with stdout redirected to a file produce zero ESC (0x1B) bytes on stdout and stderr, verified on tclsh 9.x and a repo punk kit. The same commands on a real interactive terminal retain colour (manual verification, Windows console plus one unix tty). NO_COLOR=1 still suppresses all colour; a documented force-colour override re-enables colour on piped output. The Tcl 8.6 probe-fallback policy is documented and behaves as decided. The interim src/AGENTS.md NO_COLOR bullet is revised to describe the implemented behaviour. ## Context Agents drive `tclsh src/make.tcl ...` with captured/piped output on every build-task closeout; today that output carries raw SGR sequences unless the caller remembered NO_COLOR=1, which no AGENTS doc mentioned before the interim bullet (see Notes). Humans at a terminal want the colour. The single existing switch (::punk::console::colour_disabled, set at make.tcl startup from NO_COLOR) is honoured dynamically by punk::ansi a+/a and by the define_global_ansi fallback branch, so auto-detection only needs to set that same variable from a tty probe at startup - no downstream changes. ## Approach - Startup policy in make.tcl, evaluated before any output: NO_COLOR set -> colour off; force-colour env (name settled in the work; FORCE_COLOR is the de-facto standard) -> colour on; otherwise probe stdout and enable colour only when it is a terminal. - Probe technique: channel-config key presence on stdout, mirroring the stdin_is_interactive precedent (chan configure key test; no package needed, core Tcl only). - Key the decision on stdout (primary output channel); stderr colour follows the same switch for simplicity - agents capture both anyway, and the force override covers the user-redirecting-stdout case. - Tcl 8.6 has no reliable probe (see make.tcl:316 comment); fail direction for colour is OFF (safe for agents), unlike stdin's ON. Document; reconsider only if 8.6 interactive colour matters in practice. - Out of scope: the REPL launched by `make.tcl shell` (shell colour is the shell's own concern - colour on/off + NO_COLOR there). ## Alternatives considered - Guidance-only (agents always set NO_COLOR=1) - rejected as the sole fix: relies on every harness remembering; kept as the documented explicit override and landed as interim guidance. - Post-hoc SGR stripping in harnesses - rejected: pushes the burden onto every consumer. - Per-channel colour policy (stderr coloured when it alone is a tty) - more faithful but more state; default is the stdout-keyed single switch, settled in the work. ## Progress - 2026-07-25 implementation landed (make.tcl + suite + docs; punkshell 0.22.2): startup policy block (precedence NO_COLOR > PUNK_FORCE_COLOR/FORCE_COLOR > stdout `-winsize` tty probe; modes tty|forced|nocolor|piped-plain|tcl86-plain, reported by `make.tcl check` and env PUNKBOOT_COLOUR_DEBUG=1), ansistrip write transform pushed on stdout+stderr for piped/8.6-unknown non-forced runs (zero-ESC guarantee covering module-side emitters such as punkcheck summaries; the shell subcommand pops it - repl colour stays the shell's concern), punk::ansi sgr_cache cleared per that module's contract for punk-exe-hosted pre-loaded interps, define_global_ansi has_ansi branch fully gated (A() palette all-empty when colour disabled), raw-SGR literal sites routed through the new ::punkboot::sgr gate (incl. one malformed \x1b\31m literal), make.tcl converted CRLF->LF per repo preference. DESIGN FINDING recorded: punk::ansi's colour_disabled deliberately strips COLOUR codes only (non-colour effects like bold survive; the state-changer must clear the sgr cache per the in-module contract). The zero-ESC piped contract therefore rides the make.tcl boundary transform, not colour_disabled; NO_COLOR on a real terminal keeps the module's semantics. - Verification evidence 2026-07-25: piped (native tclsh90 9.0.3): check/help/workflow/modules all exit 0 with zero ESC bytes; NO_COLOR=1 zero ESC; FORCE_COLOR=1 / PUNK_FORCE_COLOR=1 emit colour SGR on piped output (80 colour lines in help); NO_COLOR+FORCE emits effects but zero colour codes (NO_COLOR wins for colour); FORCE_COLOR=0 treated as unset. punk91 kit (script src/make.tcl): check/help/workflow exit 0, zero ESC. Tcl 8.6 (native bawt tclsh86): mode=tcl86-plain, zero ESC; FORCE re-enables ANSI. Windows console (hidden-console Start-Process recipe, real console handles): stdout -winsize probe succeeds ({120 30}), policy reports mode=tty colour_disabled=0 ansistrip=0. Unix pty (WSL native-fs staging, tclkit-902-Linux64 under script(1)): mode=tty colour_disabled=0; piped control run in the same staging: mode=piped-plain ansistrip=1. Suite: shell/testsuites/punkexe/maketclcolour.test 3/3 pass (punk902z). src/AGENTS.md interim NO_COLOR bullet replaced with implemented-behaviour guidance. Remaining manual item: human visual confirmation of colour appearance on live terminals (the mechanical decision + rendering chain is fully evidenced above). - Remains for acceptance: none beyond the recorded manual-visual note; flip is deferred to the make.tcl-window acceptance pass (with G-112/G-104) so the layout make.tcl copies sync once for the whole window. ## Notes - Activation freshness 2026-07-25: goals_xref.tcl score G-113 re-run at the proposed->active flip - G-112/G-104 window relations already recorded; new Related recorded from the re-run: G-061 (pseudoconsole expect-alternative) - a ConPTY-driven child presents a real tty to this goal's stdout probe, so G-061 machinery could later automate the interactive-colour half of the acceptance (the piped-characterization suite covers only the SGR-free direction). Activated together with G-112 + G-104 as one make.tcl-surface window. - 2026-07-22: interim src/AGENTS.md Work Guidance bullet landed (agents set NO_COLOR=1 for captured make.tcl runs) ahead of this goal's implementation; revise it to describe the auto-detection + overrides when this goal lands (tracked in Acceptance). - Existing machinery: NO_COLOR -> colour_disabled (make.tcl:5-7); punk::ansi honours colour_disabled dynamically with a -forcecolour in-module override (src/modules/punk/ansi-999999.0a1.0.tm); PUNKBOOT_PLAIN is the env-switch precedent (make.tcl:1655); stdin probe precedent at make.tcl:319. - Related: G-030 (achieved - prompt-free flags + the stdin probe this mirrors), G-011 (console stderr semantics - loose relation), G-104/G-112 (both touch make.tcl subcommand surfaces - coordinate if either goes active first). - make.tcl interface is product surface (root AGENTS.md): this behaviour change ships with a punkproject.toml bump (patch) + CHANGELOG entry.