@theokit/sdk 5.5.0 → 5.7.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 (160) hide show
  1. package/CHANGELOG.md +667 -0
  2. package/dist/{agent-CjMVbhhY.d.ts → agent-Bqzdhkqr.d.ts} +12 -3
  3. package/dist/{agent-DyB_lzrx.d.cts → agent-Dya5SmhY.d.cts} +12 -3
  4. package/dist/{agent-G3T7Q3SQ.js → agent-EQ5HKTWV.js} +15 -14
  5. package/dist/{agent-G3T7Q3SQ.js.map → agent-EQ5HKTWV.js.map} +1 -1
  6. package/dist/{agent-YWSIHHFM.cjs → agent-KA63WDQH.cjs} +16 -15
  7. package/dist/{agent-YWSIHHFM.cjs.map → agent-KA63WDQH.cjs.map} +1 -1
  8. package/dist/agent.d.ts +11 -0
  9. package/dist/{chunk-Z2JFX372.cjs → chunk-33A27NFX.cjs} +15 -15
  10. package/dist/{chunk-Z2JFX372.cjs.map → chunk-33A27NFX.cjs.map} +1 -1
  11. package/dist/{chunk-N6OOOYFZ.js → chunk-3VXQEQE4.js} +13 -4
  12. package/dist/chunk-3VXQEQE4.js.map +1 -0
  13. package/dist/{chunk-DZG77WU7.cjs → chunk-3XSWF5AP.cjs} +3 -3
  14. package/dist/{chunk-DZG77WU7.cjs.map → chunk-3XSWF5AP.cjs.map} +1 -1
  15. package/dist/{chunk-M3ZGRGOG.js → chunk-6JG2RFGK.js} +3 -3
  16. package/dist/{chunk-M3ZGRGOG.js.map → chunk-6JG2RFGK.js.map} +1 -1
  17. package/dist/{chunk-S6B5XYC3.js → chunk-7C3HWGCW.js} +33 -22
  18. package/dist/chunk-7C3HWGCW.js.map +1 -0
  19. package/dist/{chunk-LOHMT36V.cjs → chunk-7KBNBPFK.cjs} +75 -2
  20. package/dist/chunk-7KBNBPFK.cjs.map +1 -0
  21. package/dist/{chunk-UKJJRU7C.cjs → chunk-A575WPBL.cjs} +330 -129
  22. package/dist/chunk-A575WPBL.cjs.map +1 -0
  23. package/dist/{chunk-2XRAWOZZ.js → chunk-A6PDYH55.js} +74 -3
  24. package/dist/chunk-A6PDYH55.js.map +1 -0
  25. package/dist/{chunk-LX7SEXOQ.js → chunk-CA5VUAP3.js} +25 -7
  26. package/dist/chunk-CA5VUAP3.js.map +1 -0
  27. package/dist/{chunk-2QKV4CGK.js → chunk-CZWN4C6A.js} +3 -3
  28. package/dist/{chunk-2QKV4CGK.js.map → chunk-CZWN4C6A.js.map} +1 -1
  29. package/dist/chunk-DQNI52VV.js +20 -0
  30. package/dist/chunk-DQNI52VV.js.map +1 -0
  31. package/dist/{chunk-VPK6PHIE.cjs → chunk-E6QXZZKE.cjs} +8 -8
  32. package/dist/{chunk-VPK6PHIE.cjs.map → chunk-E6QXZZKE.cjs.map} +1 -1
  33. package/dist/{chunk-IU5N5224.cjs → chunk-EFETPSJZ.cjs} +17 -8
  34. package/dist/chunk-EFETPSJZ.cjs.map +1 -0
  35. package/dist/{chunk-2ZLQZHZC.cjs → chunk-ER5BOIYD.cjs} +4 -4
  36. package/dist/{chunk-2ZLQZHZC.cjs.map → chunk-ER5BOIYD.cjs.map} +1 -1
  37. package/dist/{chunk-ODBID5TG.js → chunk-FUUSIT2T.js} +234 -35
  38. package/dist/chunk-FUUSIT2T.js.map +1 -0
  39. package/dist/{chunk-VY6NKMOA.cjs → chunk-GK3UG7YQ.cjs} +34 -25
  40. package/dist/chunk-GK3UG7YQ.cjs.map +1 -0
  41. package/dist/{chunk-NQTNSHSB.cjs → chunk-HNAYCTX4.cjs} +5 -5
  42. package/dist/chunk-HNAYCTX4.cjs.map +1 -0
  43. package/dist/{chunk-QATRS7JD.cjs → chunk-MYJGWS2J.cjs} +26 -8
  44. package/dist/chunk-MYJGWS2J.cjs.map +1 -0
  45. package/dist/{chunk-KZSKIUXZ.js → chunk-N7VAE6DZ.js} +31 -3
  46. package/dist/chunk-N7VAE6DZ.js.map +1 -0
  47. package/dist/{chunk-7SZAV6QG.js → chunk-P5S3WCSZ.js} +3 -3
  48. package/dist/{chunk-7SZAV6QG.js.map → chunk-P5S3WCSZ.js.map} +1 -1
  49. package/dist/{chunk-HW7SEELD.cjs → chunk-QRVS2PRE.cjs} +31 -8
  50. package/dist/chunk-QRVS2PRE.cjs.map +1 -0
  51. package/dist/{chunk-SADXXGWU.js → chunk-R3LBRQAN.js} +3 -3
  52. package/dist/{chunk-NQTNSHSB.cjs.map → chunk-R3LBRQAN.js.map} +1 -1
  53. package/dist/{chunk-AYA65JA5.cjs → chunk-RWPLWMCZ.cjs} +25 -9
  54. package/dist/chunk-RWPLWMCZ.cjs.map +1 -0
  55. package/dist/{chunk-O7L7M42F.js → chunk-T3ZDEYTJ.js} +21 -5
  56. package/dist/chunk-T3ZDEYTJ.js.map +1 -0
  57. package/dist/{chunk-43YXGD3P.cjs → chunk-TY56BKSK.cjs} +8 -4
  58. package/dist/chunk-TY56BKSK.cjs.map +1 -0
  59. package/dist/chunk-UOLBAPDM.js +66 -0
  60. package/dist/chunk-UOLBAPDM.js.map +1 -0
  61. package/dist/{chunk-NSLHPAC7.js → chunk-X7EUUHXU.js} +6 -5
  62. package/dist/chunk-X7EUUHXU.js.map +1 -0
  63. package/dist/{chunk-WS5ULCL4.js → chunk-YRLI5DVC.js} +3 -3
  64. package/dist/{chunk-WS5ULCL4.js.map → chunk-YRLI5DVC.js.map} +1 -1
  65. package/dist/chunk-YYN6BB7R.cjs +24 -0
  66. package/dist/chunk-YYN6BB7R.cjs.map +1 -0
  67. package/dist/{chunk-C4ZQPWXJ.cjs → chunk-ZLMU5EU3.cjs} +33 -4
  68. package/dist/chunk-ZLMU5EU3.cjs.map +1 -0
  69. package/dist/{compact-session-7FMYJXQ7.cjs → compact-session-75KD2FCM.cjs} +15 -11
  70. package/dist/{compact-session-7FMYJXQ7.cjs.map → compact-session-75KD2FCM.cjs.map} +1 -1
  71. package/dist/{compact-session-YSEK7NQF.js → compact-session-QT6RJOKO.js} +3 -3
  72. package/dist/{compact-session-YSEK7NQF.js.map → compact-session-QT6RJOKO.js.map} +1 -1
  73. package/dist/context/index.cjs +11 -7
  74. package/dist/context/index.cjs.map +1 -1
  75. package/dist/context/index.js +7 -3
  76. package/dist/context/index.js.map +1 -1
  77. package/dist/context-7RP5JXGM.cjs +24 -0
  78. package/dist/{context-JTGSBJT6.cjs.map → context-7RP5JXGM.cjs.map} +1 -1
  79. package/dist/context-OJIIXJCV.js +7 -0
  80. package/dist/{context-3HPU754C.js.map → context-OJIIXJCV.js.map} +1 -1
  81. package/dist/{cron-Bgivg88c.d.cts → cron-BwxDUscv.d.cts} +1 -1
  82. package/dist/{cron-CNUa7PDo.d.ts → cron-D8uB1Xh5.d.ts} +1 -1
  83. package/dist/cron.cjs +15 -14
  84. package/dist/cron.d.cts +2 -2
  85. package/dist/cron.d.ts +2 -2
  86. package/dist/cron.js +14 -13
  87. package/dist/eval.cjs +14 -13
  88. package/dist/eval.cjs.map +1 -1
  89. package/dist/eval.js +13 -12
  90. package/dist/eval.js.map +1 -1
  91. package/dist/{index-manager-3UNPYH34.cjs → index-manager-W7RHSL6G.cjs} +6 -6
  92. package/dist/{index-manager-3UNPYH34.cjs.map → index-manager-W7RHSL6G.cjs.map} +1 -1
  93. package/dist/{index-manager-W7FDMGEG.js → index-manager-Z4XMRVWQ.js} +5 -5
  94. package/dist/{index-manager-W7FDMGEG.js.map → index-manager-Z4XMRVWQ.js.map} +1 -1
  95. package/dist/index.cjs +284 -49
  96. package/dist/index.cjs.map +1 -1
  97. package/dist/index.d.cts +197 -5
  98. package/dist/index.d.ts +197 -5
  99. package/dist/index.js +249 -25
  100. package/dist/index.js.map +1 -1
  101. package/dist/{inject-session-PC73TMP2.cjs → inject-session-L6MIIELT.cjs} +4 -4
  102. package/dist/{inject-session-PC73TMP2.cjs.map → inject-session-L6MIIELT.cjs.map} +1 -1
  103. package/dist/{inject-session-J4LBWAPH.js → inject-session-XWWN4DAA.js} +3 -3
  104. package/dist/{inject-session-J4LBWAPH.js.map → inject-session-XWWN4DAA.js.map} +1 -1
  105. package/dist/internal/memory/storage/index.cjs +32 -32
  106. package/dist/internal/memory/storage/index.js +3 -3
  107. package/dist/internal/memory/storage/memory-root.d.cts +27 -0
  108. package/dist/internal/memory/storage/memory-root.d.ts +27 -0
  109. package/dist/internal/persistence/index.cjs +4 -4
  110. package/dist/internal/persistence/index.js +1 -1
  111. package/dist/internal/runtime/compat/foreign-config-sources.d.ts +17 -4
  112. package/dist/internal/runtime/compat/managed-settings.d.ts +80 -0
  113. package/dist/internal/runtime/context/context-discovery-runner.d.ts +14 -0
  114. package/dist/internal/runtime/context/context-discovery.d.ts +64 -0
  115. package/dist/internal/runtime/context/context-manager.d.ts +21 -1
  116. package/dist/internal/runtime/context/yaml-frontmatter.d.ts +6 -3
  117. package/dist/internal/runtime/hooks/hooks-executor.d.ts +13 -1
  118. package/dist/internal/runtime/hooks/hooks-source.d.ts +36 -1
  119. package/dist/internal/runtime/skills/discover-skills.d.ts +4 -0
  120. package/dist/internal/session/compact-session.d.ts +36 -0
  121. package/dist/project.cjs +3 -3
  122. package/dist/project.js +1 -1
  123. package/dist/skills.cjs +5 -5
  124. package/dist/skills.js +2 -2
  125. package/dist/subagents-loader-CJFYQQU2.js +7 -0
  126. package/dist/{subagents-loader-AIVDQ2D5.js.map → subagents-loader-CJFYQQU2.js.map} +1 -1
  127. package/dist/subagents-loader-MOO7DC4E.cjs +16 -0
  128. package/dist/{subagents-loader-DN4LETGL.cjs.map → subagents-loader-MOO7DC4E.cjs.map} +1 -1
  129. package/dist/subagents-loader.cjs +4 -4
  130. package/dist/subagents-loader.d.cts +1 -1
  131. package/dist/subagents-loader.d.ts +1 -1
  132. package/dist/subagents-loader.js +3 -3
  133. package/dist/types/agent.d.ts +7 -2
  134. package/docs/error-codes.md +20 -18
  135. package/docs/harness-capability-map.md +9 -1
  136. package/package.json +1 -1
  137. package/dist/chunk-2XRAWOZZ.js.map +0 -1
  138. package/dist/chunk-43YXGD3P.cjs.map +0 -1
  139. package/dist/chunk-AYA65JA5.cjs.map +0 -1
  140. package/dist/chunk-C4ZQPWXJ.cjs.map +0 -1
  141. package/dist/chunk-HW7SEELD.cjs.map +0 -1
  142. package/dist/chunk-IU5N5224.cjs.map +0 -1
  143. package/dist/chunk-JNAA4G4H.js +0 -43
  144. package/dist/chunk-JNAA4G4H.js.map +0 -1
  145. package/dist/chunk-KZSKIUXZ.js.map +0 -1
  146. package/dist/chunk-LOHMT36V.cjs.map +0 -1
  147. package/dist/chunk-LX7SEXOQ.js.map +0 -1
  148. package/dist/chunk-N6OOOYFZ.js.map +0 -1
  149. package/dist/chunk-NSLHPAC7.js.map +0 -1
  150. package/dist/chunk-O7L7M42F.js.map +0 -1
  151. package/dist/chunk-ODBID5TG.js.map +0 -1
  152. package/dist/chunk-QATRS7JD.cjs.map +0 -1
  153. package/dist/chunk-S6B5XYC3.js.map +0 -1
  154. package/dist/chunk-SADXXGWU.js.map +0 -1
  155. package/dist/chunk-UKJJRU7C.cjs.map +0 -1
  156. package/dist/chunk-VY6NKMOA.cjs.map +0 -1
  157. package/dist/context-3HPU754C.js +0 -6
  158. package/dist/context-JTGSBJT6.cjs +0 -23
  159. package/dist/subagents-loader-AIVDQ2D5.js +0 -7
  160. package/dist/subagents-loader-DN4LETGL.cjs +0 -16
