Problem
The PTY fix made orchestrated commands safer in non-interactive environments, but it also removed a useful side effect: child commands no longer reliably emit colored output when DevTools is run from an interactive console.
There is a second usability issue in the same surface. Aggregate commands such as composer dev-tools, composer dev-tools standards, composer dev-tools reports, and other commands that use ProcessQueue log a DevTools message, stream the child process output directly, and then immediately log the next command. Outside GitHub Actions grouping, that output can feel visually glued together and makes it hard to see which nested command a given block belongs to.
Current Behavior
ProcessQueue streams child process output through Symfony Process callbacks without enabling PTY, which is correct for CI stability.
- GitHub Actions output can be grouped through
GithubActionOutput::group(...).
- Local console output does not get an equivalent visible section boundary.
FORCE_COLOR=1 is configured by several workflows for the top-level DevTools process, but the nested Symfony Process instances do not consistently receive a color-friendly environment.
- Tools that decide ANSI support from TTY detection or environment variables may disable color once PTY is no longer used.
Expected Behavior
- Orchestrated commands should keep colored output in interactive terminals without reintroducing PTY-dependent failures.
- GitHub Actions runs should also preserve color where the underlying tool supports forced ANSI output.
- Local console output should clearly show when a nested command starts and ends, with readable spacing or section boundaries around each child process.
- JSON and agent-oriented output modes should remain machine-readable and should not receive decorative local section chrome.
Failure Surface
Likely affected surfaces include:
src/Process/ProcessQueue.php
src/Process/ProcessBuilder.php
- aggregate commands that enqueue subprocesses, including standards, reports, docs/phpdoc, metrics, tests, code-style, refactor, wiki, dependencies, and sync flows where applicable
- workflow environments that currently set
FORCE_COLOR=1 only for the top-level command
- docs that describe JSON/pretty JSON output and command orchestration behavior
Proposed Strategy
Keep the non-PTY model and make color/section behavior explicit instead of relying on terminal inheritance.
1. Propagate a color-friendly environment to child processes
When ProcessBuilder or ProcessQueue creates/runs nested Process instances, ensure the process receives the relevant environment from the current runtime. At minimum evaluate propagating or setting:
FORCE_COLOR=1 when the parent output is decorated or when GitHub Actions already sets it;
CLICOLOR_FORCE=1 for tools that honor it;
NO_COLOR as an opt-out that must not be overridden;
TERM when present and useful;
- any existing command-specific env already set by callers.
The important rule is: do not restore PTY just to get colors. Colors should be forced through environment and explicit tool flags where available.
2. Keep tool-specific ANSI flags where they exist
Some tools support explicit ANSI flags or output configuration. Audit whether we should add or preserve flags such as:
- Symfony Console
--ansi for nested composer dev-tools ... calls when the parent output is decorated;
- PHPUnit color-related flags when not in JSON/coverage-only machine mode;
- Rector/ECS/PHP-CS-Fixer/phpDocumentor behavior under
FORCE_COLOR and non-TTY output.
Prefer central propagation first, then add per-tool flags only where the tool ignores standard color env variables.
3. Improve local nested command boundaries
GithubActionOutput::group(...) already gives CI a clear grouping primitive. For local consoles, add an equivalent presentation layer around ProcessQueue labels, for example:
- use Symfony Console
SymfonyStyle sections or ProcessHelper rendering where it gives a clear command boundary;
- or add a small DevTools-owned
ProcessOutputSectionRenderer that writes a blank line, a styled heading such as ▶ Running ..., streams output, and writes a closing newline/status line when useful;
- suppress or simplify this chrome when
--json, --pretty-json, or an agent-detected JSON mode is active.
The local renderer should avoid making output noisy, but it should make nested command ownership obvious.
4. Preserve stderr routing and GitHub Actions grouping
Any implementation must keep the current stdout/stderr separation and must not break GithubActionOutput::group(...) in CI. The local grouping behavior should complement the GitHub Actions grouping behavior, not replace it.
Implementation Notes
ProcessQueue is the best central point for runtime behavior because all orchestrated child processes flow through it.
ProcessBuilder may be the right place for default env assembly if we want built Process instances to carry inherited color env before they are enqueued.
- Avoid literal process objects in tests except where impossible; tests should mock
Process, OutputInterface, ConsoleOutputInterface, and any new renderer/capability abstractions.
- Add tests that assert child processes receive the expected env or that
Process::setEnv()/equivalent behavior is invoked without enabling PTY.
- Add tests for local output boundaries without requiring real subprocess execution.
- Verify behavior both with decorated console output and with JSON output flags.
Non-goals
- Re-enabling PTY globally for orchestrated processes.
- Changing command success/failure semantics.
- Reformatting every tool's native output into a DevTools-specific format.
- Adding decorative grouping to JSON or machine-readable output modes.
Acceptance Criteria
Functional Criteria
Regression Criteria
Suggested Verification Commands
composer dev-tools standards
FORCE_COLOR=1 composer dev-tools standards
composer dev-tools --json
composer dev-tools tests -- --filter=ProcessQueueTest
composer dev-tools code-style -- --fix --json
Problem
The PTY fix made orchestrated commands safer in non-interactive environments, but it also removed a useful side effect: child commands no longer reliably emit colored output when DevTools is run from an interactive console.
There is a second usability issue in the same surface. Aggregate commands such as
composer dev-tools,composer dev-tools standards,composer dev-tools reports, and other commands that useProcessQueuelog a DevTools message, stream the child process output directly, and then immediately log the next command. Outside GitHub Actions grouping, that output can feel visually glued together and makes it hard to see which nested command a given block belongs to.Current Behavior
ProcessQueuestreams child process output through Symfony Process callbacks without enabling PTY, which is correct for CI stability.GithubActionOutput::group(...).FORCE_COLOR=1is configured by several workflows for the top-level DevTools process, but the nested SymfonyProcessinstances do not consistently receive a color-friendly environment.Expected Behavior
Failure Surface
Likely affected surfaces include:
src/Process/ProcessQueue.phpsrc/Process/ProcessBuilder.phpFORCE_COLOR=1only for the top-level commandProposed Strategy
Keep the non-PTY model and make color/section behavior explicit instead of relying on terminal inheritance.
1. Propagate a color-friendly environment to child processes
When
ProcessBuilderorProcessQueuecreates/runs nestedProcessinstances, ensure the process receives the relevant environment from the current runtime. At minimum evaluate propagating or setting:FORCE_COLOR=1when the parent output is decorated or when GitHub Actions already sets it;CLICOLOR_FORCE=1for tools that honor it;NO_COLORas an opt-out that must not be overridden;TERMwhen present and useful;The important rule is: do not restore PTY just to get colors. Colors should be forced through environment and explicit tool flags where available.
2. Keep tool-specific ANSI flags where they exist
Some tools support explicit ANSI flags or output configuration. Audit whether we should add or preserve flags such as:
--ansifor nestedcomposer dev-tools ...calls when the parent output is decorated;FORCE_COLORand non-TTY output.Prefer central propagation first, then add per-tool flags only where the tool ignores standard color env variables.
3. Improve local nested command boundaries
GithubActionOutput::group(...)already gives CI a clear grouping primitive. For local consoles, add an equivalent presentation layer aroundProcessQueuelabels, for example:SymfonyStylesections orProcessHelperrendering where it gives a clear command boundary;ProcessOutputSectionRendererthat writes a blank line, a styled heading such as▶ Running ..., streams output, and writes a closing newline/status line when useful;--json,--pretty-json, or an agent-detected JSON mode is active.The local renderer should avoid making output noisy, but it should make nested command ownership obvious.
4. Preserve stderr routing and GitHub Actions grouping
Any implementation must keep the current stdout/stderr separation and must not break
GithubActionOutput::group(...)in CI. The local grouping behavior should complement the GitHub Actions grouping behavior, not replace it.Implementation Notes
ProcessQueueis the best central point for runtime behavior because all orchestrated child processes flow through it.ProcessBuildermay be the right place for default env assembly if we want builtProcessinstances to carry inherited color env before they are enqueued.Process,OutputInterface,ConsoleOutputInterface, and any new renderer/capability abstractions.Process::setEnv()/equivalent behavior is invoked without enabling PTY.Non-goals
Acceptance Criteria
Functional Criteria
NO_COLORopt-out is present.FORCE_COLOR=1is configured.Regression Criteria
setPty()is not called for queued processes.FORCE_COLOR=1.Suggested Verification Commands
composer dev-tools standardsFORCE_COLOR=1 composer dev-tools standardscomposer dev-tools --jsoncomposer dev-tools tests -- --filter=ProcessQueueTestcomposer dev-tools code-style -- --fix --json