feat: give listeners the same output channel as tasks - #32
Merged
Conversation
A listener that ran a command produced nothing. Tasks got their executor from TaskRunner, built with the console output; listeners got the container service, built with no output at all - and it was the only consumer of that service. So a composer install in a listener ran silently for minutes, its failure went unreported, and listeners in the field fell back to echo, which bypasses the redacting output and can print a secret. OutputChannel is now the one place that knows where Sputnik writes. It is registered empty, filled once bin/sputnik has the redacting output, and shared by the executor, the tasks and the listeners. That removes the second executor path: TaskRunner no longer constructs its own but takes the container's, so a listener command is echoed, streams, and is masked exactly like a task command. Filling the channel at startup rather than in TaskRunner::run() matters for the case this started from: context:switch is a command, not a task, so its listeners never went through the task runner. An end-to-end test drives the real binary through a context switch and asserts the listener's command output appears; removing the channel fill makes it fail.
This was referenced Aug 19, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Same content as #31, retargeted at
main. #31 is marked merged, but its base wasfeature/argv-exec, and that branch had already been squash-merged intomainas #30 — so the commit landed on a branch that no longer leads anywhere andsrc/Console/OutputChannel.phpis absent frommain. This is the identical commit cherry-picked onto currentmain; nothing was rewritten. My mistake for stacking the PR instead of waiting for #30 to land.A listener that ran a command produced nothing at all. Not truncated, not unformatted — silent.
There were two executor paths, and only one had an output:
TaskRunner::createContext()builtnew ShellExecutor($output)shellExecutorservice, built with no argumentsThe evidence is the consumer count:
@shellExecutorwas referenced in exactly one place in the codebase — as theinnerof the listener executor. That service existed only for listeners, and it was the one without a destination. Acomposer installin a listener ran silently for minutes, a failure went unreported, and listeners in the field fell back toecho, which bypasses the redacting output and can print a secret.One place that knows where Sputnik writes
Console\OutputChannelis registered empty, filled oncebin/sputnikhas wrapped the output in the redacting decorator, and shared from there:ShellExecutortakes the channel instead of two nullable output parameters, andTaskRunnerno longer constructs its own executor but takes the container's. That removes the second path rather than giving it an output too.Filling the channel at startup, not in
TaskRunner::run(), is what matters for the case this started from:context:switchis a command, not a task, so its listeners never pass through the task runner — which is exactly where the reference project's reset-on-switch listener lives.Listeners can now write properly:
Deliberately not included: variables, templates or task execution for listeners. The missing output channel is a defect with evidence; the rest has no case yet.
Verification on top of current main
The proof is an end-to-end test driving the real binary through
context:switchwith a listener runningecho, asserting the output appears. It is discriminating: removing the single line that fills the channel makes it fail, verified by mutation.🤖 Generated with Claude Code
https://claude.ai/code/session_018CTvnzcNYmFgm2HQcm821A