Krikri::OutputRouting
Per-fiber stdout/print redirection, used only by TaskExecutor's --forks fan-out (run_task_for_hosts_in_parallel) to buffer each host's output separately, so concurrent hosts' lines never interleave - they're flushed in host order once every fiber for a given task has finished.
Safe with no lock: only one fiber ever executes Crystal code at any instant (cooperative scheduling - fibers only yield at explicit I/O waits), so this Hash's mutation and lookup can never actually race, even though several hosts' fibers are "concurrent" from the task executor's point of view. A fiber with no entry here writes straight to the real STDOUT, unchanged from today - this is the path every fiber outside of a --forks fan-out (and the main fiber itself) takes.
Class methods
Crystal sets SIGPIPE to ignore, so writing to a pipe whose reader
has already exited (krikri-playbook playbook.yml | head -2,
--version | head -1, quitting out of a pager) surfaces as an
IO::Error instead of quietly killing the process - and, with
nothing rescuing it, dumped a full Crystal stack trace to stderr
and exited 1.
Real ansible-playbook is silent here and exits 0 (verified against
ansible-playbook/ansible: --help | head -1 and
--version | head -1 both produce no stderr and PIPESTATUS[0]=0),
so match that. Note this is deliberately NOT fixed by restoring the
default SIGPIPE disposition (Signal::PIPE.reset): that would let
the kernel kill the whole process on ANY EPIPE, including a write
to a subprocess stdin that closed early, turning a currently
catchable error deep in the SSH/plugin paths into an abrupt death.
Scoping it to this program's own stdout writes keeps that behavior
unchanged.
Every output path in this codebase goes through the puts/print below (there is exactly one other STDOUT reference in the whole tree - current_io above), so this is the complete set of places a stdout EPIPE can originate.