You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Externalize git hook logic into a dedicated git-hooks package #336
Issue #296 was merged as an intermediate fix while the broader git-hooks extraction work is still pending.
resources/git-hooks still contains the hook runtime logic, and the GrumPHP binary fallbacks were fixed directly in this repository in a temporary way.
Problem
We need a dedicated package boundary for Git hooks instead of keeping lifecycle scripts inside dev-tools, so this repository can depend on the extracted hook package and avoid duplicated or environment-specific logic.
Scope
Create and publish a dedicated fast-forward/git-hooks package/repository (or integrate the final target package that already exists) to host the managed Git hook scripts.
Migrate existing hook scripts from resources/git-hooks/* while preserving behavior for:
commit-msg
post-checkout
post-merge
pre-commit
Update dev-tools synchronization and installation flow to consume the externalized package instead of embedding the full hook logic.
Keep local and global execution compatibility, including GrumPHP resolution order and project-local overrides.
Keep documentation and generated output in sync with the new source-of-truth for hooks.
Acceptance Criteria
Hook execution logic is implemented in the dedicated git-hooks package and no longer maintained only inside resources/git-hooks.
dev-tools git-hooks installs and updates the expected hooks in consumer repositories.
GrumPHP invocation order remains unchanged in behavior for managed workflows:
vendor/bin/grumphp
vendor/bin/grumphp.phar
grumphp in PATH
Existing and regression tests for GitHooksCommand and self-update/global path checks remain green.
README/usage and command help that mention Git hook installation reflect the new package flow.
Any required changelog note is added for user-facing or release-process behavior changes.
changed the title [-][arch] Externalize git hook logic to dedicated git-hooks package[/-][+]Externalize git hook logic into a dedicated git-hooks package[/+]on May 11, 2026
Alignment with domain extraction plan (2026-10-21)
Retain this canonical hook extraction; #296 stays historical/closed. #240 maps domain ownership and #384 composes bootstrap. Hooks are executable assets, not installed by a skill: LLM Skills (#195) replaces skill duplication, not hook installation.
Before implementation, revalidate open #376: it proposes durable project-local GrumPHP configuration and local-or-pinned-CPX execution instead of persisting DevTools runtime/cache paths. Preserve stdin, arguments, execution once, failure propagation and project overrides; never overwrite an unknown local hook without a reviewed conflict plan. Define check/dry-run/no-op/rollback fixtures and ownership metadata consumed by DevTools.
Package/repository naming remains a proposal for validation. No repo creation or implementation is performed by this planning update.
CaptainHook assessment and isolated validation plan — 2026-10-25
Recommendation: prioritize CaptainHook for the next technical experiment in this existing issue. Felipe has explicitly prioritized the PHP ecosystem, shared configuration and actions implemented as PHP classes. These are supported capabilities, making CaptainHook a stronger first candidate for this context. Keep GrumPHP as the equivalence baseline and Lefthook as an alternative if an external/polyglot runner or parallel execution becomes decisive.
This is an evaluation and proposed experiment, not a migration decision or authorization to install tools, replace hooks, migrate consumers, update dependencies, create repositories, publish releases or merge PRs. No dynamic experiment has been run. This comment makes the assessment and acceptance criteria visible on GitHub; #336 remains canonical and #296 remains historical.
Evidence baseline and existing work
DevTools inspected at 35b5f78ac63813ec07aa3fb3eda08751541e28e1. Its GrumPHP configuration enables five workers, sets a 120-second process timeout and disables isolation of unstaged changes. The lock and installed metadata read during research identify GrumPHP shim 2.20.1.
The native composer_script task uses staged PHP paths as a trigger, then invokes composer run-script dev-tools without forwarding those paths. DevTools' standards aggregate schedules refactor, PHPDoc, code-style and reports. Switching runners alone does not make that aggregate index-scoped or lightweight.
PR #376 was re-read before publication: still open/unmerged, head 4f3cf00517fb64adbaa26689b64431d27b40266d. It advances the CPX/distribution work and must be revalidated before implementation; this assessment does not claim a full review of its new diff.
CaptainHook sources were examined at release 5.29.2, commit 805d44b0de8ed143f4acfb8c6bcb788cdbc42963. The future experiment must also identify the actual PHAR/checksum or Composer lock, including transitive versions. A source tag alone does not establish PHAR identity.
Preserve the existing #336 alignment contract: arguments/stdin, execution once, failure propagation, consumer overrides, unknown-hook preservation, preview/no-op/rollback. No duplicate extraction issue is needed.
1. Shared configuration matches the proposed adoption model, with explicit merge semantics
CaptainHook supports config.includes; its official configuration guide describes distributing shared presets in a Composer package. A small consumer configuration could include a DevTools-selected preset. The preset path/layout is still a proposal, not a file already delivered.
The 5.29.2 configuration factory resolves include paths relative to the declaring configuration, loads includes in order and appends actions from enabled included hooks before local actions. It does not deduplicate actions. A local hook's enabled flag can disable the combined hook.
Includes are not arbitrary settings inheritance: this hook-copying mechanism does not propagate the included configuration's bootstrap/plugins/settings into the root configuration. Do not assume an include alone activates a preservation plugin or exposes PHP classes. Nested includes also require an explicit root inclusion-depth contract.
Proposed boundary: #336 owns hook runtime/adapters and canonical presets; DevTools selects/composes/bootstrap-plans them. A DevTools facade is possible, but direct inclusion of the eventual hook package may avoid unnecessary nesting. Define extension versus replacement, action order, duplicate detection, missing-preset failures and consumer overrides. Never persist CPX cache paths or blindly replace custom configuration.
2. PHP classes are a real extension point; keep adapters small
The public Action interface receives configuration, console I/O, repository context and action options. It has a void execution contract with exception-based failure.
The PHP action runner constructs an action without constructor arguments. It does not automatically inject DevTools' services/container. Static-method actions exist, but their return value is discarded: returning false or 1 is not a failing-gate contract.
Prefer a thin interface-based adapter that validates options and invokes domain services or CLIs, reporting failure explicitly. Keep style/testing/upgrade policy in the owning domains. Do not introduce a universal container or duplicate domain implementations just to accommodate action construction.
3. Composer integration is separate from inline action definitions
Surface
Responsibility
Composer require/require-dev/autoload
Distribute engine, presets and classes; declare their loading.
captainhook.json
Define hooks/actions/options/conditions/includes.
Optional captainhook/hook-installer plugin
Activate/install hook scripts through Composer lifecycle integration.
Composer scripts
Can invoke installation or be invoked by an action; a separate execution layer.
The official Composer integration documents the optional installer plugin and a post-autoload-dump alternative, including the failure risk when using no-dev without the executable. Integration metadata such as executable/config paths is not an inline actions DSL. The engine package is a library, distinct from the installer plugin.
For the explicit-bootstrap/CPX direction discussed in #376, evaluate planned activation by DevTools rather than making automatic Composer-plugin installation mandatory. Dependency installation, configuration inclusion and Git-hook activation are three separate steps.
4. It covers the four currently separate events, but policy equivalence must be tested
CaptainHook declares pre-commit, commit-msg, post-checkout and post-merge. Its virtual post-change also includes post-rewrite; using it indiscriminately could broaden behavior.
Current event
Proposed mapping
Required equivalence
pre-commit
Domain-check action with staged conditions where appropriate
Required checks, actual snapshot inspected and failure behavior.
commit-msg
Message action or compatibility adapter
Exact message-file argument; do not invent a message convention absent today.
post-checkout
Conditional action for manifest changes
Branch-versus-file checkout mode, previous/new SHAs, cwd and chosen error policy.
post-merge
Conditional action for manifest changes
Squash argument and revision comparison; no assumed equivalence to ORIG_HEAD/HEAD.
Conditions include staged/changed-file filters. The FileChanged.Any source specifically discusses manifests after checkout/merge.
The existing DevTools post hooks run composer update --minimal-changes --no-interaction and suppress failure. This is our policy, not automatic CaptainHook behavior. Upstream's install example does not make install equivalent to update. Use stubs in the experiment; do not update real dependencies. Preserve legacy equivalence and proposed policy improvements as separate cases.
5. CPX/autoload is a distribution decision, not solved by includes
The Composer executable searches for an autoloader near its installation. In an isolated external runtime that does not establish visibility of consumer-defined Fast Forward classes.
Bootstrap handling differentiates Composer/PHAR execution and resolves explicit bootstrap paths relative to the configuration. Loading the application autoloader into the same process can reintroduce dependency/class conflicts: CPX process/resolver isolation is not namespace isolation.
Compare:
A small local Composer development package containing hook presets/adapters and a compatible engine.
A versioned external runtime containing both engine and adapters, invoking domain CLIs through process boundaries where appropriate.
For external-only consumers, a vendor/fast-forward/dev-tools include may not exist. Provide a durable, provenance-tracked local asset or another stable package contract; do not write ephemeral cache directories into consumer configuration. Test cache removal, offline/runtime absence, missing classes and conflicting consumer dependencies before selecting a topology.
6. Partial staging means an automated integrity test of staged/unstaged hunks
This is an engineering fixture for the agents/test harness, not a request for Felipe to evaluate files manually. Construct one file with one hunk staged for the next commit and another hunk left unstaged. Verify which bytes the checker reads and that both sets of changes remain intact after execution.
Staged-path placeholders/conditions select file names; a tool opening those paths can still read unstaged working-tree bytes. They do not themselves provide an index snapshot. The existing Code Standards clean base/head contract must not be relabeled as index selection or relaxed incidentally.
CaptainHook's optional PreserveWorkingTree plugin saves an unstaged patch, restores the working tree, handles intent-to-add entries and attempts patch restoration, including an autofix-conflict recovery path. It temporarily modifies working-tree/index state; existence of this mechanism is not proof of lossless recovery.
The hook runner calls afterHook in a finally around runHook, but invokes beforeHook before that block. Partial failure during preparation and interruption require dedicated tests. Begin with check-only; autofix remains explicit, with no automatic staging/commit. Reject unsupported combinations before writing rather than promising unproven preservation.
7. Worktree compatibility must include the exact transitive runtime
CaptainHook's installer asks the Git repository abstraction for the hook directory instead of directly appending .git/hooks in DevTools.
A concrete compatibility risk exists in the minimum allowed sebastianfeldmann/git 3.16.0 Repository implementation: it reads a gitdir pointer, prefixes the root and derives the default hooks directory from that administrative directory. Absolute gitdir pointers and common hook directories in linked worktrees need explicit fixtures. It does consult core.hooksPath.
This is not a claim that every CaptainHook installation fails: the allowed range may resolve a newer dependency and the PHAR may contain another revision. Record and inspect the actual resolved runtime. Test two worktrees concurrently, shared hooks, distinct indexes/configurations and relative/absolute hooksPath. Neither installation nor execution may capture the first worktree's paths and contaminate the other.
8. Order, CI and failure contracts
The CaptainHook runner processes actions sequentially with first-error or collect-errors behavior. This does not preserve GrumPHP's five-worker execution automatically. Measure equivalent domain work before introducing a scheduler.
The hook command skips actions and returns success when CI or CAPTAINHOOK_SKIP_HOOKS is interpreted as integer 1. CI gates must continue invoking domain commands directly; test this skip to prevent false-positive experiments. CLI actions convert unsuccessful processes into ActionFailed; do not assume numeric child-exit-code identity at the Git boundary.
Lefthook remains a useful external/polyglot parallel-runner alternative, but PHP class-based integration is now the explicit priority. None of these engines eliminates the snapshot/worktree acceptance gates. There is no demonstrated need for a custom engine.
Proposed isolated experiment and acceptance criteria — not executed
Use disposable synthetic repositories with isolated HOME/Composer/cache/Git configuration and no credentials/remotes. Install hooks only inside that laboratory when the experiment is separately authorized. Use fake executables first; dependency updates/installations are stubs.
Prove both local and external action loading, missing-class diagnostics, consumer dependency collisions, no-vendor consumers and cache removal. No temporary runtime path may be persisted.
Test multiple includes plus local actions and a two-level facade: deterministic order, no unintended double execution, missing/duplicate includes, local disable and explicit root bootstrap/plugins. Document the effective composition.
Exercise the four events individually, exact arguments/stdin/cwd, branch/file checkout, squash merge and revision comparisons. Test post-rewrite separately if virtual post-change is considered; no accidental extra event or duplicate action.
Automate partial-staging fixtures: staged and unstaged hunks in the same file, conflicting hunks, another invalid unstaged file, intent-to-add, untracked files, rename/delete, binary content, special file names and initial commit. Record actual checked bytes and compare index/working-tree/lock/stash state before and after.
Repeat preservation tests for action failure, preparation/beforeHook failure, exception, timeout, SIGINT/SIGTERM and process death. Any lost bytes or unintended staging fails acceptance. Recovery must be demonstrated, not inferred from callbacks. Test explicit autofix and conflicts only in a later round.
Run two linked worktrees concurrently with distinct indexes/configs, absolute/relative gitdir pointers and hooksPath, shared hooks and inherited Git variables. Compare the selected destination with Git's actual hook path. No cross-worktree writes or captured first-checkout paths.
Test unknown/custom hooks, non-writing preview, explicit conflict handling/apply, second-run no-op and rollback. No global Git changes or implicit Composer-plugin activation.
Compare equivalent checks against GrumPHP; measure no-op/one-file/full-project cold/warm latency and sequential-execution cost. Do not invent a latency SLA. Add Lefthook only if a concrete unmet requirement warrants it.
Keep mandatory CI on immutable head/base domain checks. Explicitly test CI=1 skip behavior, failure propagation and machine-readable result contracts. Hooks do not replace required CI checks.
Deliver a GitHub-visible pass/fail matrix with pinned versions, sanitized logs, preservation hashes and the local-versus-external distribution decision. Recommend adoption only after integrity, runtime, overrides and equivalence pass. Otherwise retain GrumPHP and record the demonstrated gap; do not automatically start a custom engine.
Tracking and publication scope
This comment is the assessment and experiment plan on the existing issue. Its original GrumPHP-specific acceptance criteria have not been silently rewritten: approving a CaptainHook implementation would require an explicit compatibility/migration/rollback revision here first. Related ownership remains #240/#384; #376 remains separate in-progress work.
No tools were installed, hooks replaced, consumers migrated, dependency updates run, repository files changed or PRs merged as part of this assessment/publication. Issue title/body, type, labels, milestone, project assignments/fields and relationships were intentionally left unchanged; existing related work is linked above. No credentials, private paths or environment dumps are included.
Activity
[-][arch] Externalize git hook logic to dedicated git-hooks package[/-][+]Externalize git hook logic into a dedicated git-hooks package[/+]coisa commentedon Oct 21, 2026
Alignment with domain extraction plan (2026-10-21)
Retain this canonical hook extraction; #296 stays historical/closed. #240 maps domain ownership and #384 composes bootstrap. Hooks are executable assets, not installed by a skill: LLM Skills (#195) replaces skill duplication, not hook installation.
Before implementation, revalidate open #376: it proposes durable project-local GrumPHP configuration and local-or-pinned-CPX execution instead of persisting DevTools runtime/cache paths. Preserve stdin, arguments, execution once, failure propagation and project overrides; never overwrite an unknown local hook without a reviewed conflict plan. Define check/dry-run/no-op/rollback fixtures and ownership metadata consumed by DevTools.
Package/repository naming remains a proposal for validation. No repo creation or implementation is performed by this planning update.
coisa commentedon Oct 25, 2026
CaptainHook assessment and isolated validation plan — 2026-10-25
Recommendation: prioritize CaptainHook for the next technical experiment in this existing issue. Felipe has explicitly prioritized the PHP ecosystem, shared configuration and actions implemented as PHP classes. These are supported capabilities, making CaptainHook a stronger first candidate for this context. Keep GrumPHP as the equivalence baseline and Lefthook as an alternative if an external/polyglot runner or parallel execution becomes decisive.
This is an evaluation and proposed experiment, not a migration decision or authorization to install tools, replace hooks, migrate consumers, update dependencies, create repositories, publish releases or merge PRs. No dynamic experiment has been run. This comment makes the assessment and acceptance criteria visible on GitHub; #336 remains canonical and #296 remains historical.
Evidence baseline and existing work
35b5f78ac63813ec07aa3fb3eda08751541e28e1. Its GrumPHP configuration enables five workers, sets a 120-second process timeout and disables isolation of unstaged changes. The lock and installed metadata read during research identify GrumPHP shim 2.20.1.composer_scripttask uses staged PHP paths as a trigger, then invokescomposer run-script dev-toolswithout forwarding those paths. DevTools' standards aggregate schedules refactor, PHPDoc, code-style and reports. Switching runners alone does not make that aggregate index-scoped or lightweight.4f3cf00517fb64adbaa26689b64431d27b40266d. It advances the CPX/distribution work and must be revalidated before implementation; this assessment does not claim a full review of its new diff.805d44b0de8ed143f4acfb8c6bcb788cdbc42963. The future experiment must also identify the actual PHAR/checksum or Composer lock, including transitive versions. A source tag alone does not establish PHAR identity.1. Shared configuration matches the proposed adoption model, with explicit merge semantics
CaptainHook supports
config.includes; its official configuration guide describes distributing shared presets in a Composer package. A small consumer configuration could include a DevTools-selected preset. The preset path/layout is still a proposal, not a file already delivered.The 5.29.2 configuration factory resolves include paths relative to the declaring configuration, loads includes in order and appends actions from enabled included hooks before local actions. It does not deduplicate actions. A local hook's enabled flag can disable the combined hook.
Includes are not arbitrary settings inheritance: this hook-copying mechanism does not propagate the included configuration's bootstrap/plugins/settings into the root configuration. Do not assume an include alone activates a preservation plugin or exposes PHP classes. Nested includes also require an explicit root inclusion-depth contract.
Proposed boundary: #336 owns hook runtime/adapters and canonical presets; DevTools selects/composes/bootstrap-plans them. A DevTools facade is possible, but direct inclusion of the eventual hook package may avoid unnecessary nesting. Define extension versus replacement, action order, duplicate detection, missing-preset failures and consumer overrides. Never persist CPX cache paths or blindly replace custom configuration.
2. PHP classes are a real extension point; keep adapters small
The public Action interface receives configuration, console I/O, repository context and action options. It has a void execution contract with exception-based failure.
The PHP action runner constructs an action without constructor arguments. It does not automatically inject DevTools' services/container. Static-method actions exist, but their return value is discarded: returning false or 1 is not a failing-gate contract.
Prefer a thin interface-based adapter that validates options and invokes domain services or CLIs, reporting failure explicitly. Keep style/testing/upgrade policy in the owning domains. Do not introduce a universal container or duplicate domain implementations just to accommodate action construction.
3. Composer integration is separate from inline action definitions
The official Composer integration documents the optional installer plugin and a post-autoload-dump alternative, including the failure risk when using no-dev without the executable. Integration metadata such as executable/config paths is not an inline actions DSL. The engine package is a library, distinct from the installer plugin.
For the explicit-bootstrap/CPX direction discussed in #376, evaluate planned activation by DevTools rather than making automatic Composer-plugin installation mandatory. Dependency installation, configuration inclusion and Git-hook activation are three separate steps.
4. It covers the four currently separate events, but policy equivalence must be tested
CaptainHook declares pre-commit, commit-msg, post-checkout and post-merge. Its virtual post-change also includes post-rewrite; using it indiscriminately could broaden behavior.
Conditions include staged/changed-file filters. The FileChanged.Any source specifically discusses manifests after checkout/merge.
The existing DevTools post hooks run
composer update --minimal-changes --no-interactionand suppress failure. This is our policy, not automatic CaptainHook behavior. Upstream's install example does not make install equivalent to update. Use stubs in the experiment; do not update real dependencies. Preserve legacy equivalence and proposed policy improvements as separate cases.5. CPX/autoload is a distribution decision, not solved by includes
The Composer executable searches for an autoloader near its installation. In an isolated external runtime that does not establish visibility of consumer-defined Fast Forward classes.
Bootstrap handling differentiates Composer/PHAR execution and resolves explicit bootstrap paths relative to the configuration. Loading the application autoloader into the same process can reintroduce dependency/class conflicts: CPX process/resolver isolation is not namespace isolation.
Compare:
For external-only consumers, a vendor/fast-forward/dev-tools include may not exist. Provide a durable, provenance-tracked local asset or another stable package contract; do not write ephemeral cache directories into consumer configuration. Test cache removal, offline/runtime absence, missing classes and conflicting consumer dependencies before selecting a topology.
6. Partial staging means an automated integrity test of staged/unstaged hunks
This is an engineering fixture for the agents/test harness, not a request for Felipe to evaluate files manually. Construct one file with one hunk staged for the next commit and another hunk left unstaged. Verify which bytes the checker reads and that both sets of changes remain intact after execution.
Staged-path placeholders/conditions select file names; a tool opening those paths can still read unstaged working-tree bytes. They do not themselves provide an index snapshot. The existing Code Standards clean base/head contract must not be relabeled as index selection or relaxed incidentally.
CaptainHook's optional PreserveWorkingTree plugin saves an unstaged patch, restores the working tree, handles intent-to-add entries and attempts patch restoration, including an autofix-conflict recovery path. It temporarily modifies working-tree/index state; existence of this mechanism is not proof of lossless recovery.
The hook runner calls afterHook in a finally around runHook, but invokes beforeHook before that block. Partial failure during preparation and interruption require dedicated tests. Begin with check-only; autofix remains explicit, with no automatic staging/commit. Reject unsupported combinations before writing rather than promising unproven preservation.
7. Worktree compatibility must include the exact transitive runtime
CaptainHook's installer asks the Git repository abstraction for the hook directory instead of directly appending .git/hooks in DevTools.
A concrete compatibility risk exists in the minimum allowed sebastianfeldmann/git 3.16.0 Repository implementation: it reads a gitdir pointer, prefixes the root and derives the default hooks directory from that administrative directory. Absolute gitdir pointers and common hook directories in linked worktrees need explicit fixtures. It does consult core.hooksPath.
This is not a claim that every CaptainHook installation fails: the allowed range may resolve a newer dependency and the PHAR may contain another revision. Record and inspect the actual resolved runtime. Test two worktrees concurrently, shared hooks, distinct indexes/configurations and relative/absolute hooksPath. Neither installation nor execution may capture the first worktree's paths and contaminate the other.
8. Order, CI and failure contracts
The CaptainHook runner processes actions sequentially with first-error or collect-errors behavior. This does not preserve GrumPHP's five-worker execution automatically. Measure equivalent domain work before introducing a scheduler.
The hook command skips actions and returns success when CI or CAPTAINHOOK_SKIP_HOOKS is interpreted as integer 1. CI gates must continue invoking domain commands directly; test this skip to prevent false-positive experiments. CLI actions convert unsuccessful processes into ActionFailed; do not assume numeric child-exit-code identity at the Git boundary.
Lefthook remains a useful external/polyglot parallel-runner alternative, but PHP class-based integration is now the explicit priority. None of these engines eliminates the snapshot/worktree acceptance gates. There is no demonstrated need for a custom engine.
Proposed isolated experiment and acceptance criteria — not executed
Deliver a GitHub-visible pass/fail matrix with pinned versions, sanitized logs, preservation hashes and the local-versus-external distribution decision. Recommend adoption only after integrity, runtime, overrides and equivalence pass. Otherwise retain GrumPHP and record the demonstrated gap; do not automatically start a custom engine.
Tracking and publication scope
This comment is the assessment and experiment plan on the existing issue. Its original GrumPHP-specific acceptance criteria have not been silently rewritten: approving a CaptainHook implementation would require an explicit compatibility/migration/rollback revision here first. Related ownership remains #240/#384; #376 remains separate in-progress work.
No tools were installed, hooks replaced, consumers migrated, dependency updates run, repository files changed or PRs merged as part of this assessment/publication. Issue title/body, type, labels, milestone, project assignments/fields and relationships were intentionally left unchanged; existing related work is linked above. No credentials, private paths or environment dumps are included.