command-code 1.0.0 → 1.1.0

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.
@@ -2,7 +2,7 @@
2
2
 
3
3
  # Agent Skills
4
4
 
5
- Command Code supports Agent Skills a lightweight, open standard for extending AI agents with specialized knowledge and workflows.
5
+ Command Code supports Agent Skills - a lightweight, open standard for extending AI agents with specialized knowledge and workflows.
6
6
 
7
7
  ---
8
8
 
@@ -154,9 +154,9 @@ Command Code also discovers skills from `.agents/skills/` (project) and `~/.agen
154
154
 
155
155
  ### Extra locations
156
156
 
157
- Beyond the standard directories, you can point Command Code at any skills folder a directory of skills, or a single skill directory containing `SKILL.md`.
157
+ Beyond the standard directories, you can point Command Code at any skills folder - a directory of skills, or a single skill directory containing `SKILL.md`.
158
158
 
159
- **In settings** add a `skills` array to `settings.json` (user-global `~/.commandcode/settings.json`, project `.commandcode/settings.json`, or `.commandcode/settings.local.json`):
159
+ **In settings** - add a `skills` array to `settings.json` (user-global `~/.commandcode/settings.json`, project `.commandcode/settings.json`, or `.commandcode/settings.local.json`):
160
160
 
161
161
  ```json theme={null}
162
162
  {
@@ -168,9 +168,9 @@ Beyond the standard directories, you can point Command Code at any skills folder
168
168
  }
169
169
  ```
170
170
 
171
- `~/` expands to your home directory; relative paths resolve against the project root (the git root, or the working directory outside a repo). Settings layers overwrite the array whole the highest layer that defines `skills` wins. This is also the way to load a skills folder kept outside the standard locations like `~/.commandcode/skills` a shared team drive, or a folder maintained for another Agent-Skills tool.
171
+ `~/` expands to your home directory; relative paths resolve against the project root (the git root, or the working directory outside a repo). Settings layers overwrite the array whole - the highest layer that defines `skills` wins. This is also the way to load a skills folder kept outside the standard locations like `~/.commandcode/skills` - a shared team drive, or a folder maintained for another Agent-Skills tool.
172
172
 
173
- **At launch** the `--skill` flag adds a location for one session and is repeatable; `--no-skills` skips discovery entirely (paths given via `--skill` still load):
173
+ **At launch** - the `--skill` flag adds a location for one session and is repeatable; `--no-skills` skips discovery entirely (paths given via `--skill` still load):
174
174
 
175
175
  ```bash theme={null}
176
176
  cmd --skill ./my-skill --skill ~/team/shared-skills
@@ -208,12 +208,12 @@ Skills, custom commands, and built-ins all share the same `/` menu. Skill locati
208
208
 
209
209
  Put simply: project-level always trumps user-level, and within the same level `.commandcode/` is favored over `.agents/`.
210
210
 
211
- When two locations provide the same skill name, the higher-precedence copy wins and every shadowed copy is reported as a **Duplicate names** warning in the `/skills` issues view and `cmd skills list --debug` a shadowed skill is never dropped silently.
211
+ When two locations provide the same skill name, the higher-precedence copy wins and every shadowed copy is reported as a **Duplicate names** warning in the `/skills` issues view and `cmd skills list --debug` - a shadowed skill is never dropped silently.
212
212
 
213
213
  **What happens on a collision.** When a skill name matches a built-in or a custom command:
214
214
 
215
- - Typing `/<name>` always resolves to the higher-precedence owner the skill is not invoked.
216
- - The skill still appears in the `/` menu with a `[skill]` badge and a ` shadowed by /<owner>` note, so you can see it exists and understand why `/<name>` runs the built-in command instead. Command Code prefers visibility with a marker over silently dropping the row.
215
+ - Typing `/<name>` always resolves to the higher-precedence owner - the skill is not invoked.
216
+ - The skill still appears in the `/` menu with a `[skill]` badge and a ` - shadowed by /<owner>` note, so you can see it exists and understand why `/<name>` runs the built-in command instead. Command Code prefers visibility with a marker over silently dropping the row.
217
217
 
218
218
  ---
219
219
 
@@ -233,12 +233,12 @@ This opens an interactive menu showing:
233
233
  - All project-level skills with `(project)` label
234
234
  - Skills from `.agents/skills/` (user or project) show a dim `[.agents]` badge
235
235
  - Use **arrow keys** to navigate
