SLOPSHOPPER

event-probe

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

newguardpromptagents
★ 2v0.0.1no licenseupdated 2026-10-06alexcz-a11y/claude-mods/docs/research/event-probe
A shopper browsing a rack in a slop shop
README

event-probe

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.

  1. Add a hook for the event you are asking about in hooks/event-probe.mjs (types: any mod's generated .claude-plugin/types/claude-code/index.d.ts).
  2. ./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).
  3. Write what you measured, with the Claude Code version, into the mod's DEVELOPMENT.md (「已实测的引擎行为」).

It lives under docs/research/ so claude --plugin-dir . never loads it with the repo's mods.

Source 1 files
hooks/event-probe.mjs 88 lines
1// 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