fix(hooks-claude): build subagent payloads from base(), run SubagentStop in the child cwd, drop agentType

Address the D agentType removal + two #124 review findings on the CC bridge's
subagent points:

- **Payloads from base()**: `subagentStart/StopPayload` bypassed `base()`, so the
  SubagentStart/SubagentStop stdin payloads omitted the CC-promised `session_id`
  and `cwd`. Replaced both with a single `subagentPayload()` built from `base(child)`
  (the child's session_id/cwd when the child is available) + `agent_id` +
  `agent_type` (+ `stop_hook_active` on Stop).
- **SubagentStop runs in the child cwd**: the listener called `runPoint(..., {})`
  with no agent, so the hook ran in the executor/server cwd. It now looks the
  child up via `ctx.get('agents').get(info.id)` — still recoverable because
  `subagent/end` fires from the service's detached `.then` BEFORE the tool caller
  disposes the child — and passes `{ agent: child }`, matching SubagentStart.
  New regression: server cwd ≠ child cwd, a `pwd` SubagentStop hook proves it
  ran in the CHILD workspace (proven red by neutering the lookup).
- **agent_type is a constant**: `info.agentType` no longer exists (removed on the
  subagent branch); both points now report the `SUBAGENT_TYPE = "general-purpose"`
  constant (Claude Code's Task-tool default), so a hooks.json default/`*`/empty
  `agent_type` matcher fires. Updated the README matcher-subject note and the
  bridge/coverage tests (dropped their agentType emits).
- **e2e comment**: hooks.e2e.ts said `./hooks.json` loads from the session cwd;
  corrected to process-level (server launch cwd), with the hook itself running
  in the session cwd.
This commit is contained in:
Tianyi Cui
2026-07-02 06:45:17 +08:00
parent 8572fba886
commit bae6141398
5 changed files with 89 additions and 25 deletions

View File

@@ -19,11 +19,14 @@ import {
/**
* With-key e2e: the Claude Code hook bridge running against the REAL acp-agent
* subprocess and the REAL model. The example `cordis.yml` loads `dsh-hooks-claude`
* pointed at `./hooks.json` in the session cwd; this test writes a `hooks.json`
* with a PreToolUse hook that BLOCKS every bash command, then asks the live model
* to write a file — and verifies the WORLD (the file never appears on disk),
* with a PROCESS-LEVEL `configPath` of `./hooks.json`, resolved once at load
* against the ACP server's launch cwd (NOT per-session); this test sets that
* launch cwd to the temp workspace and writes a `hooks.json` there with a
* PreToolUse hook that BLOCKS every bash command, then asks the live model to
* write a file — and verifies the WORLD (the file never appears on disk),
* proving the hook actually intercepted execution rather than the agent merely
* claiming it couldn't. Key-gated; owns and disposes its subprocess.
* claiming it couldn't. (The hook itself then runs in the session cwd.)
* Key-gated; owns and disposes its subprocess.
*
* A keyless companion lives in acp.e2e.ts (stdout purity + session/new); the
* full hook-fires-end-to-end transcript is the keyless `hook-prompt-block`