package/CHANGELOG.md CHANGED
@@ -1,5 +1,672 @@
1
1
  # Changelog
2
2
 
3
+ ## 5.7.0
4
+
5
+ ### Minor Changes
6
+
7
+ - [#675](https://github.com/usetheokit/theokit-sdk/pull/675) [`cbbed19`](https://github.com/usetheokit/theokit-sdk/commit/cbbed19abff45b58c10de1bf694524fbd246d6c2) Thanks [@usetheodev](https://github.com/usetheodev)! - A repo-root instruction file now requires a grant a consumer can write.
8
+
9
+ `DEFAULT_DISCOVERY_SPECS` carries four repo-root instruction files whose bodies enter the system
10
+ prompt as if we had written them — `AGENTS.md`, `GEMINI.md`, `CLAUDE.md` and `.cursor/rules/*.mdc`.
11
+ Until now exactly ONE spec in that file carried a gate, and it was `.claude/rules/*.md`. A cloned
12
+ repository's `.claude/rules/*.md` needed a declaration; the `CLAUDE.md` one directory up did not,
13
+ while both carried the same risk.
14
+
15
+ **The vocabulary landed before the gate, and the order is the whole point.** `CompatSource` went
16
+ from two literals to five:
17
+
18
+ ```ts
19
+ compatSources: ["agents"] // AGENTS.md is admitted; GEMINI.md is not
20
+ ```
21
+
22
+ Gating the four against the OLD union would have gated three formats on a grant nobody could write
23
+ and made them permanently unreachable — worse than the gap. A test asserts the invariant in both
24
+ directions and fails the moment a spec is gated on a token the union does not carry.
25
+
26
+ **Separate literals, not one `foreign` token**, so a consumer who wants `AGENTS.md` is not forced to
27
+ also admit `.cursor/rules/*.mdc`.
28
+
29
+ **BREAKING for a consumer who declares nothing**: they went from four repo-root files in the prompt
30
+ to none. That is the intended behaviour and it is LOUD — `withheldSpecs` reports every withheld file
31
+ by path with the grant that restores it, and the runner warns once naming each one. A count would be
32
+ a diagnosis with no remedy: you cannot decide whether to grant a dialect without knowing which file
33
+ it brings.
34
+
35
+ `undefined` and `[]` stay distinct: no gate configured admits everything, an empty declaration
36
+ withholds every gated spec. Collapsing them would restore the defect the first gate closed.
37
+
38
+ - [#675](https://github.com/usetheokit/theokit-sdk/pull/675) [`de40d9f`](https://github.com/usetheokit/theokit-sdk/commit/de40d9f3d398f84d5b145cec8bc56a4c6ec85a25) Thanks [@usetheodev](https://github.com/usetheodev)! - The pre-compaction seam now reaches the compaction path a consumer actually calls.
39
+
40
+ There are three compaction paths in this ecosystem. `withPreCompaction`, shipped in
41
+ `@theokit/agents@14.1.0`, covers one of them — the agents-layer strategy. The other two are
42
+ `autoCompactIfNeeded` and `Agent.compact(sessionId, { trigger })`, and **the second is the one a
43
+ person reaches**: `/compact` → `handleCompact` → `compactSession` → `Agent.compact`.
44
+
45
+ ```ts
46
+ await Agent.compact(agentId, {
47
+ trigger: "manual",
48
+ onPreCompact: async ({ messages, trigger, signal }) => {
49
+ await archive(messages, { because: trigger }) // runs BEFORE the transcript is rewritten
50
+ },
51
+ onPreCompactError: (e) => report(e), // compaction proceeds either way
52
+ })
53
+ ```
54
+
55
+ **One seam covers both SDK paths** because they converge on `compactSessionTranscript`. A seam per
56
+ caller would need each one to choose, and a caller that chooses wrong reintroduces the gap.
57
+
58
+ **`trigger` is received, not inferred.** It already existed in the options and already reached the
59
+ compaction boundary; the handler is simply told. Inferring it from the call site would be right today
60
+ and silently wrong the first time a third caller appears.
61
+
62
+ **Failure never blocks compaction.** A handler that throws is reported; one that exceeds its bound is
63
+ abandoned with its `AbortSignal` aborted first, so it can stop rather than be left running. A hook
64
+ that could block would turn a consumer's bug into a runtime that cannot reclaim its context.
65
+
66
+ **Nothing changes for a consumer who registers nothing** — the seam is not entered, asserted by a
67
+ test rather than left as an inference.
68
+
69
+ ## 5.6.0
70
+
71
+ ### Minor Changes
72
+
73
+ - [#659](https://github.com/usetheokit/theokit-sdk/pull/659) [`0aca2bf`](https://github.com/usetheokit/theokit-sdk/commit/0aca2bfd8bca0aa57f9929f0121c1bdf2159fc92) Thanks [@usetheodev](https://github.com/usetheodev)! - Two tiers no permission rule and no mode can reach: protected paths and critical paths.
74
+
75
+ Measured: `protectedPath` returned 0 files here and 0 in the dist; `criticalPath` the same, against a
76
+ control of 31/72 on the word `hooks`.
77
+
78
+ **Protected paths** are the circuit breaker that stops an agent editing its own configuration, the
79
+ git hooks or the shell rc files — `.git`, `.claude`, `.theokit`, `.ssh`, `.gnupg`, `.envrc`,
80
+ `.npmrc`, `.mcp.json`, `.pre-commit-config.yaml`, `.netrc`. An `allow`-broad setup wrote all of them
81
+ freely and the operator had no way to express the exception, because the concept was absent.
82
+
83
+ **Critical paths** are destructive operations on `/`, the home directory, the working directory and
84
+ its parents. The nearest analogue was `catastrophicShellReason` in `@theokit/sdk-tools`, reached
85
+ through an opt-in `denyCatastrophicCommands()` — and **an opt-in guard is not a floor**. The whole
86
+ point of this tier is that nothing overrides it; a function a consumer may forget to call makes the
87
+ guarantee a convention.
88
+
89
+ **Why a floor and not a deny rule.** A deny rule is ordered, and order is defeasible: it can be
90
+ shadowed by a broader rule above it, reordered, or simply not shipped. The floor is consulted before
91
+ any verdict is honoured and no rule can reach it. Each test pairs the refusal with an explicit allow
92
+ rule for the same call, because the point is not that the operation is refused — it is that the rule
93
+ loses.
94
+
95
+ **Substitutions are refused, not expanded.** The spec names `$(...)` and `"$VAR"/*`, and in both the
96
+ dangerous argument is not in the text being matched. Expanding would mean running the substitution,
97
+ and a floor that executes its input to decide whether the input is safe has the problem backwards.
98
+
99
+ **Segment-aware, never prefix-matching.** `/workspace-other` starts with `/work` as a string and is a
100
+ different directory; `.gitignore` contains `.git` and is a file projects edit routinely. A floor that
101
+ could not tell them apart would refuse ordinary work while claiming to protect something else.
102
+
103
+ The refusal **names the path and says which tier refused**, so it is not mistaken for a permissions
104
+ misconfiguration — an operator who reads "denied" goes looking at their rules and finds nothing wrong
105
+ with them.
106
+
107
+ **Reads are not gated**, deliberately: the tier is about writes and destruction, and refusing reads
108
+ would stop an agent inspecting the repository it was pointed at.
109
+
110
+ Stated rather than implied: this is **not a shell parser**. It recognises the destructive shapes the
111
+ spec names, on the arguments it names, and a determined obfuscation gets past it — `rm` reached
112
+ through a variable holding the command name, for instance. A floor described as complete would be
113
+ trusted as complete.
114
+
115
+ - [#660](https://github.com/usetheokit/theokit-sdk/pull/660) [`612da2f`](https://github.com/usetheokit/theokit-sdk/commit/612da2fcb96ce498973aeb60e05650e32f7c4216) Thanks [@usetheodev](https://github.com/usetheodev)! - A foreign root's rules now need the grant that already gates everything else in that directory.
116
+
117
+ `FileContextManager.initialize()` gated project-level context on
118
+ `settingSourcesIncludeProject || settings.manager === "file"` and consulted no foreign-dialect
119
+ grant. `compatSources` appeared nowhere under `src/internal/runtime/context/`. So a consumer who
120
+ enabled project scope for its OWN `.theokit/` and deliberately never declared `claude-code` still
121
+ received a cloned repository's `.claude/rules/*.md` in its system prompt — while the same
122
+ directory's hooks, skills, subagents and plugins were correctly withheld. Four surfaces failing
123
+ closed, and a fifth nobody had wired to the gate ([#652](https://github.com/usetheokit/theokit-sdk/issues/652)).
124
+
125
+ The root cause was not a missing `if`. `CompatSurface` was `"hooks" | "plugins" | "skills" |
126
+ "subagents"`: there was no member for instructions, so no grant could govern them and the gate had
127
+ nothing to consult. `"context"` is now a surface like the others, paired to its runtime list by the
128
+ existing compile-time exhaustiveness guard, and each discovery spec names the dialect whose grant
129
+ gates it. Adding a dialect is adding a row.
130
+
131
+ **This is a behaviour change, and the note is here rather than buried.** A consumer who declared no
132
+ compat source stops receiving `.claude/rules/*.md`. It is released as a minor because it aligns one
133
+ surface with the four that already fail closed, because the loss is announced at runtime by the
134
+ undeclared-source warning (which now names rules alongside the others), and because there is an
135
+ explicit way back: `compatSources: ["claude-code"]`, or `{ kind: "claude-code", import: ["context"] }`
136
+ for that surface alone. A reader who weighs the removal differently should say so before the cut.
137
+
138
+ **Release ordering, measured rather than assumed.** This gate must ship AFTER its consumers have a
139
+ name to grant. `@theokit/agents@13.4.0` — the version `TheoCode` resolves today — declares
140
+ `CompatSurface = 'commands' | 'hooks' | 'plugins' | 'skills' | 'subagents'`, with no `context`, and
141
+ `TheoCode` passes exactly that list as its narrowed `import`. Cutting this release first would take
142
+ `.claude/rules` from it silently, with no word it could write to ask for them back. The order is:
143
+ `@theokit/agents` publishes the vocabulary, consumers declare `context`, then this.
144
+
145
+ **What is deliberately NOT gated**, because an undocumented gap reads as an oversight:
146
+ `AGENTS.md`, `GEMINI.md` and `.cursor/rules/*.mdc` are every bit as foreign, and `adaptersFor`
147
+ registers no adapter for any of them — so `compatSources` has no spelling that admits one, and
148
+ gating them would strand three formats with no way to restore them. `CLAUDE.md` is left ungated by
149
+ judgement rather than by limit: the grant gates the foreign ROOT, that file sits at the repository
150
+ root beside the other three, and projects with no `.claude/` at all use it as a generic
151
+ agent-instructions file. Whether a repo-root instruction file should require an opt-in is a product
152
+ decision affecting every consumer, not a bug fix.
153
+
154
+ - [#661](https://github.com/usetheokit/theokit-sdk/pull/661) [`f94d92c`](https://github.com/usetheokit/theokit-sdk/commit/f94d92c396c20c6f7ebeb7572894eb8c9cbb8b0a) Thanks [@usetheodev](https://github.com/usetheodev)! - `AGENTS.local.md`, `CLAUDE.local.md` and `THEO.local.md` are discovered, and composed last.
155
+
156
+ The gitignored companion is where an operator keeps the standing corrections too personal or too
157
+ situational to commit. Nothing discovered it. Measured 2026-09-12: a grep for the four `.local`
158
+ spellings returned **0 files** across this package's source, against a control of 23 for
159
+ `CLAUDE.md`. The failure is the silent kind — the file exists, it is named the documented way,
160
+ nothing loads it, and nothing complains, so the agent behaves exactly as it would if the operator
161
+ had written nothing.
162
+
163
+ **Order, and the cost of it, stated rather than discovered later.** The three specs sit above every
164
+ public one (priorities 70/75/80) because a correction has to be composed after the rule it corrects,
165
+ and they keep the public chain's relative order among themselves so both halves read the same way.
166
+ `applyAggregateCap` fills the budget in ascending priority, so the highest numbers are the first
167
+ dropped when the total cap is reached — placing the private chain last therefore makes it the first
168
+ to go under pressure. The alternative, a low number to protect it, would compose the operator's
169
+ refinement *before* the general rule and invert its meaning, which is the defect this closes. The
170
+ table already accepts that trade: `.theokit/THEO.md`, the most specific public file, sits at 60 and
171
+ is equally droppable.
172
+
173
+ **Three and not six.** A private companion pairs with a public file this seam reads, and the
174
+ documented convention is THEO / AGENTS / CLAUDE. `GEMINI.local.md` and a private `.cursor/rules` are
175
+ not part of it, and inventing them would publish a convention nobody writes.
176
+
177
+ **Ungated**, like the repo-root files beside them. `CLAUDE.local.md` sits at the repository root
178
+ rather than inside `.claude/`, so it follows `CLAUDE.md` and not `claude-rules` — the grant added in
179
+ [#652](https://github.com/usetheokit/theokit-sdk/issues/652) gates the foreign *root*, not the files beside it.
180
+
181
+ The chains stay independent: a private file never replaces its public sibling. One falling back to
182
+ the other is the trap, where adding a `THEO.md` would silently orphan an existing `AGENTS.local.md`.
183
+
184
+ - [#659](https://github.com/usetheokit/theokit-sdk/pull/659) [`0aca2bf`](https://github.com/usetheokit/theokit-sdk/commit/0aca2bfd8bca0aa57f9929f0121c1bdf2159fc92) Thanks [@usetheodev](https://github.com/usetheodev)! - An organisation can impose policy on an agent. `managed-settings.json` is read, and the project cannot switch it off.
185
+
186
+ Measured: `grep -rl managed-settings` returned 0 here, 0 in the 4.52.1 dist and 0 in the 5.5.0 dist,
187
+ against a control of 31 files for `hooks`. Claude Code defines the file as settings a user "cannot
188
+ override, except for limited exceptions". An organisation that deployed one got it dropped in
189
+ silence, while the same file was enforced by the tool it was written for.
190
+
191
+ **This was not an incomplete feature — it was an ignored security control**, and the failure
192
+ direction is permit.
193
+
194
+ The decision behind it is recorded in `packages/agents/README.md` § "Who decides policy": an
195
+ operator who did not write the code CAN impose policy on it. Hooks, MCP servers, permissions and
196
+ skill execution were each a value the *programmer* passed at build time — defensible for a framework,
197
+ indefensible for anything an organisation deploys, because the person answerable for what an agent
198
+ may do on a machine had no way to say so.
199
+
200
+ Precedence, highest first: `managed-settings.json` → the project's `settings.json` →
201
+ `defineAgent({ … })`.
202
+
203
+ **The first control lifted is `disableAllHooks`**, because its absence is the hardest to notice: a
204
+ hook that does not run looks identical to a hook that ran and approved. It is a **veto, not a
205
+ merge** — a project file setting `disableAllHooks: false` loses, since a tier the layer below can
206
+ switch off is not a tier. It is also checked before `settingSourcesIncludeProject`, which is the
207
+ programmer choosing whether to read the project's files at all.
208
+
209
+ **Unknown keys are reported, never carried.** An organisation writing `forceModel` into the policy
210
+ and getting silence would conclude the model is forced; carrying the key through would spread that
211
+ belief downstream. The reports go through `diagFailure`, not `diag` — `diag` returns immediately when
212
+ no sink is installed, and most consumers never install one, so a policy channel using it would be
213
+ silent by default about the one thing this tier exists to make certain.
214
+
215
+ `readManagedSettings` and `ManagedSettings` cross the barrel so a HOST can read the policy the
216
+ runtime enforces instead of guessing at it. This is not the only reader: `@theokit/agents` ships from
217
+ a separate repository against a *published* version of this package, so it carries its own reader of
218
+ the same file. One FORMAT is the contract; two readers that release independently is a consequence of
219
+ the repository boundary.
220
+
221
+ Platform paths are Claude Code's own (`/etc/claude-code/`, `/Library/Application Support/ClaudeCode/`,
222
+ `%PROGRAMDATA%\ClaudeCode\`), so an organisation that already deployed a policy does not have to
223
+ deploy a second copy under a different name.
224
+
225
+ `permissionMode` and `permissions` join `disableAllHooks` and `disableSkillShellExecution` as keys an
226
+ operator may impose. `plan` is the one the posture key exists for — an explore-only run where edits
227
+ are structurally refused; a plan-mode *tool* already existed and it is something the model may call,
228
+ and the difference is who decides. A posture outside the four is reported and ignored: `"readonly"`
229
+ is what somebody writes when they mean `plan`, and applying it by shape would enforce a posture
230
+ nobody defined while dropping it silently would leave them believing edits are refused.
231
+
232
+ - [#659](https://github.com/usetheokit/theokit-sdk/pull/659) [`0aca2bf`](https://github.com/usetheokit/theokit-sdk/commit/0aca2bfd8bca0aa57f9929f0121c1bdf2159fc92) Thanks [@usetheodev](https://github.com/usetheodev)! - A permission policy can be data an operator ships, reviews and diffs. It could only be a compiled-in function.
233
+
234
+ Measured: `allowedTools` and `disallowedTools` returned 0 files in the agents layer, and the closest
235
+ facility was `CommandPolicy = (command: string) => string | null` — a code predicate. The spec's rule
236
+ language had no counterpart: `Bash(` 0/0, `domain:` 1/0 and that one in prose.
237
+
238
+ The consequence is not a missing convenience. Every policy was a function, so it could not be
239
+ audited, diffed, reviewed in a pull request, or varied per environment — and `Read(./.env)`, the
240
+ spec's own paste-ready secret-exclusion example, could not be expressed at all.
241
+
242
+ **The grammar is decided as a whole**, one shape with a self-describing specifier:
243
+
244
+ | Written | Matches |
245
+ |---|---|
246
+ | `Bash` | every call to `Bash` |
247
+ | `Bash(npm run test:*)` | the argument starts with `npm run test:` |
248
+ | `Bash(npm audit)` | the argument equals `npm audit` — not a prefix |
249
+ | `Read(path:./.env)` | the argument, read as a path, matches the glob |
250
+ | `WebFetch(domain:example.com)` | the argument's HOST equals `example.com` |
251
+
252
+ The specifier says how to read itself. Per-tool magic — knowing that `Read` means a path — cannot
253
+ work where the consumer brings their own tools: a rule naming a tool this SDK has never heard of must
254
+ still be readable.
255
+
256
+ **A domain matches the HOST, never a substring.** `example.com.evil.test` is a different host, and a
257
+ substring match here would be an open redirect in policy form.
258
+
259
+ **A path glob is anchored at both ends.** Unanchored fails in both directions — a file outside the
260
+ protected tree matches because the pattern appears in its path, and a file inside escapes by having
261
+ anything appended. It is built from the literal with every other metacharacter escaped, so a policy
262
+ line cannot smuggle a regular expression into the matcher.
263
+
264
+ **Deny is emitted first**, then `ask`, then `allow`. The engine is first-match, so emission order *is*
265
+ precedence — and a narrow deny must survive a broad allow, or `Read` plus `Read(path:./.env)` would
266
+ read the secret the second line exists to protect.
267
+
268
+ **The engine is untouched.** A specifier becomes one rule per conventional argument name
269
+ (`command`, `file_path`, `path`, `url`, `query`) rather than a matcher that inspects the whole call:
270
+ `ArgMatcher` receives a single value, and `#argsMatch` fails a matcher whose argument is absent — an
271
+ invariant with its own history ([#367](https://github.com/usetheokit/theokit-sdk/issues/367), where a predicate invoked with `undefined` widened an allow
272
+ rule written to narrow). Widening that to add a grammar would trade a tested invariant for a parser.
273
+
274
+ **The limit is stated, not guessed around.** A tool whose argument is named something else cannot be
275
+ narrowed by specifier. "Read whichever argument is the only string" was the alternative and is worse:
276
+ a rule would match an argument the operator never named, widening an allow rule exactly as often as
277
+ it narrows a deny one. A bare tool name always works and matches every call.
278
+
279
+ A rule this grammar cannot read is **refused**, not dropped. A policy line an operator wrote and the
280
+ runtime silently ignored is the belief-in-an-absent-protection this tier exists to remove.
281
+
282
+ **B-038 is decided by this change rather than inherited by it.** The engine is first-match over an
283
+ array while the format groups by category, so a ported file listing `allow` above `deny` for the same
284
+ tool would silently invert and the narrower deny would never be reached. The decision is that the
285
+ LOADER reorders, not that the engine gains category evaluation: the array semantics are what every
286
+ existing consumer already built rules against, and changing how it walks them would move ground under
287
+ code nobody asked to change. A fixture writing `allow` first pins it.
288
+
289
+ **A rule written in the documented MCP spelling now matches.** The reference names an MCP tool
290
+ `mcp__server__tool` with a double underscore; this runtime names the same tool
291
+ `mcp_server_tool`, single, because the name is sanitised for the provider. Every permission rule an
292
+ operator copied from the documentation missed its target **silently** — the deny read as configured
293
+ and the tool ran.
294
+
295
+ Normalised in the RULE, never in the runtime name: the runtime spelling is what the model sees and
296
+ what the provider validates, and changing it would break every rule already written against it and
297
+ every consumer matching it, to fix a mismatch that costs one substitution at parse time. Scoped to
298
+ the `mcp__` prefix rather than rewriting every double underscore, because a tool outside MCP is
299
+ entitled to one in its own name.
300
+
301
+ - [#659](https://github.com/usetheokit/theokit-sdk/pull/659) [`ddb9306`](https://github.com/usetheokit/theokit-sdk/commit/ddb930614c55e1aab2f970e148c8c7be46c8f104) Thanks [@usetheodev](https://github.com/usetheodev)! - The protected-path and critical-path floors now fire from the runtime, before the permission engine
302
+ and before the consumer's `canUseTool` gate.
303
+
304
+ They did not. `permission-floors.ts` was imported by `src/index.ts` and by nothing else, so a tier
305
+ whose own documentation says it sits "above every permission rule and every mode" was consulted only
306
+ if a consumer remembered to call `permissionFloorReason` themselves. A guarantee that depends on
307
+ being remembered is as strong as the memory, which is the failure this slice was written to remove.
308
+
309
+ `permission-plugin.ts` — the `pre_tool_call` decision point — now asks the floor FIRST. An `allow`
310
+ rule, an allowing gate and `permissionMode: "bypassPermissions"` together no longer reach a write
311
+ into `.claude/` or `.theokit/`, nor a destructive operation on a critical root. `bypassPermissions`
312
+ not reaching the floor is deliberate: a mode that skips it is a mode that can rewrite the policy
313
+ meant to bound it.
314
+
315
+ The `@theokit/sdk` bundle budget moves 27000 → 28000 gzipped as a direct consequence: the runtime
316
+ now carries a security tier it did not carry before (27338, 98% of the new ceiling). The headroom is
317
+ 662 bytes, so unintended growth still trips the gate.
318
+
319
+ ### Patch Changes
320
+
321
+ - [#659](https://github.com/usetheokit/theokit-sdk/pull/659) [`4f0d74a`](https://github.com/usetheokit/theokit-sdk/commit/4f0d74a1056c3ed1d1fa1621d0dae06da95eb46e) Thanks [@usetheodev](https://github.com/usetheodev)! - Block-style YAML lists in frontmatter now parse instead of vanishing.
322
+
323
+ `parseSimpleYaml` is line-oriented: it splits each line on the first `:` and coerces what follows.
324
+ A continuation line `- item` has no colon, so it was skipped outright, while the key line above it
325
+ had an empty value and coerced to `undefined`. The key disappeared entirely — a reader saw
326
+ `paths:` on disk, found no behaviour, and had nothing to grep for.
327
+
328
+ Two things made this worth fixing rather than documenting:
329
+
330
+ - **The file's own docblock recommended the shape it could not read.** Listing the inline form's
331
+ comma limitation, it advised "Use multi-line lists or reword if you need this." Multi-line lists
332
+ were the one shape this parser did not support.
333
+ - **Two parsers in this package disagreed about the same frontmatter.** The sibling
334
+ `context-yaml-lite.ts` already reads block lists, and its comment records that adding them was a
335
+ repair rather than a feature. Which loader read a file decided whether its list existed.
336
+
337
+ Blank and `#` lines do not end a list; the first other line does, and the caller resumes there — so
338
+ the key written after a block list is no longer swallowed by it. A bare `key:` with no items under
339
+ it still yields `undefined`, which is what a caller's Zod default relies on.
340
+
341
+ The collection and the key/value split moved into `collectBlockList` and `splitEntry`. That is not
342
+ tidying: inlining the collection put `parseSimpleYaml` at a cognitive complexity of 24 against the
343
+ repository's limit of 10, measured on the same file path where the pre-change version raised no
344
+ such diagnostic.
345
+
346
+ - [#659](https://github.com/usetheokit/theokit-sdk/pull/659) [`4f0d74a`](https://github.com/usetheokit/theokit-sdk/commit/4f0d74a1056c3ed1d1fa1621d0dae06da95eb46e) Thanks [@usetheodev](https://github.com/usetheodev)! - A subagent frontmatter field that belongs to Claude Code now says so, and names its siblings.
347
+
348
+ **The failure itself is unchanged, deliberately.** A file that declares frontmatter and gets it
349
+ wrong is a broken agent and still fails the load — the reason is recorded a few lines above, where
350
+ skipping was added only for files with _no_ frontmatter: "a file that HAS frontmatter and gets it
351
+ wrong is a broken agent and still fails loudly, which is what keeps a typo'd `sandbox` from
352
+ returning as a silent gate through this door." Isolating the failure per file would hand that risk
353
+ back.
354
+
355
+ What changes is the diagnosis. A user migrating a `.claude/agents/` tree learned one key per round
356
+ trip: fix `memory`, meet `permissionMode`, fix that, meet `maxTurns` — with nothing saying the set
357
+ was finite or that the tree was simply written for another runtime. The error now distinguishes
358
+ "another runtime's field" from "never heard of this", and lists the other eleven that will behave
359
+ the same way.
360
+
361
+ That distinction already exists here for `INERT_CLAUDE_CODE_FIELDS`, described as "the difference
362
+ between 'we know this one and it does nothing' and 'we have never heard of this' — two facts a bare
363
+ allow-everything would collapse into one." This adds a third fact beside them and changes only the
364
+ message, never the verdict.
365
+
366
+ - [#659](https://github.com/usetheokit/theokit-sdk/pull/659) [`820fc9c`](https://github.com/usetheokit/theokit-sdk/commit/820fc9c74e76947f8ef1204bf9fab6e5c1367cb9) Thanks [@usetheodev](https://github.com/usetheodev)! - A hook field this runtime does not implement is now refused instead of dropped in silence.
367
+
368
+ `parseClaudeCodeCommand` read `type`, `command` and `timeout` and discarded everything else on the
369
+ entry — `if`, `args`, `statusMessage`, `once`, `async`, `asyncRewake`, `shell` — with no error and
370
+ no warning.
371
+
372
+ **`if` is the field that makes this a defect rather than a missing feature.** For the others the
373
+ loss is a convenience. For `if` it is the opposite of what the operator wrote: a deny hook narrowed
374
+ to one dangerous command shape silently becomes a deny hook over *every* call of that tool. The
375
+ guard still runs, so nothing looks broken; it simply applies where it was told not to.
376
+
377
+ The fix is refusal, not implementation. Implementing `if` means adopting a condition language whose
378
+ semantics nobody here has decided; refusing the field costs one throw and cannot be wrong about what
379
+ the operator meant. The direction is what matters — a dropped `if` fails open, a refused `if` fails
380
+ closed and names the field that stopped the load.
381
+
382
+ `packages/agents`, reading the same file one layer up, already took this side: its `hookSpecSchema`
383
+ is `.strict()` and refuses an unknown key loudly. Two layers disagreed about whether a field was an
384
+ error, and the permissive one was the layer that actually ran the hook.
385
+
386
+ The error separates "a Claude Code hook field this runtime does not implement" from "never heard of
387
+ this", and lists the siblings that will behave the same way — so an operator migrating a `.claude/`
388
+ tree does not learn one key per round trip. A non-command `type` still fails for its own reason: the
389
+ new check runs after the existing ones.
390
+
391
+ - [#659](https://github.com/usetheokit/theokit-sdk/pull/659) [`4f0d74a`](https://github.com/usetheokit/theokit-sdk/commit/4f0d74a1056c3ed1d1fa1621d0dae06da95eb46e) Thanks [@usetheodev](https://github.com/usetheodev)! - `skill_read` can read a skill discovered on disk. It could not, which made the entire on-disk skills surface decorative.
392
+
393
+ `SkillReadTool.create` took `ReadonlyArray<InlineSkill>` — the shape that carries `instructions` on
394
+ the object. A disk-discovered skill is a `Skill`, whose own declaration says "the skill BODY is never
395
+ included", so it did not fit through the door at all. The result: a `SKILL.md` was listed by name and
396
+ description in the `<skills>` block, the model asked to read it, and got back a heading and an empty
397
+ body — measured, the handler returned `"# Skill: deploy\n\n"`.
398
+
399
+ The instructions under the frontmatter are the skill. Every other skill defect in this backlog is a
400
+ field inside a file whose body never showed up.
401
+
402
+ `create` now accepts `ReadableSkill` — a skill that carries its body, or one that knows where its
403
+ body is — and the handler resolves the difference. Passing inline skills is unchanged.
404
+
405
+ **Read when the model asks, not at construction.** Eager reading would turn "this agent knows about
406
+ twelve skills" into twelve file reads at startup, to answer a question the model usually does not
407
+ ask. The handler's contract already allowed `Promise<string>`, so laziness cost one `await` and no
408
+ new API.
409
+
410
+ The handler is deliberately NOT `async`. Marking the whole function async turns the input schema's
411
+ synchronous throw into a rejected promise, and this module's contract is that malformed input "fails
412
+ at the trust boundary via the schema". Measured while making this change: two trust-boundary tests
413
+ went from throwing to returning `undefined`. Only the body read is asynchronous — parse and the
414
+ not-found answer stay exactly as synchronous as they were.
415
+
416
+ A disk skill's `references/` directory is now read too, for the same reason inline skills already
417
+ render theirs in full: the two shapes describe the same thing, and one of them arriving empty was the
418
+ asymmetry. A document that cannot be read is skipped rather than failing the whole read — a skill
419
+ should not become unreadable because something beside it is.
420
+
421
+ `${CLAUDE_SKILL_DIR}` resolves to the skill's own directory — the reason skills are directories
422
+ rather than single files. A skill ships scripts and reference documents beside its `SKILL.md`, and
423
+ without the placeholder the body had no expressible path to them: the skill does not know where it
424
+ was installed, and neither does its author at the time of writing.
425
+
426
+ Only for a skill read from disk. An inline skill has no directory, so the text is left exactly as
427
+ written — an honest limit rather than a guess. Leaving it says "this does not apply here"; inventing
428
+ a path would hand the model a command that fails somewhere plausible.
429
+
430
+ - [#659](https://github.com/usetheokit/theokit-sdk/pull/659) [`820fc9c`](https://github.com/usetheokit/theokit-sdk/commit/820fc9c74e76947f8ef1204bf9fab6e5c1367cb9) Thanks [@usetheodev](https://github.com/usetheodev)! - A hook emitting the **documented** deny shape is now honoured instead of being read as `allow`.
431
+
432
+ `hooks-source.ts` advertises a config shape "identical to Claude Code's `settings.json` hooks", so a
433
+ consumer writes the guard that documentation specifies:
434
+
435
+ ```json
436
+ {
437
+ "hookSpecificOutput": {
438
+ "hookEventName": "PreToolUse",
439
+ "permissionDecision": "deny",
440
+ "permissionDecisionReason": "…"
441
+ }
442
+ }
443
+ ```
444
+
445
+ `parseDecisionFromStdout` read only a **top-level** `decision` and accepted only
446
+ `deny` / `feedback` / `allow`. The nested shape has no top-level `decision` at all, so it fell past
447
+ every branch to the final `return { decision: "allow" }`. The JSON parsed, nothing warned, and the
448
+ tool call proceeded.
449
+
450
+ Measured against four inputs before the fix: the documented shape → allow; the deprecated-but-
451
+ documented `{"decision":"block"}` → allow; `Stop` with `block` → allow; only this runtime's own
452
+ `{"decision":"deny"}` denied.
453
+
454
+ Three spellings mean deny and all three are now read: the nested `permissionDecision`, the
455
+ deprecated `block`, and the native `deny`. A nested `ask` projects to deny — collapsing it to allow
456
+ would be the same fail-open one value over, since this runtime has no third state to put the
457
+ question to.
458
+
459
+ **Deliberately unchanged**: an unrecognised shape still resolves to `allow`. Making it deny would
460
+ refuse every hook that prints diagnostics and happens to emit JSON — a behaviour change with its own
461
+ blast radius, and its own measurement. The three documented denials are unambiguous; that case is
462
+ not.
463
+
464
+ The direction is what made this expensive. A missing hook event is discoverable: the user sees
465
+ nothing happen and investigates. A veto that silently does not fire is indistinguishable from a veto
466
+ that fired and approved.
467
+
468
+ - [#659](https://github.com/usetheokit/theokit-sdk/pull/659) [`820fc9c`](https://github.com/usetheokit/theokit-sdk/commit/820fc9c74e76947f8ef1204bf9fab6e5c1367cb9) Thanks [@usetheodev](https://github.com/usetheodev)! - A hook event the runtime does not fire is now reported even when the host installed no diagnostics
469
+ sink.
470
+
471
+ The loader maps four Claude Code event names and skips the rest. Skipping is honest — the runtime
472
+ genuinely does not fire the others — but the notice went through `warnOnce` → `diag`, and `diag` is
473
+ silent by default. So an operator declaring a `PreCompact` guard got nothing: no hook, no message,
474
+ and no way to learn either.
475
+
476
+ `diag`'s silence is right for chatter: a library must not assume the host's stderr is a free-form
477
+ log, because in a TUI it is the render surface. A configuration the operator **wrote** and this
478
+ runtime will not honour is not chatter. `warnFailureOnce` routes it through `diagFailure`, the
479
+ channel that already exists for exactly this and whose docblock records the precedent — `[#189](https://github.com/usetheokit/theokit-sdk/issues/189)`,
480
+ where an MCP server failed to start, the only report went to `diag()`, the embedding UI never read
481
+ it, and "the user saw an agent with missing tools and no reason given".
482
+
483
+ A dropped hook is that shape with a sharper edge, because the missing thing is a guard: the operator
484
+ declared a refusal, it silently does not exist, and nothing distinguishes that from a refusal that
485
+ ran and approved.
486
+
487
+ A sink still takes precedence when one is installed — this only changes what happens when none is.
488
+
489
+ **A note for whoever writes the next test here.** `vitest.setup.ts` installs a stderr-forwarding
490
+ sink for the duration of every test, so the default path is the one shape this suite never
491
+ exercises. A test written the obvious way passes before the fix and proves nothing; the one added
492
+ here removes the sink in `beforeEach` to stand in for a consumer that never called
493
+ `setDiagnosticsSink`.
494
+
495
+ - [#659](https://github.com/usetheokit/theokit-sdk/pull/659) [`820fc9c`](https://github.com/usetheokit/theokit-sdk/commit/820fc9c74e76947f8ef1204bf9fab6e5c1367cb9) Thanks [@usetheodev](https://github.com/usetheodev)! - A native hook can locate its own project, and a ported hook script receives the field names it was written against.
496
+
497
+ **The project directory, in both dialects.** `CLAUDE_PROJECT_DIR` was supplied to commands imported
498
+ from Claude Code ([#522](https://github.com/usetheokit/theokit-sdk/issues/522), after a hook written the documented way expanded to a leading `/` and denied
499
+ every turn). The native dialect got `{}`, on the reasoning that a `.theokit/` hook "is written
500
+ against THIS runtime and inherits it already" — true of the runtime's *behaviour*, not of a project
501
+ *path*. Nothing in the inherited environment says where the project is, so a native hook had to
502
+ depend on the process cwd: the exact dependency the foreign fix removed. `THEOKIT_PROJECT_DIR` is
503
+ the native counterpart, under the native spelling, because a ported script reaches for the name its
504
+ own docs use.
505
+
506
+ **The stdin payload.** It carried this runtime's field names only. A script ported from Claude Code
507
+ reads `tool_name`, `tool_input`, `tool_response`, `hook_event_name` and `cwd` — it got `undefined`
508
+ for every one, and **ran**, deciding on nothing while looking like a working guard. That is worse
509
+ than a script that fails: the operator's evidence that the guard works is identical either way.
510
+
511
+ The documented names are added **beside** the existing ones, never instead. Both dialects execute
512
+ through one path, so renaming would break every native script to fix the ported ones.
513
+
514
+ Only what this runtime knows. `session_id`, `transcript_path`, `permission_mode` and `prompt_id`
515
+ stay absent because their values would have to be invented — a script that branches on an invented
516
+ session id branches on a lie. Same trade as `CLAUDE_PLUGIN_ROOT`, which stays unset for the same
517
+ reason one module over.
518
+
519
+ - [#659](https://github.com/usetheokit/theokit-sdk/pull/659) [`4f0d74a`](https://github.com/usetheokit/theokit-sdk/commit/4f0d74a1056c3ed1d1fa1621d0dae06da95eb46e) Thanks [@usetheodev](https://github.com/usetheodev)! - Skill frontmatter's two authorization fields are read instead of discarded, and one of them is now enforced.
520
+
521
+ `buildFrontmatter` kept exactly `name`, `description`, `category` and `dependencies`. `user-invocable`
522
+ and `disable-model-invocation` survived the YAML parser and were dropped one function later, so a
523
+ template could declare a restriction the runtime could not honour — and the default is disclose-all.
524
+ `create-theokit` ships seven skills carrying `user-invocable: false`, and all seven were inert.
525
+
526
+ **`disable-model-invocation: true` is now enforced.** A skill reaches the model through exactly one
527
+ place — the system-prompt context — so the declaration is a filter there. This is the capability the
528
+ format names when it says you do not want the model deciding to deploy because the code looks ready:
529
+ a side-effecting skill a human may run and the model may not propose. It is a **disclosure** rule,
530
+ not an execution rule — `skills.get(name)` still resolves a hidden skill, because a caller naming one
531
+ has already made the decision the field exists to keep away from the model. Widening it to execution
532
+ would break the case the field is for.
533
+
534
+ **`user-invocable: false` is carried, deliberately not enforced here.** This SDK has no user-facing
535
+ invocation surface for skills; there is no slash command. Reading `agent.skills.list()` as "the user"
536
+ would be a guess — a host may call it to build a picker or to introspect, and those want opposite
537
+ answers. The declaration now travels to the host that knows, instead of being thrown away.
538
+
539
+ A value the dialect cannot read is refused rather than ignored. This dialect coerces only the
540
+ literals `true` and `false`, so `disable-model-invocation: yes` — a valid YAML boolean — arrived as
541
+ the string `"yes"`, compared unequal to `true`, and the skill was disclosed. The author wrote a
542
+ restriction and got the default. A restriction that fails open is worse than an absent one, because
543
+ the author stops looking; the skill is now reported as invalid and excluded, naming the value.
544
+
545
+ - [#659](https://github.com/usetheokit/theokit-sdk/pull/659) [`429ddff`](https://github.com/usetheokit/theokit-sdk/commit/429ddff2bcfff7ab750257e6dd13de9dd91eb0a8) Thanks [@usetheodev](https://github.com/usetheodev)! - Fixes a ReDoS in the critical-path floor's destructive-command matcher (CodeQL: polynomial regular
546
+ expression on uncontrolled data).
547
+
548
+ ```
549
+ /(?:^|[;&|]\s*)\s*(rm|rmdir|shred|mkfs\S*|dd)\s/
550
+ ^^^ ^^^ adjacent quantifiers over overlapping classes
551
+ and an unbounded \S* inside the alternation
552
+ ```
553
+
554
+ Measured on the inputs CodeQL named: 60 000 spaces after a `;` took **2 094ms**, and 12 000 `&mkfs`
555
+ repetitions took **600ms** — clean quadratic growth. After the rewrite, 0.36ms and 0.33ms.
556
+
557
+ It mattered because of the commit beside it. While nothing called the floor, a slow matcher was a
558
+ latent cost; wiring the floor into `permission-plugin.ts` put it in front of **every tool call**, so
559
+ one crafted argument stalls the agent instead of being refused by it. Making a guard reachable and
560
+ making its cost matter are the same act.
561
+
562
+ The separator no longer consumes whitespace the following `\s*` already consumes, and the `mkfs`
563
+ suffix is a bounded dotted variant (`mkfs.ext4`, `mkfs.xfs`, `mkfs.btrfs`) rather than "any run of
564
+ non-space". Verified identical across thirteen cases spanning every branch.
565
+
566
+ Also documented, because measuring it turned it up: a DEVICE NODE is not a critical path.
567
+ `mkfs.ext4 /dev/sda` and `dd of=/dev/sda` pass this floor — the tier is about the working directory,
568
+ the home directory and the filesystem root, and `/dev/sda` is none of them. A reader who sees `mkfs`
569
+ in the pattern reasonably concludes otherwise, so a test now pins the real behaviour.
570
+
571
+ - [#659](https://github.com/usetheokit/theokit-sdk/pull/659) [`4f0d74a`](https://github.com/usetheokit/theokit-sdk/commit/4f0d74a1056c3ed1d1fa1621d0dae06da95eb46e) Thanks [@usetheodev](https://github.com/usetheodev)! - The order nested instruction files reach the prompt is pinned, and the one real divergence from the spec is stated.
572
+
573
+ A parity survey reported three defects here. Re-measured by execution, two did not hold:
574
+
575
+ - **`.claude/rules/*.md` is read** — spec `claude-rules`, priority 47. A probe through `runDiscovery`
576
+ over a temp project returned one source carrying the rule. The original zero-file grep was against
577
+ a layer that does not do the reading.
578
+ - **Precedence is not reversed.** `walkUpForFile` returns nearest-first, and that is not what the
579
+ model sees: `applyAggregateCap` re-sorts by priority and then by absolute path, so a three-level
580
+ project measured `walk=[DEEP,SUB,ROOT]` and `prompt=[ROOT,SUB,DEEP]` — the spec's root-down order,
581
+ where the nearer file refines the wider one instead of being buried under it.
582
+
583
+ **The correct behaviour was correct by accident**, which is why this changeset exists. Root-down falls
584
+ out of a tie-break written for prompt-cache determinism (EC-J), and nothing stated it. Change the
585
+ tie-break, or name a subdirectory lexically smaller than its parent, and a repository-wide
586
+ instruction starts being read after the nested one meant to refine it — with nothing to catch it. It
587
+ is now pinned by a test that also mutation-checks the inverse.
588
+
589
+ **The divergence that is real:** the walk stops at the git root, while the spec continues to every
590
+ directory above cwd. Kept, with the reason — a `CLAUDE.md` in a home directory or in `/tmp` would
591
+ silently apply to every repository underneath it, and an instruction file nobody in the project wrote
592
+ is the one case where finding more is worse than finding less. The operator-home question stays a
593
+ deliberate, separate decision rather than a side effect of how far a loop runs.
594
+
595
+ - [#659](https://github.com/usetheokit/theokit-sdk/pull/659) [`3ae3766`](https://github.com/usetheokit/theokit-sdk/commit/3ae37660a5a012968115e3e611155cf74397ec23) Thanks [@usetheodev](https://github.com/usetheodev)! - States, next to the write floor that does exist, that there is no read-side equivalent — and pins it
596
+ with a test so the claim and the behaviour cannot drift apart.
597
+
598
+ `permissionFloorReason` refuses a WRITE into `.claude/`, `.theokit/`, `.ssh` or `.gnupg` under every
599
+ allow rule. A READ of the same paths is refused by nothing: reads are governed by the rule language
600
+ and by nothing above it. `Read(path:./.env)` works and deny-before-allow means a deny cannot be
601
+ overtaken — but every such rule has to be written, nothing is refused by default, and a bare `Read`
602
+ allow grants reading any path the process can open.
603
+
604
+ The asymmetry was invisible from the module. `PROTECTED_SEGMENTS` reads like a list of protected
605
+ paths and is only half that: `.ssh` cannot be written through any rule, and can be read through an
606
+ ordinary allow.
607
+
608
+ `additionalDirectories` — the reference's key for an operator to WIDEN what an agent may read — has
609
+ no equivalent, and cannot have one while there is no fence to widen. Measured: zero occurrences in
610
+ this package and zero in `@theokit/agents`.
611
+
612
+ No fence was added, and not because one is unwanted. `permissionFloorReason` sees a tool name and an
613
+ argument map; a fence matching on those alone would miss every read that reaches the filesystem
614
+ another way — a shell command, a plugin, an MCP server. Shell reads are already confined by
615
+ `SandboxMode`, and a second containment vocabulary here would leave two answers to "may this be
616
+ read" that disagree at the edges. Deciding which layer owns that boundary is a measured decision,
617
+ not a docblock.
618
+
619
+ - [#659](https://github.com/usetheokit/theokit-sdk/pull/659) [`820fc9c`](https://github.com/usetheokit/theokit-sdk/commit/820fc9c74e76947f8ef1204bf9fab6e5c1367cb9) Thanks [@usetheodev](https://github.com/usetheodev)! - The hook loader stops claiming parity it does not have, and states the set it does.
620
+
621
+ The module docblock advertised a config shape "identical to Claude Code's `settings.json` hooks". The
622
+ SHAPE is identical; the event COVERAGE is four of the thirty-three documented events. Measured by
623
+ execution — a `hooks.json` declaring all thirty-three, through the shipped loader, yielded
624
+ `PreToolUse`, `PostToolUse`, `UserPromptSubmit` and `Stop`. Thirteen of the sixteen the spec marks
625
+ "Can block? Yes" were among the missing.
626
+
627
+ **The map is deliberately not grown.** Mapping a name the runtime does not fire is strictly worse
628
+ than refusing it: an operator declaring `PreCompact` today gets a report saying it will not fire;
629
+ with the name mapped they would get silence and a guard that never runs — a declared veto that does
630
+ not exist. The map grows when the seam exists, one event at a time.
631
+
632
+ `CLAUDE_CODE_EVENT_MAP` is now exported so the supported set is stated rather than implied, and a
633
+ test derives its expectation from it: adding a seam turns that test red, which is where the docblock
634
+ claim gets updated with it. The same test lists the fourteen unwired blocking events in priority
635
+ order — an unwired veto loses a capability, an unwired observer loses a signal — so the next person
636
+ picking one up does not re-derive which is which.
637
+
638
+ `postRun` has no entry on purpose: it fires per RUN, and no documented Claude Code event means that.
639
+ `SessionEnd` is the near miss, and a session is not a run.
640
+
641
+ - [#659](https://github.com/usetheokit/theokit-sdk/pull/659) [`4f0d74a`](https://github.com/usetheokit/theokit-sdk/commit/4f0d74a1056c3ed1d1fa1621d0dae06da95eb46e) Thanks [@usetheodev](https://github.com/usetheodev)! - `MEMORY.md` says, at both ends, which of two contracts it is.
642
+
643
+ The Claude Code CLI's is a plain file under its own home, capped at 200 lines / 25 KB on read and
644
+ swept on `cleanupPeriodDays`. This SDK's is the durable-memory subsystem — a SQLite+FTS5 store under
645
+ `.theokit/memory/`, with `memory_search` / `memory_get` tools, no index cap, and a different
646
+ directory entirely. Same filename, different directory, different semantics.
647
+
648
+ **A parity survey reported `CLAUDE_CONFIG_DIR` as absent; it is not.** Measured across both packages:
649
+ it is read in `claudeProjectMemoryDir`, which resolves the CLI's memory directory for interop, keyed
650
+ by git root. The original grep ran only against `@theokit/agents`, where it is genuinely absent, and
651
+ reported it absent everywhere — the third item in this release whose evidence was measured in the
652
+ wrong place or under the wrong name.
653
+
654
+ `CLAUDE_CONFIG_DIR`'s scope is now stated: it names the CLI's home so this reader finds the right
655
+ directory, and it relocates no user-level root of this product's own, because there is none — the
656
+ config roots resolved elsewhere are project-relative. A blank value is treated as unset rather than
657
+ as a root, pinned by a test: `CLAUDE_CONFIG_DIR=""` is what an unset shell variable expands to in a
658
+ wrapper script, and reading it as a root produces `/projects/…`, which exists on no machine and fails
659
+ silently as "the CLI has no memories here".
660
+
661
+ **What remains unimplemented, and why that is a decision.** `autoMemoryEnabled`,
662
+ `autoMemoryDirectory`, `CLAUDE_CODE_DISABLE_AUTO_MEMORY`, `cleanupPeriodDays` and the read cap have
663
+ no counterpart. Interop is one-way on purpose: a memory the CLI recorded stays visible, and this
664
+ runtime does not write into a store another product owns the lifecycle of. A `cleanupPeriodDays`
665
+ implemented here would delete files the CLI expects to find.
666
+
667
+ The filename is kept rather than renamed — one of the two is another product's, and renaming it here
668
+ would break the interop the reader exists for.
669
+
3
670
  ## 5.5.0
4
671
 
5
672
  ### Minor Changes