@theokit/agents 13.0.0-next.8 → 13.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 (52) hide show
  1. package/CHANGELOG.md +1707 -0
  2. package/README.md +31 -0
  3. package/dist/{agent-compiler-C2jIZ4CZ.d.ts → agent-compiler-B0hb6HCo.d.ts} +182 -26
  4. package/dist/ask.d.ts +1 -0
  5. package/dist/auth.d.ts +148 -1
  6. package/dist/auth.js +36 -0
  7. package/dist/auth.js.map +1 -1
  8. package/dist/{bridge-entry-BJ86tO6e.d.ts → bridge-entry-DBkqwe6b.d.ts} +72 -6
  9. package/dist/bridge.d.ts +6 -4
  10. package/dist/bridge.js +9 -4
  11. package/dist/{chunk-3GQJ2BHT.js → chunk-G7QBDGZ4.js} +93 -18
  12. package/dist/chunk-G7QBDGZ4.js.map +1 -0
  13. package/dist/chunk-HGVT4VCE.js +97 -0
  14. package/dist/chunk-HGVT4VCE.js.map +1 -0
  15. package/dist/{chunk-NZTLBLHB.js → chunk-KTQID5V3.js} +560 -342
  16. package/dist/chunk-KTQID5V3.js.map +1 -0
  17. package/dist/chunk-M5J3Q6YC.js +17 -0
  18. package/dist/chunk-M5J3Q6YC.js.map +1 -0
  19. package/dist/{chunk-RKWCXVYG.js → chunk-MJ6FRILJ.js} +67 -5
  20. package/dist/chunk-MJ6FRILJ.js.map +1 -0
  21. package/dist/{chunk-X4IGZHOV.js → chunk-PMMOOXR6.js} +43 -8
  22. package/dist/chunk-PMMOOXR6.js.map +1 -0
  23. package/dist/chunk-U72XTMYB.js +7 -0
  24. package/dist/chunk-U72XTMYB.js.map +1 -0
  25. package/dist/client-react.d.ts +1 -0
  26. package/dist/client.d.ts +1 -0
  27. package/dist/config.d.ts +163 -36
  28. package/dist/config.js +119 -7
  29. package/dist/config.js.map +1 -1
  30. package/dist/{define-agent-WYnUlWaH.d.ts → define-agent-DhwNmdej.d.ts} +28 -153
  31. package/dist/{delegation-scoring-BrBQXvIp.d.ts → delegation-scoring-BzJEheml.d.ts} +1 -1
  32. package/dist/hooks.d.ts +0 -32
  33. package/dist/hooks.js +42 -14
  34. package/dist/hooks.js.map +1 -1
  35. package/dist/index.d.ts +191 -17
  36. package/dist/index.js +43 -5
  37. package/dist/index.js.map +1 -1
  38. package/dist/sandbox.js.map +1 -1
  39. package/dist/setting-sources-gate-DFu51i50.d.ts +278 -0
  40. package/dist/testing.d.ts +5 -2
  41. package/dist/testing.js +1 -1
  42. package/dist/tools.d.ts +4 -2
  43. package/dist/tools.js +22 -4
  44. package/dist/tools.js.map +1 -1
  45. package/dist/usage.d.ts +30 -0
  46. package/dist/usage.js +3 -0
  47. package/dist/usage.js.map +1 -1
  48. package/package.json +3 -3
  49. package/dist/chunk-3GQJ2BHT.js.map +0 -1
  50. package/dist/chunk-NZTLBLHB.js.map +0 -1
  51. package/dist/chunk-RKWCXVYG.js.map +0 -1
  52. package/dist/chunk-X4IGZHOV.js.map +0 -1
package/CHANGELOG.md CHANGED
@@ -1,5 +1,1712 @@
1
1
  # @theokit/agents
2
2
 
