Problem
When dev-tools:sync is run from a globally installed fast-forward/dev-tools, the synchronized GrumPHP configuration can point a consumer repository at the global DevTools installation path instead of a stable project-local path.
That makes the generated extra.grumphp.config-default-path brittle: the path depends on the machine's global Composer home and can become awkward, non-portable, or invalid for collaborators and CI.
Current Behavior
dev-tools:sync:composer currently updates the consumer composer.json with extra.grumphp.config-default-path derived from the packaged grumphp.yml path.
In a normal local package install, that path resolves under the consumer vendor/fast-forward/dev-tools tree. In a global DevTools workflow, it can instead resolve to the global DevTools checkout or Composer home location.
The synchronized Git hooks also call vendor/bin/grumphp.phar directly, so there is no DevTools command boundary that can normalize the GrumPHP config path before invoking GrumPHP.
Expected Behavior
Running sync from a global DevTools installation MUST leave the consumer repository with deterministic GrumPHP behavior.
Consumer repositories MUST NOT receive an extra.grumphp.config-default-path value that points into the global DevTools install location. The resulting hooks and Composer metadata should work for local installs, global installs, collaborators, and CI without requiring machine-specific path edits.
Failure Surface
dev-tools:sync
dev-tools:sync:composer
- generated
composer.json extra.grumphp.config-default-path
- synchronized Git hooks under
resources/git-hooks/
- global invocation flows such as
composer global exec dev-tools -- dev-tools:sync or a globally installed dev-tools binary run against a consumer repository
Proposal
Stop relying on a consumer composer.json entry that points directly at the packaged DevTools grumphp.yml when sync is run globally.
A preferred direction is to introduce a DevTools-owned GrumPHP runner command or service that:
- resolves the canonical
grumphp.yml through FileLocatorInterface;
- invokes the GrumPHP shim with the resolved config path explicitly;
- keeps hook scripts pointed at a stable DevTools command boundary instead of calling
vendor/bin/grumphp.phar directly;
- avoids inserting or rewriting
extra.grumphp.config-default-path just to expose the packaged config.
If a command wrapper is not viable for every supported flow, sync may instead copy a managed grumphp.yml into the consumer repository. In that case, the copied file MUST be treated as a managed synchronized resource with deterministic overwrite or preservation rules.
Implementation Strategy
- Isolate GrumPHP config resolution behind a dedicated service that can use
FileLocatorInterface and be tested without running Git hooks.
- Update the sync/composer step so global runs do not write global-install paths into
composer.json.
- Update synchronized hooks to call the new DevTools command boundary, or update sync to copy
grumphp.yml project-locally if that path is selected.
- Preserve compatibility for repositories that already define their own GrumPHP configuration deliberately.
- Update docs that currently say sync sets
extra.grumphp.config-default-path.
Related Context
Non-goals
- Do not redesign the GrumPHP task set or rules in this issue.
- Do not solve the full
dev-tools-shim packaging design here.
- Do not migrate every synchronized resource to the manifest model in this issue.
- Do not require consumer repositories to hand-edit machine-specific GrumPHP paths.
Acceptance Criteria
Functional Criteria
Regression Criteria
Architectural / Isolation Criteria
- MUST: The core logic MUST be isolated into dedicated classes or services instead of living inside command or controller entrypoints.
- MUST: Responsibilities MUST be separated across input resolution, domain logic, processing or transformation, and output rendering when the change is non-trivial.
- MUST: The command or controller layer MUST act only as an orchestrator.
- MUST: The implementation MUST avoid tight coupling between core behavior and CLI or framework-specific I/O.
- MUST: The design MUST allow future extraction or reuse with minimal changes.
- MUST: The solution MUST remain extensible without requiring major refactoring for adjacent use cases.
- MUST: Argument and option resolution MUST be validated separately from command execution logic.
- MUST: Console formatting and rendering MUST stay separate from domain processing.
- MUST: Exit behavior, error messaging, and generated output MUST remain deterministic and testable.
- MUST: Data gathering or transformation MUST be isolated from filesystem writes or publishing steps.
- MUST: Generated output ordering and formatting MUST remain deterministic across runs.
- MUST: Re-running the workflow MUST be idempotent or clearly bounded in its side effects.
Problem
When
dev-tools:syncis run from a globally installedfast-forward/dev-tools, the synchronized GrumPHP configuration can point a consumer repository at the global DevTools installation path instead of a stable project-local path.That makes the generated
extra.grumphp.config-default-pathbrittle: the path depends on the machine's global Composer home and can become awkward, non-portable, or invalid for collaborators and CI.Current Behavior
dev-tools:sync:composercurrently updates the consumercomposer.jsonwithextra.grumphp.config-default-pathderived from the packagedgrumphp.ymlpath.In a normal local package install, that path resolves under the consumer
vendor/fast-forward/dev-toolstree. In a global DevTools workflow, it can instead resolve to the global DevTools checkout or Composer home location.The synchronized Git hooks also call
vendor/bin/grumphp.phardirectly, so there is no DevTools command boundary that can normalize the GrumPHP config path before invoking GrumPHP.Expected Behavior
Running sync from a global DevTools installation MUST leave the consumer repository with deterministic GrumPHP behavior.
Consumer repositories MUST NOT receive an
extra.grumphp.config-default-pathvalue that points into the global DevTools install location. The resulting hooks and Composer metadata should work for local installs, global installs, collaborators, and CI without requiring machine-specific path edits.Failure Surface
dev-tools:syncdev-tools:sync:composercomposer.jsonextra.grumphp.config-default-pathresources/git-hooks/composer global exec dev-tools -- dev-tools:syncor a globally installeddev-toolsbinary run against a consumer repositoryProposal
Stop relying on a consumer
composer.jsonentry that points directly at the packaged DevToolsgrumphp.ymlwhen sync is run globally.A preferred direction is to introduce a DevTools-owned GrumPHP runner command or service that:
grumphp.ymlthroughFileLocatorInterface;vendor/bin/grumphp.phardirectly;extra.grumphp.config-default-pathjust to expose the packaged config.If a command wrapper is not viable for every supported flow, sync may instead copy a managed
grumphp.ymlinto the consumer repository. In that case, the copied file MUST be treated as a managed synchronized resource with deterministic overwrite or preservation rules.Implementation Strategy
FileLocatorInterfaceand be tested without running Git hooks.composer.json.grumphp.ymlproject-locally if that path is selected.extra.grumphp.config-default-path.Related Context
Non-goals
dev-tools-shimpackaging design here.Acceptance Criteria
Functional Criteria
dev-tools:syncno longer writes anextra.grumphp.config-default-pathvalue that points into a global Composer home or globally installedfast-forward/dev-toolspath.vendor/bin/dev-tools dev-tools:syncremain compatible.Regression Criteria
dev-tools:sync:composerbehavior when DevTools is resolved outside the consumer repository.Architectural / Isolation Criteria