Commit Graph

105 Commits

Author SHA1 Message Date
Tianyi Cui
4da2b99bc3 fix(hooks-codex): gate plain-stdout→context on a clean exit; harden HMR + absence tests
Round-2 Codex review of the round-1 fixes:

- (A) The Codex plain-stdout→additionalContext fold (F1) was not gated on exit
  code, so a NON-clean hook's stdout still injected: a SessionStart `echo stale;
  exit 2` (an emit — cannot block) wrongly injected "stale", and a
  UserPromptSubmit `exit 1` (non-blocking error → falls through to context) did
  too. Gate the fold on `output.exitCode === 0`, matching the codec's own
  structured-stdout rule. Guard tests for both paths, proven red without the gate.
- (B) The Codex "SessionStart no-context no-op" absence test was unsound (a
  completed turn doesn't prove the detached hook finished). It now touches a
  marker and waitFor()s it before asserting no context.
- (B) Both HMR tests used a no-op `true` hook, so a leaked listener would still
  pass. They now use a BLOCKING (exit 2) UserPromptSubmit hook and assert the
  post-dispose turn is NOT blocked and logs no hook/invoked — a leaked listener
  fails loudly.
2026-07-01 12:03:23 +08:00
Tianyi Cui
a72ebda723 test(hooks): poll for detached-hook effects instead of a fixed sleep (flake fix)
The bridge tests that drive observe-only emit listeners (session-start,
subagent/start, subagent/end) fire their hook on a detached `.then` the test
cannot await. They waited a fixed 50-80ms, which flaked under the full
test:coverage run's heavy parallel load (transform ~400s): the sleep expired
before the async hook completed, so the injected context / marker file / warn
call had not landed. Replace each fixed sleep with a `waitFor(predicate)` poll
that retries until the observable effect appears (5s deadline) — "async state is
not synchronous state": wait for the signal that actually fires, not a guessed
duration. No behavior change; the same assertions, made robust to scheduling.
2026-07-01 11:25:54 +08:00
Tianyi Cui
253eded47b fix(hooks): pass expectedEventName so a mismatched hookSpecificOutput block is discarded
Wire the bridges to the codec's new discriminator check (merged down from
dsh-hook-protocol): each bridge passes its firing `point` as `expectedEventName`
to runHook, so a hook whose `hookSpecificOutput.hookEventName` names a different
event has its event-scoped fields discarded. Bridge-level guard test: a
PreToolUse hook emitting a UserPromptSubmit-labeled deny no longer denies the
tool (proven red without the wiring, then reverted).
2026-07-01 10:56:34 +08:00
Tianyi Cui
8870da4313 fix(hooks): address Codex review — Stop force-continue, Codex tool_name + plain-stdout context, defer continue:false
Round-1 Codex review findings on the bridges:

- Stop force-continue (both bridges): a blocking Stop hook with EMPTY stderr
  yielded decision 'deny' + reason undefined, and the `&& reason !== undefined`
  guard let the turn STOP — the opposite of a blocking Stop hook. Force-continue
  on any deny; fall back to a generic steering line when there is no reason.
- Codex payload tool_name: hardcoded "Bash" disagreed with the exec.name matcher
  subject, so a real Codex `matcher:"Bash"` never fired against the harness's
  lowercase `bash` tool. Use exec.name in both payload builders (matches the
  matcher subject and the sibling CC bridge). Doc/RFC updated.
- Codex plain-stdout context: SessionStart/UserPromptSubmit are documented to
  treat a clean hook's PLAIN (non-JSON) stdout as additionalContext, but nothing
  folded it. runPoint now folds plain stdout into context for those two events,
  gated on the codec's JSON gate so structured stdout is never dumped as prose.
- continue:false is deferred, not honored: the seams have no hard-halt primitive
  yet. TODO(hook-continue-false) at both bridges + an RFC deferred note; the two
  tests now assert the LOG records the halt request AND that the run is NOT
  actually halted (no longer misleading).
- README concurrency wording: hooks run SERIALLY (deliberate — adjacent
  invoked/result log pairs, order-independent fold), not concurrently. Fixed the
  CC README claim + an RFC note.

Regression guards proven red on the unfixed code, then reverted. The mismatched-
hookEventName discard (also flagged) is fixed in dsh-hook-protocol and merged down.
2026-07-01 10:48:23 +08:00
Tianyi Cui
8adcbceeed feat(hooks): dsh-hooks-claude + dsh-hooks-codex bridges (hooks stack PR-F)
The two bridge plugins that run a user's existing Claude Code / Codex hook
config on the harness's typed interception seams, built on the shared
dsh-hook-protocol library. A bridge is a faithfulness adapter, not a power
tool: anything it does a native cordis plugin does more powerfully — the
bridge exists only to run UNMODIFIED external hooks.

- dsh-hooks-claude: CC dialect. Seven hook points (SessionStart,
  UserPromptSubmit, PreToolUse, PostToolUse, Stop, SubagentStart,
  SubagentStop), CC per-event stdin payloads, env + ${CLAUDE_PLUGIN_ROOT}/
  ${CLAUDE_PROJECT_DIR} substitution, literal-or-regex matcher.
- dsh-hooks-codex: Codex dialect — a deliberate subset. Five hook points,
  always-regex matcher, snake_case payloads (turn_id/model, no trailing
  newline), no env/substitution, block-only decisions.

Both map the neutral merged outcome onto the seam's typed Decision and stamp
an explicit {kind:'plugin'} source on injected context (so it is never
mislabeled as a user prompt). Config parse-failure is contained; only command
hooks run. updatedInput is logged+warned (input rewrite deferred); the Stop
loop-guard is deferred (TODO).

Tests: per-file 100% — config-parse unit branches + per-seam mappings
end-to-end through the REAL loop + REAL bash + REAL shell scripts (scripted
mock model only) + a real-Loader export-shape guard. A keyless ACP snapshot
scenario (hook-prompt-block) proves a UserPromptSubmit hook blocks a prompt
end-to-end (rejected turn -> ACP cancelled, hook/* events in the log); a
with-key e2e (hooks.e2e.ts) proves a PreToolUse hook blocks real bash
(verified on disk). The snapshot normalizer now scrubs hook/result.durationMs.

RFC: docs/rfc/implemented/feature/2026-06-30-hook-bridges.md
2026-07-01 04:23:49 +08:00