@threadcrumbs/common 0.1.2 → 0.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,256 @@
1
+ export const TOOL_KINDS = Object.freeze([
2
+ 'read',
3
+ 'search',
4
+ 'edit',
5
+ 'delete',
6
+ 'move',
7
+ 'execute',
8
+ 'think',
9
+ 'fetch',
10
+ 'agent',
11
+ 'plan',
12
+ 'communicate',
13
+ 'switch_mode',
14
+ 'other',
15
+ ])
16
+
17
+ const NAMES = {
18
+ read: [
19
+ 'read', 'notebookread', 'read_file', 'read_many_files', 'view', 'view_file', 'view_file_outline', 'view_code_item',
20
+ 'view_image', 'todoread', 'cat',
21
+ ],
22
+ search: [
23
+ 'grep', 'glob', 'ls', 'list', 'list_dir', 'list_directory', 'search_file_content', 'search_files', 'grep_search',
24
+ 'find_by_name', 'codesearch', 'file_search', 'codebase_search',
25
+ ],
26
+ edit: [
27
+ 'edit', 'multiedit', 'write', 'notebookedit', 'apply_patch', 'applypatch', 'patch', 'create', 'write_file', 'replace',
28
+ 'str_replace', 'strreplace', 'write_to_file', 'replace_file_content', 'multi_replace_file_content', 'search_replace',
29
+ ],
30
+ delete: ['delete', 'delete_file'],
31
+ move: ['move', 'move_file', 'rename', 'rename_file'],
32
+ execute: [
33
+ 'bash', 'powershell', 'shell', 'exec_command', 'write_stdin', 'run_shell_command', 'run_command', 'terminal',
34
+ 'execute_code', 'process', 'bashoutput', 'killshell', 'command_status', 'send_command_input', 'local_shell',
35
+ ],
36
+ think: ['think', 'save_memory', 'memory', 'report_intent'],
37
+ fetch: [
38
+ 'webfetch', 'websearch', 'web_fetch', 'web_search', 'google_web_search', 'web_extract', 'read_url_content',
39
+ 'search_web', 'fetch',
40
+ ],
41
+ agent: ['agent', 'task', 'spawn_agent', 'wait_agent', 'send_input', 'delegate_task', 'subagent'],
42
+ plan: ['todowrite', 'todo_write', 'update_plan', 'write_todos', 'todo', 'todo_list', 'task_boundary'],
43
+ communicate: ['askuserquestion', 'ask_user', 'request_user_input', 'request_permissions', 'question', 'clarify', 'notify_user'],
44
+ switch_mode: ['enterplanmode', 'exitplanmode', 'switch_mode'],
45
+ }
46
+
47
+ const KIND_OF = new Map(Object.entries(NAMES).flatMap(([kind, names]) => names.map((name) => [name, kind])))
48
+
49
+ const PREFIXES = [
50
+ [/^browser_/, 'fetch'],
51
+ [/^web_/, 'fetch'],
52
+ ]
53
+
54
+ const normalize = (name) => String(name || '').trim().toLowerCase().replace(/[\s-]+/g, '_')
55
+
56
+ export function mcpServer(name) {
57
+ return /^mcp__(.+?)__./.exec(String(name || ''))?.[1] || null
58
+ }
59
+
60
+ export function nameKind(name) {
61
+ if (mcpServer(name)) return 'other'
62
+ const key = normalize(name)
63
+ return KIND_OF.get(key) || PREFIXES.find(([re]) => re.test(key))?.[1] || 'other'
64
+ }
65
+
66
+ const READS = /^(cat|head|tail|less|more|bat|nl|wc|type|get-content|gc)$/
67
+ const SEARCHES = /^(rg|grep|egrep|fgrep|ag|ack|findstr|select-string|sls|ls|dir|tree|fd|get-childitem|gci)$/
68
+ const DELETES = /^(rm|rmdir|del|erase|remove-item|ri)$/
69
+ const MOVES = /^(mv|move|move-item|rename-item|ren)$/
70
+ const FILTERS = /^(head|tail|sort|uniq|wc|cut|tr|grep|rg|findstr|select-string|sls|select-object|select|measure-object|measure|sort-object|less|more|cat|nl)$/
71
+ const GIT_KINDS = { show: 'read', diff: 'read', log: 'read', status: 'read', blame: 'read', grep: 'search', 'ls-files': 'search', rm: 'delete', mv: 'move' }
72
+ const QUIET_TARGET = /^(\/dev\/null|\$null|nul)$/i
73
+
74
+ function splitCommand(command) {
75
+ const steps = [{ words: [], piped: false }]
76
+ let word = ''
77
+ let quote = ''
78
+ let unsafe = false
79
+ const pushWord = () => {
80
+ if (word) steps.at(-1).words.push(word)
81
+ word = ''
82
+ }
83
+ const pushStep = (join) => {
84
+ pushWord()
85
+ steps.push({ words: [], piped: join === 'pipe' })
86
+ }
87
+ for (let i = 0; i < command.length; i++) {
88
+ const c = command[i]
89
+ const next = command[i + 1]
90
+ if (quote === "'") {
91
+ if (c === "'") quote = ''
92
+ else word += c
93
+ continue
94
+ }
95
+ if (c === '`' || (c === '$' && next === '(')) unsafe = true
96
+ if (quote === '"') {
97
+ if (c === '"') quote = ''
98
+ else if (c === '\\' && next) word += command[++i]
99
+ else word += c
100
+ continue
101
+ }
102
+ if (c === "'" || c === '"') {
103
+ quote = c
104
+ continue
105
+ }
106
+ if (c === '\\' && next && next !== '\n') {
107
+ word += command[++i]
108
+ continue
109
+ }
110
+ if ((c === '<' || c === '>') && next === '(') {
111
+ unsafe = true
112
+ continue
113
+ }
114
+ if (c === '<') {
115
+ pushWord()
116
+ continue
117
+ }
118
+ if (c === '>') {
119
+ if (/^\d*$/.test(word)) word = ''
120
+ else pushWord()
121
+ const rest = /^>?(&\d|\s*([^\s;&|<>]+))?/.exec(command.slice(i + 1))
122
+ if (!rest[1]?.startsWith('&') && !QUIET_TARGET.test(rest[2] || '')) unsafe = true
123
+ i += rest[0].length
124
+ continue
125
+ }
126
+ if (c === '&' && next === '&') {
127
+ pushStep('and')
128
+ i++
129
+ continue
130
+ }
131
+ if (c === '|' && next === '|') {
132
+ pushStep('or')
133
+ i++
134
+ continue
135
+ }
136
+ if (c === '|') {
137
+ pushStep('pipe')
138
+ continue
139
+ }
140
+ if (c === ';' || c === '\n') {
141
+ pushStep('then')
142
+ continue
143
+ }
144
+ if (c === '&') {
145
+ unsafe = true
146
+ continue
147
+ }
148
+ if (/\s/.test(c)) {
149
+ pushWord()
150
+ continue
151
+ }
152
+ word += c
153
+ }
154
+ pushWord()
155
+ if (quote) unsafe = true
156
+ return { steps: steps.filter((s) => s.words.length), unsafe }
157
+ }
158
+
159
+ const SED_ADDRESS = String.raw`(?:\d+(?:~\d+)?|\$|\/(?:[^\/\\]|\\.)*\/[IM]*)`
160
+ const SED_PRINT = new RegExp(String.raw`^(?:${SED_ADDRESS}(?:,(?:${SED_ADDRESS}|[+~]\d+))?)?\s*!?\s*p$`)
161
+ const SED_FLAGS = /^(-[nEruzs]+|--(quiet|silent|regexp-extended|separate|unbuffered|null-data|posix|debug|sandbox))$/
162
+
163
+ function sedOnlyPrints(args) {
164
+ const scripts = []
165
+ let script = null
166
+ for (let i = 0; i < args.length; i++) {
167
+ const arg = args[i]
168
+ if (arg === '-e' || arg === '--expression') scripts.push(args[++i] ?? '')
169
+ else if (arg.startsWith('--expression=')) scripts.push(arg.slice('--expression='.length))
170
+ else if (SED_FLAGS.test(arg)) continue
171
+ else if (arg.startsWith('-') && arg !== '-') return false
172
+ else if (!scripts.length && script == null) script = arg
173
+ }
174
+ if (script != null) scripts.push(script)
175
+ const parts = scripts.flatMap((s) => s.split(';')).map((s) => s.trim()).filter(Boolean)
176
+ return parts.length > 0 && parts.every((part) => SED_PRINT.test(part))
177
+ }
178
+
179
+ function positionals(args, valued) {
180
+ const out = []
181
+ for (let i = 0; i < args.length; i++) {
182
+ if (valued.test(args[i])) i++
183
+ else if (args[i] === '-' || !args[i].startsWith('-')) out.push(args[i])
184
+ }
185
+ return out
186
+ }
187
+
188
+ const WRITES_OR_RUNS = {
189
+ find: (args) => args.some((a) => /^-(exec|execdir|ok|okdir|delete|fprint|fprint0|fprintf|fls)$/.test(a)),
190
+ fd: (args) => args.some((a) => /^--exec(-batch)?(=|$)/.test(a) || /^-[^-]*[xX]/.test(a)),
191
+ rg: (args) => args.some((a) => /^--pre(=|$)/.test(a)),
192
+ tree: (args) => args.some((a) => /^-o/.test(a) || a === '-R'),
193
+ less: (args) => args.some((a) => /^-[oO]|^--(log-file|LOG-FILE)(=|$)/.test(a)),
194
+ sort: (args) => args.some((a) => /^-[^-]*o/.test(a) || /^--output(=|$)/.test(a)),
195
+ uniq: (args) => positionals(args, /^-[fsw]$/).length > 1,
196
+ }
197
+
198
+ function gitArgs(args) {
199
+ let i = 0
200
+ while (i < args.length) {
201
+ if (args[i] === '-C') i += 2
202
+ else if (args[i] === '--no-pager') i++
203
+ else break
204
+ }
205
+ return args.slice(i)
206
+ }
207
+
208
+ function stepKind(words) {
209
+ const [first, ...args] = words
210
+ const verb = first.toLowerCase().replace(/\.exe$/, '')
211
+ if (verb === 'git') {
212
+ const [sub, ...rest] = gitArgs(args)
213
+ return rest.some((a) => a.startsWith('--output')) ? null : GIT_KINDS[String(sub || '').toLowerCase()] || null
214
+ }
215
+ if (verb === 'sed') return sedOnlyPrints(args) ? 'read' : null
216
+ if (WRITES_OR_RUNS[verb]?.(args)) return null
217
+ if (verb === 'find') return 'search'
218
+ if (READS.test(verb)) return 'read'
219
+ if (SEARCHES.test(verb)) return 'search'
220
+ if (DELETES.test(verb)) return 'delete'
221
+ if (MOVES.test(verb)) return 'move'
222
+ return null
223
+ }
224
+
225
+ function isFilter(words) {
226
+ const [first, ...args] = words
227
+ const verb = first.toLowerCase()
228
+ return FILTERS.test(verb) && !WRITES_OR_RUNS[verb]?.(args)
229
+ }
230
+
231
+ export function shellKind(command) {
232
+ const source = String(command || '').trim()
233
+ if (!source) return 'execute'
234
+ const { steps, unsafe } = splitCommand(source)
235
+ if (unsafe || !steps.length) return 'execute'
236
+ const kinds = []
237
+ for (const { words, piped } of steps) {
238
+ if (piped) {
239
+ if (!isFilter(words)) return 'execute'
240
+ continue
241
+ }
242
+ if (/^(cd|pushd|popd|set-location|sl|chdir)$/i.test(words[0])) continue
243
+ const kind = stepKind(words)
244
+ if (!kind) return 'execute'
245
+ kinds.push(kind)
246
+ }
247
+ if (!kinds.length) return 'execute'
248
+ return kinds.every((k) => k === kinds[0]) ? kinds[0] : 'execute'
249
+ }
250
+
251
+ export function toolKind(name, command = null) {
252
+ const kind = nameKind(name)
253
+ const server = mcpServer(name)
254
+ if (kind !== 'execute' || command == null) return { kind, server }
255
+ return { kind: shellKind(command), server }
256
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@threadcrumbs/common",
3
- "version": "0.1.2",
3
+ "version": "0.1.4",
4
4
  "type": "module",
5
5
  "description": "Code the Threadcrumbs apps share, starting with the readers for each coding-agent harness.",
6
6
  "license": "SEE LICENSE IN LICENSE.md",
@@ -0,0 +1,246 @@
1
+ # Harness notes
2
+
3
+ What each harness saves of its reasoning and replies, what hooks it offers to other programs,
4
+ and how much of its context it keeps free for compacting. The readers use the first two; the
5
+ hooks are for apps that answer or message agents, whose code stays out of these readers.
6
+
7
+ Each section says which version was checked and when. Formats change between versions, so
8
+ recheck a fact before relying on it again.
9
+
10
+ ## Claude Code
11
+
12
+ Checked against Claude Code 2.1.287 to 2.1.317 and its docs, 2026-11-08.
13
+
14
+ - **Thoughts:** `thinking` blocks in assistant records, timed by the record's `timestamp`.
15
+ Claude 5 models usually write them with no text, only a `signature` that holds the encrypted
16
+ reasoning: the API's `thinking.display` defaults to `omitted` for them, and `updates` also
17
+ leaves the reasoning empty. Text is there only when the API returned a summary. The record's
18
+ `thinkingDisplay` field (`summarized`, `updates`, or absent) isn't documented.
19
+ `redacted_thinking` blocks hold no text either.
20
+ - **Replies:** `text` blocks in assistant records. Each content block is its own record, and the
21
+ blocks of one reply share `message.id`.
22
+ - **What a request held:** recent versions write it into the transcript. None of it is
23
+ documented. The attachments below were seen from 2.1.287 and `api-request-shape` from 2.1.301:
24
+ - `api-request-shape`: the system prompt's text and every tool definition loaded, written
25
+ again when the set changes (`shapeHash`).
26
+ - Attachments: `instructions` (memory files, with path, type, and text), `skill_listing`
27
+ (skill names and the listing's text), `deferred_tools_delta` (deferred tool names, with MCP
28
+ tools as `mcp__<server>__<tool>`), `mcp_instructions_delta` (server names and their
29
+ instructions), and `agent_listing_delta` (custom agents). After a compaction they're written
30
+ again, marked `isInitial`.
31
+ - **`/context`:** the docs don't say its output reaches the transcript, and no transcript with
32
+ it has been checked. The 2.1.319 changelog mentions a table form that Claude reads.
33
+ - **Kept free for compacting:** about 33,000 tokens by default. The docs say 1M-window models
34
+ compact at about 967K. The `autoCompactWindow` setting, `CLAUDE_CODE_AUTO_COMPACT_WINDOW`,
35
+ `--autocompact`, and `/autocompact` change it.
36
+ - **Hooks:** 33 events, each a command or `type: "http"` with `url`, `headers`, `timeout`, and
37
+ `onFailure`.
38
+ - Answering a permission request: a `PermissionRequest` hook returns
39
+ `hookSpecificOutput.decision.behavior`, `allow` or `deny`. Deny rules still win.
40
+ - A message when a turn ends: a `Stop` hook returns `decision: "block"` with a `reason`, and
41
+ the agent carries on with it.
42
+ - Hooks fire in the CLI, the IDE extensions, and the desktop app; cloud sessions don't read
43
+ local settings. They're set in user, project, or local settings, and edits are picked up
44
+ without a restart.
45
+ - **Sources:** <https://code.claude.com/docs/en/hooks>,
46
+ <https://code.claude.com/docs/en/model-config>,
47
+ <https://platform.claude.com/docs/en/build-with-claude/thinking>.
48
+
49
+ ## Codex
50
+
51
+ Checked against Codex 0.166 rollouts and openai/codex `main`, 2026-11-08. Rollouts come in two
52
+ modes, `paginated` and the older default; the records below are in both.
53
+
54
+ - **Thoughts:** `response_item` of type `reasoning`: `summary` is a list of `summary_text`
55
+ parts, timed by the record's `timestamp`. A summary can be empty. `encrypted_content` is the
56
+ full reasoning, encrypted. `content` holds raw reasoning only for models that return it.
57
+ Copies to skip: `event_msg` `agent_reasoning` (older mode) and `item_completed` with a
58
+ `Reasoning` item (paginated mode), which adds start and end times.
59
+ - **Replies:** `response_item` of type `message` with role `assistant`: `content` is a list of
60
+ `output_text` parts, and `phase` is `commentary` or `final_answer`. Copies to skip:
61
+ `agent_message`, `item_completed` with an `AgentMessage` item, and
62
+ `task_complete.last_agent_message`.
63
+ - **What a request held:** `session_meta.base_instructions` and the `developer` messages are the
64
+ system prompt; a user message starting `# AGENTS.md instructions` carries AGENTS.md;
65
+ `additional_tools` items list the MCP and app tools by namespace. Built-in tool definitions
66
+ aren't written.
67
+ - **Kept free for compacting:** Codex compacts at 90% of the model's window, and rollouts report
68
+ 95% of it as `model_context_window`, so compaction comes at about 94.7% of the reported window.
69
+ `model_auto_compact_token_limit` can only lower it. (`codex-rs/protocol/src/openai_models.rs`)
70
+ - **Hooks:** on by default, set in `~/.codex/hooks.json`, `[hooks]` in `config.toml`, a trusted
71
+ project's `.codex/`, or plugins. A hook is a command (JSON on stdin and stdout, exit 2 blocks)
72
+ or an MCP tool call, and must be trusted in `/hooks`; a changed hook must be trusted again.
73
+ - Events: SessionStart, SessionEnd, SubagentStart, SubagentStop, UserPromptSubmit,
74
+ PreToolUse, PermissionRequest, PostToolUse, PreCompact, PostCompact, Stop, Interrupt,
75
+ PreModelRequest, PostModelRequest.
76
+ - Answering a permission request: `PermissionRequest` returns
77
+ `hookSpecificOutput.decision.behavior`, `allow` or `deny`, with a `message`.
78
+ - A message when a turn ends: `Stop` returns `decision: "block"` with a `reason`, which becomes
79
+ the next prompt.
80
+ - The older `notify` setting runs a command once a turn ends, with the thread, turn, and last
81
+ reply as JSON; it can't answer.
82
+ - Whether editing `hooks.json` takes effect without a restart isn't documented.
83
+ - **Sources:** `codex-rs/protocol/src/models.rs`, `codex-rs/protocol/src/protocol.rs`,
84
+ `codex-rs/hooks/`, <https://learn.chatgpt.com/docs/hooks>.
85
+
86
+ ## Gemini CLI
87
+
88
+ Not read yet. Checked against google-gemini/gemini-cli `main`, 2026-11-08.
89
+
90
+ - **Thoughts:** chats are `~/.gemini/tmp/<project>/chats/session-<time>-<id>.jsonl`. A record
91
+ with `type: "gemini"` has `thoughts: [{ subject, description, timestamp }]`, the API's thought
92
+ summaries. They're stamped when the reply finishes, so their time is about the reply's.
93
+ - **Replies:** the same record's `content`, timed by its `timestamp`. Other lines are `$set`,
94
+ `$rewindTo`, and `$patch`.
95
+ - **Kept free for compacting:** Gemini CLI compresses at 50% of the model's limit
96
+ (`model.compressionThreshold`) and keeps the newest 30%.
97
+ - **Hooks:** set under `hooks` in settings (project, `~/.gemini`, system, or extensions), as
98
+ commands only, on by default. Events: BeforeTool, AfterTool, BeforeAgent, AfterAgent,
99
+ BeforeModel, AfterModel, BeforeToolSelection, SessionStart, SessionEnd, Notification,
100
+ PreCompress.
101
+ - Answering a permission request: no. BeforeTool can deny or ask, but `allow` doesn't skip the
102
+ prompt.
103
+ - A message when a turn ends: AfterAgent returns `decision: "deny"` with a `reason`, sent as a
104
+ new prompt.
105
+ - **Sources:** `packages/core/src/services/chatRecordingService.ts`,
106
+ `packages/core/src/services/chatCompressionService.ts`, `packages/core/src/hooks/types.ts`.
107
+
108
+ ## Copilot CLI
109
+
110
+ Not read yet. Checked against the github/copilot-sdk event types and GitHub's docs, 2026-11-08.
111
+
112
+ - **Thoughts:** `~/.copilot/session-state/<id>/events.jsonl`, one `{ type, data, id, parentId,
113
+ timestamp }` per line. `assistant.message` has `data.reasoningText` beside an encrypted copy
114
+ (`reasoningOpaque`, or `encryptedContent` for OpenAI models). The SDK also defines
115
+ `assistant.reasoning` events. Whether the text is full reasoning or a summary isn't documented.
116
+ - **Replies:** `assistant.message` `data.content`, empty on steps that only call tools.
117
+ - **Kept free for compacting:** background compaction starts at about 80% of the window and the
118
+ CLI waits for it at about 95%, so about 20% is kept free.
119
+ - **Hooks:** set in `~/.copilot/hooks/*.json`, `.github/hooks/*.json`, or `hooks` in settings,
120
+ as a command, `http` (HTTPS only; localhost needs `COPILOT_HOOK_ALLOW_LOCALHOST=1`), or a
121
+ prompt at session start. A hook that times out (30 seconds) lets things go on.
122
+ - Answering a permission request: `permissionRequest` returns `behavior`, `allow` or `deny`.
123
+ - A message when a turn ends: `agentStop` returns `decision: "block"` with a `reason`, which
124
+ becomes the next prompt, at most 8 times in a row.
125
+ - **Sources:** <https://docs.github.com/en/copilot/reference/copilot-cli-reference/cli-hooks-reference>,
126
+ `nodejs/src/types.ts` and `session-events.ts` in github/copilot-sdk.
127
+
128
+ ## Cursor
129
+
130
+ Checked against Cursor's docs and staff answers on its forum, 2026-11-08. Not checked on an
131
+ install.
132
+
133
+ - **Thoughts:** not saved apart from the reply. Agent transcripts write the thinking and the reply
134
+ into one `text` block on purpose, so they can't be told apart. The `afterAgentThought` hook
135
+ gets the thinking, in the IDE only.
136
+ - **Replies:** `{ role: "assistant", message: { content: [...] } }` with `text` and `tool_use`
137
+ blocks. Replies and tool calls carry no time and no tool results; only prompts have a
138
+ `<timestamp>` tag. Newer transcripts don't write `turn_ended`.
139
+ - **Kept free for compacting:** not documented. The `preCompact` hook is told the context's use,
140
+ size, and window when it fires.
141
+ - **Hooks:** `hooks.json` in `~/.cursor/`, a project's `.cursor/`, or enterprise settings,
142
+ reloaded when saved. Each hook is a command, with JSON on stdin and stdout, and every payload
143
+ includes `transcript_path`.
144
+ - Answering a permission request: `beforeShellExecution`, `beforeMCPExecution`, and
145
+ `preToolUse` return `permission`, `allow`, `deny`, or `ask` (`ask` isn't enforced in
146
+ `preToolUse`).
147
+ - A message when a turn ends: `stop` and `subagentStop` return `followup_message`, which Cursor
148
+ sends on its own, up to `loop_limit` (5) times.
149
+ - Others: `sessionStart`, `sessionEnd`, `postToolUse`, `afterAgentResponse`, `preCompact`.
150
+ - **Sources:** <https://cursor.com/docs/agent/hooks>, Cursor forum threads 161788 (2026-05-28)
151
+ and 166592 (2026-07-26).
152
+
153
+ ## Antigravity CLI
154
+
155
+ Checked against Antigravity's docs and open-source tools that parse its files, 2026-11-08. Not
156
+ checked on an install, and its own format isn't documented.
157
+
158
+ - **Thoughts:** `PLANNER_RESPONSE` steps carry an optional `thinking` string, timed by
159
+ `created_at`. Whether it's the full thinking or a summary isn't known.
160
+ - **Replies:** the same steps' `content`. `transcript.jsonl` cuts long fields and lists them in
161
+ `truncated_fields`; `transcript_full.jsonl` keeps them whole.
162
+ - **Kept free for compacting:** not documented. `CHECKPOINT` steps seem to mark compactions.
163
+ - **Hooks:** `.agents/hooks.json` in a project, `~/.gemini/config/hooks.json`, or a plugin's
164
+ `hooks.json`. Commands only, with camelCase JSON on stdin and stdout and a 30-second timeout.
165
+ - Answering a permission request: `PreToolUse` returns `decision`: `allow`, `deny`, `ask`, or
166
+ a stronger form such as `auto_approve`.
167
+ - A message when a turn ends: `Stop` returns `decision: "continue"` with a `reason`, added as a
168
+ system message.
169
+ - Others: `SessionStart`, `PostToolUse`, `PreInvocation`, `PostInvocation`. Payloads include
170
+ `transcriptPath`.
171
+ - **Sources:** <https://antigravity.google/docs/hooks?tab=cli>, Heeler issue 644 and
172
+ cli-sessionr pull request 17 for the step fields.
173
+
174
+ ## Hermes
175
+
176
+ Checked against NousResearch/hermes-agent at commit `0b82ddc`, 2026-11-08.
177
+
178
+ - **Thoughts:** the `messages` table's `reasoning` column, the raw reasoning text for providers
179
+ that return it, or `reasoning_content`. A reply that is only reasoning has empty `content`.
180
+ `reasoning_details` and `codex_reasoning_items` are JSON that holds the providers' encrypted
181
+ state. Timed by the row's `timestamp`, in epoch seconds.
182
+ - **Replies:** an assistant row's `content`, with `finish_reason` and `timestamp`. A reply through
183
+ the Responses API can sit in `codex_message_items` instead.
184
+ - **Kept free for compacting:** it compacts at 50% of the window, raised to 75% for windows
185
+ under 512K, so most models keep 25% free (`context_compressor.py`).
186
+ - **Hooks:** shell hooks in a `hooks:` block in `config.yaml`, in Claude Code's JSON format,
187
+ registered when the CLI or gateway starts and allowed once on first use.
188
+ - Answering a permission request: not through hooks. `pre_approval_request` and
189
+ `post_approval_response` only observe. Approvals can be answered through
190
+ `POST /v1/runs/{id}/approval` on its API server, ACP, or a messaging gateway's `/approve`.
191
+ - A message when a turn ends: `pre_verify` can return `{ "action": "continue", "message" }`,
192
+ but only on turns that edited code, at most 3 times. `POST /api/sessions/{id}/chat` runs a
193
+ new turn.
194
+ - Others: `on_session_start`, `post_llm_call`, `post_compaction`, `subagent_stop`,
195
+ `on_stream_delta`.
196
+ - **Sources:** `hermes_state_common.py`, `context_compressor.py`, and the `hooks.md`,
197
+ `session-storage.md`, and `api-server.md` docs in NousResearch/hermes-agent.
198
+
199
+ ## OpenCode
200
+
201
+ Checked against anomalyco/opencode (formerly sst/opencode) at tag `v1.19.5` for 1.x and `dev`
202
+ commit `ff4a652` for 2.x, 2026-11-08.
203
+
204
+ - **Thoughts:**
205
+ - 1.x: a `part` row with `{ type: "reasoning", text, metadata, time: { start, end } }`. The
206
+ text is written once the reasoning ends, so it's empty while the model is thinking.
207
+ `metadata` holds the provider's signature or encrypted copy.
208
+ - 2.x: a `{ type: "reasoning", text, native, time }` part, filled in when the reasoning ends.
209
+ `native` holds the provider's data for replaying it.
210
+ - **Replies:**
211
+ - 1.x: `part` rows with `{ type: "text", text, synthetic, ignored, time }`, written empty when
212
+ the text starts and filled when it ends. `synthetic` and `ignored` parts weren't written by
213
+ the model.
214
+ - 2.x: `{ type: "text", text, native }` parts with no time of their own; the assistant
215
+ message's time stands for them. Parts get their own `session_part` rows only when
216
+ `experimental.part_rows` is on, which it isn't by default; otherwise they're in the
217
+ message's `data.content`.
218
+ - **Kept free for compacting:**
219
+ - 1.x: `compaction.reserved` tokens, by default the smaller of 20,000 and the model's largest
220
+ output (`session/overflow.ts`).
221
+ - 2.x: 15% of the window, or 16,000 tokens if that's more. `compaction.buffer` changes it, as
222
+ tokens or a share such as `"20%"` (`compaction.ts`).
223
+ - **Hooks:** plugins, in JavaScript or TypeScript, and the HTTP server the TUI starts.
224
+ - 1.x plugins: `event` gets every event, `tool.execute.before` and `tool.execute.after` wrap
225
+ calls, and `permission.ask` is declared but no call to it was found.
226
+ - 2.x plugins: `ctx.permission.hook("evaluate")` can allow, ask, or deny;
227
+ `ctx.session.prompt` sends a message to steer or queue; config folders reload on their own.
228
+ - Server: `GET /event` streams events; `POST /session/:id/permissions/:permissionID` answers a
229
+ permission request (1.x), or `POST /api/session/:sessionID/permission/:requestID/reply`
230
+ (2.x); `POST /session/:id/prompt_async` sends a prompt.
231
+ - **Sources:** `packages/schema/src/v1/session.ts`, `session-message.ts`, `processor.ts`,
232
+ `compaction.ts`, `packages/plugin/src/index.ts`, <https://opencode.ai/docs/server>.
233
+
234
+ ## Kilo Code
235
+
236
+ Checked against Kilo-Org/kilocode at commit `b6a49d0` (CLI 7.8.28), 2026-11-08.
237
+
238
+ - **Thoughts and replies:** the same `part` rows as OpenCode 1.x. Kilo has no 2.x tables.
239
+ - **Kept free for compacting:** OpenCode 1.x's rule, and `compaction.threshold_percent` can set
240
+ a lower share of the window (`kilocode/session/overflow.ts`).
241
+ - **Hooks:** plugins in `~/.config/kilo/plugin/` and a project's `.kilo/plugin/`, loaded at
242
+ startup, with events such as `session.idle`, `permission.asked`, and `message.part.updated`.
243
+ Its server answers permission requests at `POST /permission/:requestID/reply` and questions at
244
+ `POST /question/:requestID/reply`.
245
+ - **Sources:** `packages/schema/src/v1/session.ts`, `kilocode/session/overflow.ts`, and the
246
+ plugins page in `kilo-docs`.