feat(hooks): Dim 9 residuals 1/2 — prompt evaluator prod wiring + 6 newly-live hook events#117
Conversation
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: ca75b7c81d
ℹ️ About Codex in GitHub
Codex has been enabled to automatically review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
When you sign up for Codex through ChatGPT, Codex can also answer questions or update the PR, like "@codex address that feedback".
| # Dim 9 — file_changed: a dedicated event for workspace mutations | ||
| # so operators can hook formatters/linters/sync without pattern- | ||
| # matching every post_tool payload. Fire-and-forget like post_tool. | ||
| if tool in _FILE_MUTATING_TOOLS: |
There was a problem hiding this comment.
Gate file_changed on successful writes
For write_file, edit_file, and notebook_edit, this branch runs after every tool result, including args_invalid, permission_denied, workspace_escape, or write_failed envelopes returned by _dispatch_builtin_inner. In those cases no workspace file changed, but formatter/sync hooks still receive file_changed with the requested path, so operators can run side effects against stale or nonexistent files; parse the result or otherwise only emit this event after a successful mutation.
Useful? React with 👍 / 👎.
| provider, upstream, _params = registry.resolve( | ||
| m, aliases=_alias_entries(_models_cfg) | ||
| ) |
There was a problem hiding this comment.
Resolve hook evaluator aliases from the live config
This prompt evaluator closes over _models_cfg from gateway startup, while the config watcher hot-reloads providers/models by replacing state.provider_registry/state.config and only calls HookRunner.reload() for hook definitions. After /admin/config/reload changes [models].aliases, models.default, or hooks.evaluator_model, prompt-kind hooks continue resolving the judge model with the boot-time alias/default until a restart, even though the rest of the provider stack has been hot-swapped.
Useful? React with 👍 / 👎.
| try: | ||
| await run_event( | ||
| "file_changed", | ||
| {"tool": tool, "path": _path}, |
There was a problem hiding this comment.
Pass tool_name to file_changed hooks
When operators configure a declarative FileChanged group with matcher = "write_file" or edit_file, it never matches this event because HookRunner.run_event_async() derives the declarative matcher input from payload["tool_name"], while this payload only sets tool. The hook still fires for wildcard matchers, but tool-specific file_changed hooks are silently skipped; include tool_name (or adjust the runner) so the new event can be filtered by the mutating tool that caused it.
Useful? React with 👍 / 👎.
…ewly-live hook events
Closes the bulk of the verified-open Dim 9 gaps
(audit/GAP_CLOSE_2026-07-08.md B1/B2/B3).
B1 — prompt-kind production evaluator. The declarative engine shipped
prompt/agent hook kinds in v1.25.0 but both HookRunner construction
sites passed only rule_matcher, so those kinds silently failed open
(_exec_evaluator's "unwired:" warn-once). New
corlinman_server.hooks_evaluators builds a single-shot LLM judge — the
hook's prompt is the judging instruction, the event payload the
evidence, the answer a {"ok", "reason"} verdict the engine already
coerces. Wired at BOTH sites (main._build_hook_runner + c2_wiring),
each resolving its provider LAZILY per fire (hot-reload picked up;
independent of provider-bootstrap ordering). Judge model:
hooks.evaluator_model > CORLINMAN_HOOK_EVAL_MODEL > models.default.
Reasoning deltas never count as verdict text; unparseable/no-model
keeps today's fail-open. Agent-kind gets the register_hook_agent_runner
late-binding seam + verdict parsing; production registration stays a
ledgered residual (needs an out-of-turn subagent entry point).
B2/B3 — six accepted-but-dormant events gain live emitters:
- pre_compact: blocking consult in the reasoning loop when compaction
is imminent (gated on the same elide threshold _compact_history
applies, so quiet rounds never fire); deny defers the budget-path
compaction one round, the overflow shrink stays unconditional as the
backstop.
- session_start: servicer chat entry, once per session_key per process
(bounded seen-set).
- session_reset: console /new + /clear.
- notification: ask_user (needs_input) + background subagent terminal
states via a new dispatcher hook_notifier seam (gateway wires it to
state.hook_runner, read lazily per fire).
- file_changed: after post_tool for write_file/edit_file/notebook_edit,
payload carries the path.
- setup: once per process (gateway lifespan boot-complete + embedded
brain assembly).
hooks_live.LIVE_HOOK_EVENTS + the /hooks & admin live/dormant labels
updated (only session_end remains dormant); file_changed/setup added to
the declarative aliases + default-async map + the runner's protocol
listing. The stale /hooks-view test assertion (session_start dormant)
updated to session_end — the behavior change is this PR's point.
Tests: 7 evaluator (incl. end-to-end deny through HookRunner), 3
pre_compact loop, 6 emit-site (file_changed/notification/dispatcher
seam), session_start once-per-key drive-through, 2 console
session_reset. Affected suites 640+ green; ruff/mypy/lint-imports green.
ca75b7c to
e18ca66
Compare
|
You have reached your Codex usage limits for code reviews. You can see your limits in the Codex usage dashboard. |
Summary
Closes the bulk of the verified-open Dim 9 gaps (
audit/GAP_CLOSE_2026-07-08.mdB1/B2/B3).B1 — prompt-kind production evaluator
v1.25.0 shipped prompt/agent hook kinds but both
HookRunnerconstruction sites passed onlyrule_matcher→ those kinds silently failed open (_exec_evaluator'sunwired:warn-once). New hooks_evaluators.py:prompt= judging instruction, event payload = evidence (8KB cap), answer = the{"ok", "reason"}verdict shape the engine already coerces;main._build_hook_runner+c2_wiring), each resolving its provider lazily per fire (hot-reload picked up; independent of provider-bootstrap ordering);hooks.evaluator_model>CORLINMAN_HOOK_EVAL_MODEL>models.default;register_hook_agent_runnerlate-binding seam + verdict parsing land now; production registration remains a ledgered residual (needs an out-of-turn subagent entry point — same blocker as the Dim 7 items).B2/B3 — six accepted-but-dormant events gain live emitters
pre_compact_compact_historyappliessession_startsession_reset/new+/clearnotificationask_user(needs_input) + subagent terminal states (new dispatcherhook_notifierseam)file_changedpost_toolforwrite_file/edit_file/notebook_editpathsetuphooks_live.LIVE_HOOK_EVENTS+/hooks& admin live/dormant labels updated — onlysession_endremains dormant. One stale test assertion (session_start listed as dormant) updated accordingly; that behavior change is this PR's point.Tests
7 evaluator (incl. end-to-end deny through
HookRunner) · 3 pre_compact loop · 6 emit-site · session_start once-per-key drive-through · 2 console session_reset. Affected suites 640+ green; ruff / mypy / lint-imports green.Notes
CHANGELOG/version bump (1.29.0) will trivially conflict with the other in-flight wave PRs (fix(security): W8 multi-tenant session isolation (critical, hunt-51) #114/feat(mcp): Dim 5 leftovers 1/2 — sampling completer prod wiring + /mcp command + embedded MCP plane #115/feat(mcp): Dim 5 leftovers 2/2 — .mcp.json layered scopes + client resources #116); whichever merges later rebases the version line.