Observer: logs the order and payloads of the engine's events around a turn, to answer what the engine sends when.

An observer mod for one question: what does the engine send, in what order, when X happens? Each hook logs one EP#nn line to the debug log and passes the event on unchanged.
hooks/event-probe.mjs (types: any mod's generated .claude-plugin/types/claude-code/index.d.ts)../run.sh "<prompt>" for one headless run: /name args for a command, plain text for a message. commands/plugcmd.md is a plugin markdown command to try (/event-probe:plugcmd foo).It lives under docs/research/ so claude --plugin-dir . never loads it with the repo's mods.
hooks/event-probe.mjs 88 lines1// Observer only: logs the order and payloads of the events around a turn
2// (EP#nn lines in the debug log). Every hook passes the event on unchanged.
3// Add the event you are asking about; see README.md.
4
5let n = 0
6const seq = () => `EP#${String(++n).padStart(2, '0')}`
7const cut = (s, k = 300) => {
8 const t = typeof s === 'string' ? s : JSON.stringify(s)
9 return t === undefined ? 'undefined' : t.length > k ? t.slice(0, k) + `...(+${t.length - k})` : t
10}
11const keys = (o) => (o && typeof o === 'object' ? Object.keys(o).join(',') : String(o))
12
13export function register(on) {
14 on('session.start', async ($, e, next) => {
15 const r = await next(e)
16 try {
17 const list = await $.command.list()
18 const rows = list.map((c) => `${c.name}=${c.source}${c.plugin ? '/' + c.plugin : ''}`)
19 $.ui.log(`${seq()} session.start cmds(${list.length}) ${rows.join(' ')}`, { to: 'debug' })
20 } catch (err) {
21 $.ui.log(`${seq()} session.start list failed ${String(err)}`, { to: 'debug' })
22 }
23 return r
24 })
25
26 on('prompt.submit', async ($, e, next) => {
27 $.ui.log(`${seq()} prompt.submit BEFORE keys=[${keys(e)}] origin=${cut(e.origin)} turnId=${e.turnId} wait=${e.wait} ctx=${e.context ? e.context.length : 'none'} attach=${e.attachments ? e.attachments.length : 'none'} text=${JSON.stringify(cut(e.text))}`, { to: 'debug' })
28 const r = await next(e)
29 $.ui.log(`${seq()} prompt.submit AFTER keys=[${keys(r)}] drop=${r && r.drop} ctx=${r && r.context ? r.context.length : 'none'} text=${JSON.stringify(cut(r && r.text))}`, { to: 'debug' })
30 return r
31 })
32
33 on('command.run', async ($, e, next) => {
34 $.ui.log(`${seq()} command.run BEFORE keys=[${keys(e)}] command=${e.command} args=${JSON.stringify(cut(e.args))} origin=${cut(e.origin)} presentation=${cut(e.presentation)}`, { to: 'debug' })
35 const r = await next(e)
36 $.ui.log(`${seq()} command.run AFTER keys=[${keys(r)}] text=${JSON.stringify(cut(r && r.text, 120))} context=${r && r.context ? 'yes:' + cut(r.context, 120) : 'none'} ref=${r && r.ref}`, { to: 'debug' })
37 return r
38 })
39
40 on('skill.prompt', async ($, e, next) => {
41 $.ui.log(`${seq()} skill.prompt BEFORE skill=${e.skill} keys=[${keys(e)}] len=${e.text.length} text=${JSON.stringify(cut(e.text))}`, { to: 'debug' })
42 const r = await next(e)
43 $.ui.log(`${seq()} skill.prompt AFTER len=${r && r.text ? r.text.length : 'none'}`, { to: 'debug' })
44 return r
45 })
46
47 on('turn.start', async ($, e, next) => {
48 $.ui.log(`${seq()} turn.start BEFORE keys=[${keys(e)}] turnId=${e.turnId} text=${JSON.stringify(cut(e.text))}`, { to: 'debug' })
49 const r = await next(e)
50 $.ui.log(`${seq()} turn.start AFTER ${cut(r)}`, { to: 'debug' })
51 return r
52 })
53
54 on('turn.step', async function* ($, e, next) {
55 $.ui.log(`${seq()} turn.step turnId=${e.turnId} index=${e.index} model=${e.model} effort=${e.effort} msgs=${e.messageCount} agentId=${e.agentId}`, { to: 'debug' })
56 return yield* next(e)
57 })
58
59 on('tool.call', { tool: 'Agent' }, async ($, e, next) => {
60 $.ui.log(`${seq()} tool.call(Agent) BEFORE tool_use_id=${e.tool_use_id} background=${e.run_in_background} agentId=${e.agentId}`, { to: 'debug' })
61 const r = await next(e)
62 $.ui.log(`${seq()} tool.call(Agent) AFTER keys=[${keys(r)}] context=${r && r.context ? r.context.length : 'none'} text=${JSON.stringify(cut(r && r.text, 160))}`, { to: 'debug' })
63 return r
64 })
65
66 on('agent.spawn', async ($, e, next) => {
67 $.ui.log(`${seq()} agent.spawn BEFORE tool_use_id=${e.tool_use_id} background=${e.background} model=${e.model} type=${e.subagentType}`, { to: 'debug' })
68 const r = await next(e)
69 $.ui.log(`${seq()} agent.spawn AFTER agentId=${r && r.agentId} model=${r && r.model}`, { to: 'debug' })
70 return r
71 })
72
73 on('turn.complete', async ($, e, next) => {
74 $.ui.log(`${seq()} turn.complete keys=[${keys(e)}] ${cut(e, 200)}`, { to: 'debug' })
75 return next(e)
76 })
77
78 on('session.append', async ($, e, next) => {
79 const m = e.message || {}
80 const first = Array.isArray(m.content) && m.content[0] && typeof m.content[0].text === 'string' ? m.content[0].text : ''
81 const skip = e.door === 'attachment' && !['command_output', 'invoked_skills', 'skill_listing_x'].includes(m.name) && !String(m.name).includes('command') && !String(m.name).includes('skill')
82 if (!skip || m.name === undefined) {
83 $.ui.log(`${seq()} session.append type=${m.type} name=${m.name} door=${e.door} origin=${cut(e.origin, 120)} text=${JSON.stringify(cut(first, 200))}`, { to: 'debug' })
84 }
85 return next(e)
86 })
87}
88