3
+ ## 13.0.0
4
+
5
+ ### Minor Changes
6
+
7
+ - c59abf6: A foreign configuration root can be imported in part
8
+
9
+ `resolveCompatSources` returned the bare literal `'claude-code'`, which the SDK reads as "import
10
+ every surface" — hooks, plugins, skills, subagents. `settingSources.claudeCode.import` now names the
11
+ surfaces, and the resolved value carries them.
12
+
13
+ The distinction is the reason the grant exists: `.claude/` usually arrives with the clone and its
14
+ `hooks.json` executes shell, so "take the skills, refuse the hooks" is the ordinary thing to want,
15
+ and the only choices were all of it or none of it.
16
+
17
+ Absent `import` still means the whole root, so nothing existing changes. An EMPTY list is refused
18
+ rather than guessed: "no surfaces" and "unset, so all of them" are both defensible readings of `[]`,
19
+ they differ by whether shell executes, and picking one would settle a security question by
20
+ convention.
21
+
22
+ `CompatSurface` and `ResolvedCompatSource` are declared here rather than imported from the SDK, for
23
+ the reason already recorded beside the literal: they do not exist in `@theokit/sdk@4.52.1`, this
24
+ package's declared floor.
25
+
26
+ Closes usetheokit/theokit#686.
27
+
28
+ - ca44ee7: A narrowed foreign root can name `commands`
29
+
30
+ `settingSources.claudeCode.import` narrows the foreign root to named surfaces, and `CompatSurface`
31
+ listed four of them — `hooks`, `plugins`, `skills`, `subagents`. It fed a fifth: this package loads
32
+ `<projectDir>/.claude/commands/*.md` itself, outside the SDK's `compatSources` path. The name was
33
+ missing from the vocabulary, so a consumer could neither ask for that directory nor be told it had
34
+ gone unread, and `loadCustomCommands` tested `sources.includes('claude-code')` — string equality
35
+ against a union whose narrowed member is an object, so every narrowed list read as _not declared_.
36
+
37
+ `CompatSurface` now includes `'commands'`, and the loader understands both shapes: the bare source
38
+ name grants every surface the root feeds, the narrowed form grants the ones it names.
39
+
40
+ Purely additive — before this, no narrowed list reached the directory at all, so nothing that works
41
+ today stops working. A narrowed list that wants foreign commands adds `'commands'` to `import`.
42
+
43
+ An enumeration used to narrow a root must cover every surface that root feeds; otherwise it is not
44
+ a narrowing but an undeclared drop. Reported as usetheokit/theokit#704, found by measuring a claim
45
+ in a consumer's adoption report rather than by any test here — the fifth time a gap in this
46
+ package's public surface was invisible from inside it.
47
+
48
+ - dcb461f: `.plugins()` refuses a filesystem-bundle entry instead of accepting and ignoring it, and its parameter says what belongs there.
49
+
50
+ **BREAKING:** `plugins` was `readonly unknown[]` and is now `readonly CodePlugin[]`. Anything that
51
+ already passed `{ name, register }` objects is unaffected.
52
+
53
+ The word `plugins` appears in both vocabularies and means different things. This layer's are CODE
54
+ objects registered with the runtime's lifecycle seam; the Claude Code format's are filesystem
55
+ BUNDLES. A consumer who read the other product's documentation passed
56
+ `[{ type: 'local', path: './p' }]`, the compiler accepted it because the parameter was `unknown[]`,
57
+ and the agent ran with the plugin absent — **the typecheck that should have caught it was what let it
58
+ through**.
59
+
60
+ **The survey's "not implemented on either side" was too strong, and the correction matters for the
61
+ error message.** A bundle _directory_ is discovered: `pluginBundleDirs` reads `.claude/plugins/*` and
62
+ `.theokit/plugins/*`, and the `skills/` and `agents/` subdirectories of each are loaded. What is
63
+ absent is the manifest (`.claude-plugin/plugin.json`), marketplaces, and the format's other
64
+ contributions — hooks, MCP servers, output styles, LSP servers.
65
+
66
+ So a path-shaped entry is not refused because bundles are unsupported. It is refused because this
67
+ _parameter_ is not how a bundle is declared, and the message says where one goes and what a bundle
68
+ does and does not contribute. A refusal that does not name where the capability lives sends the
69
+ reader to a changelog.
70
+
71
+ The check runs at the projection, the single point every authoring path converges on. Putting it on
72
+ the builder method would miss `defineAgent({ plugins })` and the capability — which is how the shape
73
+ reached the runtime unexamined in the first place.
74
+
75
+ A plain wrong value gets a different message from a bundle reference: a string is not a bundle, and
76
+ pointing its author at a `plugins/` directory would send them somewhere that cannot help.
77
+
78
+ - 6b07960: New `grantGate(store, classify)` in `@theokit/agents/auth` — the supported way to put a
79
+ `PermissionStore` in force.
80
+
81
+ The store shipped with a careful grant key, a "deny by default, always" docblock and no reader.
82
+ Measured: `isGranted` had zero callers outside its own unit test, and `PermissionStore` appeared in
83
+ zero files across six sibling repositories. An operator reading `.theokit/tool-permissions.json` to
84
+ learn what an agent may run was reading a control that was not in force — a grant and its revocation
85
+ produced identical behaviour.
86
+
87
+ `grantGate` adapts the store to `pre_tool_call`, which is documented as the only hook with veto
88
+ power and runs before the tool by construction, so a refusal is a refusal before the side effect. No
89
+ new gate and no framework wiring: nothing is enforced unless a consumer attaches the handler, and an
90
+ agent that does not is unaffected.
91
+
92
+ `classify` returns `{ governed: true, query }` **or** `{ governed: false }` — a tagged union whose
93
+ BOTH arms carry the discriminant. A bare `undefined` would say "this tool needs no permission" and
94
+ "I forgot this tool" in the same word, and on a security gate the second must not silently pass.
95
+
96
+ The discriminant is on both arms because the first shape — discriminated by whether a `governed` KEY
97
+ was present — **failed open**: a consumer writing a policy record `{ governed: true, scope }` and
98
+ spreading it into the query got the tool waved through, and so did `governed: undefined`. TypeScript
99
+ does not stop that; excess properties pass freely through a variable or a spread. Fail-closed is now
100
+ measured across `true` / `undefined` / `false` / `null`, and only `false` passes.
101
+
102
+ Named `grantGate`, not `permissionGate`, because `@theokit/sdk` already exports `PermissionGate`,
103
+ `PermissionGateContext`, `PermissionGateDecision`, `PermissionEngine` and `PermissionPlugin`. The
104
+ rename also says something true: this gates on a standing grant the operator made, and the SDK's
105
+ permission engine is a separate system — neither satisfies the other.
106
+
107
+ A classifier that throws DENIES, naming the throw, rather than ending the turn. A corrupt store
108
+ denies with a message that says so — "the permission store could not be read, so no grant applies" —
109
+ distinct from "no standing grant matches", so an operator can tell the two apart.
110
+
111
+ The read error's own text is deliberately NOT in that message: the veto travels to the model, and
112
+ `lastReadError.message` carries the absolute store path and its file mode. It stays on
113
+ `store.lastReadError` for the operator, which is where the actionable remedy (`chmod 600 …`) lives.
114
+ The scope IS still interpolated, so this narrows the exposure rather than eliminating it.
115
+
116
+ **Composing with a `pre_tool_call` you already have**: the field is singular, so assigning the gate
117
+ over an existing handler loses one of the two silently. Compose explicitly —
118
+ `async (ctx) => (await gate(ctx)) ?? (await mine(ctx))`; first veto wins.
119
+
120
+ `PermissionStore`'s docblock now states that the class enforces nothing on its own, and records the
121
+ precedence among the surfaces IN THIS PACKAGE that can refuse a tool — saying plainly that the list
122
+ is not exhaustive, because `@theokit/sdk` has its own permission system that neither knows about
123
+ these nor is known by them.
124
+
125
+ - a7e35bb: A guardrail returning `action: 'redact'` with no replacement `text` now throws
126
+ `MalformedGuardrailResultError` instead of silently redacting nothing.
127
+
128
+ `GuardrailResult.text` is optional, so such a guard compiles and reads like a working one. The
129
+ pipeline tested `r.text !== undefined` and moved on, so the caller received the original text and
130
+ believed a guard had run on it — the operator believing a protection is in place when none is.
131
+
132
+ **This is a behaviour change.** A guard relying on the previous no-op will now throw. That is
133
+ deliberate: the alternative is unredacted output reaching a model because a guard was written wrong.
134
+ `text: ''` is unaffected and always was a real redaction — a guard choosing to erase everything.
135
+
136
+ `MalformedGuardrailResultError` is exported from `@theokit/agents`, carries the guard's name and the
137
+ phase, and is not retryable.
138
+
139
+ This also reaches the streaming path (`moderateOutputStream`), which shares the same pipeline: a
140
+ malformed guard there now throws where it previously continued.
141
+
142
+ - dcb461f: `blockAppliesTo(block, filePath)` is exported from `@theokit/agents/config`.
143
+
144
+ `InstructionBlock.scopes` carries the `paths:` frontmatter, and the package shipped no way to apply
145
+ it — no glob matcher existed anywhere in the tree. A consumer who wanted to honour a scope had to
146
+ invent the semantics, and two consumers would invent two.
147
+
148
+ The field's own docblock names the consequence of getting it wrong: "A consumer rendering the block
149
+ would then apply a rule written for one subtree EVERYWHERE — the one frontmatter failure with a
150
+ consequence, and a silent one."
151
+
152
+ Delegating the **decision** to the product is deliberate and unchanged: a consumer still chooses
153
+ whether to filter. What changes is that it now has the **means**.
154
+
155
+ `scopesUnreadable` answers `false` for every path. That is the fail-closed half the flag was invented
156
+ for — a `paths:` key that was declared and yielded nothing must not read as "no scope declared",
157
+ because those two are indistinguishable in `scopes` alone and only one of them is safe to publish
158
+ everywhere.
159
+
160
+ **Supported:** `**` (crosses separators), `*` (does not), `?` (one character) — the three the SDK's
161
+ own rule activation implements. **Not supported:** brace expansion `{a,b}` and character classes
162
+ `[abc]`, which match literally and therefore almost certainly not at all. That floor is deliberate:
163
+ three wildcards are a dozen lines, and a glob dependency would carry a transitive tree into a package
164
+ with three direct dependencies. Needing the fuller grammar is a decision to make out loud.
165
+
166
+ - dcb461f: A shared session store can be declared through the authoring surface. Serverless and multi-pod were unreachable without it.
167
+
168
+ The SDK takes `local.sessionStore` — a Postgres / Redis / KV / durable-object store used as the
169
+ primary session store and resume source, for deployments where the filesystem is ephemeral or the
170
+ next request lands on a different host. Measured: `sessionStore` returned 0 files in this layer.
171
+
172
+ This layer's stated doctrine, repeated across roughly eight docblocks, is that a consumer should not
173
+ import `@theokit/sdk` directly. With no authoring surface for the store, the only way to reach it was
174
+ to do exactly that — so **the doctrine and the capability disagreed, and the consumer paid**.
175
+
176
+ `SessionStoreCapability` writes the field and `assembleM8CreateOptions` projects it. `SessionStore`
177
+ crosses the barrel with it: forwarding a capability whose type cannot be named does not close the
178
+ gap, and the item says so.
179
+
180
+ The block is written only when a store was declared. An unconditional write creates `local` for every
181
+ agent — a claim about setting sources and a cwd that no author made. The first version of the control
182
+ asserted only `local?.sessionStore`, which passes for the unconditional write too since the key lands
183
+ as `undefined` either way; it asserts the block now.
184
+
185
+ Two existing gates fired on this change and were right both times: the compile-time waist-field
186
+ exhaustiveness check demanded the new field be classified, and the derived coverage test demanded the
187
+ capability join the "everything switched on" fixture. Neither needed to be found by review.
188
+
189
+ - 5211721: `moderateOutputStream` now delivers the redacted text to the client instead of computing it and
190
+ replaying the original events.
191
+
192
+ It called `runOutputGuards` and discarded the return value, so a guard that redacted correctly had
193
+ its work thrown away: measured against the built artifact, a guard returning `[REDACTED]` delivered
194
+ `sk-abc123`. Only `block` reached the client honestly.
195
+
196
+ **Signature change**: `moderateOutputStream` takes a fourth argument,
197
+ `rebuildText: (text: string, replaced: E) => E`, which builds one event carrying the
198
+ moderated text, given the text-carrying event it replaces. It is required rather than optional —
199
+ optional would let the function compute a redaction it cannot apply, which is the defect being
200
+ removed. Only the caller knows how to construct its own events.
201
+
202
+ **`extractText` MUST match exactly one event kind.** When it matches several, they COLLAPSE INTO
203
+ ONE — measured: `[thinking('CoT: the key is sk-abc'), message(' Here you go.')]` yields a single
204
+ `message` reading `"CoT: the key is [R] Here you go."`, with no `thinking` event surviving. A
205
+ consumer who wants reasoning moderated runs a SECOND pass over that kind rather than widening one
206
+ extractor.
207
+
208
+ `replaced` does not prevent that collapse, and an earlier draft of this entry said it did. What it
209
+ buys is narrower: the surviving event keeps the KIND and metadata of the text-carrying event it
210
+ replaces, instead of being rebuilt from the text alone. `replaced` is ALWAYS the event being replaced.
211
+ It was typed `E | undefined` for a case that cannot happen — a stream where no event carried text
212
+ returns from the absence check before the guards run, so `rebuildText` is not reached at all. It is
213
+ also never a non-text event, because a caller spreading one would emit a duplicate of it.
214
+
215
+ **Second signature change**: a fifth argument, `rebuildResult: (text, result) => R`, applies the
216
+ moderated text to the generator's RETURN value. A stream has two channels and the first release of
217
+ this fix moderated one: the events were redacted while `step.value` — the aggregate `run()` returns
218
+ — still carried the original text. Measured: the guard computed `"the key is [R]"` and
219
+ `run().response` was `"the key is sk-abc123"`, so **`run()`, the primary non-streaming API, kept
220
+ delivering the secret**. That was this fix's own defect one channel over. Required for the same
221
+ reason `rebuildText` is; passed rather than re-derived, because re-running the guards on the
222
+ aggregate would apply a non-idempotent guard twice.
223
+
224
+ When the text is unchanged, the buffered events are replayed verbatim as before. When it changed,
225
+ the **last** text-carrying event is REPLACED by a newly built event carrying the whole moderated
226
+ string, and the earlier text events are dropped. Events carrying no text are never dropped. Note
227
+ "replaced", not "modified": any non-text payload the surviving event carried is lost, as is that of
228
+ the dropped ones — a consumer whose text events carry per-event metadata should moderate one kind
229
+ only, or rebuild from `replaced`. An event whose extracted text is the EMPTY STRING is still
230
+ text-carrying and can be the one replaced.
231
+
232
+ **Known consequence:** when text events straddle a non-text event, their relative order does not
233
+ survive a redaction. Given `text('tok ') , tool_call , text('sk-abc')` the client now receives
234
+ `tool_call , text('tok [R]')` — text that preceded the tool call follows it. Landing on the last
235
+ text-carrying event keeps a trailing terminator in place and keeps any completion claim after the
236
+ work that produced it; what it cannot keep is the interleaving, because the redaction is about the
237
+ whole string and the boundaries are gone by the time it exists. A test pins this so it is found
238
+ here rather than in a transcript that stopped making sense.
239
+
240
+ A guard that rewrites **unconditionally** — a disclaimer appender, a trim, an NFC normaliser —
241
+ takes this path on every stream that DOES carry text. The cost is not proportional to how much the
242
+ guard changed. It does NOT add a text event to a round that produced none — this entry claimed so,
243
+ and the absence check refuses it: a tool-only round yields its tool call and nothing else.
244
+
245
+ - dcb461f: The served handle offers a schema-validated, tool-using run. The capability existed and the door did not.
246
+
247
+ **The survey's conclusion did not survive measurement.** It said "structured output cannot be
248
+ combined with tools", on evidence that `outputFormat`, `structuredOutput`, `outputSchema` and
249
+ `responseFormat` returned 0 files here, and that `generateObject`'s options carry no `tools` field.
250
+ Both facts are true; the conclusion is not.
251
+
252
+ `generateObject` is the **toolless path by design** — it builds a transient agent whose only tool is
253
+ the synthetic output tool. The tool-using path is `agent.generate(input, { output })`, which runs the
254
+ agent's normal tool loop, the user's tools first, and then coerces the final answer into a Zod schema.
255
+ It exists on the published SDK's agent.
256
+
257
+ **The real gap was this layer's.** `SdkAgentHandle` — what is served to ACP, the delegation surfaces
258
+ and the autonomous loop — declared `send` and `dispose` and not `generate`, so a consumer of
259
+ `@theokit/agents` could reach it only by importing `@theokit/sdk` directly. Same shape as the session
260
+ store: the capability existed, the door did not.
261
+
262
+ It is optional on the interface, because a caller-injected handle (tests, a custom transport) need not
263
+ implement it, and requiring it would break every such double to add a method most never call.
264
+
265
+ **Both wrappers forward it**, and that is the half that actually breaks: `withStepCeiling` and
266
+ `withGuardrails` each construct a new object, so every method they do not name disappears — the
267
+ consumer meets the loss at runtime as "the handle has no `generate`", after the types said it had
268
+ one. Each forward is pinned by its own mutation.
269
+
270
+ `generate` is **not** put through the output guards, stated rather than assumed. It resolves to a
271
+ validated object and those guards moderate text; running them over a serialised object would moderate
272
+ a shape no guard was written against. Guarding the structured path needs its own decision about what
273
+ a redaction means to a schema, and inventing one here would be the gate that reports without gating
274
+ this module already refuses to build.
275
+
276
+ - dcb461f: A usage record can carry the cache split that justifies its cost, and usage can be narrowed by model.
277
+
278
+ **Two of the item's three claims did not survive measurement, and the correction is the point.**
279
+
280
+ The survey reported "cost is computed without cache-token accounting, so it is systematically wrong",
281
+ on evidence that `cacheCreation`, `cache_read`, `modelUsage` and `total_cost_usd` returned 0 files.
282
+ Those are the _wire_ spellings. The real field names are `cacheReadTokens` and `cacheWriteTokens`,
283
+ carried in five files of this layer — `DoneEvent.usage` has had them since V4-O.
284
+
285
+ Nothing in this layer _computes_ cost either: `costUsd` is supplied by the caller on the record, and
286
+ the storage only sums what it is given.
287
+
288
+ **What was genuinely missing is narrower and still worth closing.** `UsageRecord.tokens` was
289
+ `{ input, output }`, so a record stated a cost it could not explain: cached reads are billed at a
290
+ fraction of input tokens, and two runs with identical `input` totals and different cache ratios cost
291
+ different amounts. The audit trail showed the figure and not the reason. `cacheRead` and `cacheWrite`
292
+ are optional and **absent rather than `0`** when a provider does not report — "not reported" and
293
+ "reported as zero" are different facts, and defaulting would claim a measurement nobody made.
294
+
295
+ **The per-model breakdown was absent at the query surface**, not in the data: every record carries
296
+ `model`, and `UsageQuery` had no way to ask. It is a query rather than a second shape on
297
+ `UsageResult`, because a breakdown returned alongside a total is two numbers that can disagree; one
298
+ source asked a different question cannot.
299
+
300
+ One implementation note worth keeping. The model predicate first carried an explicit
301
+ `kind === 'tool'` exclusion, and a mutation proved it dead: `getUsage` already drops tool records
302
+ before summing, so inverting the condition left every assertion green. It was removed rather than
303
+ kept — a conjunct that cannot change an answer reads as a second condition somebody needed, which is
304
+ how a dead branch survives review.
305
+
306
+ - 1202e86: A failed injected command now aborts the expansion instead of building a prompt out of its own error message, and a fenced command block is refused instead of ignored.
307
+
308
+ **BREAKING:** `expandCommandTemplate` used to promise it never throws. It now throws in exactly two
309
+ cases, both of which produce NO prompt rather than a wrong one.
310
+
311
+ **The failure path.** The spec is explicit — "A failed command aborts the entire skill invocation…
312
+ Claude never sees the skill content for that invocation." What happened instead was substitution plus
313
+ a warning: a command whose `gh pr diff` failed produced a prompt containing
314
+ `fatal: not a git repository`, handed to a model that had been asked to review a diff, which answered
315
+ as though that _were_ the diff.
316
+
317
+ The previous behaviour was decided, and the reasoning it was decided against is worth keeping:
318
+ substituting SILENCE renders as a command that ran and returned nothing, which the model cannot
319
+ detect. That is correct, and it weighed the wrong two options. Substituting the ERROR is worse than
320
+ silence, not better — `fatal:` reads as prose, while silence at least leaves a gap. The third option
321
+ is the one the spec names, and the only one where the caller learns anything.
322
+
323
+ **A missing `@file` is still a warning**, deliberately. The line is what the template CAUSED versus
324
+ what it merely POINTED at: only the first can hand the model a plausible lie.
325
+
326
+ **The fenced form.** A ` ```! ` block containing real commands came back byte-identical — the
327
+ model received the command text as markdown, nothing ran, and nothing said so. It is now refused with
328
+ a message naming the inline form, rather than implemented: "run a multi-line block" has real
329
+ unanswered semantics (each line a command, or one script? which shell? what is the exit status of
330
+ four lines?), and inventing them would ship behaviour under a name that promises the spec's. The
331
+ detector is anchored to a line of its own, so an ordinary ` ```bash ` block is untouched.
332
+
333
+ Callers that relied on best-effort expansion should catch `ConfigurationError` with code
334
+ `command_template_segment_failed` or `command_template_fenced_block`.
335
+
336
+ - 1202e86: An operator can stop a skill body from running shell.
337
+
338
+ A skill is a markdown file a repository can carry, and `` !`command` `` in its body executes at
339
+ expansion time. Measured: `disableSkillShellExecution` returned 0 files here and 0 in the SDK dist,
340
+ against a control of 31 on the word `hooks`. The only way to decline was to stop reading skills at
341
+ all.
342
+
343
+ **The switch lives inside the expander, and that inverts an invariant on purpose.** The module's rule
344
+ was "never spawns anything and never opens a file — `shell` and `readFile` are injected, [because]
345
+ the trust decision is the caller's, not this module's." That _reason_ is what the operator-tier
346
+ decision overturned (README § "Who decides policy"): the caller decides everything the operator has
347
+ not spoken about. Leaving the check to the caller would have made it a constructor argument again,
348
+ which is the shape the decision replaced. The module still spawns nothing — it refuses _before_
349
+ calling the injected `shell`, and that ordering is pinned by a mutation test.
350
+
351
+ **This package reads the policy file itself**, rather than importing `@theokit/sdk`'s reader. It
352
+ ships from a separate repository against a published SDK (`^4.52.1 || ^5.0.0`), so a symbol added to
353
+ that package's source is not importable here until it is released — and a control that only works
354
+ after somebody else cuts a release is a control nobody can reach. One FORMAT (Claude Code's path and
355
+ key names) is the contract; two readers that release independently is a consequence of the repository
356
+ boundary, stated rather than hidden.
357
+
358
+ **What this does not pretend:** `expandCommandTemplate` has zero production callers, measured across
359
+ `theokit`, `theokit-sdk`, `theokit-tui`, `theokit-studio` and `theokit-hub`. The hazard has no live
360
+ path today. Enforcing at a call site that does not exist would have been the unreachable-control
361
+ failure; enforcing here means the switch bites the moment a caller appears rather than being
362
+ remembered then.
363
+
364
+ A value the reader cannot understand — `"disableSkillShellExecution": "true"`, a string — is reported
365
+ and ignored, never coerced. Accepting it by truthiness would make `"false"` forbid shell too.
366
+
367
+ - 1202e86: An operator can decide which MCP servers from a project `.mcp.json` may start. Nothing decided before.
368
+
369
+ Measured: `allowedMcpServers`, `deniedMcpServers`, `allowManagedMcpServersOnly`,
370
+ `enabledMcpjsonServers`, `disabledMcpjsonServers` and `enableAllProjectMcpServers` all returned 0
371
+ files, against a control of 31 on the word `hooks`, while `loadMcpJson` does read `<cwd>/.mcp.json`.
372
+
373
+ **The loader shipped and the gate did not, which is worse than having neither** — a consumer who
374
+ wanted the convenience of the file inherited the exposure without being offered the control.
375
+
376
+ **The default is now decided rather than inherited.** It stays "every declared server starts",
377
+ because the file is the project's own declaration and refusing it outright would break every existing
378
+ consumer to protect against something they wrote themselves. What changed is that an operator can
379
+ narrow it: `deniedMcpServers` removes named servers, and `allowedMcpServers` — once present — makes
380
+ the list exhaustive. An absent allow list means "no allow list", not "allow nothing"; an empty array
381
+ is a real decision and refuses everything.
382
+
383
+ **Deny wins over allow.** A server named in both is a contradiction, and the safe reading of a
384
+ contradiction is the restrictive one — resolving it the other way would let an allow entry re-enable
385
+ something an operator explicitly refused.
386
+
387
+ A refused server is **named** in the warning channel, like every other refusal in this loader: a
388
+ server that silently does not start is indistinguishable from one that started and has no tools.
389
+
390
+ One trust vocabulary, not two. This composes with the same `managed-settings.json` the hook and
391
+ skill-shell controls read, with the same "the project cannot switch it off" precedence and the same
392
+ report-never-carry rule. `TrustPosture` is unchanged and stays what it is — the gate over whether a
393
+ directory's config is read at all; this is the gate over which servers inside an admitted file may
394
+ start.
395
+
396
+ A list value of the wrong shape — `"deniedMcpServers": "postgres"` — is reported and ignored.
397
+ Coercing a bare string would make every character a server name; ignoring it silently would leave the
398
+ organisation believing a server is blocked.
399
+
400
+ - 98b2565: `resolveSettingSources` now returns `readonly GatedSettingSource[]`, and
401
+ `CompiledAgentOptions.settingSources` takes that type — so a setting root no `TrustPosture`
402
+ authorised no longer fits the field.
403
+
404
+ `define-agent.ts` claimed that field "can only ever hold roots that some posture authorized".
405
+ Measured against the emitted `.d.ts`: `setOnce(draft, 'settingSources', ['mdm','team','user','plugins'], 'cap')`
406
+ typechecked **cast-free**. Writing a `Capability` is the documented way to extend the builder, and a
407
+ capability writes the draft directly — so the gate was reachable around, for `project`, the root it
408
+ exists to protect.
409
+
410
+ A brand rather than a runtime check, because the obvious runtime check does not work: reading
411
+ `draft.provenance` to refuse a capability's write would also refuse the LEGITIMATE builder path,
412
+ which writes through `setOnce` too. What differs is where the value came from, and that is what a
413
+ brand carries.
414
+
415
+ **It refuses the accident, not the determined caller** — `as never` defeats it, like every brand.
416
+ Saying so is the point: the comment it replaces claimed an invariant nothing enforced.
417
+
418
+ **New: `settingSources.plugins`**, taking the same `ProjectSettingsGrant` as `project`.
419
+ `PluginsManager.refresh` loads executable bundles from the same cwd-controlled tree, usually
420
+ arriving with the clone, so it gets the same gate and not a weaker one. This is the root the SDK
421
+ genuinely reads and the facade withheld.
422
+
423
+ `team` and `mdm` stay absent, and that is the item's original premise dying under measurement: the
424
+ SDK never reads them — `includesSetting` is called with exactly `"project"` and `"plugins"` — so
425
+ forwarding them would be a capability in the type and nothing at runtime.
426
+
427
+ **Migration**: a consumer constructing `CompiledAgentOptions` by hand must build roots through
428
+ `resolveSettingSources` instead of a string array. That is the supported construction and always was.
429
+
430
+ **Also: a narrowed `claudeCode.import` is now REFUSED on an SDK that cannot read it.**
431
+
432
+ That field's docblock said the narrowed form was "refused at resolve time" below `@theokit/sdk`
433
+ 5.4.0. Nothing read a version for it — the only checks in this layer are the hook gate (a different
434
+ option) and a `compatSources` warning that returns silently for any major ≥ 5. So on
435
+ 5.0.0 ≤ SDK < 5.4.0, inside this package's declared `^4.52.1 || ^5.0.0`, a narrowed `import` was
436
+ forwarded, dropped by the runtime in silence, and the foreign root was **not read at all** — a
437
+ consumer asking for "the skills but not the hooks" got nothing, which is further from what they
438
+ asked for than the un-narrowed form. `compatSources` landed in 5.0.0 and the narrowing in 5.4.0;
439
+ treating the two versions as one was the defect.
440
+
441
+ `CompatImportUnsupportedError` now refuses, naming both versions and what would otherwise happen.
442
+ It refuses rather than warns because a silent nothing is discovered by wondering why a skill is
443
+ missing. An unreadable version is refused too: "cannot tell" and "is supported" must not collapse.
444
+
445
+ **And `commands` is subtracted before the compat sources reach the SDK.** The two vocabularies
446
+ diverge by one name on purpose — `.claude/commands/*.md` is read by this package and never by the
447
+ SDK — and `setting-sources-gate.ts` prescribed the subtraction as advice to consumers while the
448
+ projection that needed it did not do it. Measured: `import: ['commands']` forwarded a list
449
+ containing zero names the SDK defines, which is its own empty-list case — the exact ambiguity
450
+ `resolveCompatSources` refuses one layer up. A source whose surfaces all belong to this layer
451
+ is now dropped from the SDK's list rather than sent empty.
452
+
453
+ `CompatImportUnsupportedError` is exported from `@theokit/agents/bridge`, so a consumer can catch the
454
+ refusal by class rather than by matching its message.
455
+
456
+ - 59d6dcc: A transcript nobody can read is listed without an id, and keeps its protection
457
+
458
+ `listSessions` fell back to the filename stem whenever the first record could not be read. Under
459
+ `@theokit/sdk` 5.x that stem is a one-way hash of the id, so the fallback did not return a degraded
460
+ id — it returned an identifier belonging to no session. Session GC keyed protection on it, so a
461
+ session someone had DECLARED protected lost its protection and was planned for deletion, while the
462
+ registry removal was called with the hash and left the real entry behind.
463
+
464
+ Protection now runs in the direction that has a function. `transcriptPath(root, cwd, id)` is total on
465
+ both majors and its inverse is not, so protection is keyed by transcript PATH and caller-supplied ids
466
+ are mapped forward onto paths. Whether a transcript can be read stops mattering to whether it is
467
+ protected.
468
+
469
+ Breaking, inside the unreleased 13.0.0 line:
470
+
471
+ - `SessionSummary.id` is `string | undefined`, alongside a new `idSource: 'transcript' | 'unavailable'`.
472
+ It is never derived from the filename.
473
+ - **`protectedTranscripts` is RENAMED to `protectedTranscriptPaths`, and the rename is the fix.** Its
474
+ keys changed from session ids to transcript paths while the signature stayed `Map<string, string>`,
475
+ so a consumer that mapped the keys forward through `transcriptPath` — correct when they were ids —
476
+ kept compiling and started double-mapping, leaving its protection array matching nothing. Measured
477
+ on a real consumer against this build: `deleteSession` collected a session holding a LIVE writer
478
+ lease. The old name is gone rather than aliased; a silent break that loses data is worse than a
479
+ loud one, and an alias would have preserved the silence. `transcriptOf(id, cwd, root)` maps forward
480
+ for callers that hold an id.
481
+ - `GCCandidate.id`, `GCKept.id` and `GCError.id` admit `undefined` for the same reason.
482
+ - `RunTranscriptGCResult` gains `orphaned` — transcripts collected whose session id could not be
483
+ read, by path. A separate list rather than a second meaning inside `removed`, which answers "which
484
+ sessions did I collect"; `theo sessions gc` reports both, and never prints a filename where an id
485
+ belongs.
486
+
487
+ Closes usetheokit/theokit#668.
488
+
489
+ - feb5781: `AgentBuilder.create().hookApproval()` — the fluent twin of `defineAgent({ hookApproval })`
490
+
491
+ `13.0.0-next.7` shipped the hook approval gate reachable through `defineAgent` and capabilities, and
492
+ not through the fluent builder. A consumer that authors with `AgentBuilder.create()` had no way to
493
+ reach it: `.use()` composes presets rather than sinking capabilities, and the definition reaches
494
+ `streamAgentTurnInProcess` with `local` already assembled, so there was no downstream place to inject
495
+ `local.hooks` by hand either.
496
+
497
+ Fifth instance of one family and a NEW variant. The first four were "the symbol exists and the barrel
498
+ omits it", which the emitted-export guard now catches. This one is the opposite: the symbol is
499
+ exported, on the compiled waist, and reachable through one authoring door — the other door simply
500
+ does not offer it, and an export check is green on that.
501
+
502
+ So the guard for this variant builds the SAME agent through BOTH doors and asserts they arrive at the
503
+ same compiled waist. It catches the worst case too: an interface that declares the method while the
504
+ factory never wires it compiles, does nothing, and fails this test.
505
+
506
+ - e7a4d65: **A declared `.claude/` now reaches commands too** (theocode B-152).
507
+
508
+ `loadCustomCommands` takes `compatSources`, in the SDK's own vocabulary: `['claude-code']` adds
509
+ `<projectDir>/.claude/commands/` to what it reads.
510
+
511
+ Three surfaces already reached that directory when a consumer declared it — hooks, skills and
512
+ subagents, through the SDK's `compatSources`. Commands are loaded by this package instead, and were
513
+ the one surface that never learned about it. Measured against a real TUI: the same command file was
514
+ invocable under `.theokit/commands/` and silent under `.claude/commands/`, with no diagnostic
515
+ anywhere. A partial dialect is worse than none — whoever watched the other three work has no reason
516
+ to suspect the fourth.
517
+
518
+ **The trust gate does not move.** The foreign directory is read only when the caller declared it
519
+ AND the project is trusted, which is the same pair `resolveCompatSources` already requires. A
520
+ command is a prompt that runs on the operator's behalf, and this one usually arrives with the
521
+ repository, written for another product. An untrusted project now COUNTS the foreign commands it
522
+ refused, so the refusal is not silent either.
523
+
524
+ **The native root wins a name collision**, and the two frontmatter vocabularies stay separate: this
525
+ loader reads `description:` and nothing else, so the other product's `model` and `argument-hint`
526
+ are carried in the body rather than adopted.
527
+
528
+ - fe8a0c6: **`compatSources` reaches the SDK, and says so when the installed SDK cannot hear it** (#634).
529
+
530
+ `@theokit/sdk` stopped reading `<cwd>/.claude/` unconditionally (`theokit-sdk#524`) and put it
531
+ behind `local.compatSources`. This layer had no way to forward that, so an agent built here could
532
+ not opt into the foreign dialect at all.
533
+
534
+ The issue held the work back for a real reason: forwarding an option an older SDK does not know
535
+ would be **silently inert** — the operator declares it, nothing reads `.claude/`, and no message
536
+ explains why. The silence turns out to be removable from this side. `@theokit/sdk` declares
537
+ `"./package.json"` in `exports` — verified on **4.52.1**, the oldest version a consumer can have
538
+ today, not only on the 5.x prerelease — so the installed version is readable at runtime and a
539
+ mismatch warns once per process. The floor stays `^4.52.1`; nobody is pinned to a prerelease.
540
+
541
+ Declaring the dialect goes through `settingSources`, next to the roots it already gates:
542
+
543
+ ```ts
544
+ settingSources: {
545
+ project: { trustedBy: posture },
546
+ claudeCode: { trustedBy: posture }, // reads <cwd>/.claude/
547
+ }
548
+ ```
549
+
550
+ **Two questions, answered by two different halves of that field**, because the SDK's own docblock
551
+ separates them: _declaring_ the field answers "do I want another product's configuration imported?"
552
+ (omitting is not enabling), and the `TrustPosture` inside answers "do I trust this directory's code
553
+ to run?". The grant is `ProjectSettingsGrant` — the same type `project` takes — not because the
554
+ questions are the same, but because a separate `'foreignDialects'` capability could not carry the
555
+ distinction: `TrustPosture.allows` is `Record<K, boolean>` whose values all move with the trust
556
+ level, so a second name would promise an operator a choice `resolveTrustPosture` never gives them.
557
+
558
+ This is deliberately **stricter than the SDK**, where listing a dialect is sufficient: `.claude/`
559
+ holds a `hooks.json` that executes shell, in a directory that usually arrived with the clone.
560
+
561
+ - 2bc5d84: `delegate()` now applies the guardrails its spec declares. It accepted them and never consulted them.
562
+
563
+ Measured against the built artifact with a guard declaring both halves: the input reached the model
564
+ with its injection intact and the caller received `sk-abc123`. `bridge/agent-orchestrator.ts`
565
+ contained **zero** occurrences of `guardrail` — control on the same sweep: `loop/agent-runner.ts`
566
+ has 9 — and called `runReflectiveLoop` bare. The operator had declared guardrails and the run was
567
+ green.
568
+
569
+ This is the same defect class as the streamed-redaction fix in this release, on a sibling public
570
+ API, and it is model-reachable: `tools/delegate-tool.ts` wraps `delegate()`, so an agent can invoke
571
+ a sub-agent whose declared guards do nothing.
572
+
573
+ `checkInput` runs after `onDelegationStart` and `checkOutput` after `onDelegationComplete` — each
574
+ moderating what actually crosses the boundary rather than a string a hook may then rewrite. A
575
+ blocking input guard throws before the model is called at all, so a refused delegation costs
576
+ nothing.
577
+
578
+ `response` only. `toolCalls[].output` is tool output rather than model text, and `agent-runner.ts`
579
+ excludes it from `extractText` on the same reasoning; the docblock says so, because leaving it alone
580
+ should be a decision somebody reads rather than an omission somebody discovers.
581
+
582
+ A spec declaring no guardrails behaves byte-identically.
583
+
584
+ **A guardrail block now crosses the delegate tool as a refusal, not a crash.** `errorCodeOf` mapped
585
+ only the three delegation errors, so `GuardrailViolationError` — reachable from `delegate()` for the
586
+ first time because of this change — hit the "not a delegation outcome, a defect" arm and was
587
+ rethrown, ending the parent's turn. The tool's own description, shipped to the model, promises
588
+ `{ ok: false, error, message }` on a refusal.
589
+
590
+ It crosses as `guardrail_violation` with a FIXED message: `the delegated task was refused by a
591
+ policy guard`. The message travels only for codes on an explicit ALLOWLIST — a budget or a timeout
592
+ is a fact about the work, and the number in it is what the model acts on. A code nobody lists
593
+ withholds, so forgetting is safe in the direction that matters. A guardrail message is not — it reads
594
+ `Guardrail "pii-detector" blocked output: ssn found`, naming the guard and its exact trigger, and a
595
+ model given that learns which words to avoid rather than that it should stop. The operator keeps the
596
+ full typed error, which carries `guardName`, `phase` and `reason`.
597
+
598
+ - dcb461f: A declared `canUseTool` gate reaches the runtime, so a tool call the earlier steps did not resolve has somebody to ask.
599
+
600
+ Measured: `canUseTool` returned 0 files in this layer, against controls of `hooks` 31 and `session` 37. The SDK has the seam — a permission plugin invoked on an `ask` verdict — and this layer offered
601
+ no way to reach it.
602
+
603
+ **The default was never the problem, and saying so matters.** The SDK's engine is fail-closed: an
604
+ unmatched call resolves to `ask`, and an _absent_ gate blocks it. A tool added after the approvals
605
+ were written already defaulted to asking. What was missing is that the ask reached nobody, so it
606
+ resolved to a refusal with no way to decide otherwise.
607
+
608
+ `CanUseToolCapability` writes the gate and the adapter projects it as a permission plugin over an
609
+ engine with **no rules** — so every call resolves to `ask` and every call reaches the gate. That is
610
+ what "sees every tool call the earlier steps did not resolve" means when there are no earlier steps.
611
+ A consumer who also wants rules composes them through the SDK directly; accepting both here would
612
+ mean inventing a precedence between a rule set and a gate that nobody stated.
613
+
614
+ The plugin is **appended**, never replacing: a consumer that already registers lifecycle plugins must
615
+ not have them dropped by declaring a gate. And only when a gate was declared — installing an empty
616
+ permission plugin would gate every `ask` verdict on a callback that does not exist, which the SDK
617
+ resolves by blocking. That is strictly worse than the absence it would replace.
618
+
619
+ **`updatedInput` is decided upstream, not by silence here.** The spec lets a gate _correct_ a call,
620
+ and the SDK states its position: the `pre_tool_call` seam is veto-only, and arg rewrite is
621
+ intentionally unsupported. Surfacing a gate that accepted an `updatedInput` this runtime would
622
+ discard is the fabricated mechanism this backlog keeps finding; the narrower contract is carried as
623
+ it is.
624
+
625
+ - 019f828: Output guards now moderate `thinking` events, not only `text_delta`.
626
+
627
+ `AgentRunner` handed `moderateOutputStream` an extractor matching `text_delta` and nothing else,
628
+ while `thinking` is a public `AgentStreamEvent` that reaches the client like any other. Measured: a
629
+ guard declared over the agent's output delivered `thinking "the key is sk-abc123"` verbatim.
630
+
631
+ **This closes one channel and does not close all of them.** `DoneEvent.result` carries the model's
632
+ whole answer and is still unmoderated — measured on the same turn, `text_delta` came out
633
+ `"here: [R]"` while `done.result` came out `"here: sk-abc123"`. `task_progress.text` is a fourth and
634
+ reaches the web wire. A third pass does not extend to them: there is one `done` per round, so a pass
635
+ keyed on it would collapse every round's into one, and they need a different mechanism. Tracked
636
+ separately; stated here because a security note that overstates its coverage is worse than one that
637
+ does not exist.
638
+
639
+ The third channel of a shape fixed twice already in this release — a streamed redaction that was
640
+ computed and discarded, and `delegate()` consulting no guards at all.
641
+
642
+ **Two passes, not one wider extractor.** Widening `extractText` to match both kinds is the obvious
643
+ move and the wrong one: two kinds under one extractor COLLAPSE into a single event, so the reasoning
644
+ would be promoted into a visible one — the moderation creating the disclosure it exists to close.
645
+ Composing two passes is what `moderateOutputStream`'s own docblock prescribes, and each pass seeing
646
+ one kind is what keeps them apart.
647
+
648
+ The visible pass owns the aggregate: `DelegationResult.response` accumulates from `text_delta`
649
+ upstream, so the reasoning pass passes the result through rather than replacing it.
650
+
651
+ A blocking guard on either channel still throws before any event is emitted. An agent with no output
652
+ guard is byte-identical.
653
+
654
+ - 6ca2f50: Names the settings precedence stack: `SettingsLayer`, `SETTINGS_LAYERS`, `layerPrecedence` and
655
+ `settingsLayerChain` in `@theokit/agents/config`.
656
+
657
+ The SDK ships the mechanism — `foldLayers` folds `{ layer, precedence?, values }` and
658
+ `verifyLayerOrdering` refuses a self-contradicting chain — and deliberately not the vocabulary:
659
+ `DeclaredLayer.layer` is a free-form string and `precedence` is optional. That is the right shape
660
+ for a library, and it left one thing undecided that two consumers must agree on: which layers exist
661
+ and in what order. Each could invent their own names and numbers, fold in opposite orders, and both
662
+ pass `verifyLayerOrdering`, because a chain is only ever checked against itself.
663
+
664
+ The order is the format's, measured against <https://code.claude.com/docs/en/settings>: managed
665
+ settings, command line, project local, shared project, user. `code` — what `defineAgent()` was
666
+ passed — sits below all five, because every file above it is editable by a human who did not write
667
+ the code and is answerable for what the agent does on their machine. That single line is the whole
668
+ operator tier, and it reconciles the three levels B-026 named for four policy keys with the full
669
+ stack instead of leaving them beside it.
670
+
671
+ A layer added to the union without a declared position now fails to COMPILE, the same gate this
672
+ package puts on the compiled-options waist.
673
+
674
+ - a7f4d3d: `deleteSession` stops reporting a registry removal it cannot confirm
675
+
676
+ `registryRemoved` was computed as `outcome !== false`, so a remover that resolved saying **nothing**
677
+ was reported as a removal. That is the shape `Agent.delete` has — `Promise<void>` — and below
678
+ `@theokit/sdk@5.3.1` it is what a no-op returns: measured in `4.52.1`, `removeRegisteredAgent`
679
+ mutates an in-memory map and schedules a save only when the entry was in it, while `delete` — unlike
680
+ `list` — never hydrates from disk. In any freshly started process the entry is not in memory, so the
681
+ call resolves having left `registry.json` untouched and throws nothing.
682
+
683
+ Breaking, inside the unreleased 13.0.0 line:
684
+
685
+ - `DeleteSessionResult.registryRemoved: boolean` is replaced by
686
+ `registryOutcome: 'removed' | 'nothing-to-remove' | 'unconfirmed' | 'failed' | 'not-attempted'`.
687
+ Renamed rather than retyped: `if (result.registryRemoved)` would have kept compiling while
688
+ silently changing which branch it took.
689
+ - `SessionInUseError.registryRemoved` becomes `registryOutcome`, and the refusal no longer advertises
690
+ "the registry entry was already removed" for a half it cannot confirm — it tells the caller to
691
+ verify instead.
692
+
693
+ `not-attempted` and `unconfirmed` are distinct on purpose: "nobody asked" and "we asked and got
694
+ silence" lead a caller to opposite actions, and the old boolean said `false` to both.
695
+
696
+ The declared SDK range is unchanged. `5.3.1` fixes the behaviour and not the signature — `delete`
697
+ still returns `Promise<void>` — so `unconfirmed` remains the honest answer on every admitted version,
698
+ and raising the floor is a separate decision that would make the removal happen without making it
699
+ reportable.
700
+
701
+ Closes usetheokit/theokit#675.
702
+
703
+ - 0ed0d96: Telemetry is reachable from the authoring surface: `defineAgent({ telemetry })`,
704
+ `AgentBuilder.create().telemetry(...)`, and `TelemetryCapability` all forward the SDK's
705
+ `TelemetrySettings` to `Agent.create({ telemetry })`, so a run emits OpenTelemetry spans for
706
+ `agent.send`, `llm.call`, `tool.call` and `memory.search`.
707
+
708
+ The SDK has emitted these spans since 4.52.1 — with an exporter selector, a service name, and
709
+ auto-detection of Langfuse / Sentry / PostHog. This layer never passed the field through, so an
710
+ operator could not turn any of it on and had no way to see a run as a trace alongside the rest of
711
+ their system. Measured before the change: four occurrences of the word "telemetry" in the package
712
+ source, all four in prose, zero assignments.
713
+
714
+ `@opentelemetry/api` is an OPTIONAL peer of the SDK. Without it, telemetry is a silent no-op even
715
+ with `enabled: true` — which is the usual explanation for a run that reports no spans, rather than a
716
+ misconfigured collector. Content (prompts, responses, tool args) is omitted unless you set
717
+ `includeContent: true`.
718
+
719
+ - 462bb62: The pre-spawn hook approval gate crosses the layer, or refuses to pretend it did
720
+
721
+ `@theokit/sdk@5.4.0` added `local.hooks.approve` — a consumer's decision point before the runtime
722
+ spawns a hook. This layer never forwarded it, so a hook declared in a config root, including a
723
+ foreign dialect imported through `compatSources`, ran shell without passing the consumer's approval.
724
+
725
+ Measured in a consumer against 5.4.0, with a control arm proving the zero was not an empty turn:
726
+
727
+ ```
728
+ control_nothing fires=0 tool_ran=yes
729
+ claude_project_unapproved fires=1 tool_ran=yes
730
+ ```
731
+
732
+ `defineAgent({ hookApproval })` and the new `HookApprovalCapability` now carry it to
733
+ `Agent.create({ local: { hooks } })`. Named `hookApproval` rather than `hooks` because
734
+ `defineAgent({ hooks })` is already the LIFECYCLE seam, and two security-relevant things under one
735
+ name is how a consumer configures the wrong one.
736
+
737
+ **Declaring it against an SDK older than 5.4.0 is REFUSED, not forwarded.** The option is 5.4.0-only
738
+ while this package's floor is `^4.52.1`, so a pass-through would compile and do nothing on most
739
+ admitted versions — a gate that silently does not gate, which is worse than offering none, because
740
+ whoever configured it stops looking. An unreadable SDK version is refused for the same reason:
741
+ "cannot tell" and "is gated" must not collapse.
742
+
743
+ The floor is unchanged, so no consumer is pinned to a newer SDK for a feature they did not ask for.
744
+
745
+ Closes usetheokit/theokit#686 (the `hooks` half; `resolveCompatSources` widening is tracked there).
746
+
747
+ - ecda4ae: The agent-module parameter names what it accepts, so a wrong shape fails at compile time
748
+
749
+ `streamAgentTurnInProcess` and `compileAgentModule` took `mod: unknown`, so the contract lived only
750
+ in the runtime guard. A consumer whose producer became `async` handed a `Promise` straight through
751
+ and shipped two releases in which no turn could run — with typecheck, 1213 tests, lint and twelve CI
752
+ checks green. The runtime was never wrong: it refused the Promise and threw a typed error. What
753
+ failed was the moment.
754
+
755
+ Both now take `AgentModule`, exported alongside them and re-exported from `theokit/server/agent`.
756
+ The type mirrors the runtime guard rather than the fuller `CompiledAgentOptions` — an array under
757
+ `tools`, an object under `agents` — so it refuses nothing that compiled before.
758
+
759
+ `@theokit/tauri/sidecar`'s `runTurnToJsonl` is narrowed too: a desktop sidecar imports its agent
760
+ module statically, which is exactly where a type catches the mistake.
761
+
762
+ Entry points that receive a module from a path discovered at runtime keep taking `unknown`, and now
763
+ say so by calling the new `compileLoadedAgentModule`. Their `unknown` has a reason; before this, the
764
+ boundary that had one was indistinguishable from the one that did not.
765
+
766
+ Closes usetheokit/theokit#663.
767
+
768
+ - cfe7f4c: **`@theokit/sdk@5.x` is now supported, alongside 4.x**
769
+ ([#654](https://github.com/usetheokit/theokit/issues/654)).
770
+
771
+ The declared range becomes `^4.52.1 || ^5.0.0` (`^4.49.0 || ^5.0.0` for `@theokit/presenter`), and
772
+ the full suite passes on both halves: 7533 tests against `4.52.1` and 7533 against `5.0.1`.
773
+
774
+ This unblocks plugins whose open-ended `@theokit/sdk` peer resolves to 5.x. Until now `theokit` was
775
+ the package that **refused** that resolution — the ERESOLVE named the plugin, but the bound that
776
+ could not be satisfied was this one.
777
+
778
+ **What had to change, and why it was not a version bump.** SDK 5.x writes a transcript to
779
+ `${sessionUuidFor(sessionId)}.jsonl` where 4.x wrote `${safeSessionId(sessionId)}.jsonl` — a
780
+ SHA-256 over a namespace, so the filename stopped being the session id and the mapping does not
781
+ invert. `listSessions` derived ids from filenames, so listing, protection, GC and deletion all
782
+ returned UUIDs where callers passed ids. One defect, twenty-nine failing tests.
783
+
784
+ The id is now read from the transcript **record**, which the SDK writes on both majors and which is
785
+ authoritative where the name was only a convention. The filename stem remains the fallback, so a
786
+ truncated transcript still appears in a listing rather than dropping out of GC's sight.
787
+
788
+ `LiveTranscriptError` — 5.x's new name for `LiveSessionError` — deliberately does not cross the
789
+ `@theokit/agents` layer: it does not exist on the 4.x half, and 5.x keeps the old name working and
790
+ deprecated, so the name that crosses is the one both majors have.
791
+
792
+ - 0731584: `resolveCompatSources` now returns `readonly GatedCompatSource[]`, and
793
+ `CompiledAgentOptions.compatSources` takes that type — so a compat source no `TrustPosture`
794
+ authorised no longer fits the field.
795
+
796
+ **BREAKING for hand-built compiled options**, exactly as its sibling was. Build compat sources
797
+ through `resolveCompatSources`.
798
+
799
+ The twin of the `settingSources` brand, and it exists because that fix closed one of the two fields
800
+ one `SettingSourcesSelection` feeds and left the other bare. Measured, with the `settingSources`
801
+ route as the control: the control errored, and `setOnce(draft, 'compatSources', ['claude-code'],
802
+ 'cap')` compiled cast-free — while `agent-compiler.ts` told the reader that field "can only hold a
803
+ source some posture granted".
804
+
805
+ It carries more authority than its twin, not less. `applyLocalSources` forwards it to
806
+ `Agent.create({ local: { compatSources } })`, which reads `<cwd>/.claude/` — `hooks.json` included,
807
+ and that executes shell.
808
+
809
+ **Signature narrowing**: `moderateOutputStream`'s `rebuildText` is now
810
+ `(text: string, replaced: E) => E`. It was typed `E | undefined` for a case that cannot happen — a
811
+ stream where no event carried text returns from the absence check before the guards run, so
812
+ `rebuildText` is never reached. The branch handling that case was dead code, and three shipping
813
+ artifacts described it as live.
814
+
815
+ **Fixed**: `isPort` discriminated on the presence of `run`, so an object carrying both `compiled`
816
+ and a `run` — reachable through a spread, which is how targets are built in practice — took the port
817
+ branch and skipped `delegate()` entirely: no declared guardrails, no inherited parent veto, no
818
+ budget clamp. A tie now goes to the spec, because the spec branch is the guarded one.
819
+
820
+ ### Patch Changes
821
+
822
+ - 1202e86: A backslash escapes a `$N` placeholder: `\$1` now renders as the literal `$1` with the backslash
823
+ dropped, instead of being substituted with the first argument.
824
+
825
+ Measured before the fix: `price \$1.00 here` with argument `alpha` produced `price \alpha.00 here`.
826
+ A template describing a price produced a template describing an argument, and the backslash the
827
+ author typed to prevent that survived into the output as stray punctuation.
828
+
829
+ **Two neighbouring behaviours are deliberately unchanged**, because a parity survey flagged all three
830
+ together and only one of them is a defect:
831
+
832
+ - `$1` is the FIRST argument here. That convention is stated in the module docblock, fixed by its
833
+ existing tests, and depended on by its own `` !`git diff $1` `` example. Changing it would silently
834
+ rebind every argument of every command already written — a compatibility decision, not a fix.
835
+ - An unmatched `$3` expands to empty rather than to the literal, which the call site documents:
836
+ "Empty, never the literal. A leaked `$3` reads to the model as text the user wrote."
837
+
838
+ The escape was the one of the three with no decision behind it.
839
+
840
+ - bb0f451: A guardrail refusal thrown inside a round is no longer renamed into a delegation failure.
841
+
842
+ `runReflectiveLoop` wrapped any error that was not already a delegation error into
843
+ `DelegationError`, whose message reads `Delegation to agent "X" failed: ${cause.message}`. That code
844
+ is on the delegate tool's message allowlist — a delegation failure's text is a fact about the work —
845
+ so the wrapper carried the guard's own words to the model: `Guardrail "pii-detector" blocked output:
846
+ ssn found`, naming the guard and its exact trigger.
847
+
848
+ Measured through `createDelegateTool` with a consumer-supplied `streamFactory` that throws
849
+ mid-round. `streamFactory` is a public option, so this was reachable rather than theoretical.
850
+
851
+ `GuardrailViolationError` now passes through as itself, alongside the two delegation errors that
852
+ already did, so the tool classifies it `guardrail_violation` and withholds the message.
853
+
854
+ Fixed by classification rather than by suppressing text downstream: a guard refusal is not a
855
+ delegation failure, and a layer that renames an error cannot be expected to maintain a list of what
856
+ the new name must hide.
857
+
858
+ - 9725bf1: A malformed guardrail result no longer hands the model the name of the guard that failed, and a third guardrail error class is covered by construction.
859
+
860
+ B-015 made `GuardrailViolationError` pass through `run-reflective-loop.ts` unwrapped, because
861
+ `DelegationError` interpolates its cause and `delegation_failed` is on the delegate tool's message
862
+ allowlist. Its sibling in the same file was not added. `MalformedGuardrailResultError` takes the same
863
+ wrapping, and its message also names the guard: `Guardrail "X" returned action 'redact' for output
864
+ with no replacement text.`
865
+
866
+ Smaller payload than a violation — the guard's name and phase, not its trigger text — and the same
867
+ leak through the same allowlist. It additionally **mislabelled a guard defect as a delegation
868
+ failure**, which is a fact about the work the model would act on.
869
+
870
+ **The passthrough is now structural.** `GuardrailError` is the abstract base every guardrail error
871
+ shares, and `run-reflective-loop.ts` and `errorCodeOf` both key on it — so a new class is covered by
872
+ its own declaration rather than by someone remembering to extend a list of two. A list of two is how
873
+ this defect existed.
874
+
875
+ A base alone is not airtight: a class can still extend `TheokitAgentError` directly. The test walks
876
+ the guardrails barrel and fails on any exported error class that skipped the base. The two together
877
+ are the construction; either alone is a convention.
878
+
879
+ A malformed result crosses as `guardrail_error`, not `guardrail_violation`: a guard that is written
880
+ wrong did not refuse anything, and telling the model it was refused would be wrong in the other
881
+ direction. Neither code is on the message allowlist, so both withhold. `CostBudgetExceededError` now
882
+ descends from the same base and crosses as a refusal instead of being rethrown as a defect.
883
+
884
+ - dcb461f: A hook's `matcher: "*"` now fires on every tool, as the format defines it.
885
+
886
+ `new RegExp("*")` throws — "nothing to repeat" — and the catch in `matches()` reads a throw as
887
+ no-match. So the spelling an author is most likely to write for "always" was the one spelling that
888
+ meant "never", while the two synonyms worked.
889
+
890
+ Measured end to end before the fix, a vetoing `pre_tool_call` against tool `Bash`:
891
+
892
+ ```
893
+ matcher "*" -> allowed through <- documented match-all, guard never ran
894
+ matcher "" -> VETOED
895
+ matcher "Bash" -> VETOED
896
+ matcher omitted -> VETOED
897
+ matcher "Edit, Write" -> allowed through <- documented exact-list form, matched nothing
898
+ ```
899
+
900
+ The comma-separated list is the second half: as a regex it required the space to be part of a tool
901
+ name, so it matched nothing at all. It is now read as the list it is.
902
+
903
+ Both shapes are recognised **before** the regex engine sees them, because neither is valid regex and
904
+ one of them throws.
905
+
906
+ **Unchanged and deliberate:** a matcher that cannot compile still does not match. A broken matcher
907
+ must not take down the turn — that trade is the reason the `catch` exists, and this fix does not
908
+ touch it.
909
+
910
+ - 9725bf1: An agent served over HTTP or the terminal now applies its declared guardrails. It applied none of them.
911
+
912
+ `streamAgentUIMessages` called `createSdkAgentStream` directly on both of its branches. Guardrails
913
+ were applied in exactly two other places — `AgentRunner.stream()` and `withGuardrails`, the latter
914
+ reached only from `toAgentFactory` — and neither is on this path. Measured:
915
+ `grep -c guardrail agent-endpoint.ts` → 0, against 23 files in `packages/agents/src`.
916
+
917
+ Its reachable callers are the HTTP mount, the terminal runner and the streamer builder: every surface
918
+ a deployed agent is actually reached through. So a `defineAgent({ guardrails: [...] })` served to a
919
+ browser ran no input guard, applied no `redact`, and a `block` never threw.
920
+
921
+ This is the same shape as three defects already closed in this release, one layer up. B-014 and B-018
922
+ measured _channels_ inside a stream that was already being moderated; this is the surface where the
923
+ moderation never started.
924
+
925
+ **The composition is copied from `AgentRunner.stream()`, not reinvented** — two passes over the two
926
+ text-carrying kinds, visible inner and reasoning outer. NOT one wider extractor: two kinds under one
927
+ `extractText` collapse into a single event, so the model's private reasoning would be promoted into
928
+ the visible answer, and the moderation would create the disclosure it exists to close. That mutation
929
+ survived the first version of the test, which asserted over the flattened stream where every word is
930
+ still present; the assertions are per channel now.
931
+
932
+ Moderation runs on the wire chunks rather than upstream events, because the translator sits between
933
+ them — moderating upstream and letting the translator re-derive text would moderate one channel and
934
+ deliver another. `done.result` and `task_progress.text` remain uncovered here, exactly as in
935
+ `AgentRunner`: there is one `done` per round, so a pass keyed on it would collapse every round's into
936
+ one. They need a different mechanism and are tracked separately.
937
+
938
+ An agent that declared no guardrail takes an untouched pass-through — wrapping unconditionally would
939
+ buffer every served stream to enforce an empty list.
940
+
941
+ - ac29eff: A permission-gate veto now emits a debug line.
942
+
943
+ `grantGate` refused and emitted nothing — no log, no counter, no debug line. An operator could
944
+ observe the refusal only through the tool result the model received, and the two causes the veto
945
+ message distinguishes ("no standing grant matches" versus "the permission store could not be read,
946
+ so no grant applies") are indistinguishable from there.
947
+
948
+ That is pillar 3 of the wiring triad missing on a refusal seam. `bridge/approval-posture.ts`, the
949
+ sibling gate, already logged through this exact seam.
950
+
951
+ The QUERY is logged — tool, scope, and which of the two causes fired — and the grant is not: a query
952
+ names what an operator needs to diagnose, while the store's contents are the thing being protected.
953
+ A classifier that threw logs as its own event rather than as an ordinary veto, so a consumer-code
954
+ defect is not read as a denied tool.
955
+
956
+ - 1202e86: An inline `` !`command` `` is now recognised only at a boundary: the start of a line, or after
957
+ whitespace. When `!` follows another character the placeholder stays literal and the command does
958
+ not run, which is what the contract specifies.
959
+
960
+ `REFERENCE_REGEX` applied its leading-boundary guard `(?<!\S)` to the `@file` branch only, so the
961
+ shell branch matched anywhere. Measured before the fix: `` inline !`echo hi` and KEY=!`echo boom` ``
962
+ produced `inline <ran:echo hi> and KEY=<ran:echo boom>` — both ran.
963
+
964
+ This was the one place this module did **more** than its contract allows. Every other divergence
965
+ found in the same survey is something that fails to happen; this was something that happened, from a
966
+ markdown file loaded out of a working directory.
967
+
968
+ The test keeps three positive cases beside the negative one — a change that stopped recognising
969
+ inline commands altogether would satisfy the negative and destroy the feature.
970
+
971
+ - 1202e86: `${VAR}` in a `.mcp.json` `env` or `headers` block now resolves against the host environment.
972
+
973
+ `.mcp.json` is committed to the repository, so a named reference is the specification's only way to
974
+ keep a credential out of it. Nothing expanded the placeholder, and the shape check accepts it as a
975
+ perfectly valid string — so the entry validated, the server started, and it authenticated with the
976
+ literal text `${API_KEY}`. The failure surfaced as a remote auth error with no path back to the
977
+ config line.
978
+
979
+ An unset reference is **reported and left as written**. Substituting empty would start the server
980
+ with a blank credential and fail somewhere further away; dropping the key would look like the author
981
+ never wrote it.
982
+
983
+ The environment is injected (`loadMcpJson(cwd, { env })`, defaulting to `process.env`), matching how
984
+ the rest of the package reads env — so a test proves the expansion without mutating the process it
985
+ runs in.
986
+
987
+ **This does not loosen the posture this loader already takes.** `buildEntry` refuses `envPolicy`
988
+ deliberately: "A file committed to the repository is no place to loosen a process-level defence."
989
+ That refusal is about a committed file handing a server the _whole_ environment. A named reference
990
+ resolves _one_ variable the host already chose to set — and refusing to expand it protects nothing,
991
+ since it pushes the author to paste the literal secret into the file instead.
992
+
993
+ - 1202e86: The two permission vocabularies meet, and the mirrored `permissionMode` field says what it mirrors.
994
+
995
+ Measured: `permissionMode` appeared in exactly one file, as a mirrored field nothing branches on;
996
+ `dontAsk`, `autoMode`, `useAutoModeDuringPlan` and `classifyAllShell` all returned 0 files. This
997
+ layer offers `suggest | auto-edit | full-auto` — the values a real consumer put in front of users —
998
+ while the runtime resolves `default | plan | acceptEdits | bypass`. A reader of either had no way to
999
+ the other.
1000
+
1001
+ `approvalModeToPermissionMode` translates them. `suggest` maps to `default`, **not** to something
1002
+ that asks unconditionally: `default` means the rules decide and an unmatched call asks, so mapping it
1003
+ otherwise would discard every allow rule the operator shipped. `full-auto` maps to `bypass`, which
1004
+ allows everything **except an explicit deny** — mapping the most permissive local mode onto something
1005
+ that also cleared denies would turn a UI convenience into a policy override.
1006
+
1007
+ The signature takes `ApprovalMode`, so a fourth local mode is a compile error rather than a silent
1008
+ fallthrough to a posture nobody chose.
1009
+
1010
+ **`plan` has no local counterpart, deliberately.** It is an explore-only posture an _operator_
1011
+ imposes, not something this surface offers, and inventing a fourth local name would put a decision
1012
+ that belongs to the operator into the user's mode picker. The absence is named rather than filled.
1013
+
1014
+ `PermissionGate`'s mirrored field is now the SDK's own `PermissionMode` instead of a bare `string`. A
1015
+ mirror that accepts any word mirrors nothing in particular — `"readonly"`, what somebody writes when
1016
+ they mean `plan`, would have sat there looking applied. Restating the union by hand was the first
1017
+ attempt and the package's own type test refused it: the shape must fit `PreToolCallContext`
1018
+ structurally, and a copy that drifts by one member stops fitting. Importing the source makes it a
1019
+ mirror by construction.
1020
+
1021
+ Unchanged, and still the documented decision: the gate ignores the mode. `bypass` does not disable
1022
+ it, because a standing grant is the operator's and not the run's.
1023
+
1024
+ - 9725bf1: A text event whose `content` is not a string is refused instead of delivered unexamined.
1025
+
1026
+ Both extractors tested `typeof e.content === 'string'` and returned `undefined` otherwise.
1027
+ `moderateOutputStream` reads `undefined` as "this event carries no text", so the payload was never
1028
+ accumulated, never shown to a guard, and yielded **verbatim**.
1029
+
1030
+ The failure direction is DELIVER, not block: a guard declared to stop that payload silently never saw
1031
+ it, and the run reported green. Reachable in practice — `StreamEvent` is
1032
+ `{ type: string; [key: string]: unknown }`, `event-translator.ts` casts an unvalidated provider
1033
+ content block to `{ text?: string }`, and a consumer-supplied `streamFactory` is a public option.
1034
+
1035
+ **Two different questions had been collapsed into one answer.** "Not this event kind" and "this kind,
1036
+ content unreadable" both returned `undefined`, and they need opposite handling. The new
1037
+ `textPayloadExtractor` keeps `undefined` for the first and throws `UnreadableTextPayloadError` for
1038
+ the second.
1039
+
1040
+ **Refused, not coerced.** Coercing would moderate `"[object Object]"` — a guard consulted about a
1041
+ string the model never produced, returning a verdict about nothing, while the real payload rides
1042
+ along underneath. That is the redaction-computed-and-discarded shape with an extra step.
1043
+
1044
+ One implementation, used by both seams. The defect existed as two hand-written copies of the same
1045
+ predicate in `AgentRunner.stream()` and the served endpoint; fixing one and leaving the other is the
1046
+ half-fix this release keeps finding.
1047
+
1048
+ Only runs that declared an output guard can meet the error: `moderateOutputStream` is a transparent
1049
+ pass-through when no guard defines `checkOutput`, so the extractor is never consulted otherwise.
1050
+
1051
+ - 2bc27d3: `inheritHooks` no longer lets a member's `transform_tool_result` or `pre_user_send` handler replace
1052
+ its parent's — both now chain parent-first, matching the six events that already composed.
1053
+
1054
+ `inheritHooks` documents its security property as "the parent's refusal is evaluated first, and a
1055
+ member can only ever ADD a reason to refuse". For these two events the plain object spread did the
1056
+ opposite: a member declaring either handler silently discarded the parent's. The reachable surface is the EXPORTED `inheritHooks`, called with two handler maps.
1057
+ `delegate()` passes `undefined` for the member (`agent-orchestrator.ts:175`), so that path composed
1058
+ nothing and was never affected — a distinction the first version of this note got wrong.
1059
+
1060
+ `pre_user_send` composes additively — both contributions reach the model, parent first — because
1061
+ `PreUserSendResult` carries only `recalledContext` and the seam exposes no prompt mutation.
1062
+
1063
+ - dcb461f: Thirteen types named in exported signatures now cross a barrel, and the guard that finds them derives the requirement instead of listing it.
1064
+
1065
+ `bridge/index.ts` enumerates this shape four times by issue number — #663, #668, #675, #686 — and
1066
+ B-004 was the fifth. Each was found by _installing_ the published package, because the source is
1067
+ correct every time: the type is exported from its own module and only the barrel omits it.
1068
+
1069
+ The guard that followed reads the emitted barrel, which was the right move, and it is a **hand-written
1070
+ list** — so it catches the entries somebody remembered to add. B-004 was added to it _after_ a review
1071
+ found the miss, which is precisely what the list existed to prevent.
1072
+
1073
+ **This one derives it.** It parses every built `.d.ts`, collects the names each chunk imports and uses
1074
+ in an exported signature, and flags any that no public barrel re-exports. Measured on the built
1075
+ output: thirteen, including `BudgetTracker`, `InlineSkill`, `PluginsSettings`, `SkillsOptions`,
1076
+ `ContextWindowOptions`, `Plugin`, `ProviderRoutingSettings`, `RetryOptions`,
1077
+ `DiscoverSubagentsOptions`, and four reached through subpath entries.
1078
+
1079
+ Two things the first version of the guard got wrong are worth recording, because both produced a
1080
+ green over nothing:
1081
+
1082
+ - **A rollup does not put `export` on its declarations.** It emits them bare and lists every public
1083
+ name in one `export { … }` clause at the end. A single forward pass keyed on the `export` modifier
1084
+ visited zero exported declarations and reported no problem — while `index.d.ts` imported
1085
+ `PluginsSettings` on line 1, used it in a signature on line 359, and did not carry it in the clause
1086
+ on line 1445. It takes two passes.
1087
+ - **Reachable means every barrel a consumer can import from**, not just the root. A type exported from
1088
+ `auth.d.ts` is nameable through `@theokit/agents/auth`; the hashed internal chunks are not
1089
+ importable at all, so what they export is not reachable and what they import is still a finding.
1090
+
1091
+ The guard caught one of the changes made in this same release — `PermissionMode`, added to
1092
+ `PermissionGate` hours earlier — which is the difference between a list and a derivation.
1093
+
1094
+ - eaac7b0: The "will NOT fire" warning for a declared-but-unwired hook event now names where the capability
1095
+ already lives, instead of ending "the handler does not exist yet".
1096
+
1097
+ Two of the three unwired events are served today by purpose-built seams — `Guardrail.checkOutput`
1098
+ for `transform_llm_output`, and `createToolHooksPlugin({ processInput })` for `pre_user_send` — so
1099
+ the old message told consumers to wait for work that will not come. The third, `on_session_end`, is
1100
+ named as genuinely uncovered, with the reason: its handler returns `void` and cannot refuse an
1101
+ ending, so wiring it would produce a hook that runs and cannot decide.
1102
+
1103
+ Nothing is wired. `HOOK_EVENTS`, `WIRED_EVENTS` and `OBSERVATIONAL_EVENTS` keep the same members.
1104
+
1105
+ - 1202e86: `$ARGUMENTS` works, and the substitutions this module does and does not perform are written down.
1106
+
1107
+ The template understood `$1`, `$2`, … and nothing else. A command that wanted everything the user
1108
+ typed had to guess how many positions to concatenate, and stopped being correct at the first
1109
+ invocation that passed one more. Measured: `$ARGUMENTS` returned 0 hits against a control of 31.
1110
+
1111
+ It expands to the **raw** string, not the split tokens rejoined. Splitting strips the quotes that
1112
+ decided the grouping — `"two words" solo` is two arguments and five words — so rejoining would hand
1113
+ the model `two words solo` and lose the only mark saying which three belonged together. A placeholder
1114
+ whose whole job is "what the user typed" must not quietly retype it. The backslash escape works the
1115
+ same way `$N`'s does.
1116
+
1117
+ **`templateHints` no longer asks for escaped placeholders.** `\$1` is prose about a placeholder, not
1118
+ a request for one, and listing it asked the user to supply an argument the template would never
1119
+ substitute. It also broke the sort outright: the match carries the backslash, so `slice(1)` produced
1120
+ `"$1"`, `Number` produced `NaN`, and a comparator returning `NaN` leaves the order unspecified.
1121
+ `$ARGUMENTS` sorts first — reading it after `$3` suggests it is a fourth position.
1122
+
1123
+ A table above the regex now states the supported set where an author will meet it, including two
1124
+ deliberate absences: `${CLAUDE_SKILL_DIR}` belongs to a skill body rather than a command template
1125
+ (and is substituted by `@theokit/sdk`'s `skill_read`, the only place that knows which directory the
1126
+ skill came from), and `${CLAUDE_PLUGIN_ROOT}` / `${CLAUDE_PLUGIN_DATA}` are **out of scope rather
1127
+ than pending** — the plugin format is not implemented on either side, so substituting a root for a
1128
+ plugin that cannot be loaded would have to invent the layout it points into.
1129
+
1130
+ - ec899f4: An observational hook handler is now assigned to its own key rather than chosen by comparison.
1131
+
1132
+ The dispatch loop used a two-branch conditional over a list of two event names, so a third
1133
+ observational event would have landed on `post_assistant_reply` — silently, with no test objecting.
1134
+ No behaviour changes for the events wired today; the fix removes the trap for the next one added.
1135
+
1136
+ - dcb461f: `@Checkpoint` says, at the name, that it is run-state checkpointing and not file undo.
1137
+
1138
+ Two different things share the word and only one of them exists here. What this package has is
1139
+ run-state checkpointing — enough to resume a conversation. What it does not have is file
1140
+ checkpointing: snapshot and restore of the files an agent edited, the thing an interactive coding
1141
+ agent needs to offer an undo.
1142
+
1143
+ Measured: `rewindFiles`, `rewind_files`, `restoreFile` and `backup` returned 0 files here, 0 in the
1144
+ SDK `.d.ts` and 0 in `@theokit/sdk-tools`, while `checkpoint` returned six — all of them run state.
1145
+
1146
+ **The harm is not the missing feature, it is the collision.** Any parity checklist that greps for
1147
+ `checkpoint` is satisfied by the wrong one and reports a capability that does not exist. A consumer
1148
+ reads the checklist, believes undo is available, and finds out when a user asks for it.
1149
+
1150
+ The absence is stated **at the colliding name**, which is the only place a grep will reach, and a
1151
+ test pins both the statement and the reason it matters — a docblock nothing checks is a docblock that
1152
+ gets tidied away. It fails if a file-restore surface ever appears under the run-state name without
1153
+ the statement being updated with it.
1154
+
1155
+ Not implemented here, and the reason is stated rather than implied: there is no pre-write seam on the
1156
+ edit tools to hang snapshot and restore on, which makes it a feature with its own design questions
1157
+ rather than a wiring job. Half-building it under a name that already means something else would make
1158
+ the collision worse.
1159
+
1160
+ - e4f2e78: Documents that `sandbox` here is not the Claude Code CLI's `sandbox.*` settings block, at the door a
1161
+ reader of the local vocabulary meets (`@theokit/agents/sandbox`) and in a new measured surface note,
1162
+ `docs/surfaces/sandbox-vocabulary.md`.
1163
+
1164
+ They share an enforcement mechanism — bubblewrap and seccomp on Linux — and differ in what they
1165
+ expose above it. This package offers an execution BACKEND (`SandboxBackend`, `LocalSandbox` /
1166
+ `LinuxSandbox`) whose policy is three modes and four `SandboxConfig` fields. The CLI's is a SETTINGS
1167
+ POLICY of 38 keys spanning per-path filesystem rules, a network allowlist with proxies and TLS
1168
+ termination, and per-variable credential masking.
1169
+
1170
+ The gap that matters is network: this package has no network policy at any level, so a checklist
1171
+ that greps for `sandbox`, finds this module and stops reports a domain allowlist that does not
1172
+ exist. Every one of the 38 keys is enumerated with a verdict — absent, coarser, or not applicable to
1173
+ a library — rather than dismissed in aggregate.
1174
+
1175
+ Two corrections fell out of doing it. The count is 38, not the 39 previously recorded: two
1176
+ independent fetches of the reference returned the same 38 keys while the summarising step attached a
1177
+ different total to each. And `sandbox.failIfUnavailable` has no equivalent here — `createSandboxBackend`
1178
+ degrades to an unconfined `LocalSandbox` after one warning when bubblewrap is missing, and an
1179
+ operator who believes they are sandboxed cannot make that a hard failure.
1180
+
1181
+ - 1202e86: A `.mcp.json` written against the current MCP spec is accepted.
1182
+
1183
+ The specification renamed the HTTP transport to "Streamable HTTP". `validateRemote` accepted
1184
+ `"http"` and `"sse"` and refused anything else, so a server declared with the spec own current
1185
+ name was dropped — with a message about a field the author had written correctly.
1186
+
1187
+ It is an ALIAS, normalised at the boundary, not a third transport. They are one transport under two
1188
+ names, and forwarding the synonym downstream would ask every consumer of the parsed config to learn
1189
+ it too; the SDK own `McpServerConfig` does not carry it. The parser accepts what the author wrote
1190
+ and hands on what the runtime speaks.
1191
+
1192
+ An invented transport is still refused. Accepting the alias must not turn the check into a
1193
+ pass-through, and a silent acceptance would be worse than the refusal this started as.
1194
+
1195
+ - dcb461f: Sub-agents declared through the authoring chain now spawn, and the per-run door is nameable.
1196
+
1197
+ Three halves that did not connect: `SubAgentsCapability` wrote `draft.agents`,
1198
+ `CompiledAgentOptions.agents` held it, and `assembleM8CreateOptions` had no `agents` field to
1199
+ project it into. Declaring a sub-agent compiled cleanly and spawned nothing.
1200
+
1201
+ `agent-compiler.ts` recorded the gap as ADR D3 — "a resolver here is carried, not invoked" — and a
1202
+ test pinned the boundary with an instruction attached: if someone wires the projection, go red and
1203
+ record that the deferral ended. **That deferral has ended.** What could not be defended was the shape
1204
+ a consumer meets: `SubAgentsCapability`, `SubagentDefinition`, `discoverSubagents`,
1205
+ `loadSubagentDefinition` and `listSubagentNames` all cross the public barrel, so the authoring chain
1206
+ reads as complete. This package already names that failure four times by issue number (#663, #668,
1207
+ #675, #686): the type crosses, the capability does not.
1208
+
1209
+ **Why projecting rather than un-exporting.** The shapes already agreed everywhere except in the one
1210
+ type nothing consumed. `RuntimeOverrides.agents` — the per-run door that always worked — is
1211
+ `Record<string, AgentDefinition>`, the SDK's own shape, and that same shape already crossed the
1212
+ barrel as `SubagentDefinition`. The odd one out was `CompiledSubAgent` (`{ model?, systemPrompt? }`),
1213
+ referenced in exactly two places, both of them its own declaration and the field that held it. It
1214
+ could not have been projected as it stood: `AgentDefinition` requires `description` and `prompt`, and
1215
+ a sub-agent with no description is one the parent model has no basis to delegate to.
1216
+ `CompiledSubAgent` is now an alias of the SDK type, so `model` is `ModelSelection | "inherit"` rather
1217
+ than a bare string.
1218
+
1219
+ **`RuntimeOverrides` is exported, type-only.** It was declared by the adapter and exported by zero
1220
+ barrels, so a consumer could pass the value and could not name the type — no helper, no wrapper, no
1221
+ typed variable to hold one.
1222
+
1223
+ Per-run overrides still win over the compiled set: `sdk-adapter.ts` spreads `...m8, ...extra`, and a
1224
+ per-run value that lost to a compile-time one would be the opposite of what "override" promises. The
1225
+ key is written only when something was declared — an unconditional `agents: {}` would hand
1226
+ `Agent.create` a claim ("this agent has children") that no author made.
1227
+
1228
+ - dcb461f: `@ContextWindow` now documents that declaring it is also the on-switch for instruction discovery.
1229
+
1230
+ The SDK constructs its `FileContextManager` only under `if (options.context !== undefined)`, and
1231
+ `ContextWindowCapability` is the only place this layer sets that field. The consequence was
1232
+ undocumented and load-bearing:
1233
+
1234
+ - declare `@ContextWindow(...)` → `CLAUDE.md`, `AGENTS.md`, `GEMINI.md`, `.cursor/rules` and
1235
+ `.theokit/rules` are discovered;
1236
+ - omit it → every one of those files is inert, with no warning.
1237
+
1238
+ The option's entire surface is one key whose doc comment reads "Maximum tokens before compaction
1239
+ triggers", and the module docblock above it is about compaction and strategy knobs. A consumer
1240
+ debugging "why is my CLAUDE.md ignored" had no path from that symptom back to a decorator named after
1241
+ a token budget.
1242
+
1243
+ **No behaviour changes.** The coupling is stated where the author decides whether to declare the
1244
+ decorator, and pinned by a test that goes red if discovery ever gains a switch of its own — at which
1245
+ point the documentation should be deleted rather than left quietly false.
1246
+
1247
+ `ContextSettings.maxBytesPerFile` and `maxBytesTotal` are now reachable from this surface too.
1248
+ They were not, and the gap bit hardest exactly here: the option that enables `CLAUDE.md` discovery
1249
+ is the same option that decides at which size a `CLAUDE.md` is truncated (40 000 characters by
1250
+ default, head/tail with a marker) or dropped (120 000 aggregate, lower-priority sources first). A
1251
+ 60 000-character instruction file was silently cut, and the only knob on offer was named after
1252
+ tokens. Keys are written only when declared, so an agent that never asks about bytes keeps the SDK's
1253
+ own defaults.
1254
+
1255
+ - a2b0a59: The hook gate's vocabulary crosses with the capability that takes it
1256
+
1257
+ `13.0.0-next.6` exported `HookApprovalCapability` and withheld `HookApprovalGate`,
1258
+ `HookApprovalRequest` and `HookGateUnsupportedError`. A consumer could build the gate, and could
1259
+ neither type the object it takes nor catch its refusal by class — which matters more here than
1260
+ usual, because refusing loudly is the whole design.
1261
+
1262
+ Fourth instance of the same shape (#663 `AgentModule`, #668 `transcriptOf`, #675 `RegistryOutcome`),
1263
+ and the first three were each found by installing the published package. The source is correct every
1264
+ time: the type IS exported from its own module, and only the barrel omits it.
1265
+
1266
+ A guard now reads the EMITTED `dist/index.d.ts` export list rather than the source, and it was
1267
+ proved able to fail by removing one export and watching it name that one.
1268
+
1269
+ - 9915c19: `RegistryOutcome` is exported alongside the field that uses it
1270
+
1271
+ `registryOutcome` shipped in `13.0.0-next.4` and its type did not, so a consumer could read the
1272
+ value and not name it — `function handle(o: RegistryOutcome)` did not compile. A union whose members
1273
+ cannot be named is read as `string`, which loses every distinction it exists to make.
1274
+
1275
+ Third instance of the same omission (`AgentModule` in #663, `transcriptOf` in #668), and the third
1276
+ found by installing the published package into an empty project rather than by reading the source.
1277
+
1278
+ - 5451233: Say, where a consumer can read it, why `CompatSurface` has five names and the SDK's has four
1279
+
1280
+ `CompatSurface` gained `'commands'` in #704 because this package reads
1281
+ `<projectDir>/.claude/commands/*.md` itself. The SDK's own union still has four. A caller who
1282
+ builds SDK `local` options directly therefore cannot pass a value of this type, and `TS2345` names
1283
+ the mismatch without naming the reason.
1284
+
1285
+ The explanation existed only in `//` comments, which do not survive into the emitted `.d.ts` — so
1286
+ it was invisible to exactly the audience that hits the error. It now lives in the JSDoc block that
1287
+ does ship, together with the guidance to derive the SDK's four from this five rather than writing a
1288
+ second list by hand: a hand-copied list goes stale the day a surface is added, which is the
1289
+ divergence #704 existed to remove.
1290
+
1291
+ Documentation only; no behaviour changes. Reported by a consumer who hit the compiler error while
1292
+ converging four call sites onto one declaration, and who supplied the sentence that was missing.
1293
+
1294
+ - Updated dependencies [cfe7f4c]
1295
+ - @theokit/presenter@0.9.0
1296
+
1297
+ ## 13.0.0-next.13
1298
+
1299
+ ### Minor Changes
1300
+
1301
+ - 6b07960: New `grantGate(store, classify)` in `@theokit/agents/auth` — the supported way to put a
1302
+ `PermissionStore` in force.
1303
+
1304
+ The store shipped with a careful grant key, a "deny by default, always" docblock and no reader.
1305
+ Measured: `isGranted` had zero callers outside its own unit test, and `PermissionStore` appeared in
1306
+ zero files across six sibling repositories. An operator reading `.theokit/tool-permissions.json` to
1307
+ learn what an agent may run was reading a control that was not in force — a grant and its revocation
1308
+ produced identical behaviour.
1309
+
1310
+ `grantGate` adapts the store to `pre_tool_call`, which is documented as the only hook with veto
1311
+ power and runs before the tool by construction, so a refusal is a refusal before the side effect. No
1312
+ new gate and no framework wiring: nothing is enforced unless a consumer attaches the handler, and an
1313
+ agent that does not is unaffected.
1314
+
1315
+ `classify` returns `{ governed: true, query }` **or** `{ governed: false }` — a tagged union whose
1316
+ BOTH arms carry the discriminant. A bare `undefined` would say "this tool needs no permission" and
1317
+ "I forgot this tool" in the same word, and on a security gate the second must not silently pass.
1318
+
1319
+ The discriminant is on both arms because the first shape — discriminated by whether a `governed` KEY
1320
+ was present — **failed open**: a consumer writing a policy record `{ governed: true, scope }` and
1321
+ spreading it into the query got the tool waved through, and so did `governed: undefined`. TypeScript
1322
+ does not stop that; excess properties pass freely through a variable or a spread. Fail-closed is now
1323
+ measured across `true` / `undefined` / `false` / `null`, and only `false` passes.
1324
+
1325
+ Named `grantGate`, not `permissionGate`, because `@theokit/sdk` already exports `PermissionGate`,
1326
+ `PermissionGateContext`, `PermissionGateDecision`, `PermissionEngine` and `PermissionPlugin`. The
1327
+ rename also says something true: this gates on a standing grant the operator made, and the SDK's
1328
+ permission engine is a separate system — neither satisfies the other.
1329
+
1330
+ A classifier that throws DENIES, naming the throw, rather than ending the turn. A corrupt store
1331
+ denies with a message that says so — "the permission store could not be read, so no grant applies" —
1332
+ distinct from "no standing grant matches", so an operator can tell the two apart.
1333
+
1334
+ The read error's own text is deliberately NOT in that message: the veto travels to the model, and
1335
+ `lastReadError.message` carries the absolute store path and its file mode. It stays on
1336
+ `store.lastReadError` for the operator, which is where the actionable remedy (`chmod 600 …`) lives.
1337
+ The scope IS still interpolated, so this narrows the exposure rather than eliminating it.
1338
+
1339
+ **Composing with a `pre_tool_call` you already have**: the field is singular, so assigning the gate
1340
+ over an existing handler loses one of the two silently. Compose explicitly —
1341
+ `async (ctx) => (await gate(ctx)) ?? (await mine(ctx))`; first veto wins.
1342
+
1343
+ `PermissionStore`'s docblock now states that the class enforces nothing on its own, and records the
1344
+ precedence among the surfaces IN THIS PACKAGE that can refuse a tool — saying plainly that the list
1345
+ is not exhaustive, because `@theokit/sdk` has its own permission system that neither knows about
1346
+ these nor is known by them.
1347
+
1348
+ - 5211721: `moderateOutputStream` now delivers the redacted text to the client instead of computing it and
1349
+ replaying the original events.
1350
+
1351
+ It called `runOutputGuards` and discarded the return value, so a guard that redacted correctly had
1352
+ its work thrown away: measured against the built artifact, a guard returning `[REDACTED]` delivered
1353
+ `sk-abc123`. Only `block` reached the client honestly.
1354
+
1355
+ **Signature change**: `moderateOutputStream` takes a fourth argument,
1356
+ `rebuildText: (text: string, replaced: E) => E`, which builds one event carrying the
1357
+ moderated text, given the text-carrying event it replaces. It is required rather than optional —
1358
+ optional would let the function compute a redaction it cannot apply, which is the defect being
1359
+ removed. Only the caller knows how to construct its own events.
1360
+
1361
+ **`extractText` MUST match exactly one event kind.** When it matches several, they COLLAPSE INTO
1362
+ ONE — measured: `[thinking('CoT: the key is sk-abc'), message(' Here you go.')]` yields a single
1363
+ `message` reading `"CoT: the key is [R] Here you go."`, with no `thinking` event surviving. A
1364
+ consumer who wants reasoning moderated runs a SECOND pass over that kind rather than widening one
1365
+ extractor.
1366
+
1367
+ `replaced` does not prevent that collapse, and an earlier draft of this entry said it did. What it
1368
+ buys is narrower: the surviving event keeps the KIND and metadata of the text-carrying event it
1369
+ replaces, instead of being rebuilt from the text alone. `replaced` is ALWAYS the event being replaced.
1370
+ It was typed `E | undefined` for a case that cannot happen — a stream where no event carried text
1371
+ returns from the absence check before the guards run, so `rebuildText` is not reached at all. It is
1372
+ also never a non-text event, because a caller spreading one would emit a duplicate of it.
1373
+
1374
+ **Second signature change**: a fifth argument, `rebuildResult: (text, result) => R`, applies the
1375
+ moderated text to the generator's RETURN value. A stream has two channels and the first release of
1376
+ this fix moderated one: the events were redacted while `step.value` — the aggregate `run()` returns
1377
+ — still carried the original text. Measured: the guard computed `"the key is [R]"` and
1378
+ `run().response` was `"the key is sk-abc123"`, so **`run()`, the primary non-streaming API, kept
1379
+ delivering the secret**. That was this fix's own defect one channel over. Required for the same
1380
+ reason `rebuildText` is; passed rather than re-derived, because re-running the guards on the
1381
+ aggregate would apply a non-idempotent guard twice.
1382
+
1383
+ When the text is unchanged, the buffered events are replayed verbatim as before. When it changed,
1384
+ the **last** text-carrying event is REPLACED by a newly built event carrying the whole moderated
1385
+ string, and the earlier text events are dropped. Events carrying no text are never dropped. Note
1386
+ "replaced", not "modified": any non-text payload the surviving event carried is lost, as is that of
1387
+ the dropped ones — a consumer whose text events carry per-event metadata should moderate one kind
1388
+ only, or rebuild from `replaced`. An event whose extracted text is the EMPTY STRING is still
1389
+ text-carrying and can be the one replaced.
1390
+
1391
+ **Known consequence:** when text events straddle a non-text event, their relative order does not
1392
+ survive a redaction. Given `text('tok ') , tool_call , text('sk-abc')` the client now receives
1393
+ `tool_call , text('tok [R]')` — text that preceded the tool call follows it. Landing on the last
1394
+ text-carrying event keeps a trailing terminator in place and keeps any completion claim after the
1395
+ work that produced it; what it cannot keep is the interleaving, because the redaction is about the
1396
+ whole string and the boundaries are gone by the time it exists. A test pins this so it is found
1397
+ here rather than in a transcript that stopped making sense.
1398
+
1399
+ A guard that rewrites **unconditionally** — a disclaimer appender, a trim, an NFC normaliser —
1400
+ takes this path on every stream that DOES carry text. The cost is not proportional to how much the
1401
+ guard changed. It does NOT add a text event to a round that produced none — this entry claimed so,
1402
+ and the absence check refuses it: a tool-only round yields its tool call and nothing else.
1403
+
1404
+ - 98b2565: `resolveSettingSources` now returns `readonly GatedSettingSource[]`, and
1405
+ `CompiledAgentOptions.settingSources` takes that type — so a setting root no `TrustPosture`
1406
+ authorised no longer fits the field.
1407
+
1408
+ `define-agent.ts` claimed that field "can only ever hold roots that some posture authorized".
1409
+ Measured against the emitted `.d.ts`: `setOnce(draft, 'settingSources', ['mdm','team','user','plugins'], 'cap')`
1410
+ typechecked **cast-free**. Writing a `Capability` is the documented way to extend the builder, and a
1411
+ capability writes the draft directly — so the gate was reachable around, for `project`, the root it
1412
+ exists to protect.
1413
+
1414
+ A brand rather than a runtime check, because the obvious runtime check does not work: reading
1415
+ `draft.provenance` to refuse a capability's write would also refuse the LEGITIMATE builder path,
1416
+ which writes through `setOnce` too. What differs is where the value came from, and that is what a
1417
+ brand carries.
1418
+
1419
+ **It refuses the accident, not the determined caller** — `as never` defeats it, like every brand.
1420
+ Saying so is the point: the comment it replaces claimed an invariant nothing enforced.
1421
+
1422
+ **New: `settingSources.plugins`**, taking the same `ProjectSettingsGrant` as `project`.
1423
+ `PluginsManager.refresh` loads executable bundles from the same cwd-controlled tree, usually
1424
+ arriving with the clone, so it gets the same gate and not a weaker one. This is the root the SDK
1425
+ genuinely reads and the facade withheld.
1426
+
1427
+ `team` and `mdm` stay absent, and that is the item's original premise dying under measurement: the
1428
+ SDK never reads them — `includesSetting` is called with exactly `"project"` and `"plugins"` — so
1429
+ forwarding them would be a capability in the type and nothing at runtime.
1430
+
1431
+ **Migration**: a consumer constructing `CompiledAgentOptions` by hand must build roots through
1432
+ `resolveSettingSources` instead of a string array. That is the supported construction and always was.
1433
+
1434
+ **Also: a narrowed `claudeCode.import` is now REFUSED on an SDK that cannot read it.**
1435
+
1436
+ That field's docblock said the narrowed form was "refused at resolve time" below `@theokit/sdk`
1437
+ 5.4.0. Nothing read a version for it — the only checks in this layer are the hook gate (a different
1438
+ option) and a `compatSources` warning that returns silently for any major ≥ 5. So on
1439
+ 5.0.0 ≤ SDK < 5.4.0, inside this package's declared `^4.52.1 || ^5.0.0`, a narrowed `import` was
1440
+ forwarded, dropped by the runtime in silence, and the foreign root was **not read at all** — a
1441
+ consumer asking for "the skills but not the hooks" got nothing, which is further from what they
1442
+ asked for than the un-narrowed form. `compatSources` landed in 5.0.0 and the narrowing in 5.4.0;
1443
+ treating the two versions as one was the defect.
1444
+
1445
+ `CompatImportUnsupportedError` now refuses, naming both versions and what would otherwise happen.
1446
+ It refuses rather than warns because a silent nothing is discovered by wondering why a skill is
1447
+ missing. An unreadable version is refused too: "cannot tell" and "is supported" must not collapse.
1448
+
1449
+ **And `commands` is subtracted before the compat sources reach the SDK.** The two vocabularies
1450
+ diverge by one name on purpose — `.claude/commands/*.md` is read by this package and never by the
1451
+ SDK — and `setting-sources-gate.ts` prescribed the subtraction as advice to consumers while the
1452
+ projection that needed it did not do it. Measured: `import: ['commands']` forwarded a list
1453
+ containing zero names the SDK defines, which is its own empty-list case — the exact ambiguity
1454
+ `resolveCompatSources` refuses one layer up. A source whose surfaces all belong to this layer
1455
+ is now dropped from the SDK's list rather than sent empty.
1456
+
1457
+ `CompatImportUnsupportedError` is exported from `@theokit/agents/bridge`, so a consumer can catch the
1458
+ refusal by class rather than by matching its message.
1459
+
1460
+ - 2bc5d84: `delegate()` now applies the guardrails its spec declares. It accepted them and never consulted them.
1461
+
1462
+ Measured against the built artifact with a guard declaring both halves: the input reached the model
1463
+ with its injection intact and the caller received `sk-abc123`. `bridge/agent-orchestrator.ts`
1464
+ contained **zero** occurrences of `guardrail` — control on the same sweep: `loop/agent-runner.ts`
1465
+ has 9 — and called `runReflectiveLoop` bare. The operator had declared guardrails and the run was
1466
+ green.
1467
+
1468
+ This is the same defect class as the streamed-redaction fix in this release, on a sibling public
1469
+ API, and it is model-reachable: `tools/delegate-tool.ts` wraps `delegate()`, so an agent can invoke
1470
+ a sub-agent whose declared guards do nothing.
1471
+
1472
+ `checkInput` runs after `onDelegationStart` and `checkOutput` after `onDelegationComplete` — each
1473
+ moderating what actually crosses the boundary rather than a string a hook may then rewrite. A
1474
+ blocking input guard throws before the model is called at all, so a refused delegation costs
1475
+ nothing.
1476
+
1477
+ `response` only. `toolCalls[].output` is tool output rather than model text, and `agent-runner.ts`
1478
+ excludes it from `extractText` on the same reasoning; the docblock says so, because leaving it alone
1479
+ should be a decision somebody reads rather than an omission somebody discovers.
1480
+
1481
+ A spec declaring no guardrails behaves byte-identically.
1482
+
1483
+ **A guardrail block now crosses the delegate tool as a refusal, not a crash.** `errorCodeOf` mapped
1484
+ only the three delegation errors, so `GuardrailViolationError` — reachable from `delegate()` for the
1485
+ first time because of this change — hit the "not a delegation outcome, a defect" arm and was
1486
+ rethrown, ending the parent's turn. The tool's own description, shipped to the model, promises
1487
+ `{ ok: false, error, message }` on a refusal.
1488
+
1489
+ It crosses as `guardrail_violation` with a FIXED message: `the delegated task was refused by a
1490
+ policy guard`. The message travels only for codes on an explicit ALLOWLIST — a budget or a timeout
1491
+ is a fact about the work, and the number in it is what the model acts on. A code nobody lists
1492
+ withholds, so forgetting is safe in the direction that matters. A guardrail message is not — it reads
1493
+ `Guardrail "pii-detector" blocked output: ssn found`, naming the guard and its exact trigger, and a
1494
+ model given that learns which words to avoid rather than that it should stop. The operator keeps the
1495
+ full typed error, which carries `guardName`, `phase` and `reason`.
1496
+
1497
+ - 019f828: Output guards now moderate `thinking` events, not only `text_delta`.
1498
+
1499
+ `AgentRunner` handed `moderateOutputStream` an extractor matching `text_delta` and nothing else,
1500
+ while `thinking` is a public `AgentStreamEvent` that reaches the client like any other. Measured: a
1501
+ guard declared over the agent's output delivered `thinking "the key is sk-abc123"` verbatim.
1502
+
1503
+ **This closes one channel and does not close all of them.** `DoneEvent.result` carries the model's
1504
+ whole answer and is still unmoderated — measured on the same turn, `text_delta` came out
1505
+ `"here: [R]"` while `done.result` came out `"here: sk-abc123"`. `task_progress.text` is a fourth and
1506
+ reaches the web wire. A third pass does not extend to them: there is one `done` per round, so a pass
1507
+ keyed on it would collapse every round's into one, and they need a different mechanism. Tracked
1508
+ separately; stated here because a security note that overstates its coverage is worse than one that
1509
+ does not exist.
1510
+
1511
+ The third channel of a shape fixed twice already in this release — a streamed redaction that was
1512
+ computed and discarded, and `delegate()` consulting no guards at all.
1513
+
1514
+ **Two passes, not one wider extractor.** Widening `extractText` to match both kinds is the obvious
1515
+ move and the wrong one: two kinds under one extractor COLLAPSE into a single event, so the reasoning
1516
+ would be promoted into a visible one — the moderation creating the disclosure it exists to close.
1517
+ Composing two passes is what `moderateOutputStream`'s own docblock prescribes, and each pass seeing
1518
+ one kind is what keeps them apart.
1519
+
1520
+ The visible pass owns the aggregate: `DelegationResult.response` accumulates from `text_delta`
1521
+ upstream, so the reasoning pass passes the result through rather than replacing it.
1522
+
1523
+ A blocking guard on either channel still throws before any event is emitted. An agent with no output
1524
+ guard is byte-identical.
1525
+
1526
+ - 0731584: `resolveCompatSources` now returns `readonly GatedCompatSource[]`, and
1527
+ `CompiledAgentOptions.compatSources` takes that type — so a compat source no `TrustPosture`
1528
+ authorised no longer fits the field.
1529
+
1530
+ **BREAKING for hand-built compiled options**, exactly as its sibling was. Build compat sources
1531
+ through `resolveCompatSources`.
1532
+
1533
+ The twin of the `settingSources` brand, and it exists because that fix closed one of the two fields
1534
+ one `SettingSourcesSelection` feeds and left the other bare. Measured, with the `settingSources`
1535
+ route as the control: the control errored, and `setOnce(draft, 'compatSources', ['claude-code'],
1536
+ 'cap')` compiled cast-free — while `agent-compiler.ts` told the reader that field "can only hold a
1537
+ source some posture granted".
1538
+
1539
+ It carries more authority than its twin, not less. `applyLocalSources` forwards it to
1540
+ `Agent.create({ local: { compatSources } })`, which reads `<cwd>/.claude/` — `hooks.json` included,
1541
+ and that executes shell.
1542
+
1543
+ **Signature narrowing**: `moderateOutputStream`'s `rebuildText` is now
1544
+ `(text: string, replaced: E) => E`. It was typed `E | undefined` for a case that cannot happen — a
1545
+ stream where no event carried text returns from the absence check before the guards run, so
1546
+ `rebuildText` is never reached. The branch handling that case was dead code, and three shipping
1547
+ artifacts described it as live.
1548
+
1549
+ **Fixed**: `isPort` discriminated on the presence of `run`, so an object carrying both `compiled`
1550
+ and a `run` — reachable through a spread, which is how targets are built in practice — took the port
1551
+ branch and skipped `delegate()` entirely: no declared guardrails, no inherited parent veto, no
1552
+ budget clamp. A tie now goes to the spec, because the spec branch is the guarded one.
1553
+
1554
+ ### Patch Changes
1555
+
1556
+ - bb0f451: A guardrail refusal thrown inside a round is no longer renamed into a delegation failure.
1557
+
1558
+ `runReflectiveLoop` wrapped any error that was not already a delegation error into
1559
+ `DelegationError`, whose message reads `Delegation to agent "X" failed: ${cause.message}`. That code
1560
+ is on the delegate tool's message allowlist — a delegation failure's text is a fact about the work —
1561
+ so the wrapper carried the guard's own words to the model: `Guardrail "pii-detector" blocked output:
1562
+ ssn found`, naming the guard and its exact trigger.
1563
+
1564
+ Measured through `createDelegateTool` with a consumer-supplied `streamFactory` that throws
1565
+ mid-round. `streamFactory` is a public option, so this was reachable rather than theoretical.
1566
+
1567
+ `GuardrailViolationError` now passes through as itself, alongside the two delegation errors that
1568
+ already did, so the tool classifies it `guardrail_violation` and withholds the message.
1569
+
1570
+ Fixed by classification rather than by suppressing text downstream: a guard refusal is not a
1571
+ delegation failure, and a layer that renames an error cannot be expected to maintain a list of what
1572
+ the new name must hide.
1573
+
1574
+ - ac29eff: A permission-gate veto now emits a debug line.
1575
+
1576
+ `grantGate` refused and emitted nothing — no log, no counter, no debug line. An operator could
1577
+ observe the refusal only through the tool result the model received, and the two causes the veto
1578
+ message distinguishes ("no standing grant matches" versus "the permission store could not be read,
1579
+ so no grant applies") are indistinguishable from there.
1580
+
1581
+ That is pillar 3 of the wiring triad missing on a refusal seam. `bridge/approval-posture.ts`, the
1582
+ sibling gate, already logged through this exact seam.
1583
+
1584
+ The QUERY is logged — tool, scope, and which of the two causes fired — and the grant is not: a query
1585
+ names what an operator needs to diagnose, while the store's contents are the thing being protected.
1586
+ A classifier that threw logs as its own event rather than as an ordinary veto, so a consumer-code
1587
+ defect is not read as a denied tool.
1588
+
1589
+ ## 13.0.0-next.12
1590
+
1591
+ ### Minor Changes
1592
+
1593
+ - a7e35bb: A guardrail returning `action: 'redact'` with no replacement `text` now throws
1594
+ `MalformedGuardrailResultError` instead of silently redacting nothing.
1595
+
1596
+ `GuardrailResult.text` is optional, so such a guard compiles and reads like a working one. The
1597
+ pipeline tested `r.text !== undefined` and moved on, so the caller received the original text and
1598
+ believed a guard had run on it — the operator believing a protection is in place when none is.
1599
+
1600
+ **This is a behaviour change.** A guard relying on the previous no-op will now throw. That is
1601
+ deliberate: the alternative is unredacted output reaching a model because a guard was written wrong.
1602
+ `text: ''` is unaffected and always was a real redaction — a guard choosing to erase everything.
1603
+
1604
+ `MalformedGuardrailResultError` is exported from `@theokit/agents`, carries the guard's name and the
1605
+ phase, and is not retryable.
1606
+
1607
+ This also reaches the streaming path (`moderateOutputStream`), which shares the same pipeline: a
1608
+ malformed guard there now throws where it previously continued.
1609
+
1610
+ ### Patch Changes
1611
+
1612
+ - 2bc27d3: `inheritHooks` no longer lets a member's `transform_tool_result` or `pre_user_send` handler replace
1613
+ its parent's — both now chain parent-first, matching the six events that already composed.
1614
+
1615
+ `inheritHooks` documents its security property as "the parent's refusal is evaluated first, and a
1616
+ member can only ever ADD a reason to refuse". For these two events the plain object spread did the
1617
+ opposite: a member declaring either handler silently discarded the parent's. The reachable surface is the EXPORTED `inheritHooks`, called with two handler maps.
1618
+ `delegate()` passes `undefined` for the member (`agent-orchestrator.ts:175`), so that path composed
1619
+ nothing and was never affected — a distinction the first version of this note got wrong.
1620
+
1621
+ `pre_user_send` composes additively — both contributions reach the model, parent first — because
1622
+ `PreUserSendResult` carries only `recalledContext` and the seam exposes no prompt mutation.
1623
+
1624
+ - eaac7b0: The "will NOT fire" warning for a declared-but-unwired hook event now names where the capability
1625
+ already lives, instead of ending "the handler does not exist yet".
1626
+
1627
+ Two of the three unwired events are served today by purpose-built seams — `Guardrail.checkOutput`
1628
+ for `transform_llm_output`, and `createToolHooksPlugin({ processInput })` for `pre_user_send` — so
1629
+ the old message told consumers to wait for work that will not come. The third, `on_session_end`, is
1630
+ named as genuinely uncovered, with the reason: its handler returns `void` and cannot refuse an
1631
+ ending, so wiring it would produce a hook that runs and cannot decide.
1632
+
1633
+ Nothing is wired. `HOOK_EVENTS`, `WIRED_EVENTS` and `OBSERVATIONAL_EVENTS` keep the same members.
1634
+
1635
+ - ec899f4: An observational hook handler is now assigned to its own key rather than chosen by comparison.
1636
+
1637
+ The dispatch loop used a two-branch conditional over a list of two event names, so a third
1638
+ observational event would have landed on `post_assistant_reply` — silently, with no test objecting.
1639
+ No behaviour changes for the events wired today; the fix removes the trap for the next one added.
1640
+
1641
+ ## 13.0.0-next.11
1642
+
1643
+ ### Patch Changes
1644
+
1645
+ - 5451233: Say, where a consumer can read it, why `CompatSurface` has five names and the SDK's has four
1646
+
1647
+ `CompatSurface` gained `'commands'` in #704 because this package reads
1648
+ `<projectDir>/.claude/commands/*.md` itself. The SDK's own union still has four. A caller who
1649
+ builds SDK `local` options directly therefore cannot pass a value of this type, and `TS2345` names
1650
+ the mismatch without naming the reason.
1651
+
1652
+ The explanation existed only in `//` comments, which do not survive into the emitted `.d.ts` — so
1653
+ it was invisible to exactly the audience that hits the error. It now lives in the JSDoc block that
1654
+ does ship, together with the guidance to derive the SDK's four from this five rather than writing a
1655
+ second list by hand: a hand-copied list goes stale the day a surface is added, which is the
1656
+ divergence #704 existed to remove.
1657
+
1658
+ Documentation only; no behaviour changes. Reported by a consumer who hit the compiler error while
1659
+ converging four call sites onto one declaration, and who supplied the sentence that was missing.
1660
+
1661
+ ## 13.0.0-next.10
1662
+
1663
+ ### Minor Changes
1664
+
1665
+ - ca44ee7: A narrowed foreign root can name `commands`
1666
+
1667
+ `settingSources.claudeCode.import` narrows the foreign root to named surfaces, and `CompatSurface`
1668
+ listed four of them — `hooks`, `plugins`, `skills`, `subagents`. It fed a fifth: this package loads
1669
+ `<projectDir>/.claude/commands/*.md` itself, outside the SDK's `compatSources` path. The name was
1670
+ missing from the vocabulary, so a consumer could neither ask for that directory nor be told it had
1671
+ gone unread, and `loadCustomCommands` tested `sources.includes('claude-code')` — string equality
1672
+ against a union whose narrowed member is an object, so every narrowed list read as _not declared_.
1673
+
1674
+ `CompatSurface` now includes `'commands'`, and the loader understands both shapes: the bare source
1675
+ name grants every surface the root feeds, the narrowed form grants the ones it names.
1676
+
1677
+ Purely additive — before this, no narrowed list reached the directory at all, so nothing that works
1678
+ today stops working. A narrowed list that wants foreign commands adds `'commands'` to `import`.
1679
+
1680
+ An enumeration used to narrow a root must cover every surface that root feeds; otherwise it is not
1681
+ a narrowing but an undeclared drop. Reported as usetheokit/theokit#704, found by measuring a claim
1682
+ in a consumer's adoption report rather than by any test here — the fifth time a gap in this
1683
+ package's public surface was invisible from inside it.
1684
+
1685
+ ## 13.0.0-next.9
1686
+
1687
+ ### Minor Changes
1688
+
1689
+ - c59abf6: A foreign configuration root can be imported in part
1690
+
1691
+ `resolveCompatSources` returned the bare literal `'claude-code'`, which the SDK reads as "import
1692
+ every surface" — hooks, plugins, skills, subagents. `settingSources.claudeCode.import` now names the
1693
+ surfaces, and the resolved value carries them.
1694
+
1695
+ The distinction is the reason the grant exists: `.claude/` usually arrives with the clone and its
1696
+ `hooks.json` executes shell, so "take the skills, refuse the hooks" is the ordinary thing to want,
1697
+ and the only choices were all of it or none of it.
1698
+
1699
+ Absent `import` still means the whole root, so nothing existing changes. An EMPTY list is refused
1700
+ rather than guessed: "no surfaces" and "unset, so all of them" are both defensible readings of `[]`,
1701
+ they differ by whether shell executes, and picking one would settle a security question by
1702
+ convention.
1703
+
1704
+ `CompatSurface` and `ResolvedCompatSource` are declared here rather than imported from the SDK, for
1705
+ the reason already recorded beside the literal: they do not exist in `@theokit/sdk@4.52.1`, this
1706
+ package's declared floor.
1707
+
1708
+ Closes usetheokit/theokit#686.
1709
+
3
1710
  ## 13.0.0-next.8
4
1711
 
5
1712
  ### Minor Changes