agentfootprint 9.85.0 → 9.86.1

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 (163) hide show
  1. package/CHANGELOG.md +547 -24
  2. package/canonical-notes.json +1 -1
  3. package/dist/core/Agent.js +15 -4
  4. package/dist/core/Agent.js.map +1 -1
  5. package/dist/core/agent/AgentBuilder.js.map +1 -1
  6. package/dist/core/agent/buildAgentChart.js +5 -0
  7. package/dist/core/agent/buildAgentChart.js.map +1 -1
  8. package/dist/core/agent/buildDynamicAgentChart.js +22 -4
  9. package/dist/core/agent/buildDynamicAgentChart.js.map +1 -1
  10. package/dist/core/agent/buildToolRegistry.js +12 -1
  11. package/dist/core/agent/buildToolRegistry.js.map +1 -1
  12. package/dist/core/agent/coverage/ledger.js +8 -2
  13. package/dist/core/agent/coverage/ledger.js.map +1 -1
  14. package/dist/core/agent/selfCallNotice.js +20 -7
  15. package/dist/core/agent/selfCallNotice.js.map +1 -1
  16. package/dist/core/agent/skillBrains.js +3 -2
  17. package/dist/core/agent/skillBrains.js.map +1 -1
  18. package/dist/core/agent/stages/stepNudge.js +13 -4
  19. package/dist/core/agent/stages/stepNudge.js.map +1 -1
  20. package/dist/core/agent/stages/toolCalls.js +484 -64
  21. package/dist/core/agent/stages/toolCalls.js.map +1 -1
  22. package/dist/core/agent/stages/wrapUp.js +54 -6
  23. package/dist/core/agent/stages/wrapUp.js.map +1 -1
  24. package/dist/core/agent/window/currentRequest.js +4 -2
  25. package/dist/core/agent/window/currentRequest.js.map +1 -1
  26. package/dist/core/runbook/recording.js +3 -2
  27. package/dist/core/runbook/recording.js.map +1 -1
  28. package/dist/core/slots/buildToolsSlot.js +20 -4
  29. package/dist/core/slots/buildToolsSlot.js.map +1 -1
  30. package/dist/doors/skill-graph.js +4 -1
  31. package/dist/doors/skill-graph.js.map +1 -1
  32. package/dist/esm/core/Agent.js +15 -4
  33. package/dist/esm/core/Agent.js.map +1 -1
  34. package/dist/esm/core/agent/AgentBuilder.d.ts +2 -1
  35. package/dist/esm/core/agent/AgentBuilder.js.map +1 -1
  36. package/dist/esm/core/agent/buildAgentChart.js +5 -0
  37. package/dist/esm/core/agent/buildAgentChart.js.map +1 -1
  38. package/dist/esm/core/agent/buildDynamicAgentChart.js +22 -4
  39. package/dist/esm/core/agent/buildDynamicAgentChart.js.map +1 -1
  40. package/dist/esm/core/agent/buildToolRegistry.d.ts +20 -0
  41. package/dist/esm/core/agent/buildToolRegistry.js +12 -1
  42. package/dist/esm/core/agent/buildToolRegistry.js.map +1 -1
  43. package/dist/esm/core/agent/coverage/ledger.d.ts +5 -0
  44. package/dist/esm/core/agent/coverage/ledger.js +8 -2
  45. package/dist/esm/core/agent/coverage/ledger.js.map +1 -1
  46. package/dist/esm/core/agent/selfCallNotice.d.ts +20 -7
  47. package/dist/esm/core/agent/selfCallNotice.js +20 -7
  48. package/dist/esm/core/agent/selfCallNotice.js.map +1 -1
  49. package/dist/esm/core/agent/skillBrains.d.ts +12 -4
  50. package/dist/esm/core/agent/skillBrains.js +3 -2
  51. package/dist/esm/core/agent/skillBrains.js.map +1 -1
  52. package/dist/esm/core/agent/stages/stepNudge.d.ts +13 -4
  53. package/dist/esm/core/agent/stages/stepNudge.js +13 -4
  54. package/dist/esm/core/agent/stages/stepNudge.js.map +1 -1
  55. package/dist/esm/core/agent/stages/toolCalls.d.ts +185 -0
  56. package/dist/esm/core/agent/stages/toolCalls.js +480 -63
  57. package/dist/esm/core/agent/stages/toolCalls.js.map +1 -1
  58. package/dist/esm/core/agent/stages/wrapUp.d.ts +44 -2
  59. package/dist/esm/core/agent/stages/wrapUp.js +52 -5
  60. package/dist/esm/core/agent/stages/wrapUp.js.map +1 -1
  61. package/dist/esm/core/agent/types.d.ts +22 -4
  62. package/dist/esm/core/agent/window/currentRequest.d.ts +4 -2
  63. package/dist/esm/core/agent/window/currentRequest.js +4 -2
  64. package/dist/esm/core/agent/window/currentRequest.js.map +1 -1
  65. package/dist/esm/core/runbook/recording.js +3 -2
  66. package/dist/esm/core/runbook/recording.js.map +1 -1
  67. package/dist/esm/core/slots/buildToolsSlot.d.ts +1 -1
  68. package/dist/esm/core/slots/buildToolsSlot.js +20 -4
  69. package/dist/esm/core/slots/buildToolsSlot.js.map +1 -1
  70. package/dist/esm/doors/skill-graph.d.ts +1 -1
  71. package/dist/esm/doors/skill-graph.js +4 -1
  72. package/dist/esm/doors/skill-graph.js.map +1 -1
  73. package/dist/esm/events/payloads.d.ts +34 -2
  74. package/dist/esm/index.js +6 -5
  75. package/dist/esm/index.js.map +1 -1
  76. package/dist/esm/lib/injection-engine/index.d.ts +1 -1
  77. package/dist/esm/lib/injection-engine/index.js +4 -1
  78. package/dist/esm/lib/injection-engine/index.js.map +1 -1
  79. package/dist/esm/lib/injection-engine/skillGraph.d.ts +56 -0
  80. package/dist/esm/lib/injection-engine/skillGraph.js +29 -3
  81. package/dist/esm/lib/injection-engine/skillGraph.js.map +1 -1
  82. package/dist/esm/lib/injection-engine/skillSteps.d.ts +20 -1
  83. package/dist/esm/lib/injection-engine/skillSteps.js +28 -3
  84. package/dist/esm/lib/injection-engine/skillSteps.js.map +1 -1
  85. package/dist/esm/lib/injection-engine/skillToolDescriptors.d.ts +18 -0
  86. package/dist/esm/lib/injection-engine/skillToolDescriptors.js +103 -10
  87. package/dist/esm/lib/injection-engine/skillToolDescriptors.js.map +1 -1
  88. package/dist/esm/lib/injection-engine/types.d.ts +12 -10
  89. package/dist/esm/lib/injection-engine/types.js +5 -4
  90. package/dist/esm/lib/injection-engine/types.js.map +1 -1
  91. package/dist/esm/lib/saidByPerson.d.ts +48 -15
  92. package/dist/esm/lib/saidByPerson.js +52 -17
  93. package/dist/esm/lib/saidByPerson.js.map +1 -1
  94. package/dist/esm/lib/spokenIds.d.ts +52 -0
  95. package/dist/esm/lib/spokenIds.js +11 -0
  96. package/dist/esm/lib/spokenIds.js.map +1 -0
  97. package/dist/esm/lib/trace-toolpack/traceToolpack.js +33 -19
  98. package/dist/esm/lib/trace-toolpack/traceToolpack.js.map +1 -1
  99. package/dist/index.js +6 -5
  100. package/dist/index.js.map +1 -1
  101. package/dist/lib/injection-engine/index.js +5 -2
  102. package/dist/lib/injection-engine/index.js.map +1 -1
  103. package/dist/lib/injection-engine/skillGraph.js +31 -4
  104. package/dist/lib/injection-engine/skillGraph.js.map +1 -1
  105. package/dist/lib/injection-engine/skillSteps.js +28 -3
  106. package/dist/lib/injection-engine/skillSteps.js.map +1 -1
  107. package/dist/lib/injection-engine/skillToolDescriptors.js +103 -10
  108. package/dist/lib/injection-engine/skillToolDescriptors.js.map +1 -1
  109. package/dist/lib/injection-engine/types.js +5 -4
  110. package/dist/lib/injection-engine/types.js.map +1 -1
  111. package/dist/lib/saidByPerson.js +53 -18
  112. package/dist/lib/saidByPerson.js.map +1 -1
  113. package/dist/lib/spokenIds.js +15 -0
  114. package/dist/lib/spokenIds.js.map +1 -0
  115. package/dist/lib/trace-toolpack/traceToolpack.js +33 -19
  116. package/dist/lib/trace-toolpack/traceToolpack.js.map +1 -1
  117. package/dist/types/core/Agent.d.ts.map +1 -1
  118. package/dist/types/core/agent/AgentBuilder.d.ts +2 -1
  119. package/dist/types/core/agent/AgentBuilder.d.ts.map +1 -1
  120. package/dist/types/core/agent/buildAgentChart.d.ts.map +1 -1
  121. package/dist/types/core/agent/buildDynamicAgentChart.d.ts.map +1 -1
  122. package/dist/types/core/agent/buildToolRegistry.d.ts +20 -0
  123. package/dist/types/core/agent/buildToolRegistry.d.ts.map +1 -1
  124. package/dist/types/core/agent/coverage/ledger.d.ts +5 -0
  125. package/dist/types/core/agent/coverage/ledger.d.ts.map +1 -1
  126. package/dist/types/core/agent/selfCallNotice.d.ts +20 -7
  127. package/dist/types/core/agent/selfCallNotice.d.ts.map +1 -1
  128. package/dist/types/core/agent/skillBrains.d.ts +12 -4
  129. package/dist/types/core/agent/skillBrains.d.ts.map +1 -1
  130. package/dist/types/core/agent/stages/stepNudge.d.ts +13 -4
  131. package/dist/types/core/agent/stages/stepNudge.d.ts.map +1 -1
  132. package/dist/types/core/agent/stages/toolCalls.d.ts +185 -0
  133. package/dist/types/core/agent/stages/toolCalls.d.ts.map +1 -1
  134. package/dist/types/core/agent/stages/wrapUp.d.ts +44 -2
  135. package/dist/types/core/agent/stages/wrapUp.d.ts.map +1 -1
  136. package/dist/types/core/agent/types.d.ts +22 -4
  137. package/dist/types/core/agent/types.d.ts.map +1 -1
  138. package/dist/types/core/agent/window/currentRequest.d.ts +4 -2
  139. package/dist/types/core/agent/window/currentRequest.d.ts.map +1 -1
  140. package/dist/types/core/runbook/recording.d.ts.map +1 -1
  141. package/dist/types/core/slots/buildToolsSlot.d.ts +1 -1
  142. package/dist/types/core/slots/buildToolsSlot.d.ts.map +1 -1
  143. package/dist/types/doors/skill-graph.d.ts +1 -1
  144. package/dist/types/doors/skill-graph.d.ts.map +1 -1
  145. package/dist/types/events/payloads.d.ts +34 -2
  146. package/dist/types/events/payloads.d.ts.map +1 -1
  147. package/dist/types/index.d.ts.map +1 -1
  148. package/dist/types/lib/injection-engine/index.d.ts +1 -1
  149. package/dist/types/lib/injection-engine/index.d.ts.map +1 -1
  150. package/dist/types/lib/injection-engine/skillGraph.d.ts +56 -0
  151. package/dist/types/lib/injection-engine/skillGraph.d.ts.map +1 -1
  152. package/dist/types/lib/injection-engine/skillSteps.d.ts +20 -1
  153. package/dist/types/lib/injection-engine/skillSteps.d.ts.map +1 -1
  154. package/dist/types/lib/injection-engine/skillToolDescriptors.d.ts +18 -0
  155. package/dist/types/lib/injection-engine/skillToolDescriptors.d.ts.map +1 -1
  156. package/dist/types/lib/injection-engine/types.d.ts +12 -10
  157. package/dist/types/lib/injection-engine/types.d.ts.map +1 -1
  158. package/dist/types/lib/saidByPerson.d.ts +48 -15
  159. package/dist/types/lib/saidByPerson.d.ts.map +1 -1
  160. package/dist/types/lib/spokenIds.d.ts +53 -0
  161. package/dist/types/lib/spokenIds.d.ts.map +1 -0
  162. package/dist/types/lib/trace-toolpack/traceToolpack.d.ts.map +1 -1
  163. package/package.json +1 -1
