fix(hooks): share matcher validation instances
This commit is contained in:
@@ -10,7 +10,7 @@ Why a shared lib at all: Codex deliberately reimplements a *subset* of the Claud
|
||||
|
||||
| Concern | Here (`dsh-hook-protocol`) | The bridge (`dsh-hooks-claude` / `-codex`) |
|
||||
|---|---|---|
|
||||
| Matcher validation + test | `matcherDiagnostic(pattern, mode)` for parse-time diagnostics; `compileMatchers(patterns, mode)` for repeated config-lifetime matching; `matchesMatcher(pattern, query, mode)` for one-shot contained matching | picks its native regex `mode` (`claude` = JavaScript, `codex` = Rust `regex`), rejects a config group carrying a diagnostic, and disposes the compiled set on teardown |
|
||||
| Matcher validation + test | `compileMatchers(patterns, mode)` exposes diagnostics and repeated config-lifetime matching from one compiled set; `matcherDiagnostic` / `matchesMatcher` are contained one-shot helpers | picks its native regex `mode` (`claude` = JavaScript, `codex` = Rust `regex`), compiles the unique runnable patterns once, rejects a group carrying a registry diagnostic, and disposes that same set on failure or teardown |
|
||||
| Run a hook | `runHook(bash, hook, opts, now)` — stdin payload + env via `ctx.bash`, decode | builds the per-event stdin **payload** + the dialect's **env** |
|
||||
| Decode output | `parseHookOutput(exit, stdout, stderr)` → neutral `HookOutput` | maps the neutral `HookOutput` onto a seam-specific typed Decision |
|
||||
| Merge N hooks | `mergeHookOutputs(outputs)` → most-restrictive `MergedHookOutcome` | — |
|
||||
@@ -19,7 +19,7 @@ Why a shared lib at all: Codex deliberately reimplements a *subset* of the Claud
|
||||
|
||||
## Primitives
|
||||
|
||||
- **`matcherDiagnostic(matcher, mode)` / `compileMatchers(matchers, mode)` / `matchesMatcher(matcher, query, mode)`** — match-all on absent/`''`/`'*'`; both dialects treat a pure `[A-Za-z0-9_|]+` pattern as exact pipe-separated alternatives. Other patterns are unanchored regexes compiled in the native dialect: JavaScript for Claude Code, Rust `regex` for Codex (including inline flags such as `(?i)`). Bridge parsers discard matcher fields for events without matcher subjects, then use `matcherDiagnostic` to reject an invalid consumed regex with a stable diagnostic before registering any hooks. Each bridge uses `compileMatchers` to compile every unique config pattern once, reuses it at every hook point, and disposes the finite set after detached runs drain on plugin teardown; this avoids the Rust/WASM allocator's non-shrinking high-water mark growing on every match. `matchesMatcher` remains the contained one-shot predicate, and invalid runtime patterns are non-matches rather than exceptions.
|
||||
- **`compileMatchers(matchers, mode)` / `matcherDiagnostic(matcher, mode)` / `matchesMatcher(matcher, query, mode)`** — match-all on absent/`''`/`'*'`; both dialects treat a pure `[A-Za-z0-9_|]+` pattern as exact pipe-separated alternatives. Other patterns are unanchored regexes compiled in the native dialect: JavaScript for Claude Code, Rust `regex` for Codex (including inline flags such as `(?i)`). A bridge parser first discards matcher fields for events without matcher subjects and collects the remaining runnable groups, then compiles their unique patterns once. It reads `registry.diagnostic(pattern)` from those exact instances, disposes the registry before throwing on an invalid consumed regex, or returns the same registry for runtime matching. The plugin reuses it at every hook point and disposes it after detached runs drain on teardown. Thus neither validation nor matching reconstructs a Rust/WASM regex and raises its non-shrinking memory high-water mark. `matcherDiagnostic` and `matchesMatcher` remain contained one-shot helpers; invalid runtime patterns are non-matches rather than exceptions.
|
||||
- **`runHook(bash, hook, options, now)`** — require and forward the caller-owned `options.signal`, serialize `options.payload` to the hook's stdin (with a trailing newline iff `options.trailingNewline`), merge `options.env` after the executor's credential scrub (the `dsh-bash` trusted-plugin surface), honor the hook's `timeoutSec` (else `options.defaultTimeoutMs` — the bridge owns the default, its config defaulting to the lib's `DEFAULT_HOOK_TIMEOUT_MS` 10-minute reference), and decode the result (threading `options.expectedEventName` to the codec). Cancellation therefore reaches the executor's process-group kill and join boundary. Never throws: an executor rejection (infra fault) becomes a `HookOutput` with `exitCode: undefined` (a non-blocking error). `now` is injected for testable durations.
|
||||
- **`parseHookOutput(exitCode, stdout, stderr, expectedEventName?)`** decodes exit status and structured stdout. Exit 2 blocks with stderr; other failures are non-blocking. A matching hook-specific permission decision overrides the legacy top-level decision; mismatched or missing event discriminators suppress only event-specific fields. Top-level fields remain event-agnostic, and successful non-JSON output is left to the bridge.
|
||||
- **`mergeHookOutputs(outputs)`** — fold the results of every hook that matched one point: permission precedence **deny > ask > allow**, halt sticky on the first `continue:false`, block reasons joined with `\n\n`, `additionalContext`/`systemMessages` accumulated in order.
|
||||
|
||||
Reference in New Issue
Block a user