Carry forward PR #3055 at 92f53c4c onto ef648e01, preserving contributor attribution and current main. Focused maintainer fixes reuse shared input ceilings, preserve late root context through outer bootstrap, bound frontmatter before immutable parsing, and assert real oversized input in the limits workflow.
Move stable hook metadata to a validated sidecar while preserving hook commands and installer identity. Reject moved fingerprints and duplicate IDs, and validate before updating metadata. Independent local review passed at c315271624a1fd055b992f2bff889ad2a0ff8a6b; CI run 34678210149 passed. Rollback: revert this squash commit.
* refactor(hooks): consolidate PostToolUse hooks into sync/async dispatchers
Replace 10 individual PostToolUse entries in hooks.json with two
consolidated dispatcher entries (post:dispatcher:sync /
post:dispatcher:async). The dispatcher's internal registry preserves
every hook ID, matcher, and profile, so ECC_DISABLED_HOOKS and
ECC_HOOK_PROFILE gating behave exactly as before.
Performance (Edit event, actual hooks.json commands spawned in
parallel like the harness does, median of 7 runs):
- Blocking hook latency: 81ms -> 49ms (~40% faster; 7 blocking
processes -> 1 sync dispatcher)
- Node processes per tool call: 10 -> 2 (7 blocking + 3 async
-> 1 sync + 1 async)
- observe-runner now runs in-process (~370ms) inside the async
dispatcher, which stays backgrounded (async: true, timeout 45s),
so it adds no user-facing latency.
Also:
- dashboard-web lists dispatcher-managed child hooks so the hook
inventory stays complete
- post-edit-console-warn refactored to export run() for in-process
dispatch while keeping standalone stdin behavior
- dispatcher stdin reading is multi-byte safe (StringDecoder) and
child hook exit codes propagate to the dispatcher exit code
* test(hooks): replace emoji literal with unicode escape for CI unicode safety check
* fix(hooks): adopt explicit cli() entrypoint and merge multi-hook stdout
Address Greptile review on #2494:
- Replace the non-standard 'require.main === undefined' guard with an
explicit exported cli(). The hooks.json bootstraps now call
require(s).cli(), so merely requiring the module (dashboard-web,
test runners, Jest, worker threads) can never trigger dispatch,
attach stdin listeners, or set process.exitCode.
- Replace last-writer-wins stdout with mergeHookStdout(): when several
hooks emit additionalContext envelopes they merge into a single
PostToolUse envelope; non-mergeable raw stdout keeps the last hook's
output and emits a stderr warning naming the dropped hook IDs, so
nothing is lost silently.
Also includes local formatter reformatting of the dispatcher and its
test file (no behavioral changes beyond the above).
* fix(hooks): keep post:bash:dispatcher phase reachable in minimal profile
The Greptile P1 premise was partially incorrect: sub-hooks without
explicit profiles default to standard,strict via parseProfiles()
(scripts/lib/hook-flags.js), so audit/cost logs never ran under the
minimal profile on main either — there is no user-visible regression.
However, main did spawn the bash dispatcher phase unconditionally and
let each sub-hook gate itself. Restore that semantic by opening the
outer registry gate to minimal,standard,strict so a future sub-hook
that opts into minimal is not silently blocked at the phase level.
Adds the previously missing minimal-profile async dry-run test.
* test(hooks): assert failing hook exit code propagates to real process status
Spawns the actual dispatcher subprocess with an injected failing hook
and asserts the OS-level exit status, stderr diagnostic, and suppressed
pass-through — closing the E2E gap CodeRabbit flagged on #2494.
* chore: retrigger CI (flaky windows powershell bootstrap test)