feat(code): integrate Hooks v2 server lifecycle events - #4997
Conversation
There was a problem hiding this comment.
Non-interactive mode unconditionally trusts project hook configuration, enabling arbitrary command execution from a committed .deepagents/hooks.json when running against an untrusted repository in CI. A SubagentStart hook denial is also not enforced: the middleware only rewrites the task description and still calls the tool handler, allowing a hook policy intended to block subagent spawning to be bypassed.
Wire PreToolUse, PostToolUse, Stop, SubagentStart, and SubagentStop through LangGraph interrupts so the client HooksRuntime can execute handlers and return typed decisions. Co-authored-by: Johannes du Plessis <johannesduplessis117@gmail.com>
Co-authored-by: Johannes du Plessis <johannesduplessis117@gmail.com>
Co-authored-by: Johannes du Plessis <johannesduplessis117@gmail.com>
Co-authored-by: Johannes du Plessis <johannesduplessis117@gmail.com>
Apply PreToolUse ask/context/continue, surface notices, wire workspace trust, mount middleware on subagents, and harden Stop/SubagentStop decision handling. Co-authored-by: Cursor <cursoragent@cursor.com>
2028620 to
54c2175
Compare
Enforce SubagentStart denies, skip Stop on subagent graphs, and require `--trust-project-hooks` before loading project hook commands headlessly. Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Preserve server hook identity across graph replay, run pre-tool policy before HITL, and deduplicate client fulfillment side effects. Co-authored-by: Cursor <cursoragent@cursor.com>
Alexander Olsen (aolsenjazz)
left a comment
There was a problem hiding this comment.
Started reviewing this over the weekend, apparently I didn't press submit sorry.
These may just be noise by now
| fields = _context_mapping(runtime_context) | ||
| snapshot_id = fields.get("hooks_snapshot_id") | ||
| events = fields.get("hooks_server_events") | ||
| if not isinstance(snapshot_id, str) or not snapshot_id: |
There was a problem hiding this comment.
I mentioned this in a comment on _context_mapping but we're not making life easier for ourselves by returning dict[str, Any] from the context mapping fn - case in point this
There was a problem hiding this comment.
addressed
Alexander Olsen (aolsenjazz)
left a comment
There was a problem hiding this comment.
Stamping, but left a few questions which are worth ensuring we know the answer to
Didn't get too into depth but checked out the core files + systems
A hook resume against a stale configuration snapshot previously surfaced as an untyped ValueError traceback from the stream loop. The client fulfillment now raises HooksSnapshotChangedError, and both fulfillment call sites (Textual adapter and non-interactive runner) re-raise server-side resume validation failures as the same typed error so callers can distinguish the stale-checkpoint case and restart the turn.
The hook interrupt/resume payloads are produced by pydantic dump in JSON mode, so the shared JsonObject alias describes them more precisely than dict[str, Any] / dict[str, object]. Also drops the unused _ResumePayload alias.
Project-level hooks now require an explicit workspace trust decision before their commands run. Interactive users can allow once, remember the workspace, or skip project hooks; headless runs remain opt-in through --trust-project-hooks. Re-anchored onto the rebuilt dcd-71 branch after #4997 squash-merged; carries the full PR #5044 delta.
Hook execution now reports progress, warnings, notices, and permission decisions consistently in the TUI and non-interactive client. Configured statusMessage values appear while handlers run, and server-owned hook output is no longer confined to logs. Re-anchored onto the rebuilt project-trust branch after #4997 squash-merged; carries the full PR #5045 delta.
Project-level hooks now require an explicit workspace trust decision before their commands run. Interactive users can allow once, remember the workspace, or skip project hooks; headless runs remain opt-in through --trust-project-hooks. Re-anchored onto the rebuilt dcd-71 branch after #4997 squash-merged; carries the full PR #5044 delta.
Hook execution now reports progress, warnings, notices, and permission decisions consistently in the TUI and non-interactive client. Configured statusMessage values appear while handlers run, and server-owned hook output is no longer confined to logs. Re-anchored onto the rebuilt project-trust branch after #4997 squash-merged; carries the full PR #5045 delta.
Supersedes #5010 (auto-closed when #4997 squash-merged; GitHub seals force-pushed closed PRs). Related: [DCD-71](https://linear.app/langchain/issue/DCD-71/hooks-v2-integrate-client-owned-lifecycle-events) dcode now runs client-owned Hooks v2 lifecycle events consistently in interactive and headless sessions, including hook-driven approval decisions before permission prompts are shown. --- - Invokes `SessionStart` and `SessionEnd` at startup, resume, clear, compact, switch, and exit boundaries. - Applies `PermissionRequest` allow, deny, ask, interrupt, notice, terminal, and stop effects before client approval resolution. - Routes supported dcode notifications through one typed service with explicit wire mappings while avoiding migrated legacy duplicates. - Preserves session-start context for the next model turn and adds TUI/headless decision-parity coverage. - Ignores generated local transcript state through `.gitignore`. This branch was re-anchored onto `main` after #4997 squash-merged; the diff is identical to the approved #5010 head (`f4e8ee672`), minus a stale resurrection of `todo_list_prompt.md` that #5098 deleted on main. ## Review Guide 1. `hooks/client_lifecycle.py` — `ClientHookService`, context, permission outcomes 2. `app.py` — SessionStart/End boundaries + service wiring 3. `tui/textual_adapter.py`, `client/non_interactive.py` — interactive vs headless parity 4. Skim: `transcript.py`, `server_middleware.py` (PermissionRequest), `runtime.py` / `projection.py` 5. Tests: `test_client_lifecycle.py`, then adapter / non-interactive / server-lifecycle deltas <details> <summary>Test plan</summary> - 266 hooks unit tests pass on the rebuilt tip - 412 non-interactive / Textual adapter / status widget tests pass </details> <!-- branch-stack-start --> ------------------------- - main - **feat(code): integrate Hooks v2 client lifecycle events** :point_left: - #5105 - #5045 <sup>[Stack](https://www.git-town.com/how-to/proposal-breadcrumb.html) generated by [Git Town](https://github.com/git-town/git-town)</sup> <!-- branch-stack-end --> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
Project-level hooks now require an explicit workspace trust decision before their commands run. Interactive users can allow once, remember the workspace, or skip project hooks; headless runs remain opt-in through --trust-project-hooks. Re-anchored onto the rebuilt dcd-71 branch after #4997 squash-merged; carries the full PR #5044 delta.
Hook execution now reports progress, warnings, notices, and permission decisions consistently in the TUI and non-interactive client. Configured statusMessage values appear while handlers run, and server-owned hook output is no longer confined to logs. Re-anchored onto the rebuilt project-trust branch after #4997 squash-merged; carries the full PR #5045 delta.
Hook execution now reports progress, warnings, notices, and permission decisions consistently in the TUI and non-interactive client. Configured statusMessage values appear while handlers run, and server-owned hook output is no longer confined to logs. Re-anchored onto the rebuilt project-trust branch after #4997 squash-merged; carries the full PR #5045 delta.
Supersedes #5044 (auto-closed when #4997 squash-merged; GitHub seals force-pushed closed PRs). Project-level hooks now require an explicit workspace trust decision before their commands run. Interactive users can allow once, remember the workspace, or skip project hooks; headless runs remain opt-in through `--trust-project-hooks`. --- This closes the execution gap that left interactive project hooks permanently disabled while preserving fail-closed behavior. Trust is keyed to the canonical repository root, saved atomically, and enforced again at runtime if a snapshot is ever constructed inconsistently. `config path` and the threat model now expose the relevant project, user, and trust locations. Re-anchored onto `main` after #5104 squash-merged, so the PR diff contains only this branch's commits. ## Review Guide 1. `hooks/trust.py` — store, keys, atomic write, trust APIs 2. `hooks/loading.py` — project source ingest vs skip 3. `hooks/runtime.py` — fail-closed runtime guard 4. `main.py` — `--trust-project-hooks` + interactive prompt 5. Skim: `app.py`, `client/commands/config.py`, `THREAT_MODEL.md` 6. Tests: `test_trust.py`, then config/app/main touches <details> <summary>Test plan</summary> - Full hooks suite passes on the rebuilt stack tip. `test_trust.py` keeps one case per regressable behavior: canonical persistence, fail-closed corrupt-store handling, project-source provenance, the runtime trust guard, the three prompt outcomes, prompt suppression on persisted trust, and Textual wiring. </details> <!-- branch-stack-start --> ------------------------- - main - **feat(code): add project hooks workspace trust** 👈 - #5045 <sup>[Stack](https://www.git-town.com/how-to/proposal-breadcrumb.html) generated by [Git Town](https://github.com/git-town/git-town)</sup> <!-- branch-stack-end --> --------- Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
Hook execution now reports progress, warnings, notices, and permission decisions consistently in the TUI and non-interactive client. Configured statusMessage values appear while handlers run, and server-owned hook output is no longer confined to logs. Re-anchored onto the rebuilt project-trust branch after #4997 squash-merged; carries the full PR #5045 delta.
…4997) <!-- CURSOR_AGENT_PR_BODY_BEGIN --> Depends on langchain-ai#4971 Closes DCD-69 Server-owned Hooks v2 events now pause the agent over LangGraph interrupts, run on the client `HooksRuntime`, and resume with typed decisions. --- This starts the DCD-69 stack on top of the DCD-70 leaf (langchain-ai#4971). - Adds a versioned `hook_invocation` interrupt/resume transport around `HookInvocationRequest` / `HookInvocationResponse`. - Installs `ServerHooksMiddleware` for `PreToolUse`, `PostToolUse`, `Stop`, `SubagentStart`, and `SubagentStop`, gated by the session's configured server events so idle sessions skip the round-trip. - Creates a session-scoped client `HooksRuntime` and fulfills server interrupts from both the Textual adapter and headless runner. - Applies `deny`/`ask` as blocking tool errors for now; `ask` approval UX remains for DCD-71. ## Review Guide 1. `hooks/server_middleware.py` — middleware, gating, interrupt raise, decision apply 2. `hooks/interrupt.py` — `hook_invocation` pause/resume contract 3. `hooks/client.py` — client fulfillment + resume ledger 4. `agent.py` — middleware install (main + subagents) 5. `tui/textual_adapter.py`, `client/non_interactive.py` — interrupt fulfillment call sites 6. Skim: `envelope.py`, `context.py`, `runtime.py`, `_cli_context.py`, small `app`/`auto_mode`/`main`/`snapshot`/`engine` touches 7. Tests: `test_server_lifecycle.py`, then `test_agent` / `test_non_interactive` / `test_engine` <details> <summary>Test plan</summary> - `uv run --group test pytest tests/unit_tests/hooks` — 133 passed - `make lint` in `libs/code` </details> <!-- branch-stack-start --> ------------------------- - main - **feat(code): integrate Hooks v2 server lifecycle events** :point_left: - langchain-ai#5010 - langchain-ai#5044 - langchain-ai#5045 <sup>[Stack](https://www.git-town.com/how-to/proposal-breadcrumb.html) generated by [Git Town](https://github.com/git-town/git-town)</sup> <!-- branch-stack-end --> --------- Co-authored-by: Cursor Agent <cursoragent@cursor.com> Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
…5104) Supersedes langchain-ai#5010 (auto-closed when langchain-ai#4997 squash-merged; GitHub seals force-pushed closed PRs). Related: [DCD-71](https://linear.app/langchain/issue/DCD-71/hooks-v2-integrate-client-owned-lifecycle-events) dcode now runs client-owned Hooks v2 lifecycle events consistently in interactive and headless sessions, including hook-driven approval decisions before permission prompts are shown. --- - Invokes `SessionStart` and `SessionEnd` at startup, resume, clear, compact, switch, and exit boundaries. - Applies `PermissionRequest` allow, deny, ask, interrupt, notice, terminal, and stop effects before client approval resolution. - Routes supported dcode notifications through one typed service with explicit wire mappings while avoiding migrated legacy duplicates. - Preserves session-start context for the next model turn and adds TUI/headless decision-parity coverage. - Ignores generated local transcript state through `.gitignore`. This branch was re-anchored onto `main` after langchain-ai#4997 squash-merged; the diff is identical to the approved langchain-ai#5010 head (`f4e8ee672`), minus a stale resurrection of `todo_list_prompt.md` that langchain-ai#5098 deleted on main. ## Review Guide 1. `hooks/client_lifecycle.py` — `ClientHookService`, context, permission outcomes 2. `app.py` — SessionStart/End boundaries + service wiring 3. `tui/textual_adapter.py`, `client/non_interactive.py` — interactive vs headless parity 4. Skim: `transcript.py`, `server_middleware.py` (PermissionRequest), `runtime.py` / `projection.py` 5. Tests: `test_client_lifecycle.py`, then adapter / non-interactive / server-lifecycle deltas <details> <summary>Test plan</summary> - 266 hooks unit tests pass on the rebuilt tip - 412 non-interactive / Textual adapter / status widget tests pass </details> <!-- branch-stack-start --> ------------------------- - main - **feat(code): integrate Hooks v2 client lifecycle events** :point_left: - langchain-ai#5105 - langchain-ai#5045 <sup>[Stack](https://www.git-town.com/how-to/proposal-breadcrumb.html) generated by [Git Town](https://github.com/git-town/git-town)</sup> <!-- branch-stack-end --> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
Supersedes langchain-ai#5044 (auto-closed when langchain-ai#4997 squash-merged; GitHub seals force-pushed closed PRs). Project-level hooks now require an explicit workspace trust decision before their commands run. Interactive users can allow once, remember the workspace, or skip project hooks; headless runs remain opt-in through `--trust-project-hooks`. --- This closes the execution gap that left interactive project hooks permanently disabled while preserving fail-closed behavior. Trust is keyed to the canonical repository root, saved atomically, and enforced again at runtime if a snapshot is ever constructed inconsistently. `config path` and the threat model now expose the relevant project, user, and trust locations. Re-anchored onto `main` after langchain-ai#5104 squash-merged, so the PR diff contains only this branch's commits. ## Review Guide 1. `hooks/trust.py` — store, keys, atomic write, trust APIs 2. `hooks/loading.py` — project source ingest vs skip 3. `hooks/runtime.py` — fail-closed runtime guard 4. `main.py` — `--trust-project-hooks` + interactive prompt 5. Skim: `app.py`, `client/commands/config.py`, `THREAT_MODEL.md` 6. Tests: `test_trust.py`, then config/app/main touches <details> <summary>Test plan</summary> - Full hooks suite passes on the rebuilt stack tip. `test_trust.py` keeps one case per regressable behavior: canonical persistence, fail-closed corrupt-store handling, project-source provenance, the runtime trust guard, the three prompt outcomes, prompt suppression on persisted trust, and Textual wiring. </details> <!-- branch-stack-start --> ------------------------- - main - **feat(code): add project hooks workspace trust** 👈 - langchain-ai#5045 <sup>[Stack](https://www.git-town.com/how-to/proposal-breadcrumb.html) generated by [Git Town](https://github.com/git-town/git-town)</sup> <!-- branch-stack-end --> --------- Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
> [!CAUTION] > Merging this PR will automatically publish to **PyPI** and create a **GitHub release**. For the full release process, see [`.github/RELEASING.md`](https://github.com/langchain-ai/deepagents/blob/main/.github/RELEASING.md). --- _Release notes preview: keep this section in sync with the package `CHANGELOG.md`. Publish reads the merged CHANGELOG via `release.yml`, not this PR description — keep them aligned anyway so the PR stays an accurate historical record for reviewers and anyone returning later._ --- ## [0.1.50](deepagents-code==0.1.49...deepagents-code==0.1.50) (2026-07-30) ### Highlights - Added project hooks workspace trust and expanded Hooks v2 support with client and server lifecycle events plus runtime feedback ([#5105](#5105), [#5104](#5104), [#4997](#4997), [#5045](#5045)). - Added an option to mute the “YOLO is active” toast ([#5103](#5103)). - Made the splash screen `thread` ID clickable to copy it ([#5173](#5173)). - Show `ask_user` answers directly on the answered tool row ([#5100](#5100)). - Show a toast when submitting an empty required `ask_user` answer ([#5095](#5095)). - Added thread message counts to the Debug Console ([#5117](#5117)). ### Fixes and improvements - Gated Hooks v2 behind `DEEPAGENTS_CODE_EXPERIMENTAL` and improved hook resume stability across identity and Command tool results ([#5146](#5146), [#5176](#5176)). - Kept server hook state out of task results ([#5164](#5164)). - Stopped duplicate Auto transcript events during interrupt replay ([#5157](#5157)). - Kept `/update` and `/install --package` prompts responsive ([#5127](#5127)). - Refreshed the `/threads` cache after each turn ([#5174](#5174)). - Anchored toasts above the chat input and added a toast when media is dropped into a free-text question ([#5101](#5101), [#5099](#5099)). - Improved thread status message styling and links ([#5118](#5118)). - Made resume hints echo the launched command name ([#5119](#5119)). - Scoped selection copy to the clicked screen ([#5140](#5140)). - Ignored mouse hits on detached widgets ([#5114](#5114)). _End release notes preview._ --- > [!NOTE] > A **New Contributors** section is appended to the GitHub release notes automatically at publish time (see [Release Pipeline](https://github.com/langchain-ai/deepagents/blob/main/.github/RELEASING.md#release-pipeline), step 2). --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: langchain-oss-automated-triage[bot] <248757908+langchain-oss-automated-triage[bot]@users.noreply.github.com> Co-authored-by: Johannes du Plessis <johannes@langchain.dev>
Supersedes #5010 (auto-closed when #4997 squash-merged; GitHub seals force-pushed closed PRs). Related: [DCD-71](https://linear.app/langchain/issue/DCD-71/hooks-v2-integrate-client-owned-lifecycle-events) dcode now runs client-owned Hooks v2 lifecycle events consistently in interactive and headless sessions, including hook-driven approval decisions before permission prompts are shown. --- - Invokes `SessionStart` and `SessionEnd` at startup, resume, clear, compact, switch, and exit boundaries. - Applies `PermissionRequest` allow, deny, ask, interrupt, notice, terminal, and stop effects before client approval resolution. - Routes supported dcode notifications through one typed service with explicit wire mappings while avoiding migrated legacy duplicates. - Preserves session-start context for the next model turn and adds TUI/headless decision-parity coverage. - Ignores generated local transcript state through `.gitignore`. This branch was re-anchored onto `main` after #4997 squash-merged; the diff is identical to the approved #5010 head (`f4e8ee672`), minus a stale resurrection of `todo_list_prompt.md` that #5098 deleted on main. 1. `hooks/client_lifecycle.py` — `ClientHookService`, context, permission outcomes 2. `app.py` — SessionStart/End boundaries + service wiring 3. `tui/textual_adapter.py`, `client/non_interactive.py` — interactive vs headless parity 4. Skim: `transcript.py`, `server_middleware.py` (PermissionRequest), `runtime.py` / `projection.py` 5. Tests: `test_client_lifecycle.py`, then adapter / non-interactive / server-lifecycle deltas <details> <summary>Test plan</summary> - 266 hooks unit tests pass on the rebuilt tip - 412 non-interactive / Textual adapter / status widget tests pass </details> <!-- branch-stack-start --> ------------------------- - main - **feat(code): integrate Hooks v2 client lifecycle events** :point_left: - #5105 - #5045 <sup>[Stack](https://www.git-town.com/how-to/proposal-breadcrumb.html) generated by [Git Town](https://github.com/git-town/git-town)</sup> <!-- branch-stack-end --> --------- Co-authored-by: Cursor <cursoragent@cursor.com>
Supersedes #5044 (auto-closed when #4997 squash-merged; GitHub seals force-pushed closed PRs). Project-level hooks now require an explicit workspace trust decision before their commands run. Interactive users can allow once, remember the workspace, or skip project hooks; headless runs remain opt-in through `--trust-project-hooks`. --- This closes the execution gap that left interactive project hooks permanently disabled while preserving fail-closed behavior. Trust is keyed to the canonical repository root, saved atomically, and enforced again at runtime if a snapshot is ever constructed inconsistently. `config path` and the threat model now expose the relevant project, user, and trust locations. Re-anchored onto `main` after #5104 squash-merged, so the PR diff contains only this branch's commits. ## Review Guide 1. `hooks/trust.py` — store, keys, atomic write, trust APIs 2. `hooks/loading.py` — project source ingest vs skip 3. `hooks/runtime.py` — fail-closed runtime guard 4. `main.py` — `--trust-project-hooks` + interactive prompt 5. Skim: `app.py`, `client/commands/config.py`, `THREAT_MODEL.md` 6. Tests: `test_trust.py`, then config/app/main touches <details> <summary>Test plan</summary> - Full hooks suite passes on the rebuilt stack tip. `test_trust.py` keeps one case per regressable behavior: canonical persistence, fail-closed corrupt-store handling, project-source provenance, the runtime trust guard, the three prompt outcomes, prompt suppression on persisted trust, and Textual wiring. </details> <!-- branch-stack-start --> ------------------------- - main - **feat(code): add project hooks workspace trust** 👈 - #5045 <sup>[Stack](https://www.git-town.com/how-to/proposal-breadcrumb.html) generated by [Git Town](https://github.com/git-town/git-town)</sup> <!-- branch-stack-end --> --------- Co-authored-by: open-swe[bot] <open-swe@users.noreply.github.com>
> [!CAUTION] > Merging this PR will automatically publish to **PyPI** and create a **GitHub release**. For the full release process, see [`.github/RELEASING.md`](https://github.com/langchain-ai/deepagents/blob/main/.github/RELEASING.md). --- _Release notes preview: keep this section in sync with the package `CHANGELOG.md`. Publish reads the merged CHANGELOG via `release.yml`, not this PR description — keep them aligned anyway so the PR stays an accurate historical record for reviewers and anyone returning later._ --- ## [0.1.50](deepagents-code==0.1.49...deepagents-code==0.1.50) (2026-07-30) ### Highlights - Added project hooks workspace trust and expanded Hooks v2 support with client and server lifecycle events plus runtime feedback ([#5105](#5105), [#5104](#5104), [#4997](#4997), [#5045](#5045)). - Added an option to mute the “YOLO is active” toast ([#5103](#5103)). - Made the splash screen `thread` ID clickable to copy it ([#5173](#5173)). - Show `ask_user` answers directly on the answered tool row ([#5100](#5100)). - Show a toast when submitting an empty required `ask_user` answer ([#5095](#5095)). - Added thread message counts to the Debug Console ([#5117](#5117)). ### Fixes and improvements - Gated Hooks v2 behind `DEEPAGENTS_CODE_EXPERIMENTAL` and improved hook resume stability across identity and Command tool results ([#5146](#5146), [#5176](#5176)). - Kept server hook state out of task results ([#5164](#5164)). - Stopped duplicate Auto transcript events during interrupt replay ([#5157](#5157)). - Kept `/update` and `/install --package` prompts responsive ([#5127](#5127)). - Refreshed the `/threads` cache after each turn ([#5174](#5174)). - Anchored toasts above the chat input and added a toast when media is dropped into a free-text question ([#5101](#5101), [#5099](#5099)). - Improved thread status message styling and links ([#5118](#5118)). - Made resume hints echo the launched command name ([#5119](#5119)). - Scoped selection copy to the clicked screen ([#5140](#5140)). - Ignored mouse hits on detached widgets ([#5114](#5114)). _End release notes preview._ --- > [!NOTE] > A **New Contributors** section is appended to the GitHub release notes automatically at publish time (see [Release Pipeline](https://github.com/langchain-ai/deepagents/blob/main/.github/RELEASING.md#release-pipeline), step 2). --------- Co-authored-by: github-actions[bot] <41898282+github-actions[bot]@users.noreply.github.com> Co-authored-by: langchain-oss-automated-triage[bot] <248757908+langchain-oss-automated-triage[bot]@users.noreply.github.com> Co-authored-by: Johannes du Plessis <johannes@langchain.dev>
Depends on #4971
Closes DCD-69
Server-owned Hooks v2 events now pause the agent over LangGraph interrupts, run on the client
HooksRuntime, and resume with typed decisions.This starts the DCD-69 stack on top of the DCD-70 leaf (#4971).
hook_invocationinterrupt/resume transport aroundHookInvocationRequest/HookInvocationResponse.ServerHooksMiddlewareforPreToolUse,PostToolUse,Stop,SubagentStart, andSubagentStop, gated by the session's configured server events so idle sessions skip the round-trip.HooksRuntimeand fulfills server interrupts from both the Textual adapter and headless runner.deny/askas blocking tool errors for now;askapproval UX remains for DCD-71.Review Guide
hooks/server_middleware.py— middleware, gating, interrupt raise, decision applyhooks/interrupt.py—hook_invocationpause/resume contracthooks/client.py— client fulfillment + resume ledgeragent.py— middleware install (main + subagents)tui/textual_adapter.py,client/non_interactive.py— interrupt fulfillment call sitesenvelope.py,context.py,runtime.py,_cli_context.py, smallapp/auto_mode/main/snapshot/enginetouchestest_server_lifecycle.py, thentest_agent/test_non_interactive/test_engineTest plan
uv run --group test pytest tests/unit_tests/hooks— 133 passedmake lintinlibs/codeStack generated by Git Town