NAME
App::karr::Foundation::Runner - karr-foundation command execution -- fork/pipe/select tee + run classification
VERSION
version 0.600
DESCRIPTION
App::karr::Foundation::Runner runs a single agent command for App::karr::Foundation. It forks the command under /bin/sh -c, reads its combined stdout/stderr over a native pipe, and tees each chunk to the persistent .karr.log, the terminal (when streaming), and an in-memory buffer the run is classified from, enforcing the per-run max_runtime timeout. A weak back-reference to the owning foundation supplies shared options and helpers (dry_run, _stream_to_terminal, _prompt_for, _append_log, _say_verbose).
That buffer is read twice over, in this order. First for the run's own report: an agent invoked with --output-format json ends its output with a JSON object saying whether the run failed, how it ended, how many turns it took, how long it ran and what it cost. _run_result finds it -- at the tail of the output, which is the only place a mixture of prose and JSON cannot be misread -- and _result_error says whether the ending it describes is a common error and of what kind.
Only where a run left no report does the older text scan run: observable common errors (rate limit, auth, network, 5xx, ...) matched against the transcript, where a symptom word counts only next to a failure word on the same line, or inside a phrase an API really emits, and an HTTP status only where something adjacent marks it as one. The drain asks that at all only for a run that made no progress -- see App::karr::Foundation's "Drain semantics".
The command is a shell template, not a string karr rewrites: PROMPT, KARR_REPO, KARR_ROLE and KARR_TASK are exported into the child's environment and /bin/sh expands them like any other parameter. A prompt's own backticks therefore stay text, and awk '{print $2}' reaches awk intact. KARR_TASK holds the id of the task a mode: ticket run was given and is empty in every other mode; the same id is spelled out in the prompt.
Where the agent came from a definition with an invocation contract that asks for structured live output (kind: claude-code, #188), the tee renders it: the assistant's own text goes to the terminal and to .karr.log as it arrives, while the raw stream stays in the classification buffer. That is what lets the contract ask for a machine-readable format without losing the live output an interactive run is watched for. A board that names no agent is on the older path -- the octets the command printed, verbatim, to both sinks.
A .karr.log it cannot open ends the run for that board before the command is started, never after: the agent is refused rather than launched unwatched. Once the fork has happened the parent owes it a waitpid, so nothing between the two may throw.
The agent is not the only thing that goes through this door. The on_drained hook (App::karr::Foundation) is a command in a repository that must not outlive the run that started it either, so it is started here rather than beside here -- one process-group kill, one timeout, one tee, one place where the live child is registered for the shutdown handler. What it does not share is the identity: role => 'hook' puts KARR_ROLE=hook in its environment and leaves PROMPT empty, so its own karr writes land in a different activity log from the agent's and it is never handed the instruction to go and pick a card. max_runtime => N gives it its own budget, because how long a board's agent may run says nothing about how long whatever the operator hung on on_drained may take. Nothing else in this method asks who the caller is: the run is classified by the drain, which simply does not classify a hook.
The coordination agent (App::karr::Foundation::Coordinator) is the third, and the one that needed a third option: it is an agent and needs an instruction, but not a board's -- so it passes prompt => ... beside role => 'coordinator' and gets its own text in $PROMPT instead of the board's or the hook's silence.
foundation
The owning App::karr::Foundation instance, held weak_ref to avoid a reference cycle. Supplies the shared options and helpers a run needs (dry_run, _stream_to_terminal, _prompt_for, _append_log, _say_verbose) that do not belong to the Runner itself.
SUPPORT
Issues
Please report bugs and feature requests on GitHub at https://github.com/Getty/karr/issues.
IRC
Join #langertha on irc.perl.org or message Getty directly.
CONTRIBUTING
Contributions are welcome! Please fork the repository and submit a pull request.
AUTHOR
Torsten Raudssus <getty@cpan.org>
COPYRIGHT AND LICENSE
This software is Copyright (c) 2026 by Torsten Raudssus <torsten@raudssus.de> https://raudssus.de/.
This is free software, licensed under:
The Artistic License 2.0 (GPL Compatible)