fix(hooks): run hooks in the session cwd; honest process-level config + best-effort session-start; surface systemMessage drop

Address review on the bridges:

- Hook cwd (blocking): the bridges never passed a workdir to runHook, so hooks
  ran in the executor default (the ACP server launch dir), not the session
  cwd — a hook doing `pwd`/relative reads/marker writes operated in the wrong
  tree. Both bridges now thread the agent's session `header.cwd` (the
  session/new.cwd) as the hook workdir for agent-scoped points. Regression per
  bridge: server cwd ≠ session cwd, a `pwd` hook proves it ran in the session
  workspace (proven red without the workdir).
- Example config honesty (blocking): `configPath: ./hooks.json` is read ONCE at
  load against the PROCESS cwd, not per-session — the comment/README now say so
  explicitly (a project-local per-session hooks.json is not discovered;
  TODO(per-session-hook-config)). The hooks-run-in-session-cwd fix above is the
  distinct, separately-documented half.
- Session-start timing (blocking): agent/session-start is a synchronous emit and
  the hook runs on a detached .then, so injected context is BEST-EFFORT — not
  guaranteed before the first request. Downgrade the contract in code comments +
  README + RFC (TODO(session-start-gating)) rather than implying "first request
  sees it", and add a no-wait regression that asserts the safe properties
  without pre-waiting for the inject.
- systemMessage (non-blocking): the merge collects merged.systemMessages but no
  bridge surfaced it. Warn per hook (like updatedInput) and document it as
  deferred in both READMEs + the RFC; tests assert the warn + non-surfacing.
This commit is contained in:
Tianyi Cui
2026-07-01 16:34:28 +08:00
parent f011699e43
commit 09c8e549b0
9 changed files with 189 additions and 16 deletions

View File

@@ -86,11 +86,14 @@
- id: tool-todo
name: '@deepseek-ai/dsh-tool-todo'
# The Claude Code hook bridge, pointed at a `hooks.json` in the session cwd. A
# scenario that ships `workspace/hooks.json` (copied into the cwd before the run)
# exercises the hooks path end-to-end; every other scenario has no such file, so
# the bridge's parse fails-soft and it registers nothing (a silent no-op — the
# ACP app loads no logger exporter, so the warning never reaches stdout).
# The Claude Code hook bridge. `configPath` is read ONCE at load and resolves
# `./hooks.json` against the PROCESS cwd (not per-session) — in these snapshot
# runs the harness launches the subprocess with process cwd = the scenario's temp
# workspace, so a scenario that ships `workspace/hooks.json` (copied into that cwd
# before the run) exercises the hooks path end-to-end; every other scenario has no
# such file, so the parse fails-soft and the bridge registers nothing (a silent
# no-op — the ACP app loads no logger exporter, so the warning never reaches
# stdout). Hooks themselves run in the session cwd (the bridge passes it as workdir).
- id: hooks-claude
name: '@deepseek-ai/dsh-hooks-claude'
config:

View File

@@ -96,11 +96,16 @@
- id: tool-todo
name: '@deepseek-ai/dsh-tool-todo'
# The Claude Code hook bridge, pointed at a `hooks.json` in the session cwd. With
# no such file present the parse fails-soft and the bridge registers nothing (a
# silent no-op); a session whose cwd holds a `hooks.json` runs those hooks on the
# interception seams. stdout is the ACP JSON-RPC channel — the bridge's warnings
# go through ctx.logger (no exporter here), never to stdout.
# The Claude Code hook bridge. `configPath` is PROCESS-LEVEL: it is read ONCE at
# load and the relative `./hooks.json` resolves against the ACP server's launch
# cwd, NOT each `session/new.cwd`. So a single `hooks.json` next to where the
# server starts applies to every session; a project-local, per-session hooks.json
# is NOT discovered (per-session config resolution is a TODO — see the bridge
# README). With no file present the parse fails-soft and the bridge registers
# nothing (a silent no-op). Hooks THEMSELVES run in the session cwd (the bridge
# passes it as the workdir); only WHERE the config is read from is process-level.
# stdout is the ACP JSON-RPC channel — the bridge's warnings go through ctx.logger
# (no exporter here), never to stdout.
- id: hooks-claude
name: '@deepseek-ai/dsh-hooks-claude'
config: