command-code 0.52.5 → 1.0.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.
Files changed (61) hide show
  1. package/CHANGELOG.md +2541 -0
  2. package/dist/bundled/command-code-knowledge/SKILL.md +52 -0
  3. package/dist/bundled/command-code-knowledge/reference/checkpoints.md +366 -0
  4. package/dist/bundled/command-code-knowledge/reference/custom-agents.md +91 -0
  5. package/dist/bundled/command-code-knowledge/reference/custom-slash-commands.md +345 -0
  6. package/dist/bundled/command-code-knowledge/reference/headless.md +234 -0
  7. package/dist/bundled/command-code-knowledge/reference/hooks.md +1097 -0
  8. package/dist/bundled/command-code-knowledge/reference/mcp.md +626 -0
  9. package/dist/bundled/command-code-knowledge/reference/memory.md +98 -0
  10. package/dist/bundled/command-code-knowledge/reference/models.md +81 -0
  11. package/dist/bundled/command-code-knowledge/reference/permissions.md +412 -0
  12. package/dist/bundled/command-code-knowledge/reference/plan-mode.md +101 -0
  13. package/dist/bundled/command-code-knowledge/reference/product-help.md +421 -0
  14. package/dist/bundled/command-code-knowledge/reference/skills.md +993 -0
  15. package/dist/bundled/mod-builder/SKILL.md +128 -0
  16. package/dist/bundled/mod-builder/examples/block-dangerous-commands.ts +41 -0
  17. package/dist/bundled/mod-builder/examples/custom-entry-renderer.ts +32 -0
  18. package/dist/bundled/mod-builder/examples/custom-tool.ts +53 -0
  19. package/dist/bundled/mod-builder/examples/flags-and-options.ts +29 -0
  20. package/dist/bundled/mod-builder/examples/input-shortcuts.ts +41 -0
  21. package/dist/bundled/mod-builder/examples/kitchen-sink.ts +110 -0
  22. package/dist/bundled/mod-builder/examples/lifecycle-hooks.ts +64 -0
  23. package/dist/bundled/mod-builder/examples/observe-events.ts +30 -0
  24. package/dist/bundled/mod-builder/examples/slash-command.ts +33 -0
  25. package/dist/bundled/mod-builder/reference/api.md +81 -0
  26. package/dist/bundled/mod-builder/reference/hooks-and-events.md +308 -0
  27. package/dist/bundled/mod-builder/reference/overview.md +161 -0
  28. package/dist/bundled/mod-builder/reference/packaging.md +63 -0
  29. package/dist/bundled/mod-builder/reference/ui.md +63 -0
  30. package/dist/bundled/mod-builder/reference/verify.md +58 -0
  31. package/dist/bundled/skill-builder/SKILL.md +104 -0
  32. package/dist/cli.mjs +5 -5
  33. package/package.json +35 -26
  34. package/vsix/commandcode-vscode.vsix +0 -0
  35. /package/{skills → dist/bundled}/agent-browser/SKILL.md +0 -0
  36. /package/{skills → dist/bundled}/design/SKILL.md +0 -0
  37. /package/{skills → dist/bundled}/design/references/border.md +0 -0
  38. /package/{skills → dist/bundled}/design/references/button.md +0 -0
  39. /package/{skills → dist/bundled}/design/references/checkup.md +0 -0
  40. /package/{skills → dist/bundled}/design/references/color.md +0 -0
  41. /package/{skills → dist/bundled}/design/references/create.md +0 -0
  42. /package/{skills → dist/bundled}/design/references/design-html.md +0 -0
  43. /package/{skills → dist/bundled}/design/references/deslop.md +0 -0
  44. /package/{skills → dist/bundled}/design/references/finish.md +0 -0
  45. /package/{skills → dist/bundled}/design/references/interaction.md +0 -0
  46. /package/{skills → dist/bundled}/design/references/layout.md +0 -0
  47. /package/{skills → dist/bundled}/design/references/motion.md +0 -0
  48. /package/{skills → dist/bundled}/design/references/redesign.md +0 -0
  49. /package/{skills → dist/bundled}/design/references/refine.md +0 -0
  50. /package/{skills → dist/bundled}/design/references/relayout.md +0 -0
  51. /package/{skills → dist/bundled}/design/references/report-html.md +0 -0
  52. /package/{skills → dist/bundled}/design/references/responsive.md +0 -0
  53. /package/{skills → dist/bundled}/design/references/review.md +0 -0
  54. /package/{skills → dist/bundled}/design/references/setup.md +0 -0
  55. /package/{skills → dist/bundled}/design/references/shadow.md +0 -0
  56. /package/{skills → dist/bundled}/design/references/smell.md +0 -0
  57. /package/{skills → dist/bundled}/design/references/surface.md +0 -0
  58. /package/{skills → dist/bundled}/design/references/tokenize.md +0 -0
  59. /package/{skills → dist/bundled}/design/references/typeset.md +0 -0
  60. /package/{skills → dist/bundled}/design/references/voice.md +0 -0
  61. /package/{skills → dist/bundled}/design/references/writing.md +0 -0
@@ -0,0 +1,308 @@
1
+ <!-- GENERATED FILE — do not edit. Source: packages/docs/src/app/mods/page.mdx. Regenerate: pnpm generate:knowledge -->
2
+
3
+ # Hooks and events
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.
6
+
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
+
9
+ ## Lifecycle — where each hook fires
10
+
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
+
13
+ ```bash
14
+ run({state, userInput?, config})
15
+ fork AbortController; working := state (+ user message when userInput given)
16
+ ctx := {emit, signal, cwd, session} — built ONCE per run,
17
+ passed as the LAST argument to every ◆ hook below
18
+ → run_start {sessionId}
19
+ poll getSteeringMessages once (catch pre-run queued input)
20
+ loop:
21
+ abort check ──────────────────────────────────► stop: interrupted (→ interrupted)
22
+ ◆ onTurnStart (each mod, in order; returns new state)
23
+ → turn_start {turnNumber}
24
+ read live permission mode ONCE — pinned for this round
25
+ resolve systemPrompt (string | builder({sessionId, state, permissionMode}))
26
+ ◆ appendSystemPrompt (each mod; non-empty returns joined with '\n\n', in
27
+ registration order, and appended AFTER the base prompt)
28
+ ◆ transformContext (each mod; messages threaded mod→mod; result is EPHEMERAL —
29
+ used for this call only, never written back to state.messages)
30
+ prepareForSend (core wire-validity pass: orphan heal, merge, strip meta)
31
+ → message_start
32
+ → model_request_start {model}
33
+ modelClient.complete(...) (streams → text_delta / thinking_* /
34
+ message_update / continuation_recovery /
35
+ tool_input_coerced; retry → api_retry)
36
+ → model_request_end {model, usage, stopReason}
37
+ → message_end {content}
38
+ working += assistant message (provider-executed blocks filtered out)
39
+ if client tool calls:
40
+ runTools(...) (see tool-dispatch order below)
41
+ working += tool-results user message
42
+ working := setModState(...) for every afterToolCall `modState`
43
+ contribution across the batch (BEFORE onTurnEnd runs)
44
+ any deny ────────────────────────► pendingStop: permission_denied
45
+ any terminate ───────────────────► pendingStop: terminate
46
+ else poll getSteeringMessages → append (source 'steering')
47
+ ◆ onTurnEnd (each mod; receives THIS turn's usage; returns new state)
48
+ → turn_end {turnNumber, hadToolCalls, usage}
49
+ onCommit(working) ← the durability boundary
50
+ pendingStop? ──────────────────────► stop
51
+ ◆ shouldStopAfterTurn (any true) ──► stop: stop_hook
52
+ turnNumber ≥ maxTurns ─────────────► stop: max_turns
53
+ if no tool calls:
54
+ no follow-up wanted ─────────────► ◆ onStop (any continue → keep going)
55
+ else stop: end_turn
56
+ ◆ prepareNextTurn (merged; later mods win) → may swap model / effort
57
+ ◆ onRunEnd (each mod, awaited)
58
+ → run_end {result}
59
+ ```
60
+
61
+ ### Tool-dispatch order
62
+
63
+ ```bash
64
+ for each tool_use: → tool_queued {toolCallId, toolName, input} ← ALWAYS the original input
65
+ phase 1 — permissions (always sequential):
66
+ permissions.generateDescription (5s timeout → null)
67
+ permissions.check({toolName, input, description, permissionMode})
68
+ throw ⇒ deny (fail closed)
69
+ deny → tool_denied; batch aborted: every call gets a tool_result,
70
+ run stops with permission_denied AFTER the turn commits
71
+ phase 2 — execution (sequential, or parallel when config.toolExecution='parallel'):
72
+ ◆ beforeToolCall (each mod, in order; `input` rewrites CHAIN mod→mod;
73
+ execution uses the LAST mod's input; a mod may also set `terminate`)
74
+ block ⇒ → tool_hook_blocked {hookOutput}; the block reason becomes the
75
+ tool_result text; NO tool_running / tool_completed / tool_errored fires
76
+ → tool_running {toolCallId, toolName, description}
77
+ toolRunner.execute({toolName, input: <possibly rewritten>, signal, onUpdate})
78
+ ◆ afterToolCall (each mod, in order; sees current content, may replace it,
79
+ append `additionalContext`, flip `isError`, set `terminate`, or persist
80
+ `modState`)
81
+ beforeToolCall additionalContext strings appended to the final content
82
+ → tool_completed {result} (or → tool_errored {error})
83
+ ```
84
+
85
+ ### Ordering guarantees mod authors can rely on
86
+
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.
91
+ - `onTurnStart` precedes `turn_start`; `onTurnEnd` precedes `turn_end` and `onCommit`.
92
+ - `onCommit` fires once per completed turn with the full serializable state.
93
+ - `shouldStopAfterTurn` is evaluated **after** the turn commits, so a stop_hook never loses the turn that triggered it.
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.
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.
98
+
99
+ ## The hook contracts
100
+
101
+ ### `transformContext({messages, state, signal?}, ctx?) → messages`
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".
104
+
105
+ ### `shouldStopAfterTurn({state, turnNumber}, ctx?) → boolean`
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.
108
+
109
+ ### `prepareNextTurn({state, turnNumber}, ctx?) → {model?, effort?} | undefined`
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).
112
+
113
+ ### `appendSystemPrompt({state}, ctx?) → string | undefined`
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.
116
+
117
+ ### `beforeToolCall({toolCallId, toolName, input, state}, ctx?) → {block?, additionalContext?, input?, terminate?} | undefined`
118
+
119
+ Fires per tool call, after the permission check passed, before execution.
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.
126
+
127
+ ### `afterToolCall({toolCallId, toolName, input, result, isError, state}, ctx?) → {content?, terminate?, additionalContext?, isError?, modState?} | undefined`
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).
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.
132
+ - `content` **replaces** the tool result the model will see (threaded mod→mod).
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.
137
+
138
+ ### `onTurnStart({state, turnNumber}, ctx?) → AgentState` / `onTurnEnd({state, turnNumber, hadToolCalls, usage}, ctx?) → AgentState`
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.
141
+
142
+ ### `onRunEnd({state, result}, ctx?) → void`
143
+
144
+ Fires once, awaited, after the loop exits and before the `run_end` event. `state` is the final state; `result` carries `finalText`, `stopReason`, `turnCount`, accumulated `usage`. Cannot alter the result. This is the "must-complete work" hook (learning, flushes).
145
+
146
+ ### `onStop({state, stopReason, turnNumber, lastAssistantText}, ctx?) → {continue?, reason?} | undefined`
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.
149
+
150
+ - **ANY** mod returning `continue` wins (first wins, short-circuits).
151
+ - Does **NOT** fire for hard stops (`max_turns`, `terminate`, `permission_denied`, `interrupted`).
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.
154
+ - Distinct from `shouldStopAfterTurn` (force EARLY stop) and `onRunEnd` (observe the stop): only `onStop` can push a finished run onward.
155
+
156
+ ```ts
157
+ const persistUntilTestsPass = {
158
+ id: 'until-green',
159
+ onStop: async ({lastAssistantText}) => {
160
+ if (/all tests pass/i.test(lastAssistantText)) return {continue: false};
161
+ return {continue: true, reason: 'Tests are not green yet — keep going.'};
162
+ },
163
+ };
164
+ ```
165
+
166
+ ## `ModContext.session` — the mods' persistence surface
167
+
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
+
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
+
174
+ ```ts
175
+ const MOD_ID = 'turn-counter';
176
+ const CUSTOM_TYPE = 'turn-counter/count';
177
+
178
+ const mod = {
179
+ id: MOD_ID,
180
+ onTurnStart: async ({state}, ctx) => {
181
+ if (!ctx?.session) return state; // a bare unit-test config without a store
182
+ const priorCount = ctx.session.getCustomEntries({
183
+ customType: CUSTOM_TYPE,
184
+ }).length;
185
+ ctx.session.appendCustomEntry({customType: CUSTOM_TYPE, data: {count: priorCount + 1}});
186
+ return state;
187
+ },
188
+ };
189
+ ```
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.
192
+
193
+ ## `modState` conventions
194
+
195
+ `AgentState.modState` is a `Readonly<Record<string, unknown>>` — one slot per mod, keyed by the mod's `id`:
196
+
197
+ ```ts
198
+ const value = getModState<MyShape>({state, modId: 'my-mod'}); // undefined when unset
199
+ const next = setModState({state, modId: 'my-mod', value: {...}}); // fresh AgentState
200
+ ```
201
+
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.
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
+ - 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
+
207
+ ## `AgentEvent` catalog
208
+
209
+ One sync sink, fan out with `createEventBus`. Payloads are snapshots — never live references.
210
+
211
+ | Event | Fires | Payload highlights |
212
+ |---|---|---|
213
+ | `run_start` / `run_end` | run boundaries | `sessionId` / `result` |
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 | — |
216
+ | `text_delta`, `thinking_start/delta/end` | streaming | deltas; `thinking_end` carries full text |
217
+ | `message_update` | streaming (ModelClient-accumulated) | whole partial assistant message incl. partial tool JSON |
218
+ | `message_end` | response complete | full assistant content |
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 |
223
+ | `tool_running` | execution begins (after beforeToolCall) | `description` from permissions.generateDescription |
224
+ | `tool_update` | streaming tool progress | `partial` content |
225
+ | `tool_completed` / `tool_errored` | after afterToolCall | post-hook `result` / `error` text; `afterToolCall`'s `isError` can select which one fires |
226
+ | `tool_hooks` | emitted by the user-hooks mod, before the phase's terminal tool event | `phase: 'pre'\|'post'`, `lines`, `outcome` |
227
+ | `subagent_start` / `subagent_stop` | the `task` tool brackets a nested sub-agent run | `toolCallId`, `subagentType`; stop adds `tokensUsed` |
228
+ | `api_retry` | retry loop, after 3 silent attempts | `attempt`, `error`, `delayMs` |
229
+ | `compaction_start` / `compaction_done` | compaction mod | `tokensSaved` (done, only when > 0) |
230
+ | `notice` | user-facing info/warning | `level`, `message` |
231
+ | `skill_loaded` | a skill was activated by an explicit user `/name` invocation | `name` |
232
+ | `session_titled` | the auto-generated session title was persisted | `title` |
233
+ | `continuation_recovery` | a turn was auto-continued (pause/empty/length/intent) | `kind`, `attempt`, `maxAttempts` |
234
+ | `tool_input_coerced` | ModelClient rescued malformed (array/null) tool input | `rawType`, `recovered` |
235
+ | `tool_input_repaired` | repair layer healed tool input pre-execution | `rulesFired`, `hintCount`, `receivedKeys` |
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 | — |
238
+ | `run_error` | non-retryable failure | the raw `Error` |
239
+
240
+ ## Worked example — a write-quota mod
241
+
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
+
244
+ ```ts
245
+ import type {AgentMod, AgentState} from '@commandcode/harness';
246
+ import {getModState, setModState} from '@commandcode/harness';
247
+
248
+ const MOD_ID = 'write-quota';
249
+
250
+ interface WriteQuotaState {
251
+ readonly writesThisSession: number; // durable — survives resume
252
+ }
253
+
254
+ export function createWriteQuotaMod(options: {
255
+ readonly allowedRoot: string;
256
+ readonly maxWrites: number;
257
+ readonly report: (summary: string) => void;
258
+ }): AgentMod {
259
+ // Run-scoped tally lives in the closure; flushed into modState at turn end
260
+ // (beforeToolCall cannot return state — see modState conventions).
261
+ let writesThisTurn = 0;
262
+
263
+ return {
264
+ id: MOD_ID,
265
+
266
+ beforeToolCall: async ({toolName, input, state}) => {
267
+ if (toolName !== 'write_file' && toolName !== 'edit_file') return undefined;
268
+ const path = typeof input.file_path === 'string' ? input.file_path : '';
269
+ if (!path.startsWith(options.allowedRoot)) {
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.
272
+ return {
273
+ block: true,
274
+ additionalContext: `Writes outside ${options.allowedRoot} are not allowed.`,
275
+ };
276
+ }
277
+ const durable = getModState<WriteQuotaState>({state, modId: MOD_ID});
278
+ if ((durable?.writesThisSession ?? 0) + writesThisTurn >= options.maxWrites) {
279
+ return {block: true, additionalContext: 'Write quota exhausted for this session.'};
280
+ }
281
+ writesThisTurn += 1;
282
+ return undefined;
283
+ },
284
+
285
+ onTurnEnd: async ({state}) => {
286
+ if (writesThisTurn === 0) return state;
287
+ const durable = getModState<WriteQuotaState>({state, modId: MOD_ID});
288
+ const next: AgentState = setModState({
289
+ state,
290
+ modId: MOD_ID,
291
+ value: {writesThisSession: (durable?.writesThisSession ?? 0) + writesThisTurn},
292
+ });
293
+ writesThisTurn = 0;
294
+ return next; // committed via onCommit at the turn boundary
295
+ },
296
+
297
+ onRunEnd: async ({state, result}) => {
298
+ const durable = getModState<WriteQuotaState>({state, modId: MOD_ID});
299
+ options.report(
300
+ `run stopped (${result.stopReason}) after ${result.turnCount} turns; ` +
301
+ `${durable?.writesThisSession ?? 0} writes used`,
302
+ );
303
+ },
304
+ };
305
+ }
306
+ ```
307
+
308
+ Wire it as a loadable mod (`cmd.hooks({...})` with the same handlers) — or, when embedding the harness, `createHarness({..., mods: [createWriteQuotaMod({...})]})`.
@@ -0,0 +1,161 @@
1
+ <!-- GENERATED FILE — do not edit. Source: packages/docs/src/app/mods/page.mdx. Regenerate: pnpm generate:knowledge -->
2
+
3
+ # Mods
4
+
5
+ Mods are loadable plugins written against the `ModApi` — TypeScript files that Command Code discovers on disk, loads at startup, and compiles onto its agent loop. A mod can add tools the model calls, slash commands, mutating lifecycle hooks, event observers, typed-input interception, custom feed rendering, configurable flags, and model providers. Command Code's own built-in features (providers, session titling, the update notice) are written as mods against this same API.
6
+
7
+ A loadable mod IS an `AgentMod` once loaded; the mod host just builds it from a factory file instead of a code import:
8
+
9
+ ```bash
10
+ ~/.commandcode/mods/review-guard.ts one file = one mod
11
+
12
+ ▼ jiti (TypeScript, no build step)
13
+ default-export factory(cmd: ModApi)
14
+
15
+ ▼ createModHost().register(...)
16
+ hooks + addTool ──► ONE AgentMod (id `mod:<name>`), appended after the built-ins
17
+ addCommand ──► /slash dispatch + autocomplete in the TUI
18
+ on(event) ──► AgentEvent bus subscription (observe-only)
19
+ hooks.transformInput ─► typed-prompt interception (transform / consume)
20
+ addProvider ──► ProviderModule appended to the host's provider set
21
+ addRenderer ──► custom feed entries (showEntry → styled lines in the TUI)
22
+ queueMessage ──► the loop's steering / follow-up drains
23
+ ```
24
+
25
+ The factory receives the API bound as `cmd` (the product's name, VS Code style: `vscode.commands…` → `cmd.addCommand…`). Registration verbs are all `add*` and each returns a `Disposable` — call `.dispose()` to undo exactly that one registration.
26
+
27
+ This page is the whole mods surface end to end: the quick start and loading rules first, then the full **ModApi reference**, the **hooks and events** contract, the **UI surface**, **packaging and install**, and how to **verify a mod**. Jump to any section from the sidebar.
28
+
29
+ ---
30
+
31
+ ## Quick start
32
+
33
+ Create `~/.commandcode/mods/review-guard.ts`:
34
+
35
+ ```ts
36
+ import type {ModApi} from '@commandcode/harness';
37
+
38
+ export default function (cmd: ModApi) {
39
+ // Block dangerous writes (a mutating hook — see the hooks catalog).
40
+ cmd.hooks({
41
+ beforeToolCall: async ({toolName, input}) => {
42
+ if (toolName !== 'shell_command') return undefined;
43
+ const command = typeof input.command === 'string' ? input.command : '';
44
+ if (!command.includes('rm -rf')) return undefined;
45
+ const allow = await cmd.ui.confirm({title: 'Allow rm -rf?'});
46
+ return allow ? undefined : {block: true, additionalContext: 'Blocked by review-guard.'};
47
+ },
48
+ });
49
+
50
+ // A tool the model can call.
51
+ cmd.addTool({
52
+ schema: {
53
+ name: 'count_todos',
54
+ description: 'Count TODO markers in the repo',
55
+ input_schema: {type: 'object', properties: {}, required: []},
56
+ },
57
+ run: async () => {
58
+ const result = await cmd.exec({command: 'grep', args: ['-rc', 'TODO', '.']});
59
+ return {ok: true, content: [{type: 'text', text: result.stdout}]};
60
+ },
61
+ });
62
+
63
+ // A host slash command: /todos in the TUI.
64
+ cmd.addCommand({
65
+ name: 'todos',
66
+ description: 'Summarize open TODOs',
67
+ handler: () => ({prompt: 'List every TODO comment in this repo and rank by urgency.'}),
68
+ });
69
+
70
+ // Observe the event stream (never mutates — mutation is what hooks are for).
71
+ cmd.on('turn_end', () => cmd.ui.notify('turn finished'));
72
+ }
73
+ ```
74
+
75
+ It loads on the next session (or `cmd --mod ./review-guard.ts` to try it without installing). The factory may be async; jiti compiles the TypeScript at load time, so there is no build step.
76
+
77
+ ---
78
+
79
+ ## Where mods load from
80
+
81
+ | Location | Scope | Notes |
82
+ |---|---|---|
83
+ | built-in | shipped | compiled-in first-party mods (providers, titling, …); registered first |
84
+ | `~/.commandcode/mods/*.ts` | user | loose files, one mod each |
85
+ | `~/.commandcode/mods/<dir>/` | user | `package.json` manifest → `mods/` dir → `index.ts` |
86
+ | `<project>/.commandcode/mods/…` | project | same layout; loads only once the workspace is trusted |
87
+ | settings `mods.paths` | user/project | explicit files/dirs, relative to the settings scope |
88
+ | settings `mods.sources` | user/project | installed packages (see [Packaging and install](./packaging.md#packaging-and-install)) |
89
+ | `--mod <path>` | session | repeatable; loads ahead of installed, wins name collisions |
90
+
91
+ Dot-entries and `node_modules` are never scanned (the package registry lives under `mods/.registry/` precisely so discovery skips it). Duplicate mod names keep the first and warn. Disable without deleting via settings:
92
+
93
+ ```json
94
+ {"mods": {"disabled": ["review-guard"]}}
95
+ ```
96
+
97
+ ---
98
+
99
+ ## The one rule: hooks mutate, `on` observes
100
+
101
+ - **`cmd.hooks({...})`** is the only place that can change behavior — block a tool (`beforeToolCall`), rewrite a result (`afterToolCall`), add to the prompt (`appendSystemPrompt`), rewrite typed input (`transformInput`), force a finished run to keep going (`onStop`), react to session start/end (`onSessionStart`/`onSessionEnd`), or run post-turn work (`onRunEnd`). Multiple `hooks()` calls compose in registration order.
102
+ - **`cmd.on(event, ...)`** only observes — it cannot block or rewrite. Handlers are isolated (a throw becomes a `mod_error` event, never a crash).
103
+
104
+ If you find yourself wanting an `on` handler to stop a tool, you want a hook instead. The full mutating surface is in [Hooks and events](./hooks-and-events.md#hooks-and-events); the full registration/live surface is the [ModApi reference](./api.md#mod-api-reference).
105
+
106
+ ---
107
+
108
+ ## Built-in mods
109
+
110
+ Command Code's own features ride this exact API. First-party providers (`provider-anthropic` / `provider-copilot` / `provider-openai`), the update notice, and the harness-side titling and taste-learning triggers are **built-in mods**: compiled-in factories registered on the same host before any discovered mod. They are not special — they use `cmd.addProvider`, `cmd.on`, and `cmd.hooks` like any mod, appear in `cmd mods list` with source `builtin`, and honor the same disable key:
111
+
112
+ ```json
113
+ {"mods": {"disabled": ["provider-copilot", "update-notice", "titling", "learning"]}}
114
+ ```
115
+
116
+ Built-in mods always win a name collision against a discovered mod of the same name (the loader shadows the file with a warning), and they are compiled in — never jiti-loaded from a writable path, so they are not supply-chain surface. Structural harness mods (workspace, compaction, checkpoints) are load-bearing for correctness and stay unconditional; only the observer-style built-ins above are disable-able.
117
+
118
+ ---
119
+
120
+ ## Runnable examples
121
+
122
+ Runnable, single-file example mods ship with Command Code inside the bundled `mod-builder` skill (`src/skills/bundled/mod-builder/examples/`) and are validated in CI — they load through the real loader on every test run, so they never rot. Ask Command Code to "build a mod" and it reads these.
123
+
124
+ | File | Shows |
125
+ |---|---|
126
+ | `slash-command.ts` | `addCommand` — a `/command` returning `{prompt}` or `{message}` |
127
+ | `custom-tool.ts` | `addTool` — a model-callable tool with `run` + `exec` |
128
+ | `block-dangerous-commands.ts` | `hooks.beforeToolCall` — block/allow with a confirm |
129
+ | `input-shortcuts.ts` | `hooks.transformInput` — rewrite / handle typed input |
130
+ | `observe-events.ts` | `on(event)` + the cross-mod `events` bus |
131
+ | `custom-entry-renderer.ts` | `addRenderer` + `showEntry` — styled feed rows |
132
+ | `flags-and-options.ts` | `addFlag` / `getFlag` + `--mod-option` |
133
+ | `lifecycle-hooks.ts` | `hooks.onStop` (Stop) + `onSessionStart`/`onSessionEnd` + `afterToolCall` `isError` + `on('subagent_start'/'subagent_stop')` |
134
+ | `status-and-widgets.ts` | `ui.setStatus` footer segment + `ui.widget` above the editor + a timed confirm |
135
+ | `kitchen-sink.ts` | every capability in one annotated file |
136
+
137
+ ---
138
+
139
+ ## Boundaries (deliberate)
140
+
141
+ - **Hooks mutate, `on` observes.** Event handlers cannot block tools or rewrite context; that is what `cmd.hooks` is for.
142
+ - **Project mods are trust-gated** like project skills: they load only after the workspace trust prompt, because a mod is arbitrary code. User-scope and `--mod` mods always load. There is no sandbox — install packages you trust. Package installs run npm with `--ignore-scripts` (mods are jiti-loaded TypeScript; they need no build step, so lifecycle scripts are pure attack surface).
143
+ - **Print mode loads user-scope and `--mod` mods only**, with the ui bridge degraded to headless defaults (confirm → false, select/input → undefined — never auto-approved; `setStatus`/`widget` render nowhere; timed dialogs resolve `timeoutValue` immediately). Project mods stay out of headless runs because print never shows a trust prompt; pass `--dangerously-skip-permissions` to opt a repo's own mods into a headless run (CI).
144
+ - **Mod-queued messages don't echo in the feed** the way typed input does — they land in the transcript and steer the model, but the visible record is the model's response.
145
+ - **Rendering is line-based, not component-based.** `cmd.addRenderer` returns styled text lines the host prints as feed rows; mods do not mount React components into the TUI. That keeps renderers host-agnostic (the same mod renders in any future host) and a crashing renderer degrades to a warning notice, never a broken screen.
146
+ - **Reload is the `/reload` path.** Mods load once per process; `/reload` restarts the process, which re-discovers and re-imports every mod (jiti caches nothing between loads). There is no in-place hot swap.
147
+ - **Session controls stop at the harness surface.** `cmd.sessions` covers what the live harness owns (compact, tree, navigate, labels); creating/switching/forking sessions is host lifecycle, not harness state, and stays with the host UI.
148
+
149
+ Embedders wire the same machinery directly: `createModHost` + `loadMods` + `createHarness({modHost})` (or `createNodeHarnessSession({modHost})`). See `packages/harness/src/mod-host/` for the pieces and its `__tests__/` for executable examples.
150
+
151
+ ---
152
+
153
+ ## The reference sections
154
+
155
+ The rest of this page is the complete reference, in order:
156
+
157
+ - [ModApi reference](./api.md#mod-api-reference) — every field, registration verb, and live method.
158
+ - [Hooks and events](./hooks-and-events.md#hooks-and-events) — the mutating lifecycle, the `AgentEvent` catalog.
159
+ - [UI surface](./ui.md#ui-surface) — dialogs, timed dialogs, footer status, editor widgets, custom feed rendering.
160
+ - [Packaging and install](./packaging.md#packaging-and-install) — `cmd mods add`, manifests, filtering, scopes.
161
+ - [Verify a mod](./verify.md#verify-a-mod) — load it, list it, test it, ship it.
@@ -0,0 +1,63 @@
1
+ <!-- GENERATED FILE — do not edit. Source: packages/docs/src/app/mods/page.mdx. Regenerate: pnpm generate:knowledge -->
2
+
3
+ # Packaging and install
4
+
5
+ A mod starts life as a loose file in `~/.commandcode/mods/`. When it should be shared — with a team, or the world — it becomes a **package**: an npm package, a git repo, or a local directory that `cmd mods add` installs and Command Code loads on every session.
6
+
7
+ ## Install, remove, list, update
8
+
9
+ ```bash
10
+ cmd mods add npm:@team/review-mod@1.2.0 # npm registry
11
+ cmd mods add owner/repo@v1 # GitHub shorthand (any git host via git:<host>/…)
12
+ cmd mods add ./tools/local-mod # local path, referenced in place
13
+ cmd mods add -g owner/repo # user scope instead of project
14
+ cmd mods list
15
+ cmd mods update # reinstall missing, reconcile pinned refs
16
+ cmd mods remove owner/repo
17
+ ```
18
+
19
+ Sources persist in the `mods.sources` settings key (project scope writes `.commandcode/settings.json`, `-g` writes `~/.commandcode/settings.json`). Identity is version/ref-agnostic — `owner/repo@v1` and `https://github.com/owner/repo` are the same package, and a project entry shadows the same identity at user scope. Installs land in `<scope>/.commandcode/mods/.registry/{npm,git}/…`; startup never runs npm/git on its own — a configured-but-missing package is a warning pointing at `cmd mods update`.
20
+
21
+ ## What a package ships
22
+
23
+ A package declares what it ships via `package.json` — exact paths, directories, or globs (expanded against the package root; entries escaping the root are dropped):
24
+
25
+ ```json
26
+ {
27
+ "name": "@team/review-mod",
28
+ "commandcode": {"mods": ["./src/review.ts", "src/checks/*.ts"]}
29
+ }
30
+ ```
31
+
32
+ No manifest → the `mods/` convention directory → a root `index.ts`, in that order.
33
+
34
+ ## Filtering a source's mods
35
+
36
+ A `mods.sources` entry can be the object form to load only part of a package:
37
+
38
+ ```json
39
+ {
40
+ "mods": {
41
+ "sources": [
42
+ {"source": "owner/review-pack", "mods": ["review/*.ts", "!review/slow.ts"]}
43
+ ]
44
+ }
45
+ }
46
+ ```
47
+
48
+ Four pattern kinds, applied in precedence order: `-path` force-exclude (exact, beats everything) → `+path` force-include (exact, restores what globs dropped) → `!glob` exclude → plain-glob include (when any includes exist, an entry must match one). Patterns match the entry's package-relative path or its mod name. `cmd mods add` / `remove` preserve hand-written object entries — they never flatten your filters.
49
+
50
+ ## Disabling without deleting
51
+
52
+ ```json
53
+ {"mods": {"disabled": ["review-guard"]}}
54
+ ```
55
+
56
+ Works for discovered files, installed packages, and the disable-able built-ins (`provider-copilot`, `update-notice`, `titling`, `learning`).
57
+
58
+ ## Trust and safety
59
+
60
+ - **There is no sandbox** — a mod is arbitrary code; install packages you trust.
61
+ - **Project mods are trust-gated** like project skills: they load only after the workspace trust prompt. User-scope and `--mod` mods always load.
62
+ - **Package installs run npm with `--ignore-scripts`** — mods are jiti-loaded TypeScript; they need no build step, so lifecycle scripts are pure attack surface.
63
+ - **Print mode loads user-scope and `--mod` mods only.** Project mods stay out of headless runs because print never shows a trust prompt; pass `--dangerously-skip-permissions` to opt a repo's own mods into a headless run (CI).
@@ -0,0 +1,63 @@
1
+ <!-- GENERATED FILE — do not edit. Source: packages/docs/src/app/mods/page.mdx. Regenerate: pnpm generate:knowledge -->
2
+
3
+ # UI surface
4
+
5
+ Everything a mod can put on screen rides `cmd.ui`, `cmd.addRenderer`, and `cmd.showEntry`. All of it is line-based and host-agnostic: the TUI wires real rendering, headless runs degrade to deterministic defaults, and a crashing renderer becomes a warning — never a broken screen.
6
+
7
+ ## Notifications and dialogs
8
+
9
+ - `cmd.ui.notify(message)` — a `notice` feed row.
10
+ - `cmd.ui.confirm({title})` / `cmd.ui.select({title, options})` / `cmd.ui.input({title})` — the Interaction question modal in the TUI. Headless, each resolves its deterministic default: confirm → `false`, select/input → `undefined` — never auto-approved.
11
+
12
+ ### Timed dialogs
13
+
14
+ Each dialog accepts `{timeoutMs, timeoutValue}`: after `timeoutMs` the dialog auto-resolves `timeoutValue` (default: the dialog's headless default). The TUI shows a visible countdown and dismisses the modal at the deadline; on a TIMED dialog a dismissal without an answer (auto-dismiss or Esc) also resolves `timeoutValue`. Headless, a timed dialog resolves `timeoutValue` immediately — it never blocks a print run.
15
+
16
+ ```ts
17
+ const proceed = await cmd.ui.confirm({
18
+ title: 'Deploy to staging?',
19
+ timeoutMs: 15_000,
20
+ timeoutValue: true, // no answer in 15s = go ahead
21
+ });
22
+ ```
23
+
24
+ ## Footer status segments
25
+
26
+ `cmd.ui.setStatus(text | null)` — a persistent per-mod segment in the TUI footer (under the input panel). One segment per mod: a new call replaces the text, `null` clears it, the returned `Disposable` clears it too. Segments from multiple mods concatenate in load order. Headless: renders nowhere (no-op).
27
+
28
+ ## Editor widgets
29
+
30
+ `cmd.ui.widget({placement: 'above-editor' | 'below-editor', render: () => lines})` — a line-based widget the TUI renders around the input panel. Same verbatim-lines contract as `addRenderer` (style with ansi); `render` re-runs on every repaint, and `cmd.ui.refreshWidgets()` requests a repaint after your data changes. A throwing render is skipped (reported as a `mod_error`) so siblings keep rendering. Headless: no-op.
31
+
32
+ ```ts
33
+ let failing = 0;
34
+ cmd.ui.widget({
35
+ placement: 'above-editor',
36
+ render: () => [failing > 0 ? `✗ ${failing} tests failing` : '✓ tests green'],
37
+ });
38
+ cmd.on('tool_completed', () => {
39
+ failing = readFailingCount();
40
+ cmd.ui.refreshWidgets();
41
+ });
42
+ ```
43
+
44
+ ## Custom feed rendering
45
+
46
+ - `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.
47
+ - `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.
48
+
49
+ Rendering is deliberately **line-based, not component-based**: mods return styled text lines, never React components. That keeps renderers host-agnostic (the same mod renders in any future host) and a crashing renderer degrades to a warning notice, never a broken screen.
50
+
51
+ ## Headless behavior at a glance
52
+
53
+ | Surface | Interactive TUI | Headless (`-p`) |
54
+ |---|---|---|
55
+ | `notify` | notice feed row | printed notice |
56
+ | `confirm` | modal | resolves `false` |
57
+ | `select` / `input` | modal | resolves `undefined` |
58
+ | timed dialog | countdown, auto-resolve at deadline | resolves `timeoutValue` immediately |
59
+ | `setStatus` | footer segment | no-op |
60
+ | `widget` | rendered around the editor | no-op |
61
+ | `showEntry` | rendered feed row | dropped |
62
+
63
+ The `status-and-widgets.ts` bundled example exercises all of this in one file — see [Runnable examples](./overview.md#runnable-examples).