236
- - Press **Enter** to open any skill in your editor (or toggle it see [Enable or disable skills](#enable-or-disable-skills))
236
+ - Press **Enter** to open any skill in your editor (or toggle it - see [Enable or disable skills](#enable-or-disable-skills))
237
237
  - Press **Esc** to close and return to your session
238
238
 
239
239
  Every installed skill is also surfaced as a first-class slash command in the `/` menu. If you install a skill named `pr-desc`, you will see `/pr-desc`, sorted after built-ins and any custom commands in the slash menu.
240
240
 
241
- If a skill name collides with a built-in or a custom command, the skill still appears in the `/` menu with a `[skill]` badge and a ` shadowed by /<owner>` note. Typing `/<name>` always resolves to the higher-precedence owner, not the skill. See [Selection priority](#selection-priority) above for the full order.
241
+ If a skill name collides with a built-in or a custom command, the skill still appears in the `/` menu with a `[skill]` badge and a ` - shadowed by /<owner>` note. Typing `/<name>` always resolves to the higher-precedence owner, not the skill. See [Selection priority](#selection-priority) above for the full order.
242
242
 
243
243
  ---
244
244
 
@@ -266,13 +266,13 @@ Please follow /code-conv when writing the new module and /pr when opening the re
266
266
 
267
267
  ### 3. Inferred suggestion
268
268
 
269
- Command Code scans your prompt and suggests a matching skill when one clearly fits. Ordinary prompts keep working exactly like before inferred suggestions just help you discover what is available.
269
+ Command Code scans your prompt and suggests a matching skill when one clearly fits. Ordinary prompts keep working exactly like before - inferred suggestions just help you discover what is available.
270
270
 
271
271
  ---
272
272
 
273
273
  ## Create skills
274
274
 
275
- The fastest way to create a skill is to ask Command Code to build it for you. The bundled `skill-builder` skill walks the agent through scoping the job, picking a valid name and location, writing spec-compliant frontmatter, and verifying the result loads just describe the workflow you want captured, or invoke `/skill-builder` directly.
275
+ The fastest way to create a skill is to ask Command Code to build it for you. The bundled `skill-builder` skill walks the agent through scoping the job, picking a valid name and location, writing spec-compliant frontmatter, and verifying the result loads - just describe the workflow you want captured, or invoke `/skill-builder` directly.
276
276
 
277
277
  ### User-level skills
278
278
 
@@ -417,7 +417,7 @@ All endpoints require Bearer token in Authorization header except:
417
417
  EOF
418
418
  ```
419
419
 
420
- You can also install community skills directly from GitHub without manually creating files see [`cmd skills add`](#cmd-skills-add) in the command reference.
420
+ You can also install community skills directly from GitHub without manually creating files - see [`cmd skills add`](#cmd-skills-add) in the command reference.
421
421
 
422
422
  ---
423
423
 
@@ -500,7 +500,7 @@ Each skill must be in its own directory:
500
500
  └── SKILL.md
501
501
  ```
502
502
 
503
- Skills can also be grouped under intermediate folders see [Nested skill folders](#nested-skill-folders).
503
+ Skills can also be grouped under intermediate folders - see [Nested skill folders](#nested-skill-folders).
504
504
 
505
505
  ### Adding supporting files
506
506
 
@@ -614,7 +614,7 @@ Command Code additionally honors these extension fields. Several mirror the Agen
614
614
  | -------------------------- | -------- | --------------------------------------------------------------------------------------------------------------------------------------- |
615
615
  | `argument-hint` | No | Shown next to the skill in the `/` menu so you know what arguments it expects (e.g. `argument-hint: "<branch> [--draft]"`). |
616
616
  | `when_to_use` | No | Extra trigger context (trigger phrases, example requests) appended to the description in the model-facing catalog, so auto-invocation matches on it too. |
617
- | `disable-model-invocation` | No | When `true`, the skill is hidden from the model entirely it never appears in the skill catalog and can't be auto-loaded. Only an explicit `/skill-name` invocation runs it. Use for destructive or highly contextual workflows. |
617
+ | `disable-model-invocation` | No | When `true`, the skill is hidden from the model entirely - it never appears in the skill catalog and can't be auto-loaded. Only an explicit `/skill-name` invocation runs it. Use for destructive or highly contextual workflows. |
618
618
  | `user-invocable` | No | When `false`, the skill stays model-invocable but is hidden from the `/` menu. Use for background knowledge that isn't a command you'd run by hand. |
619
619
  | `disallowed-tools` | No | Space-delimited list (or YAML array) of tools the skill should not use. Parsed and surfaced alongside `allowed-tools`. |
620
620
  | `arguments` | No | Ordered names for positional arguments (`arguments: issue branch`), enabling `$issue` / `$branch` placeholders in the body. |
@@ -643,12 +643,12 @@ When you invoke a skill with `/skill-name <args>`, Command Code substitutes thes
643
643
  | `$ARGUMENTS` | All arguments as a single string. If the body has no placeholder, non-empty arguments are appended as an `ARGUMENTS: …` footer. |
644
644
  | `$ARGUMENTS[N]` / `${N}` | The Nth argument, 0-indexed (`$ARGUMENTS[0]` / `${0}` is the first). |
645
645
  | `$name` | A [declared named argument](#skills-specification). With `arguments: issue branch`, `$issue` is the first argument and `$branch` the second. Only declared names are replaced. |
646
- | `${COMMANDCODE_SKILL_DIR}` | Absolute path to the skill's own directory use it to reference bundled `scripts/` or `references/` regardless of the working directory. |
646
+ | `${COMMANDCODE_SKILL_DIR}` | Absolute path to the skill's own directory - use it to reference bundled `scripts/` or `references/` regardless of the working directory. |
647
647
  | `${COMMANDCODE_PROJECT_DIR}` | The project root (git root, otherwise the current directory). |
648
648
  | `${COMMANDCODE_SESSION_ID}` | The current session id, when available. |
649
649
  | `${COMMANDCODE_EFFORT}` | The active reasoning-effort level, when available. |
650
650
 
651
- The `${CLAUDE_SKILL_DIR}`, `${CLAUDE_PROJECT_DIR}`, `${CLAUDE_SESSION_ID}`, and `${CLAUDE_EFFORT}` aliases resolve to the same values, so a skill written for Claude Code works unchanged. Only these known tokens are substituted any other `${...}` (for example a literal `${HOME}`) is left untouched.
651
+ The `${CLAUDE_SKILL_DIR}`, `${CLAUDE_PROJECT_DIR}`, `${CLAUDE_SESSION_ID}`, and `${CLAUDE_EFFORT}` aliases resolve to the same values, so a skill written for Claude Code works unchanged. Only these known tokens are substituted - any other `${...}` (for example a literal `${HOME}`) is left untouched.
652
652
 
653
653
  All placeholders substitute in a single pass and arguments are inserted literally: argument text that itself looks like a placeholder (`$ARGUMENTS`, `$$`, a declared `$name`) lands as-is and is never expanded again.
654
654
 
@@ -656,8 +656,8 @@ All placeholders substitute in a single pass and arguments are inserted literall
656
656
 
657
657
  A skill body can run shell commands and inline their output before the body reaches the model, so the model reads live data instead of a command. Two forms are recognized:
658
658
 
659
- * **Inline** `` !`git status --short` `` runs a single command. The `!` is only honored at the start of a line or directly after whitespace, so `KEY=!`cmd`` stays literal.
660
- * **Fenced** a ` ```! ` code block runs a multi-line script:
659
+ * **Inline** - `` !`git status --short` `` runs a single command. The `!` is only honored at the start of a line or directly after whitespace, so `KEY=!`cmd`` stays literal.
660
+ * **Fenced** - a ` ```! ` code block runs a multi-line script:
661
661
 
662
662
  ````md
663
663
  ```!
@@ -666,9 +666,9 @@ git rev-parse --short HEAD
666
666
  ```
667
667
  ````
668
668
 
669
- Each placeholder is replaced with the command's output, once, left to right output is never re-scanned for more placeholders. Commands run from the project directory, with the `COMMANDCODE_SKILL_DIR` / `COMMANDCODE_PROJECT_DIR` (and `CLAUDE_*` alias) values exported to their environment.
669
+ Each placeholder is replaced with the command's output, once, left to right - output is never re-scanned for more placeholders. Commands run from the project directory, with the `COMMANDCODE_SKILL_DIR` / `COMMANDCODE_PROJECT_DIR` (and `CLAUDE_*` alias) values exported to their environment.
670
670
 
671
- To turn this off so opening a skill never shells out set `disableSkillShellExecution: true` in `settings.json`. Every placeholder is then replaced with `[shell command execution disabled by policy]` instead of running.
671
+ To turn this off - so opening a skill never shells out - set `disableSkillShellExecution: true` in `settings.json`. Every placeholder is then replaced with `[shell command execution disabled by policy]` instead of running.
672
672
 
673
673
  ### Optional directories
674
674
 
@@ -788,9 +788,9 @@ cmd skills add https://github.com/remotion-dev/skills --skill remotion-best-prac
788
788
 
789
789
  **Multi-skill repos.** When a repo contains multiple skills and `--skill` is not specified, Command Code shows an interactive prompt to select which skills to install.
790
790
 
791
- **Name collisions.** Skill names share the `/` menu with built-in commands (`/clear`, `/help`, `/share`, `/rewind`, …) and with the custom commands in `.commandcode/commands/`. Built-ins and custom commands take precedence typing `/<name>` resolves to the owner, not to the skill.
791
+ **Name collisions.** Skill names share the `/` menu with built-in commands (`/clear`, `/help`, `/share`, `/rewind`, …) and with the custom commands in `.commandcode/commands/`. Built-ins and custom commands take precedence - typing `/<name>` resolves to the owner, not to the skill.
792
792
 
793
- If you install a skill whose name collides with one of those, `cmd skills add` still completes normally nothing blocks the install. The skill then appears in the `/` menu with a `[skill]` badge and a `shadowed by /<owner>` note so you can see it exists, while `/<name>` continues to route to the higher-precedence owner. See [Selection priority](#selection-priority) for the full order.
793
+ If you install a skill whose name collides with one of those, `cmd skills add` still completes normally - nothing blocks the install. The skill then appears in the `/` menu with a `[skill]` badge and a `shadowed by /<owner>` note so you can see it exists, while `/<name>` continues to route to the higher-precedence owner. See [Selection priority](#selection-priority) for the full order.
794
794
 
795
795
  ### cmd skills list
796
796
 
@@ -885,7 +885,7 @@ cmd skills remove frontend-design --global
885
885
  cmd skills remove my-skill --yes
886
886
  ```
887
887
 
888
- **Skill name rules.** Skill names must use lowercase letters, numbers, and hyphens only no path separators, dots, or uppercase.
888
+ **Skill name rules.** Skill names must use lowercase letters, numbers, and hyphens only - no path separators, dots, or uppercase.
889
889
 
890
890
  ---
891
891
 
@@ -925,13 +925,13 @@ Each skill should do one thing well:
925
925
 
926
926
  ### Pick a name that does not collide
927
927
 
928
- Skill names share the `/` menu with built-in commands like `/clear`, `/help`, `/share`, and `/rewind`, and with any custom commands you define in `.commandcode/commands/`. Those take precedence typing `/<name>` routes to the owner and the skill renders with a `shadowed by` note next to its `[skill]` badge.
928
+ Skill names share the `/` menu with built-in commands like `/clear`, `/help`, `/share`, and `/rewind`, and with any custom commands you define in `.commandcode/commands/`. Those take precedence - typing `/<name>` routes to the owner and the skill renders with a `shadowed by` note next to its `[skill]` badge.
929
929
 
930
930
  Prefer task-oriented names that are unlikely to overlap:
931
931
 
932
932
  **Safe:** `pr-desc`, `api-guidelines`, `release-notes`, `code-review`
933
933
 
934
- **Risky:** `clear`, `help`, `share`, `rewind` these are built-in.
934
+ **Risky:** `clear`, `help`, `share`, `rewind` - these are built-in.
935
935
 
936
936
  ### Use progressive disclosure
937
937
 
@@ -977,7 +977,7 @@ Command Code fully implements the [Agent Skills open standard](https://agentskil
977
977
  * **Shareable**: Publish for your team or community
978
978
  * **Auditable**: Plain text files anyone can read
979
979
 
980
- Validation follows the standard strictly including the rule that a skill's `name` must match its directory name. Skills that break a rule are skipped with a categorized warning (visible in `/skills` and `cmd skills list --debug`) rather than failing the session, and a missing or empty `description` always prevents loading. Command Code's extension fields (`argument-hint`, `disable-model-invocation`, `model`) live alongside the standard fields and are ignored by other implementations; unknown frontmatter fields from other tools are ignored in turn.
980
+ Validation follows the standard strictly - including the rule that a skill's `name` must match its directory name. Skills that break a rule are skipped with a categorized warning (visible in `/skills` and `cmd skills list --debug`) rather than failing the session, and a missing or empty `description` always prevents loading. Command Code's extension fields (`argument-hint`, `disable-model-invocation`, `model`) live alongside the standard fields and are ignored by other implementations; unknown frontmatter fields from other tools are ignored in turn.
981
981
 
982
982
  ---
983
983
 
@@ -2,7 +2,7 @@
2
2
 
3
3
  # ModApi reference
4
4
 
5
- `export default function (cmd: ModApi)` the factory. `ModFactory = (cmd: ModApi) => void | Promise<void>`. Every `add*` / `on` / `hooks` call returns `Disposable { dispose(): void }` `.dispose()` undoes exactly that one registration (idempotent; a second call is a no-op). Disposables make a registration retractable and are the seam future hot-reload will recycle.
5
+ `export default function (cmd: ModApi)` - the factory. `ModFactory = (cmd: ModApi) => void | Promise<void>`. Every `add*` / `on` / `hooks` call returns `Disposable { dispose(): void }` - `.dispose()` undoes exactly that one registration (idempotent; a second call is a no-op). Disposables make a registration retractable and are the seam future hot-reload will recycle.
6
6
 
7
7
  ## Fields
8
8
 
@@ -29,13 +29,13 @@
29
29
 
30
30
  What each does:
31
31
 
32
- - `cmd.hooks(hooks)` the mutating lifecycle hooks: the `AgentMod` surface (transformContext, beforeToolCall, afterToolCall, onTurnStart/End, appendSystemPrompt, shouldStopAfterTurn, prepareNextTurn, onRunEnd, **onStop**) **plus `transformInput`, `onSessionStart`, and `onSessionEnd`**. Multiple calls compose in order with mod-runner semantics (threading, chaining, any-true, later-wins). See [the hook contracts](./hooks-and-events.md#the-hook-contracts) for the full contract of each hook.
33
- - `cmd.addTool(toolModule)` a `ToolModule` the model can call; same collision policy as `AgentMod.tools` (existing names win, collision emits `mod_error`). The tool's `run` receives `{input, runtime, signal}` and returns `{ok: true, content: [{type: 'text', text}]}` or `{ok: false, error}`. Mark `readOnly: true` when the tool never mutates (stays available in plan mode).
34
- - `cmd.addCommand({name, description, handler})` a `/name` slash command. The handler returns data, not callbacks: `{prompt}` submits an automated turn, `{message}` renders an info row, nothing = pure side effect. It gets `{args, ui, cwd, exec}`. First registration of a name wins across mods.
35
- - `cmd.addFlag(name, {type, default})` / `cmd.getFlag(name)` named options; values come from repeatable `--mod-option name=value` CLI flags (defaults apply otherwise).
36
- - `cmd.on(event, handler)` observe any `AgentEvent` plus the host lifecycle events `session_start` / `session_shutdown` (fired when a harness binds the host and when a session switch or dispose replaces it). Handlers are isolated: a throw becomes a `mod_error {hook: 'on:<event>'}` event, never a crash.
37
- - `cmd.addProvider(providerModule)` register a model provider through the same `ProviderModule` seam the built-ins use (id, transport, auth hooks, model list). The host appends mod providers to its set mods extend the provider set, never replace it.
38
- - `cmd.addRenderer(customType, data => lines)` a renderer for a custom entry type; returns the lines to print (style them with ansi escapes picocolors, `@commandcode/tui` helpers, or raw codes). First registration per type wins across mods.
32
+ - `cmd.hooks(hooks)` - the mutating lifecycle hooks: the `AgentMod` surface (transformContext, beforeToolCall, afterToolCall, onTurnStart/End, appendSystemPrompt, shouldStopAfterTurn, prepareNextTurn, onRunEnd, **onStop**) **plus `transformInput`, `onSessionStart`, and `onSessionEnd`**. Multiple calls compose in order with mod-runner semantics (threading, chaining, any-true, later-wins). See [the hook contracts](./hooks-and-events.md#the-hook-contracts) for the full contract of each hook.
33
+ - `cmd.addTool(toolModule)` - a `ToolModule` the model can call; same collision policy as `AgentMod.tools` (existing names win, collision emits `mod_error`). The tool's `run` receives `{input, runtime, signal}` and returns `{ok: true, content: [{type: 'text', text}]}` or `{ok: false, error}`. Mark `readOnly: true` when the tool never mutates (stays available in plan mode).
34
+ - `cmd.addCommand({name, description, handler})` - a `/name` slash command. The handler returns data, not callbacks: `{prompt}` submits an automated turn, `{message}` renders an info row, nothing = pure side effect. It gets `{args, ui, cwd, exec}`. First registration of a name wins across mods.
35
+ - `cmd.addFlag(name, {type, default})` / `cmd.getFlag(name)` - named options; values come from repeatable `--mod-option name=value` CLI flags (defaults apply otherwise).
36
+ - `cmd.on(event, handler)` - observe any `AgentEvent` plus the host lifecycle events `session_start` / `session_shutdown` (fired when a harness binds the host and when a session switch or dispose replaces it). Handlers are isolated: a throw becomes a `mod_error {hook: 'on:<event>'}` event, never a crash.
37
+ - `cmd.addProvider(providerModule)` - register a model provider through the same `ProviderModule` seam the built-ins use (id, transport, auth hooks, model list). The host appends mod providers to its set - mods extend the provider set, never replace it.
38
+ - `cmd.addRenderer(customType, data => lines)` - a renderer for a custom entry type; returns the lines to print (style them with ansi escapes - picocolors, `@commandcode/tui` helpers, or raw codes). First registration per type wins across mods.
39
39
 
40
40
  ## Live methods (any time after the harness binds)
41
41
 
@@ -51,16 +51,16 @@ What each does:
51
51
 
52
52
  Details:
53
53
 
54
- - `cmd.ui` `notify` (a `notice` feed row), `confirm` / `select` / `input` (the Interaction question modal in the TUI; deterministic defaults headless: false/undefined), `setStatus`, `widget`, `refreshWidgets`. The full dialog/status/widget contract is in [UI surface](./ui.md#ui-surface).
55
- - `cmd.queueMessage({content, deliverAs})` `steer` lands after the current tool batch (behind any queued user messages in the same poll), `follow-up` only when the run would stop (ahead of continuation nudges, behind queued user input).
56
- - `cmd.session` the `ModSessionApi` persistence seam (appendCustomEntry / appendCustomMessageEntry / getCustomEntries), undefined until bound. See [ModContext.session the mods' persistence surface](./hooks-and-events.md#mod-context-session-the-mods-persistence-surface).
57
- - `cmd.showEntry(customType, data)` render a custom entry into the live feed through the renderer registered for that type (unrendered types pretty-print as JSON). The TUI wires the sink; headless runs drop entries. Pair with `cmd.session.appendCustomEntry` when the data should also persist.
58
- - `cmd.sessions` live session controls: `compact()`, `tree()` (the session branch tree as stable `{id, label, children}` nodes), `leafId()`, `navigateTree({targetId, summarize?, customInstructions?})`, and `setLabel({targetId, label})`. These read live harness state, so unlike the buffered setters they throw with a clear message when no session is bound yet.
59
- - `cmd.exec({command, args})` run a process through the harness Runtime (args are shell-quoted).
60
- - `cmd.getAllTools()` / `cmd.getActiveTools()` / `cmd.setActiveTools(names)` a mod-managed tool filter: disabled tools vanish from the model's schemas and refuse execution.
61
- - `cmd.events` a tiny cross-mod pub/sub bus (`emit` / `on`).
54
+ - `cmd.ui` - `notify` (a `notice` feed row), `confirm` / `select` / `input` (the Interaction question modal in the TUI; deterministic defaults headless: false/undefined), `setStatus`, `widget`, `refreshWidgets`. The full dialog/status/widget contract is in [UI surface](./ui.md#ui-surface).
55
+ - `cmd.queueMessage({content, deliverAs})` - `steer` lands after the current tool batch (behind any queued user messages in the same poll), `follow-up` only when the run would stop (ahead of continuation nudges, behind queued user input).
56
+ - `cmd.session` - the `ModSessionApi` persistence seam (appendCustomEntry / appendCustomMessageEntry / getCustomEntries), undefined until bound. See [ModContext.session - the mods' persistence surface](./hooks-and-events.md#mod-context-session-the-mods-persistence-surface).
57
+ - `cmd.showEntry(customType, data)` - render a custom entry into the live feed through the renderer registered for that type (unrendered types pretty-print as JSON). The TUI wires the sink; headless runs drop entries. Pair with `cmd.session.appendCustomEntry` when the data should also persist.
58
+ - `cmd.sessions` - live session controls: `compact()`, `tree()` (the session branch tree as stable `{id, label, children}` nodes), `leafId()`, `navigateTree({targetId, summarize?, customInstructions?})`, and `setLabel({targetId, label})`. These read live harness state, so unlike the buffered setters they throw with a clear message when no session is bound yet.
59
+ - `cmd.exec({command, args})` - run a process through the harness Runtime (args are shell-quoted).
60
+ - `cmd.getAllTools()` / `cmd.getActiveTools()` / `cmd.setActiveTools(names)` - a mod-managed tool filter: disabled tools vanish from the model's schemas and refuse execution.
61
+ - `cmd.events` - a tiny cross-mod pub/sub bus (`emit` / `on`).
62
62
 
63
- ## `ModHooks` (the mutating lifecycle all optional, all composable)
63
+ ## `ModHooks` (the mutating lifecycle - all optional, all composable)
64
64
 
65
65
  `transformContext`, `appendSystemPrompt`, `beforeToolCall`, `afterToolCall`, `onTurnStart`, `onTurnEnd`, `shouldStopAfterTurn`, `prepareNextTurn`, `onRunEnd`, `onStop`, plus the host-level `transformInput`, `onSessionStart`, and `onSessionEnd`.
66
66
 
@@ -72,10 +72,10 @@ Details:
72
72
 
73
73
  The per-hook contracts, ordering guarantees, and error policy are in [the hook contracts](./hooks-and-events.md#the-hook-contracts).
74
74
 
75
- - `transformInput({text})` intercepts typed user prompts before they reach the model (the mods' **UserPromptSubmit** hook): return `{action: 'transform', text}` to rewrite (handlers chain the next sees the rewrite), `{action: 'handled', message?}` to consume the prompt entirely (an optional info row renders in its place), or `undefined`/`{action: 'continue'}` to pass through. Only real typed input is intercepted automated turns, meta messages, image-carrying prompts, and slash commands never route through it; a throwing handler is skipped (`mod_error {hook: 'transformInput'}`), never a swallowed prompt. It lives in `hooks` because it MUTATES what the agent sees.
76
- - `onStop({state, stopReason, turnNumber, lastAssistantText})` is the **Stop** hook return `{continue: true, reason?}` to force a run that would otherwise finish to keep going (the reason rides an automated turn). Fires only on natural completion, any-mod wins, and the loop caps consecutive continuations.
77
- - `onSessionStart({source})` / `onSessionEnd({reason})` are the once-per-session **SessionStart** / **SessionEnd** hooks fired when the host binds to / tears down a session (`source: 'startup' | 'resume'`, `reason: 'shutdown' | 'replaced'`). Act through the mod's captured `cmd`; they run fire-and-forget (a throw becomes `mod_error`), so a session hook never blocks bind/dispose. Pure observation is also available via `cmd.on('session_start' | 'session_shutdown')` these named hooks add the typed metadata.
75
+ - `transformInput({text})` intercepts typed user prompts before they reach the model (the mods' **UserPromptSubmit** hook): return `{action: 'transform', text}` to rewrite (handlers chain - the next sees the rewrite), `{action: 'handled', message?}` to consume the prompt entirely (an optional info row renders in its place), or `undefined`/`{action: 'continue'}` to pass through. Only real typed input is intercepted - automated turns, meta messages, image-carrying prompts, and slash commands never route through it; a throwing handler is skipped (`mod_error {hook: 'transformInput'}`), never a swallowed prompt. It lives in `hooks` because it MUTATES what the agent sees.
76
+ - `onStop({state, stopReason, turnNumber, lastAssistantText})` is the **Stop** hook - return `{continue: true, reason?}` to force a run that would otherwise finish to keep going (the reason rides an automated turn). Fires only on natural completion, any-mod wins, and the loop caps consecutive continuations.
77
+ - `onSessionStart({source})` / `onSessionEnd({reason})` are the once-per-session **SessionStart** / **SessionEnd** hooks - fired when the host binds to / tears down a session (`source: 'startup' | 'resume'`, `reason: 'shutdown' | 'replaced'`). Act through the mod's captured `cmd`; they run fire-and-forget (a throw becomes `mod_error`), so a session hook never blocks bind/dispose. Pure observation is also available via `cmd.on('session_start' | 'session_shutdown')` - these named hooks add the typed metadata.
78
78
 
79
79
  ## `on` event types
80
80
 
81
- Any `AgentEvent['type']` including `run_start`, `run_end`, `turn_start`, `turn_end`, `model_request_start`, `model_request_end`, `tool_running`, `tool_completed`, `tool_errored`, `subagent_start`, `subagent_stop`, `compaction_start`, `compaction_done`, `notice`, `session_titled`, `mod_error` plus the two host lifecycle events `session_start` / `session_shutdown`. (`subagent_start`/`subagent_stop` are SubagentStart/Stop; `compaction_start`/`compaction_done` are Pre/PostCompact; `notice` is Notification.) The full payload catalog is in [the AgentEvent catalog](./hooks-and-events.md#agent-event-catalog).
81
+ Any `AgentEvent['type']` - including `run_start`, `run_end`, `turn_start`, `turn_end`, `model_request_start`, `model_request_end`, `tool_running`, `tool_completed`, `tool_errored`, `subagent_start`, `subagent_stop`, `compaction_start`, `compaction_done`, `notice`, `session_titled`, `mod_error` - plus the two host lifecycle events `session_start` / `session_shutdown`. (`subagent_start`/`subagent_stop` are SubagentStart/Stop; `compaction_start`/`compaction_done` are Pre/PostCompact; `notice` is Notification.) The full payload catalog is in [the AgentEvent catalog](./hooks-and-events.md#agent-event-catalog).
@@ -2,18 +2,18 @@
2
2
 
3
3
  # Hooks and events
4
4
 
5
- The harness's extension seam. A **mod** is a plain object of lifecycle hooks that mutates agent state or changes loop decisions. Pure observers (UI, telemetry, loggers) are **not** mods they subscribe to the `AgentEvent` stream. Hooks change behavior; subscribers watch it. In a loadable mod, `cmd.hooks({...})` registers the hooks and `cmd.on(event, ...)` subscribes to events.
5
+ The harness's extension seam. A **mod** is a plain object of lifecycle hooks that mutates agent state or changes loop decisions. Pure observers (UI, telemetry, loggers) are **not** mods - they subscribe to the `AgentEvent` stream. Hooks change behavior; subscribers watch it. In a loadable mod, `cmd.hooks({...})` registers the hooks and `cmd.on(event, ...)` subscribes to events.
6
6
 
7
7
  A mod may also contribute **tools**, and every hook receives a **`ModContext`** (`{emit, signal, cwd, session?}`) as an extra last argument, so a mod can raise its own events and persist durable state without extra plumbing.
8
8
 
9
- ## Lifecycle where each hook fires
9
+ ## Lifecycle - where each hook fires
10
10
 
11
11
  One `run()` = one user turn → many model turns ("rounds"). The loop below is the agent loop, verbatim in ordering; mod hooks are marked `◆`, events `→`.
12
12
 
13
13
  ```bash
14
14
  run({state, userInput?, config})
15
15
  fork AbortController; working := state (+ user message when userInput given)
16
- ctx := {emit, signal, cwd, session} built ONCE per run,
16
+ ctx := {emit, signal, cwd, session} - built ONCE per run,
17
17
  passed as the LAST argument to every ◆ hook below
18
18
  → run_start {sessionId}
19
19
  poll getSteeringMessages once (catch pre-run queued input)
@@ -21,11 +21,11 @@ run({state, userInput?, config})
21
21
  abort check ──────────────────────────────────► stop: interrupted (→ interrupted)
22
22
  ◆ onTurnStart (each mod, in order; returns new state)
23
23
  → turn_start {turnNumber}
24
- read live permission mode ONCE pinned for this round
24
+ read live permission mode ONCE - pinned for this round
25
25
  resolve systemPrompt (string | builder({sessionId, state, permissionMode}))
26
26
  ◆ appendSystemPrompt (each mod; non-empty returns joined with '\n\n', in
27
27
  registration order, and appended AFTER the base prompt)
28
- ◆ transformContext (each mod; messages threaded mod→mod; result is EPHEMERAL
28
+ ◆ transformContext (each mod; messages threaded mod→mod; result is EPHEMERAL -
29
29
  used for this call only, never written back to state.messages)
30
30
  prepareForSend (core wire-validity pass: orphan heal, merge, strip meta)
31
31
  → message_start
@@ -62,13 +62,13 @@ run({state, userInput?, config})
62
62
 
63
63
  ```bash
64
64
  for each tool_use: → tool_queued {toolCallId, toolName, input} ← ALWAYS the original input
65
- phase 1 permissions (always sequential):
65
+ phase 1 - permissions (always sequential):
66
66
  permissions.generateDescription (5s timeout → null)
67
67
  permissions.check({toolName, input, description, permissionMode})
68
68
  throw ⇒ deny (fail closed)
69
69
  deny → tool_denied; batch aborted: every call gets a tool_result,
70
70
  run stops with permission_denied AFTER the turn commits
71
- phase 2 execution (sequential, or parallel when config.toolExecution='parallel'):
71
+ phase 2 - execution (sequential, or parallel when config.toolExecution='parallel'):
72
72
  ◆ beforeToolCall (each mod, in order; `input` rewrites CHAIN mod→mod;
73
73
  execution uses the LAST mod's input; a mod may also set `terminate`)
74
74
  block ⇒ → tool_hook_blocked {hookOutput}; the block reason becomes the
@@ -85,59 +85,59 @@ phase 2 — execution (sequential, or parallel when config.toolExecution='parall
85
85
  ### Ordering guarantees mod authors can rely on
86
86
 
87
87
  - Every hook receives `ModContext` (`{emit, signal, cwd, session?}`) as its LAST argument. It's declared optional on every hook signature, so a hook that only declares the params object keeps compiling and running unchanged; the core always supplies a real object at runtime.
88
- - `beforeToolCall` completes (for every mod) **before** `tool_running` is emitted. Its `input` rewrites chain across mods, but the `tool_queued` event fired with the ORIGINAL input before any hook ran and is never re-emitted display always shows what the model asked for, only execution sees the rewrite.
89
- - `afterToolCall` completes **before** `tool_completed` / `tool_errored` is emitted the terminal event carries the post-hook content, and its `isError` (when set) decides which of the two fires, independent of whether execution itself threw.
90
- - `afterToolCall`'s `modState` is committed right after the batch's tool-result message is appended BEFORE `onTurnEnd` runs, so `onTurnEnd` sees it.
88
+ - `beforeToolCall` completes (for every mod) **before** `tool_running` is emitted. Its `input` rewrites chain across mods, but the `tool_queued` event fired with the ORIGINAL input before any hook ran and is never re-emitted - display always shows what the model asked for, only execution sees the rewrite.
89
+ - `afterToolCall` completes **before** `tool_completed` / `tool_errored` is emitted - the terminal event carries the post-hook content, and its `isError` (when set) decides which of the two fires, independent of whether execution itself threw.
90
+ - `afterToolCall`'s `modState` is committed right after the batch's tool-result message is appended - BEFORE `onTurnEnd` runs, so `onTurnEnd` sees it.
91
91
  - `onTurnStart` precedes `turn_start`; `onTurnEnd` precedes `turn_end` and `onCommit`.
92
92
  - `onCommit` fires once per completed turn with the full serializable state.
93
93
  - `shouldStopAfterTurn` is evaluated **after** the turn commits, so a stop_hook never loses the turn that triggered it.
94
94
  - `prepareNextTurn` runs only when the loop actually continues (never on a stopping turn).
95
- - `onRunEnd` is **awaited** before the `run_end` event it is the place for must-complete work (flush, trigger learning); event subscribers must never be.
95
+ - `onRunEnd` is **awaited** before the `run_end` event - it is the place for must-complete work (flush, trigger learning); event subscribers must never be.
96
96
  - Mods run in **registration order** in every phase; the built-ins come first and caller mods last (they see post-compaction context and post-hook tool results).
97
- - Hooks **never throw** upward: the runner catches per-mod, per-hook. A failing `transformContext` leaves messages unchanged; a failing `onTurnStart`/`onTurnEnd` keeps the prior state; a failing `shouldStopAfterTurn` means "don't stop"; failing tool hooks are skipped. Every catch site ALSO emits a `mod_error {modId, hook, error}` event before falling back to its no-crash default the decision the loop makes is unchanged, but the degradation is never silent.
97
+ - Hooks **never throw** upward: the runner catches per-mod, per-hook. A failing `transformContext` leaves messages unchanged; a failing `onTurnStart`/`onTurnEnd` keeps the prior state; a failing `shouldStopAfterTurn` means "don't stop"; failing tool hooks are skipped. Every catch site ALSO emits a `mod_error {modId, hook, error}` event before falling back to its no-crash default - the decision the loop makes is unchanged, but the degradation is never silent.
98
98
 
99
99
  ## The hook contracts
100
100
 
101
101
  ### `transformContext({messages, state, signal?}, ctx?) → messages`
102
102
 
103
- Fires once per round, after `appendSystemPrompt`, before `prepareForSend`. Messages are threaded through every mod in order each receives the previous mod's output. **The result is used for this model call only and never written back to `state.messages`**: the durable log is untouched (compaction relies on this the recorded transcript stays complete). Return the input array unchanged (same reference) to signal "no change".
103
+ Fires once per round, after `appendSystemPrompt`, before `prepareForSend`. Messages are threaded through every mod in order - each receives the previous mod's output. **The result is used for this model call only and never written back to `state.messages`**: the durable log is untouched (compaction relies on this - the recorded transcript stays complete). Return the input array unchanged (same reference) to signal "no change".
104
104
 
105
105
  ### `shouldStopAfterTurn({state, turnNumber}, ctx?) → boolean`
106
106
 
107
- Fires after the turn commits. **Any** mod returning `true` ends the run with `stopReason: 'stop_hook'` first true short-circuits. This is *early stop* (goal budget spent); it is the opposite of the Stop hook forcing continuation.
107
+ Fires after the turn commits. **Any** mod returning `true` ends the run with `stopReason: 'stop_hook'` - first true short-circuits. This is *early stop* (goal budget spent); it is the opposite of the Stop hook forcing continuation.
108
108
 
109
109
  ### `prepareNextTurn({state, turnNumber}, ctx?) → {model?, effort?} | undefined`
110
110
 
111
- Fires at the bottom of a continuing round. Results are merged across mods later mods override earlier ones per field. A returned `model`/`effort` applies from the **next** model call onward (`config` is never mutated).
111
+ Fires at the bottom of a continuing round. Results are merged across mods - later mods override earlier ones per field. A returned `model`/`effort` applies from the **next** model call onward (`config` is never mutated).
112
112
 
113
113
  ### `appendSystemPrompt({state}, ctx?) → string | undefined`
114
114
 
115
- Fires once per round, right after the round's base `systemPrompt` resolves and before `transformContext`. Every mod's non-empty return is joined with `'\n\n'`, in registration order, and appended AFTER the base prompt. May return a plain string OR a Promise. **Must be byte-stable across rounds for the same durable inputs** the provider's prompt-prefix cache keys off the system prompt's bytes, so a value that changes turn-to-turn without a corresponding `state`/`modState` change busts the cache every round. Compute once, store in `modState`, read it back.
115
+ Fires once per round, right after the round's base `systemPrompt` resolves and before `transformContext`. Every mod's non-empty return is joined with `'\n\n'`, in registration order, and appended AFTER the base prompt. May return a plain string OR a Promise. **Must be byte-stable across rounds for the same durable inputs** - the provider's prompt-prefix cache keys off the system prompt's bytes, so a value that changes turn-to-turn without a corresponding `state`/`modState` change busts the cache every round. Compute once, store in `modState`, read it back.
116
116
 
117
117
  ### `beforeToolCall({toolCallId, toolName, input, state}, ctx?) → {block?, additionalContext?, input?, terminate?} | undefined`
118
118
 
119
119
  Fires per tool call, after the permission check passed, before execution.
120
120
 
121
- - `block: true` the tool does not run. The core emits `tool_hook_blocked` with `hookOutput` = your `additionalContext` (or `'Blocked by a pre-tool hook.'`), and that same text becomes the tool_result the model sees. Later mods' `beforeToolCall` for this call do not run. No `tool_running`/`tool_completed`/`tool_errored` is emitted for a blocked call.
122
- - `additionalContext` (without `block`) collected across mods and appended as extra text blocks to the tool_result **after** execution and after `afterToolCall` overrides.
123
- - `input` rewrites the input the tool actually executes with. **Chained across mods**: the next mod's `input` param is YOUR rewrite, not the original.
124
- - `terminate: true` stops the run after this tool batch finishes. ANY-semantics (one hook, on one call, in one batch, is enough). Combines with `block` on the same return value.
125
- - `undefined` no opinion.
121
+ - `block: true` - the tool does not run. The core emits `tool_hook_blocked` with `hookOutput` = your `additionalContext` (or `'Blocked by a pre-tool hook.'`), and that same text becomes the tool_result the model sees. Later mods' `beforeToolCall` for this call do not run. No `tool_running`/`tool_completed`/`tool_errored` is emitted for a blocked call.
122
+ - `additionalContext` (without `block`) - collected across mods and appended as extra text blocks to the tool_result **after** execution and after `afterToolCall` overrides.
123
+ - `input` - rewrites the input the tool actually executes with. **Chained across mods**: the next mod's `input` param is YOUR rewrite, not the original.
124
+ - `terminate: true` - stops the run after this tool batch finishes. ANY-semantics (one hook, on one call, in one batch, is enough). Combines with `block` on the same return value.
125
+ - `undefined` - no opinion.
126
126
 
127
127
  ### `afterToolCall({toolCallId, toolName, input, result, isError, state}, ctx?) → {content?, terminate?, additionalContext?, isError?, modState?} | undefined`
128
128
 
129
- Fires per tool call after execution (also after an execution error `result` is then the error text content). `input` is the FINAL input the tool actually ran with (after any `beforeToolCall` rewrites).
129
+ Fires per tool call after execution (also after an execution error - `result` is then the error text content). `input` is the FINAL input the tool actually ran with (after any `beforeToolCall` rewrites).
130
130
 
131
- - `isError` (param) whether the tool's OWN execution failed, read before any hook override and threaded across mods like `result`. This is the **PostToolUse vs PostToolUseFailure** distinction: a hook can branch on `isError` to react only to failures. It is the INPUT signal; the returned `isError` field below is the OUTPUT override that selects the terminal event.
131
+ - `isError` (param) - whether the tool's OWN execution failed, read before any hook override and threaded across mods like `result`. This is the **PostToolUse vs PostToolUseFailure** distinction: a hook can branch on `isError` to react only to failures. It is the INPUT signal; the returned `isError` field below is the OUTPUT override that selects the terminal event.
132
132
  - `content` **replaces** the tool result the model will see (threaded mod→mod).
133
133
  - `terminate: true` ends the run with `stopReason: 'terminate'` **after the whole batch finishes**.
134
- - `additionalContext` appended as a SEPARATE text block after `content` (and after any `beforeToolCall` additionalContext strings).
135
- - `isError` overrides whether the terminal event is `tool_completed` or `tool_errored`, independent of whether execution itself threw.
136
- - `modState` a `Record<string, unknown>` merged into `state.modState[mod.id]` right after the tool-result message commits a full replace of that mod's slot, exactly like `setModState`. The durable-state channel for tool hooks: neither tool hook can return a whole `AgentState` the way `onTurnStart`/`onTurnEnd` can.
134
+ - `additionalContext` - appended as a SEPARATE text block after `content` (and after any `beforeToolCall` additionalContext strings).
135
+ - `isError` - overrides whether the terminal event is `tool_completed` or `tool_errored`, independent of whether execution itself threw.
136
+ - `modState` - a `Record<string, unknown>` merged into `state.modState[mod.id]` right after the tool-result message commits - a full replace of that mod's slot, exactly like `setModState`. The durable-state channel for tool hooks: neither tool hook can return a whole `AgentState` the way `onTurnStart`/`onTurnEnd` can.
137
137
 
138
138
  ### `onTurnStart({state, turnNumber}, ctx?) → AgentState` / `onTurnEnd({state, turnNumber, hadToolCalls, usage}, ctx?) → AgentState`
139
139
 
140
- The only hooks that can **persist state changes** unconditionally they return the new `AgentState` (typically via `setModState`), which the loop threads onward and commits. `onTurnEnd`'s `usage` is *this turn's* token usage. Both fire every round, including the round that stops.
140
+ The only hooks that can **persist state changes** unconditionally - they return the new `AgentState` (typically via `setModState`), which the loop threads onward and commits. `onTurnEnd`'s `usage` is *this turn's* token usage. Both fire every round, including the round that stops.
141
141
 
142
142
  ### `onRunEnd({state, result}, ctx?) → void`
143
143
 
@@ -145,12 +145,12 @@ Fires once, awaited, after the loop exits and before the `run_end` event. `state
145
145
 
146
146
  ### `onStop({state, stopReason, turnNumber, lastAssistantText}, ctx?) → {continue?, reason?} | undefined`
147
147
 
148
- The **Stop hook** the mods' force-continue channel. Fires when a turn WOULD end the run **naturally** the model returned no tool calls and no follow-up provider wants to continue. Returning `{continue: true}` keeps the run going: `reason` (or a neutral default) is appended as an automated `source: 'stop_hook'` user turn, so the model is told why it must keep working.
148
+ The **Stop hook** - the mods' force-continue channel. Fires when a turn WOULD end the run **naturally** - the model returned no tool calls and no follow-up provider wants to continue. Returning `{continue: true}` keeps the run going: `reason` (or a neutral default) is appended as an automated `source: 'stop_hook'` user turn, so the model is told why it must keep working.
149
149
 
150
150
  - **ANY** mod returning `continue` wins (first wins, short-circuits).
151
151
  - Does **NOT** fire for hard stops (`max_turns`, `terminate`, `permission_denied`, `interrupted`).
152
152
  - A follow-up provider (e.g. the continuation nudger) gets first say; `onStop` is consulted only when the provider declines.
153
- - The loop caps consecutive stop-hook continuations at **8** a hook that always says `continue` can't loop forever.
153
+ - The loop caps consecutive stop-hook continuations at **8** - a hook that always says `continue` can't loop forever.
154
154
  - Distinct from `shouldStopAfterTurn` (force EARLY stop) and `onRunEnd` (observe the stop): only `onStop` can push a finished run onward.
155
155
 
156
156
  ```ts
@@ -158,18 +158,18 @@ const persistUntilTestsPass = {
158
158
  id: 'until-green',
159
159
  onStop: async ({lastAssistantText}) => {
160
160
  if (/all tests pass/i.test(lastAssistantText)) return {continue: false};
161
- return {continue: true, reason: 'Tests are not green yet keep going.'};
161
+ return {continue: true, reason: 'Tests are not green yet - keep going.'};
162
162
  },
163
163
  };
164
164
  ```
165
165
 
166
- ## `ModContext.session` the mods' persistence surface
166
+ ## `ModContext.session` - the mods' persistence surface
167
167
 
168
168
  The harness's tree-format session store gives mods a durable, per-entry seam onto the SAME append-only file the transcript lives in. Available in a loadable mod as `cmd.session` and inside hooks as `ctx.session`. Two entry kinds:
169
169
 
170
- - **`appendCustomEntry({customType, data?})`** a `custom` tree entry. Mod-private data; **never** sent to the LLM, never rendered. Use it for durable bookkeeping a mod wants to survive resume (counters, cursors, cached decisions).
171
- - **`appendCustomMessageEntry({customType, content, display, details?})`** a `custom_message` tree entry. Content the model SHOULD see: it is projected as an ordinary `user` message on the NEXT turn (`display: true` also renders it in the TUI with distinct styling; `display: false` is context-only). The call returns `{entryId, message}` a mod MUST fold `message` onto the `AgentState` it hands back from its hook for the model to see it that turn.
172
- - **`getCustomEntries({customType})`** reads back every `custom` entry this mod itself wrote (filtered by `customType`), in file order, over the ACTIVE branch's full entry list. The standard reload pattern: a mod with in-memory state seeds it from `getCustomEntries` at first `onTurnStart`.
170
+ - **`appendCustomEntry({customType, data?})`** - a `custom` tree entry. Mod-private data; **never** sent to the LLM, never rendered. Use it for durable bookkeeping a mod wants to survive resume (counters, cursors, cached decisions).
171
+ - **`appendCustomMessageEntry({customType, content, display, details?})`** - a `custom_message` tree entry. Content the model SHOULD see: it is projected as an ordinary `user` message on the NEXT turn (`display: true` also renders it in the TUI with distinct styling; `display: false` is context-only). The call returns `{entryId, message}` - a mod MUST fold `message` onto the `AgentState` it hands back from its hook for the model to see it that turn.
172
+ - **`getCustomEntries({customType})`** - reads back every `custom` entry this mod itself wrote (filtered by `customType`), in file order, over the ACTIVE branch's full entry list. The standard reload pattern: a mod with in-memory state seeds it from `getCustomEntries` at first `onTurnStart`.
173
173
 
174
174
  ```ts
175
175
  const MOD_ID = 'turn-counter';
@@ -188,11 +188,11 @@ const mod = {
188
188
  };
189
189
  ```
190
190
 
191
- `session` is absent (not merely empty) only for a bare unit-test config built without a durable store every hook must treat `ctx.session` as possibly `undefined` and no-op gracefully. A `--no-session` run DOES populate `session` entries still append and are readable for the lifetime of the process, they simply never touch disk.
191
+ `session` is absent (not merely empty) only for a bare unit-test config built without a durable store - every hook must treat `ctx.session` as possibly `undefined` and no-op gracefully. A `--no-session` run DOES populate `session` - entries still append and are readable for the lifetime of the process, they simply never touch disk.
192
192
 
193
193
  ## `modState` conventions
194
194
 
195
- `AgentState.modState` is a `Readonly<Record<string, unknown>>` one slot per mod, keyed by the mod's `id`:
195
+ `AgentState.modState` is a `Readonly<Record<string, unknown>>` - one slot per mod, keyed by the mod's `id`:
196
196
 
197
197
  ```ts
198
198
  const value = getModState<MyShape>({state, modId: 'my-mod'}); // undefined when unset
@@ -200,26 +200,26 @@ const next = setModState({state, modId: 'my-mod', value: {...}}); // fresh Agen
200
200
  ```
201
201
 
202
202
  - **Namespacing is by convention**: write only your own `id`'s slot.
203
- - Values must be **JSON-serializable** `modState` is persisted with the session and survives resume. Run-scoped/volatile data (budgets, locks, in-flight promises) belongs in the mod factory's closure instead.
203
+ - Values must be **JSON-serializable** - `modState` is persisted with the session and survives resume. Run-scoped/volatile data (budgets, locks, in-flight promises) belongs in the mod factory's closure instead.
204
204
  - `onTurnStart`/`onTurnEnd` return the whole `AgentState` (typically via `setModState`). `afterToolCall` can also persist state via its `modState` return field. `beforeToolCall` has no state channel; a before-hook that needs to accumulate data stores it in the closure and flushes it via `afterToolCall`'s `modState` or `onTurnEnd`.
205
205
  - Custom data never goes into `state.messages`: the message union is closed wire types. When a mod must put something in front of the model, it emits real messages via `transformContext` (or `appendCustomMessageEntry`).
206
206
 
207
207
  ## `AgentEvent` catalog
208
208
 
209
- One sync sink, fan out with `createEventBus`. Payloads are snapshots never live references.
209
+ One sync sink, fan out with `createEventBus`. Payloads are snapshots - never live references.
210
210
 
211
211
  | Event | Fires | Payload highlights |
212
212
  |---|---|---|
213
213
  | `run_start` / `run_end` | run boundaries | `sessionId` / `result` |
214
214
  | `turn_start` / `turn_end` | round boundaries (after onTurnStart / onTurnEnd) | `turnNumber`; end adds `hadToolCalls`, this turn's `usage` |
215
- | `message_start` | before each model call | |
215
+ | `message_start` | before each model call | - |
216
216
  | `text_delta`, `thinking_start/delta/end` | streaming | deltas; `thinking_end` carries full text |
217
217
  | `message_update` | streaming (ModelClient-accumulated) | whole partial assistant message incl. partial tool JSON |
218
218
  | `message_end` | response complete | full assistant content |
219
219
  | `model_request_start/end` | bracket the inference call | `model`; end adds `usage`, raw-preferred `stopReason` |
220
- | `tool_queued` | per call, before permission checks | `input` the ORIGINAL input, never the rewritten one |
221
- | `tool_denied` | permission denied | (batch then aborts) |
222
- | `tool_hook_blocked` | a mod's beforeToolCall blocked | `hookOutput` **terminal for that call**: no tool_running/completed/errored follows |
220
+ | `tool_queued` | per call, before permission checks | `input` - the ORIGINAL input, never the rewritten one |
221
+ | `tool_denied` | permission denied | - (batch then aborts) |
222
+ | `tool_hook_blocked` | a mod's beforeToolCall blocked | `hookOutput` - **terminal for that call**: no tool_running/completed/errored follows |
223
223
  | `tool_running` | execution begins (after beforeToolCall) | `description` from permissions.generateDescription |
224
224
  | `tool_update` | streaming tool progress | `partial` content |
225
225
  | `tool_completed` / `tool_errored` | after afterToolCall | post-hook `result` / `error` text; `afterToolCall`'s `isError` can select which one fires |
@@ -234,10 +234,10 @@ One sync sink, fan out with `createEventBus`. Payloads are snapshots — never l
234
234
  | `tool_input_coerced` | ModelClient rescued malformed (array/null) tool input | `rawType`, `recovered` |
235
235
  | `tool_input_repaired` | repair layer healed tool input pre-execution | `rulesFired`, `hintCount`, `receivedKeys` |
236
236
  | `mod_error` | a mod hook threw (any phase), or a mod tool collided with an existing tool | `modId`, `hook`, `error` |
237
- | `interrupted` | abort observed | |
237
+ | `interrupted` | abort observed | - |
238
238
  | `run_error` | non-retryable failure | the raw `Error` |
239
239
 
240
- ## Worked example a write-quota mod
240
+ ## Worked example - a write-quota mod
241
241
 
242
242
  Blocks writes outside an allowlist, counts tool activity durably in modState, and reports at run end. Exercises the block channel, the closure-vs-modState split, and `onRunEnd`.
243
243
 
@@ -248,7 +248,7 @@ import {getModState, setModState} from '@commandcode/harness';
248
248
  const MOD_ID = 'write-quota';
249
249
 
250
250
  interface WriteQuotaState {
251
- readonly writesThisSession: number; // durable survives resume
251
+ readonly writesThisSession: number; // durable - survives resume
252
252
  }
253
253
 
254
254
  export function createWriteQuotaMod(options: {
@@ -257,7 +257,7 @@ export function createWriteQuotaMod(options: {
257
257
  readonly report: (summary: string) => void;
258
258
  }): AgentMod {
259
259
  // Run-scoped tally lives in the closure; flushed into modState at turn end
260
- // (beforeToolCall cannot return state see modState conventions).
260
+ // (beforeToolCall cannot return state - see modState conventions).
261
261
  let writesThisTurn = 0;
262
262
 
263
263
  return {
@@ -268,7 +268,7 @@ export function createWriteQuotaMod(options: {
268
268
  const path = typeof input.file_path === 'string' ? input.file_path : '';
269
269
  if (!path.startsWith(options.allowedRoot)) {
270
270
  // Block: this text becomes the tool_result the model sees, and the
271
- // core emits tool_hook_blocked. The run continues the model adapts.
271
+ // core emits tool_hook_blocked. The run continues - the model adapts.
272
272
  return {
273
273
  block: true,
274
274
  additionalContext: `Writes outside ${options.allowedRoot} are not allowed.`,
@@ -305,4 +305,4 @@ export function createWriteQuotaMod(options: {
305
305
  }
306
306
  ```
307
307
 
308
- Wire it as a loadable mod (`cmd.hooks({...})` with the same handlers) or, when embedding the harness, `createHarness({..., mods: [createWriteQuotaMod({...})]})`.
308
+ Wire it as a loadable mod (`cmd.hooks({...})` with the same handlers) - or, when embedding the harness, `createHarness({..., mods: [createWriteQuotaMod({...})]})`.