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.
- package/CHANGELOG.md +7 -0
- package/dist/bundled/command-code-knowledge/SKILL.md +2 -1
- package/dist/bundled/command-code-knowledge/reference/custom-agents.md +7 -7
- package/dist/bundled/command-code-knowledge/reference/custom-slash-commands.md +5 -5
- package/dist/bundled/command-code-knowledge/reference/headless.md +7 -7
- package/dist/bundled/command-code-knowledge/reference/hooks.md +7 -7
- package/dist/bundled/command-code-knowledge/reference/mcp.md +8 -8
- package/dist/bundled/command-code-knowledge/reference/permissions.md +75 -75
- package/dist/bundled/command-code-knowledge/reference/plan-mode.md +2 -0
- package/dist/bundled/command-code-knowledge/reference/plan-review.md +226 -0
- package/dist/bundled/command-code-knowledge/reference/skills.md +27 -27
- package/dist/bundled/mod-builder/reference/api.md +21 -21
- package/dist/bundled/mod-builder/reference/hooks-and-events.md +48 -48
- package/dist/bundled/mod-builder/reference/overview.md +19 -17
- package/dist/bundled/mod-builder/reference/packaging.md +6 -6
- package/dist/bundled/mod-builder/reference/ui.md +9 -9
- package/dist/bundled/mod-builder/reference/verify.md +11 -11
- package/dist/cli.mjs +2 -2
- package/package.json +4 -4
- package/vsix/commandcode-vscode.vsix +0 -0
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
# Agent Skills
|
|
4
4
|
|
|
5
|
-
Command Code supports Agent Skills
|
|
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
|
|
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**
|
|
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
|
|
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**
|
|
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`
|
|
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
|
|
216
|
-
- The skill still appears in the `/` menu with a `[skill]` badge and a `
|
|
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
|
|
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 `
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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**
|
|
660
|
-
* **Fenced**
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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`
|
|
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
|
|
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)`
|
|
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)`
|
|
33
|
-
- `cmd.addTool(toolModule)`
|
|
34
|
-
- `cmd.addCommand({name, description, handler})`
|
|
35
|
-
- `cmd.addFlag(name, {type, default})` / `cmd.getFlag(name)`
|
|
36
|
-
- `cmd.on(event, handler)`
|
|
37
|
-
- `cmd.addProvider(providerModule)`
|
|
38
|
-
- `cmd.addRenderer(customType, data => lines)`
|
|
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`
|
|
55
|
-
- `cmd.queueMessage({content, deliverAs})`
|
|
56
|
-
- `cmd.session`
|
|
57
|
-
- `cmd.showEntry(customType, data)`
|
|
58
|
-
- `cmd.sessions`
|
|
59
|
-
- `cmd.exec({command, args})`
|
|
60
|
-
- `cmd.getAllTools()` / `cmd.getActiveTools()` / `cmd.setActiveTools(names)`
|
|
61
|
-
- `cmd.events`
|
|
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
|
|
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
|
|
76
|
-
- `onStop({state, stopReason, turnNumber, lastAssistantText})` is the **Stop** hook
|
|
77
|
-
- `onSessionStart({source})` / `onSessionEnd({reason})` are the once-per-session **SessionStart** / **SessionEnd** hooks
|
|
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']`
|
|
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
|
|
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
|
|
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}
|
|
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
|
|
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
|
|
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
|
|
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
|
|
89
|
-
- `afterToolCall` completes **before** `tool_completed` / `tool_errored` is emitted
|
|
90
|
-
- `afterToolCall`'s `modState` is committed right after the batch's tool-result message is appended
|
|
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
|
|
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
|
|
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
|
|
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'`
|
|
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
|
|
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**
|
|
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`
|
|
122
|
-
- `additionalContext` (without `block`)
|
|
123
|
-
- `input`
|
|
124
|
-
- `terminate: true`
|
|
125
|
-
- `undefined`
|
|
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
|
|
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)
|
|
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`
|
|
135
|
-
- `isError`
|
|
136
|
-
- `modState`
|
|
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
|
|
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**
|
|
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**
|
|
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
|
|
161
|
+
return {continue: true, reason: 'Tests are not green yet - keep going.'};
|
|
162
162
|
},
|
|
163
163
|
};
|
|
164
164
|
```
|
|
165
165
|
|
|
166
|
-
## `ModContext.session`
|
|
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?})`**
|
|
171
|
-
- **`appendCustomMessageEntry({customType, content, display, details?})`**
|
|
172
|
-
- **`getCustomEntries({customType})`**
|
|
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
|
|
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>>`
|
|
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**
|
|
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
|
|
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`
|
|
221
|
-
| `tool_denied` | permission denied |
|
|
222
|
-
| `tool_hook_blocked` | a mod's beforeToolCall blocked | `hookOutput`
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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)
|
|
308
|
+
Wire it as a loadable mod (`cmd.hooks({...})` with the same handlers) - or, when embedding the harness, `createHarness({..., mods: [createWriteQuotaMod({...})]})`.
|