Skip to content

feat: let exec() and shell() take a cwd - #41

Merged
refsz merged 1 commit into
mainfrom
feat/exec-cwd
Aug 19, 2026
Merged

feat: let exec() and shell() take a cwd#41
refsz merged 1 commit into
mainfrom
feat/exec-cwd

Conversation

@refsz

@refsz refsz commented Aug 19, 2026

Copy link
Copy Markdown
Owner

Follow-up to the review finding in #40, where I listed this as "additive, so not a 0.2 decision". The additive part is right; the "so leave it" part was not, and here is why it is worth doing rather than deferring.

The gap

TaskContext hardcoded the project root as the working directory, while ExecutorInterface has accepted a cwd option all along. So a task that needs to run something in a subdirectory had exactly one route:

$ctx->shell('cd frontend && npm ci');

That puts the command back through a shell — the thing #30 removed as the default. A monorepo with a frontend/ is not an exotic case, so the safe path had a hole in it that pushed people to the unsafe one.

The change

cwd joins env, tty and timeout in the options array and defaults to the project root, so every existing call behaves exactly as before. That is what makes it additive: no signature changes, no behaviour changes, only a key that was previously impossible to pass.

Proven against the binary with the same argv twice:

> cat marker.txt                             cat: marker.txt: No such file or directory
> cat marker.txt   (cwd: <root>/frontend)    ich-bin-im-frontend

Three tests: the default is the project root, exec() honours an override, shell() takes the same option.

Correcting myself on the second item

In #40 I put two findings under one label. cwd is additive. The other one — TaskContext still taking ?OutputInterface and ?SputnikOutput next to the OutputChannel from #32 — is not additive, and my reason for deferring it does not survive a check:

TaskContext is final, so it cannot be mocked. A user who wants to unit-test their own task has to construct a real one, with nine required arguments, two of which are final classes. There is no documentation anywhere on how to test a task. So that constructor is user-facing, and changing it is a breaking change — I called it cheap because "users never construct one", which I had not verified.

That does not make it a 0.2 blocker, but it changes the reason: it is deferred because breaking changes are acceptable before 1.0, not because nobody is affected. The right time to do it is together with a documented testing story — a factory or builder so a task test does not need nine collaborators — and that is a design question, not a pre-tag cleanup.

Verified

vendor/bin/phpunit                        750 tests, OK (3 new)
vendor/bin/phpstan analyse                [OK] No errors
vendor/bin/php-cs-fixer fix --dry-run     0 of 153 files
mkdocs build --strict                     clean

Both this and #40 touch docs/tasks.md in the same region. They are independent branches off main; if the merge of the second one conflicts, tell me and I will rebase it rather than resolving it in the web editor.

🤖 Generated with Claude Code

https://claude.ai/code/session_018CTvnzcNYmFgm2HQcm821A

A command in a subdirectory had no way to say so: TaskContext hardcoded the
project root as cwd, even though ExecutorInterface has accepted a cwd option all
along. The workaround was shell('cd frontend && npm ci'), which puts the command
back through a shell - the thing argv execution exists to avoid.

`cwd` now joins env, tty and timeout in the options array and defaults to the
project root, so every existing call behaves exactly as before. Proven against
the binary, same argv twice:

    > cat marker.txt                          -> No such file or directory
    > cat marker.txt (cwd: .../frontend)      -> ich-bin-im-frontend
@refsz
refsz merged commit 0e92e5b into main Aug 19, 2026
10 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant