feat(hooks): add turn_end observer hook

Harvested the narrow Rust/docs slice of PR #2578 by @AresNing for #1364. The event uses the maintained structured observer path: JSON stdin, stdout ignored, warn-only failures, and no ability to block or mutate the turn.

The hook fires after post-turn app state, usage totals, cost, notification, receipt, and queue-recovery state are updated, before queued follow-up dispatch. Docs, RFC notes, /hooks discovery, and v0.9 tracking now describe the observer-only contract.

Co-authored-by: AresNing <49557311+AresNing@users.noreply.github.com>
This commit is contained in:
Hunter B
2026-06-04 00:09:01 -07:00
parent 586640a437
commit 0d66ef34d1
9 changed files with 313 additions and 5 deletions
+55
View File
@@ -592,6 +592,61 @@ the message. Existing environment variables remain available.
`shell_env` hooks keep their existing `KEY=VALUE` stdout contract;
the JSON stdout contract applies only to `message_submit`.
### Turn-end observer hooks
`turn_end` hooks observe the end of each model turn after post-turn
state, usage totals, cost accounting, notifications, receipts, and
queue recovery have been updated. They receive JSON on stdin and are
observer-only: stdout is ignored, failures are logged as warnings, and
the hook cannot block user input, mutate the transcript, or change the
next queued follow-up.
```toml
[[hooks.hooks]]
event = "turn_end"
command = "~/.codewhale/hooks/turn-audit.sh"
timeout_secs = 2
continue_on_error = true
```
The payload includes common hook metadata plus post-turn accounting:
```json
{
"event": "turn_end",
"session_id": "sess_12345678",
"workspace": "/path/to/workspace",
"mode": "agent",
"model": "deepseek-chat",
"turn_id": "turn_12345678",
"status": "completed",
"error": null,
"duration_ms": 1834,
"usage": {
"input_tokens": 1200,
"output_tokens": 180,
"prompt_cache_hit_tokens": 900,
"prompt_cache_miss_tokens": 300,
"reasoning_tokens": null,
"reasoning_replay_tokens": null
},
"totals": {
"session_tokens": 1380,
"conversation_tokens": 1380,
"input_tokens": 1200,
"output_tokens": 180
},
"tool_count": 2,
"queued_message_count": 1,
"stop_hook_active": false
}
```
For `interrupted` or `failed` turns, `status` reflects that terminal
state and `error` carries the engine error string when one is available.
`stop_hook_active` is reserved for future re-entry protection and is
currently always `false`.
### Sub-agent lifecycle hooks
`subagent_spawn` and `subagent_complete` hooks observe sub-agent lifecycle
+1 -1
View File
@@ -124,7 +124,7 @@ v0.9 branch so the remaining Windows/manual checks are explicit.
| #2529 workspace shell opt-in | Draft/conflicting | Review with permissions/sandbox stabilization. |
| #2530 mention depth cap hint | Draft/mergeable | Already present locally as `a97675824` and `29f57665e`; close/comment after branch is public. |
| #2576 PrefixCacheChange events | Mergeable | Already present locally through `29acb87a9d`; close/comment after branch is public or merged. |
| #2578 turn_end observer hook | Conflicting | Defer to hook lifecycle lane. |
| #2578 turn_end observer hook | Conflicting / locally harvested | Narrow Rust/docs slice landed in the hook lifecycle lane: `turn_end` now uses the existing structured observer path, fires after post-turn state updates and before queued follow-up dispatch, and includes status, usage, totals, duration, tool count, and queued-message count. Close/comment after branch is public, crediting @AresNing and #1364 reporter @esinecan. |
| #2579 AppendLog session messages | Conflicting | Defer; large architectural change. |
| #2581 provider fallback chain design doc | Mergeable / empty diff | Manually harvested into `docs/rfcs/2574-provider-fallback-chain.md`; close original PR after branch is public, keep #2574 open for implementation. |
| #2623 plan prompt modal scroll support | Mergeable | Already harvested into the 22-commit stack. Comment/close original after integration branch is public. |
+10 -1
View File
@@ -64,6 +64,13 @@ Non-goals:
- no blocking of user input
- no transcript mutation from `turn_end`
Implementation note for the v0.9 branch: the narrow #2578 harvest uses the
shared structured observer path introduced for sub-agent lifecycle hooks. It
fires before queued follow-up dispatch, after queue-recovery state is known, so
the payload can report the queued-message count without letting a hook change
what gets sent next. Stdout is ignored for `turn_end`; only `message_submit`
has a stdout mutation contract.
### PR 3: Subagent lifecycle observer hooks
Expose subagent start and completion as observer-only hook events.
@@ -251,7 +258,9 @@ transcript content in the first version.
- Existing observer-only hooks keep working.
- Existing env vars remain available.
- `shell_env` keeps its existing stdout `KEY=VALUE` contract.
- Structured stdout is interpreted only by `message_submit` in PR 1.
- Structured stdout is interpreted only by `message_submit` in PR 1. Structured
observer hooks such as `turn_end`, `subagent_spawn`, and `subagent_complete`
receive JSON on stdin, but their stdout is ignored by the caller.
## 6. Review checkpoints