package/CHANGELOG.md CHANGED
@@ -7,6 +7,496 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
7
7
 
8
8
  ## [Unreleased]
9
9
 
10
+ ## [9.86.1] - 2026-09-06
11
+
12
+ The release that removed hand-counted lists shipped with one, and with main red.
13
+
14
+ `chore: release v9.86.0` failed CI (run 34008382993, the `coverage` job) while both
15
+ plain `test` jobs and the local release gate passed. Two tests parse every file
16
+ under `src/` with the TypeScript compiler and were called fresh inside every `it`
17
+ — seven parses in one suite, three in the other — and under the coverage job's
18
+ v8 instrumentation each parse took 5.3–6.2 s on the CI runner, past vitest's 5 s
19
+ default. Locally the same parse takes about a second. The release script ran
20
+ `npm test` and never `npm run test:coverage`, so the one command that would have
21
+ shown the failure was the one it did not run. Both walks now parse `src/` once per
22
+ suite and carry a 60 s budget of their own, and Gate 4 of `scripts/release.sh` runs
23
+ `npm run test:coverage` — the instrumented run is a superset of the plain one, so
24
+ the gate now sees what CI sees.
25
+
26
+ Everything else here is one review of 9.86.0, taken finding by finding.
27
+
28
+ ### Fixed
29
+
30
+ - **The `'guard'` refusal asserted a fact it did not have.** With no menu
31
+ outstanding, `composeReadSkillRefusal` ended every refusal with "Declared routes
32
+ moved the cursor instead." The gate handed it a boolean derived from two of
33
+ `TurnRoute.by`'s six values, so the clause was composed for the other four too:
34
+ false for `'continuity'` (the cursor was carried over from the previous turn and
35
+ nothing moved it — the verdict every follow-up produces under `{ strictness:
36
+ 'guard', continuity: 'conversation' }`), false for `'menu'` resolved by the
37
+ model's own pick, and unprovable for `'none'`. The composer now takes
38
+ `turnStartedBy: TurnRoute['by']` and says one past fact per value — "the turn's
39
+ start had already been resolved decisively", "the cursor had been carried over
40
+ from the previous turn", "the menu had already been resolved by an earlier
41
+ pick", "the menu had been resolved by the configured decider before the turn's
42
+ first call" — and nothing for `'none'`. The old tail is gone from every arm.
43
+
44
+ - **Two refusals named a cursor the role may not be told about.** The `read_skill`
45
+ description withholds a hidden cursor's name by its own law, and the gate's
46
+ refusal printed the same id raw in two clauses ("was not reachable from 'alpha'.
47
+ Skills reachable from 'alpha' when that call was made: …"). The cursor now goes
48
+ through the same filter as the hops: a hidden cursor is anchored as "the skill
49
+ the cursor stood in" — the skill is real and merely unnamed — and "the turn's
50
+ start" is kept for a genuine cold start, which is a different fact. The
51
+ `propose-transition` refusal in the tool-effects judge had the same leak twice
52
+ over: it composed "(reachable: beta, gamma)" from the raw hop set and "from
53
+ 'alpha'" from the raw cursor, and that sentence is appended to the tool result
54
+ the model reads. It reads `scope.hiddenSkillIds` now, names the filtered hops,
55
+ omits the clause when the filter emptied them, and anchors a hidden cursor the
56
+ same way. `skill.rejected.currentSkillId` stays raw on purpose: it is the
57
+ operator's record on the event channel, not a sentence the model reads.
58
+
59
+ - **A same-batch STAY did not compete for the transition slot.** The law is "first
60
+ ACCEPTED proposal wins; later proposals to OTHER targets are superseded", and a
61
+ stay is accepted — but it `continue`d past the bookkeeping, so a tool that judged
62
+ its data first and said "stay" lost to a sibling later in call order that said
63
+ "move", with two `'accepted'` events in one batch, the cursor moved, and no
64
+ `route_conflict` on the record. A stay judged first now holds the slot (writing
65
+ nothing to `pendingToolTransition`, because a stay moves nothing) and the later
66
+ hop is `'superseded'` with the batch's `route_conflict` naming the stay as the
67
+ winner; a stay judged after an accepted hop is the one superseded. Two stays are
68
+ both accepted, as two hops to one target are.
69
+
70
+ - **Both 9.86.0 frames pointed with the word the same release repaired elsewhere.**
71
+ The wrap-up instruction read "exhausted before this call … This call was for the
72
+ final answer" and the stepped-skill nudge "This call was for running them". A
73
+ frame is written into the `iteration_end` payload the checkpoint snapshots and
74
+ restored verbatim by `applyContinuation`, so on the next `.continue()` turn —
75
+ tools back on the wire — a model resolves "this call" to the call it is
76
+ answering and reads "no tools were offered on it" about a request that offers
77
+ them. They now name the call: "the wrap-up call this message opened … That call
78
+ was for the final answer", and "This message asked for them to be run". The
79
+ checker only knew `on this call`; a bare `this call` row catches the shape now,
80
+ and it found nine more: seven `inspect_tool_call` result lines and the
81
+ `inspect_tool_run` retention note, all anchored to `call '<id>'`; the coverage
82
+ ledger's `COVERAGE_NOTE` ("ground the call this result answers did not look at";
83
+ `canonical-notes.json` is regenerated by the build); and the runbook
84
+ `recording_note`. The checkIn-resume refusal in the tool-calls stage — "cannot be
85
+ retried this turn … Answer without it, or finish", a forecast plus a standing
86
+ order on a persistent result — is a past fact about the resumed call now.
87
+
88
+ ### Changed
89
+
90
+ - **The shape rows match the grammar they claim.** A second probe of seventeen
91
+ sentences written AGAINST the rows — the plainest forecast forms, not the ones the
92
+ rows had been derived from — walked past all four 9.86.0 shape rows: the
93
+ effect-verb row knew no future or modal tense ("will move you", "can switch
94
+ you"), the cursor row wanted a quote right after `in ` ("You are in skill
95
+ 'alpha'", "You're in 'alpha'", "Your current skill is 'alpha'"), the copula row
96
+ knew six nouns ("is enabled", "is mounted", "are offered", "is off the wire",
97
+ "have been withheld"), and a headed list ("Available tools: calc, probe.")
98
+ has no copula at all. The rows are widened, a headed-inventory row and a
99
+ next-call-forecast row are added, and the seventeen sit beside the fifteen in
100
+ `test/modelFacingSurfaces.test.ts` so the next narrowing fails by name. The
101
+ `src/` walk then flagged **twenty-five** more literals: ten are repaired above,
102
+ fourteen are host-facing errors and check-up warnings now classified, and one —
103
+ an integrity finding's frame line — joins the work list. The ledger stands at
104
+ **ninety-one files / one hundred and seventy-eight literals**, with
105
+ **thirty-four** unrepaired across thirteen entries; the suite computes those
106
+ numbers.
107
+
108
+ - **A row may no longer exempt every lifetime.** `provableWhen` naming both
109
+ lifetimes compiled, carried an argument, and disabled the row everywhere — the
110
+ exemption-with-no-argument defect in a new coat. The suite asserts a strict
111
+ subset now. The header of `test/helpers/modelFacingClaims.ts` also says what
112
+ "may speak in the present" means beside the `now` row: present TENSE reported
113
+ as the state of the request, not the deictic adverbs, which point at the moment
114
+ of reading on every surface.
115
+
116
+ - **The divergence walk's summary block is checked whole.** Only `walk.cases` was
117
+ read back; the other five numbers were written on update and never compared.
118
+ All six are now derived from the recorded case outcomes and the row set. The
119
+ placeholder gate also refuses `TODOs` and `to-do`, and the header names five
120
+ defects, not three. A `claim-swallowed` row's auto-composed `cause` says "names
121
+ this claimant as the winner of a wire it never reached" when the shadow report
122
+ names the swallowed claimant itself — the framework's `skill-scoped:self-explain`
123
+ provider — instead of "describes a different pair", which it does not.
124
+
125
+ - **Anchors in the walk's baseline and the design note name symbols, not lines.**
126
+ The ten 9.86.0 rows cited `buildToolRegistry.ts` line ranges from the 9.85.0 tree
127
+ that the same release had moved by about twenty-nine lines, beside a
128
+ `buildToolsSlot.ts` line from HEAD. They name the checks now (`holders.includes
129
+ (PRESENT_TOOL_NAME)`, the `seenNames` loop, the `sharedSkillTools` backfill), and
130
+ the baseline's `note` says so. The `present-vs-mcp` row no longer claims the MCP
131
+ cell proves the blind spot is the provider channel: both claimants mount through
132
+ `staticTools()`, so the cell shows an MCP catalog inherits that seam unchanged and
133
+ nothing more.
134
+
135
+ - **`scripts/release.sh` Gate 4 runs `npm run test:coverage`.** See the opening.
136
+
137
+ ### Docs
138
+
139
+ - `docs/design/2026-09-recorded-not-built.md`: entry 1's "16 baseline rows" is 22
140
+ (18 with a provider's or a skill's tool dead, 4 with the framework's own), the
141
+ appendix lists it as the third corrected sentence, and the row paragraph says
142
+ four-and-six rather than eight-and-two.
143
+ - `docs/api-reference/interfaces/AgentOptions.md`, tracked and last regenerated at
144
+ 9.58.0, still quoted the pre-9.86.0 wrap-up sentence ("Do not request tools");
145
+ the quote is updated in place, as are the three other copies.
146
+ - The skill-graph quickstart says a host wiring its own `read_skill` under a
147
+ `tree()` must set `ReadSkillOffer.treeRouted`, and why.
148
+
149
+ ### Changelog corrections
150
+
151
+ Four sentences in 9.86.0 are corrected in place, each marked where it stands:
152
+
153
+ - _"Eight of the ten are already-recorded seams … Two are defects"_ — four rows
154
+ reach recorded seams and six record the two new defects.
155
+ - `ToolRegistryArtifacts.toolDeclaringSkills` and `AgentState.hiddenSkillIds` were
156
+ listed under Added as if public; neither type is exported from any door.
157
+ - `unknownToolResult` was called "exported"; it is a module export inside the
158
+ tool-calls stage and not on any door.
159
+ - The `report-misattributed` bullet did not say that `reported` — a field the
160
+ ratchet compares — changed body on nine unrenamed rows.
161
+
162
+ ### Deliberately not changed
163
+
164
+ - **The two permission-denied arms** ("This will not change during this run — do
165
+ not call it again") stay on the unrepaired ledger. Making the sentence true means
166
+ latching a denial per run, a behaviour change with no field finding behind it;
167
+ rewording it is that entry's own packet.
168
+ - **The `now` row keeps no exemption.** The header now argues the same thing the
169
+ row does, rather than the row being softened.
170
+ - **`skill.rejected.currentSkillId`** is not role-filtered — see above.
171
+
172
+ ## [9.86.0] - 2026-09-05
173
+
174
+ Every hand-counted list in 9.84.0 and 9.85.0 was short by one or two.
175
+
176
+ "Five classes of `role: 'user'` message are authored by this library" — seven
177
+ were. "Reachability OR posture" — three arms refuse. "Sixty configurations,
178
+ crossing every source" — the cross skipped four of the seven sources it named,
179
+ and there are seventy-six. The rules that catch a sentence which outlives its
180
+ moment were a transcript of the wordings that had already escaped: thirteen of
181
+ fifteen plausible forward-looking sentences walked straight through them.
182
+
183
+ None of those was a typo. Each was a fact the library computed in one place and
184
+ re-derived, by hand, wherever a second consumer needed it — and a hand-derived
185
+ list is a list that is short the day after somebody adds the next case. Three
186
+ of them are now WALKS rather than counts: the user-turn producers are parsed out
187
+ of `src/` with the TypeScript compiler, every sentence-shaped literal in `src/`
188
+ is run through the model-facing rules with a file and a line on failure, and the
189
+ offer/dispatch cross iterates its source list whole instead of filtering it.
190
+ Two more are single owners: one function answers "is this `read_skill` target
191
+ the cursor?", and one scope key answers "which skill ids may this role see?".
192
+
193
+ ### Fixed
194
+
195
+ - **`read_skill` refusals answered for a fact nobody owned.** Five call sites
196
+ needed to know that `makeReachableSkills` filters the cursor out of its own
197
+ successor set — correct for a MOVE, silent about a READ. Three of them wrote
198
+ their own `requested === cursor` line. Two never heard: a tool proposing
199
+ `propose-transition` back to the cursor's own skill was refused as unreachable,
200
+ and the `skill_read` permission gate was asked to grant a capability the model
201
+ was already exercising, then told the model that its own skill was "not
202
+ available in this context".
203
+
204
+ `classifySkillTarget({ cursor, target, hops, open })` now owns it, returning
205
+ `'self' | 'hop' | 'open' | 'unreachable'`. It is a pure function in the
206
+ injection engine, exported through the `agentfootprint/context` barrel and the
207
+ `agentfootprint/skill-graph` door — the same function object through both, so a
208
+ foreign host cannot re-derive it wrongly either. The five consumers switch on
209
+ it: the gate arm, `describeOffer`, the tool-effects judge, the `skill_read`
210
+ permission gate, and the refusal composer. `makeReachableSkills` keeps its own
211
+ exclusion — it is the PRODUCER of the hop set, and excluding the cursor is what
212
+ a move means — and its doc comment now says the exclusion is about movement and
213
+ sends the next reader to `classifySkillTarget`.
214
+
215
+ - **A refusal could name a skill the caller's own policy hides.** Role visibility
216
+ was a property of one builder: `Agent.hiddenSkillIdsNow()` fed
217
+ `buildReadSkillTool` and nothing else. So the description named nothing hidden
218
+ while the gate, one stage downstream, composed its refusals — and filled
219
+ `skill.rejected.allowed` — from the graph's raw sets.
220
+
221
+ The tools slot now resolves the hidden set once per iteration, publishes it on
222
+ `scope.hiddenSkillIds`, and both chart shapes bubble it. The gate keeps two sets
223
+ on purpose: the RAW one it judges with, and the filtered one it speaks with. It
224
+ judges with the raw set because a narrowing may take a schema off the wire and
225
+ may never take a name out of the dispatch map — filtering admission would remove
226
+ a capability, which the monotone rule forbids. In practice a hidden id never
227
+ reaches the gate, because the same checker denies it upstream; the filter is what
228
+ makes that true by construction rather than by coincidence.
229
+
230
+ - **Two refusal composers that contradicted each other forty lines apart are one.**
231
+ `skillRefusal` and `postureRefusal` are replaced by `composeReadSkillRefusal`,
232
+ and every arm of it is a past fact about the one call it names. Gone with them:
233
+ "from here" (deixis — a different place on every re-read), "Pick one of these, or
234
+ finish" (an exhortation in a string that persists for the rest of the run), and a
235
+ posture arm that named a hop the very next arm would have declined. A refusal now
236
+ opens `read_skill("X") was not granted on that call:` and every clause after it
237
+ refers back to that call.
238
+
239
+ - **`Unknown tool: X` told the model it was wrong and never what would have
240
+ worked.** Both dispatch doors now compose one `unknownToolResult` (a module
241
+ export inside the tool-calls stage, not on any package door — _corrected in
242
+ 9.86.1_), which
243
+ names the dispatch roster: `Unknown tool 'X' on that call. Tool names that
244
+ resolved to an implementation on that call: …`, or, with an empty roster, that
245
+ none did. The leading `Unknown tool` token is preserved, so every matcher on it
246
+ is untouched.
247
+
248
+ It says *resolved*, not *could be dispatched*, because two gates sit between
249
+ resolution and a tool running — the `tool_call` permission check and the
250
+ middleware chain — and neither is asked to phrase an error. And the roster is
251
+ role-filtered before it is named: it used to read the dispatch map raw and could
252
+ name a tool belonging to a skill the caller's own policy hides, which is the
253
+ leak the refusals had just closed, one sentence over. `buildToolRegistry` now
254
+ returns `toolDeclaringSkills` (tool name → the skills that declare it) from the
255
+ walk it was already doing, and a name is withheld only when EVERY declaring
256
+ skill is hidden — a tool two skills share stays named. Dispatch is untouched.
257
+
258
+ - **A filtered-empty list was reported as an empty one — a Lens denying what the
259
+ Fold holds.** Three sentences branched on `length > 0` over an already-filtered
260
+ array, so "the graph held nothing" and "the role filter emptied it" composed the
261
+ same words. A cursor whose only declared hop was hidden answered `read_skill`
262
+ with *"No skill was reachable from 'alpha' when that call was made."* while the
263
+ graph was routing `alpha`; a `'guard'` menu whose every id had been hidden since
264
+ the turn started said *"no menu was outstanding when that call was made.
265
+ Declared routes moved the cursor instead."* — two false clauses in one breath.
266
+
267
+ A model told the map is a dead end stops asking for the door it may not be
268
+ shown, and the checker cannot see it: every one of those sentences passes
269
+ `unprovable()`, because the defect is in what the composer was handed, not in
270
+ how it was worded. So the fact is now a type. `SpokenIds` carries both halves of
271
+ a filtered set — `named`, and `held` for whether the unfiltered set held
272
+ anything — and `held` is required, so the compiler asks every caller the
273
+ question every call site forgot to answer. Where a filter empties a set the
274
+ clause is OMITTED. Omission is free and always true; the negative is a denial.
275
+
276
+ The fourth sentence was the one the model reads to CHOOSE. `describeOffer`
277
+ computed its columns from an already-filtered catalog, so a cursor whose only
278
+ declared hop is hidden was told *"Nothing is reachable from here — answer with
279
+ the skill you are in, or finish."* while the graph held that edge. It classifies
280
+ the hop set over the unfiltered catalog now and drops the clause when the filter
281
+ is what emptied it; with nothing wired out at all the sentence still stands,
282
+ because that absence is one the description has evidence for. Reaching it meant
283
+ moving `SpokenIds`/`spoken` to `src/lib/spokenIds.ts`: it lived in the tool-calls
284
+ stage, on the wrong side of the skill-graph fence, so the description — composed
285
+ inside `src/lib/injection-engine/`, which may not import the agent loop — was the
286
+ one surface that could not use the fact its own refusals were repaired with.
287
+
288
+ - **Under a `.tree()` with nothing open, `read_skill` is no longer offered.** A
289
+ tree routes by predicate on every iteration and keeps no cursor, so the tool had
290
+ nothing it could do and a menu of one refusal is worse than no menu. The schema
291
+ leaves the request; the NAME stays in the dispatch map, which is the same law as
292
+ everywhere else. With open skills present, the description explains the tree and
293
+ lists exactly what a pick can open, instead of printing "Nothing is reachable
294
+ from here".
295
+
296
+ - **Two library-authored user turns were credited to a person.** The out-of-budget
297
+ wrap-up instruction and the stepped-skill nudge both append to `scope.history`
298
+ with `role: 'user'` and took no registered opening, so `isSaidByPerson` said a
299
+ person wrote them. Two things followed. The window's refusal engine could pin
300
+ "the current request" on the framework's own wrap-up instruction and drop the
301
+ real request underneath it. And a routing rule written the documented way —
302
+ `saidByPerson(ctx).some((m) => m.content.includes(…))` — matched on the library's
303
+ own bookkeeping: the wrap-up said "Do not request tools", and the nudge names a
304
+ skill id and every unrun step's tool name.
305
+
306
+ Both are registered now, and `LIBRARY_AUTHORED_PREFIXES` holds all six openings
307
+ frozen, so the writer and the recogniser read one constant. Both sentences were
308
+ also rewritten: each was composed once and re-read on every later call of the
309
+ turn, which made their present-tense clauses predictions.
310
+
311
+ - **Three trace-toolpack results said "this call" and "right now".** They are
312
+ anchored to the call they answer, in the past tense — including the one arm the
313
+ new deictic-container rule caught the first time it was ever composed.
314
+
315
+ - **`escalation` counts three kinds of refusal, and its docs named two.**
316
+ "Reachability OR posture" is wrong in the JSDoc behind `EscalationPolicy` and
317
+ `SkillGraphOptions`, in the `skill.escalated` payload doc and on the skills page:
318
+ the counter fires beside all three `skill.rejected` emit sites, self-call
319
+ included. No behaviour changed — the self-call site has counted since 9.84.0,
320
+ deliberately.
321
+
322
+ ### Added
323
+
324
+ - **A WALK over every `role: 'user'` construction site in `src/`.**
325
+ `test/lib/injection-engine/userTurnProducers.test.ts` parses the tree with the
326
+ TypeScript compiler (a `PropertyAssignment` of `role: 'user'`, so type members
327
+ and comments quoting the string are not counted) and requires every site to be
328
+ classified as an authored frame, a person's own words, or never-in-history, each
329
+ with a written reason. **Thirty-five** sites are classified today: six authored
330
+ frames, seven person, twenty-two never-in-history. Sites are keyed by file with
331
+ the per-file COUNT asserted, so a new producer inside an already-listed file
332
+ fails as loudly as one in a new file. One producer the parser cannot see — the
333
+ message an injection delivers, whose role is copied off the `Injection` — is
334
+ named in the header and pinned by its `injectedBy` marker instead.
335
+
336
+ - **The model-facing checker judges SHAPE, not just remembered wordings.** Four
337
+ new rules: a present-tense copula with a capability noun, deictic-present
338
+ adverbs, second-person effect verbs, and a standing imperative at a clause
339
+ start. Fifteen plausible forward-looking sentences were written out and put
340
+ to the rules: "You are currently in 'alpha'", "Calling read_skill switches you
341
+ to beta", "The following tools are available to you: …", "Nothing is live in
342
+ this scope at the moment". All fifteen are caught by the rules as they stand,
343
+ and the suite asserts exactly that. **Thirteen** of them passed against the
344
+ rule list AS IT STOOD BEFORE THIS RELEASE — the number that motivated the
345
+ work, measured once against a list this tree no longer contains, so it is a
346
+ record of why the rules changed rather than something a run here can
347
+ reproduce. `exemptBecause` is now structurally
348
+ required: `BannedClause` is a discriminated union, and because the root
349
+ `tsconfig.json` excludes `test/`, that is proven where it can actually be
350
+ compiled — `test/type-regressions/`.
351
+
352
+ - **A WALK over every sentence-shaped literal in `src/`.** The registry's own
353
+ header used to say the gap it could not close was "a scan of `src/` … and this
354
+ is not that". `test/modelFacingScan.test.ts` is that: it parses every `.ts` file
355
+ under `src/` with the TypeScript compiler, folds `+` chains and template holes,
356
+ runs each literal through the rules at the persistent lifetime, and fails with
357
+ `file:line` unless the file's flagged literals are accounted for in a ledger of
358
+ **eighty-four files / one hundred and sixty-three literals**, every entry naming
359
+ where the string is delivered and how many literals it covers. Per-file counts
360
+ are the guard again. Four things it cannot see are stated in its header rather
361
+ than left to be found: a sentence assembled across statements, text that lives
362
+ in data rather than in `src/`, literals under twenty-five characters, and any
363
+ falsehood that avoids all the shapes.
364
+
365
+ Its ledger carries an `unrepaired` bucket of **thirty-three** literals across
366
+ thirteen entries that are model-facing, persistent and correctly caught, and
367
+ that were left alone because each needs its own tests. They are named with their
368
+ delivery site, so the bucket is a work list rather than a pardon. The bucket's
369
+ arithmetic is asserted by the suite itself — the counts above come from a run,
370
+ not from a report, which is the failure this whole entry is about.
371
+
372
+ - **Five live producers are registered and read.** The `read_skill` refusal
373
+ composer (every arm), `unknownToolResult`, the trace toolpack's inspection
374
+ results, and — closing the gap 9.85.0's registry named — the wrap-up and
375
+ stepped-skill frames at a shared `INJECTED_TURN` surface.
376
+
377
+ - **The offer/dispatch cross iterates its source list whole.** `frameworkCases()`
378
+ re-derived a source list inside the walk —
379
+ `CLAIMANTS.filter((c) => ['static', 'provider', 'skill-active'].includes(c.id))`
380
+ — so a second hand-written list of sources existed with nothing keeping it in
381
+ sync with the first, and 9.85.0's "crosses every source" was false. That mattered
382
+ because the framework's four auto-attach reservations each read a DIFFERENT
383
+ build-time list, so which source holds a contested name is precisely what decides
384
+ whether a reservation can see it. The walk goes from **sixty configurations to
385
+ seventy-six** and from **thirty-six divergence rows to forty-six**; all ten new
386
+ rows carry a hand-written, checked `tolerated`. Three new tests own what was
387
+ previously true only because somebody had typed it: that every claimant is
388
+ crossed against every auto-attach name, that the header's arithmetic equals the
389
+ recorded case count, and that a placeholder `tolerated` is refused (empty,
390
+ `todo`/`tbd`/`fixme`/`xxx` on a word boundary in any case, or under forty
391
+ characters — a floor on effort, not a measure of truth).
392
+
393
+ Four of the ten reach seams the 9.85.0 baseline already recorded, through a
394
+ source that had never been crossed, and say so. Six record two defects nobody
395
+ had recorded — four rows for entry 4 and two for entry 5 of
396
+ `docs/design/2026-09-recorded-not-built.md` — rather than papering over them
397
+ (_corrected in 9.86.1: this paragraph said "eight of the ten" and "two", a
398
+ count of defects presented as a count of rows_): `.selfExplain()` reserves its trace-tool names against
399
+ `this.registry` and never `this.injectionList`, making it the one auto-attach
400
+ family with no net at all against a skill's `tools: []`; and the misattributed
401
+ shadow report can now name a `skill-scoped:self-explain` provider — one the
402
+ consumer did not write and cannot open — as the file to go look at.
403
+
404
+ - **`report-misattributed` rows carry their attribution in the row id.** A shadow
405
+ event's meaning lives in its `schemaFromId`/`dispatchToId`, and the row was keyed
406
+ on case + tool + epoch, so two reports naming different sources in one epoch — a
407
+ strictly worse fact than one wrong report — collapsed into one `Map` entry and
408
+ vanished. The `reported` column of every row now carries the `*Id` halves too
409
+ (`schemaFrom=provider(static) dispatchTo=skill(desk-active)`), which is why nine
410
+ rows whose ids did not move changed body in the same re-record (_added in
411
+ 9.86.1; the re-record changed a compared field and the entry did not say so_).
412
+
413
+ - **`SkillRejectedPayload.allowed` is what the model was actually told.** Role-
414
+ filtered rather than the graph's raw set. Shape unchanged; only agents with a
415
+ `PermissionChecker` governing `'skill_read'` see any difference. The field's own
416
+ JSDoc says so at the call site, which is the doc a consumer actually reads.
417
+
418
+ - **`ToolRegistryArtifacts.toolDeclaringSkills`** (internal — `ToolRegistryArtifacts`
419
+ is not exported from any door; _corrected in 9.86.1_) — tool name → the ids of the
420
+ skills whose `inject.tools` carry it, recorded on the walk `buildToolRegistry`
421
+ was already doing and thrown away. Empty for an agent whose skills carry no
422
+ tools. Its one consumer is the unknown-tool roster's role filter; it exists so
423
+ that consumer does not walk `Agent.injections` a second time to re-derive what
424
+ this file already knew.
425
+
426
+ - **`ToolEffectPayload.stay?: true`** on `agentfootprint.tools.effect` — a
427
+ `propose-transition` naming the cursor's own skill is accepted as a no-op.
428
+ Deliberately not a fourth `outcome`, so an exhaustive consumer switch keeps
429
+ compiling.
430
+
431
+ - **`AgentState.hiddenSkillIds?: readonly string[]`** (internal — `AgentState` is
432
+ not exported from any door; _corrected in 9.86.1_) — the per-iteration
433
+ role-hidden set, written by the tools slot and read by the `read_skill` gate.
434
+
435
+ - **`ReadSkillOffer.treeRouted?: boolean`** — declares the mounted graph a decision
436
+ tree, which is what lets the descriptor withhold the offer.
437
+
438
+ ### Two decisions worth stating plainly
439
+
440
+ - **A self-call at a MOUNTED cursor is answered BEFORE the permission gate**, because
441
+ it exercises no capability. `read_skill` naming the cursor's own skill activates
442
+ nothing and moves nothing, so there is no grant for a `PermissionChecker` to make or
443
+ withhold; asking it produced a denial about the one skill whose body was already in
444
+ that call's system prompt. The skip stops at a PARKED cursor, and deliberately: a
445
+ park suppresses a map's contribution without moving the cursor, so the gate below
446
+ reads the same id as a RE-ENGAGEMENT and puts the body and its tools back on the
447
+ wire — which is a capability, and the policy's question to answer. One predicate,
448
+ `atMountedCursor`, is what both gates ask. Every other id still goes to the policy.
449
+ The refusal BUDGET is
450
+ unchanged and still counts the self-call, including the `surfaceMode: 'both'`
451
+ re-read that returns the body — the 9.84.0 argument stands, and it is about the
452
+ loop rather than about the wording: a model that keeps asking the graph where it
453
+ stands instead of working is exactly the stuck run escalation exists for.
454
+
455
+ - **A `propose-transition` naming the cursor's own skill is a STAY**, accepted as a
456
+ no-op with `stay: true` on the event and no refusal on the result. The tool asked
457
+ for a state the run is already in; there is nothing to move and nothing to refuse.
458
+
459
+ ### Deliberately not changed
460
+
461
+ - **The three `STATED:` prose pins** in `src/core/agent/buildToolRegistry.ts` are
462
+ untouched, word for word. `test/core/agent/epoch-laws.test.ts` and
463
+ `test/core/agent/toolDivergenceWalk.test.ts` both read them.
464
+ - **The escalation budget still counts `'both'`-mode self-call re-reads**, per the
465
+ argument above.
466
+ - **The grounding gate** — item 5 of the "Offer, Not Dispatch" review — is a new
467
+ DIAL, not a fix for anything here, and is not in this release.
468
+ - **The flat default is not narrowed.** `scopeTools` stays `false` until 10.0.0.
469
+ - **`isLibraryAuthoredTurn`** (the evidence gate's exempt corpus) is deliberately
470
+ narrower than `isSaidByPerson` and was not widened to the two new frames. It
471
+ decides who SUPPLIED a value, not who wrote a turn; widening it would change which
472
+ values the evidence gate exempts, with no finding behind it.
473
+
474
+ ### Changelog corrections
475
+
476
+ A reader auditing this project by its changelog has to be able to trust the older
477
+ entries, so six sentences in 9.84.0 and 9.85.0 are corrected in place, each marked
478
+ where it stands:
479
+
480
+ - **9.84.0** — _"Five classes of `role: 'user'` message"_: seven kinds are
481
+ library-authored; the wrap-up instruction and the stepped-skill nudge went
482
+ unregistered until this release.
483
+ - **9.84.0** — _"The window's own refusal engine has always applied that rule"_: it
484
+ applied a three-class version, and 9.84.0 widened it to five.
485
+ - **9.84.0** — _"a step or park hold-out says the tools were withheld rather than
486
+ naming them"_: the withheld arm names the declared tools, and a parked cursor
487
+ never reaches the notice at all.
488
+ - **9.85.0** — the fifth _"model-facing sentence"_ bullet credited 9.85.0 with a
489
+ `read_skill` description fix that shipped in 9.84.0, and quoted a sentence that
490
+ existed only in a source comment. Removed, with the reason left in its place; the
491
+ count above it is now four.
492
+ - **9.85.0** — _"drives a real run per configuration. Sixty configurations,
493
+ thirty-six divergences"_: forty of seventy-six are driven, twenty-six are refused
494
+ at build and ten are not constructible; the enumeration it replaced was a
495
+ development draft, never a shipped list.
496
+ - **9.85.0** — law 1 was restated unscoped. It is scoped to the tools
497
+ `buildToolRegistry` routes, with the shadow seam and the walk as its recorded
498
+ exceptions.
499
+
10
500
  ## [9.85.0] - 2026-09-04
11
501
 
12
502
  A sentence composed once and read many times is not a fact — it is a prediction.
@@ -19,7 +509,7 @@ guard asserting a boundary it cannot verify.
19
509
 
20
510
  ### Fixed
21
511
 
22
- - **Five model-facing sentences that outlive the moment they were true.** Each is
512
+ - **Four model-facing sentences that outlive the moment they were true.** Each is
23
513
  now anchored to one named call, in the past tense, after tracing it to its
24
514
  delivery point to confirm it really is re-read:
25
515
 
@@ -34,12 +524,14 @@ guard asserting a boundary it cannot verify.
34
524
  sent right now"_, on a card that rides every call while a map is parked. Its
35
525
  falsifier is compose order, not staleness: the card is written in the
36
526
  injection-engine pass and the tools slot that acts on the park runs after it.
37
- - The `read_skill` description no longer predicts what `read_skill` will do.
38
- Naming the cursor is the fix; every sentence tried beside it turned out false
39
- somewhere. The last one — _"You do not need read_skill to go on using it"_ —
40
- was argued to be a necessity claim no posture, budget or hold-out could
41
- falsify. The PARK falsifies it: a parked member keeps the cursor, loses its
42
- body and its tools, and `read_skill` is then the only door back.
527
+
528
+ _Corrected in 9.86.0._ A fifth bullet stood here, crediting 9.85.0 with the
529
+ `read_skill` description fix and quoting _"You do not need read_skill to go on
530
+ using it"_ as a wording it had replaced. `skillToolDescriptors.ts` has no
531
+ non-comment change between `v9.84.0` and `v9.85.0`: that fix shipped in 9.84.0,
532
+ where it is also recorded, and the quoted sentence lived only inside a source
533
+ comment — no release ever put it on the wire. What 9.85.0 added to that file is
534
+ the LENS LAW block above `describeOffer`, which is a Documentation change.
43
535
 
44
536
  ### Added
45
537
 
@@ -59,13 +551,24 @@ guard asserting a boundary it cannot verify.
59
551
  `test/core/agent/toolDivergenceWalk.test.ts` crosses every source that can put
60
552
  a name on the wire or answer to one — static, provider, MCP, always-on skill,
61
553
  active skill, inactive skill, stepped skill — against six narrowing states and
62
- the framework's auto-attach names, and drives a real run per configuration.
63
- Sixty configurations, thirty-six divergences, each with a mechanically derived
64
- cause and a stated reason it is tolerated. New fails. Disappeared fails.
65
- Vacuous fails, and is unbaselineable.
554
+ the framework's auto-attach names. _Corrected in 9.86.0:_ at 9.85.0 the
555
+ auto-attach cross did NOT reach every source — it filtered `CLAIMANTS` down to
556
+ three of the seven — and the walk does not drive a real run per configuration.
557
+ Both are true of the walk as it stands after 9.86.0 widened it, with these
558
+ counts. Of its **seventy-six** configurations, **forty** are driven
559
+ as real runs (thirty-six divergent, four clean), **twenty-six** are refused at
560
+ build — which is the walk exercising a refusal, and its `because` records the
561
+ refusal's first line — and **ten** are not constructible at all, so no run is
562
+ attempted. **Forty-six** divergence rows come out of the forty driven, each
563
+ with a mechanically derived cause and a stated reason it is tolerated. New
564
+ fails. Disappeared fails. Vacuous fails, and is unbaselineable.
66
565
 
67
566
  It replaced a hand-written enumeration that claimed completeness and was
68
- falsified three rounds running. It then found three classes nobody seeded: a
567
+ falsified three rounds running — _corrected in 9.86.0:_ that enumeration was
568
+ drafted and falsified during this work, and no released version ever carried
569
+ it, so the walk shipped in place of a draft rather than of a shipped list.
570
+
571
+ It then found three classes nobody seeded: a
69
572
  provider tool whose name a registry holder already owns is dead in both
70
573
  directions and the shadow report cannot see it; the auto-attach names disagree
71
574
  about what they refuse; and `selfExplain` is a fourth family whose reservation
@@ -74,10 +577,17 @@ guard asserting a boundary it cannot verify.
74
577
  ### Documentation
75
578
 
76
579
  - **Three laws stated where the code lives**, epoch-scoped, after two earlier
77
- phrasings were false in shipped configurations. Every offered capability
78
- resolves to a dispatchable implementation with stable identity for that epoch;
79
- attention may alter the offer, but omission from the offer must not be
80
- presented as proof of permanent capability loss. Only static skill-registry
580
+ phrasings were false in shipped configurations. Law 1, as the source states it
581
+ and _corrected here in 9.86.0_, is SCOPED: **among the tools `buildToolRegistry`
582
+ routes**, every offered capability resolves to a dispatchable implementation
583
+ with stable identity for that epoch — same-epoch offer implies same-epoch
584
+ dispatch. It is not a claim about the whole wire, and the source names its
585
+ recorded exceptions rather than implying there are none: the SHADOW SEAM (the
586
+ wire list is merged one layer out in `buildToolsSlot` and carries provider
587
+ schemas these maps never hold), with the full enumeration delegated to
588
+ `test/core/agent/toolDivergenceWalk.test.ts`. The second clause is unscoped and
589
+ unchanged: attention may alter the offer, but omission from the offer must not
590
+ be presented as proof of permanent capability loss. Only static skill-registry
81
591
  tools are known to remain dispatchable after leaving the offer.
82
592
 
83
593
  - **`docs/design/2026-09-recorded-not-built.md`** — three real defects with
@@ -108,9 +618,13 @@ guard asserting a boundary it cannot verify.
108
618
  stands and which tools it could call, taken from the merged wire list the LLM
109
619
  stage actually sent, intersected with the skill's own declared tools — never
110
620
  from the declaration alone. Every configuration that would make that false has
111
- its own wording: a skill declaring no tools says so, a step or park hold-out
112
- says the tools were withheld rather than naming them, and a call whose wire
113
- cannot be established says nothing about tools at all. Mechanically it is still
621
+ its own wording: a skill declaring no tools says so, a hold-out names the
622
+ declared tools and states that they were withheld — _corrected in 9.86.0:_ this
623
+ read "a step or park hold-out says the tools were withheld rather than naming
624
+ them", and the withheld arm does name them; a PARK never reaches the notice at
625
+ all, because a self-call at a parked map member is a re-engagement request
626
+ (9.59.0) answered on an earlier arm — and a call whose wire cannot be
627
+ established says nothing about tools at all. Mechanically it is still
114
628
  a rejection — no activation, no cursor move, and the refusal budget still
115
629
  counts it, because a self-call _loop_ is exactly the stuck model that budget
116
630
  exists to escalate.
@@ -159,12 +673,21 @@ guard asserting a boundary it cannot verify.
159
673
  ### Added
160
674
 
161
675
  - **`saidByPerson(ctx)` / `isSaidByPerson(msg)` — telling what a person said from
162
- what the library wrote.** Five classes of `role: 'user'` message are authored
163
- by this library, not by a person: the compaction frame, the drop notice (whose
676
+ what the library wrote.** _Corrected in 9.86.0:_ this said "five classes", and
677
+ **seven** kinds of `role: 'user'` message are authored by this library, not by a
678
+ person. Five are registered here: the compaction frame, the drop notice (whose
164
679
  text names tools), the schema-check and evidence-check corrections, and any
165
- injection-delivered message. The window's own refusal engine has always applied
166
- that rule; a `when` predicate could not, because `InjectionContext.history`
167
- exposes only `{ role, content, toolName? }`. An author writing an entry rule
680
+ injection-delivered message. The out-of-budget wrap-up instruction and the
681
+ stepped-skill nudge were library-authored the whole time and went unregistered
682
+ until 9.86.0. _Also corrected:_ the window's own refusal engine had not "always
683
+ applied that rule" — it applied a THREE-class version (drop notice, compaction
684
+ frame, injection-delivered), and 9.84.0 widened it to the five registered here,
685
+ so a schema-check or evidence-check frame can no longer become the protected
686
+ anchor. That widening changes the anchor only when the run's own message text is
687
+ absent from the window, because the anchor is matched by content first and only
688
+ falls back to the last thing a person said. A `when` predicate could apply no
689
+ version of it, because `InjectionContext.history` exposes only
690
+ `{ role, content, toolName? }`. An author writing an entry rule
168
691
  that reads history was silently matching on our own bookkeeping. One
169
692
  implementation, reused by both — the rule cannot drift between routing and the
170
693
  window.
@@ -2,7 +2,7 @@
2
2
  "$comment": "GENERATED by scripts/gen-canonical-notes.mjs from this package's own built exports - do not edit by hand. These are the strings a tool written in another language must reproduce byte for byte: the static notes each result shape carries, the reserved marker keys that make those shapes recognizable, and the coverage block's heading. Keys are the exported constant names, so the same value is reachable as a TypeScript import from the package's main entry point. The package version lives in package.json.",
3
3
  "notes": {
4
4
  "ABSENCE_NOTE": "The search ran and matched nothing. This is an ANSWER, not an error: nothing failed, nothing was substituted for what was asked, and calling this tool again with the same arguments returns this same result. `checked` is the ground this answer covers; anything under `not_checked` or `cannot_cover` is ground it does NOT cover, and reaching that needs a different question, not a retry.",
5
- "COVERAGE_NOTE": "This result covers only what `checked` lists. `not_checked` is ground this call did not look at, and `cannot_cover` is ground this tool can never see — a clean result here is NOT evidence about either, and no retry changes `cannot_cover`. Carry these limits into any answer you build on this result.",
5
+ "COVERAGE_NOTE": "This result covers only what `checked` lists. `not_checked` is ground the call this result answers did not look at, and `cannot_cover` is ground this tool can never see — a clean result here is NOT evidence about either, and no retry changes `cannot_cover`. Carry these limits into any answer you build on this result.",
6
6
  "SEMANTICS_NOTE": "Typed data, not prose. `grain` and `provenance` are caveats that travel with the numbers: check `is_counter` before summing values from a series, read `measured_at` (and `age_seconds`) as how old the data is, and treat everything under `not_covered` as ground this result does NOT cover — a clean look here says nothing about it."
7
7
  },
8
8
  "markers": {