@sema-agent/client-core 0.108.0 → 0.110.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 (179) hide show
  1. package/CHANGELOG.md +294 -0
  2. package/README.md +77 -40
  3. package/dist/adapt/arms.js +41 -13
  4. package/dist/adapt/textStream.d.ts +2 -0
  5. package/dist/adapt/textStream.js +37 -16
  6. package/dist/adapter/activeRunSelfHeal.js +1 -1
  7. package/dist/adapter/downstream/eventToSdkMessage.js +11 -5
  8. package/dist/agentSession/engineAgentRegistryRead.js +2 -0
  9. package/dist/bashExitReading.d.ts +12 -0
  10. package/dist/bashExitReading.js +44 -0
  11. package/dist/checkpointRowVerdict.d.ts +1 -1
  12. package/dist/checkpointRowVerdict.js +4 -2
  13. package/dist/compensations.js +1 -1
  14. package/dist/declared/supportFloorDeclared.js +1 -1
  15. package/dist/displayUntrusted.js +132 -14
  16. package/dist/engineAgentAbsence.d.ts +2 -0
  17. package/dist/engineAgentAbsence.js +28 -0
  18. package/dist/engineErrorCodes.d.ts +7 -0
  19. package/dist/engineErrorCodes.js +7 -0
  20. package/dist/engineHealthFacts.d.ts +26 -0
  21. package/dist/engineHealthFacts.js +63 -4
  22. package/dist/engineInlineTaskStats.d.ts +4 -3
  23. package/dist/engineInlineTaskStats.js +18 -2
  24. package/dist/engineNoticeCodes.d.ts +138 -0
  25. package/dist/engineNoticeCodes.js +662 -55
  26. package/dist/engineRepoint.d.ts +140 -0
  27. package/dist/engineRepoint.js +285 -0
  28. package/dist/engineRowRef.js +2 -0
  29. package/dist/engineSessionMessages.d.ts +2 -1
  30. package/dist/engineSessionMessages.js +5 -0
  31. package/dist/errorSupplementRoles.js +5 -1
  32. package/dist/fleet/fleetProjection.js +3 -2
  33. package/dist/fleetTaskDesc.js +3 -3
  34. package/dist/generated/engineFactTables.d.ts +3 -0
  35. package/dist/generated/engineFactTables.js +18 -0
  36. package/dist/generated/engineNoticeTables.js +3 -0
  37. package/dist/generated/sdkFactTables.d.ts +1 -0
  38. package/dist/generated/sdkFactTables.js +4 -0
  39. package/dist/hitl/approvalAskers.d.ts +6 -0
  40. package/dist/hitl/approvalAskers.js +30 -0
  41. package/dist/hitl/approvalCardReasons.js +25 -4
  42. package/dist/hitl/approvalRequestFrameReading.d.ts +21 -0
  43. package/dist/hitl/approvalRequestFrameReading.js +90 -0
  44. package/dist/hitl/approvalResolution.d.ts +8 -1
  45. package/dist/hitl/approvalResolution.js +4 -0
  46. package/dist/hitl/approvalsFeed.js +1 -1
  47. package/dist/hitl/askGateWire.js +0 -2
  48. package/dist/hitl/askParkRowRouting.js +5 -2
  49. package/dist/hitl/automaticAnswerVerdict.d.ts +5 -2
  50. package/dist/hitl/automaticAnswerVerdict.js +18 -4
  51. package/dist/hitl/decisionConflict.d.ts +2 -1
  52. package/dist/hitl/decisionConflict.js +10 -4
  53. package/dist/hitl/decisionRefusal.js +1 -0
  54. package/dist/hitl/decisionSend.d.ts +3 -0
  55. package/dist/hitl/decisionSend.js +29 -11
  56. package/dist/hitl/editedApprovalAcceptance.d.ts +25 -0
  57. package/dist/hitl/editedApprovalAcceptance.js +26 -0
  58. package/dist/hitl/frameRouter.js +13 -19
  59. package/dist/hitl/gateLedger.d.ts +0 -4
  60. package/dist/hitl/gateLedger.js +0 -19
  61. package/dist/hitl/hitlBridge.d.ts +5 -0
  62. package/dist/hitl/hitlBridge.js +13 -9
  63. package/dist/hitl/hitlHostSurface.d.ts +1 -0
  64. package/dist/hitl/hitlHostSurface.js +7 -2
  65. package/dist/hitl/hostWithdrawal.d.ts +15 -0
  66. package/dist/hitl/hostWithdrawal.js +38 -0
  67. package/dist/hitl/livePendingAsk.js +17 -16
  68. package/dist/hitl/localAllowRule.js +2 -1
  69. package/dist/hitl/noAllowCause.d.ts +1 -1
  70. package/dist/hitl/noAllowCause.js +4 -2
  71. package/dist/hitl/parkExpiryJudgements.js +26 -1
  72. package/dist/hitl/parkOwnership.d.ts +2 -0
  73. package/dist/hitl/parkOwnership.js +48 -8
  74. package/dist/hitl/parkResolver.js +32 -16
  75. package/dist/hitl/parkedUnresumable.d.ts +13 -0
  76. package/dist/hitl/parkedUnresumable.js +80 -0
  77. package/dist/hitl/permissionRuleScope.d.ts +4 -0
  78. package/dist/hitl/permissionRuleScope.js +31 -0
  79. package/dist/hitl/persistedRulesWire.d.ts +2 -0
  80. package/dist/hitl/persistedRulesWire.js +13 -12
  81. package/dist/hitl/planReviewWire.js +14 -4
  82. package/dist/hitl/sessionAllowRoute.js +5 -4
  83. package/dist/hitl/sessionPolicyWire.js +6 -4
  84. package/dist/hitl/standingRows.d.ts +2 -5
  85. package/dist/hitl/standingRows.js +30 -15
  86. package/dist/hitl/toolApprovalWire.d.ts +42 -9
  87. package/dist/hitl/toolApprovalWire.js +267 -44
  88. package/dist/hooksWireCaps.js +35 -4
  89. package/dist/hostSettingsSource.js +2 -0
  90. package/dist/hostTurnSettings.d.ts +10 -4
  91. package/dist/hostTurnSettings.js +112 -5
  92. package/dist/i18n/messages.en.d.ts +251 -0
  93. package/dist/i18n/messages.en.js +254 -2
  94. package/dist/i18n/messages.zh.js +264 -2
  95. package/dist/index.d.ts +48 -12
  96. package/dist/index.js +29 -7
  97. package/dist/inheritEnvCapability.js +2 -0
  98. package/dist/inputAuthor.d.ts +16 -0
  99. package/dist/inputAuthor.js +44 -0
  100. package/dist/interruptRefusalNote.d.ts +2 -0
  101. package/dist/interruptRefusalNote.js +17 -0
  102. package/dist/liveModelCatalog.d.ts +2 -0
  103. package/dist/liveModelCatalog.js +21 -7
  104. package/dist/liveQuestionStore.d.ts +4 -1
  105. package/dist/liveQuestionStore.js +12 -3
  106. package/dist/localEngineJudge.d.ts +75 -0
  107. package/dist/localEngineJudge.js +242 -0
  108. package/dist/localEngineOccupancy.d.ts +88 -0
  109. package/dist/localEngineOccupancy.js +269 -0
  110. package/dist/localEngineSeatReading.d.ts +29 -0
  111. package/dist/localEngineSeatReading.js +294 -0
  112. package/dist/localEngineSpawn.d.ts +113 -0
  113. package/dist/localEngineSpawn.js +486 -0
  114. package/dist/localEngineStep.d.ts +286 -0
  115. package/dist/localEngineStep.js +1086 -0
  116. package/dist/localEngineStopLadder.d.ts +29 -0
  117. package/dist/localEngineStopLadder.js +139 -0
  118. package/dist/localEngineSwapCard.d.ts +43 -0
  119. package/dist/localEngineSwapCard.js +205 -0
  120. package/dist/localEngineTiming.d.ts +8 -0
  121. package/dist/localEngineTiming.js +8 -0
  122. package/dist/mcpEngineLeg.d.ts +3 -0
  123. package/dist/mcpEngineLeg.js +38 -1
  124. package/dist/mcpPanel.js +2 -0
  125. package/dist/mcpReconnect.d.ts +9 -1
  126. package/dist/mcpReconnect.js +132 -20
  127. package/dist/mcpWireCaps.js +2 -2
  128. package/dist/memoryEntriesWire.d.ts +1 -1
  129. package/dist/memoryEntriesWire.js +5 -0
  130. package/dist/modelCapabilityProbe.js +4 -3
  131. package/dist/modsFace.d.ts +8 -1
  132. package/dist/modsFace.js +27 -0
  133. package/dist/noticePresentation.d.ts +2 -0
  134. package/dist/noticePresentation.js +79 -1
  135. package/dist/notifications.d.ts +17 -6
  136. package/dist/notifications.js +67 -19
  137. package/dist/ownKey.d.ts +5 -0
  138. package/dist/ownKey.js +8 -1
  139. package/dist/peerFrames.d.ts +40 -2
  140. package/dist/peerFrames.js +158 -3
  141. package/dist/pluginHooksWire.d.ts +2 -1
  142. package/dist/pluginHooksWire.js +56 -1
  143. package/dist/replayBody.js +10 -2
  144. package/dist/request/taskRequest.d.ts +5 -0
  145. package/dist/request/taskRequest.js +138 -21
  146. package/dist/resumeRefusalCopy.d.ts +1 -3
  147. package/dist/resumeRefusalCopy.js +2 -1
  148. package/dist/retryStatus.d.ts +2 -2
  149. package/dist/retryStatus.js +2 -0
  150. package/dist/ruleReadings.js +1 -1
  151. package/dist/seam.d.ts +24 -2
  152. package/dist/seam.js +15 -2
  153. package/dist/seatContract.d.ts +125 -10
  154. package/dist/seatContract.js +373 -7
  155. package/dist/sessionBackground.js +4 -1
  156. package/dist/sessionBackgroundHandoff.js +2 -1
  157. package/dist/sessionEngine.d.ts +2 -0
  158. package/dist/sessionEngine.js +111 -0
  159. package/dist/sessionMemoryStatus.d.ts +2 -1
  160. package/dist/sessionSyncFileHistory.d.ts +4 -0
  161. package/dist/sessionSyncFileHistory.js +28 -0
  162. package/dist/sseIdleTriage.d.ts +13 -1
  163. package/dist/sseIdleTriage.js +24 -6
  164. package/dist/subagent/engineSubagentTail.js +6 -0
  165. package/dist/subagentContentStore.d.ts +12 -0
  166. package/dist/subagentContentStore.js +72 -11
  167. package/dist/terminalStatuses.d.ts +1 -0
  168. package/dist/terminalStatuses.js +4 -1
  169. package/dist/toolResult.js +18 -5
  170. package/dist/turnFailurePresentation.js +9 -2
  171. package/dist/typePins.d.ts +11 -0
  172. package/dist/usageAnalytics.d.ts +17 -0
  173. package/dist/usageAnalytics.js +44 -2
  174. package/dist/wireFailureShape.d.ts +22 -5
  175. package/dist/wireFailureShape.js +47 -26
  176. package/dist/wireRefusalCopy.d.ts +31 -1
  177. package/dist/wireRefusalCopy.js +167 -2
  178. package/docs/INTEGRATION-CLIENTS.md +3269 -21
  179. package/package.json +5 -5
package/README.md CHANGED
@@ -35,7 +35,7 @@ Renamed from **`@sema-agent/wire-cc-adapter`** (0.1.x, deprecated — see *Migra
35
35
 
36
36
  ## Scope
37
37
 
38
- **Version:** 0.108.0
38
+ **Version:** 0.110.0
39
39
 
40
40
  - **Today** — the adapter seam, the whole `adapt()` pipeline (all 14 A-layer arms plus the
41
41
  B/D/E tool-card layers), the notification/caps/model families, the adapter kernel (stream driver
@@ -71,13 +71,13 @@ Renamed from **`@sema-agent/wire-cc-adapter`** (0.1.x, deprecated — see *Migra
71
71
  against — the tables live upstream precisely so this package does not keep a second copy that can
72
72
  fall behind. The browser bundle really bundles the SDK through (the portability guard would
73
73
  exit 3 rather than quietly mark it external).
74
- - The declared floor is `>=23.1.0` (raised from `>=23.0.2` in 0.99.0: the package re-exports the sdk's request-attachment entry type `UserAttachment`, which the sdk declares from 23.1.0 on — a consumer on 23.0.x cannot resolve that name from the package's type declarations; runtime is unchanged; before that raised from `>=21.1.0` in 0.98.0 with the sdk 23 pick-up: the live respond receipt is read as the sdk's two-arm union (applied / replayed first decision), the session rule record follows the sdk's three-bucket type, the parked-resume startup cause is the sdk's closed five-word set and the crash-converged row is the sdk's own row type; before that raised from `>=20.0.0` in 0.97.0 with the sdk 21 pick-up, together with the server and engine floors: the two decision doors' gate-kind tables (`APPROVAL_DECIDE_GATE_KINDS` / `PLAN_REVIEW_GATE_KINDS`, 21.1.0 on) are passed through from the sdk and the package's gate-kind judge is derived from them, the run terminal's failure detail reads the sdk's three-arm union and the approval card word comes from the sdk's closed card-word list; before that raised from `>=19.0.0` in 0.96.0 with the sdk 20 pick-up, together with the server and engine floors: the stream terminal reads the sdk's five-arm `done.result` union — idempotent replay body and refusal-after-open included — and fleet rows carry `workspaceSnapshot`; before that raised from `>=18.1.0` in 0.95.0 with the sdk 19 pick-up: the ask-row ending-cause table, the run-death verdict codes and the terminal-frame rule now come from the sdk's own tables; before that raised from `>=12.0.1` in 0.94.0: the request-side permission-mode vocabulary, the decision-attribution key type, the cancellation-cause list, the status-derived coarse-code list and the routing-failure denial word are now taken from the SDK itself, all of which it declares from 17.x or 18.x on; before that raised from `>=11.3.0` in 0.84.0: the per-session background listing (`sessions.background`), its capability bit `capabilities.background.listFace` and the closed background-status vocabulary are typed from 12.x on, and the package reads all three, so it no longer compiles against 11.x; before that raised from `>=11.2.1` in 0.80.0: a deny decision may now name its settler — `settledBy: "policy"` is typed on both the durable decide body and the live respond body from 11.3.0 on, and the gate record's settlement vocabulary carries its thirteenth word `policy_refused`, which this package reads to place a refusal in the policy bucket rather than the person's; the package now compiles against 11.3.0 and no consumer ships 11.2.x any more, so the older floor lost its witness; before that raised from `>=11.0.1` in 0.79.0: `capabilities.mcpProbe`, the MCP probe face (`mcpCapabilities` / `probeMcp` with `McpProbeFace`) and the write receipt's third liveness arm (`stillLive: "unknown"`) are typed from 11.2.x on, 11.2.1 adds `TaskRequest.approverPosture` and the `mandated` approval-frame key to the types and the runtime key anchor, the package now compiles against 11.2.1, and no consumer ships 11.0.x / 11.1.x any more, so the older floor lost its witness; before that raised from `>=9.8.1` in 0.78.0: `DeniedBy` carries its tenth word `read_boundary`, `rules.write` answers a `stillLive`-discriminated body, `RemovalLiveness` / `RuleWriteRequest` / `RuleWriteResult` / `RuleWriteBehavior` are exported from the SDK root and `TaskRequest.excludeAllTools` is typed from 11.x on, the package now compiles against 11.0.1, and no consumer ships 9.8.x any more, so the older floor lost its witness; before that raised from `>=9.7.1` in 0.75.0: `Capabilities.deviceExecutor.management` is typed from 9.8.x on, the package now compiles against 9.8.1, and no consumer ships 9.7.x any more, so the older floor lost its witness; before that raised from `>=9.6.0` in 0.74.0: `Capabilities.approvalsStreamLive` / `.executionLane`, `LivePendingRow.frame`, the `live_*` approval-stream events and `gates[].toolCallId` are typed from 9.7.x on, and no consumer ships 9.6.0 any more, so the older floor lost its witness; before that raised from `>=9.4.0` in 0.71.0: the `tool_disclosure` / `tool_progress` frames and `ToolApprovalFrame.readRootCandidate` are typed there; earlier: raised from `>=8.8.0` in 0.69.0: the `reasoning_end` frame and `McpStatusPanel.lastLegMcp` are typed from 9.4.0 on), and it is *witnessed*: the guard checks that an actually
74
+ - The declared floor is `>=25.0.0` (raised from `>=23.1.0` in 0.110.0: the package's runtime entry statically imports the sdk's consumed-approval-frame predicate `isApprovalCompleteFrameV1`, which the sdk exports from 25.0.0 on, so a consumer on 23.x or 24.x failed at the import of the package root; the sdk released in lockstep with the lowest supported server (7.139.0) is already above 25.0.0, so the lower floor was a promise no supported deployment needed, and the compatibility reads kept alive only for the 23.x and 24.x lines — the error-class name guard, the compaction-started frame guard, the parked-resume cause lead registry and the same-named brain-status phase type — were retired with it; before that `>=23.1.0` since 0.99.0, raised from `>=23.0.2`: the package re-exports the sdk's request-attachment entry type `UserAttachment`, which the sdk declares from 23.1.0 on — a consumer on 23.0.x cannot resolve that name from the package's type declarations; runtime is unchanged; before that raised from `>=21.1.0` in 0.98.0 with the sdk 23 pick-up: the live respond receipt is read as the sdk's two-arm union (applied / replayed first decision), the session rule record follows the sdk's three-bucket type, the parked-resume startup cause is the sdk's closed five-word set and the crash-converged row is the sdk's own row type; before that raised from `>=20.0.0` in 0.97.0 with the sdk 21 pick-up, together with the server and engine floors: the two decision doors' gate-kind tables (`APPROVAL_DECIDE_GATE_KINDS` / `PLAN_REVIEW_GATE_KINDS`, 21.1.0 on) are passed through from the sdk and the package's gate-kind judge is derived from them, the run terminal's failure detail reads the sdk's three-arm union and the approval card word comes from the sdk's closed card-word list; before that raised from `>=19.0.0` in 0.96.0 with the sdk 20 pick-up, together with the server and engine floors: the stream terminal reads the sdk's five-arm `done.result` union — idempotent replay body and refusal-after-open included — and fleet rows carry `workspaceSnapshot`; before that raised from `>=18.1.0` in 0.95.0 with the sdk 19 pick-up: the ask-row ending-cause table, the run-death verdict codes and the terminal-frame rule now come from the sdk's own tables; before that raised from `>=12.0.1` in 0.94.0: the request-side permission-mode vocabulary, the decision-attribution key type, the cancellation-cause list, the status-derived coarse-code list and the routing-failure denial word are now taken from the SDK itself, all of which it declares from 17.x or 18.x on; before that raised from `>=11.3.0` in 0.84.0: the per-session background listing (`sessions.background`), its capability bit `capabilities.background.listFace` and the closed background-status vocabulary are typed from 12.x on, and the package reads all three, so it no longer compiles against 11.x; before that raised from `>=11.2.1` in 0.80.0: a deny decision may now name its settler — `settledBy: "policy"` is typed on both the durable decide body and the live respond body from 11.3.0 on, and the gate record's settlement vocabulary carries its thirteenth word `policy_refused`, which this package reads to place a refusal in the policy bucket rather than the person's; the package now compiles against 11.3.0 and no consumer ships 11.2.x any more, so the older floor lost its witness; before that raised from `>=11.0.1` in 0.79.0: `capabilities.mcpProbe`, the MCP probe face (`mcpCapabilities` / `probeMcp` with `McpProbeFace`) and the write receipt's third liveness arm (`stillLive: "unknown"`) are typed from 11.2.x on, 11.2.1 adds `TaskRequest.approverPosture` and the `mandated` approval-frame key to the types and the runtime key anchor, the package now compiles against 11.2.1, and no consumer ships 11.0.x / 11.1.x any more, so the older floor lost its witness; before that raised from `>=9.8.1` in 0.78.0: `DeniedBy` carries its tenth word `read_boundary`, `rules.write` answers a `stillLive`-discriminated body, `RemovalLiveness` / `RuleWriteRequest` / `RuleWriteResult` / `RuleWriteBehavior` are exported from the SDK root and `TaskRequest.excludeAllTools` is typed from 11.x on, the package now compiles against 11.0.1, and no consumer ships 9.8.x any more, so the older floor lost its witness; before that raised from `>=9.7.1` in 0.75.0: `Capabilities.deviceExecutor.management` is typed from 9.8.x on, the package now compiles against 9.8.1, and no consumer ships 9.7.x any more, so the older floor lost its witness; before that raised from `>=9.6.0` in 0.74.0: `Capabilities.approvalsStreamLive` / `.executionLane`, `LivePendingRow.frame`, the `live_*` approval-stream events and `gates[].toolCallId` are typed from 9.7.x on, and no consumer ships 9.6.0 any more, so the older floor lost its witness; before that raised from `>=9.4.0` in 0.71.0: the `tool_disclosure` / `tool_progress` frames and `ToolApprovalFrame.readRootCandidate` are typed there; earlier: raised from `>=8.8.0` in 0.69.0: the `reasoning_end` frame and `McpStatusPanel.lastLegMcp` are typed from 9.4.0 on), and it is *witnessed*: the guard checks that an actually
75
75
  installed SDK at that line still exports every value-level symbol this package imports and still
76
76
  declares `TaskStats.costMicroUsd` (the key `costOrNull` reads). A floor nobody ever ran is a
77
77
  promise, not a contract.
78
78
  - `@sema-agent/mods-kernel` — the mods kernel (the extension bus shared by the engine and a client shell: registration, admission, chain, failure and budget law). **Optional**: it is declared in `peerDependenciesMeta` as optional and is reached only through the `./mods` subpath entry point (`import { … } from '@sema-agent/client-core/mods'`), which re-exports the kernel's 67 root names (26 values and 41 types) unchanged and adds one reader of its own, `modsKernelIdentity()`, returning the kernel's contract number and a digest of its event table. The root entry never references the kernel, so a host that does not import `./mods` neither needs it installed nor pays for it; a host that imports `./mods` without the kernel installed fails at load time, loudly, and is never handed a degraded stand-in. The declared floor is `>=7.54.0`, which is range acceptance, not verification: the generations actually verified are the columns of the guard that pins this entry (today 7.54.0, which is also the exact development dependency). The package does **not** promise that a host's whole install tree holds a single copy of the kernel — package managers do not guarantee that for optional peers — so a host that enables mods should assert, in its assembly root, that the `./mods` values are the same references as the kernel module its own consumers resolve; `modsKernelIdentity(kernelModule)` gives the readings to compare when two copies are loaded as separate instances (equal readings show equal content, not the same instance).
79
79
  - **Support floor:** the oldest server, engine and SDK this package still supports — server **7.139.0**,
80
- engine **7.60.0**, SDK **23.1.0** (server and engine raised in 0.106.0 from 7.129.1 / 7.51.2, withdrawing support for servers 7.129.1 – 7.138.x: every request key this package carries is accepted by every server in the supported range, so none is sent conditionally; SDK raised in 0.99.0 from 23.0.2, with the peer floor; server and engine raised in 0.98.0 from 7.126.0 / 7.49.5, withdrawing support for servers 7.126.0 – 7.129.0; SDK raised in 0.98.0 from 21.1.0, with the peer floor; all three raised together in 0.97.0, from 7.125.0 / 7.48.0 / 20.0.0; raised together in 0.96.0 from 7.115.0 / 7.40.1 / 19.0.0; server / engine raised from 7.106.0 / 7.35.2 in 0.95.0 and from 7.104.0 / 7.33.8 in 0.92.0; SDK raised from 18.1.0 in 0.95.0 and from 12.0.1 in 0.94.0). The values are
80
+ engine **7.60.0**, SDK **25.0.0** (SDK raised in 0.110.0 from 23.1.0, with the peer floor, withdrawing support for SDK 23.x and 24.x; server and engine raised in 0.106.0 from 7.129.1 / 7.51.2, withdrawing support for servers 7.129.1 – 7.138.x: every request key this package carries is accepted by every server in the supported range, so none is sent conditionally; SDK raised in 0.99.0 from 23.0.2, with the peer floor; server and engine raised in 0.98.0 from 7.126.0 / 7.49.5, withdrawing support for servers 7.126.0 – 7.129.0; SDK raised in 0.98.0 from 21.1.0, with the peer floor; all three raised together in 0.97.0, from 7.125.0 / 7.48.0 / 20.0.0; raised together in 0.96.0 from 7.115.0 / 7.40.1 / 19.0.0; server / engine raised from 7.106.0 / 7.35.2 in 0.95.0 and from 7.104.0 / 7.33.8 in 0.92.0; SDK raised from 18.1.0 in 0.95.0 and from 12.0.1 in 0.94.0). The values are
81
81
  declared in `scripts/support-floor.json` and exported at runtime as `SUPPORT_FLOOR`; servers below the floor
82
82
  are no longer witnessed, and the readings that existed only for them were retired in the same release.
83
83
  - The direction is constitutional: **sdk (wire) + agent-types (vocabulary) → this package**,
@@ -301,18 +301,18 @@ guard still cross-checks the table by name).
301
301
  | `scripts/run-client-core-pure-test.mjs` | Consumer-view behaviour of every moved-in module; segment floors (B1–B8 + C) that only move up The non-throwing attachments verdict names a retired key, and gives its reason and way out, from the retirement rule itself; a negative control plants a second retired key in a copy of the build and checks that it is reported by its own name. The configuration refusal registry includes the inline-image refusal code, whose one sentence names no codes or identifiers, recognises only that code, and is checked against both the installed engine (minted in exactly one place) and the receiving server's older engine (not minted at all). The background-agent task-output reader hands the state and error lines that directly follow the status line to optional slots, positionally: lines written inside the agent's own result text never become slots; checked on twelve forms minted by the installed engine's own functions and by the receiving server's older engine. It also hands the result label's parenthetical note and the header bytes not mapped to a named slot to optional slots, with a rebuild invariant (status line, state, error, the unmapped header bytes, the result label and the output rebuild the unwrapped text, up to four named normalizations) checked on those same forms and on a seeded random pool of forged headers. A Bash command that is moved to the background mid-flight (by the user, or by the engine on a timeout or a queued message) is registered for the background panel by the same event and the same record as a model-requested background command: the reader recognises only the structured detached result (never the receipt prose, never the request's background flag), at most one registration is minted per card (a card both readers recognise yields one, taken from the structured result), the package keeps no record of what it has already published (the same task id on a second card is registered again, because only the consumer knows whether it still has the row), the receipt reader and the detached reader both stand down when the structured result says the engine failed to register, and the stream is compared byte for byte against the previous published build on inputs without a detached result (including two receipts for one task id), differing only by that one event on inputs that have one. The attachments env lane's product default now also carries the todo reminder (one default table shared by the absent, empty, unknown-word and `default` paths); `off` removes the key and an explicit list that leaves it out does not get it. The network word table matches terminated and other side closed as whole words: a local Unterminated string in JSON parse error is classified as internal instead of a connection failure, while the sentences undici itself produces, the errno codes and the UND_ERR family still classify as transport. The error-triage readers (`classifyTurnWireError`, `isWireTransportError` and the three sibling readers of the same fields) never throw on an error object whose `status`, `errorCode`, `message`, `cause` or `name` is a throwing getter, a revoked proxy or a throwing string conversion; an unreadable `status` is its own state (neither an HTTP answer nor a transport failure), and ordinary shapes answer exactly as a frozen copy of the previous implementation does. |
302
302
  | `scripts/run-client-core-portability-test.mjs` | Kernel / A-layer / index import closures, the runtime-dependency equality gate, barrel reachability, and a real esbuild `--platform=browser` bundle Since 0.98.0 the gate-script family sweep has four forms: a pattern that can enclose a whole block comment (judged by running it on constructed probes, not by how it is spelled), a pattern that covers the line-comment marker, a hand-written quote-pairing lexer (registration only, currently empty) and string-method calls whose first argument is a comment marker (the shared lexer library is the only home), each proved on planted spellings and clean neighbours. |
303
303
  | `scripts/run-client-core-diff-test.mjs` | Differential equivalence against the CLI reference bridge + replay-id invariant + ledger round-trip |
304
- | `scripts/run-seat-contract-keys-test.mjs` | The seat IPC contract: verb list ↔ SPEC ↔ types, element-wise. Since 0.83.4 the seat approval request also carries the four card bits that say whether an approval must be asked and where it came from (`mandated`, `mandate`, `ruleOffersAbsence` — the parked-row form of mandated, so the seat and the card give the same `approvalIsMandated` answer — and `origin`, which the resident-posture predicate reads), and they reach the seat through `toolPermissionRequestAskBits`, which reads them exactly as the card request does — a strict own `true`, one of the closed mandate words byte for byte, a non-empty own string — so for a host that builds the seat request through that crossing an off-list word, an empty string or an inherited key never lands on the seat; the seat validator does not become a second judge that could drop a safety ask over one bit, which leaves a host that builds the request by hand to apply the same narrow read itself. Since 0.96.0 the seat chrome list also carries the two segment-end events (`text_segment_end` and `thinking_segment_end`), so the authoritative replacement of a segment (redaction or rewrite) can reach a seat lane (since 0.98.0 also the six presentation arms the renderer already consumes: `retry_status`, `context_usage`, `last_turn_usage`, `bgshell_register`, `bgshell_settle` and `memory_saved`; `workflow_notification_enqueue` stays with the host process that owns the session's idle auto-submit and its notification queue, so the completion notice has exactly one deliverer — a real adapter-minted one is pinned as rejected by the seat gate; every `required` chrome arm must have a written seat disposition or the gate goes red, and the set of required arms may not shrink: the frozen floor is declared on the chrome arm table's own type, so turning an existing required arm optional fails to compile, and the gate checks the runtime list against that same declaration), and its type is bound to the discriminants of `ChromeEvent` itself (a word that is not a chrome arm no longer compiles); the approval request carries the seat's allow-for-this-session bit; since 0.97.0 that bit is `sessionRow` (replacing `rememberRefused`): present only when the card judge's session-row premise holds and the request id's domain can carry a session answer, absent otherwise, which hides that row — never invented as `false`; the crossing never copies the bit from its input and never mints it for a seat request itself (the seat reads the request's own bit) Since 0.98.0 it also checks that every outbound gate accepts only plain records — a Date, Map, Set, array, class instance or object with a custom prototype carrying otherwise-valid fields is refused (structured cloning would drop those fields in transit), while a plain object from another JavaScript realm is accepted — and that four unused event fields are gone from the contract. 0.98.0: the outbound seat validator lists the turn-failure superset key without ever refusing an error event because of it. Since 0.99.0 the event no longer has the two positions no host ever sent (`request`, `sources`) while `code` stays as the failure event's status-code field; the event list gains `subagent_content`, whose payload is the subagent content event in exactly the package's own projection, checked end to end from a wire frame to the renderer's own content store; and the six presentation arms (retry status, context usage, last-turn usage, background shell register and settle, memory saved) carry closed payloads, with the engine's turn usage reduced to its six declared keys plus a count of the keys this package does not know. Since 0.101.0 the seat chrome list also carries the two compaction events (`compaction_started` and `compaction_outcome`) and the engine notice (`engine_notice`), so a seat lane can show that a compaction is running, learn how one that did not land ended, and show the engine's session notices; every chrome arm now has one written seat disposition (crosses to the seat, or stays with the host process), checked at compile time and by the gate, and the seat list is derived from that table. The resolution words also include `engine_unavailable` (the host retired its runtime and withdrew the card while the parked row is still on the engine, so it is not an expiry); the outbound gate stays open to unknown words and the `parked` bit stays reserved for `expired`. Since 0.108.0 the seat's resource verbs take an optional session scope: `listModels(scope?)` and `uploadAttachment(req, scope?)` answer for the engine the named session lives on, or — with no scope — for the default lane that new sessions start on, never for the focused session; `checkSeatResourceScope` accepts only `{ sessionId }` and names the offending key otherwise, and `checkStartSessionOptions` accepts the new-session lane intent `engine` only as `local` or `cloud`. Every seat validator treats an object whose prototype is a hand-made prototype-less object as not a plain record, so a field inherited from such a prototype can never slip past the closed-shape check. |
304
+ | `scripts/run-seat-contract-keys-test.mjs` | The seat IPC contract: verb list ↔ SPEC ↔ types, element-wise. Since 0.83.4 the seat approval request also carries the four card bits that say whether an approval must be asked and where it came from (`mandated`, `mandate`, `ruleOffersAbsence` — the parked-row form of mandated, so the seat and the card give the same `approvalIsMandated` answer — and `origin`, which the resident-posture predicate reads), and they reach the seat through `toolPermissionRequestAskBits`, which reads them exactly as the card request does — a strict own `true`, one of the closed mandate words byte for byte, a non-empty own string — so for a host that builds the seat request through that crossing an off-list word, an empty string or an inherited key never lands on the seat; the seat validator does not become a second judge that could drop a safety ask over one bit, which leaves a host that builds the request by hand to apply the same narrow read itself. Since 0.96.0 the seat chrome list also carries the two segment-end events (`text_segment_end` and `thinking_segment_end`), so the authoritative replacement of a segment (redaction or rewrite) can reach a seat lane (since 0.98.0 also the six presentation arms the renderer already consumes: `retry_status`, `context_usage`, `last_turn_usage`, `bgshell_register`, `bgshell_settle` and `memory_saved`; `workflow_notification_enqueue` stays with the host process that owns the session's idle auto-submit and its notification queue, so the completion notice has exactly one deliverer — a real adapter-minted one is pinned as rejected by the seat gate; every `required` chrome arm must have a written seat disposition or the gate goes red, and the set of required arms may not shrink: the frozen floor is declared on the chrome arm table's own type, so turning an existing required arm optional fails to compile, and the gate checks the runtime list against that same declaration), and its type is bound to the discriminants of `ChromeEvent` itself (a word that is not a chrome arm no longer compiles); the approval request carries the seat's allow-for-this-session bit; since 0.97.0 that bit is `sessionRow` (replacing `rememberRefused`): present only when the card judge's session-row premise holds and the request id's domain can carry a session answer, absent otherwise, which hides that row — never invented as `false`; the crossing never copies the bit from its input and never mints it for a seat request itself (the seat reads the request's own bit) Since 0.98.0 it also checks that every outbound gate accepts only plain records — a Date, Map, Set, array, class instance or object with a custom prototype carrying otherwise-valid fields is refused (structured cloning would drop those fields in transit), while a plain object from another JavaScript realm is accepted — and that four unused event fields are gone from the contract. 0.98.0: the outbound seat validator lists the turn-failure superset key without ever refusing an error event because of it. Since 0.99.0 the event no longer has the two positions no host ever sent (`request`, `sources`) while `code` stays as the failure event's status-code field; the event list gains `subagent_content`, whose payload is the subagent content event in exactly the package's own projection, checked end to end from a wire frame to the renderer's own content store; and the six presentation arms (retry status, context usage, last-turn usage, background shell register and settle, memory saved) carry closed payloads, with the engine's turn usage reduced to its six declared keys plus a count of the keys this package does not know. Since 0.101.0 the seat chrome list also carries the two compaction events (`compaction_started` and `compaction_outcome`) and the engine notice (`engine_notice`), so a seat lane can show that a compaction is running, learn how one that did not land ended, and show the engine's session notices; every chrome arm now has one written seat disposition (crosses to the seat, or stays with the host process), checked at compile time and by the gate, and the seat list is derived from that table. The resolution words also include `engine_unavailable` (the host retired its runtime and withdrew the card while the parked row is still on the engine, so it is not an expiry); the outbound gate stays open to unknown words and the `parked` bit stays reserved for `expired`. Since 0.108.0 the seat's resource verbs take an optional session scope: `listModels(scope?)` and `uploadAttachment(req, scope?)` answer for the engine the named session lives on, or — with no scope — for the default lane that new sessions start on, never for the focused session; `checkSeatResourceScope` accepts only `{ sessionId }` and names the offending key otherwise, and `checkStartSessionOptions` accepts the new-session lane intent `engine` only as `local` or `cloud`. Every seat validator treats an object whose prototype is a hand-made prototype-less object as not a plain record, so a field inherited from such a prototype can never slip past the closed-shape check. Since 0.110.0 the seat approval request also carries the two reason-for-asking bits of the card (`probeReason`, the probe's own sentence, and `probeCause`, the engine's structured cause), minted by `toolPermissionRequestAskBits` through the same single reading point as `askProbeReasonDetail`: the prose bit only when it is an own data string that is not blank (original bytes), the cause bit only when it is an own data record (the same reference, members unchecked), never from an accessor or an inherited key, each bit independent of the other, and from a raw parked row they are read under its `riskDescriptor`; the seat validator does not become a second judge of either, and a seat card reads its "why asked" line by handing the request to `askProbeReasonDetail`. Since 0.110.0 the seat approval request carries two more display bits through the same crossing: `requestedBy` (the extension that made the call, read through the one carrier-level reader the card uses and minted as a frozen fresh copy of `{ kind: 'extension', name }`) and `frameReading` (why the card is the generic card: one of the three closed words of the frame reading, present only when the frame could not be read and the cause is known, never an `ok` word); each lands only when its own reading is present, so an absent, malformed, accessor, inherited, off-list or throwing slot leaves the key out and the other bit untouched, neither is read from a raw parked row, and the seat validator is not a second judge of either. Since 0.110.0 the seat approval request also carries `inputSource` through the same crossing: it copies the bit the card already carries (an own data slot holding exactly `record` or `stream`; the card says whether its arguments came from the record side or from the model's original call in the event stream), never re-judges the source from the input, reads nothing from a raw frame or a raw parked row, and the seat validator is not a second judge of it. |
305
305
  | `scripts/run-approval-frame-keys-test.mjs` | The tool-approval frame key mirror, element-wise against the SDK's runtime anchor (one carve-out: AHEAD_OF_ANCHOR entries — keys the server already emits but the SDK anchor has not caught up to — may lead by one generation; the gate turns red the day the SDK catches up, forcing the entry's removal — the register is occupied again — this time by the bit that says a saved allow rule cannot retire a given approval card, carrying both the release that minted it and the byte coordinates that prove it, so the lead is a dated record rather than an exemption; its predecessor left the register the other way, by being retired upstream rather than by the anchor catching up). Beside the key mirror it now guards three further faces of that bit: the closed word table the durable leg reads it through must be **the very array object the SDK exports**, not a same-looking copy — reference identity, because an equal-contents check still permits a second table that diverges the day upstream adds a member; the one predicate a client is meant to call answers over both legs — the live card's bit and the parked row's absence word, which is all the row carries, since the row has no such bit at all — and answers `false` for a malformed value exactly as its presence-only siblings do, a strictness the upstream mint shares; and the one sentence minted for it must never point the reader at writing a rule, since a rule written in answer to a mandated question can never take effect where it was written. The same bit's key also has to reach the card request itself, which the SDK's card anchor does not list — a fact that arrives at the package boundary and stops there is the shape of defect this file's guards exist to catch. Since 0.83.2 the frame also mirrors `mandate` (the word a mandated question stands on) ahead of the SDK anchor, with its own exit condition; the released server carries it from 7.102.0, so the allowance for the server fixture was removed by its own exit condition. The installed engine must declare the member with the six-word closed type, and the key must reach the card request. Since 0.91.0 the frame also mirrors `probeReason` ahead of the development SDK anchor, with the same exit condition, and the probe-tree comparison reads the newest configured SDK first. |
306
306
  | `scripts/run-segment-authority-single-source-test.mjs` | The authoritative-segment replacement verdict, single-sourced. `text_end.content` and the `text_delta` stream stopped being byte-identical the day the engine started redacting the former through the same filter as the result, so every consumer now has to decide six ways what to do with the segment it has half-emitted — and until this release that decision existed **twice**: once here for the transcript lane, once in the shell for the print lane, hot-fixed a version apart. The verdict is now one pure function both lanes call, and the guard pins it on the quantity that actually decides the outcome: whether the authoritative text still *starts with* the bytes that already left, not whether a flush has happened — the latter is a precondition, and anchoring on it withholds a perfectly ordinary answer. Each of the six forms is checked with its counter-case, the prefix length is pinned to UTF-16 code units against a non-ASCII sample whose UTF-8 byte count differs (slicing by bytes leaves the very thing being redacted on screen), and the withheld-segment ledger is compared by normalised equality rather than substring, because a short redaction marker quoted in an unrelated later answer would otherwise suppress that answer entirely. The same file pins the session-level memory-capture declaration to one mint point — the wire value is a single-member closed set, and a consumer that spells it wrong gets a loud refusal rather than a silently dropped privacy request — and pins the SDK URL/health transit to be the **same function reference**, since wrapping it would discard the one guarantee the transit exists for. A last section strips comments with the TypeScript parser and asserts the second expression has not grown back Since 0.98.0 the gate also pins the single reading of engine liveness: no string expression in the package source evaluates, after static folding, to a `/health` path, so a second hand-written probe cannot come back however it is spelled, and the package's one liveness helper answers six real HTTP fixtures exactly as the SDK probe does (a self-reported degraded body, a non-JSON body and a 503 are not alive; 200, 201 and 299 with an ok body are). |
307
307
  | `scripts/run-print-bash-iserror-test.mjs` | The print lane's Bash `is_error` authority (structured over regex). A second section pins where the denial classification word lands on this lane: on the message envelope, never inside the tool-result block, because that block is forwarded verbatim to the provider on compaction and a self-minted key there is the shape of an old, real defect. A word outside the upstream table — or an empty string, a non-string, or nothing at all — mints no key rather than a guess, and the word never moves the error flag, because attribution does not decide anything |
308
308
  | `scripts/run-bash-benign-exit-interpretation-test.mjs` | Benign non-zero Bash exits (`returnCodeInterpretation`) stay non-errors across all three derivation arms, and the annotation transits to the card |
309
- | `scripts/run-sdk-floor-test.mjs` | The SDK version floor — and, more to the point, that the *installed* type declarations still carry the keys this package reads — including, from 0.80.0, the three declarations that justify the floor itself: the key naming who settled a refusal on both decision legs, and the thirteenth word in the settlement vocabulary. They are found through the syntax tree rather than by searching text, because this guard's own comment stripper blanks string contents and would have made that check permanently, silently green. From 0.84.0 the floor is 12.0.1 and the guard witnesses the declarations the package now reads: the per-session background listing method, the `background.listFace` capability bit, and the closed background-status vocabulary that the registry classification switches over exhaustively. Every installed SDK the guard can see at or above the floor must carry those declarations too, so lowering the floor to a line where they do not exist fails. From 0.94.0 the floor is 18.1.0: the shipped declarations name the approval-attribution type and the six-word request permission-mode type that SDK 17 introduced, and the cancellation-cause, status-derived-code and routing-failure values are generated from the same SDK at build time; the guard reads those values on the floor-line SDK it can see. Since 0.99.0 the floor is SDK 23.1.0, and the floor-line witness must export the user-attachment type that the package root re-exports unchanged, with the attachment field declared on both the task request and the capabilities. |
309
+ | `scripts/run-sdk-floor-test.mjs` | The SDK version floor — and, more to the point, that the *installed* type declarations still carry the keys this package reads — including, from 0.80.0, the three declarations that justify the floor itself: the key naming who settled a refusal on both decision legs, and the thirteenth word in the settlement vocabulary. They are found through the syntax tree rather than by searching text, because this guard's own comment stripper blanks string contents and would have made that check permanently, silently green. From 0.84.0 the floor is 12.0.1 and the guard witnesses the declarations the package now reads: the per-session background listing method, the `background.listFace` capability bit, and the closed background-status vocabulary that the registry classification switches over exhaustively. Every installed SDK the guard can see at or above the floor must carry those declarations too, so lowering the floor to a line where they do not exist fails. From 0.94.0 the floor is 18.1.0: the shipped declarations name the approval-attribution type and the six-word request permission-mode type that SDK 17 introduced, and the cancellation-cause, status-derived-code and routing-failure values are generated from the same SDK at build time; the guard reads those values on the floor-line SDK it can see. Since 0.99.0 the floor is SDK 23.1.0, and the floor-line witness must export the user-attachment type that the package root re-exports unchanged, with the attachment field declared on both the task request and the capabilities. Since 0.110.0 the floor is SDK 25.0.0, and every named value that the built output imports or re-exports from the SDK (root and subpath entries, read from the syntax tree of the shipped JavaScript) must exist on the floor-line witness's runtime entry, so a floor that the package's own import lines cannot load fails. |
310
310
  | `scripts/run-engine-caps-ledger-test.mjs` | A per-key disposition ledger for `GET /v1/capabilities`. The SDK's `Capabilities` grew from 74 keys to 93 in one release and nothing on the board could see it: this package consumes that table through four synchronous readers, and *nineteen new positions arriving while the package does not move* is exactly the disease shape this repo keeps logging on other axes — the fact is already on the wire, the package boundary is the cell that swallows it, and no client can read it however they write their side. So the ledger is reconciled **element-wise against the SDK interface in both directions**: a key the SDK added with no ledger row is red (someone must classify it), and a row for a key the SDK removed is red too (a registration that no longer does anything). Each row then has to survive its own claim — a `read` row names the source file, and the **code** there (comments stripped) must really mention the key, because prose asserting an alignment is the classic way these guards go hollow; a `not_read` row must have **zero** read sites in the tree, so wiring one up while the ledger still says the package ignores it is red rather than invisible. The census behind those two directions recognises five call shapes, each of which really occurs here — a reader whose base argument carries its own parentheses, a direct `caps.<key>`, a narrowing cast, an own-property read helper, and a `*_CAP` constant — and proves it on fabricated samples first, since a census that recognises one shape reports "nothing here" for the other four. What the guard deliberately does **not** judge is whether a position *ought* to be read: that is a design call, and the ledger only pins that every capability was looked at once by a person and that what they wrote down does not contradict the code In 0.98.0 the census reads the syntax tree — call heads, the second argument, constants, property reads, narrow casts and own-property reads are nodes, not text matched by a pattern with bracket balancing and quote pairing — and proves six more shapes on fabricated samples (a closing bracket inside a string, a pattern literal holding brackets and commas, a template with a closing bracket, a string with a comma and both quote kinds, an optional call, call-looking text inside a string) plus a control that removing the whole class empties the census. |
311
311
  | `scripts/run-sql-engine-capability-test.mjs` | The SQL-posture read face and the four-state capability reader underneath it. One capability cell here carries **four different things**, and each one points an operator somewhere else: nothing has been observed yet in this process (a one-shot doctor run is always in that state), the response arrived but cannot say (no store posture, or one that cannot be read), the engine reports a store posture without an SQL seat — *this deployment has no SQL backend*, which is a **positive fact** rather than an absence — and a full reading. Fold any two together and the screen states something flatly, confidently, and wrongly, so every positive control here is paired with a control pointing the opposite way, and the four sentences the doctor row can print are checked to be pairwise distinct and non-implying. The reading itself is narrowed no tighter than the mint: `txnMode: null` is a **legal value** — two of the three engines always report it that way, and the upstream type note names reading it as "optimistic" as the error — so treating it as malformed would throw away the entire reading for ordinary deployments, which is the same disease this repo logged when a consumer's domain was narrower than the producer's. A response that cannot be parsed **clears** the cell rather than leaving the previous engine's answer in place, and a separate invalidation port exists for the case the generation latch cannot catch — a same-port respawn whose new probe never succeeded, where the stale reading would otherwise be answered as current fact. Untrusted values (the isolation string is read back from a database server variable) are sanitised and bounded before display, and the bound is applied **before** escaping so a visible escape never gets cut in half. Finally the export names are themselves a guard: the shell still carries a copy that is meant to go red on the package's same-named export and be swapped out, so renaming anything here would silently disarm that lock. Current engines (≥7.106.0) moved the SQL posture under the always-present store posture (`store.sql`) and dropped the top-level key, and from 0.92.0 the reader reads `store.sql` alone (absent there means the engine reports no SQL posture): the top-level position of servers below the support floor is no longer read, a response without a readable store posture is "not reported" — the same answer the other readers sharing that witness give — and an unreadable SQL seat clears the cell; the engine's own projection is replayed to prove every form it mints reads back |
312
312
  | `scripts/run-web-search-backend-capability-test.mjs` | The deployment-default WebSearch backend read face (`capabilities.webSearch.backend`, engine ≥7.82.1). Same four-state discipline as the SQL and write-protection cells, with two things that are specific here and therefore guarded: a **missing key** (an older engine) and an explicit **`"none"`** (the engine says this deployment has no default search backend) point an operator in opposite directions — "cannot tell" versus "not configured" — and must never be folded; and the `none` sentence has to say both halves of the contract at once: the default scenario mounts no WebSearch tool, **and** a caller-supplied `webSearch` setting can still mount it on a single-user lane, because the capability advertises the deployment default, not whether this request has search. The backend word is read as an **open set** — the engine's closed set is typed from its own provider tuple and grows with it, so hand-copying three words here would turn a newly configured backend into "unreadable" (the narrower-than-the-mint disease this repo already logged once). `webSearch: null` is malformed rather than `none` (the mint never emits `null`), extra members never cross, an unparseable response clears the cell, a stale probe's late landing is dropped by the capabilities cache, the invalidation port clears to "not observed", and the open-set word is sanitised and bounded before display |
313
313
  | `scripts/run-terminal-cause-projection-test.mjs` | The `7.64.0` wire reshape, projected. A run's ending stopped being eight parallel flat keys and became **one tagged cause** (`completed \| failed \| blocked \| paused`), and a tool call's gate stopped being four orthogonal words and became **one record** (`disposition` / `settlement?` / `origin?`). Both are read in exactly one place in this package, and this guard pins them at **two levels**, because the dangerous seam is "the reader was updated, the consumer was not": each terminal arm is checked on the reader *and* on the `subtype` / `is_error` / `errors[]` the projector actually emits. Two properties carry most of the weight. First, a terminal word this reader does not know is **never** laundered into an empty success — it lands on an `unknown` arm carrying the word verbatim, while a payload with no terminal word at all (the mock lane) keeps the success arm exactly as before, which is the one and only case the reader answers `null`. Second, the three window words (`approval_window_expired`, `denial_limit_window_expired`, `park_sla_expired`) must each be told apart by a different predicate: the previous generation collapsed all three onto one `timeout`, and re-merging them would throw away the discrimination this reshape just restored. Two byte generations are read by one reader, keyed on the discriminator upstream nailed (`"terminal" in result`): the current cause form, and the **flat** form that a current engine still emits on two lanes — replayed persisted bytes, which the service passes through verbatim rather than back-filling, and the service's own rejection envelope. A cause-form payload that also carries stale flat keys must ignore them entirely: keeping one compatibility read is what gives a single fact two sources. The same file also pins the MCP delivery verdict and HTTP status riding the wiring manifest, the four-state write-protection reading (where three of the four states mean *cannot tell*, and none of them may be printed as "there is no table"), and the park-reopen fetch identity: that predicate is asserted through the **real entry point**, since the defect being fixed was precisely a call site wired to a different predicate than the one that routed the row there. From 0.80.0 one of those three boundaries flips: the key naming **who settled a refusal** stopped being a dead byte and became part of the wire, so the check stopped scanning the build output for the word and started reading the request bodies the two decision legs actually send. A refusal attributed to the deployment's own policy carries the word; one attributed to a person, one with no attribution at all, and one carrying a word the vocabulary does not hold carry nothing — the wire has no slot for “a person decided this” other than the key's absence, so inventing one would be minting a word upstream does not have. The allow family never carries it on any of its routes, because that combination is refused before the approval is judged while the side effects of allowing have already landed, and the three refusals nobody was asked about (a card that failed, a user who walked away, an interruption) carry nothing either. A deployment that signs the bodies it accepts does not sign that word, and there is no capability bit to ask beforehand, so a refusal on exactly that ground is answered by re-sending the same decision once with that one key removed — byte-for-byte the same otherwise — rather than letting an optional note take the whole denial down with it. The guard measures that along three axes: the decision still lands and is reported as decided with the attribution handed back and a separate flag saying it never reached the wire; a caller who aborted in between gets no second request; every other refusal code, and every decision that never carried the key, send exactly once. The classification of a second failure is made from what the second body actually carried, not from what the card asked for. From engine `7.104` a synchronous submit that stops at a gate returns the engine result itself plus a three-key receipt: it now carries the tagged cause, so it reads as `paused` on the cause generation (older engines still send the flat three-key body, which keeps reading as before), and the top-level park word is looked at first, the same order the SDK documents for all three generations, so a body the server says is parked is never read as finished. A parked run row now carries its result too, and the headless reconnect path turns it into the parked terminal frame on the first attempt instead of spending its whole retry budget — both generations are pinned, including an end-to-end drive through the public reconnect entry point. When the caller passes the failure's error carrier and it is a session-history tool_use mismatch, the edit-and-resend reading yields to the recovery sentence that line already carries; the carrier is handed to the same mismatch classifier, so the reading and the classifier never disagree on a carrier, whatever its shape; without a carrier the reading is compared byte for byte with the previous release's package. The machine-readable cause of a `blocked` ending is pinned as well: when a deployment hook keeps a prompt from being submitted, the blocked reading carries an optional detail with three arms — a known kind with a known cause word, a known kind with a cause word this version does not know, and a kind this version does not know — and a word outside the tables comes back verbatim and is never folded into a known one. An absent or malformed detail is left out entirely rather than guessed at, only own properties are read (planting the same names on the global prototype changes nothing), a flat-plane row and the other endings have no such position, and a reading without a detail is compared byte for byte with the previous release's package. Real engine and server packages from both generations are driven end to end — the engine's four hook endings, the server's redaction of the hook's own words, and the reader — and a per-generation fact pin says which engine and server versions mint the detail. The "parked at an approval gate" predicate recognises both the live frame form and the archived session-row form. |
314
314
  | `scripts/run-auto-mode-unavailable-test.mjs` | The fact behind "you are being asked because the auto-mode classifier could not run", and the one place its sentence is minted. The cause table is a **copy**, reconciled word for word in both directions against the installed engine's own bytes — it narrowed upstream, and the guard follows rather than keeping the old shape: a table checked against something nobody ships any more is the oldest way for a guard to be green and wrong. The retirement is held from both sides — the removed table must really be gone upstream, and the removed reader and word must really be gone here — while the word that left keeps arriving cleanly from an older engine, because the reader takes the cause as an **open set**: the vocabulary belongs upstream, so a copied list here would discard a legal value the day one is added, and the value discarded is precisely "this outage is a NEW kind". The reader's one exclusion is the word the engine says it never stamps here — the classifier did run and did answer, just outside its contract, so reading it as a failure would invent an event the engine denies. That exclusion used to be derived from a second table which no longer exists; the reason for it never lived in that table, so it is now stated where it actually comes from, pinned as a **named** set (a magic literal scattered through the reader reds) and cross-checked against the engine's own verdict declaration and against the reader having exactly one such comparison. One reader serves both the live ask and its durable parked twin, since the two carry the same key path and a second copy is how two ledgers drift apart. Absence is pinned as absence — most asks never consulted a classifier at all — and the sentences are checked mutually distinct, prototype-safe, and walked end to end: an unknown word reaches the sentence a person reads (the fallback that names it verbatim) and the status reading (unavailable for this round, never a fallback to "available"), with counter-controls proving neither assertion is vacuous |
315
- | `scripts/run-engine-notice-catalog-test.mjs` | The engine-notice catalog and its audience table. Whether a notice deserves a person's attention is not decided by whether this end happens to have a phrasing for it — that drifts with each client's build order — but by whether the engine minted the code into its own written catalog; the audience row answers the separate question of *who* the fact is for, since an operations fact pushed at an end user is noise and a user-facing fact buried in an operator log is something withheld from the person who could act on it. Both tables are reconciled against the installed engine's own artefacts in both directions and pinned in lockstep with each other, unknown codes fall back to the conservative operator side, and catalog membership is tested on the raw value so a code carrying control characters cannot impersonate a registered one after sanitizing. The reader for a dropped MCP injection keys on its own code alone and treats a missing session, server or reason as absence rather than throwing at a read site. A reverse pin enforces the upstream's single-mint contract: the engine composes those sentences from the host's facts, so a copy of them appearing in this package's source or build is a second source that would drift, and fails. From 0.84.0 it also covers the reader for the two read-directory grant notices: it recognises only those two codes, needs the tool call id to match a card, passes the rejection reason through as written, and treats only the granted notice as evidence that a directory was added; a granted notice without both the directory and the spelling the engine now holds, or with a scope other than `exact`, is not read at all, and the scope word is pinned to the engine's type at compile time. The server also mints a few notices of its own through the same channel; those codes live in a second table with their own audiences, kept apart from the engine mirror (which must stay equal to the engine's catalog) and reconciled against the server's published package when one is supplied, so a user-facing server notice is no longer filed under operations. One dispatcher returns the typed facts for every code that has a reader, discriminated by code and tagged with its audience — only the user-audience codes belong on a user surface — and the guard ties the dispatch table to the module's own exported readers in both directions, so a reader cannot be exported without a row and a row cannot be dropped without the guard failing. From 0.85.0 it also covers a third server-minted notice, the one saying that part of a session's saved history could not be read when the session was reopened: it is a user-audience notice, its two counts are read one by one and anything that is not a non-negative integer reads as `unknown` rather than zero (neither "nothing was skipped" nor "nothing is left" may be invented), and one extra sentence — the context is empty but the turn runs — is given only when the count of entries left is exactly zero. When the server package is supplied, the guard drives the server's own emitter for that notice and reads what it emits back through the dispatcher. From 0.86.0 the catalog grows by six engine notices — a personal rule store that could not be read and was skipped, an auto-mode classification that could not decide and where the call went, a requested permission mode that did not take effect, a permission mode answering a question on the person's behalf, a mode release past a read-deny table, and a legacy checkpoint's mode keys being migrated — each with its own typed reader: branching words (the classifier's cause and destination, the reason a mode did not take effect) are read as closed sets, descriptive words pass through as written, and a malformed notice reads as absent; two of them are checked against notices produced by the engine's own emitters. The unresolvable-ask notice's optional remedy sentence is carried verbatim when it is a non-empty string — never trimmed, rewritten or filtered by cause — and its absence (older engines, other causes, an empty or non-string value) leaves the rest of the view unchanged; a notice without it reads byte-for-byte as before, and the engine's own mint function drives three cases through the dispatcher. A fourth server-minted code, the user-facing notice that a model endpoint could not be connected to, is registered with its audience and has its own reader: the failure code, the endpoint, the remedy sentence and the session are required, the name of the proxy variable is optional, the code and remedy pass through as written, and the server's sentences are never copied into the package. With server fixtures, the server's own decorator mints the notice for a direct and a proxied connection and for every connect-failure code it knows, and stays silent when a response arrived or the call did not end in error; the audience table is reconciled against both fixture generations, a row newer than the older fixture being required to be absent there. From 0.88.0 the catalog grows by one operator notice — a provider request whose tool turn broke the pairing rules was repaired on its way out — with a typed reader: each repair's form word passes through as written (the engine does not publish that word list from its entry point and has announced more words), the call ids are read as a non-empty list, any malformed entry makes the whole notice read as absent, and notices produced by the engine's own repair law and emitter read back field for field. The model-unreachable reader reads only the notice detail's own properties, like the other server-minted readers. From 0.89.0 the engine's own catalog registers those server-minted codes as well — each code carries an audience and a record of who mints it — so the package's separate table for them is retired and both the membership check and the audience lookup answer from the engine's tables alone, the four codes keeping the audiences they had; the guard reconciles the engine's record of server-minted codes against the published server's stream allow-list on the newer server generation and against the older server's own table on the older one, and the generator refuses an engine whose minter table is not in lockstep with its catalog. Two user-facing notices gain typed readers: an incomplete tool turn (cause, count, session and run are required, the call ids are optional and copied, and a cause word the package does not know passes through as written) and user input a run may not have used — one reader for the steer and follow-up codes, the kind decided by the code, the count required, the cause passed through as written and never filled in when absent — plus one user-facing sentence that names only the count, does not claim the input went unused when the server could not tell, and falls back to a cautious sentence for any cause it does not recognise. Notices produced by the engine's and the server's own emitters read back field for field, and every reader answers the same for frozen and unfrozen input. Every reader and the by-code entry point read a notice's code and detail as own properties only: a notice whose code or detail sits on its prototype, or on a polluted global prototype, reads as nothing through all three doors alike. From 0.90.0 the catalog is eighty-five notices: the engine retired the notice for the one-off migration of old parked rows, so the catalog, audience and minter tables lose a row, its fact reader and named type are gone from the public surface, and the code is treated like any code outside the catalog (the dispatcher answers nothing, the catalog check answers false, the audience stays the conservative default). The unused-input reader also reads the child-session field carried by the copy a parent session receives when a sub-agent ends with inputs unused — taken from the engine's real minting site — and the user-facing sentence says so for the two definite shapes, with no identifier in it; every malformed form of the field drops only that field. The register now holds the allow-list notice under its 0.92.0 engine name with the user audience; the earlier engine name is not registered and falls to the operator default, which is the audience it had on the engines that minted it. Since 0.94.0 the user-input-not-used reader takes its lane and count key from the generated lane table rather than from two hand-written codes, and the catalogue expectations follow the installed engine generation. From 0.94.0 the allow-list notice has a fact reader: each entry that reads as nothing comes back with its list source, its machine value exactly as the server sent it (control characters included), a display form produced by the package's single untrusted-text exit, its reason and the optional skill, list, verdict, current-name and value-type positions; a malformed entry list makes the whole reading absent. The model-unreachable reader carries the leg's run id as an optional `taskId` when the server sends one, and drops only that key when it is unusable. Since 0.96.0 the dropped-tool-turn reader also carries the optional outcome word (absent means the engine did not say), and the catalog follows the renamed path-view notice and three new route notices. Since 0.99.0 the catalog follows the newer engine at 95 codes, and the audience of each new code is the engine's own. Since 0.101.0 the catalog follows the engine at 100 codes: two new codes (a detached routing seat, and an advisor seat that was asked for but not attached) are registered with the engine's own audience and minter, and no reader is added for them. Since 0.107.0 the catalog also recognizes the old name `context.image_removed` as a user notice, because servers up to 7.142 still send it; the reader of `context.media_removed` reads it too, and a malformed new-name notice is never read through the old shape. The legacy-code table retires row by row: each row has exactly one retirement entry anchored on that row, and retiring one row at its floor leaves the other row in place. |
315
+ | `scripts/run-engine-notice-catalog-test.mjs` | The engine-notice catalog and its audience table. Whether a notice deserves a person's attention is not decided by whether this end happens to have a phrasing for it — that drifts with each client's build order — but by whether the engine minted the code into its own written catalog; the audience row answers the separate question of *who* the fact is for, since an operations fact pushed at an end user is noise and a user-facing fact buried in an operator log is something withheld from the person who could act on it. Both tables are reconciled against the installed engine's own artefacts in both directions and pinned in lockstep with each other, unknown codes fall back to the conservative operator side, and catalog membership is tested on the raw value so a code carrying control characters cannot impersonate a registered one after sanitizing. The reader for a dropped MCP injection keys on its own code alone and treats a missing session, server or reason as absence rather than throwing at a read site. A reverse pin enforces the upstream's single-mint contract: the engine composes those sentences from the host's facts, so a copy of them appearing in this package's source or build is a second source that would drift, and fails. From 0.84.0 it also covers the reader for the two read-directory grant notices: it recognises only those two codes, needs the tool call id to match a card, passes the rejection reason through as written, and treats only the granted notice as evidence that a directory was added; a granted notice without both the directory and the spelling the engine now holds, or with a scope other than `exact`, is not read at all, and the scope word is pinned to the engine's type at compile time. The server also mints a few notices of its own through the same channel; those codes live in a second table with their own audiences, kept apart from the engine mirror (which must stay equal to the engine's catalog) and reconciled against the server's published package when one is supplied, so a user-facing server notice is no longer filed under operations. One dispatcher returns the typed facts for every code that has a reader, discriminated by code and tagged with its audience — only the user-audience codes belong on a user surface — and the guard ties the dispatch table to the module's own exported readers in both directions, so a reader cannot be exported without a row and a row cannot be dropped without the guard failing. From 0.85.0 it also covers a third server-minted notice, the one saying that part of a session's saved history could not be read when the session was reopened: it is a user-audience notice, its two counts are read one by one and anything that is not a non-negative integer reads as `unknown` rather than zero (neither "nothing was skipped" nor "nothing is left" may be invented), and one extra sentence — the context is empty but the turn runs — is given only when the count of entries left is exactly zero. When the server package is supplied, the guard drives the server's own emitter for that notice and reads what it emits back through the dispatcher. From 0.86.0 the catalog grows by six engine notices — a personal rule store that could not be read and was skipped, an auto-mode classification that could not decide and where the call went, a requested permission mode that did not take effect, a permission mode answering a question on the person's behalf, a mode release past a read-deny table, and a legacy checkpoint's mode keys being migrated — each with its own typed reader: branching words (the classifier's cause and destination, the reason a mode did not take effect) are read as closed sets, descriptive words pass through as written, and a malformed notice reads as absent; two of them are checked against notices produced by the engine's own emitters. The unresolvable-ask notice's optional remedy sentence is carried verbatim when it is a non-empty string — never trimmed, rewritten or filtered by cause — and its absence (older engines, other causes, an empty or non-string value) leaves the rest of the view unchanged; a notice without it reads byte-for-byte as before, and the engine's own mint function drives three cases through the dispatcher. A fourth server-minted code, the user-facing notice that a model endpoint could not be connected to, is registered with its audience and has its own reader: the failure code, the endpoint, the remedy sentence and the session are required, the name of the proxy variable is optional, the code and remedy pass through as written, and the server's sentences are never copied into the package. With server fixtures, the server's own decorator mints the notice for a direct and a proxied connection and for every connect-failure code it knows, and stays silent when a response arrived or the call did not end in error; the audience table is reconciled against both fixture generations, a row newer than the older fixture being required to be absent there. From 0.88.0 the catalog grows by one operator notice — a provider request whose tool turn broke the pairing rules was repaired on its way out — with a typed reader: each repair's form word passes through as written (the engine does not publish that word list from its entry point and has announced more words), the call ids are read as a non-empty list, any malformed entry makes the whole notice read as absent, and notices produced by the engine's own repair law and emitter read back field for field. The model-unreachable reader reads only the notice detail's own properties, like the other server-minted readers. From 0.89.0 the engine's own catalog registers those server-minted codes as well — each code carries an audience and a record of who mints it — so the package's separate table for them is retired and both the membership check and the audience lookup answer from the engine's tables alone, the four codes keeping the audiences they had; the guard reconciles the engine's record of server-minted codes against the published server's stream allow-list on the newer server generation and against the older server's own table on the older one, and the generator refuses an engine whose minter table is not in lockstep with its catalog. Two user-facing notices gain typed readers: an incomplete tool turn (cause, count, session and run are required, the call ids are optional and copied, and a cause word the package does not know passes through as written) and user input a run may not have used — one reader for the steer and follow-up codes, the kind decided by the code, the count required, the cause passed through as written and never filled in when absent — plus one user-facing sentence that names only the count, does not claim the input went unused when the server could not tell, and falls back to a cautious sentence for any cause it does not recognise. Notices produced by the engine's and the server's own emitters read back field for field, and every reader answers the same for frozen and unfrozen input. Every reader and the by-code entry point read a notice's code and detail as own properties only: a notice whose code or detail sits on its prototype, or on a polluted global prototype, reads as nothing through all three doors alike. From 0.90.0 the catalog is eighty-five notices: the engine retired the notice for the one-off migration of old parked rows, so the catalog, audience and minter tables lose a row, its fact reader and named type are gone from the public surface, and the code is treated like any code outside the catalog (the dispatcher answers nothing, the catalog check answers false, the audience stays the conservative default). The unused-input reader also reads the child-session field carried by the copy a parent session receives when a sub-agent ends with inputs unused — taken from the engine's real minting site — and the user-facing sentence says so for the two definite shapes, with no identifier in it; every malformed form of the field drops only that field. The register now holds the allow-list notice under its 0.92.0 engine name with the user audience; the earlier engine name is not registered and falls to the operator default, which is the audience it had on the engines that minted it. Since 0.94.0 the user-input-not-used reader takes its lane and count key from the generated lane table rather than from two hand-written codes, and the catalogue expectations follow the installed engine generation. From 0.94.0 the allow-list notice has a fact reader: each entry that reads as nothing comes back with its list source, its machine value exactly as the server sent it (control characters included), a display form produced by the package's single untrusted-text exit, its reason and the optional skill, list, verdict, current-name and value-type positions; a malformed entry list makes the whole reading absent. The model-unreachable reader carries the leg's run id as an optional `taskId` when the server sends one, and drops only that key when it is unusable. Since 0.96.0 the dropped-tool-turn reader also carries the optional outcome word (absent means the engine did not say), and the catalog follows the renamed path-view notice and three new route notices. Since 0.99.0 the catalog follows the newer engine at 95 codes, and the audience of each new code is the engine's own. Since 0.101.0 the catalog follows the engine at 100 codes: two new codes (a detached routing seat, and an advisor seat that was asked for but not attached) are registered with the engine's own audience and minter, and no reader is added for them. Since 0.107.0 the catalog also recognizes the old name `context.image_removed` as a user notice, because servers up to 7.142 still send it; the reader of `context.media_removed` reads it too, and a malformed new-name notice is never read through the old shape. The legacy-code table retires row by row: each row has exactly one retirement entry anchored on that row, and retiring one row at its floor leaves the other row in place. Since 0.109.0 a read-root grant notice whose covers key is hidden by a lying has trap is not recognised instead of being read as the directory meaning. |
316
316
  | `scripts/run-tool-roster-projection-test.mjs` | The leg's tool roster — what the engine says it actually mounted and what face each tool wears — replacing three word lists that were only ever an estimate taken from one traffic capture against one pinned engine. The reader copies the engine's own all-or-nothing discipline: a roster whose row cannot be read, or whose declared count disagrees with the rows, is dropped whole rather than handed over short, because a consumer reading a short roster concludes the missing tools are not mounted — the upstream says in as many words that this is worse than sending nothing. A malformed *face* on a row (path target, render hints) drops only that face, since a face is not an identity. Shims are built strictly from roster rows and never guessed from a tool's name, and an axis that cannot be read stays absent rather than defaulting to `false` or `never`, which would render "unknown" as "safe". For run-time changes the guard pins the one hard rule in the contract: a digest that does not match is **not** a rejection — the carried roster is the new state regardless and only the summary becomes unusable, because refusing the swap would leave the consumer holding a stale roster forever. One reading here answers a question that the terminal state structurally cannot: whether this run was assembled with any file-and-shell tools at all. The engine's terminal vocabulary says a run finished, not whether the work got done, so an orchestrator that waits for the end and then guesses has nothing to guess from — while the assembly manifest already said it at the start, one row per mounted instance with the single condition that mounted it. The reading is three-state and both folds are refused: a roster that is readable and carries no such row is the engine stating a fact, while no roster at all is not that fact — the static half of a manifest never carries one, and an older engine reports rosters without naming the mount condition at all, where an empty count would be a statement about the reader rather than about the run. Those two are kept apart in the reason the reading carries, and the wording for every unknown case is checked never to claim the run had no tools. The same roster now decides the tool list on the first line of a non-interactive run: the host holds that line until the roster arrives and lists exactly what the engine mounted at the start of the run, in mount order. The guard runs a real assembly frame through the projection into the decision, and pins that the host falls back to the estimate only once the roster is known not to be coming — a manifest without one, an unreadable one, model output or the run's end arriving first — rather than on a timer alone (model activity counts, including a model call that is still waiting or retrying; an error line the stream synthesizes when a run fails before assembly counts as the run ending), that a sub-run's manifest is never mistaken for the run's own, that an empty roster is taken as the engine's answer rather than as silence, and that the wait bound covers both sequential default budgets the engine gives an external tool server to connect and list its tools. The holding logic itself lives in the package as a small per-run gate — buffer, decide once, release the held messages in arrival order, then pass through — and the guard drives real stream output through it to pin that the release happens exactly once, at the manifest, releasing exactly the held prefix. The ordering itself also lives in the package as a stream wrapper, and the guard checks the final output a consumer reads: the first line is always the tool-list line, a message that arrives while that line is still being built comes after it, a timer firing races nothing out of order, a source that ends or fails before the decision still gets its first line and held messages out before the error, and an early exit closes the source. From 0.84.0 the roster-derived sentence source no longer throws on a value it does not recognise, including a reading of the manifest's `hands` section passed by mistake: it answers the same "not stated" sentence as the `hands` reader, from one shared source, and its six known sentences do not change. From 0.86.0 that first line also says where its tool list came from, in two added keys the wrapper writes onto the host's own line object in place: a source of `engine-roster`, `estimate` or `host-static`, and, only for an estimate, which of the four reasons made the roster known not to be coming. The guard checks every decision path end to end, that the host's object keeps its identity and every other key byte for byte, that stale values of the same two keys left by the host are replaced (a stale reason is removed rather than left behind), and that a host line that cannot be written is passed through untouched instead of failing the first line. |
317
317
  | `scripts/run-permission-rule-issue-codes-test.mjs` | The rule-lint refusal codes an engine reports when it will not compile a permission rule. The SDK publishes neither a schema nor a type for them, so the package mints the table from the engine's own bytes and the guard pays the cost of that copy instead of leaving it to somebody remembering: it parses the codes the engine actually mints and reconciles them against the table in both directions, so a code added upstream (the user would see a bare code) and a code only the package believes in (a branch that can never fire) both fail. It also reconciles the table plus a small retired ledger against the engine's declared union, which is deliberately not the same set — one member was renamed and its old name is still declared — so reviving a code the engine will never mint again is impossible and a future stale member shows up immediately. Sentences are pinned one per code, mutually distinct, and split by family: a rule that is wrong and a rule that is legal but unsupported on this lane are different next steps and may not share a sentence. The engine's own message rides along as prose — sanitized and capped after escaping, never matched on The set of codes the engine actually mints now also counts declared codes minted outside the rule compiler (the allow layer reports an entry that is not a string with one). Codes an engine release retired but older engines still mint are found by comparing the receiving server's bundled engine with the installed one rather than by a hand-kept list; each keeps the exact sentence it had before, and the codes that replaced them each get their own sentence with no machine identifiers in it. Since 0.94.0 the base-path refusal sentence is checked to hold on both engine generations (base missing, or declared but not a usable absolute path), with the premise run on the installed engine. Since 0.99.0 the tool-name refusal code minted by the newer engine is in the table with its own sentence, and the two-way equality with the engine's minted set covers it. |
318
318
  | `scripts/run-gate-vocabulary-test.mjs` | The two gate vocabularies — who denied a call (`DeniedBy`, the installed engine's ten words) and who asked about it (`AskOrigin`, eleven) — together with the one place their sentences are minted, so the same denial does not read three different ways across three clients. The tables are copies, not opinions: the asker table is reconciled in both directions against the lists the installed SDK and engine packages publish, and the denier table is exactly the installed engine's own list (from 0.92.0 the one word only engines below the support floor still sent is no longer in it), checked so that every word the SDK declares is present except that retired word, and every word ahead of the SDK comes from the engine — a word added upstream (nobody renders it, the user sees a bare code) and a word only the package believes in (a branch that can never fire) both fail. Every word must carry its own literal sentence and no two may collide, including the sibling pairs the upstream deliberately split apart — an organization store and a personal rule store being unreadable send you to different people, and the two tighten origins exist precisely to name which layer of engine logic asked. The two fallbacks are pinned distinct because an unknown word means different things in each: for a denial layer this build does not know, the sentence says only that this build cannot say which layer refused the call or why — it no longer guesses at a newer engine or a damaged record, since the most likely unknown word is the retired one replayed from an old session; the asker vocabulary is genuinely open (the server only checks for a non-empty string, so an unknown word just means the client is older than the engine). Alongside them sits an **uplift anchor** rather than a third table: the reason a call was decided the way it was is a distinct semantic face from who denied it and who asked, one upstream has not mirrored into the SDK at all, and one whose newest member — a shell command allowed because it only reads — has no sentence anywhere yet. Minting the union here would create the second drifting source the day upstream publishes it, so the guard instead asserts the **absence** from both ends: the SDK declarations carry no such union near that word, and the installed engine’s own list does not carry the word either. The engine end fires first, on the batch that raises the dependency, which is exactly when the ownership question should be answered; the SDK end fires when the mirror lands. Either red is the work order to mint the sentence, never a reason to delete the anchor. A fourth mint now sits beside the three tables and is not a table at all: a single presence-only fact — that no saved rule and no standing posture can retire this question — earns one sentence, taking no argument precisely so a caller cannot mistake it for a second kind of mandate, pinned distinct from every sentence the tables mint, pinned never to point at rule-writing, and pinned not to overclaim the stronger neighbouring demand that a person rather than a configuration must answer; it must not say the question is asked every time — an answer for this one call may come from the person, a hook or an automatic check the deployment runs — and its wording is checked against the engine package's own description of the mandate. A fifth table joins them from 0.80.0: the words for **how a wait ended** (thirteen from 0.80.0, fourteen from 0.90.0), mirrored in both directions from the engine's own declarations — the table's owner — with the wire SDK's copy held alongside as a second witness that must match it word for word and in order, so the day the SDK falls a generation behind, that is what turns red rather than the mirror silently following the wrong source. The newest of them says a deployment's own policy answered the card — not a person, and not “nobody could be asked” — so the guard pins it apart from both neighbours by behaviour, feeding every one of the words through all five named predicates and checking which word makes which one speak, rather than what any predicate returns. Two of them also decide how a refusal is filed in the session transcript; that mapping is minted once and reused by both of the package's own entry points, and anything outside those two words yields nothing rather than a guess. Since 0.83.2 a sixth list covers the word a mandated question stands on (`APPROVAL_MANDATE_WORDS`, six words): it must equal the engine's own list word for word and in order, membership is exact, the card reader `readApprovalMandate` answers only for an own key holding one of the six words, and each word has one fixed sentence explaining why the question must be confirmed — six distinct sentences that never point the reader at writing a rule, never promise a question every time, never claim only a person may answer, and repeat no other sentence the package mints. The list is also pinned against the engine's type at compile time in both directions, while the published build references no engine package at all: every `.js` and `.d.ts` file in the build is scanned, and the same scan is first shown to fire on references planted in a scratch directory. From 0.90.0 the settlement table carries `policy_allowed` (a machine allow, listed before `policy_refused`) and the mirror is checked against the installed engine byte for byte, in order; the installed SDK (12.x) predates the word, so its list is compared as "the engine's list minus one registered lagging word", and every SDK witness at 16.0.0 or later must carry exactly the engine's set of words. The party that settled is the engine's five-word set, read open (any word, including an unknown one, comes back as written); a policy allow is no person's approval and no refusal (all five named predicates stay silent on it, and it files no denial class in the transcript), and the refusal word reads the same under both party spellings of the two server generations. The one lagging word that loosens the compile-time check against the installed SDK is itself guarded: it must stay a non-empty union of string literals (widening it to `string` compiles cleanly, so only this check catches it), the check's declared type must stay exactly as written, the word list must equal the list this check registers, and every lagging word must be absent from the SDK at the peer floor yet present in the newest witness SDK and in the installed engine's list. The origin word on a gate record and on a released-by-mode record is read as an own key only, and a structural pin holds that every source file reading an ask origin goes through the one reader. A retired mandate word that an older server still sends reads as the sentence of the word that replaced it (one sentence per fact), the sentence table holds the current words only (a leftover sentence for a retired word fails to compile), and the modules that read the rename table are pinned to a named roster. |
@@ -339,9 +339,9 @@ guard still cross-checks the table by name).
339
339
  | `scripts/run-approvals-stream-live-capability-test.mjs` | The engine's live-approval-push self-description (`capabilities.approvalsStreamLive`, engine ≥7.87.1), read the same four-state way as its four sibling capability readers: an absent key is reported as not reported (never folded into `false`), the value must be a strict boolean, and the one decision the feed consumer needs — whether it must keep pulling suspended asks itself — is answered by `livePendingNeedsReconcile`, which only says no when the engine explicitly says it pushes. |
340
340
  | `scripts/run-model-facing-anchors-test.mjs` | Five places where model-facing engine text is parsed back into card slots: the task-output cursor semantics machine-readable key wins over the spool marker text (and its absence is never read as `full`), the `--- result (…) ---` line accepts a parenthesised note and reads it in linear time even on crafted input (a note can no longer swallow the start of the next line), the bash header accepts the engine's exit-note and image variants and carries the note into the same benign-exit slot the structured path uses, the notification seed scan treats both producers' terminal words as terminal, and the parked background-task phrase is recognised. |
341
341
  | `scripts/run-execution-lane-capability-test.mjs` | The deployment execution-lane self-description (`capabilities.executionLane`, engine ≥7.86.0), read the same four-state way as its three sibling capability readers. An absent key is reported as "not reported" and never as "tools do not run on this host" as a fact about the deployment; since every server at or above the support floor reports the key, the single "do tools run here" predicate answers `false` for a body that reported nothing (from 0.92.0, so no skill `baseDir` is sent on a reading that proves nothing), and keeps using the caller's own inference only while nothing has been observed yet. The implication is one-way (`toolsOnThisHost: false` means `baseDir` is never sent; `true` is only a necessary condition), `provider` is read as an open non-empty string rather than a hand-copied closed set and is sanitised and bounded before display, `toolsOnThisHost` must be a strict boolean, an `isolated` position that can only be read by running a getter (or whose descriptor read throws) makes the whole entry unreadable rather than degrading that one position, extra members never cross, the reading comes only from the capabilities cache's landed response (there is no separate write port), and the single "do tools run here" predicate uses the bit when it is present. Current engines (≥7.106.0) also say whether the lane's tools sit behind an OS isolation boundary (`isolated`, present only when the lane has tools of its own); the reading carries it when it is a strict boolean and otherwise records which of three absences it is — a lane without tools on a current engine, an older engine, or a value that cannot be read (including a current engine that says tools run on this host yet omits the flag it must then send) — none of which is ever read as `false`, and an unreadable value never costs the rest of the lane reading. The single "no OS sandbox on this machine" predicate (`isolated` false and tools on this host) answers yes, no or unknown under three-valued logic, one sentence goes with the yes answer, the operator diagnostics segment is read by the same rule, and the engine's own projection for every lane is replayed: only the host lane answers yes |
342
- | `scripts/run-live-pending-ask-test.mjs` | Suspended in-stream asks (the `livePending` section of `GET /v1/approvals`): an absent key reads as not reported (never an empty list or a zero count), an unreadable section leaves the previous snapshot in place, rows are narrow-read (malformed rows dropped and counted, optional flags only-if-true, extra members never cross), the feed digest covers the section so an ask appearing or settling produces a snapshot, the host-gated reconcile cadence re-lists while the push leg is connected and backs off when nothing changes, the tracker surfaces each `approvalId` once and reports it gone once, and the decision runs through the same respond chain as the in-stream frame leg — with the card stating that the tool arguments are not visible and edits on that card never forwarded. Since 0.73.4 a push-side fetch that did not get a readable body (thrown, or an unreadable `livePending` section) is owed a look: it is retried with doubling back-off until one fetch succeeds (never abandoned by count), cancelled by any successful fetch, cleared on stop, and switchable off. The session scope of the suspended-ask view, count and tracker must be chosen explicitly (one session, all sessions, or identity pending): an unresolved or malformed scope answers null counts, an empty view and a tracker that never emits, and the three entry points refuse to compile without a scope. On servers that present approvals as `approval_request` frames (engine ≥7.133.0), a suspended row carries that frame instead of a `tool_approval` one; either opening-frame form is accepted when its `approvalId` matches the row, both go through the same card, holder table and respond call keyed by the row's `approvalId`, the card face is read from `card` and the out-of-card facts (mandated, origin, …) from the frame's top level, and anything outside the two forms still yields the blind card; the `tool_approval` card is byte-identical to the previous release. A malformed top-level `requiresRealApproval` on such a card is recorded in `unreadableEvidence` (the automatic-answer verdict stays `unknown`), and a parked row without an approval id still joins the same live card by run, call id and ask signature — call id, sub-agent run and ask classes read from the card face, origin and mandate from the frame's top level, and none of the out-of-card facts for an unrecognised version — so no second card or second decision appears. The per-session waiting count includes parked rows of background children (and grandchildren) this session launched, using the same session test as card ownership. |
342
+ | `scripts/run-live-pending-ask-test.mjs` | Suspended in-stream asks (the `livePending` section of `GET /v1/approvals`): an absent key reads as not reported (never an empty list or a zero count), an unreadable section leaves the previous snapshot in place, rows are narrow-read (malformed rows dropped and counted, optional flags only-if-true, extra members never cross), the feed digest covers the section so an ask appearing or settling produces a snapshot, the host-gated reconcile cadence re-lists while the push leg is connected and backs off when nothing changes, the tracker surfaces each `approvalId` once and reports it gone once, and the decision runs through the same respond chain as the in-stream frame leg — with the card stating that the tool arguments are not visible and edits on that card never forwarded. Since 0.73.4 a push-side fetch that did not get a readable body (thrown, or an unreadable `livePending` section) is owed a look: it is retried with doubling back-off until one fetch succeeds (never abandoned by count), cancelled by any successful fetch, cleared on stop, and switchable off. The session scope of the suspended-ask view, count and tracker must be chosen explicitly (one session, all sessions, or identity pending): an unresolved or malformed scope answers null counts, an empty view and a tracker that never emits, and the three entry points refuse to compile without a scope. On servers that present approvals as `approval_request` frames (engine ≥7.133.0), a suspended row carries that frame instead of a `tool_approval` one; either opening-frame form is accepted when its `approvalId` matches the row, both go through the same card, holder table and respond call keyed by the row's `approvalId`, the card face is read from `card` and the out-of-card facts (mandated, origin, …) from the frame's top level, and anything outside the two forms still yields the blind card; the `tool_approval` card is byte-identical to the previous release. A malformed top-level `requiresRealApproval` on such a card is recorded in `unreadableEvidence` (the automatic-answer verdict stays `unknown`), and a parked row without an approval id still joins the same live card by run, call id and ask signature — call id, sub-agent run and ask classes read from the card face, origin and mandate from the frame's top level, and none of the out-of-card facts for an unrecognised version — so no second card or second decision appears. The per-session waiting count includes parked rows of background children (and grandchildren) this session launched, using the same session test as card ownership. The decision identity of a suspended row (its `approvalId`) is read once: the row reader takes the own data descriptor of the row id and of the frame id one time each (a getter is never run, an id that lives only on the prototype chain does not count; a bad row id drops the row and counts it, a bad or mismatching frame id drops only the frame position so the row surfaces as a blind card), the live card entry takes the own data descriptor of the frame id once before its first await (an unreadable id surfaces no card and sends nothing — `not_sent` / `card_unavailable`), and the suspended-row leg hands the row reader's reading to the entry as the card identity, so a frame changed after the read, or a proxy that answers the descriptor read differently each time, cannot move a decision to another approval. |
343
343
  | `scripts/run-leader-conflict-test.mjs` | The leader-run terminal `needs_human` + `result.conflict` (engine ≥7.83.0), read once for all three shells. A `result` without a `conflict` key is reported as `none` and worded as "no conflict details" rather than "no conflict": on the wire it is indistinguishable from an older engine that never reports one, and the reader does not pick a side. A `conflict` that is present but unreadable is a third word, never folded into `none`, because a tree that carries conflict markers must not be rendered as clean. `filesTruncated` is honoured only as `true` (absent means the list is complete; any other value makes the section unreadable), `workers[].applied` is passed through as the boolean fact it is, `salvaged[].patch` is handed to the save path byte-for-byte (an artifact, not screen text; a malformed row is dropped alone, and a salvage list that is present but cannot be fully read is flagged rather than rendered as "no patches"), and `rejHead` — the one on-screen diagnostic — is escaped and bounded before display. The record never throws on hostile input, extra members do not cross, arrays are fresh copies, and the single sentence minted here names the base, the files, which branches landed and which did not, and how many patches can be saved, without ever suggesting a retry: `needs_human` is a run waiting for a person, and the no-details sentence says plainly that it is not proof of a merge |
344
- | `scripts/run-mcp-reconnect-test.mjs` | The in-session MCP re-dial verb (`POST /v1/sessions/:id/mcp/reconnect`, engine ≥7.85.0), consumed. The single discriminant is `outcome` and all three answers are HTTP 200, so the reader branches on the word and never on the status; the `unsupported` answer carries exactly five keys and the reader refuses to invent a zero or an empty list for the four fields the engine did not produce, while `accepted` / `refused` treat those four as required and go malformed when one is missing. The tool roster follows the **presence** of `toolNames` (absent = untouched, empty = withdrawn), the connection record passes `errorCode` through as an open set, and every remote-authored string is sanitised and bounded before display. Failures are classified by `errorCode` alone, a missing code is reported as unknown rather than guessed, the verb never throws, and the request-side guard (non-empty name, ≤190 chars) stops a call that the contract would reject anyway. The capability bit reads absent as "cannot tell" rather than "unavailable", and the one sentence the contract insists every UI carries — that re-dialing is a transaction, not a refresh — is minted here once. The engine's answer-deadline reply — a 503 whose machine code says the request stopped waiting before the re-dial settled — is its own verdict arm: it is read by that code alone and never by the status, carries no outcome, and is never rendered as a failure, because the re-dial was not cancelled and may still be running; the one sentence for it is minted here once, and the gate pins that no server release up to 7.117.0 contains the code at all |
344
+ | `scripts/run-mcp-reconnect-test.mjs` | The in-session MCP re-dial verb (`POST /v1/sessions/:id/mcp/reconnect`, engine ≥7.85.0), consumed. The single discriminant is `outcome` and all three answers are HTTP 200, so the reader branches on the word and never on the status; the `unsupported` answer carries exactly five keys and the reader refuses to invent a zero or an empty list for the four fields the engine did not produce, while `accepted` / `refused` treat those four as required and go malformed when one is missing. The tool roster follows the **presence** of `toolNames` (absent = untouched, empty = withdrawn), the connection record passes `errorCode` through as an open set, and every remote-authored string is sanitised and bounded before display. Failures are classified by `errorCode` alone, a missing code is reported as unknown rather than guessed, the verb never throws, and the request-side guard (non-empty name, ≤190 chars) stops a call that the contract would reject anyway. The capability bit reads absent as "cannot tell" rather than "unavailable", and the one sentence the contract insists every UI carries — that re-dialing is a transaction, not a refresh — is minted here once. The engine's answer-deadline reply — a 503 whose machine code says the request stopped waiting before the re-dial settled — is its own verdict arm: it is read by that code alone and never by the status, carries no outcome, and is never rendered as a failure, because the re-dial was not cancelled and may still be running; the one sentence for it is minted here once, and the gate pins that no server release up to 7.117.0 contains the code at all Since 0.109.0 a toolNames or listingIncomplete key hidden by a lying has trap makes the whole result malformed instead of being read as absent. Since 0.110.0 the same gate pins the one place that words a re-dial verdict that did not succeed (nine verdicts times a server name times both languages, the name painted as visible escapes and bounded, remote error text never rendered, malformed input — including a successful verdict whose result cannot be read — answering nothing and never throwing) and the pure reader that says whether a failure record names the engine's own authorization-required class (only that exact class word counts: HTTP status numbers, near-miss spellings and unreadable input are never read as a yes, and unreadable input is never folded into a no). The small status fragment that follows a failure class on a connector row has one maker too: only the authorization-required class carries the re-authorize hint, a 5xx status says the server is down, and every other status — 401 and 403 under the plain HTTP-status class included — is shown as the bare number. |
345
345
  | `scripts/run-core-value-ports-test.mjs` | The port-injection seam for ten **engine value-level** facilities (autonomous-loop prompt assembly, permission-rule loosening, tool-policy composition, protocol/retired-name/grammar lookups, rule compilation; the discussion-workflow name port left in 0.96.0 together with that engine feature). This package cannot re-export them (the engine barrel drags Node built-ins into the browser bundle), so it declares the ports and honest-absence readers; a Node host installs the engine's own functions verbatim. The guard pins: every reader returns `undefined` when nothing is installed (never a fabricated empty array or default policy), arguments and results pass through by reference, engine errors propagate unchanged, partial installs read partially, restore functions unwind to the previous bag, and the module source has zero engine imports From 0.90.0 the mirror of the allow/deny policy options is also compared with the engine's own declaration, both ways: the same set of option keys (the engine added an optional `scope` — whether an allow-list hit is a stated grant or only a bound on which tools a lane may see), with `scope` optional and its two-word union and the invalid-name union spelled identically; the value handed to the installed function is passed through untouched. From 0.101.0 the engine retires that option (passing it is refused by name when the policy is built), so the mirror has no `scope` member and the check asserts it is absent on both sides, that the engine refuses it, and that a stray one is still handed to the installed function untouched rather than dropped. Since 0.94.0 there are twelve ports: the engine's allow-list reader joins them, with the same absence and pass-through rules. From 0.94.0 the seam also carries the engine's five route-adjudication functions (`routeKeyOf`, `joinRequestUrl`, `resolveRouteCredential`, `adjudicateModelRoute`, `routePairingStatus`): a host that builds the full `CoreValuePorts` bag fails to compile until it adds them (the gate compiles that bag in memory and checks each missing member is named), the engine's own functions type-check against the mirrors, and each reader answers `undefined` while its port is not installed. Since 0.96.0 the whole engine bag also assigns into the full port type with no cast (the rule-grammar mirror follows the engine). Also in 0.96.0 the engine's rule tool-name judge (`judgeRuleToolName`, reader `ruleToolNameJudgement`) joins the seam and the retired-name table port leaves it (its one reader now asks the judge): the gate pins the swapped name list, pass-through and error propagation for the judge, and compiles the full bag both ways — missing the judge, or still carrying the retired-name table, fails and names the member. In 0.98.0 the full-bag assembly check compiles under three option sets — plain strict, strict plus exact optional property types and unchecked indexed access, and non-strict — and all sixteen engine functions must install into the full type with no error in each (before, the two strict sets rejected the combine-policies and validate-rules members, which were function-typed properties checked contravariantly), with a sabotage control that goes red under all three. In 0.101.0 the nineteenth port is the engine's default root address for the first-party hosted API: a value rather than a function, installed from the engine's package-root constant. Its reader returns `undefined` while nothing is installed (never the literal root), returns exactly what was installed otherwise (no trimming, no shape check, an empty string stays an empty string), the engine's own constant type-checks into the port type under all three option sets, a full-type bag built for the previous eighteen ports stops compiling and names the new member, and the reader body is a single read of the port with the host name absent from the module source. The twentieth port is the engine's reader for an MCP server's `toolFaces` declaration: absent when not installed (never an invented ok), passed through with the same references when installed, the engine's own function type-checks into the port under all three option sets, a full-type bag built for the previous nineteen ports stops compiling and names the new member, and the reader's signature takes a narrow input type rather than an unknown one. The twenty-first port is the engine's covering-rule matcher (whether a rule name the engine reads as covering every tool of a server covers a given tool name): absent when not installed (never an invented false), both arguments passed through and the answer returned as given when installed, a throw from the engine propagates, the engine's own function type-checks into the port under all three option sets, a full-type bag built for the previous twenty ports stops compiling and names the new member, the real engine answers twelve fixed pairs identically through the port, and the reader's signature has no unknown in it. The twenty-second port is the engine's frozen tuple of the three settings-layer names the importer reads (a value, installed from the engine's package-root constant; the types package carries the matching type but no matching value, so the seam carries the value): absent when not installed (never the three literals), and when installed the very same tuple is returned (no copy, no re-freeze, no shape check, so the engine's frozen tuple stays frozen at the read site). The values are checked element by element with a syntax-tree reader against the engine's own declaration, its compiled initializer, its runtime value and the tuple declared on the port member in this package's shipped declaration file, and the tuple and its element type are checked both ways against the engine's own type and the types package's type with the real type checker; every reader and comparison is first proven against planted samples (a wrong word, a missing member, a swapped order, a missing declaration, an unfrozen initializer, a renamed re-export, a copying reader, a reader with a literal fallback). The engine's own constant type-checks into the port under all three option sets, a full-type bag built for the previous twenty-one ports stops compiling and names the new member, and the reader body is a single read of the port. |
346
346
  | `scripts/run-read-face-posture-projection-test.mjs` | The operator-face `readFace: ReadFacePosture` reader (server >=7.65.0). Three ways of "can't say" are pinned to three different, literal sentences, and none of them may read as "nothing is pinned" — that statement belongs to exactly one case, `face: null`, which is a positive fact reported by the engine, not an absence: not having read an operator response yet, having read one from an engine too old to report the key, and the engine actually saying nothing is pinned are three different next steps for an operator and must not collapse into each other. `source` is read as an open set (the server's closed four words plus an escape hatch) rather than narrowed to an enum, so a new word added upstream is not silently turned into a bad reading. The free-text `note` is sanitized and length-capped before it is ever rendered. A companion pure function flags disagreement between this face and the tenant-facing `capabilities.readFace` — silent only when the two actually agree, honest-absent when either side cannot be read at all, never asserting agreement as a fact. The gate's last leg reads the installed SDK's own `openapi.yaml` directly rather than restating the schema in prose, so the package's leniency cannot quietly drift from the real contract |
347
347
  | `scripts/run-display-body-test.mjs` | The engine wraps text it hands a model in a fence — an opening marker naming the payload, the payload itself, and a closing marker — so the model reads it as data and not as instructions. That fence is minted and read in one place here, which makes stripping it for a human reader this package's job rather than each shell's: a shell that renders the envelope verbatim is showing a person a defence that was written for a model. The reader answers with a discriminated union — fenced, with the label and the payload, or not fenced, with the text as it came in — and it reaches that answer through the **same** matcher the mint side registers, never a second copy of it; the guard proves that by walking the syntax tree of every source file and requiring exactly one literal carrying the marker text, and by requiring the reader's own body to contain no matcher of its own. Eighteen shapes are run through both entry points and required to agree line for line. Anything the package does not recognise — a near-miss in the wording, a hyphen where the marker has a dash, a different case, an opening marker with no close, a close before an open, a truncated close, or any non-whitespace byte outside the pair — comes back unfenced with the input returned **verbatim**: no guessing, no trimming, no repair, because a half-stripped envelope puts a sentence on screen that nobody wrote. Only the outermost layer is removed, so a nested fence, or one forged inside the payload, survives byte-for-byte in the body — those bytes are part of what the engine said, not part of this protocol. Nothing else is washed: control characters, leading and trailing whitespace and a twenty-thousand-character payload all pass through untouched, and so does the label, because sanitising and length-capping belong to the mint point that puts a string on a screen and a passage of text must not have two launderers. A value that is not text is answered with **nothing at all** rather than with an empty payload: the reader never stringifies it, never calls its `toString`, and never emits `[object Object]`, and it does not hand back a body of zero length either — an empty payload is a real reading (a fence can legitimately wrap nothing, and an empty string is an empty string), so folding "there was no readable text" into it would leave a caller unable to show a degraded line at all. Those three stay apart: no text yields nothing, an empty string yields an unfenced empty payload, and an empty fenced payload yields a fenced one with its label. The `fenced` discriminator is always present on a reading, and the label key exists only on the fenced arm, so a missing label is never rendered as an empty one. One shape needed more than the whole-string match this started with. When the engine reports back from a delegated run, the fence is only **one section** of the report: ahead of it sit a frame header, a handful of optional field lines and a section label, behind it a closing instruction addressed to the model, an optional internal identifier and a usage block. A matcher anchored to both ends of the input answers *not fenced* on that, and the whole scaffold - written for a model - goes on screen. The reader therefore also locates the fence **inside** a recognised report frame, using four anchors that are always present and always byte-for-byte fixed, and hands the located slice back to the same single matcher rather than a second one; the guard assembles its corpus from the engine package actually installed (the fence from that package's own constructor, the frame lines read structurally out of the minting file and then compared byte-for-byte with what this package registers), so a rewording or a reordering upstream turns the guard red the day it lands. Three readings are pinned one cell each: a fenced result section yields the payload the delegate actually wrote; a partial-findings section yields that text and says which of the two it is, with the prefix line kept out of the payload; and a section the engine filled with its own no-text sentinel yields an empty payload with a reason, which stays distinguishable from an input that was simply an empty string. Whatever surrounded the fence is returned alongside rather than dropped - the bytes before it, the slice itself and the bytes after it reassemble into the input exactly - and the payload never contains a line of the frame. The criterion deliberately does **not** enumerate the lines outside the fence: several of those field lines carry interpolated untrusted text and cannot be told apart from prose, so requiring every one of them to be recognised would mean that a single new field line upstream sends every failed report back to being unreadable, and a failed report is exactly when a person most needs to read what the delegate said. Fourteen negative shapes hold the line against the easy widening, strip anything that looks like a fence: a fence sitting in ordinary text, a missing frame header, a different sentence where the closing instruction belongs, a report with no closing instruction at all while the fence sits at the very end, a header and label in the wrong order, mismatched open and close tags, an open with no close, a bare unfenced section, a stray line between the label and the fence or between the fence and the closing instruction, an entirely absent section, a no-text sentinel with another line after it, a partial-findings prefix followed by something that is not a fence, and the whole report in carriage-return line endings all come back unfenced with the input verbatim and no frame at all. Above all, **provenance is not in the text**: locating a fence inside a frame happens only when the caller states where the bytes came from, because four anchors can only recognise a shape and never prove an origin. The default reading is byte-for-byte what it was before, so a passage of ordinary prose that happens to quote a report - with real warnings on either side of the quoted part - is returned untouched and those warnings stay on screen; a caller that does state the origin gets the located reading, and even then every surrounding byte comes back alongside. Twenty-three malformed origin values fall back to the narrower default without throwing: a near-miss in case, a camel-cased spelling, the right word padded with spaces or tabs or a newline or a zero-width character, a string wrapper object, an object whose `toString` or `valueOf` reports the right word, and an object whose converters both throw. The guard compares the caller’s argument for **exact equality** and nothing else — no trimming, no stringifying, no calling the value’s own converters, because that would let a value of unknown provenance choose its own lane — and a source-level cell requires that the argument reach the comparison unreassigned and unnormalised, with its own two-way check that those patterns speak. The two accepted words are read off the published type rather than copied into the guard; adding a third word later is a type-compatibility change for any caller that switches exhaustively on them. Each of the three anchors must also be **unique** in the text, and ambiguity means the reader declines. The reason is not hypothetical: the frame header interpolates the task's own description verbatim, and the engine only requires that description to be a string, so it can carry newlines and a complete set of protocol lines. Any rule that picks one candidate out of several can therefore be made to pick the planted one, hiding the real result among the surrounding bytes - a guard cell reproduces exactly that, with a planted section and a real one, and requires the reader to decline and the real text to stay on screen. Two reports back to back are the same ambiguity and are declined the same way, with both payloads left visible; a delegate that quotes any one of the three anchor lines inside its own answer also falls back to the input verbatim, which is the registered cost of the rule, and a discrimination cell shows the same corpus reads cleanly once the quoted line is gone. The two legs are not the same shape either: the forked one ends at its closing instruction with no trailing bytes at all, and that real shape has its own cell. The witness arm reads each leg only inside its own array of lines, decodes every extracted literal to its **runtime** value rather than trusting the spelling in the source, and fails loudly if it cannot - an escape rewrite upstream leaves the runtime label unchanged while the spelling diverges, and since that same extracted value builds the corpus and serves as the expectation, trusting the spelling would close a self-proving loop. The two payload labels are therefore also pinned in the guard and compared against what was extracted, so an equivalent rewrite stays green while a real rename turns red the day it lands. A delegate that quotes the frame lines inside its own answer does not move the location, and those quoted lines survive in the payload byte-for-byte. For a recognised report frame the reader also hands back the header slots a shell used to assemble by counting lines: the state line (read only when it sits immediately after the frame header and is the only state line in the frame, so a line planted in the task description cannot masquerade as the engine's own), an error line and a usage line (each only when it is the only one of its kind), and every line it did not claim, verbatim and in order; when the state line cannot be read from that position all four slots stay absent and every byte stays in the remainder |
@@ -360,7 +360,7 @@ guard still cross-checks the table by name).
360
360
  | `scripts/run-usage-verbatim-channel-test.mjs` | The two complementary usage disciplines (core 3.0.0 metering semantics): the CC `ModelUsage` mirror stays pure (five pinned keys, `totalInputTokens` has no seat), while the sema-owned channel forwards the engine `turn_end.usage` object **verbatim** (six keys, incl. `totalInputTokens`) via `last_turn_usage.engineUsage` / `handle.latestEngineUsage` — honest absence on pre-3.0.0 engines, no fabricated zeros |
361
361
  | `scripts/run-plan-review-decide-verify-test.mjs` | `decidePlanReview`'s post-decide honesty ([2315]/[2316], engine RB-471 family): a 2xx from the decide endpoint is **not** a terminal — the wire re-pulls the task status and words the outcome by the real shape (still-locked / legal new gate / genuinely left park / unverified), never claiming success it hasn't earned; when the engine answers that the session's stored resume context cannot be read, the outcome names the unreadable row and says the decision was not applied. Driven against a real fake-engine HTTP server through the shipped dist. The outcome queue item also carries a machine-readable `_sema_planReviewOutcome` (task id, a package-minted dispatch number, decision, effect) so a host can tell which in-flight decision an outcome belongs to without searching the prose; the prose is byte-identical, the number is minted only for a decision the in-flight latch admits, and a caller that passes no metadata gets no key. A plan-review card answered with free text and no option picked (or on the reject option with a note) is read as a rejection carrying that text as its reason, on both the first card and a reopened card; the reason is narrowed by the same step as an approval-denial reason (trimmed, blank dropped, capped at 4096 characters) and a note on the card wins over any reason the caller supplied. An approval with a note stays an approval and sends no note; an empty answer, a contradictory multi-select, several questions or an option outside the shown card stay dismissed; an answer without a note produces a request body byte-identical to before. |
362
362
  | `scripts/run-shell-gate-durable-allow-test.mjs` | The durable approval leg for **shell** gates. The tool_end HOLD/REJECT predicate must cover Bash the same way park detection already does (otherwise the park poison frame `Operation aborted` hits the transcript, `endedCalls` swallows the real replayed result, and the user who pressed Yes watches a command that really ran be reported as aborted); a replayed, already-decided park must resume reading the stream instead of being reported as a failed turn; `lastEventId` must track numeric `seq` too. Mutation-proven: each of the three fixes reverted turns the gate red |
363
- | `scripts/run-hitl-gate-honesty-test.mjs` | [2393] the four HITL disciplines that a passing type-check cannot see. (1) The park predicate and the `tool_end` predicate must cover the **same** set — the park side admits a first-class `kind:'tool_approval'` gate for *any* tool name, and a `tool_end` frame carries no `kind`, so the frame-level judge falls back to the engine's exact abort marker; otherwise the poison frame hits the transcript and `markEnded` swallows the real replayed result (the same replay-swallowing disease as on shell gates, reopened on kind-only gates). (2) The already-decided identity criterion is **one-shot**: its two inputs are monotonic, so without consumption one successful decide makes every later park failure — including a real `approvals.list` outage — read as "already resolved" until the 24-hop budget runs out and reports a cause that has nothing to do with what happened. (3) A `plan_review` card dismissed without an answer must be re-presentable: the idempotent re-arm short-circuit re-publishes the still-armed card, and a stale armed id (responder gone) re-arms from scratch rather than presenting a card nobody can answer. (4) `HitlSafetyError` is a safety signal — the `remember` fallback arm must re-raise it instead of auto-retrying the decide, while a plain unknown-key 400 still falls back. (5) The polling leg reschedules after an escaping throw and flips `mode()` to `idle` once it consistently fails, so the honesty surface stops reporting a dead feed as live. (6) The live-frame leg carries the fact behind "you are being asked because the auto-mode classifier could not run" all the way to the card port. Transit narrows on SHAPE only — a non-empty cause string is taken verbatim, an open set, because the word table's owner is the engine and re-checking a closed table at the package boundary would drop a legal value the day a new cause word appears, which is exactly the information worth keeping. A malformed carrier degrades to absence rather than half-minting, and absence stays absence: it covers "the classifier answered", "this ask never qualified" and "this deployment has no classifier" at once, so nothing may render it as reassurance. The guard also pins the division of labour that makes the open set safe — the same word that transits is judged again by the public display reader, which narrows to the availability axis, so a word the engine says it never stamps on this fact renders no sentence while still being visible on the card for triage. A later section pins the split this release introduced on the deny close-out frame. Until now every denied tool call was stamped with the same sentence — the one that says *the user* does not want to proceed — including the calls denied automatically on a lane that has no approval surface at all, where nobody was ever asked. The guard drives all three shapes (a person pressed No, a rule settled it, nobody said which) through both close-out arms and the durable park leg, and pins that the third shape is byte-identical to the previous release: an attribution nobody supplied is not evidence for either answer. The rule-settled shape carries the shell's own reason on a second line when there is one and stands alone when there is not, because a blank line where a reason should be reads worse than no line at all. The attribution is read from own data properties only, so neither a polluted prototype nor a getter can make an automatic denial claim a person made it — and the getter case is pinned to never run at all. The transcript classification word is minted only on the two paths where the upstream transcript format really carries one; the three classifier words and the two abort words are left absent, with the abort words pinned against the strings this package actually normalises interruptions to, which are different strings. A final section pins the decide-operation observer: one `start` in the same tick as the first request and exactly one `end` after the last attempt has settled, across success, retried timeouts, exhausted transient failures, semantic refusal, binding mismatch and both kinds of caller abort, with nothing between retries; an observer that throws or rejects — even when logging that fault fails — never changes what is sent or returned, and the stream-level dependency reaches both durable park legs. A package-internal re-delivery of the same decision (plain approve after an older server rejects the session-scope flag, or a re-send without the attribution key) is reported as one operation with a single start and end. An operation handle only groups sends for the same session and bound call — a send for another gate through the same handle is its own operation — and closing a handle while a send is still in flight defers the end until that send settles. When a person picks "allow for this session" on a parked card and the grant is known not to have been stored — the server answers so, which newer servers do for the gates they can recognise from the parked row as needing a person each time, or the server refuses the session-wide grant with one of the refusal codes that are fixed by the row or the deployment and the package falls back to a plain approval — the parked path now says so with the same line the live path uses, and the receipt carries the server's bit for hosts that call that path directly; an answer without the bit, or any other failure — including a conflict that an internal retry can hit after an earlier send already stored the grant — is treated as unknown and says nothing, a plain approval that never asked for the grant says nothing, and a host logger that throws after a successful decision, on either card path or in the bridge's retry step, can no longer turn it into a second send or a failure. From 0.83.0 it also pins approval cards whose arguments are not the tool's real input: a live frame whose arguments were omitted over the size cap or never sent (the card holds at most a path recovered from the question), and a parked approval whose stored input is missing or replaced by the upstream size marker. On every such card an edited approval sends nothing at all — no respond, no decide, no session-grant attempt — ends as edit-refused, raises the same notice once and flags the card so hosts do not offer editing; both production entry points are driven end to end, and the parked path neither mistakes it for an already-decided gate nor reconnects. The explanatory line is pinned word for word in its four forms: a recovered path is the only thing it claims to have recovered, an unrecoverable one says nothing was recovered, neither claims a reconstructed diff, and the parked forms each say which of the two gaps it is, with near-miss shapes of the size marker still rendered as ordinary input. Real input on the stream, the frame or the record keeps edits flowing, and plain approvals and denials are untouched. Since 0.83.2 the live frame's `mandate` word reaches the card only when it is one of the six known words, read back identically by `readApprovalMandate`; it never adds the separate mandated key to the card, but a known word on its own makes `approvalIsMandated` answer true, because the engine only sends the word as the whole reason for that bit. The frame's `origin` is read from its own keys only, so a value inherited through the prototype chain never reaches the card. The already-resolved verdict a decision leg uses (`isAlreadyResolvedFailure`: the closed failure code first; codes that say the gate is still standing or failed to start never fall through to the wording check; the moved-gate codes by code; then the four-word wording fallback) is exported from the root entry as the same function the package calls, and a malformed or hostile input answers `false` without throwing, since a `true` would let a caller silently re-attach. The same section compares it, input by input, with the copy a client kept of the same combination, and requires every disagreement to come from one of the four extra checks that copy adds. It also replays the documented replacement order (the closed failure code first, answered by this function; then the copy's extra checks; then this function) and requires it to reproduce the copy's answers on every input, while moving the extra checks ahead of the closed code must disagree somewhere. A refusal that says the stored form of a parked gate cannot be continued while the gate is still pending is classified by its code and its `retriable` bit as a gate that is still standing, so failure text that happens to contain words from the wording fallback no longer makes the leg re-attach; both decision legs carry the bit on their failure outcome. The judge of whether a remember request was refused by the server specifically is a root export — it answers the matching code for the five dedicated codes and nothing otherwise, never throws, and agrees with the durable approval leg code for code. |
363
+ | `scripts/run-hitl-gate-honesty-test.mjs` | [2393] the four HITL disciplines that a passing type-check cannot see. (1) The park predicate and the `tool_end` predicate must cover the **same** set — the park side admits a first-class `kind:'tool_approval'` gate for *any* tool name, and a `tool_end` frame carries no `kind`, so the frame-level judge falls back to the engine's exact abort marker; otherwise the poison frame hits the transcript and `markEnded` swallows the real replayed result (the same replay-swallowing disease as on shell gates, reopened on kind-only gates). (2) The already-decided identity criterion is **one-shot**: its two inputs are monotonic, so without consumption one successful decide makes every later park failure — including a real `approvals.list` outage — read as "already resolved" until the 24-hop budget runs out and reports a cause that has nothing to do with what happened. (3) A `plan_review` card dismissed without an answer must be re-presentable: the idempotent re-arm short-circuit re-publishes the still-armed card, and a stale armed id (responder gone) re-arms from scratch rather than presenting a card nobody can answer. (4) `HitlSafetyError` is a safety signal — the `remember` fallback arm must re-raise it instead of auto-retrying the decide, while a plain unknown-key 400 still falls back. (5) The polling leg reschedules after an escaping throw and flips `mode()` to `idle` once it consistently fails, so the honesty surface stops reporting a dead feed as live. (6) The live-frame leg carries the fact behind "you are being asked because the auto-mode classifier could not run" all the way to the card port. Transit narrows on SHAPE only — a non-empty cause string is taken verbatim, an open set, because the word table's owner is the engine and re-checking a closed table at the package boundary would drop a legal value the day a new cause word appears, which is exactly the information worth keeping. A malformed carrier degrades to absence rather than half-minting, and absence stays absence: it covers "the classifier answered", "this ask never qualified" and "this deployment has no classifier" at once, so nothing may render it as reassurance. The guard also pins the division of labour that makes the open set safe — the same word that transits is judged again by the public display reader, which narrows to the availability axis, so a word the engine says it never stamps on this fact renders no sentence while still being visible on the card for triage. A later section pins the split this release introduced on the deny close-out frame. Until now every denied tool call was stamped with the same sentence — the one that says *the user* does not want to proceed — including the calls denied automatically on a lane that has no approval surface at all, where nobody was ever asked. The guard drives all three shapes (a person pressed No, a rule settled it, nobody said which) through both close-out arms and the durable park leg, and pins that the third shape is byte-identical to the previous release: an attribution nobody supplied is not evidence for either answer. The rule-settled shape carries the shell's own reason on a second line when there is one and stands alone when there is not, because a blank line where a reason should be reads worse than no line at all. The attribution is read from own data properties only, so neither a polluted prototype nor a getter can make an automatic denial claim a person made it — and the getter case is pinned to never run at all. The transcript classification word is minted only on the two paths where the upstream transcript format really carries one; the three classifier words and the two abort words are left absent, with the abort words pinned against the strings this package actually normalises interruptions to, which are different strings. A final section pins the decide-operation observer: one `start` in the same tick as the first request and exactly one `end` after the last attempt has settled, across success, retried timeouts, exhausted transient failures, semantic refusal, binding mismatch and both kinds of caller abort, with nothing between retries; an observer that throws or rejects — even when logging that fault fails — never changes what is sent or returned, and the stream-level dependency reaches both durable park legs. A package-internal re-delivery of the same decision (plain approve after an older server rejects the session-scope flag, or a re-send without the attribution key) is reported as one operation with a single start and end. An operation handle only groups sends for the same session and bound call — a send for another gate through the same handle is its own operation — and closing a handle while a send is still in flight defers the end until that send settles. When a person picks "allow for this session" on a parked card and the grant is known not to have been stored — the server answers so, which newer servers do for the gates they can recognise from the parked row as needing a person each time, or the server refuses the session-wide grant with one of the refusal codes that are fixed by the row or the deployment and the package falls back to a plain approval — the parked path now says so with the same line the live path uses, and the receipt carries the server's bit for hosts that call that path directly; an answer without the bit, or any other failure — including a conflict that an internal retry can hit after an earlier send already stored the grant — is treated as unknown and says nothing, a plain approval that never asked for the grant says nothing, and a host logger that throws after a successful decision, on either card path or in the bridge's retry step, can no longer turn it into a second send or a failure. From 0.83.0 it also pins approval cards whose arguments are not the tool's real input: a live frame whose arguments were omitted over the size cap or never sent (the card holds at most a path recovered from the question), and a parked approval whose stored input is missing or replaced by the upstream size marker. On every such card an edited approval sends nothing at all — no respond, no decide, no session-grant attempt — ends as edit-refused, raises the same notice once and flags the card so hosts do not offer editing; both production entry points are driven end to end, and the parked path neither mistakes it for an already-decided gate nor reconnects. The explanatory line is pinned word for word in its four forms: a recovered path is the only thing it claims to have recovered, an unrecoverable one says nothing was recovered, neither claims a reconstructed diff, and the parked forms each say which of the two gaps it is, with near-miss shapes of the size marker still rendered as ordinary input. Real input on the stream, the frame or the record keeps edits flowing, and plain approvals and denials are untouched. Since 0.83.2 the live frame's `mandate` word reaches the card only when it is one of the six known words, read back identically by `readApprovalMandate`; it never adds the separate mandated key to the card, but a known word on its own makes `approvalIsMandated` answer true, because the engine only sends the word as the whole reason for that bit. The frame's `origin` is read from its own keys only, so a value inherited through the prototype chain never reaches the card. The already-resolved verdict a decision leg uses (`isAlreadyResolvedFailure`: the closed failure code first; codes that say the gate is still standing or failed to start never fall through to the wording check; the moved-gate codes by code; then the four-word wording fallback) is exported from the root entry as the same function the package calls, and a malformed or hostile input answers `false` without throwing, since a `true` would let a caller silently re-attach. The same section compares it, input by input, with the copy a client kept of the same combination, and requires every disagreement to come from one of the four extra checks that copy adds. It also replays the documented replacement order (the closed failure code first, answered by this function; then the copy's extra checks; then this function) and requires it to reproduce the copy's answers on every input, while moving the extra checks ahead of the closed code must disagree somewhere. A refusal that says the stored form of a parked gate cannot be continued while the gate is still pending is classified by its code and its `retriable` bit as a gate that is still standing, so failure text that happens to contain words from the wording fallback no longer makes the leg re-attach; both decision legs carry the bit on their failure outcome. The judge of whether a remember request was refused by the server specifically is a root export — it answers the matching code for the five dedicated codes and nothing otherwise, never throws, and agrees with the durable approval leg code for code. A parked question card reads its questions from the pending row first and from the event stream only as a fallback, and the fallback frame carries an additive marker; with neither source holding a presentable question no card is shown. The fallback form of a parked approval card (row input unusable, stream copy shown) is driven end to end through the production entry as well: an edited approval sends nothing and the leg does not reconnect. |
364
364
  | `scripts/run-park-hop-progress-test.mjs` | L-80: the park re-attach loop budgets **stalled** rounds, not parks. A turn where the model keeps hitting gates and every one of them is really decided (a card was answered, the engine really moved on) must never be cut off by the hop budget — the budget counts consecutive rounds that produced no progress, and "the engine revived and immediately parked again on the same coordinates" is not progress. The three non-progress arms (already-resolved, decide-transport-exhausted, and a re-scan that was adopted but led nowhere) share one same-cause limit instead of one arm having a limit and the others having none, and every non-progress re-attach is announced once through the host callback rather than only to the debug log. When the limit is spent the resolver reads the approval queue once more and puts whatever is decidable in front of the user before it gives up; only when there is genuinely nothing to show does it fail soft, and the terminal message then carries the real cause and a real way out instead of a sentence about a budget. On the self-heal side, a reopen verdict that reports `decidedWithoutCard` — the chain settled the gate by rule, so there was no card to present — is progress, not a reopen failure, and the user is not told their message was NOT sent. Negative control: a genuinely empty queue with a run that never moves still fails soft |
365
365
  | `scripts/run-notif-fleet-honesty-test.mjs` | [2393] the five notification/fleet disciplines a green type-check cannot see, each proven by reverting the fix. (1) The workflow-side dedup `return` keeps a count and a trace — without it "suppressed by design" and "a real completion swallowed because the runId minting changed" are the same observation. (2) `seq` normalisation has exactly one mint point, so a 0-based or fractional wire `seq` cannot make the watcher lane and the frame lane key the same completion differently (which would feed the model twice). (3) The TTL sweep defers to a probe arm that is still inside its own deadline — an entry recorded as "abandoned" must not be delivered a moment later — while an arm that has outlived its deadline never blocks the sweep, so the headless exit gate keeps its liveness. (4) The reset hook really clears every ledger it claims to (the sticky `prompt` ledger leaked across cases). (5) The fleet ledger counts all three drop paths (malformed / unknown frame type / isolation drop), and the panel projection's settled recycling is anchored on the settle instant and skips still-present rows, so the dedup token is never carried off with the entry (which would re-emit `end`) |
366
366
  | `scripts/run-public-surface-test.mjs` | The outward promises: the npm export surface baseline (an **exact set**, both directions — a new export that never entered the baseline is one nobody watched leave, and deleting it later would not be red), the peer floor witness, and this README's claims. The floor witnesses are a list since 0.90.0 (the installed SDK plus the 15.x and 16.0.0 probe trees); every listed SDK is checked for the value-level symbols and the cost field, and a missing 16.0.0 witness is reported as a skipped section rather than as green. |
@@ -371,7 +371,7 @@ guard still cross-checks the table by name).
371
371
  | `scripts/run-client-core-singleton-test.mjs` | Module-level singletons ⇄ `docs/refactor/p1-scan/singleton-manifest.json`, **both directions**: an unregistered singleton is red (registering it forces someone to answer "what if this got duplicated"), a stale entry is red, and the `dupRisk: high` count only goes down |
372
372
  | `scripts/run-catalog-loader-gates-test.mjs` | The model-catalog candidate chain (`loadCatalogWithSources`) and the provider device-code seam: offline ⇒ `bundled` with an honest `online.reason`, a good source ⇒ `online` plus a cache write, a second offline run ⇒ `cacheHit`; the three hostile source shapes (malformed JSON, `schemaVersion: 99`, off-domain `http`) each fall through to the bundled table, and an off-allowlist target is **never dialled** — including a `302` to another host, proven by a real loopback server's hit counter staying at zero; a one-byte edit to `catalog.sha256` drops that source while an unavailable sidecar only warns; and the device-code poller's `pending → ok` / `expired` arms run against a real loopback HTTP server with an injected clock |
373
373
  | `scripts/run-abortable-sleep-test.mjs` | The shared `abortableSleep(ms, signal)` leaf (consumed by `workflowClient.ts` and `agentSession/backgroundView.ts`'s poll backoff): normal timeout resolution, immediate wake-up on `abort` mid-wait, `clearTimeout` really firing on that path, and a post-resolve late abort staying a no-op. Since 0.91.0 it is exported from the root entry, and the guard pins that the root export is the very function the package's own loops call rather than a second copy |
374
- | `scripts/run-durable-card-display-keys-test.mjs` | The durable approval row's two display keys survive the row→card recast in `surfaceFsApprovalAndDecide`: `governanceForced` stamps on strict `true` only (absence is "no evidence", never `false`), the row's rule offers (`ruleOffers`; a row that carries only the retired `ruleSuggestions` key yields no offers) pass through the same shape-narrowing reader as the live-frame leg and land on the **read-only** card key `ruleOffersReadOnly` — plus a standing pin that the durable leg never stamps the redeemable `ruleOffers` card position (the `/decide` body has no rule slot; offering a "don't ask again" option there would be an affordance nothing can honour), and a section for the parked twin of the classifier-unavailable fact: the upstream declares that key on the parked action itself, verbatim and under the same name as the synchronous ask, so this leg reads it rather than guessing a carrier name the way the deliberately unprojected keys must. The guard drives both legs with the same cause and asserts the card ends up byte-identical either way — the observable consequence of one reader serving two key paths, and the thing that silently diverges the day someone writes a second copy. Its own reach is printed rather than implied: what is proven is the package-boundary promise "on the row ⇒ on the card", not that today's engine flattens that key onto the pending row. A further section covers the two display facts the recast had been dropping for far longer. One of them the row has carried all along under a DIFFERENT NAME than the live frame uses — the frame puts it at the top level, the row nests it under the risk descriptor — and that difference in name is exactly why it went unnoticed; unlike the keys this leg deliberately refuses to project, its carrier is witnessed in the engine's own artefact rather than guessed. Neither is decoration: the shell's stand-aside arm reads them, so a call that matched a remembered allow rule which could NOT silence it looked like an ordinary ask on the durable path and was auto-approved with no card at all. Both land on the SAME card slot the live leg uses (one shape for the ends), verbatim bytes, present only when non-blank, never folded into an empty string — and the guard pins the discipline in both directions, including that a top-level key the upstream row does not actually have must still not grow this position. The security-class approval bit (`requiresRealApproval`) rides a parked row's card when the row carries it at top level, on strict `true` only, while look-alike nested carriers are ignored; this is pinned with a constructed row, because today's pending list does not carry the bit yet. A second, separate bit (`irreversibleParkGate`) marks a parked card whose row sits on the irreversible-ask gate kind — a gate-kind fact that covers asks the engine flagged for real approval at the first decision plus safety-tightened gates, with a known engine gap for approval demands raised only on a storage recheck — on the park path only, on the exact gate word only, and never in place of the real bit. Since 0.83.2 the recast also carries the row's ask origin (`origin`) exactly as the live-frame leg does — a non-empty string, verbatim, open vocabulary, never invented or defaulted — and the word a mandated question stands on (`mandate`), accepted only when it is one of the six known words and read back through `readApprovalMandate`; a word outside that set, or a malformed value, leaves the card without it. The word never adds the separate mandated key to the card, but a known word on its own makes `approvalIsMandated` answer true, because the engine only sends the word as the whole reason for that bit. An `origin` inherited through the row's prototype chain is not read, and since 0.83.4 neither is an inherited `mandated` on either leg — both legs stamp it from an own strict `true`, the same reading the seat crossing uses; the single judge `approvalIsMandated` reads `mandated` and `ruleOffersAbsence` the same way, so an inherited key no longer makes it answer true. |
374
+ | `scripts/run-durable-card-display-keys-test.mjs` | The durable approval row's two display keys survive the row→card recast in `surfaceFsApprovalAndDecide`: `governanceForced` stamps on strict `true` only (absence is "no evidence", never `false`), the row's rule offers (`ruleOffers`; a row that carries only the retired `ruleSuggestions` key yields no offers) pass through the same shape-narrowing reader as the live-frame leg and land on the **read-only** card key `ruleOffersReadOnly` — plus a standing pin that the durable leg never stamps the redeemable `ruleOffers` card position (the `/decide` body has no rule slot; offering a "don't ask again" option there would be an affordance nothing can honour), and a section for the parked twin of the classifier-unavailable fact: the upstream declares that key on the parked action itself, verbatim and under the same name as the synchronous ask, so this leg reads it rather than guessing a carrier name the way the deliberately unprojected keys must. The guard drives both legs with the same cause and asserts the card ends up byte-identical either way — the observable consequence of one reader serving two key paths, and the thing that silently diverges the day someone writes a second copy. Its own reach is printed rather than implied: what is proven is the package-boundary promise "on the row ⇒ on the card", not that today's engine flattens that key onto the pending row. A further section covers the two display facts the recast had been dropping for far longer. One of them the row has carried all along under a DIFFERENT NAME than the live frame uses — the frame puts it at the top level, the row nests it under the risk descriptor — and that difference in name is exactly why it went unnoticed; unlike the keys this leg deliberately refuses to project, its carrier is witnessed in the engine's own artefact rather than guessed. Neither is decoration: the shell's stand-aside arm reads them, so a call that matched a remembered allow rule which could NOT silence it looked like an ordinary ask on the durable path and was auto-approved with no card at all. Both land on the SAME card slot the live leg uses (one shape for the ends), verbatim bytes, present only when non-blank, never folded into an empty string — and the guard pins the discipline in both directions, including that a top-level key the upstream row does not actually have must still not grow this position. The security-class approval bit (`requiresRealApproval`) rides a parked row's card when the row carries it at top level, on strict `true` only, while look-alike nested carriers are ignored; this is pinned with a constructed row, because today's pending list does not carry the bit yet. A second, separate bit (`irreversibleParkGate`) marks a parked card whose row sits on the irreversible-ask gate kind — a gate-kind fact that covers asks the engine flagged for real approval at the first decision plus safety-tightened gates, with a known engine gap for approval demands raised only on a storage recheck — on the park path only, on the exact gate word only, and never in place of the real bit. Since 0.83.2 the recast also carries the row's ask origin (`origin`) exactly as the live-frame leg does — a non-empty string, verbatim, open vocabulary, never invented or defaulted — and the word a mandated question stands on (`mandate`), accepted only when it is one of the six known words and read back through `readApprovalMandate`; a word outside that set, or a malformed value, leaves the card without it. The word never adds the separate mandated key to the card, but a known word on its own makes `approvalIsMandated` answer true, because the engine only sends the word as the whole reason for that bit. An `origin` inherited through the row's prototype chain is not read, and since 0.83.4 neither is an inherited `mandated` on either leg — both legs stamp it from an own strict `true`, the same reading the seat crossing uses; the single judge `approvalIsMandated` reads `mandated` and `ruleOffersAbsence` the same way, so an inherited key no longer makes it answer true. Since 0.110.0 the durable path does not carry `requestedBy` either at the row top level or inside `riskDescriptor`; the same data on a live frame does reach the card. |
375
375
  | `scripts/run-session-memory-status-test.mjs` | The session **memory-status** read face (S-53): the two judgements three clients would otherwise each get wrong. First, *same status, different code* — this route's 404 carries two unrelated meanings (`not_found.session` = unknown or non-owned session; `not_found.route` = a pre-7.53 server that has no such route at all), so dispatching on the **status** would report "your deployment lacks this surface" as "your session does not exist". The verdict is anchored on `errorCode`, the two 404s are pinned to **different** verdicts, and — the load-bearing negative control — a 404 carrying **no** code falls to `failed` rather than guessing either way, since a wrong guess in either direction is a false statement a user would act on. 501 is allowed a codeless fallback because both of its arms mean the same thing here, and `capability.*` stays split from `feature.*` because those two share a status while their dispositions are opposite. Second, *absence means something different per key*: `optOutSource` and `lastCaptureAt` are legitimately absent on a **healthy** session (a zero-history session really is `{captureOptedOut:false, committedCount:0, foldedCount:0}` with no degradation at all), so reading absence as "off/none/0" asserts something unprovable. Two combined readers are pinned: capture opt-out is read from **both** its keys (a record-store fault yields `indeterminate`, never `active` — the difference between "your conversation is being remembered" and "nobody knows"), and last-capture is a **three-state** read whose discriminator is the *other* key, because `lastCaptureAt`'s absence alone covers both "ledger unreadable" and "genuinely no contributions" and therefore decides nothing; the two shapes are pinned to different verdicts so a single-key read turns red. The thin wrapper is the only IO: it never throws, drops malformed keys to absence rather than trusting them (an unreadable value must answer "don't know", never render as truth), refuses to spend a request on an empty `sessionId`, and passes `signal` through untouched |
376
376
  | `scripts/run-crash-converged-projection-test.mjs` | The `crashConverged` read face on `GET /v1/approvals` (L-38): what the *previous life* of a crashed local engine left behind, projected for every client. Three judgements are pinned. First, **absence is not an empty list** — a missing key (an older server, deps not present, or a carrier that is not an array at all) returns `undefined`, and the client renders nothing; an empty array returns a present zero-count object, which is the server actually saying "none". Folding the first into `{total:0}` would have the client assert "nothing was left behind" on a surface a person uses to decide whether it is safe to re-run something — the worst possible direction for a false statement — so the two cases are pinned to different **return shapes** and a test asserts the two verdicts are unequal. Second, bucketing is a **four-term conjunction**: `orphanState === 'pending'` *and* `resumeSafe === true` *and* both approval-evidence keys (`originalDecision`, `decidedAtMs`) absent. A fifth term rejects any row carrying an **accessor**, and accessors are never invoked at all — reading one means synchronously running someone else's code, and `catch` catches throwing, not *never returning*, so a looping getter would pin the startup thread forever (the row cap does nothing against that shape). The same rule covers the three untrusted reads outside the row as well — the envelope's `crashConverged` key, the carrier's `length`, and every numeric index are read as own property *descriptors* and only data descriptors are used, so accessors and prototype entries read as absent and are never invoked. Such a key is treated as absent: if it was a required field the row is counted as dropped, if it was optional or additive the row survives without it. That also closes the ordering attack, since spreading runs getters in property order and an earlier one could `delete` the approval evidence before it is ever copied (measured before the fix: such a row reached the resume-safe bucket), and the check therefore moves ahead of the read, onto the property descriptors — from which the snapshot is then built directly, because checking descriptors and *then* spreading is two independent observations of the same row, and a non-throwing proxy can make the two `ownKeys` calls disagree (first showing `originalDecision: 'approve'` so the row reads as plain data, then omitting that configurable key so the snapshot loses the evidence; measured before the fix: the dangerous row reached the resume-safe bucket after exactly two enumerations, and after it, one). Keys are written with `Object.defineProperty` rather than plain assignment, because `'__proto__'` is a legal own enumerable key and `o['__proto__'] = x` does not store a value — it calls the prototype setter, letting a row whose own properties are all plain data (so the accessor gate never fires) inject a prototype whose `sessionId` getter deletes the approval evidence from the snapshot during validation; `defineProperty` fires no setter, so the key survives as ordinary additive data and the snapshot keeps `Object.prototype`. A row that simply arrives with a custom prototype is treated the same way, since the snapshot only enumerates own properties: approval evidence sitting on the prototype would never reach it, and a perfectly ordinary object with no proxy and no accessors could otherwise be called safe to re-run — real bodies come from `JSON.parse` and always carry `Object.prototype`, so nothing genuine trips it). Validation itself runs on a **null-prototype** dictionary and the bucketing verdict is carried out of that same pass rather than re-read from the delivered row, because every property lookup on an ordinary `{}` reaches `Object.prototype`: a polluted `sessionId` getter there would delete the approval evidence from the snapshot mid-validation and send the row to the safe bucket (measured before the fix). The row handed to the client is still an ordinary object — the null prototype is an implementation detail of the check, not of the value) — real JSON bodies are all data properties, so only a middle-layer-synthesised payload ever trips it, and it too lands in the human bucket rather than being dropped. The `decided` arm means the human had already approved and side effects may be half-landed, so it always goes to the human bucket, as does `resumeSafe === false` and — the last two terms — any row whose own fields contradict each other, since `pending` claims nothing ran while that evidence says somebody pressed approve. Deciding "not safe" costs one extra question (recoverable); deciding "safe" wrongly has somebody re-run work that already partly happened (not). A 2x2 truth table pins that exactly one cell is resume-safe, so reading either key alone turns red, and the contradictory rows are routed to the human bucket rather than dropped — they are real orphans, and the ones most worth showing. Third, unreadable rows are **dropped and counted**, never thrown and never passed through: the product is declared as `CrashConvergedRow`, so letting a row missing a required field — or carrying one of the wrong type — past would be a lie at the type level, and the closed literal discriminators (`decision` / `cause` / `orphanState`) decide family membership rather than being an open vocabulary. The measuring stick stops at the **type** floor, though: degenerate-but-well-typed values (`ts: NaN`, an empty `toolName`) are kept, because swallowing a real orphan over a decorative field is the worse direction, and the one deliberate exception is `approvalId`, which must be non-empty to be a row identity at all. `dropped` is kept separate from `total` so unreadable rows never inflate "N approvals were affected"; each row is a **one-shot snapshot** — every own enumerable key is read exactly once, and validation, bucketing and the handed-back value all read that same snapshot, so additive upstream keys survive while a **non-idempotent** getter (one that never throws, just answers differently on a second read) can no longer erase the approval evidence between the check and the bucketing (measured before the fix: such a row landed in the resume-safe bucket while its checked value was `"approve"`). Hostile carriers are counted rather than allowed to reject: **every** touch of the carrier is guarded — envelope property reads, `Array.isArray` itself (it throws on a revoked proxy), the `length` read, each indexed read and each row's property reads — and a traversal that dies halfway returns absence rather than a half-counted total. A row that cannot be read never takes the batch with it: its own shape check is inside its own guard, so one revoked-proxy row costs a `dropped` tick rather than collapsing the whole projection to absence — which a client would have read as "this deployment does not offer the surface". Traversal goes by **numeric index, never the carrier's own iterator protocol**, because `for...of` hands the carrier the question of which rows exist: an array carrying an overridden `Symbol.iterator` can yield nothing (measured before the fix: a real orphan became `{total:0}`, which a client reads as "the server said there are none") or swap a dangerous `decided` row for a safe-looking one (measured: `fake-safe` was returned in place of `real-danger`). Row count is capped at 100000 and the cap is checked **before** the walk: requiring only a non-negative integer `length` does not stop a proxy trap reporting a billion, and this surface runs on the startup / `--resume` path, where a synchronous spin freezes the thread (measured before the cap: twenty million rows took 18.3 seconds and twenty million index reads; a billion does not come back). The honest boundary is stated rather than overclaimed — a proxy can still lie in its `length` or index traps, which is the same thing as a host injecting a lying transport — and the widening of `ApprovalsResourceLike.list()` is proven **additive** by really running tsc over a legacy `{ pending }` mock *and* over the real `AgentClient` path — the projector takes `unknown` precisely because a parameter shaped as "an object with an optional `crashConverged`" is a TypeScript weak type that the installed SDK's own `list()` return shape shares no property with, which only a real-client compile would have caught — with a known-red control so a clean run means the checker spoke Every row in the `needsHuman` bucket also carries a closed-set `needsHumanReason` (`approved_then_interrupted` / `pending_unsafe` / `unstable_row` / `contradictory`) derived from the same single read that bucketed it; `resumeSafe` rows never carry it, a same-named key on the supplied row is overwritten or removed, and the bucketing itself is unchanged. The `cause` discriminant is a closed set of two words — a crash, or an orderly shutdown that cut an unfinished step off — and neither the bucket nor the reason ever reads it: every combination buckets identically under both words, a word outside the set still drops the row, and the row reaches the client with its word unchanged and no sentence attached. An exhaustive switch over `cause` that only knows the older word fails to compile, by design. When server fixtures are present, rows minted by the server's own audit store are projected for both generations: the newer one yields the shutdown rows and an untracked step's pending orphan that is no longer marked safe to re-run, the older one yields neither. The one line a person reads about those leftovers is minted here too, by a single speaker: `crashConvergedNotice(projection)` takes the projection as it is (a host that drops rows it has already disclosed passes the two buckets back with `total` recomputed) and produces one English line — how many approvals were still waiting with the recorded cause codes repeated verbatim, then either one sentence per needs-human reason in a fixed order (`NEEDS_HUMAN_REASON_ORDER`, deliberately not the union's declaration order, which is the bucketing priority) with a fallback sentence last and the "check them by hand" closing said exactly once, or the "based on what was recorded" re-run sentence, then "sema has not re-run anything" and a note for unreadable records. It never re-judges a row (bucket membership is taken from the projection as given), it says nothing for an absent, empty or unreadable projection — including one whose buckets do not add up to its `total`, so a host that filtered rows without recounting gets silence rather than self-contradicting numbers — and a row whose reason cannot be read only falls into the fallback sentence. `crashConvergedNeedsHumanSentence(reason, n, total)` gives the per-reason sentence, or `undefined` for anything outside the four words or a bad count. The order constant is pinned at compile time to be a permutation of the reason union, and the pin is proven to sit on the thing it guards: the guard edits the source in memory and requires the line of the matching pin to fail when a member is removed from either side or duplicated. When the terminal's source tree is present, the same 9432 projections also run through the terminal's own sentence table and assembly, extracted by syntax tree, and must match byte for byte. |
377
377
  | `scripts/run-self-orchestration-denial-test.mjs` | The three judgements behind a **denied self-orchestration request** (server 7.57.0), each of which all three clients would otherwise get wrong on their own. First, whether to retry at all is a **conjunction that may not be loosened**: HTTP 501 *and* an `errorCode` that is **exactly** `capability.self_orchestration_required`. That code shares its shape with every other `capability.*` 501, so dispatching on the prefix would drag "some other capability is not wired up" into the retry arm — those requests do not become acceptable once the two keys are gone, so the client would spend a request and then tell the user the wrong reason. Negative controls cover all four directions: a sibling `capability.*` code, a truncated or suffixed variant of the right one, a codeless 501 (it decides nothing, so it decides nothing — no guessing), and the right code under 500 / 400 / 503 or a string `"501"`. The classifier reads structurally rather than by `instanceof` (a host may inject its own transport; across realms or duplicate SDK instances an understandable error would read as unreadable), so a class instance, a bare `{status, errorCode}` literal and an error carrying those fields on its **prototype** all reach the same verdict — and a hostile proxy or a throwing getter yields `null` instead of throwing, because this classifier runs inside a `catch` block where anything it throws escapes the caller's own guard. Second, removing the intent is a **structural** operation, not wording: `selfOrchestration` sits at the top level while `ultracode` sits under `settings` — two different stamping legs — and a client hand-writing `delete` will miss the second one, which costs the user the same failure twice. The single stripper is pinned to touch exactly those two: other `settings` sub-keys and their values survive byte for byte, `deferTools` is left alone (pulling `Workflow` out would be a behaviour change, not a removal of intent), additive unknown keys survive at both levels, the input object is never mutated, `settings` is only dropped entirely when `ultracode` was really there and nothing else remains (an already-empty one is left as is), a non-object `settings` is not touched at all, an `ultracode` that only exists on the prototype does not count, and the whole thing is idempotent. The end-to-end leg runs a real `buildTaskRequest` product through it and asserts the stripped body still passes the registration gate key by key. Third, on the capabilities body, **absence is not "switched off"**: a pre-7.57 server has no `workflowsGate` key at all, so reading absence as "the engine says no" asserts something the server never said, and the mirror-image disease is folding an **unrecognised** `denial` into `null`, which would have the client render "nothing was denied" when the truth is "denied, for a reason I do not recognise". Five shapes are pinned — caps unreadable, gate absent, closed-set member, unknown value, accessor — with the unknown arm carrying the raw token (or an empty one when the value is not even a string) and never collapsing to `null`. All four untrusted reads go through own **data descriptors** only, and the guard pins the getter invocation count at zero, since `catch` catches throwing but not *never returning*; a descriptor trap that throws and a revoked proxy both yield honest absence rather than an exception — though *what* absence means differs by field, and the guard pins that split rather than a blanket rule: an accessor on `workflows`, `workflowsGate` or `engineCan` reads as absent, while an accessor on `denial` reads as `{unknown:''}`, because a key that is **not there** is the gate saying "nothing was denied" whereas a key that is there but cannot be read is "denied, and I could not read why" — folding the second into the first is exactly the false statement this face exists to prevent. Two further pins came out of an adversarial review. The exported retry list is **frozen at runtime**, not merely `as const`: the verdict hands out that same reference, so any consumer splicing it once would poison every later verdict in the process — the guard asserts `Object.isFrozen`, that four different mutation attempts leave it byte-identical, and that a verdict issued *after* those attempts still carries the original two entries. And the classifier reads `denial` only **after** both criteria have passed, since it is not a criterion but an extra field on the verdict: the guard pins the getter invocation count at zero for any error that does not match and at most one for an error that does. The scope line is drawn explicitly rather than overclaimed — "no getter ever runs" holds for `projectWorkflowsGate`, which reads **wire JSON** where every field is an own data property by definition, but not for the classifier, which reads a **thrown value** that may well be an SDK `APIError` class instance carrying `status` and `errorCode` on its prototype; insisting on own data descriptors there would report a perfectly readable error as unreadable, so that side promises only that it never throws. A final pin covers the **integration document's own worked example** rather than the library: the shipped SDK's `tasks.stream()` is an `async` generator, so calling it issues no request at all — the POST happens inside `streamRaw` on the first iteration, and a `try` wrapped around the `stream(...)` call itself can never catch the 501. A client following a submit-shaped recipe on the streaming leg would never run the classifier, and the whole strip-and-retry path would silently do nothing. The guard drives the **real** `TasksResource` against a fake transport, offline, and pins both halves: the synchronous leg is in flight the moment it is called, the streaming leg has issued zero requests after the call and raises on the first `next()` — and it does so through the **real** error path, with `openStream` returning an actual 501 `Response` that the SDK's own `errorFromResponse` turns into the typed error, pinning the `openStream`→`errorFrom` call order so a transport that stops minting `errorCode` cannot pass. The documented recipe is then **executed** rather than keyword-counted: exactly one retry, a second body that really lost both keys while every other setting survives byte for byte, the caller's own request object left untouched, one disclosure and only one, a second 501 propagating with the request count still at two, and — after the first 501 — an abort leaving the count at one with nothing disclosed. A last leg is type-level: `stripSelfOrchestrationIntent` carries an SDK `TaskRequest` overload, because the wide `Record<string, unknown>` form erases the caller's type and the document's "strip and resubmit" line would not compile without an unsafe cast; a real tsc run over a virtual file proves both the narrow and the wide path, with a known-red control — and it compiles the document's two recipes **verbatim**, extracted from the section itself, because a recipe that does not compile is a recipe that was never given: `{ transientOk: true, signal }` is a TS2379 under `exactOptionalPropertyTypes`, which no amount of prose review had caught. The last thing pinned is the one that would have been quietest of all: the SDK's `stream()` returns only on a `done` or `failed` frame, so a stream truncated mid-run — or yielding nothing at all — ends the `for await` just as normally as a completed one. The documented `runOnce` therefore tracks whether it ever saw a terminal frame and raises when it did not, the guard's success fixture emits a real terminal and asserts the handler received it, and a truncated-stream control asserts that shape is reported as a failure with no retry and nothing disclosed. That terminal-frame rule then needed one more turn of its own: the underlying reader returns *normally* when the signal is aborted, so the check as first written rewrote a user's cancellation into a generic stream fault — a client keying off `AbortError` to suppress the error would instead have shown a failure, or resubmitted. Cancellation is therefore checked first, a real-SDK case aborts from inside the handler and asserts the original `AbortError` survives with no retry and nothing disclosed, and the document is checked for that ordering. The harness runs the documented `handle` and `transcript.note` as real spies rather than pushing frames itself, the drive loop rethrows exactly as the document does, and the disclosure ledger is proven to be the caller's own array by a positive identity assertion — without which the cancellation leg's "nothing disclosed" would have been vacuously true. Each recipe is compiled **on its own**, with a preamble that declares only what a host supplies and injects no library symbol, since compiling them together let the second one borrow the first one's imports, and the preamble's own types are decoupled from what the recipes import so the "remove the imports and it must fail" control fails for the right reason — which is checked by attribution, not merely by redness. Ordering is the last thing to get right: the cancellation check must come before the truncation error but **both** must sit behind the terminal-frame test, because a cancellation that lands after the run already reported `done` would otherwise overwrite a real outcome — one that may have already had effects — with "cancelled", and a person reading that will run it again. Aborting from inside `handle(done)` and `handle(failed)` are both pinned to still report success, and the ordering assertion is anchored inside the streaming `runOnce` body rather than the section, since the section's first `throwIfAborted` belongs to the synchronous recipe and would have made a reversed streaming recipe pass — and that ordering check is now anchored on the TypeScript AST rather than on text, since a comment reproducing the two statements in the right order let a genuinely reversed body pass. One more timing fact had to be written into the recipe: a single SSE read buffers several frames and the SDK yields them back to back, so checking the signal only after the loop lets a cancelled run keep consuming the rest of the chunk — measured, an abort inside `handle(turn_start)` still swallowed the `done` that followed and reported success. The recipe therefore re-checks after every non-terminal frame. Finally, the behavioural matrix is no longer run against a copy of the recipe: both recipes are extracted from the document, transpiled, and **executed** with injected host objects, so the disclosure assertion really exercises the document's own `transcript.note(disclose(...))` line, and the synchronous leg gets the same full matrix the streaming one does |
@@ -381,7 +381,7 @@ guard still cross-checks the table by name).
381
381
  | `scripts/run-mods-kernel-transit-test.mjs` | The package's `./mods` subpath entry point, which re-exports the mods kernel package unchanged and keeps it out of the root entry entirely: the kernel is an optional peer dependency, so a host that never imports the subpath installs, type-checks and runs without it, while a host that does import it without the kernel installed fails loudly at load time. The 67 re-exported names (26 values and 41 types, a fixed commitment for the pinned kernel generation; a name the kernel adds later is not forwarded until reviewed) are read from the file the subpath entry actually resolves to; every object and function value must be the **same reference** as the kernel's own, and because reference equality cannot see a locally re-minted number or string, the entry file may contain nothing but named `export … from` statements (a syntax-tree allow-list) and the guard also loads the built entry into a sandbox beside a synthetic kernel whose every primitive has been changed, requiring each re-exported value to follow it. The root entry's whole emitted graph (script and declaration) must carry no reference to the kernel or to the subpath's modules; the subpath's own graph is exactly two files with the kernel as its only outward edge and is disjoint from the root graph; the kernel's emitted code and the entry are free of Node built-ins and Node globals and bundle for a browser. Install behaviour is exercised in a sandbox with a restricted compiler host and real Node loads: root-only consumers type-check with no kernel present, subpath consumers without the kernel get errors naming exactly the kernel, and with the kernel installed the subpath re-exports the kernel's own values. The kernel is pinned exactly as a development dependency (no caret range) and each pinned generation has a column of expected readings: the contract number and a SHA-256 of the canonical event table (drift alarms), three behaviour canaries run through the entry (the chain rule, how a pinned key is judged, which events a surface-side bus accepts), and a same-version pairing check against the engine package's embedded copy (byte-identical sources). The entry's one own export, an identity reader returning the kernel's contract number and the event-table digest, is checked against an independent recomputation over a seeded pool and throws on malformed input. |
382
382
  | `scripts/run-mods-kernel-install-matrix-test.mjs` | The install matrix for the package's optional kernel peer (the mods kernel behind the `./mods` subpath). The kernel is declared as an optional peer dependency, and no package manager guarantees that a host tree ends up with a single copy of an optional peer, so this guard asks the question of the real installers instead of a hand-built sandbox. Four host layouts — the kernel absent, the same-version kernel declared by the host, an out-of-range kernel declared by the host, and the kernel brought in by another dependency — are installed for real with four package managers (npm, pnpm, yarn classic and yarn modern, each at a version pinned in the guard, fetched from the registry and checked against a pinned integrity value and its own reported version), always from a tarball packed from the working tree. In every cell whose installation succeeds, a Node process inside the installed host records what the install and the entry actually did (an installation the package manager refuses leaves no host, so its reading is recorded as a refusal with every host-side field absent): whether the install succeeded and what it said about the kernel peer, whether the root entry and the `./mods` subpath load (a missing kernel must fail loudly and name the kernel), which kernel version the host and the package's own build each resolve to, how many copies of the kernel the whole tree holds, whether the subpath's values are the same references as the host's own import of the kernel, and what the identity reader reports. The expected readings are pinned per kernel generation after a recorded first run, and the first cell that differs fails the guard. The out-of-range kernel is the installed kernel repackaged with only the version field changed (checked byte for byte), and every copy found in every host must carry the real kernel's emitted files. The guard does not claim that a host holds a single kernel; it records what each installer does and fails when that drifts. Without a reachable registry the matrix part is skipped explicitly (never reported green) while the offline parts — materials, the shape of the expected table, and a check that the judge and the probe can tell one copy from two — still run. |
383
383
  | `scripts/run-sdk-wire-transit-test.mjs` | The package's pass-through of a few SDK names (`sdkWireTransit`), pinned: every value re-export is the **same reference** as the SDK's own (a client class the host recognises with `instanceof`, the two approval-frame predicates, the three session-bundle calls and the SDK error class), not a look-alike wrapper — wrapping would discard the one anti-drift guarantee a pass-through has; every type re-export is present by name in the emitted declaration file; the gate's list and the source file's export lists are compared in both directions so a name added to one without the other turns red; and a name the SDK does not export must fail the same test, so the gate is not vacuously green. |
384
- | `scripts/run-rules-side-test.mjs` | The persisted-permission-rules lane's shared decision half. From 0.92.0 the governance surface reads one capability bit, the rules lane: every server at or above the support floor that advertises it also serves the revoke routes, so the second bit is no longer consulted and a lane-only body shows the surface. Failure classification is by **disposition, not cause**: the two 404s (route missing vs. dead ticket) never share a bucket, a 503 `rule_import_retry` means *the ticket is still alive* (the opposite handling of a dead one), and a stale-cursor 400 drops the cursor and re-lists from the top exactly once — never resuming a stale keyset, never surfacing a partial governance list, and never paging past the hard cap. The persist-ack reader is **merged into** `readToolApprovalRespondAck`: the three-state verdict (`persisted` / `refused` / `unknown`) is derived only from an ack that passed the package's structural narrowing, and a half-shaped object such as `{rulePersisted: true}` with no `delivery` reads as `unknown` — the pre-merge shell read would have said `persisted`, which is precisely the double-ledger drift this file closes, so that case is pinned in reverse. The local-allow-rule skeleton pins all five narrowings (whole-tool, tool-name match, literal anchor with the escaped-star counter-example, bare interpreter prefix consulted only for Bash, and the canonical dangerous-pattern overlay) **with their refusal strings byte-for-byte** — the cli's 128-assertion suite anchors the same strings, so a one-character edit here changes observable behaviour on three clients — and asserts the parse is a pure function of its input, because the same call backs both "render the option" and "resolve the selected value" `listAllPersistedRules` needs only `list` (`RulesListFacade`; the other four methods are known to the parameter type as optional members): a synthetic consumer compiled against the built declarations passes a two-method object, a list-only literal, a `Pick` slice, the named type, the full facade, and the full or two-method facade written as an inline object literal — including arrow functions with untyped parameters — all with zero diagnostics, while a misspelled method name in such a literal is still reported. From 0.94.0 a `request.query_invalid` answer is split by whether that request carried a cursor: with a cursor it is still a stale cursor, without one the listing's query itself was refused (`query-refused`, for example a project scope that is not an absolute path), the server's sentence is kept and the listing is not retried. Since 0.95.0 a caller that cannot say whether its request carried a cursor (no second argument, a non-boolean, or a context whose read throws) gets the generic error with the server's sentence instead of a stale-cursor claim: re-listing from the top is useless for a request that carried no cursor, so naming that cause without evidence is withheld. |
384
+ | `scripts/run-rules-side-test.mjs` | The persisted-permission-rules lane's shared decision half. From 0.92.0 the governance surface reads one capability bit, the rules lane: every server at or above the support floor that advertises it also serves the revoke routes, so the second bit is no longer consulted and a lane-only body shows the surface. Failure classification is by **disposition, not cause**: the two 404s (route missing vs. dead ticket) never share a bucket, a 503 `rule_import_retry` means *the ticket is still alive* (the opposite handling of a dead one), and a stale-cursor 400 drops the cursor and re-lists from the top exactly once — never resuming a stale keyset, never surfacing a partial governance list, and never paging past the hard cap. The persist-ack reader is **merged into** `readToolApprovalRespondAck`: the three-state verdict (`persisted` / `refused` / `unknown`) is derived only from an ack that passed the package's structural narrowing, and a half-shaped object such as `{rulePersisted: true}` with no `delivery` reads as `unknown` — the pre-merge shell read would have said `persisted`, which is precisely the double-ledger drift this file closes, so that case is pinned in reverse. The local-allow-rule skeleton pins all five narrowings (whole-tool, tool-name match, literal anchor with the escaped-star counter-example, bare interpreter prefix consulted only for Bash, and the canonical dangerous-pattern overlay) **with their refusal strings byte-for-byte** — the cli's 128-assertion suite anchors the same strings, so a one-character edit here changes observable behaviour on three clients — and asserts the parse is a pure function of its input, because the same call backs both "render the option" and "resolve the selected value" `listAllPersistedRules` needs only `list` (`RulesListFacade`; the other four methods are known to the parameter type as optional members): a synthetic consumer compiled against the built declarations passes a two-method object, a list-only literal, a `Pick` slice, the named type, the full facade, and the full or two-method facade written as an inline object literal — including arrow functions with untyped parameters — all with zero diagnostics, while a misspelled method name in such a literal is still reported. From 0.94.0 a `request.query_invalid` answer is split by whether that request carried a cursor: with a cursor it is still a stale cursor, without one the listing's query itself was refused (`query-refused`, for example a project scope that is not an absolute path), the server's sentence is kept and the listing is not retried. Since 0.95.0 a caller that cannot say whether its request carried a cursor (no second argument, a non-boolean, or a context whose read throws) gets the generic error with the server's sentence instead of a stale-cursor claim: re-listing from the top is useless for a request that carried no cursor, so naming that cause without evidence is withheld. Since 0.109.0 the whole-tool check in `parseLocalAllowRule` is the package's single whole-tool predicate, evaluated on the same object in the same order; the guard compares it with the previous release on plain parse results, proxies that change on read and throwing getters, and fails when the previous release is not supplied. |
385
385
  | `scripts/run-read-credential-refusal-projection-test.mjs` | The read-credential refusal on the engine's pre-gate read routes (workflow reads and the live fleet stream), projected as a named state instead of a silent retry loop or a generic failure. On a deployment that requires a service credential, these reads now answer 401 when the client presents none it accepts, and before this change the workflow monitor kept reporting "still loading" and retried every few seconds forever, while the background view folded the refusal into "unavailable". The guard drives the real monitor through a scripted transport: a 401 with the unauthorized code, or with no code, on either the detail read or the list read, yields the new `unauthorized` state carrying the last error and — when the run had been live — the last snapshot; the loop then waits on a long back-off (the timer values it schedules are recorded) rather than the short one, opens no activity stream, and returns to live once the credential is accepted. A 403, a 407 and the two other 401 codes (a missing principal, an unverifiable signed principal) do not enter this state, and for every other status and for connection errors the outcome is compared, case by case, with the previous judgement frozen inside the guard — a deployment without a service credential never reaches the new state. The background view's fleet source gets the matching `unauthorized` health word for both 401 shapes, keeps its rows out of the view, leaves the assistant source untouched and recovers to `ok`; every other failure shape is again compared with the frozen previous judgement, and since 0.95.0 the assistant source's own 401 gets the same word from the same judge in the same order (a surface that is not mounted at all is still reported as not configured first, and a capability probe that itself answers 401 does not rewrite the verdict), with both sources answering identically for any one thrown error. The workflow monitor's status keeps the HTTP status code only for the two degrades the engine states itself (501 and 404) and null for every other failure, so that whether the credential was accepted has exactly one reading, the monitor's resolution. One exported sentence serves both places: it says the engine requires a credential the client did not present, and never that there is nothing to show or that the engine is down. |
386
386
  | `scripts/run-registry-quota-usage-test.mjs` | The projection of the cloud control plane's quota reading, and the three different things a missing number can mean there. This response says `null` in two places and means something different each time: no token quota is configured for this principal on this instance, and this window has no cap at all. Both are **facts the server is asserting**, not gaps in the reading — while a key that is absent or carries the wrong type is a genuine gap. All three have to survive to the screen separately, because folding them is how a user ends up staring at a confident `0`: an uncapped window rendered as if nothing were left, or a deployment that simply never configured quotas rendered as if the quota were exhausted. The complaint that started this was the opposite direction — a centrally configured quota that the command line could not see at all — so the reading also refuses to let an unreadable response masquerade as "no quota configured". Two fields deliberately do not share one signal: whether a window is exhausted and whether there is a recovery time, since the recovery time is only ever populated in the exhausted case and reading its absence as "not exhausted" would answer a question the response never answered. Counts that the server always provides are narrowed no further than the mint: a used counter has no uncapped state, so a null there is unreadable rather than zero. The wording helper carries the only human-facing phrasing, and the sentences for "no cap" and "unknown" are checked to contain no digits at all |
387
387
  | `scripts/run-engine-directory-test.mjs` | The read faces for the cloud control plane's two engine endpoints: the directory of engines visible in a scope, and the "my engine" resolution a client asks for right after login. Four things are guarded because each one is a way to be confidently wrong. An unreadable response is not an empty directory: only an explicit empty list means "no engines here", and a body that is missing the list, or is not an object at all, voids the reading rather than reporting zero. A row that cannot be read is dropped without taking its neighbours with it, but the roster then says how many rows it could not read, so nobody treats it as complete. The relay path on a row is the one string the client appends to the center's origin and sends the user's token to, so it is accepted only as a same-origin relative path — an absolute URL, a protocol-relative form, one that only becomes protocol-relative once the URL parser has eaten a tab or newline, or one with a dot segment (plain or percent-encoded) that the parser would fold into a different path makes the whole row unreadable instead of being cleaned up and let through. And the words the server may add later are read as an open set: an unlisted kind, posture or resolution rule is passed through verbatim rather than thrown on or folded into a known word, while a missing online flag stays "not reported" instead of becoming "offline". The decision on top of the reading never guesses: a target the server named is used only when the candidate roster is present and agrees with it, a target the client could not read is never turned into "no engine", a resolution rule this version does not know with no target neither claims "no engine" nor "ambiguous", and a body whose rule contradicts its target or roster is not acted on. The explicit engine override is checked against the candidate roster only, by exact bytes; a name that also appears on a row that could not be read counts as a duplicate rather than disappearing with the filtered row; and when the roster itself cannot be read the override is refused rather than falling back to connecting by name |
@@ -389,7 +389,7 @@ guard still cross-checks the table by name).
389
389
  | `scripts/run-rewind-archive-capability-test.mjs` | The read face for whether a rewind can restore the code archive, and the honest three-state answer the ends render from it. The shells used to decide this from a local backup table that only their own in-process tools ever fill, so on any session where the engine runs the tools it stayed empty and the two code-restoring rewind modes simply never appeared, while the engine had been keeping a file history the whole time. The judgement now comes from what the engine itself advertises, and each of the four bits it advertises answers a different question: whether the conversation can be forked at a message at all, whether a fork can restore the tracked set, whether a code-only restore is possible, and whether this deployment understands the current spelling of the request key. The mode that rewinds the conversation and restores the code together needs both of the first two, and the engine offers no single bit for that combination, so the combination is made here: one bit stated off is enough to rule the mode out, both stated on make it available, anything else stays unknown — reading the file-history bit alone would offer that mode on a deployment that keeps file history but has no conversation anchors, where the request can only fail. A bit that is absent means an older engine that never spoke about it, which is not the same as an engine that said no, and a bit reported in a shape this reader cannot read is a third thing again — it is recorded as unreadable rather than quietly filed under "not reported", because those two send an operator to different places. One unreadable bit does not discard its siblings; only a response that is not a capability object at all clears the cell. What the ends get is available, unavailable or unknown, and unknown stays unknown: folding it into unavailable would hide the mode again, which is the mirror image of the bug this replaces. The sentences the doctor row can print are checked to be pairwise distinct and to avoid implying a refusal the engine never made, and the spelling of the outgoing request is deliberately not made to follow the epoch bit, since the older spelling is rejected outright by current engines |
390
390
  | `scripts/run-hitl-words-readout-test.mjs` | The five wording and reader fixes of the HITL surface. (1) Seven user-facing failure sentences (pending-row fetch, approve and deny decisions on approval cards, the question-card decision, the parked-row wait, local rule parsing, background stop) carry the thrown value's own message and never the error class name — a thrown value with no usable message becomes one fixed sentence instead of a bare class name, `[object Object]` or `null`; the original thrown value goes only to the host log. (2) `resumeContextUnavailableContent` is a single English source (three ` · ` segments, with and without a run id, the retention seconds only when the engine gave them) and a plan-review decision that meets a 409 `conflict.resume_context_unavailable` says it. (3) The plan-review `permissionModeAfter` pre-check no longer mirrors the server's decision pairing: only the value set is checked locally, and the engine's `request.field_conflict` speaks for the rest. (4) `requestEngineCompact` reports how the fire settled through `onSettled` (accepted / refused / failed / offline), with a 409 `compact.not_running` also surfaced on the host surface. (5) Every exported `project*Capability` reads only own data descriptors: an accessor is never run and reads as `undefined`, and a throwing descriptor read is swallowed the same way. Pinned with throwing and class-named values at all seven sites, both plan-review entry points, the compact outcomes under 409 / 404-retry / 5xx / the 60 s injected deadline, and a per-projector throwing-proxy row plus a source scan. The decision exits treat hostile thrown values uniformly: a throwing message getter, a throwing toString, a revoked proxy, or throwing status, error-code or name getters all end in the thrown value's own message or the fixed fallback sentence, never in the getter's own error text, and the transient-failure classification reads each slot independently. |
391
391
  | `scripts/run-hitl-wave2-test.mjs` | The second HITL batch of this release. (1) On the live approval leg an aborted card sends exactly one interrupt deny unless the engine's closing frame has already withdrawn the card (then, like a retracted card, nothing is sent); the resolution is `unsettled` / `interrupted`, the same as the durable leg, and the CC-514 regression (durable leg already decided ⇒ closing frame withdraws ⇒ nothing sent) is pinned. (2) The stream is no longer held while a card waits for a person: with a card on screen, later frames of the same stream leave the bridge within a bounded wait; a settlement frame for the same approval withdraws the card through its abort signal and nothing is sent from it; a host that ignores the signal still sends nothing after that point, and when the settlement says the approval was parked the user gets the existing "answer it again on the parked card" sentence; an answer given before the settlement is sent as before; a refusal frame that arrives while the answer is in flight still carries the refusal stamp; the in-stream frame and the suspended-ask path share one single-holder table with the parked leg, so the same approval shows one card and sends at most one answer, and the second caller gets the holder's outcome marked as joined; leaving the bridge does not withdraw a waiting card. (3) One reader produces the "why was I asked" sentence: when the structured cause is a record, the package's own sentence comes first (naming the check code only when it has the engine's code shape), followed by one space and the probe's own prose when present, passed through the untrusted-text display exit; the prose alone when there is no structured cause; nothing when neither is present or the card cannot be read. (4) One reader turns the engine's structured subagent state into a sentence, keeping status and approval words verbatim and computing the last-activity age from the engine's own read time. |
392
- | `scripts/run-park-decision-layer-test.mjs` | The decision layer behind the "stuck behind a card" family, shared by every client. A pending row that is **not in the queue** is three states, not one: a bounded, interruptible re-probe loop distinguishes *a decidable row*, *not born yet* (no positive evidence that anything settled — an empty queue proves nothing) and *settled elsewhere*, always probes at least once so a zero budget keeps the pre-fix semantics verbatim, cuts a hung read face off at the window rather than only noticing afterwards, and reports the honest failure when the window is spent instead of inventing a decision. The decision-note reader is likewise three-state: an explicit `noteRecorded: false` outranks an echoed note body, absence renders **no line at all**, and untrusted note text is flattened and bounded before it ever reaches a renderer. Row routing anchors on the deciding quantity — a row carrying `gateKind: "human"` with `toolName: "Write"` is a tool gate, because `human` is the engine's *generic* "someone must decide", not a synonym for a question — and the queue scan refuses to surface a row it cannot positively prove belongs to this session. A chain that fails after the row vanished is split by whether a card was ever presented: decided-elsewhere, or not-its-turn-yet. A row-level single-flight makes "at most one card per pending item" structural rather than incidental. The resume three-way card pins the option **order** (the zero-effect choice sits at index 0, because the frame carries no default-focus field and a stray Enter must not attach or cancel), renders only options the wired verbs can honour, collapses every ambiguous answer to zero action, omits the liveness line entirely when the engine gave no evidence, and — when there is no card lane at all — prints three real routes and exits on a dedicated code rather than reporting success |
392
+ | `scripts/run-park-decision-layer-test.mjs` | The decision layer behind the "stuck behind a card" family, shared by every client. A pending row that is **not in the queue** is three states, not one: a bounded, interruptible re-probe loop distinguishes *a decidable row*, *not born yet* (no positive evidence that anything settled — an empty queue proves nothing) and *settled elsewhere*, always probes at least once so a zero budget keeps the pre-fix semantics verbatim, cuts a hung read face off at the window rather than only noticing afterwards, and reports the honest failure when the window is spent instead of inventing a decision. The decision-note reader is likewise three-state: an explicit `noteRecorded: false` outranks an echoed note body, absence renders **no line at all**, and untrusted note text is flattened and bounded before it ever reaches a renderer. Row routing anchors on the deciding quantity — a row carrying `gateKind: "human"` with `toolName: "Write"` is a tool gate, because `human` is the engine's *generic* "someone must decide", not a synonym for a question — and the queue scan refuses to surface a row it cannot positively prove belongs to this session. A chain that fails after the row vanished is split by whether a card was ever presented: decided-elsewhere, or not-its-turn-yet. A row-level single-flight makes "at most one card per pending item" structural rather than incidental. The resume three-way card pins the option **order** (the zero-effect choice sits at index 0, because the frame carries no default-focus field and a stray Enter must not attach or cancel), renders only options the wired verbs can honour, collapses every ambiguous answer to zero action, omits the liveness line entirely when the engine gave no evidence, and — when there is no card lane at all — prints three real routes and exits on a dedicated code rather than reporting success. Ownership is three-way too: a pending row is *owned*, *provably foreign* or *unknown*, and "not mine" needs positive proof just like "mine" — a readable own-session id, a row session id read from an own data slot that differs from it, no delegation link back to this session, and no own-run hit. Anything unreadable (accessors, proxy traps, non-string slots, a missing own-session id) stays *unknown*. The owned-pending count skips a malformed row of another session instead of failing as a whole, and still fails on a malformed row that is ours or cannot be placed |
393
393
  | `scripts/run-selfheal-reopen-test.mjs` | The 409 active-run self-heal decision chain: `governanceForced` narrows on strict `true` only; triage prefers the wire's `pendingGate.kind` and falls back to the status table (an off-table kind is never guessed into a card arm — hands-off plus the honest wording); a first-sight card makes zero closed/reopened claims and a host presentation receipt of `presented: false` demotes the outcome to reopen-failed; park-row ownership is a fail-closed positive proof (own-run ledger or session id — unprovable is not owned); the three gate-identity key literals live in exactly one mint (`hitl/gateIdentity.ts`, AST string-token scan); the armed-gate presentation ledger is per-session; and the `plan_review` reopen arm shares the arm arm's card body, three-state verdict and delivery pipe, consuming the presentation history once a decision is delivered. The same chain also carries the `running` three-way card: both plan-family gate kinds route to the plan arm and all four ask-family kinds to the ask arm (an off-table kind still never gets guessed into either); the card is offered only for verbs that can actually be honoured and a missing presenter means zero action rather than a silent cancel; a steer is sent **exactly once** with its three delivery outcomes worded apart (a `queued` receipt is the wire correcting the triage input, so the named park word decides which card gets reopened, and an unrecognised park word drives neither arm), and a steer failure is split into *provably not delivered* (4xx) and *delivery unknown*, because telling a user to resend a non-idempotent instruction that may already have landed is how duplicates get made. After a user-chosen cancel, "the session is free" is asserted only from a whitelist of terminal states — park states hold the claim, an unrecognised state word is not a release, a failed read is *unknown* rather than a release, and only a 404 counts as one — and the honest timeout line quotes how long it really waited. The two "card could not be reopened" rows can carry a host-declared way to keep the conversation, which says the card comes back on resume only if it is still waiting: it is placed before the route that abandons it, never offered for an injected submission, while a decision is still on its way, or once the pending approval has been proven gone (the outcome then carries a flag saying so; the proof only counts before the cleanup card is shown, so a fallback after the card carries no flag unless a fresh read finds the run finished, and a recheck that finds the approval back clears it), the host function is not even called in those cases, and it is treated as unavailable when it throws or returns an empty value; a host can also switch off the engine decide route on the interactive rows while the cancel route stays, and with neither given all four rows are pinned byte-for-byte to the text the previous release produced. When the plan reopen refuses because the user closed that card (`dismissedByUser`), the outcome carries the flag and one package-owned line says so — you closed it, a message brings it back — in the typed, the injected and the queued-steer forms, with no way-to-keep and no engine decide route; the reopen port receives `{ trigger }` mapped from the submission origin (typed ⇒ `user`, injected ⇒ `automatic`, absent ⇒ the old single-argument call). When the reopen refuses because no view is subscribed to question frames for that session (`_sema_noPresentationSurface`), the plan and ask outcomes carry `noPresentationSurface`, and the five lines that talk about the card not coming back — both arms' typed line, the queued-steer typed and injected lines, and the injected not-delivered line — add one package-owned clause saying the session has no view that can show decision cards, byte-identical to the plain refusal otherwise; loose values and the other refusals (answer in flight, user-closed card, plain refusal) carry neither the flag nor the clause. For a host that reopens under its own session key with a presentation receipt, the suite drives the whole sequence end to end: subscribing for that key, reopening, and registering the receipt through the per-session call `registerArmedGateFromQuestionIdFor` gives `reopened: true` with the right first-sight reading, a second reopen reads as seen before, a decisive answer (with no extra bookkeeping from the host) makes the next reopen read as first sight again, and a dismissal does not; registering through the default-session call instead leaves the receipt window to run out with `{ reopened: false }`. Every bit the package reads off a host's reopen verdict is read once, inside the guard: a bit whose getter throws (or a proxy that throws) is treated as absent, so the self-heal still settles — with exactly the outcome it would give if that bit were missing — and the rows never throw, including for an outcome the host built itself; a plain verdict lands in the queued-steer outcome byte for byte, key order and any extra keys the host added included, a key whose getter throws is the only one left out, and an own key literally named `__proto__` is copied as a key rather than changing the copy's prototype; symbol keys are copied too and a verdict with a null prototype stays one (compared key by key, value by value and by prototype, not through JSON); and an outcome whose `reopened` property itself cannot be read is phrased as if it had none. The two reopen-failed outcomes carry `errorCode` only when the reopen gave up because a request failed — the outer wire code of that request (a folded policy code is read through `blockedBy`), taken from a reopen port that threw, from the host verdict's `_sema_errorCode`, or from the stale-park arm's own three requests; a later request that changed what the reopen gave up on (the poll finally answered, the row appeared), a failure with no wire code, a reopen port that failed after the caller had already aborted, and the three judgment refusals (answer in flight, user-closed card, no view) all leave it absent, while a read that only confirms (the row is still pending after a failed decision) keeps it; only a request that failed counts, never a field that cannot be read on a record that did come back; and every row is byte-identical with or without it. The error objects the self-heal leg reads (thrown by the run lookup, the steer and the cancel) are read inside the guard too: a `status` or `message` whose getter throws, or a proxy that throws on any read, is treated as absent, so the self-heal still settles instead of rejecting, and the at-most-once failure class reads such an error as delivery unknown; the same goes for the run record a successful lookup returns — when its `status` cannot be read, the self-heal treats it as a record without a status rather than rejecting — and for a steer receipt, whose two readers answer `null` for a field they cannot read. When the delivery of a steer is missing, unreadable or an unknown word, the injected-submission line says the message was accepted but its delivery was not reported and to watch that reply before deciding whether anything needs sending again, instead of the handed-over wording, and it never says nothing is needed, with or without a follow declaration. The `running` three-way card also gives a fourth way out, *attach*, under the same single rule as the resume card (the run is still running and can be taken over): one shared predicate decides for both cards, only a strict `true` supply flag counts, and a host that does not state the flag gets the request byte-for-byte as before (no `canAttach` key at all, which is not the same as `false`). Choosing it hands the decision to the host without sending any write request (no steer, no cancel), the refused message is reported as not sent, and an `attach` answer on a card that never offered it is zero action. The resume card's two cancel closing rows, confirmed and unconfirmed, are mutually exclusive and come from one private mint. A follow-up message carrying a plan-review outcome is never passed into a reply already in progress: when every queued item in that submission is such an outcome naming the very run that holds the session, the outcome is dropped as redundant (whatever effect it recorded) and the host is told to follow that run; otherwise it is held and released for a new message exactly once, when that run is read as finished or paused. Submissions without such an item — and every typed submission — behave byte for byte as before. The same holds when the run that holds the session has already ended: an outcome naming that run is dropped as redundant and the host follows the end of that run, instead of the outcome being reported as not delivered — on the in-progress path and on the stale-pause path alike; the redundant sentence no longer says the reply is still in progress. The plan-review carrier list is read exactly once per self-heal on every path. |
394
394
  | `scripts/run-terminal-identity-copy-test.mjs` | Terminal-state **identity**, in both lanes where a stop gets a name. A run stopped by this deployment's own governance knobs — the open-set `limits.*` family, `output.invalid`, and the `blocked` contract terminal a ReportBlocked agent produces — is not a provider failure, and labelling it `API Error:` sends the reader to check the network, the key and the quota when the handle is the `--max-turns` they passed themselves. Those terminals now render a neutral row; the reverse direction is guarded just as hard, because asserting "this is *not* an API error" on a code the package does not recognise is the same misfiling pointed the other way — a real `gateway HTTP 502`, a `conflict.session_active_run` and any unknown code all keep the `API Error:` prefix, and the row keeps its `_sema_api_error_message` class flag (spelled `isApiErrorMessage` before 0.83.0) so brief-mode visibility filtering does not silently drop it. The second half is who the rejected submission belonged to: the self-heal copy told every caller "Your message was NOT sent … send it again", which is three separate untruths for a system injection (a plan-review outcome, a cron wake-up, a task notification) — not the user's message, and not re-sendable, since a host queue marks those non-editable and non-recallable. The injected form says so instead, and the one sentence that promises re-delivery is pinned to the single disposition that earns it: `selfHealSubmissionDisposition` is the same function the host consults before putting the item back on its queue, so the promise and the behaviour cannot drift apart, and the arms where no card could be surfaced state plainly that nothing was delivered and nothing will retry. Since 0.72.6 the same gate pins the **follow intent** after a steer (): a message handed to a live run only pays off if someone tails that run's own event stream, so `steerFollowIntent` decides from the delivery word whether to tail now, after the pending decision, or only after a wake — and the "watch that run" sentence ("watch that reply" on the rows about follow-up messages sema sent on its own) turns into a factual "sema is following that run" ("… that reply") **only** when the host declares it attached that tail, so a shell that did not wire it can never claim it did. Since 0.83.0 the rows about follow-up messages sema sent on its own carry no engine-internal words and say what happened per delivery shape, promising a resend or "nothing for you to do" only where the code guarantees it |
395
395
  | `scripts/run-seat-wave2-test.mjs` | The seat capability table, the folder picker and the session folder, added in 0.94.0. Whether a renderer may offer an optional seat action is answered only by the capability table the host publishes (`seatCapability`: reported, not reported, or unreadable — reading own data properties only, never running a getter), never by calling the action and watching it fail; "not reported" is neither folded into "available" nor into "cannot", and the disabled sentence comes from one place (`seatCapabilityUnavailableDetail`). The subscription helper subscribes before it reads, so a renderer that subscribes after the host has published and then withdrawn a capability sees the withdrawn state on its first callback. `pickFolder` returns a non-empty path or `null` for a cancel, and the session record's `cwd` is the folder the session was started in, absent when none was chosen. The same gate pins `deviceFlowPollingPlan`: the device-code response's own positive finite numbers win, anything missing or malformed falls back to the contract numbers generated from the settings schema at build time (no number is written by hand), and the poll interval is never below one second |
@@ -409,13 +409,13 @@ guard still cross-checks the table by name).
409
409
  | `scripts/run-file-history-leg-notice-test.mjs` | What a client says after the engine reports, for one run, whether it is capturing file history. A five-way verdict combines the engine's capture capability as it was read when the request was assembled (not as it reads when the report arrives), whether this run's submitted request actually carried the per-run opt-out, whether the user turned file checkpoints off, and the word reported for the run; only one outcome produces a sentence (the run carried the opt-out and the engine still captures), with its conclusion first so a truncated status line still reads. The guard pins every one of the 54 cells of the verdict table, sweeps alternative spellings and malformed values for each input, checks that an unreadable capability never produces a sentence while a carried opt-out is judged without consulting the capability, and that inherited input properties are ignored. A process-local ledger records, per submission, whether the outbound request body carried the opt-out (only a plain own data property counts; a body with its own serialisation hook or an accessor for that key is read as unknown, so a fact is never recorded that the sent JSON could contradict) and binds the submission to its run only on a frame proving the run was created, using the same rule as the prompt-delivery ledger, so session-busy refusals and replayed approval cards carrying another run's id never attach this submission's fact to that run; answers do not depend on call order, conflicting records read as unknown, and runs this process never bound read as unknown. An end-to-end section sends real requests through the SDK stream parser to a local stub and drives the reported word through the projection and adapter layers to the verdict. A body whose property descriptor and property read disagree (a proxy), or that carries any enumerable getter, reads as unknown, because serialization runs those getters and may drop the field; a value that is a non-null object (a wrapped string, an object with its own serialisation hook) also reads as unknown, since what it serialises to cannot be proved. |
410
410
  | `scripts/run-sealed-key-capability-test.mjs` | The engine's sealed-key discovery segment (`capabilities.sealedKey`), read the same four-state way as the other capability readers: exactly three non-empty strings (`alg`, `publicKeyId`, `publicKey`) make a usable reading and are passed through as they are; an absent key means *do not seal* (an older engine, or an engine that could not set up its key custody — the same action either way) and is never treated as a key; a present but malformed segment is dropped rather than turned into a half reading; and anything the segment carries beyond the three public fields — a private key, a creation time, anything else — never reaches the reading, so this reader cannot become a credential channel. The reading is checked against the projection a `7.104` engine package actually mints, both with a key store and without one. |
411
411
  | `scripts/run-store-center-wiring-capability-test.mjs` | The two cloud-mode capability positions (engine ≥7.106.0), read the same four-state way as the other capability readers. The store posture (`capabilities.store`: declared backend, assembled backend, whether it is durable, an optional degradation reason, and the SQL engine seat) is always present on a current engine, so an absent key means an older engine and is never read as “not durable”; words are carried as reported rather than folded into known ones, the SQL seat is judged by the very same rule as the SQL-posture reader, and one malformed seat discards the whole reading. The center wiring (`capabilities.centerWiring`) is present only when the engine is connected to a configuration center: with no quota lease configured its three lease-dependent keys are legitimately absent, and an absent key is split by the same response — a current engine (it carries the store posture) is reported as not connected, an older one as not reported. Two predicates live in the package (does this replica persist; is the caller's budget lease-enforced — known words matched exactly, everything else unknown), and each reader has one table of doctor sentences in which no two states read alike, the older-engine sentence never claims “not durable” or “not connected”, the two failure postures cannot be said the wrong way round, and unrecognised words are shown escaped and bounded. Both are checked against the engine's own projections: every form they mint reads back exactly, every word in their vocabularies lands on a known arm, and the member lists the engine declares for these two positions equal the reading's own keys. |
412
- | `scripts/run-park-failed-reason-projection-test.mjs` | The fail-soft true cause when a parked approval cannot be decided and the engine's own `done{suspended}` terminal is the one handed back: that terminal is still returned as-is (no second terminal), but it carries `_sema_park_failed_reason` — the same sentence, byte for byte, that the durable leg puts on its synthesized terminal — and the result frame carries it through with credentials washed the same way `errors[]` is (value only; same sentence as the durable leg after washing). The key is left off when this host has no decision surface at all (no approval card port, a port installed under another session key, or no question overlay): that sentence's way out is to decide on the card, which such a host never shows. Pinned with a policy-fold refusal on decide, both stall exits, a host-retracted card, credential samples through the full bridge on both legs, the no-decision-surface forms, and the absent forms (user interrupt, malformed values, ordinary terminals). |
412
+ | `scripts/run-park-failed-reason-projection-test.mjs` | The fail-soft true cause when a parked approval cannot be decided and the engine's own `done{suspended}` terminal is the one handed back: that terminal is still returned as-is (no second terminal), but it carries `_sema_park_failed_reason` — the same sentence, byte for byte, that the durable leg puts on its synthesized terminal — and the result frame carries it through with credentials washed the same way `errors[]` is (value only; same sentence as the durable leg after washing). The key is left off when this host has no decision surface at all (no approval card port, a port installed under another session key, or no question overlay): that sentence's way out is to decide on the card, which such a host never shows. Pinned with a policy-fold refusal on decide, both stall exits, a host-retracted card, credential samples through the full bridge on both legs, the no-decision-surface forms, and the absent forms (user interrupt, malformed values, ordinary terminals). The way-out tail of that sentence follows which leg ended the round: when the engine answered and refused (the card does not come back on its own, so the tail tells the user to send a message to reopen it) the tail changes; every other leg — the host withdrawing the card, the card port reporting a failure, an engine server error, no answer from the engine, or a stall — keeps saying the card can be decided when it is shown again, because nothing shows otherwise. Both tails live in the message catalog under `approval.park.resume.*` (English and Chinese), and each English sentence appears in the shipped build in the catalog only. |
413
413
  | `scripts/run-models-dropped-entries-projection-test.mjs` | The live model catalog's dropped-entries reader (`droppedModelEntriesOf`): which catalog entries the engine did not accept and why, read from `GET /v1/models` `droppedEntries`. An absent key (an older server) reads as `not_reported` — never as an empty list — while a present empty array is a real "nothing was dropped". Rows keep the server's order; a malformed row (non-integer index, empty reason code, bad identity, accessor fields, sparse holes) is counted as unreadable without dropping its siblings; unknown reason codes and additive keys pass through unchanged. Only own data properties are read, accessors never run, a throwing read never escapes, and the result is frozen. The exported row type is derived from the SDK's `models.list()` return type, so it compiles on the peer floor SDK as well. |
414
414
  | `scripts/run-model-catalog-read-projection-test.mjs` | The single reader of the `GET /v1/models` body (`readLiveModelCatalog`) and the readers built on the catalog it returns. A row is keyed by its `name` (the value a request submits and the `@` handle); a malformed row is counted and dropped without losing its siblings, a later row repeating a name is counted, and the result is a frozen deep copy. The default model is the server's answer read from `defaultModel` only (`liveDefaultModelOf`): two rows sharing an upstream id resolve by `defaultModel.name`, a default missing from the catalog reads as `not_listed`, and the first row is never offered as the default. Per-row readers answer whether a model may be `@`-mentioned (`atMentionOf`: an absent key means the deployment has no mention list, not "cannot be mentioned") and what line-compatibility declaration it carries (`modelCompatOf`); the accepted thinking levels stay with `effortLevelsOf`, never re-derived from the compatibility block. `liveModelRowsOf` gives one frozen view per row with every field taken from those same readers; a context window is carried only when it is a positive finite number, with no lower floor. The desktop seat's `listModels` returns this same catalog and its outbound gate shares the row rule. A host can also ask whether the engine's own catalog serves a given model name: `modelAcceptanceOf` answers accepted, refused or unknown, and unknown is never folded into either of the others — the name missing, no catalog, a catalog that cannot be read or that lost rows while being read, and — when the model list carries no `aliases` (an older server) — the words an engine always admits itself (tier words, tier aliases, `default`, which the rows do not list) all read unknown; a name that a fully read catalog does not list (or lists as disabled) is refused. When the model list carries `aliases` (the extra words the engine accepts besides row names), a word is accepted when it is a row name or an alias, matched by exact name only and never inferred from a row's upstream id; tier words and `default` are then judged like any other word, and an absent `aliases` key keeps them unknown (an older server). A row or alias that the deployment's mention list marks `atMentionable: false` is refused with its own reason, and an `aliases` value that cannot be read leaves an unmatched word unknown. The one-line notice `modelNotServedNotice` is produced for a refused reading only, in both languages, with a separate sentence for a word the deployment's list does not allow, and quotes the model names through the untrusted-text path. |
415
415
  | `scripts/run-seat-carriers-test.mjs` | The desktop seat's request carriers and the park bit. One inbound judge for `sendMessage` (`checkSendMessageRequest` / `isSendMessageRequest`): a closed key set, each key's vocabulary taken from the package's own tuple (permission modes, reasoning efforts, rewind modes, turn settings) and the attachment cap; the two subagent keys are judged (boolean, or `true` / `{ ttlMs?, max? }` with non-negative integers); an unregistered key, an accessor or a throwing read is refused with a key path; so is a non-enumerable own key, whatever its value (a structured clone would drop it, so the gate does not let it pass unread). The request and its three nested closed shapes (`rewind`, `settings`, `retainSubagentSessions`) must be plain records, so `Date`, `Map`, `Set`, class-instance and custom-prototype carriers are refused. The rewind carrier `{ mode, targetMessageUuid }` uses the same tuple as `rewindSpecForMode`, and the request keys it is projected to equal what that projector emits. `SEAT_TURN_SETTINGS_ORIGINS` records, for every settings sub-key of the request builder, whether the renderer carries it or the host supplies it; host-supplied keys are refused on the seat. Optional seat positions — verbs and carrier keys alike — are answered only by the seat capability table (`rewind` added). `tool_permission_resolved` may carry `parked: true` only when its outcome is `expired`, minted from the closing frame through `approvalCompleteParked`, the same reader the frame router uses. |
416
416
  | `scripts/run-compaction-started-projection-test.mjs` | The `compaction_started` frame (a compaction pass announced at its commit point, before the summary call) projected to an internal arm and then to the additive `compaction_started` chrome event — `pass` and `trigger` required (open-set trigger), `startedAt` only when it is a finite number, malformed frames dropped, zero transcript rows, the lane told honestly; checked against the server package's own frame builders (the start frame and the outcome frame carry the same `pass`). |
417
417
  | `scripts/run-compaction-status-tracker-test.mjs` | The single reading of "is a compaction running": `createCompactionStatusTracker()` pairs `compaction_started` with the frame that ends the same `pass` (the landed boundary, a failed or mooted outcome, or the end of the stream), never lets a sub-agent's pass light the main lane, ignores outcomes that carry no `pass`, and gives the interactive lane `{ compacting, since? }`; `compactionStatusFrameOf` turns each change into a `system/status` frame in the reference client's shape for the print lane (no heartbeat, no output-size field). The suite covers interleaved passes, the sub-agent counter-example, end-of-stream closing, older servers, hostile input, both lanes fed the same sequence, and the server package's own frame builders. An opening frame whose `trigger` is not a non-empty string is not recognised on any of the three ways the tracker can be fed (the same rule the projection applies): it lights nothing, is not recorded, produces no status frame, and a good frame for the same `pass` still opens. |
418
- | `scripts/run-model-capability-probe-test.mjs` | Whether a model on the OpenAI-completions lane **thinks**, and whether that thinking can be **turned off** — a question nobody can answer by looking at a model name, and one whose wrong answer costs every later call. The probe is judgement only: the network half arrives as an injected port, so the package mints no URL, reads no credential and never calls `fetch` — pinned by a source-level assertion, because a package that reaches the network once has changed what every host must trust it with. The seven dialect words are a **copy**, reconciled element-wise against the installed engine’s own bytes in both directions, since the words belong upstream and a private table drifts the day a dialect is added; the settings package deliberately declines to restate them, so the table cannot be imported from there and this guard is what stands in for the import. The **order** the dialects are tried in is a public promise rather than an implementation detail — each extra attempt is real money and real latency against someone’s gateway — so the guard pins the exact call sequence a stub records, and reversing it reds on the wasted round trip; the template-parameter spelling leads because an observed gateway keeps thinking, and answers with an empty body, when handed the top-level switch instead. That observation is also why an empty answer is **not** accepted as *thinking is off*: a knob that deletes the reply is not a knob that disabled reasoning, and accepting it would write a spelling into the catalogue that the gateway does not honour. Two dialect words whose request bytes are identical to another’s do not each burn an attempt. The two verdicts that look alike are held apart from both directions: *tried everything, still thinking* requires at least one attempt to have **cleanly answered**, and when every attempt was refused the verdict is *could not tell* instead — and on the unanswerable path the result carries **no** thinking flag at all rather than a fabricated `false`, while the pure write-back returns the very same entry object untouched. A verdict that reasoning cannot be disabled **removes** a previously declared spelling rather than leaving it, since a refuted spelling keeps the engine sending bytes the gateway ignores while the catalogue still renders it as already off. Evidence is lengths, finish positions and status codes only — a planted secret in both the answer and the reasoning channel must appear nowhere in the result, so the record can go into a log or a ticket whole. One cross-package premise is checked by really running the other package’s parser rather than quoting its documentation: everything this probe writes eventually passes through the settings schema on its way into a catalogue, and that field is declared parse-transparent precisely so the vocabulary can live on the consuming side. If it ever narrows, the spelling is stripped **silently** — indistinguishable from the probe never having run — so the guard feeds the probe’s real output through the real parser, checks the compat object comes back key for key, and checks a dialect word this client has never heard of survives too. Two shapes that must be rejected really are rejected, since otherwise the survival checks would hold on a parser that accepts anything, and a bare entry is asserted valid first, because the first run of this section reddened on a space in a fixture’s name — a fixture that cannot pass would disguise the real alarm as already having fired |
418
+ | `scripts/run-model-capability-probe-test.mjs` | Whether a model on the OpenAI-completions lane **thinks**, and whether that thinking can be **turned off** — a question nobody can answer by looking at a model name, and one whose wrong answer costs every later call. The probe is judgement only: the network half arrives as an injected port, so the package mints no URL, reads no credential and never calls `fetch` — pinned by a source-level assertion, because a package that reaches the network once has changed what every host must trust it with. The seven dialect words are a **copy**, reconciled element-wise against the installed engine’s own bytes in both directions, since the words belong upstream and a private table drifts the day a dialect is added; the settings package deliberately declines to restate them, so the table cannot be imported from there and this guard is what stands in for the import. The **order** the dialects are tried in is a public promise rather than an implementation detail — each extra attempt is real money and real latency against someone’s gateway — so the guard pins the exact call sequence a stub records, and reversing it reds on the wasted round trip; the template-parameter spelling leads because an observed gateway keeps thinking, and answers with an empty body, when handed the top-level switch instead. That observation is also why an empty answer is **not** accepted as *thinking is off*: a knob that deletes the reply is not a knob that disabled reasoning, and accepting it would write a spelling into the catalogue that the gateway does not honour. Two dialect words whose request bytes are identical to another’s do not each burn an attempt. The two verdicts that look alike are held apart from both directions: *tried everything, still thinking* requires at least one attempt to have **cleanly answered**, and when every attempt was refused the verdict is *could not tell* instead — and on the unanswerable path the result carries **no** thinking flag at all rather than a fabricated `false`, while the pure write-back returns the very same entry object untouched. A verdict that reasoning cannot be disabled **removes** a previously declared spelling rather than leaving it, since a refuted spelling keeps the engine sending bytes the gateway ignores while the catalogue still renders it as already off. Evidence is lengths, finish positions and status codes only — a planted secret in both the answer and the reasoning channel must appear nowhere in the result, so the record can go into a log or a ticket whole. One cross-package premise is checked by really running the other package’s parser rather than quoting its documentation: everything this probe writes eventually passes through the settings schema on its way into a catalogue, and that field is declared parse-transparent precisely so the vocabulary can live on the consuming side. If it ever narrows, the spelling is stripped **silently** — indistinguishable from the probe never having run — so the guard feeds the probe’s real output through the real parser, checks the compat object comes back key for key, and checks a dialect word this client has never heard of survives too. Two shapes that must be rejected really are rejected, since otherwise the survival checks would hold on a parser that accepts anything, and a bare entry is asserted valid first, because the first run of this section reddened on a space in a fixture’s name — a fixture that cannot pass would disguise the real alarm as already having fired. A model’s capability bits are three-state — declared on, declared off, or **not declared** — and the guard holds the package to one folding path: a catalogue row’s `reasoning` reads as yes, no or unknown, where an absent key, a non-boolean value, an accessor or a prototype-chain value is *unknown* and never *no*, and the row view, the effort-level reading and the vision reading all answer from that one fold, so a row view without a `reasoning` key means undeclared rather than `false`. The write-back never fabricates a `false`: across every verdict and every entry shape, an output of `false` can only be an input’s own explicit `false`, while the two verdicts that actually saw thinking still write `true` because the engine sends the switch-off keys only to a model declared as reasoning. The engine’s notice that images were replaced by a placeholder gets a reader and a two-language sentence that speaks only for the one capability-and-status pair the engine actually issues, and hands every other pair back to the engine’s own message rather than inventing a sentence for a state it has not shipped |
419
419
  | `scripts/run-classifier-status-test.mjs` | What state the auto-mode classifier is in **on this session** — the question a doctor line, a model settings page and a permission card’s status row all ask, and a different question from the one the approval card asks (*why am I being asked right now*), so the sentences are pinned mutually distinct from that face’s as well as from each other. The session-level half of this reading — a breaker record the engine used to keep — was **retired upstream**, and the guard now holds that retirement from **both** sides: the engine's own declarations must really no longer carry it (a fact coming back would mean the removal here was the wrong disposition, and that deserves a conversation rather than silence), and this package must carry no alias, no state word and no leftover narrowing for it — a reading kept alive for something nobody emits any more is a promise the interface cannot keep, and it left the doctor line advertising a state it can never reach. What remains is ordered by the quantity that actually decides whether the classifier is running: the fact from **this round** first, then whether this leg is armed — a decider is minted per run, so a later leg can be armed again. Not armed, and a section that never arrived, both answer **undefined** rather than *available*; that arming question has its own field and answering it twice grows a second ledger. Arming and availability are also **two words, not one**: the engine says a decider was minted *for this leg*, which is an assembly-time fact, while whether that decider answers any given round is a **per-call** one — so an armed leg reads `armed` and only a positive per-call fact (an ask whose origin is the classifier's own denial-bound fallback, which by construction stands *after* the classifier ran) reads `available`. Every other ask origin is refused as evidence and for a stated reason rather than out of caution: several are ones the classifier is structurally forbidden to answer, and for the rest a surviving ask is precisely the case where it did **not** resolve one — so reading availability off them would be a guess. The projection is a **whitelist**, so an older engine still sending the retired member loses it at the boundary while the two live facts beside it ride through untouched. Rendering never throws and never impersonates: a state word this client does not know — including the retired one, which a restored view can still carry — reaches an honest fallback that names it verbatim, carries no invented explanation of a mechanism that no longer exists, and is proven distinct from all three real sentences; prototype keys reach that same fallback rather than a function body, checked against a real out-of-table word so the comparison cannot hold vacuously The ask origin is read the one way it is read everywhere else — an own key holding a non-empty string — so an origin that exists only on a prototype no longer counts as proof that the classifier ran. |
420
420
  | `scripts/run-decide-receipt-test.mjs` | What a decision verb actually **answered** — and, more importantly, what it did not. A success response on the newest lane is only an acknowledgement that the decision was accepted for delivery: the approval is still pending, and a client that clears the card on it shows either a ghost card that was already approved or a card that vanished while the decision was lost. So the package deliberately has **no** "was it resolved" predicate — nothing in that body can answer it — only the opposite one, whose `false` is likewise not evidence of resolution; resolution is only ever the next running arm on the stream. The guard pins that inversion in the product source too: the success path must no longer clear the latched gate, while the stream-observing path that really clears it must still be there. The body has four shapes with **no** key common to all of them, so every position is read as honestly absent, and the handoff handle — which run to watch from here on — requires **two** facts together, since either one alone would either point the stream at the run it already had or mint an empty handle. The record of what finally happened to an already-decided action is read through the **same** reader as every other gate record rather than a second copy, and its absence means **unknown**, never *it was allowed* — the two can even contradict each other, so the card says nothing at all when it is missing. The three refusals on that lane each get one distinct sentence and a disposition taken from **why** each was refused rather than from severity: one cannot be helped by re-sending at all, one waits on the host, one just drops an option — and none of them carries a countdown, because the server never mints a wait for them. Recognition is a **closed set**: an unrecognised code on the same prefix returns nothing rather than a guess, since that prefix also houses a safety signal whose whole rule is never to retry automatically, and the recovery handle is read as absent when unreadable rather than substituted from a different identifier that no longer appears on that lane **0.68.3 (core 7.18.0):** `gate.disposition.classifier` is read key by key into a named view (requested model, model that answered, and the ladder fallback when one happened); a half-shaped record yields no classifier at all rather than a half view, and absence stays "unknown", never "the seat answered itself" |
421
421
  | `scripts/run-approval-frame-chrome-arms-test.mjs` | The two in-stream approval frames finally reaching every host through the shared pipeline instead of one shell's private branch — the shape of a layering defect: hosts that only consume the package could not rebuild their pending cards after a reconnect, and did not clear a card the engine had withdrawn. The payload is deliberately carried as the **envelope** the upstream types declare rather than the first-version card: the stream parser applies no predicate, so narrowing here would let a legitimately newer frame pass as the older shape and invite consumers to read keys a newer card never promised. The guard therefore pins that every open key survives untouched, that an unknown version still passes through, and that narrowing is left to the host's own predicates — with the fallback being a generic card and a person, **never** an automatic denial. A frame whose version cannot be read at all is reported as malformed rather than dropped in silence, because both frames carry user-visible decisions and state changes. Both arms are registered as **required** host duties, and their duty text names the load-bearing rules a host would otherwise have to rediscover: which predicate to narrow with, that the reconnect preamble — not a replayed historical frame — is the authority on which cards exist, and that a withdrawal frame can be lost entirely. Unlike the sibling arms, these carry **no** sub-stream cutoff: an approval raised under a delegated call still has to reach a person, and filtering it by ownership is the host's job, not a reason to discard it. Finally the upstream bytes that justify the envelope discipline are checked to still be there, since the whole design rests on them |
@@ -445,9 +445,9 @@ guard still cross-checks the table by name).
445
445
  | `scripts/run-task-request-omission-receipt-test.mjs` | Where every key a client hands to the request constructor ends up. The constructor used to answer "not stamped" the same way for four different reasons — value absent, no such row, wrong lane, live gate closed — and a key it had never heard of did not even get that: an unattended run could pass a system prompt, an output schema and a spend cap and receive a body holding the objective and the session id, with nothing anywhere saying what was left out or why. The guard pins the three answers apart. **Seated** keys reach the body verbatim on the unattended lane. Keys the package **knows but did not carry** never throw, never reach the body, and each gets a receipt row with one word from a frozen cause list — every present key is on the body or on the receipt, never both and never neither, checked across both lanes with the live gate open and closed against a key-by-key table written independently of the package's own routing. Keys the package **does not know** are refused loudly and are a separate cell, not a fourth cause: the cause list has no word that could hold them, and the seat reader answers `unknown`, not `none`. The cause list is bitten from both sides (exact, every word producible, nothing produced outside it, the judge table's keys read from source through the TypeScript parser) and no second hand-copied list may exist in `src/`. Upstream claims are read straight off the installed SDK typings: a key seated in this release must be a named request field, a key registered as having no upstream counterpart must not be — the day it appears the guard turns red — and the index signature counts as evidence for nothing. The per-run file-history opt-out word is covered the same way: seated on the two user lanes behind the live gate and absent from the side-channel lane, stamped only for its one legal word, refused loudly for any other value, and read after every existing declaration so that it cannot erase them. The wording shown to end users is pinned alongside the wording for integrators: a second sentence table, keyed by the same frozen cause list and exhaustive over it at compile time (the check sits on a two-column table literal — a lead-in plus a reason clause — from which the full sentences are derived), gives each cause one plain sentence that says the item was not sent and why (the integrator table now carries the same literal-level check — before, a missing sentence failed the build but an extra one did not), with no integrator vocabulary and no claim about whether anything took effect; every sentence differs from the others and from the integrator wording and passes the published-text hygiene list, and an unknown word, a non-string, a throwing object or a prototype-chain name gets one honest generic sentence instead of a throw, an echo or an invented cause. The full sentences and the generic fallback stay byte-identical to the sentences shipped in 0.88.0, every reason clause inside them starts lower-case with no closing full stop, an unreadable cause gets the generic sentence, no full sentence may appear as a literal anywhere in the source, and the separate reason-clause export withdrawn in 0.98.0 is checked to be absent from the root entry. When a consuming client's own copy of the cause table is on disk, the guard also checks that every word in it is one of the package's causes, with no word listed twice (a word only the client's table has fails; a word only the package has is reported, not judged), and reports that check as skipped rather than passed when the table is not there. A request key the engine retired and refuses by name is, from 0.92.0, refused at construction by the same unknown-key rule as any other key the package does not know (it names the key and never echoes the value), on every lane, in every live state and through both doors; the seat reader answers unknown and the wire-side judge names it. |
446
446
  | `scripts/run-session-policy-wire-test.mjs` | The per-session tool-rule face: the capability bit that says whether an engine keeps such rules at all, and the narrow read plus tightening orchestration built on it. The bit is read the same four-state way as its sibling capability readers — an absent key is not reported (this binary predates the position itself, which says nothing about whether the face exists), `true` is present, `false` is a positive absent (this deployment keeps no per-session rules), any non-boolean value is unreadable and drops the cell rather than being folded into "absent", and a capabilities body that is not an object at all (an array included) is unreadable rather than "not reported". Its single-source verdict answers whether to show the tightening entry: only an engine that says yes is `yes`, both a positive no and a binary too old to answer are `no`, and never having observed a capabilities body is `unknown`. Whether to put a request on the wire is deliberately a **different** question with a different answer for that last state, and lives with the orchestration. The read narrows three ways that must not collapse into each other: a record that really is empty (present, version zero — what an engine answers for a session no rules were ever written for), a record that cannot be read, and a call that failed with a typed disposition. A half-bad record — one rule bucket well-formed and another the wrong shape — counts as unreadable in full, because the write verb replaces the whole record: dropping the bad bucket and writing the rest back would empty it, which relaxes the rules while the caller sees a 200. An unreadable version stamp is never filled in with a zero, a bucket that reports an implausible number of entries is unreadable rather than walked or truncated, each array's length and each of its indices are read exactly once, and a throwing accessor is unreadable rather than propagated. Every load-bearing key is read as an own property — the envelope, the version stamp, each of the five buckets and each array index — because a prototype lookup would let a polluted prototype put a bucket into the reading that the wire never carried, and since the write replaces the whole record the union would then write that invented restriction back as a real one; a guard pollutes the object and array prototypes in place and proves all four shapes stay out. Because the write replaces the whole record, adding a restriction means writing "what is already there, plus the new entries": the union only ever adds, de-duplicates verbatim, keeps a bucket that is present but empty (present-and-empty and absent are opposite meanings, and dropping it would relax the rules), mints no bucket neither side had, copies entry bytes as they came (no trimming, sorting or path rewriting — those judgements belong to the engine), and takes its bucket names from the engine's own type surface rather than a hand-copied list, so a new bucket upstream is a compile error instead of a silently dropped one. Which differences count as relaxing is the engine's judgement and is never re-implemented here: a refusal on those grounds is reported verbatim, never swallowed and never retried. The orchestration is guarded on three axes. Timing: when the record moves between the read and the write, it re-reads and re-writes **exactly once** — two reads and two writes, no more — and the second attempt's union carries the other writer's entries, which is the entire point of re-reading; a second collision is reported rather than retried a third time, and an uncontended write makes exactly one round trip. Concurrency: an explicit barrier holds both orchestrations first reads at the same version before either may write, and the criterion is how many times the store actually rejected a stale version rather than how many writes it saw — the latter is equally true of two serial successes, so it would stop detecting contention the day the interleaving changed. Under real contention the store rejects exactly once, both writers land on strictly different versions, both writers entries survive in the final record, and the round trips are exactly three reads and three writes; the same two orchestrations run serially are asserted to reject zero times in two reads and two writes, which is what proves those numbers are discriminating. On a store where every write loses the race both report a collision having written exactly twice each. Failure classification: a relaxation refusal, a refusal to stamp a version the store cannot establish (the same status code as the relaxation refusal but a different machine code, and folding it into that arm would send the caller off to edit entries that are not the problem), a missing session, a deployment without the face, an ownerless session, a collision code, a bare conflict with no machine code, a rejected body and an unauthorized call each land on their own arm — the collision arm is matched on the machine code verbatim rather than on the status, because two different situations share that status and only one of them is worth retrying. The remaining split is not "which code is this" but "did the engine answer at all": an answered client-side refusal is allowed to say nothing was written, because every such refusal on this endpoint is emitted before the record is touched, while a throw with no answer at all — a dropped connection, a timeout, a response body the transport itself could not decode, a server fault — can only say "unknown", since that throw may well have happened after the record was already saved. Two guards prove that is not theoretical: a write whose receipt cannot be read, and a write that throws after the fixture store has committed, both leave the record changed. Neither is success nor failure: the only honest answer is "unknown", it carries no version, and it is never retried. The direction of the change is likewise never claimed. The engine’s tighten-only rule is an identity gate, not a field gate — for a principal the deployment treats as an operator it does not run at all, so a union that adds a name to an existing allowlist is accepted and really does widen it. This package does not mint a second copy of that rule, so what it reports is the fact it can stand behind: the record now holds what it already had plus the entries sent here. The sentence for a saved write is pinned to contain no claim of tightening, narrowing or restriction, and a guard reproduces the operator case to prove the widening is real while the wording stays honest. Every sentence the module mints is checked pairwise distinct, with the receipt-unreadable one required to keep its "may already be in effect" and the record-unreadable one required to say nothing was written. When a read comes back without a usable record, one sentence per failure kind and per unreadable position says so in plain words, never naming internal codes, never saying the record is empty, and falling back to one general sentence for anything it does not recognise. A sixteenth sentence covers a session that has not begun on the engine yet — a state only the host knows, since the server gives the same answer for a session that does not exist and for someone else's session — and it is checked like the others: distinct from the other fifteen and from the general fallback, free of machine words, never claiming the record is empty or that nothing is restricted, never saying the contents are unknown (there is nothing to know yet), read from own data properties only, and compared with the previous release's package so that the other fifteen sentences and the general fallback stay unchanged byte for byte. |
447
447
  | `scripts/run-session-policy-deliverable-test.mjs` | Which of a batch of user-written permission rules can be written into a session’s own rule record without changing their meaning, and why each of the others cannot. The record holds whole tool names and command names only, so exactly one class maps across losslessly: a deny rule that names one tool with no qualifier. Everything else is withheld with one word from a closed seven-word list — an ask rule (the record has no ask tier), a deny rule with a parenthesised qualifier (recording just the name could block more), a rule that names a server or agent peer without naming one of its tools (for every protocol namespace the engine knows, checked against the engine package's own table) or contains a wildcard (*) anywhere (an engine that compares exact names would block nothing), an entry that is not a tool name, and a name the engine refuses at the start of every run — a retired tool name, or one containing "__" without a protocol prefix, where the prefix check is case-sensitive (once such a name is in the record, every later run of the session fails at startup until that entry is removed; the retired-name list is checked entry by entry against the engine package's own list, and a withheld retired name carries its current name when the engine says it was renamed), and — only when the caller passes the tool roster of a run — a rule for which the engine's own matching finds no tool in that roster: no tool name or alias equal to it, compared exactly with letter case counted, and, for a name the engine also reads as covering every tool of a server, no listed tool of that server (recorded as it is it would block nothing on that deployment) — and each word has one sentence, which never echoes the rule itself; asking for the sentence never throws, even with a value that throws when turned into a string. A name with leading or trailing whitespace counts as not a tool name: the record compares exact bytes, so it would block nothing. The guard pins the batch semantics: the deliverable part is either the whole batch or empty, never a subset, so a caller cannot send half a change and report it as saved. An end-to-end check runs the engine package itself: every batch this function would deliver — the recorded vectors and a fixed-seed sample of generated names — is written into an in-memory session rule store and the next run must get past its start-up checks and reach the model, while every name withheld as refused — every retired name included — must indeed make that run fail at start-up, and every string literal in the judgement source that it withholds as refused must be one the engine package's own tables refuse. It also checks that malformed input never throws and never delivers anything (non-arrays, non-string entries, holes, a polluted array prototype, a length or index that throws, a changing index read once), that a batch which cannot be read at all is marked `unreadable: true` while an empty batch is not, so the two stay tellable apart, that each word is produced by some vector and nothing outside the list is produced, and — when a checkout of the previous in-client implementation is present — that this function gives the same answer on every recorded vector and on tens of thousands of generated rules and pairs, except for five deliberately stricter classes (whitespace-padded names; rules with a wildcard anywhere, which the previous implementation sent as exact names unless the wildcard was the whole tool part of a server rule; peer-wide rules outside the MCP namespace, which it did not recognise; names the engine refuses at start-up, which it sent as ordinary names; and names the engine's rule compiler does not accept as one whole tool, such as an escaped parenthesis or a backslash in the name), whose disagreements are counted per class and must match an independent count exactly. The optional roster only ever withholds more: with no roster, or an absent or null one, every answer is byte-identical to the last release before the roster existed, checked against that release's published file over tens of thousands of inputs, apart from the rule-syntax class described below; a name withheld without a roster stays withheld with the same word, and the same current name, when the roster lists it as a name or an alias; a name delivered without a roster is withheld as not in the roster exactly when the engine's start-up name audit would find it matches no tool in that roster — an exact name or alias, or, for an MCP name the engine reads two ways, a tool its covering reading reaches (that covering check asks the installed engine's own covering matcher; when it is not installed or cannot answer, a batch that needs it is marked unreadable instead of falling back to exact names); the engine's own namespace-covering forms that are withheld without a roster keep their word; and a roster or options value that cannot be read marks the whole batch unreadable instead of falling back to no roster, while a readable empty roster is a roster. An end-to-end check takes the roster from a real run of the engine package that mounts a tool with an alias: every name delivered with that roster passes the engine's start-up name audit without being reported, and every name withheld as not in the roster is one that audit reports as matching no mounted tool. When the previous in-client implementation carries its own roster check, the two are compared under a given roster and may differ only for an alias (delivered here, withheld there) and for a refused name that the roster lists (withheld here, delivered there). A deny rule is delivered only when the installed engine's rule compiler, asked about that one string the same way the request-body permission assembly asks about whole tool names, accepts it as a rule for one whole tool: a name with an escaped parenthesis, a backslash, a lone closing parenthesis or more characters than a rule may have is withheld as not a tool name, because the record would compare those bytes and match no tool. When the compiler is not installed or cannot answer, a batch that needs it is marked unreadable rather than delivered, while batches that never reach that step answer as before. The guard checks that every delivered name is one the compiler accepts, that nothing delivered now was withheld by the earlier release and the extra withholding is exactly that class, that a roster never makes such a name deliverable, that the compiler of every engine generation in the server fixtures gives the same answers, and that the request-body assembly reaches the same verdict on the same names. Since 0.99.0 the cells that depend on the engine's tool-name grammar take their expectation from the installed engine generation, which refuses blank, invisible and wildcard-only tool names. |
448
- | `scripts/run-session-policy-refused-removal-test.mjs` | The way out for a session that already carries a tool name the engine refuses. The engine refuses to start a run when any rule applied to it names a tool its own tool-name check does not call current — a retired tool, a name containing "__" without a protocol prefix, or (engines from 7.48.0) a protocol name whose server or peer segment it never mints — so a session whose own rule record holds such a name fails at startup on every run. The guard pins three pieces. First, the refusal test itself: since 0.97.0 it is the engine's own tool-name check read through `ruleToolNameVerdictOf`, compared name by name, in both directions, with that check called directly over a corpus that includes padded, wildcard and parenthesised forms, and it is shown to be wider than the verdict used before a write — a padded or wildcard form is withheld there for another reason yet still refuses the run, so a removal driven by that verdict would leave the session broken. Every corpus name is then written alone into the deny list and into the allow list of a real engine run: the test says "refused" exactly when the run fails at startup with the refusal code and the model is never called, while the same shapes in the command lists are not audited at all. Second, the removal verb: it takes only the refused entries out of those two lists and leaves every other entry, list and ordering byte-for-byte, keeping an emptied allow list as a present empty list; after it runs, the same session's next real engine run gets past startup, for every refused name in the corpus and in both lists. Against the installed engine as an ordinary caller, taking a deny entry out is refused as a loosening — reported as such, written exactly once, the record unchanged and the next run still refused, never a false success — while an allow-list-only removal, which narrows, succeeds; against a model of the newer contract the engine has announced, the removal succeeds, and the same model still refuses a removal of anything else. A concurrent writer causes exactly one re-read, judged again on the other writer's record, and a second collision is reported rather than retried; read failures, coded write refusals and unconfirmed writes (no verdict, a receipt that cannot be read, or a receipt that does not account for the removal, including one that still holds a removed entry) each land in their own arm, and running the verb again after an unconfirmed write reports nothing left to remove. Third, the wording: one sentence per outcome; the loosening refusal names operators and says the record is unchanged, the unconfirmed one says the change may already be in effect, and none carries a rule string or engine text. The startup-failure sentence answers to that one code only, lists every place the entry can be set without naming who may remove it, and a real run with the name in the caller's own policy rather than in the session record yields the same code — which is why the sentence may not claim the entry is in the session record. The verb also requires the tool roster reported by a run on the deployment: a name listed there, as a tool name or an alias and compared exactly as the engine compares it, is left in place even when it is a retired name, because the engine accepts that rule — with a real engine run that mounts a deployment tool under a retired name (or with that name as an alias), the removal keeps the deny rule and the next run still starts. Without a roster, or with one that cannot be read, nothing is read or written and the outcome says why; a readable empty roster is still a roster. The removal sentence speaks separately about names taken out of the deny list and out of the allow list. |
448
+ | `scripts/run-session-policy-refused-removal-test.mjs` | The way out for a session that already carries a tool name the engine refuses. The engine refuses to start a run when any rule applied to it names a tool its own tool-name check does not call current — a retired tool, a name containing "__" without a protocol prefix, or (engines from 7.48.0) a protocol name whose server or peer segment it never mints — so a session whose own rule record holds such a name fails at startup on every run. The guard pins three pieces. First, the refusal test itself: since 0.97.0 it is the engine's own tool-name check read through `ruleToolNameVerdictOf`, compared name by name, in both directions, with that check called directly over a corpus that includes padded, wildcard and parenthesised forms, and it is shown to be wider than the verdict used before a write — a padded or wildcard form is withheld there for another reason yet still refuses the run, so a removal driven by that verdict would leave the session broken. Every corpus name is then written alone into the deny list and into the allow list of a real engine run: the test says "refused" exactly when the run fails at startup with the refusal code and the model is never called, while the same shapes in the command lists are not audited at all. Second, the removal verb: it takes only the refused entries out of those two lists and leaves every other entry, list and ordering byte-for-byte, keeping an emptied allow list as a present empty list; after it runs, the same session's next real engine run gets past startup, for every refused name in the corpus and in both lists. Against the installed engine as an ordinary caller, taking a deny entry out is refused as a loosening — reported as such, written exactly once, the record unchanged and the next run still refused, never a false success — while an allow-list-only removal, which narrows, succeeds; against a model of the newer contract the engine has announced, the removal succeeds, and the same model still refuses a removal of anything else. A concurrent writer causes exactly one re-read, judged again on the other writer's record, and a second collision is reported rather than retried; read failures, coded write refusals and unconfirmed writes (no verdict, a receipt that cannot be read, or a receipt that does not account for the removal, including one that still holds a removed entry) each land in their own arm, and running the verb again after an unconfirmed write reports nothing left to remove. Third, the wording: one sentence per outcome; the loosening refusal names operators and says the record is unchanged, the unconfirmed one says the change may already be in effect, and none carries a rule string or engine text. The startup-failure sentence answers to that one code only, lists every place the entry can be set without naming who may remove it, and a real run with the name in the caller's own policy rather than in the session record yields the same code — which is why the sentence may not claim the entry is in the session record. The verb also requires the tool roster reported by a run on the deployment: a name listed there, as a tool name or an alias and compared exactly as the engine compares it, is left in place even when it is a retired name, because the engine accepts that rule — with a real engine run that mounts a deployment tool under a retired name (or with that name as an alias), the removal keeps the deny rule and the next run still starts. Without a roster, or with one that cannot be read, nothing is read or written and the outcome says why; a readable empty roster is still a roster. The removal sentence speaks separately about names taken out of the deny list and out of the allow list. A last check reads the installed engine package's own files to confirm where that refusal code is minted — one constant definition, one throw site that uses it, and no second spelling of the code outside the engine's code registry — for each layout the engine has had; the reader is itself checked against synthetic package trees (the real layouts accepted, the broken ones refused), and this check does not need the older-engine fixture, so it runs in every environment. |
449
449
  | `scripts/run-rules-legacy-tool-name-projection-test.mjs` | The retired session-rule write refusal for tool names, pinned as retired. Servers before 7.129.0 refused a session rule write naming a retired tool or a name with "__" but no protocol prefix with a dedicated code; from 7.129.0 that code is gone and the same names are refused with the inert-entry code, which this package reads as its own named outcome. With the support floor at 7.129.1 the old code is no longer read: its constant is gone from the root entry, and when an older server still sends it the failure classifier answers the generic answered-refusal arm (no list, name, verdict or current name is carried) for any 4xx status, while other statuses follow the usual source rules; the family prefix test still recognises the code. Against a model of the old refusing write gate, the tightening flow answers unknown with a refused write, writes exactly once and leaves the stored record untouched, and the removal verb answers a refused write; the before-write withholding of such names is unchanged. The run-startup refusal sentence is still given for the startup code only, the old write sentence is no longer given for either the old code or a hand-made old outcome, and the cross-copy body-slot read is pinned on the live inert-entry arm instead. |
450
- | `scripts/run-persisted-rule-write-test.mjs` | The **single-step tightening write** for persisted permission rules — the dual of the revoke surface, and the half where a hopeful reading is expensive. The two behaviours this entry accepts are **derived** from the three-state vocabulary by subtracting the widening one, never hand-copied: the guard bites in both directions (every word in the derived table is really accepted; every constructed outsider — casing variants, trailing whitespace, the widening word itself — is refused before a single round trip), keeps a word-count canary against the parent table, and pins that the source file contains **exactly one** array literal carrying two or more behaviour words, so a second hand-written table shows up as a boundary failure rather than as drift nobody reads. A standing approval is minted by answering a permission question or by importing settings; this entry is not a third route, and the widening word is unspellable in the type. The outcome is a discriminated union whose two failure arms are **not** interchangeable: ten refusal causes each promise the same single thing — not one byte reached the store — while three separate words say the opposite, that the outcome could not be read at all. The service's own "I cannot tell" (a write that could not be confirmed as standing: store wobble, a redemption leg with no decidable ending, or a write that landed and was revoked concurrently before the read-back) stays in the second group, because announcing "nothing was written" invites a clean retry that is not clean, and announcing success misreports a tightening that may already be gone. Anything the shared failure classifier does not recognise defaults to the same place — this is a non-idempotent verb, so "unclassified" must mean "unknown", never "no write": a 500 can happen after the store commits. A 2xx whose body cannot be read is pinned in the same direction and from both sides: it reads as unknown, and the unknown arm carries **neither** the revision nor the written row, so a consumer cannot even spell the shape that would let "unreadable" pass for "written". `persisted` is guarded against the reading everyone reaches for first: it says *this call wrote*, not *a new rule now exists* — an equivalent rule already in the store still mints a fresh causal point, so the lane honestly reports `persisted`, and the material for judging whether the **logical** rule is new (the approval ledger on the returned row) is handed to the caller rather than folded into the discriminant, since the package does not have the one fact that judgement needs. The returned row goes through the **same single narrower** the listing surface uses — proven by running one row corpus through both legs and asserting the two verdicts agree entry for entry (an adversarial pass that forks the listing leg back into an inline copy reds here immediately), plus a source pin that the predicate is defined once and called from exactly the two legs. That sharing is what keeps a row whose behaviour cell is unreadable **visible in both places** rather than hidden by one of them — and the shared narrower is deliberately followed by a second, *different* question only the write leg can ask: is the row that came back **the rule that was just sent**? A rule's identity is a triple, so a receipt missing its behaviour cell, carrying the sibling state, carrying the widening one, or naming another text or another scope is not evidence that the requested tightening is standing; it reads as unknown with its own word, kept distinct from "unreadable" so the two stay tellable apart, while display and derived cells may vary freely. An adversarial pass found both of the gaps this pins: the receipt check that only looked at whether the row was renderable, and a subtler one — pulling the verb off the injected port and calling it bare drops the receiver, so a host that hands over a real resource object (a class instance whose verbs reach the transport through `this`) would see every write throw and be reported as "could not tell", retry after retry, while the package's three other ports call their verbs as methods and work fine. Both are pinned from the failing side: a shorthand-method facade and a class-instance facade must reach the transport and return a real outcome, with the bare-call throw proven to be a real failure mode first. A 405 is split in two, because only the engine's own bare code is evidence about **the engine**: with it, the path exists and this verb does not, so this worker predates the verb; without it — an absent, empty, or foreign code, which is what a proxy or gateway blocking the method typically returns as HTML or an empty body — what was seen is that the verb was refused, while **who** refused it and **at which hop** is unknown, so it lands in the unreadable-outcome arm with its own word rather than sending someone to upgrade a worker that is fine, hiding an entry that is live, or — the part a second adversarial pass insisted on — promising that nothing was written. That promise is what the refusal group means, and a middlebox is free to forward the request and only then answer 405 on its own policy, so a caller who skipped reconciliation on that word would leave behind a standing refusal the user believes never took effect; the guard pins exactly that shape, with a double that writes the rule and *then* answers 405, and with the engine's own bare code still landing in the refusal group beside it. (The same passes caught the naive status-only reading and the asymmetry where an empty-string code fell through to a different bucket.) The documented recovery for an unreadable outcome — retry, then reconcile — is pinned to be **ledger-safe** rather than merely asserted: against a double that models the engine's own "is this identity already standing?" question, re-sending the same identity comes back as a no-op with the approval ledger and the bucket revision both unmoved, however many times it is repeated, while two concurrent writers each landing a causal point are both honestly reported as having written. Finally the four local gates are pinned to be free: a missing write verb on the injected port, an unwritable direction, an unreadable identity pair, and a principal key that is present but cannot name anyone all refuse **without sending anything** — the last of those because silently degrading a blank target into absence would land a tightening aimed at someone else in the caller's own bucket and return a 200; an empty operator list (501 `capability.operator_principals_required`) is reported as a refusal (`operators_unconfigured`) rather than an unknown outcome |
450
+ | `scripts/run-persisted-rule-write-test.mjs` | The **single-step tightening write** for persisted permission rules — the dual of the revoke surface, and the half where a hopeful reading is expensive. The two behaviours this entry accepts are **derived** from the three-state vocabulary by subtracting the widening one, never hand-copied: the guard bites in both directions (every word in the derived table is really accepted; every constructed outsider — casing variants, trailing whitespace, the widening word itself — is refused before a single round trip), keeps a word-count canary against the parent table, and pins that the source file contains **exactly one** array literal carrying two or more behaviour words, so a second hand-written table shows up as a boundary failure rather than as drift nobody reads. A standing approval is minted by answering a permission question or by importing settings; this entry is not a third route, and the widening word is unspellable in the type. The outcome is a discriminated union whose two failure arms are **not** interchangeable: ten refusal causes each promise the same single thing — not one byte reached the store — while three separate words say the opposite, that the outcome could not be read at all. The service's own "I cannot tell" (a write that could not be confirmed as standing: store wobble, a redemption leg with no decidable ending, or a write that landed and was revoked concurrently before the read-back) stays in the second group, because announcing "nothing was written" invites a clean retry that is not clean, and announcing success misreports a tightening that may already be gone. Anything the shared failure classifier does not recognise defaults to the same place — this is a non-idempotent verb, so "unclassified" must mean "unknown", never "no write": a 500 can happen after the store commits. A 2xx whose body cannot be read is pinned in the same direction and from both sides: it reads as unknown, and the unknown arm carries **neither** the revision nor the written row, so a consumer cannot even spell the shape that would let "unreadable" pass for "written". `persisted` is guarded against the reading everyone reaches for first: it says *this call wrote*, not *a new rule now exists* — an equivalent rule already in the store still mints a fresh causal point, so the lane honestly reports `persisted`, and the material for judging whether the **logical** rule is new (the approval ledger on the returned row) is handed to the caller rather than folded into the discriminant, since the package does not have the one fact that judgement needs. The returned row goes through the **same single narrower** the listing surface uses — proven by running one row corpus through both legs and asserting the two verdicts agree entry for entry (an adversarial pass that forks the listing leg back into an inline copy reds here immediately), plus a source pin that the predicate is defined once and called from exactly the two legs. That sharing is what keeps a row whose behaviour cell is unreadable **visible in both places** rather than hidden by one of them — and the shared narrower is deliberately followed by a second, *different* question only the write leg can ask: is the row that came back **the rule that was just sent**? A rule's identity is a triple, so a receipt missing its behaviour cell, carrying the sibling state, carrying the widening one, or naming another text or another scope is not evidence that the requested tightening is standing; it reads as unknown with its own word, kept distinct from "unreadable" so the two stay tellable apart, while display and derived cells may vary freely. An adversarial pass found both of the gaps this pins: the receipt check that only looked at whether the row was renderable, and a subtler one — pulling the verb off the injected port and calling it bare drops the receiver, so a host that hands over a real resource object (a class instance whose verbs reach the transport through `this`) would see every write throw and be reported as "could not tell", retry after retry, while the package's three other ports call their verbs as methods and work fine. Both are pinned from the failing side: a shorthand-method facade and a class-instance facade must reach the transport and return a real outcome, with the bare-call throw proven to be a real failure mode first. A 405 is split in two, because only the engine's own bare code is evidence about **the engine**: with it, the path exists and this verb does not, so this worker predates the verb; without it — an absent, empty, or foreign code, which is what a proxy or gateway blocking the method typically returns as HTML or an empty body — what was seen is that the verb was refused, while **who** refused it and **at which hop** is unknown, so it lands in the unreadable-outcome arm with its own word rather than sending someone to upgrade a worker that is fine, hiding an entry that is live, or — the part a second adversarial pass insisted on — promising that nothing was written. That promise is what the refusal group means, and a middlebox is free to forward the request and only then answer 405 on its own policy, so a caller who skipped reconciliation on that word would leave behind a standing refusal the user believes never took effect; the guard pins exactly that shape, with a double that writes the rule and *then* answers 405, and with the engine's own bare code still landing in the refusal group beside it. (The same passes caught the naive status-only reading and the asymmetry where an empty-string code fell through to a different bucket.) The documented recovery for an unreadable outcome — retry, then reconcile — is pinned to be **ledger-safe** rather than merely asserted: against a double that models the engine's own "is this identity already standing?" question, re-sending the same identity comes back as a no-op with the approval ledger and the bucket revision both unmoved, however many times it is repeated, while two concurrent writers each landing a causal point are both honestly reported as having written. Finally the four local gates are pinned to be free: a missing write verb on the injected port, an unwritable direction, an unreadable identity pair, and a principal key that is present but cannot name anyone all refuse **without sending anything** — the last of those because silently degrading a blank target into absence would land a tightening aimed at someone else in the caller's own bucket and return a 200; an empty operator list (501 `capability.operator_principals_required`) is reported as a refusal (`operators_unconfigured`) rather than an unknown outcome Since 0.109.0 a principal key hidden by a lying has trap refuses the whole revoke or write instead of being read as acting on the caller own bucket. |
451
451
  | `scripts/run-gate-negative-controls-test.mjs` | Whether the registry-shaped guards among the suites above actually turn red when the material they check really breaks — a census found the ones clean enough to rehearse safely (closed sets, mirrors, baselines, floors, a type-shape ratchet) without touching any judgement code. Each is exercised by tampering a disk copy of the real material, spawning the guard's own unmodified script, asserting it exits non-zero and names the disease, then restoring the file byte-for-byte. The guards of the same shape that are too costly to rehearse, and the behaviour/projection suites that red on their own assertions, are catalogued rather than rehearsed — see `docs/GATE-NEGATIVE-CONTROLS.md` for the three tables (all generated from `gates-manifest.json`, which carries each suite's classification), the reasons, and a one-minute manual replay recipe for each blind one. The suite reconciles its case list against that classification by name in both directions, so a case quietly dropped from the array without the manifest following is itself an undeclared blind guard. The backup that makes the restore possible is taken by **exclusive create**: checking for it and then copying are otherwise two steps, and two instances can pass the check together — the later one overwrites the only clean copy with material the earlier one has already tampered, and the rehearsal that promises to leave no trace leaves a permanently corrupted file instead. That interleaving is rehearsed too, in a throwaway directory of its own. A run that is interrupted — by the suite runner's time limit, by Ctrl-C or by its terminal closing — restores what it was in the middle of tampering with before it exits: the rehearsed guard's whole process group is stopped first, so nothing that guard started can write the tampered state back afterwards; the file is restored byte-for-byte from its backup and the backup removed only once the bytes match; the build output is regenerated from the restored sources within the grace period the runner allows (a rebuild that would overrun it is cut short, its compiler stopped, and the run says the build output was not regenerated); and the run exits with 128 plus the signal number rather than passing. Before the first rehearsal the suite also looks for backups left behind by an earlier run that could not clean up — in the trees its targets live in, following a symlinked dependency directory — and if it finds one it refuses to start, names the file and how to check and restore it, and leaves the backup untouched rather than overwriting what may be the only clean copy. |
452
452
  | `scripts/run-registrar-tables-test.mjs` | The four registrar table bodies — this Guards table and the three census tables in the repository's negative-control record — are **generated** from `scripts/gates-manifest.json`, the one file that describes a suite. Every row's text must equal what the generator emits, the manifest's suite set must equal the suites on disk, each entry must declare how it is negative-controlled (rehearsed, blind, or behavioural, with the census taker itself declared as such since it does not appear in its own tables), and each row's outward prose is scanned against the published-surface word list — the same list the packaging-hygiene guard uses, shared rather than copied — before the generator may write it into this file. Adding a guard is therefore one manifest entry plus one generator run instead of six hand edits across three files, and a description that drifts in one place and not the others stops being expressible. The row count is no longer what is compared: the earlier arrangement checked the census tables by **length**, so rows naming the wrong suites reconciled green. Positive controls run entirely on in-memory copies — a changed description, a dropped entry, an added entry, a changed class and a hand-edited row on disk each have to make the same judgement speak — and the quieter halves are pinned too: nothing outside a table body may move, a line inside one that is not a recognisable row makes the generator refuse rather than drop it, byte equality is backed by a column-count check (a cell holding a bare pipe splits a row into extra columns, and a code span does not protect it), and the malformed rows kept byte-for-byte as they are found are registered individually, so the registration turns red the day it stops being needed rather than outliving its reason — audited in both directions, since a registration pointing at a row that is no longer malformed and one pointing at a guard that was reclassified or deleted are both exemptions nobody reads |
453
453
  | `scripts/run-cc-message-key-census-test.mjs` | The one rule behind CC-shaped messages this package emits: every top-level key on a `user` / `assistant` / `result` / `system` message must be a member the CC SDK mirror (`@sema-agent/agent-types`) declares for that same arm, or one of this package's `_sema_`-prefixed additive keys, or a row in a dated transition table that goes red the day its retire version arrives. Mint sites are found syntactically (identifier, quoted and computed `type` names alike) and their key sets are resolved syntactically too — inline literals, both branches of a conditional, the nullish-coalescing and logical or/and operators, `const` initializers and every `return` of a helper — so a type assertion, a `Partial<Pick<…>>` narrowing or a computed name cannot launder a key past the check, while a spread the tool cannot follow (a parameter, a `let`, a member access) fails the tool, never the product. Arms that carry a `subtype` discriminator are checked against that subtype's own member set, so a key declared only for another subtype does not pass on the strength of the arm-wide union. The runtime section drives the real adapter pipeline and pins the tool-result record's SDK-spelled `tool_use_result` (the transitional camelCase twin rides along by reference until 0.83.0). A second runtime section feeds the same tool-result frame to both the interactive adapter and the non-interactive frame builder and requires the structured-result key and the no-output marker to be present together, absent together and equal on the two outputs, while the transitional camelCase name stays on the transcript record only; a static section checks a per-key routing table for the internal tool-result frame against the key sets resolved at the three mint sites, in both directions, and requires the non-interactive frame's structured-result value to trace back to the very same syntax node the transcript record uses — so recomputing or copying that logic on the non-interactive path fails even when the values happen to agree. |
@@ -457,11 +457,11 @@ guard still cross-checks the table by name).
457
457
  | `scripts/run-memory-saved-projection-test.mjs` | Engine memory writes (a successful `Remember` tool call) moved off the transcript onto the additive `memory_saved` chrome event, driven through the real pipeline: zero transcript rows for the write (the transcript is byte-identical to the same frames with the write reported as not successful), exactly one event whose `notes` carry the note text verbatim (notes, not file paths) and whose key set is exactly kind / laneProof / id / notes; no event for a missing, empty or non-string note, a non-`true` `ok`, a tool error, a missing result or another tool name; two writes give two events in order with distinct ids; a sub-agent write rides the sub-agent lane and an empty parent id emits nothing rather than falling back to the main lane; the `id` is derived from the write's wire key (the tool-end event id, else the tool-start event id, else the call id; empty ids count as absent), so projecting the same wire events twice gives the same id, and it never collides with the tool result row of the same or another call; the event sits right after the tool result row; the arm is registered as required. |
458
458
  | `scripts/run-result-frame-projection-test.mjs` | Result frames and the synthesized terminal rows. The CC key `terminal_reason` is minted on result frames only where it follows from what the engine reported: `completed` on success, `max_turns`, `budget_exhausted` and `structured_output_retry_exhausted` for the three matching engine codes, on both the done-frame path and the failed-event path. Every other outcome leaves the key absent as an own property rather than present with an undefined value: wall-clock and token-budget limits, the classifier denial limit, cancellation, unknown codes, blocked, paused, unreadable or missing terminal records, and the busy-session refusal. The public reader `terminalReasonForResult` shares the minting predicate and is checked to agree with the minted key on every frame the gate produces. Both the minted key and the reader derive the word from the frame's CC subtype (success with `is_error` strictly false, and the three limit subtypes), not from the error code, so a replayed row whose status is paused, blocked or unrecognised never carries a word that contradicts its subtype. The four words are checked against the mirrored CC union, and the minting file is checked to hold no hand-copied code literals. The renamed superset keys (`_sema_error_code`, `_sema_salvaged_result`, `_sema_model_degraded`, `_sema_selected_model`, and the row flag `_sema_api_error_message`) are driven through the real stream pipeline. Each must be present under its new name, the old name must be absent, and every frame the gate saw is swept for old names. The selected model appears on error envelopes whenever the terminal record carries it, and never on a failed event, which has no record. It stays separate from the provider-reported model name. The two in-package readers still work: the interactive result arm reads the salvaged text under its new name, and the print init gate treats the renamed flag as the run having ended. Since 0.86.0 the result arm no longer reads `result` on an error envelope, meaning a frame whose subtype is a string starting with `error_`, including error subtypes it does not recognise yet; a compile-time check pins that every CC error subtype carries that prefix. Such an envelope that carries the salvaged text only under the old `result` key yields no answer row, no appended suffix and no divergence event; when both names are present, or the new one is present but not a string, the old one is never read. Everything else reads `result` exactly as before, in all three producing cases (whole answer, missing suffix, divergence): success frames, a success frame flagged `is_error: true` (checked equal to the same frame with `is_error: false`), and frames whose subtype is a string without that prefix (empty, unrecognised, `Success`, a bare `error`), absent, or not a string (each checked equal to the same frame with subtype `success`). From 0.94.0 four root readers return the result frame's string superset keys (`resultErrorCodeOf`, `resultSelectedModelOf`, `resultSalvagedResultOf`, `resultParkFailedReasonOf`): they read only the frame's own data properties, never fall back to the retired names, run no getter and never throw. A synthesized terminal row that carries `_sema_api_error_message: true` has its text credential-washed on both projection paths (minted by the stream projection, or rebuilt by `adapt()` from an older transcript row), so the flag itself tells a client the text is already washed; model rows are left byte-for-byte alone. |
459
459
  | `scripts/run-layering-shadow-export-test.mjs` | Same-name shadows across the first-party clients that consume this package (terminal, desktop, web and the admin console). Each client's product sources are read at the local clone's `origin/main` (or its HEAD when there is no such ref), without fetching, and parsed with the TypeScript parser; every top-level runtime export the client declares itself is compared with this package's public runtime exports. The guard prints which ref, commit and commit date it read for each client, and warns (without failing) when that commit is more than seven days old, because the result then only describes that older snapshot. A client-side declaration carrying the name of a package export means a piece of shared logic now lives in two places and can drift apart. It fails the guard unless it is listed in `scripts/layering-shadow-exemptions.json`, and a listed row must carry one due-by version no more than three minor lines ahead (it fails once the package reaches it; that single field is the only one the guard reads for expiry, and the older retire-by and review-by fields that still sit on the rows for one more version are only checked to agree with it). It also fails once the client has removed the shadow and the row still stands. Every row also names what kind of duplicate it is, and carries the evidence that kind requires. A client copy that is this package's own function object behind a type assertion, or a thin wrapper that only passes the client's own dependencies into this package's function, must be proven so on every run by reading the client's syntax tree; such rows are due for review within two minor versions, and the guard fails as soon as the proof no longer holds. A wrapper whose extra logic the client has confirmed to be host-specific carries that confirmation (the client's own wording and where it was stated) together with a pinned review record that is due within two minor versions. A wrapper that adds its own decisions, or a same-name function that does something else, carries a review record pinned to a hash of the normalized client declaration (comments and formatting do not count); once the client's copy changes, the guard fails until it is reviewed again. A second implementation of the same logic carries a semantic diff: the client's copy is taken from the commit being read, the named declarations and the client modules they import are extracted with the TypeScript parser, transpiled and run in memory against this package's build on the same inputs, and any disagreement outside the classes registered on that row fails the guard (each class is a fixed predicate over both answers). A separate register lists same-meaning copies that carry a different name (a name-based comparison cannot see those): each row names the client declaration and the package export it duplicates, and fails when it is malformed, past its due-by version, or when the client no longer declares that name; the register only records and checks liveness and expiry, it does not scan for such copies. A row can follow a client-side rename to a new name, and can be registered ahead of a client change for a limited number of versions before the client code exists. Re-exports of this package's own exports are the intended form and never count. A client tree that is not present is reported as a skipped section, not as a pass. The ruler proves itself on an in-memory fake client (planted shadows must be caught, legal forms must not), on a throwaway repository (a missing `origin/main` falls back to HEAD, a broken one is a fault rather than a silent fallback), and refuses to report zero on a client whose scan surface is empty. |
460
- | `scripts/run-display-untrusted-projection-test.mjs` | The single display-safety outlet (`displayUntrusted`) and the credential wash on the end-of-run rows this package mints. The outlet composes two credential nets (URL structure: userinfo, every query value, the fragment, path parameters and path segments that start with a known secret prefix; key/value words such as `Authorization: Bearer ...`, `Authorization: token ...` or `api_key=...`, plus well-known secret literals that appear without a label, such as `sk-...`, `ghp_...`, `AKIA...`, JWTs and the body of a PEM private key) with three character nets (control characters, bidirectional and format characters, whitespace folding). The credential nets match on a view of the text with ANSI sequences, format characters, control characters and the outlet's own escape tokens stripped, and map the result back onto the original, so colouring or an invisible character wedged between a label, its separator and its value cannot hide the value, and no stray marker is left behind. Whitespace of any length around the separator is accepted. Hosts, ports, paths, query key names and surrounding prose stay byte-for-byte, clean text comes back unchanged, the result is idempotent (also with a length cap), a length cap never splits an escape token or a surrogate pair, an invalid cap means no cap, and every net can be switched off on its own. A few narrow shapes are left alone because they name something rather than carry a value (a plain English word after `bearer` or `basic`, a back-quoted credential variable name, a plain integer after `tokens:`, a list of key names after `keys:`), each with a counter-example that is still washed. Regional flag emoji built from tag characters are kept whole. The existing single-line helpers (`escapeDisplayControlChars`, `collapseLabel`, `capForDisplay`, peer sender names and the hook failure banner) run on the same engine and, since 0.96.0, read the same character table as the outlet: each is checked equal to the outlet with both credential nets off (escape without folding, fold then escape, space mark then fold, and the same length cap) over a seeded pool of more than 600 strings; every character class the old sets left alone (zero-width characters, the word joiner, invisible operators, U+FEFF, the soft hyphen, tag characters and other format characters, plus C1 controls, bidirectional controls and lone surrogates in sender names) is marked class by class, while joiners inside emoji sequences and joining or Brahmic scripts and RGI regional flags stay as written; no character the old sets marked is ever left raw, input without the newly marked characters stays byte-identical to the previous output over every BMP code unit plus random strings, all of them are idempotent, and neither `capForDisplay` nor the hook banner's 512-character cap splits a `\u{…}` escape token or a surrogate pair. The approval decision-note echo, the subagent resume receipt (and its failure debug line) and the startup list of plugin hooks that will not run now also drop bidirectional and format characters (and, for the receipt, C1 controls); a note that is empty after cleaning is treated as absent. The synthetic end-of-run rows (`API Error:`, `Run stopped:`, `Model output error:`, `Outcome unknown:`) and the result frame's `errors[]` pass both credential nets before they leave the package, on the print lane and on the interactive lane (which also keeps the row-class flag); this covers a blocked reason whoever wrote it, while assistant text rows, a successful `result` and salvaged output are never touched, and a non-string `errors[]` entry is passed through unchanged. The known-secret-prefix check is a local copy of the configuration package's detector and is compared with the installed one entry by entry. Since 0.83.5 the outlet has two opt-in switches and a position read-out. `escapeBackslashes` (escape form only) writes every literal backslash as a pair, so each output decodes back to exactly one input (a real invisible character and its literal six-character spelling no longer look alike); a reference decoder round-trips thousands of random strings, the output is byte-identical to the default when the input has no backslash, a length cap is measured on the paired output and keeps the longest fitting prefix, credentials are washed exactly as in the default, and the switch is not idempotent by design (use it only at the final render). Zero-width joiners and non-joiners are kept only inside emoji sequences drawn as emoji (so not between symbols such as © or ™ that display as text) and between letters of scripts where they change the shaping (joining scripts such as Arabic, and the Brahmic family), each listed script checked both ways; the one other place a joiner is kept is right after a virama at the end of a word, the older spelling still found in Malayalam and Bengali text. Next to Latin, Cyrillic, CJK and other letters, next to modifier letters shared across scripts, at the start of a word, at the end of a word without a virama before it, or on their own they are now marked. `blanks` marks characters that look like a space but are not an ASCII space (no-break and other width spaces, the ideographic space, Hangul fillers, the blank Braille pattern) before whitespace folding, for names that must never look alike. `displayUntrustedMarks` returns the same text plus the position of every character mark; its text is compared with the outlet over thousands of inputs. With both credential nets off, the character face stays byte-identical to the previous release for input without joiners. The credential nets read escape sequences the way a terminal would when one is cut short: an unfinished colouring or character-set sequence interrupted by another one is dropped as a whole, a sequence never takes the `@` of an address as its final character, and a final character that starts a well-known secret literal (`sk-`, `ghp_`, `AKIA`, a JWT) is also read as the start of that literal; escape tokens this outlet writes are read as one unit, while look-alike text it never writes (an upper-case `\U`, or a code point it never marks) is read as plain text. A URL is cut before a run of non-ASCII blanks followed by a credential label or scheme word, Hangul fillers and the blank Braille pattern count as spaces around a label's separator, and a value that itself starts with a quoted label (`token= "password":"..."`) is left to that inner label. With `blanks` on, a blank written as an escape token right after a label is read as a blank when the value is judged, so `password:` followed by a no-break space and `missing` stays unmasked and an empty value gets no marker; the credential nets also read the text the way it looks after default whitespace folding and combine what each reading masks, so whatever the default form masks stays masked with `blanks` on (checked over a seeded corpus for the escape form, with paired backslashes and without folding; the exceptions are text that itself contains a literal six-character blank escape, which cannot be told apart from one the outlet wrote, and the dot and space marks, which cannot tell a marked blank from a real dot or space). A lone surrogate wedged between a label, its separator and its value no longer hides the value: the credential nets treat it exactly like a format character, in the machine-readable wash, in a single pass of the display outlet, and on the end-of-run rows and the result frame's `errors[]` on both lanes, while lone surrogates anywhere else are left byte-for-byte. Since 0.84.1 an address-shaped value (`scheme://…`) that sits right after an invisible single character — a format character, a lone surrogate, a non-whitespace control character, or the outlet's own escape token for one — is no longer let through as an address: the scheme stays and the host and path are replaced, with the query and fragment masked as before; `user:<password>@` followed only by stripped units and then a boundary, a port or another `@` is masked as userinfo. A value after real whitespace or after a colour sequence is still treated as an address, and clean text stays unchanged; forms that the previous release masked are checked not to leak on the same outlets. A `user:<password>@` candidate that is the value of a credential label or scheme word — including a label or scheme word split by stripped characters, and a quoted value that goes on past whitespace — is left to the label pass, so the whole value is masked as in the previous release; both reported shapes and frozen samples from the targeted pools are checked on every outlet. A scheme-less `user:password@host` whose password holds a double quote, backtick or angle bracket is still masked when a credential label or scheme word sits right before the user name, or when the user name is a plain word that is not itself a label, the host has a real shape and the address is not wrapped by that quote; quoted JSON values, quoted or bracketed e-mail addresses and quoted arrays stay byte-identical. When a label inside a quoted value ends right at that value's closing quote, the quote and separator stay outside the mask, and an address that begins the rest of a quoted value counts as an address only up to the next blank, so washing the output again never changes it. The credential stage now repeats its single scan on its own output until nothing changes (the cap counts rewrites, at most eight; clean text takes exactly one pass), so masking the output again never changes it for every input that settles within the cap — the random, exhaustive and grammar pools and the two known step-by-step constructions settle in at most three passes counting the confirming one, and the searched constructions kept in the pool in at most six; a deliberately constructed input can need more rewrites as it grows, and at the cap the last rewrite is returned, which a further pass can only mask more of. The checks cover the pass cap and how reaching it is reported, single-pass clean text, a grammar sample of the busiest shapes, constructions that once advanced one step per pass, the mark positions on the final output and how positions compose across rewrites. A username that is itself a credential label with a quote or angle bracket in the password is masked as userinfo (also with only the URL net on) unless the label's own value really reaches the `@`; a URL parameter such as `;token=...` or `,token=...` stays inside the URL piece; a private-key header the outlet cannot recognise does not hide the labels inside its block; the tail glued after the host of a label's address value is masked to the value's end; and a value this package's own transcript wash split at a carriage return is masked when the display joins it back. A bare `Token <value>` / `token <value>` (no label, no separator) is now washed like `Bearer`, with a wider allow-list of ordinary English words after `token` so prose such as `token count: 1234` or `token expired` stays readable; a `/token` URL path tail followed by a status code is not a scheme word and stays as is, and neither is `token` right after `unexpected` in a parser error such as `SyntaxError: Unexpected token '<'`, which stays readable. The `token` scheme word only adds masking of its own value: label values and `bearer` / `basic` values are read exactly as in the previous release, so a value that starts with another character, or a `bearer` value, followed by `token`, a non-ASCII space and an address is still masked whole. When a credential label or scheme word is followed by a colon and a YAML block scalar header (`\|` or `>`, optionally with `-`, `+` or digits, invisible characters allowed between them, and nothing else on that line but blanks, invisible characters or a comment, ending in a line break), the whole indented block becomes a single marker: the block runs until the first line with visible content that is not indented deeper than the label's line, blank lines included, and the line break before that line and the line itself stay as they were on outlets that keep line breaks (outlets that draw line breaks as escapes or dots merge the next word into the marker, as they already do after any value). Raw and JSON-escaped line breaks are both read; blanks inside escape sequences or control strings never count as indentation, and a block line holding only a control string counts as content and is masked whole. Forms where the marker is followed by more text on the same line, `=`-separated forms such as `key = \|` or the `=>` arrow, and a header at the very end of the text read exactly as in the previous release. From 0.94.0 the private-key header has one definition, generated from the settings contract: the whole-string check finds it anywhere in the text (as the contract's own write-side check does from its 10.0.0 release), and the key-block cutter builds its header line from the same definition, so a lowercase header or one without a space after BEGIN has its key body masked like the canonical form, while public keys and certificates are left as they were. Since 0.94.0 the credential words also read through YAML node properties (type tags such as `!!str` and anchors such as `&name`) between the colon and the value: the properties and the value after them get one marker each, block scalars and block-sequence items included, and a property with no value after it on the same line reads exactly as before. A list after a credential label (a block sequence, a flow sequence including JSON arrays, or a same-line comma list after a plural label) is judged item by item with the same exemptions as a single value: a list of plain key names after a bare `keys`, or of integers after a bare `tokens`, is left alone, and any value-like item masks every non-neutral item. A dash right after the colon on the same line is a list bullet, not a value. The retry overlay's error text (`error.formatted` on the four error arms) passes through the same outlet, and the label vocabulary is pinned as it stands (members masked, known out-of-family keys such as `passphrase` left as written). From 0.95.0 the credential words also read a value wrapped in round or square brackets after a scheme word or label (placeholder-shaped contents such as `(API_KEY)` or `(your-token)` and bracketed prose stay as written), a JSON-escaped tab between a scheme word or label and its value, a same-line associative-array arrow with a quoted label and a quoted value, and a `token` scheme word with its value after a credential label as one value; a word with no label at all is masked when it is shaped like a random token (length, three character classes, entropy and run structure), while hex hashes, UUID lists, base64 image headers and data URIs, integrity strings, provider object ids and ordinary identifiers stay as written; a known secret prefix glued after an underscore or hyphen inside an identifier or a URL path segment is masked from the prefix on; and a masked value no longer swallows a closing backquote at its end, or a closing round or square bracket at the end of a URL that has no matching opener, while a backquote inside a value never cuts it short; an address after `Bearer` or `Basic` that is itself the value of a credential label (including `Authorization:`) is masked whole, keeping the scheme word, while an address with no label stays as written. Credential masking also covers three shapes that used to print as written: a name glued onto a credential word (PGPASSWORD=, CUSTOMSECRET=, dbPassword:, secretAccessKey=; singular only, so token-count names such as maxTokens or inputTokens and English words such as monkey stay visible), Bearer or Basic glued to a preceding word character (_Bearer, xBearer), and a known secret prefix followed by one unbroken run of six or more letters and digits (sk-abcdefghijk). The new readings only add masks: a value that was already masked whole stays masked whole when it contains one of these shapes. The configured-address display and the upload check read the same label rule, so a glued name used as a query key counts as a credential in both. A frozen copy of a pattern net that a downstream host used to run locally is replayed against the output as a no-regression check. A length cap never splits a redaction marker: the cut backs off to before it on every capped path (plain, position-tracking, paired backslashes, and the shared cap of the field-escaping helper); strings that are not complete markers are cut as before. Since 0.98.0 a value glued to a semicolon, a value wrapped in braces or angle brackets (template placeholders stay readable), a bare `Basic` / `Bearer` assigned to an environment-style credential name, and an ordinary word right after `Bearer` / `Basic` in a credential header are masked on every outlet, under a 65 KB linear-time bound; the diagnostic-word predicate answers from the same word list the outlet uses. Since 0.102.0 the configured-address outlet's seven arms are pinned cell by cell (a userinfo whose boundary with the query or fragment cannot be told apart, and a single-slash address that does not parse with a host, show one marker; a leading scheme-less `user:pass@host` keeps host and path; a scheme-less relative request keeps its path and masks every parameter value; a backslash inside a special scheme's authority is read through whichever of the text and the parsed-component reading hides everything the other hides, matched by position, else the whole string is withheld), the reference client's golden inputs are checked to be present verbatim when its tree is available, the general nets' wider scheme-less userinfo and the boundary rule on addresses in prose are pinned with neighbouring negatives, path segment names and values in configured addresses and the Slack webhook tail are pinned over forty shapes (free-text path segments stay as written), non-string input to the three outlets shows one marker without running any caller code, and an in-gate negative control on the boundary rule turns those cells red; pass counts are read as `collections`. Since 0.107.0 the guard also covers a third value of the backslash-pairing switch, `'auto'`, which decides per string: a string whose shown form carries a mark, or a literal sequence that reads as an escape (a pair of backslashes, `\u` with four hex digits, or `\u{…}`), is shown in the injective paired form and any other string exactly as written — so the real U+2800 and the literal six characters `\u2800` always come out different, clean Windows paths stay untouched, and every output decodes back to its input on its own, including under a length cap, where a mark that only appears after the cut (a joiner whose right-hand neighbour was cut off) also switches the string to the paired form; the marks read-back reports which form was taken (`exact`) from the same scan, with mark positions checked against an independent decoder; and the decision-body profile marks one-cell-wide space characters but not U+3000, identically for plan text, diffs and new-file bodies. |
460
+ | `scripts/run-display-untrusted-projection-test.mjs` | The single display-safety outlet (`displayUntrusted`) and the credential wash on the end-of-run rows this package mints. The outlet composes two credential nets (URL structure: userinfo, every query value, the fragment, path parameters and path segments that start with a known secret prefix; key/value words such as `Authorization: Bearer ...`, `Authorization: token ...` or `api_key=...`, plus well-known secret literals that appear without a label, such as `sk-...`, `ghp_...`, `AKIA...`, JWTs and the body of a PEM private key) with three character nets (control characters, bidirectional and format characters, whitespace folding). The credential nets match on a view of the text with ANSI sequences, format characters, control characters and the outlet's own escape tokens stripped, and map the result back onto the original, so colouring or an invisible character wedged between a label, its separator and its value cannot hide the value, and no stray marker is left behind. Whitespace of any length around the separator is accepted. Hosts, ports, paths, query key names and surrounding prose stay byte-for-byte, clean text comes back unchanged, the result is idempotent (also with a length cap), a length cap never splits an escape token or a surrogate pair, an invalid cap means no cap, and every net can be switched off on its own. A few narrow shapes are left alone because they name something rather than carry a value (a plain English word after `bearer` or `basic`, a back-quoted credential variable name, a plain integer after `tokens:`, a list of key names after `keys:`), each with a counter-example that is still washed. Regional flag emoji built from tag characters are kept whole. The existing single-line helpers (`escapeDisplayControlChars`, `collapseLabel`, `capForDisplay`, peer sender names and the hook failure banner) run on the same engine and, since 0.96.0, read the same character table as the outlet: each is checked equal to the outlet with both credential nets off (escape without folding, fold then escape, space mark then fold, and the same length cap) over a seeded pool of more than 600 strings; every character class the old sets left alone (zero-width characters, the word joiner, invisible operators, U+FEFF, the soft hyphen, tag characters and other format characters, plus C1 controls, bidirectional controls and lone surrogates in sender names) is marked class by class, while joiners inside emoji sequences and joining or Brahmic scripts and RGI regional flags stay as written; no character the old sets marked is ever left raw, input without the newly marked characters stays byte-identical to the previous output over every BMP code unit plus random strings, all of them are idempotent, and neither `capForDisplay` nor the hook banner's 512-character cap splits a `\u{…}` escape token or a surrogate pair. The approval decision-note echo, the subagent resume receipt (and its failure debug line) and the startup list of plugin hooks that will not run now also drop bidirectional and format characters (and, for the receipt, C1 controls); a note that is empty after cleaning is treated as absent. The synthetic end-of-run rows (`API Error:`, `Run stopped:`, `Model output error:`, `Outcome unknown:`) and the result frame's `errors[]` pass both credential nets before they leave the package, on the print lane and on the interactive lane (which also keeps the row-class flag); this covers a blocked reason whoever wrote it, while assistant text rows, a successful `result` and salvaged output are never touched, and a non-string `errors[]` entry is passed through unchanged. The known-secret-prefix check is a local copy of the configuration package's detector and is compared with the installed one entry by entry. Since 0.83.5 the outlet has two opt-in switches and a position read-out. `escapeBackslashes` (escape form only) writes every literal backslash as a pair, so each output decodes back to exactly one input (a real invisible character and its literal six-character spelling no longer look alike); a reference decoder round-trips thousands of random strings, the output is byte-identical to the default when the input has no backslash, a length cap is measured on the paired output and keeps the longest fitting prefix, credentials are washed exactly as in the default, and the switch is not idempotent by design (use it only at the final render). Zero-width joiners and non-joiners are kept only inside emoji sequences drawn as emoji (so not between symbols such as © or ™ that display as text) and between letters of scripts where they change the shaping (joining scripts such as Arabic, and the Brahmic family), each listed script checked both ways; the one other place a joiner is kept is right after a virama at the end of a word, the older spelling still found in Malayalam and Bengali text. Next to Latin, Cyrillic, CJK and other letters, next to modifier letters shared across scripts, at the start of a word, at the end of a word without a virama before it, or on their own they are now marked. `blanks` marks characters that look like a space but are not an ASCII space (no-break and other width spaces, the ideographic space, Hangul fillers, the blank Braille pattern) before whitespace folding, for names that must never look alike. `displayUntrustedMarks` returns the same text plus the position of every character mark; its text is compared with the outlet over thousands of inputs. With both credential nets off, the character face stays byte-identical to the previous release for input without joiners. The credential nets read escape sequences the way a terminal would when one is cut short: an unfinished colouring or character-set sequence interrupted by another one is dropped as a whole, a sequence never takes the `@` of an address as its final character, and a final character that starts a well-known secret literal (`sk-`, `ghp_`, `AKIA`, a JWT) is also read as the start of that literal; escape tokens this outlet writes are read as one unit, while look-alike text it never writes (an upper-case `\U`, or a code point it never marks) is read as plain text. A URL is cut before a run of non-ASCII blanks followed by a credential label or scheme word, Hangul fillers and the blank Braille pattern count as spaces around a label's separator, and a value that itself starts with a quoted label (`token= "password":"..."`) is left to that inner label. With `blanks` on, a blank written as an escape token right after a label is read as a blank when the value is judged, so `password:` followed by a no-break space and `missing` stays unmasked and an empty value gets no marker; the credential nets also read the text the way it looks after default whitespace folding and combine what each reading masks, so whatever the default form masks stays masked with `blanks` on (checked over a seeded corpus for the escape form, with paired backslashes and without folding; the exceptions are text that itself contains a literal six-character blank escape, which cannot be told apart from one the outlet wrote, and the dot and space marks, which cannot tell a marked blank from a real dot or space). A lone surrogate wedged between a label, its separator and its value no longer hides the value: the credential nets treat it exactly like a format character, in the machine-readable wash, in a single pass of the display outlet, and on the end-of-run rows and the result frame's `errors[]` on both lanes, while lone surrogates anywhere else are left byte-for-byte. Since 0.84.1 an address-shaped value (`scheme://…`) that sits right after an invisible single character — a format character, a lone surrogate, a non-whitespace control character, or the outlet's own escape token for one — is no longer let through as an address: the scheme stays and the host and path are replaced, with the query and fragment masked as before; `user:<password>@` followed only by stripped units and then a boundary, a port or another `@` is masked as userinfo. A value after real whitespace or after a colour sequence is still treated as an address, and clean text stays unchanged; forms that the previous release masked are checked not to leak on the same outlets. A `user:<password>@` candidate that is the value of a credential label or scheme word — including a label or scheme word split by stripped characters, and a quoted value that goes on past whitespace — is left to the label pass, so the whole value is masked as in the previous release; both reported shapes and frozen samples from the targeted pools are checked on every outlet. A scheme-less `user:password@host` whose password holds a double quote, backtick or angle bracket is still masked when a credential label or scheme word sits right before the user name, or when the user name is a plain word that is not itself a label, the host has a real shape and the address is not wrapped by that quote; quoted JSON values, quoted or bracketed e-mail addresses and quoted arrays stay byte-identical. When a label inside a quoted value ends right at that value's closing quote, the quote and separator stay outside the mask, and an address that begins the rest of a quoted value counts as an address only up to the next blank, so washing the output again never changes it. The credential stage now repeats its single scan on its own output until nothing changes (the cap counts rewrites, at most eight; clean text takes exactly one pass), so masking the output again never changes it for every input that settles within the cap — the random, exhaustive and grammar pools and the two known step-by-step constructions settle in at most three passes counting the confirming one, and the searched constructions kept in the pool in at most six; a deliberately constructed input can need more rewrites as it grows, and at the cap the last rewrite is returned, which a further pass can only mask more of. The checks cover the pass cap and how reaching it is reported, single-pass clean text, a grammar sample of the busiest shapes, constructions that once advanced one step per pass, the mark positions on the final output and how positions compose across rewrites. A username that is itself a credential label with a quote or angle bracket in the password is masked as userinfo (also with only the URL net on) unless the label's own value really reaches the `@`; a URL parameter such as `;token=...` or `,token=...` stays inside the URL piece; a private-key header the outlet cannot recognise does not hide the labels inside its block; the tail glued after the host of a label's address value is masked to the value's end; and a value this package's own transcript wash split at a carriage return is masked when the display joins it back. A bare `Token <value>` / `token <value>` (no label, no separator) is now washed like `Bearer`, with a wider allow-list of ordinary English words after `token` so prose such as `token count: 1234` or `token expired` stays readable; a `/token` URL path tail followed by a status code is not a scheme word and stays as is, and neither is `token` right after `unexpected` in a parser error such as `SyntaxError: Unexpected token '<'`, which stays readable. The `token` scheme word only adds masking of its own value: label values and `bearer` / `basic` values are read exactly as in the previous release, so a value that starts with another character, or a `bearer` value, followed by `token`, a non-ASCII space and an address is still masked whole. When a credential label or scheme word is followed by a colon and a YAML block scalar header (`\|` or `>`, optionally with `-`, `+` or digits, invisible characters allowed between them, and nothing else on that line but blanks, invisible characters or a comment, ending in a line break), the whole indented block becomes a single marker: the block runs until the first line with visible content that is not indented deeper than the label's line, blank lines included, and the line break before that line and the line itself stay as they were on outlets that keep line breaks (outlets that draw line breaks as escapes or dots merge the next word into the marker, as they already do after any value). Raw and JSON-escaped line breaks are both read; blanks inside escape sequences or control strings never count as indentation, and a block line holding only a control string counts as content and is masked whole. Forms where the marker is followed by more text on the same line, `=`-separated forms such as `key = \|` or the `=>` arrow, and a header at the very end of the text read exactly as in the previous release. From 0.94.0 the private-key header has one definition, generated from the settings contract: the whole-string check finds it anywhere in the text (as the contract's own write-side check does from its 10.0.0 release), and the key-block cutter builds its header line from the same definition, so a lowercase header or one without a space after BEGIN has its key body masked like the canonical form, while public keys and certificates are left as they were. Since 0.94.0 the credential words also read through YAML node properties (type tags such as `!!str` and anchors such as `&name`) between the colon and the value: the properties and the value after them get one marker each, block scalars and block-sequence items included, and a property with no value after it on the same line reads exactly as before. A list after a credential label (a block sequence, a flow sequence including JSON arrays, or a same-line comma list after a plural label) is judged item by item with the same exemptions as a single value: a list of plain key names after a bare `keys`, or of integers after a bare `tokens`, is left alone, and any value-like item masks every non-neutral item. A dash right after the colon on the same line is a list bullet, not a value. The retry overlay's error text (`error.formatted` on the four error arms) passes through the same outlet, and the label vocabulary is pinned as it stands (members masked, known out-of-family keys such as `passphrase` left as written). From 0.95.0 the credential words also read a value wrapped in round or square brackets after a scheme word or label (placeholder-shaped contents such as `(API_KEY)` or `(your-token)` and bracketed prose stay as written), a JSON-escaped tab between a scheme word or label and its value, a same-line associative-array arrow with a quoted label and a quoted value, and a `token` scheme word with its value after a credential label as one value; a word with no label at all is masked when it is shaped like a random token (length, three character classes, entropy and run structure), while hex hashes, UUID lists, base64 image headers and data URIs, integrity strings, provider object ids and ordinary identifiers stay as written; a known secret prefix glued after an underscore or hyphen inside an identifier or a URL path segment is masked from the prefix on; and a masked value no longer swallows a closing backquote at its end, or a closing round or square bracket at the end of a URL that has no matching opener, while a backquote inside a value never cuts it short; an address after `Bearer` or `Basic` that is itself the value of a credential label (including `Authorization:`) is masked whole, keeping the scheme word, while an address with no label stays as written. Credential masking also covers three shapes that used to print as written: a name glued onto a credential word (PGPASSWORD=, CUSTOMSECRET=, dbPassword:, secretAccessKey=; singular only, so token-count names such as maxTokens or inputTokens and English words such as monkey stay visible), Bearer or Basic glued to a preceding word character (_Bearer, xBearer), and a known secret prefix followed by one unbroken run of six or more letters and digits (sk-abcdefghijk). The new readings only add masks: a value that was already masked whole stays masked whole when it contains one of these shapes. The configured-address display and the upload check read the same label rule, so a glued name used as a query key counts as a credential in both. A frozen copy of a pattern net that a downstream host used to run locally is replayed against the output as a no-regression check. A length cap never splits a redaction marker: the cut backs off to before it on every capped path (plain, position-tracking, paired backslashes, and the shared cap of the field-escaping helper); strings that are not complete markers are cut as before. Since 0.98.0 a value glued to a semicolon, a value wrapped in braces or angle brackets (template placeholders stay readable), a bare `Basic` / `Bearer` assigned to an environment-style credential name, and an ordinary word right after `Bearer` / `Basic` in a credential header are masked on every outlet, under a 65 KB linear-time bound; the diagnostic-word predicate answers from the same word list the outlet uses. Since 0.102.0 the configured-address outlet's seven arms are pinned cell by cell (a userinfo whose boundary with the query or fragment cannot be told apart, and a single-slash address that does not parse with a host, show one marker; a leading scheme-less `user:pass@host` keeps host and path; a scheme-less relative request keeps its path and masks every parameter value; a backslash inside a special scheme's authority is read through whichever of the text and the parsed-component reading hides everything the other hides, matched by position, else the whole string is withheld), the reference client's golden inputs are checked to be present verbatim when its tree is available, the general nets' wider scheme-less userinfo and the boundary rule on addresses in prose are pinned with neighbouring negatives, path segment names and values in configured addresses and the Slack webhook tail are pinned over forty shapes (free-text path segments stay as written), non-string input to the three outlets shows one marker without running any caller code, and an in-gate negative control on the boundary rule turns those cells red; pass counts are read as `collections`. Since 0.107.0 the guard also covers a third value of the backslash-pairing switch, `'auto'`, which decides per string: a string whose shown form carries a mark, or a literal sequence that reads as an escape (a pair of backslashes, `\u` with four hex digits, or `\u{…}`), is shown in the injective paired form and any other string exactly as written — so the real U+2800 and the literal six characters `\u2800` always come out different, clean Windows paths stay untouched, and every output decodes back to its input on its own, including under a length cap, where a mark that only appears after the cut (a joiner whose right-hand neighbour was cut off) also switches the string to the paired form; the marks read-back reports which form was taken (`exact`) from the same scan, with mark positions checked against an independent decoder; and the decision-body profile marks one-cell-wide space characters but not U+3000, identically for plan text, diffs and new-file bodies. The label and capped-length helpers wash credentials first and cap the washed text, so a credential-shaped name never appears in full or as a leftover prefix at the cut. |
461
461
  | `scripts/run-display-untrusted-baseline-diff-test.mjs` | The credential side of the display outlet (`displayUntrusted` and `washCredentials`), diffed byte for byte against the previous published release: five input pools (seeded random strings, exhaustive shapes around labels, quoted values, userinfo, schemes and invisible units, and URL parameters separated by `;` or `,`) run through fifteen outlets. A secret the previous release masked must stay masked wherever it sits in a credential position; the outlet with only the URL net enabled is judged within that net's own reach. Clean lines from the package's own documents pass through unchanged, and every outlet is idempotent (masking the output again changes nothing). The previous release tarball is supplied through `SEMA_DU_BASELINE_TGZ`; without it the diff sections report as skipped and the run counts as partial, not green. The minimal-shape pool also carries scheme-less userinfo whose password holds a quote or angle bracket after a credential label, and the shapes that once washed differently on a second pass. A full-product grammar pool of 64,512 strings joins the idempotence check, and every pool is also checked for the credential stage reaching its pass cap (it must never do so). The pools now include the bare `token` scheme-word forms and a full `token` grammar product, so the new wash is held to the same idempotence and no-regression bars against the previous release. They also include label and `bearer` / `basic` values that run into a `token` scheme word, a non-ASCII space and an address, with or without a leading character, and quoted values where `token` and an address follow `bearer`. A YAML block-scalar pool (block headers with every chomping and indentation indicator, comments, raw and JSON-escaped line breaks, non-ASCII and tab indentation, invisible characters at line starts, credentials, control strings and bidi characters inside the block, and seven kinds of line after the block) joins all checks: no secret inside a block may show on any outlet that runs the word net, the previous release must show some on the same pool, and the non-block forms in that pool must stay byte for byte as in the previous release. A sixth pool covers lists and YAML node properties after a credential label: block, flow and comma lists plus tag and anchor prefixes, alongside look-alike non-list forms. The secrets in the list pool must stay masked on every outlet, the look-alike forms must read byte for byte as in the previous release, and the three forms whose reading changed on purpose (a lone dash after the colon, a value after a dash, a block sequence of plain key names) are pinned as registered divergences. A seventh pool covers the 0.95.0 credential-net shapes (bracketed values after scheme words and labels, a JSON-escaped tab before the value, unlabelled random-looking tokens built inside the shape rule, associative-array arrows, a label followed by a `token` scheme word and an address, and known prefixes glued after an underscore or hyphen) alongside normal long strings and placeholders: the secrets must stay masked on every outlet while the previous release shows some, the normal strings must read byte for byte as in the previous release, eleven readings that changed on purpose are pinned as registered divergences, and the share of purely random tokens the shape rule catches is read at four lengths with a floor of 0.9. A nested-shape pool pins that the three new credential readings only add masks: a value that the previous release masked whole must stay masked whole when it contains one of the new shapes. At the capped exit, differences against the previous release are accepted only when the old output ended in a truncated redaction marker and the new output is the old one without that fragment. Since 0.101.0 a pool of percent-encoded YAML block scalars and block sequences (166 plain shapes in one to three layers of encoding) is pinned: no secret shows on any outlet while the previous release shows some, benign encoded multi-line text reads byte for byte as in the previous release, six readings that changed on purpose are registered as divergences, and washing twice equals washing once. Since 0.102.0 two more sections compare with the previous release: the wider scheme-less userinfo and the boundary rule on addresses in prose (registered rows byte for byte on the machine wash, no password byte on any outlet with a credential net on, unchanged with both nets off, neighbouring negatives unchanged, idempotent), and a pool of capability addresses judged by zero residue of the secret on the configured-address outlet and in prose (Slack and known-prefix rows masked, the other rows in prose unchanged); the pass histogram reads `collections`. The `'auto'` value of the backslash-pairing switch is checked against the earlier release too: wherever both releases show a string the same way, `'auto'` must equal the earlier release's paired or unpaired form, chosen from that release's own marks, and undoing the pairing must give back the unpaired form across every pool, so the credential side cannot move. |
462
- | `scripts/run-automatic-answer-verdict-test.mjs` | The single verdict `automaticAnswerVerdict(card, lane, facts?)` that decides whether an automatic lane (the standing permission mode, or a standing allow rule) may answer an approval card on the user's behalf. The only `may_answer` exit is a well-formed class set whose classes are all known to this build, whose folded row does not block on that lane, with no must-ask evidence on the card and, for the mode lane, a host reading that its own ask rules did not match; every other card is `must_ask` (with the road that decided it) or `unknown` (with why). The guard derives every single-class card and every ordered pair of classes on both lanes from the engine's own arbitration table, walks every row of the decision table on both sides (including positive evidence when the class set is absent or malformed, class sets that change while being read, and class names this build does not know), compares the package's fold against the engine's fold function over all singles, pairs, triples and a random sample, checks the mode lane against the engine's bypass-mode arbitration (including the release disclosure), confirms on a real engine run that a matched ask rule always puts `ask_rule` in the class set, checks that the two older predicates never answer more loosely than this verdict nor than the previous release, and, when a terminal reference tree is available, that the terminal's own decision chain auto-approves nothing new when it is built against this package. Since 0.94.0 it also covers the class-aware form of the mandated-question sentence: the clause naming an automatic check appears only when the card's question classes let the classifier or the sandbox answer, checked class by class against the engine's own table, and any unreadable card falls back to the generic sentence. Since 0.94.0 the mode lane reads the bypass column on the tool face of the card (the face comes only from the generated shell-face name list, matched exactly, and releasable classes are read per face from the generated table): a write whose protection could not be resolved is answerable on the shell face and still asked on file tools, while lookalike tool names (case, whitespace, zero-width or homoglyph characters, protocol prefixes, rule syntax, inherited or accessor names, non-strings, absence) always read as the other face; when the installed engine exposes faces, the fold and the bypass arbitration are compared per face against the engine and every catalog name is checked against its face. A third lane, `permission_request_hook`, reads the same column as the standing-rule lane (whether a stored rule may clear the class) with the same must-ask evidence first, ignores allow-rule shadow evidence and host facts, and `permissionRequestHookScope` turns its verdict into allow-or-deny, deny-only or skip; a real engine run confirms that an automatic answerer's allow is refused exactly on asks that carry the real-approval bit, and that the lane never reads more loosely than the engine does. The terminal-chain comparison accepts exactly the shell-face release as the one intended widening. The hook lane's class column is the set of ask classes the engine stamps the real-approval bit on, derived from the installed engine: a card raised by an ask rule lets a hook answer, while every class in that set, and every card carrying the bit, still skips the hook; an engine run per class confirms that the engine refuses an automatic answerer's allow exactly where the lane skips. |
462
+ | `scripts/run-automatic-answer-verdict-test.mjs` | The single verdict `automaticAnswerVerdict(card, lane, facts?)` that decides whether an automatic lane (the standing permission mode, or a standing allow rule) may answer an approval card on the user's behalf. The only `may_answer` exit is a well-formed class set whose classes are all known to this build, whose folded row does not block on that lane, with no must-ask evidence on the card and, for the mode lane, a host reading that its own ask rules did not match; every other card is `must_ask` (with the road that decided it) or `unknown` (with why). The guard derives every single-class card and every ordered pair of classes on both lanes from the engine's own arbitration table, walks every row of the decision table on both sides (including positive evidence when the class set is absent or malformed, class sets that change while being read, and class names this build does not know), compares the package's fold against the engine's fold function over all singles, pairs, triples and a random sample, checks the mode lane against the engine's bypass-mode arbitration (including the release disclosure), confirms on a real engine run that a matched ask rule always puts `ask_rule` in the class set, checks that the two older predicates never answer more loosely than this verdict nor than the previous release, and, when a terminal reference tree is available, that the terminal's own decision chain auto-approves nothing new when it is built against this package. Since 0.94.0 it also covers the class-aware form of the mandated-question sentence: the clause naming an automatic check appears only when the card's question classes let the classifier or the sandbox answer, checked class by class against the engine's own table, and any unreadable card falls back to the generic sentence. Since 0.94.0 the mode lane reads the bypass column on the tool face of the card (the face comes only from the generated shell-face name list, matched exactly, and releasable classes are read per face from the generated table): a write whose protection could not be resolved is answerable on the shell face and still asked on file tools, while lookalike tool names (case, whitespace, zero-width or homoglyph characters, protocol prefixes, rule syntax, inherited or accessor names, non-strings, absence) always read as the other face; when the installed engine exposes faces, the fold and the bypass arbitration are compared per face against the engine and every catalog name is checked against its face. A third lane, `permission_request_hook`, reads the same column as the standing-rule lane (whether a stored rule may clear the class) with the same must-ask evidence first, ignores allow-rule shadow evidence and host facts, and `permissionRequestHookScope` turns its verdict into allow-or-deny, deny-only or skip; a real engine run confirms that an automatic answerer's allow is refused exactly on asks that carry the real-approval bit, and that the lane never reads more loosely than the engine does. The terminal-chain comparison accepts exactly the shell-face release as the one intended widening. The hook lane's class column is the set of ask classes the engine stamps the real-approval bit on, derived from the installed engine: a card raised by an ask rule lets a hook answer, while every class in that set, and every card carrying the bit, still skips the hook; an engine run per class confirms that the engine refuses an automatic answerer's allow exactly where the lane skips. Since 0.109.0 the verdict never treats the tool arguments on a card as grounds to approve: on every card the arguments shown are the server's redacted copy, so an automatic answerer that reads them cannot be trusted to approve. The hook lane never answers `may_answer` (its scope is `deny_only` or `skip`; `allow_or_deny` is removed), and the standing-rule lane answers `may_answer` only when the host reports, as `allowRule`, the whole-tool allow rule its matcher returned; otherwise both answer `unknown` (`answer_may_read_arguments`). The verdict is first computed exactly as in the previous release and only then narrowed, without reading the card again, so on any input the change can only turn an approval into `unknown`; the guard checks this against the previous release over the whole card pool, including the exact sequence of reads on the card, and fails when the previous release is not supplied. Since 0.110.0 a question card (the engine's own question to a person, recognised by the origin word `content_question` or by a well-formed class set that contains it) is `must_ask` on the hook lane as well, so the hook scope is `skip` and a hook's refusal cannot close the question; a frame that only carries a card word, a gate kind or a tool name is not treated as a question card, and the other two lanes read exactly as before. |
463
463
  | `scripts/run-allow-rule-shadow-projection-test.mjs` | The reader for the fact that a standing allow layer matched a call but could not clear its ask (`readApprovalAllowRuleShadow`, `approvalAllowRuleShadowed`, and the structured `allowRuleShadow` disclosure stamped on the approval card). Servers send a list of members with their matched entries, and that list is the only thing read. Since 0.98.0 the single text that older servers and older parked rows sent is no longer read as a disclosure: a frame or row that carries only that older text has no disclosure and no sentence, while its presence still counts against automatic answers — the card paths record it as unreadable evidence and the automatic-answer check answers "not known" for it, also when a bare frame is handed straight to the check — so retiring the reading never lets a remembered rule answer an ask it could not answer before. The guard covers every member, several members in order, empty entry lists, the count of omitted entries, member names this build does not know, four kinds of malformed values (all of which still count as evidence), absence, the older text on both card paths and the parked-ask path through the real card port (including a row whose risk descriptor is inherited or comes from a getter), a check against the previous release that every older-shaped frame and row it treated as evidence is still not answered automatically, real frames and durable rows minted by the server fixtures when available, and the effect on every automatic lane. The one sentence a card shows for that fact is minted in one place (`allowRuleShadowSentence`): an absent disclosure renders nothing (it never says there is no rule), an unreadable one says so, and entries and member words arrive display-sanitised. |
464
- | `scripts/run-engine-agent-absence-projection-test.mjs` | Absent background agents: when the engine stops reporting a background agent and no final state has arrived, the row is marked absent and this package owns every decision about it, so all clients agree. One predicate says whether a row is absent (the mark, not the status, decides). An absent row keeps its last known status, never counts as running, and is never counted as completed, failed or stopped; its elapsed time stops at the last moment it was seen, and its sentence says it may still be running. The end-of-turn sweep never settles an absent row (or a resident one). A row that comes back, or a real final state for the current cycle, clears the mark; a late final state from an earlier cycle does not. Absent rows are never removed at the short grace window. After the hard limit (30 minutes from the last time they were seen) the host is asked for the background-agent registry reading of each row: only a reading that the agent has ended or is not listed lets the row go, and each removal is returned as a fact the host must act on and announce; a reading of running, unknown, missing or unrecognised keeps the row and schedules nothing, so no standing poll is created. Until a registry reading is available every absent row stays. A row someone is viewing is held and reported separately only once the registry confirms it is gone. The row sentence, the removal sentence and the late-result sentence come from one place, never state an outcome or that the agent finished, and escape control characters in names, in the engine's removal word and in the late-result status. An end-to-end cell drives the real fleet projection and the real absence channel through every decision. From 0.84.0 the package also reads the registry itself: a reader lists the session's background registry only when the server advertises the listing, classifies its failures as unavailable (a failed read keeps the HTTP status and error code when the error carries them, and never invents either), and refuses a partly readable listing as a whole; a classifier turns the listing into the per-row reading, treating a registry status it cannot place as unknown and reading "not listed" only for keys shaped like registry handles and only when the host states the engine is a single process and names the row's session, because a key from another identity space proves nothing by being absent and, on a multi-replica deployment, a listing answered by another replica does not contain this session's agents at all (without that statement a missing row reads unknown and the row stays); and a predicate returns the agents the registry still counts as live that the host has no row for. A listing at the server's 500-row cap cannot show that an agent is gone either, so a missing row there reads unknown; the cap is checked against the engine's own list clamp and, when a server build is supplied, against the server's route. Each listing carries its session and a per-client sequence number, and a listing superseded by a later delivered read, or read for another session than the one the host names, counts for nothing. Only the exact listing object the reader returned can authorize removing a row or adding one: a spread copy, a structured clone, a filtered or a hand-built listing reads unknown and adds nothing, the listing, its row array and every row are frozen, the cap check uses the row count recorded when the page was read, and rewriting a listing's sequence number fools nothing. The fill predicate holds back agents first seen within the absence settle window — timed on one monotonic local clock from when this client's reader first saw the id, so a skew between the server's clock and this one, or reconnecting to a long-running agent, cannot skip the window — agents without a readable registration time and agents the host saw end since the read went out; the registry key of a row, progress or absence event is whichever of its two ids is shaped like a registry handle; and none of the reader, the classifier or the predicate throws. |
464
+ | `scripts/run-engine-agent-absence-projection-test.mjs` | Absent background agents: when the engine stops reporting a background agent and no final state has arrived, the row is marked absent and this package owns every decision about it, so all clients agree. One predicate says whether a row is absent (the mark, not the status, decides). An absent row keeps its last known status, never counts as running, and is never counted as completed, failed or stopped; its elapsed time stops at the last moment it was seen, and its sentence says it may still be running. The end-of-turn sweep never settles an absent row (or a resident one). A row that comes back, or a real final state for the current cycle, clears the mark; a late final state from an earlier cycle does not. Absent rows are never removed at the short grace window. After the hard limit (30 minutes from the last time they were seen) the host is asked for the background-agent registry reading of each row: only a reading that the agent has ended or is not listed lets the row go, and each removal is returned as a fact the host must act on and announce; a reading of running, unknown, missing or unrecognised keeps the row and schedules nothing, so no standing poll is created. Until a registry reading is available every absent row stays. A row someone is viewing is held and reported separately only once the registry confirms it is gone. The row sentence, the removal sentence and the late-result sentence come from one place, never state an outcome or that the agent finished, and escape control characters in names, in the engine's removal word and in the late-result status. An end-to-end cell drives the real fleet projection and the real absence channel through every decision. From 0.84.0 the package also reads the registry itself: a reader lists the session's background registry only when the server advertises the listing, classifies its failures as unavailable (a failed read keeps the HTTP status and error code when the error carries them, and never invents either), and refuses a partly readable listing as a whole; a classifier turns the listing into the per-row reading, treating a registry status it cannot place as unknown and reading "not listed" only for keys shaped like registry handles and only when the host states the engine is a single process and names the row's session, because a key from another identity space proves nothing by being absent and, on a multi-replica deployment, a listing answered by another replica does not contain this session's agents at all (without that statement a missing row reads unknown and the row stays); and a predicate returns the agents the registry still counts as live that the host has no row for. A listing at the server's 500-row cap cannot show that an agent is gone either, so a missing row there reads unknown; the cap is checked against the engine's own list clamp and, when a server build is supplied, against the server's route. Each listing carries its session and a per-client sequence number, and a listing superseded by a later delivered read, or read for another session than the one the host names, counts for nothing. Only the exact listing object the reader returned can authorize removing a row or adding one: a spread copy, a structured clone, a filtered or a hand-built listing reads unknown and adds nothing, the listing, its row array and every row are frozen, the cap check uses the row count recorded when the page was read, and rewriting a listing's sequence number fools nothing. The fill predicate holds back agents first seen within the absence settle window — timed on one monotonic local clock from when this client's reader first saw the id, so a skew between the server's clock and this one, or reconnecting to a long-running agent, cannot skip the window — agents without a readable registration time and agents the host saw end since the read went out; the registry key of a row, progress or absence event is whichever of its two ids is shaped like a registry handle; and none of the reader, the classifier or the predicate throws. The registry reader also carries the engine's own "kept running on purpose" mark on a row, only when the engine sets it to true; it is never read as someone watching the row. |
465
465
  | `scripts/run-plan-review-dismissal-test.mjs` | An automatic reopen does not put back a plan-review card the user closed (the first-presentation path neither checks nor clears that record, so a replayed park frame still presents its card). A plan-review card the user dismissed (Esc, abort, or any answer that is not approve or reject) is recorded per session and run at the moment of dismissal, synchronously, before anything queued behind the card can be released; a reopen marked `trigger: 'automatic'` then refuses with `{ reopened: false, dismissedByUser: true }` instead of minting a new card the user's next keystroke would land on, while the user's own next action (`trigger: 'user'`) reopens it and clears the record. The record is keyed by gate instance when the host supplies an instance reader: a new plan gate on the same run is still surfaced, and anything that cannot prove the gate is new (no reader, a failed, empty, thrown or timed-out read) refuses on the conservative side. The asynchronous form re-checks after its reads and before minting — a decision handed over meanwhile (seen by the package, or reported by the host's optional hand-over predicate) answers as "your answer is on its way"; a record that changed meanwhile makes the stale evaluation mint nothing and answer from the current record: another close refuses as the user's close and keeps the newer record, a card already back on screen (the user's own action or a concurrent automatic reopen, waited for within the receipt window and re-read once the wait is over) answers `reopened: true`, and a record that is gone (session change, ledger overflow) answers a plain refusal without `dismissedByUser`; a hand-over predicate that throws refuses with a plain `{ reopened: false }`. Each run has at most one reopen on its way: a reopen that arrives while an earlier one's card is published but not yet settled joins it instead of minting a second card and retiring the first card's answer path. Instance readers are snapshotted when they resolve, so a host that hands over its own live set still gets a new gate recognised; and a decisive-looking host answer note does not clear the record for the very card the package's own responder already judged non-decisive (the label was not on that card). A successful reopen replaces only the record taken before the card was minted (with an on-screen marker, not a deletion), a decisive answer clears it, the per-session ledger is bounded, and a session change clears its bucket. Without `trigger` the reopen answers exactly as before, apart from joining a reopen already on its way. A reopen for a session with no view subscribed to question frames (`onQuestionFrameFor(sessionKey, handler)`; `onQuestionFrame` for the default session) refuses with `{ reopened: false, _sema_noPresentationSurface: true }` — in the synchronous and the receipt form alike, per session key, and also when the view goes away while the asynchronous check is reading — and that answer comes before the user-closed check, at the entry and after the asynchronous read alike, whether or not the read proved a newer gate (after the read, a decision handed over while the read was in progress, and a dismissal record that was replaced or cleared meanwhile, still take precedence over the missing view); a host log or probe port that throws changes none of the reopen answers (the suite runs the same arrangement with a quiet and with a throwing log port and requires the same answer and the same card frames); every other refusal (receipt window lapsed, decision in flight, user-closed card, non-default slot without a delivery path, record gone) keeps its own keys without the flag. The first-presentation call does not check for a view: without one it still answers `true` and the card frame goes nowhere, which the suite records as today's behaviour, not as a guarantee. A free-text answer on the plan card (no option picked, a non-blank note) counts as a decisive answer: it clears the record and delivers exactly one rejection; a blank note, or a contradictory multi-select that carries a note, does not. |
466
466
  | `scripts/run-plan-review-injected-wire-test.mjs` | The plan-review orchestration accepts a host-supplied engine client, so a browser host behind a same-origin relay — which cannot install a token-bearing engine target — runs the package's own decision path instead of rebuilding it. With a client injected, the decision, its in-flight latch, the resend-once-without-the-key handling of a `request.field_conflict` refusal, the post-decide status re-pull, the six-state effect classification and the outcome text are byte-for-byte what the installed-target path produces, proven against a real relay-form SDK client and a fake engine while the installed target points at a different fake engine that must receive nothing. The three-choice card depends only on the host's declaration; since 0.103.0 the connection's optional `capsBaseUrl` is the same capability-evidence key every injected connection carries, read only to decide whether a decision carries the gate identity, and a connection whose `capsBaseUrl` cannot be read is unusable like any other injected connection (no card, nothing sent); every request of the relay-form client leaves without an `authorization` header, checked beside a token-bearing client on the same spy so the absence is not the spy's blindness; and a planted token never reaches a log line, an outcome sentence, a queue item or the host callback on any of the failure paths. A decision may carry a `reason` (trimmed, dropped when blank, capped at the server's limit), an `onOutcome` callback hands the outcome back to hosts that have no model-prompt queue (called once per admitted decision, never for a latched duplicate, and its own failure never affects delivery), a reopen with an injected client no longer needs a host delivery function on a non-default session, and the two capability probes take the same injected client with a bounded wait. A decision that the injected client's own time limit cuts off is reported as sent without an answer within the time limit (it may have taken effect), in the same sentence the installed-target path uses when its limit is reached, while a connection that cannot be made is still reported as unreachable and one that drops after the decision went out is reported as possibly applied (the same no-answer judgement the background-agent verbs use); a client built with the documented decision budget answers a slow approval exactly as the installed target does, and a client whose namespaces are callable functions is accepted as before. The package's own reads through an injected client — the two probes and the post-decide re-pull — settle at their deadlines even while the client sleeps through a retry back-off, and abort the request underneath; a malformed injected connection is reported as not sent, never thrown. |
467
467
  | `scripts/run-subagent-injected-wire-test.mjs` | The background-agent verbs — the child report read, the live tail, the task-handle output read and stop, steer, the resume assembly, manual compaction with its capability warm-up, and the delegated-prompt read — accept a host-supplied engine client, and the plan-review orchestration now resolves its client through the same single construction point, so a browser host behind a same-origin relay runs every one of these paths without an installed engine target. With a client injected, each entry sends only through it: proven against a real relay-form SDK client and a fake engine while the installed target (default and keyed slots alike) points at a different fake engine that must receive nothing, with the row's own session parameter carried, no `authorization` header on any request (checked beside a token-bearing client on the same spy), and a planted token never reaching a log line, a return value or a failure detail on the injected, installed-target and unreachable paths. The capability evidence behind the task-handle verbs and the row stop gate is read only under the key the host names, never from the installed target; the report, tail and compaction warm-up cache their own probe under that key and re-probe when it is absent. A malformed injected client or connection lands on each entry's existing cannot-send outcome (`null`, `no-wire`, `unavailable`, `offline`, `unreadable`) with zero requests, never a throw or an unhandled rejection. Every request through an injected client settles at the package's own deadline even while the client sleeps through a retry back-off and aborts the request underneath; the caller's own signal still applies, and the old signatures answer as before apart from the no-answer outcomes described next. A steer, resume or stop that may have reached the engine but got no answer — the package's own deadline on an injected client, the client's per-request limit on either path, or a transport failure that cannot be shown to have happened before anything was sent (the connection dropped after the request went out) — is reported with a machine-readable `unconfirmed: true` beside the unchanged `reason`, so a host can say it may have taken effect instead of calling it a failure; a cancellation by the caller after the request went out is reported the same way (only the wait was cancelled), while a real 4xx or 5xx answer, a connection that could not be made at all, a verb that threw before returning a promise, and a cancellation already in place before the call (nothing is sent) carry no such key. An installed target is copied by value when a call resolves it, so a delayed or retried compaction still goes to the engine and credentials it was resolved against even if the host's target object changes meanwhile, and a verb that throws synchronously is classified exactly like one that rejects. The resume failure classifier that hosts reuse for their own direct calls makes the same judgement from the same single source, so every client renders it the same way. A successful sub-agent steer carries the server's acceptance id (`messageId`) only when the receipt holds a non-empty string; older servers leave it out. |
@@ -505,7 +505,7 @@ guard still cross-checks the table by name).
505
505
  | `scripts/run-durable-decision-single-holder-test.mjs` | The single holder of a durable approval decision: for one approval identity (the call id of the fetched pending row, or the task id when the row has none), one decision (card, answer, decide including the package's own resends) is in flight per process, and any other caller that arrives while it is in flight, or that started reading the queue before it settled, gets a copy of that same outcome marked `joinedInFlight: true` without presenting a card, sending a decision or resending anything. Two concurrent callers produce exactly one card and one decide in both arrival orders, including the real park resolver racing a direct re-presentation of the same row (the stream finishes and both sides settle); a failed decide or a throwing card port gives both callers the same failure and clears the registration so the next call is a new decision; an abort signal from either caller interrupts that one decision: the holder's card is withdrawn and the holder alone sends exactly one interruption deny that both callers report, with identical readings whichever path holds, while an abort that arrives after the decision settled sends nothing and returns the settled outcome; different identities never join; the joining caller reports the holder's card through `onPresented` only when that card was actually handed to the card port (a throwing callback does not change its outcome) and never reports a decide operation; the copy never alters the holder's object and the flag is never written as `false`; and a syntax-tree scan pins that the joining and registration code paths contain no card, decide or notice call (with a planted violation proving the scan can fail) Since 0.94.0 the identity also carries the row instance (its creation time): when the engine parks the same call a second time for a different check, that second row is a new decision in both arrival orders (while the first card is still on screen, or when a read that started before the first decision settled returns the second row), and two callers fetching the same row instance still meet. |
506
506
  | `scripts/run-approval-cross-leg-holder-test.mjs` | One holder per approval across the live card and the durable paths. While the package holds the live card for an approval (the card is on screen or its answer is being sent), a durable path for the same approval (the package's own parked-row decision path, or a host's own durable route that asks `joinApprovalHolder` first) presents no second card and sends no decision; once the holder lets go, an approval it settled is joined (the holder's outcome marked `joinedInFlight: true`), and one it did not settle (the answer was not delivered, the row is being parked or was parked, the host withdrew the card) is taken over by the durable path, never while the live card is still on screen. Identity is the approval id when the caller has one; otherwise the same run and the same gated call (and the same session when both sides know it), matched only while the live card still waits for an answer, and any component that cannot be read means no match. Cells cover the host timing that produced the second card (an answer after more than the 750 ms grace window: zero second card, zero durable decision, the next approval's card shows at once), the negative controls (nobody holds it; the live answer fails with 404 and the durable card is requested only after the live card was answered), unreadable or different identities, aborts from either side (one interruption deny), the parked-row path joining an allow and a deny, a second approval of the same call while the first answer is in flight, a parked close-out handing over to the parked row, and the scope: a host that picks the route by frame type with no timed fallback is not on this path |
507
507
  | `scripts/run-approval-hold-scope-test.mjs` | One holder per approval across every card-opening path. The live-card entry, the parked-row decision path and the host's own paths (registered through `claimApprovalCard`) all register in one table and see each other through one symmetric matching rule: same approval id when both sides have one, otherwise the same run and the same call while the holder is still waiting for the human. A later arrival waits for the holder and, when the holder settled the question, sends nothing and gets the holder's outcome. Also checks the superseding-card position (`supersedes`, with the earlier answer disclosed only when the engine did not take it), the parked-row position (`parkRow`, deadline from the row's own SLA wall) and the parking notice function. |
508
- | `scripts/run-hitl-answer-outlets-projection-test.mjs` | The new decision-chain outlets. `answerPayloadFor` builds the base decision body for both lanes from the one answer object (decision word, session approval, edited input, the deny note behind its capability and length gates) and the package's own lanes use it; a Yes note never reaches the wire while `answerCarriage` answers `none`; the answer-committed event fires exactly once per answered card before the first send. `rememberOfferable` is checked card by card against `standingRowsOf`; `localAllowRuleEnforceable` against the permissions declaration judge. `lateDecideOutcomeOf` is checked against a frozen copy of the previous host-side classification. `RESPOND_DECISIONS` is frozen with exactly three words. |
508
+ | `scripts/run-hitl-answer-outlets-projection-test.mjs` | The new decision-chain outlets. `answerPayloadFor` builds the base decision body for both lanes from the one answer object (decision word, session approval, edited input, the deny note behind its capability and length gates) and the package's own lanes use it; a Yes note never reaches the wire while `answerCarriage` answers `none`; the answer-committed event fires exactly once per answered card before the first send. `rememberOfferable` is checked card by card against `standingRowsOf`; `localAllowRuleEnforceable` against the permissions declaration judge. `lateDecideOutcomeOf` is checked against a frozen copy of the previous host-side classification. `RESPOND_DECISIONS` is frozen with exactly three words. Since 0.109.0 `localAllowRuleEnforceable` takes two arguments and reports only the engine leg: a local allow rule is enforceable exactly when it would be sent to the engine, and a third argument changes nothing. |
509
509
  | `scripts/run-hitl-single-reading-projection-test.mjs` | One reading per fact on the approval surface. When a park carries the identity of the call it holds, the collateral marking of aborted sibling calls reads that identity directly, so two calls of the same family are told apart, while frames from a sub-agent lane or naming a different gate are left unmarked; without the identity the earlier inference is unchanged. The session-approval fallback re-sends a plain approval only for the dedicated refusal codes. Reopening a plan-review card with the reused question id and an injected delivery path cannot be expressed in types and is refused at runtime. The suspended-ask tracker reports only appeared and gone. The sub-agent test reads the explicit marker only. An unreadable pending section escapes into the poll-leg breaker while a partly readable one commits with a dropped count and still owes a complete read. Tool-start argument snapshots live in one table. A panel row keeps its first binding. The fleet description filter no longer hides tools named Task or Fork. The gate record view carries which seats were present but unreadable, so the denial-kind reader answers the same on the view as on the raw frame. Both tool-output text readers share one reading. The three out-of-card live facts on an opening frame (mandated flag, mandate word, origin word) are read by one function, called from a fixed list of places in the source; the per-fact readers are otherwise used only on gate records, projected cards and the seat crossing, and every path answers the same for the same frame. |
510
510
  | `scripts/run-approval-settled-single-source-test.mjs` | Whether an approval already has an outcome is recorded once per process, in the package's approval-holder registry, at two points only: when the live card's holder settles (the decision was delivered and acknowledged, or the interruption denial it sent was accepted) and when the engine's close-out frame for that approval arrives. Every path that could put up a card for the same approval reads that one record first: the in-stream frame path, the suspended-ask path (same entry point), and a host's own durable route through `joinApprovalHolder`, which now answers `joined` for an approval that was already answered or closed by the engine even after the holder let go (an approval the engine moved to the parked queue still answers `free`, since it remains open there). Cells cover the host's durable route after a delivered answer and after a close-out with no card ever shown, the parked negative control, the suspended-ask path writing and reading the same record, a new bridge still recognising a settled approval, the replay verdict reading only this record (a host-supplied map is ignored), the outcomes that are not recorded (a delivery failure including 404, a withdrawn card, a refused edit, an interruption denial that failed, threw or was answered with the parking reply, or that a parked close-out overtook), the write happening at the acknowledgement rather than when the card is answered, and the 4096-entry bound |
511
511
  | `scripts/run-approval-respond-parking-test.mjs` | The live-card respond answer when the engine replies `202` with the reason `parking_in_progress`: the card's row is moving to parked, so the answer did not take effect. The outcome is `decision: 'unresolved'` plus a `parking` position carrying the approval id and, when the engine supplies them, the suggested retry interval and the decide path of the parked card; the resolution reader reports it on the existing unsettled arm with the new cause `parking_in_progress` (the union keeps its three arms; a retracted or edit-refused card still wins over it, and it wins over the decision word). The parking fact is carried only by an own data property naming the reason and a non-empty approval id that matches the card; a retry interval or decide path that is unusable drops only itself and the parking still stands. A body that names the parking reason but whose approval id is missing, unusable or belongs to another card is reported as not settled, never as decided and never as parked, with no notice and no resend. The answer is re-sent exactly once with the same body after the interval the server names (within five seconds; an absent or longer interval means no re-send), never has its attribution key dropped or renamed afterwards, and a longer interval is not waited for; no accompanying notice (for example the one saying the approval itself went through) and no attribution position is produced; exactly one user-facing sentence is shown (with the engine's suggested wait appended in seconds when given) and it contains no engine text. A 200 acknowledgement keeps its reading unchanged, bodies that are neither acknowledgements nor parking bodies are not read as parking, the durable decision path and its single holder have no parking cause, and a syntax-tree scan pins one reader declaration, one call site and the places where the reason word is minted, while the 202 body shape is pinned against every server generation in the fixture list. From 0.96.0 the base sentence is a public constant (`APPROVAL_PARKING_WARN_TEXT`, exactly the notice shown when the engine gives no wait hint, so a host that words its own parking notice no longer keeps a verbatim copy); the longer form with the wait hint and the notice function itself stay internal |
@@ -516,14 +516,14 @@ guard still cross-checks the table by name).
516
516
  | `scripts/run-workflow-decide-refusal-test.mjs` | Reader for the two refusals the decide route returns when a workflow sub-agent's pending approval cannot be handed to a single run record: `decide.workflow_park_forked` (the same run was resumed more than once and each copy waits on this same approval; the body's `runIds` come back exactly as sent — not reordered, not deduplicated, as a frozen copy — and a malformed list drops only that one field) and `decide.workflow_parks_unreadable` (the run record that owns the approval is unreadable or predates the owner field; the body's `runId` and the optional `readableHolderRunId` are read, the latter being a hint rather than a way out). Both join the existing workflow-lane refusal reader and are read by code, never by sentence; each has one user-facing sentence that carries no machine words, says nothing was delivered and the approval is still pending, and does not promise that resuming makes the decision deliverable. Both codes also count as "the approval is still waiting" by code, so a reworded server sentence containing a word like "already" is never mistaken for an already-decided approval. The guard drives the reader with real SDK error mappings from every witnessed SDK generation, with the bodies each server generation mints (older packages are pinned to "no fork code; the unreadable code carries only `runId`"; a package that mints the new bodies is run when present and reported as skipped otherwise), and compares plain inputs that carry neither new code byte for byte against the previous release. The reader never throws on a hostile error object: a field whose getter throws, or a list that is revoked part-way through validation, is dropped on its own while the recognised refusal is still returned. |
517
517
  | `scripts/run-output-style-seat-test.mjs` | The per-turn output-style seat. From 7.121.0 the request accepts a top-level `outputStyle` object (`name` and an optional `turnReminder`) that the engine states once per attachment cycle, and the capabilities body carries a constant top-level bit for it; the reader keeps the same three states as its constant-bit siblings and does not confuse it with the nested bit for the style body. The live-defaults layer places the host's `outputStyleSeat` on the request only when that reading is admitted, copies it as given (an empty name or extra keys are passed through for the engine to refuse at prepare time with `config.output_style_invalid`, which the configuration-refusal table recognises), never overwrites a caller's own top-level value, never invents one, and leaves the style body seat unchanged; the field is registered in the live-defaults list, survives the unregistered-key check on both lanes, and is reported as belonging to another channel when passed to the request assembler. |
518
518
  | `scripts/run-request-uplift-test.mjs` | Six request-assembly and reader pieces shared by every client. The per-run declaration that nobody on an unattended run can answer approval requests is carried only on the unattended lane, only when the engine reports it accepts that field (an older engine rejects the whole request), with one plain sentence for each reason it was held back; the headless permission mode is always stated explicitly, falling back to `default` when neither flag nor environment sets it; a predicate tells apart the denial an engine makes when nobody is reachable and no queue can hold the request; a host-reported route problem (no gateway address, or the anthropic channel with no credentials) turns a model that has a name but cannot start into a stated configuration problem; a reader gives, per model, which thinking levels the engine accepts; attachments from MCP resources and the new not-attached reasons are written as data envelopes rather than as tool calls; and servers the client mounts itself state their own allow rule, audited separately from the user's rules, with the rule-source trust order exported once. |
519
- | `scripts/run-decision-refusal-standing-test.mjs` | Shared decision-refusal reading for every client: `decisionRefusalAskReading` is the one reading of whether the question a refused decision was aimed at is already settled (settled, not settled, or unreadable), and settled comes only from positive evidence (a local no-pending stop, a 409 `conflict.ask_decided`, a `gone.ask` or a 410 other than a parked ask, a 404 whose body names an ending cause, or the matching error class when the code was stripped); a 404 without an ending cause, a not-found error class, a parked ask, the codes that only say the approval you looked at is no longer current, and wording in the failure text are not evidence. When the engine refuses an approval decision, `classifyDecisionRefusal` reports settled elsewhere exactly when that reading is settled, otherwise ambiguous (5xx except 501) or refused while the question may still be pending (every other 4xx and 501), returns nothing when no server verdict can be read, and marks transient refusals (408, 425, 429, 502, 503, 504, a body that says the request is retriable, or a 404 whose body says the id belongs to another replica); `liveAskPendingReading` reads whether the question is still in the engine's live pending list as pending, not pending or unknown (an unreported, malformed or partly dropped list is unknown); `decisionRefusalClosesOut` closes out only a refusal that is neither settled elsewhere nor transient and whose question the read confirms is still pending; `undeliverableDecisionContent` is the one sentence for each way the close-out ended. The guard covers the golden status/code table and near-miss codes, values that are not a verdict, guarded reads (throwing getters, prototype accessors, revoked proxies; the reading of the question itself takes every machine-readable slot of the failure from own data properties only: a value found only on the prototype chain is absent, and an own accessor makes the reading unreadable and is never run; the outcome note and the flattened outcome that also feed the reading take their status and error code from own data properties too, so a value on the prototype chain cannot be laundered into evidence), the outcome note shape, codes unsafe for display, the full close-out truth table and the exact sentences, and that the code literal and the classifier exist in exactly one place; it also checks that the classifier, the interrupt-deny check and the park-resume check give the same settled answer across 6912 failure shapes (the park-resume check also re-reads the stream when the approval it looked at is no longer current), the two shapes that used to get different answers, and that all three read the one shared reading. |
520
- | `scripts/run-no-allow-cause-test.mjs` | Why an automatic lane did not allow a call, and the refusal line for the two automatic-refusal lanes. `noAllowCauseOf(card, ladder)` combines the card's standing-rule verdict with the host's rule-ladder evidence (only an own, strictly `true` property counts): allowed or denied by a rule gives no cause; a local ask rule gives `ask_rule`; an exhausted automatic-approval budget gives `budget`; an ask rule the card names (its class or origin) gives `engine_ask_rule` when the ladder evidence shows that none of the user's own ask rules matched (the end-of-ladder no-match flag, or the dedicated flag for the case where an allow rule matched but the card's question class overruled it), and stays `ask_rule` when that evidence is absent, because the user's own rule cannot be ruled out; complete no-match evidence on a card a rule could clear gives `no_allow_rule`; everything else gives `other`, so no lane suggests adding an allow rule when that would not help. `policyDenyReason` mints the single line for the lane without an approval UI and for the dontAsk mode, one form per cause, naming only this one call and offering host-specific remedies (an allow-rule flag, a budget setting) only when the host supplies them. The guard covers the golden cases, pairwise ordering, strict reads, all ten sentences verbatim (the engine-side ask rule line says the user's settings cannot approve it and offers neither removing a rule nor adding an allow rule), unsafe tool names kept out of the sentence, and the single mint site. |
519
+ | `scripts/run-decision-refusal-standing-test.mjs` | Shared decision-refusal reading for every client: `decisionRefusalAskReading` is the one reading of whether the question a refused decision was aimed at is already settled (settled, not settled, or unreadable), and settled comes only from positive evidence (a local no-pending stop, a 409 `conflict.ask_decided`, a `gone.ask` or a 410 other than a parked ask, a 404 whose body names an ending cause, or the matching error class when the code was stripped); a 404 without an ending cause, a not-found error class, a parked ask, the codes that only say the approval you looked at is no longer current, and wording in the failure text are not evidence. When the engine refuses an approval decision, `classifyDecisionRefusal` reports settled elsewhere exactly when that reading is settled, otherwise ambiguous (5xx except 501) or refused while the question may still be pending (every other 4xx and 501), returns nothing when no server verdict can be read, and marks transient refusals (408, 425, 429, 502, 503, 504, a body that says the request is retriable, or a 404 whose body says the id belongs to another replica); `liveAskPendingReading` reads whether the question is still in the engine's live pending list as pending, not pending or unknown (an unreported, malformed or partly dropped list is unknown); `decisionRefusalClosesOut` closes out only a refusal that is neither settled elsewhere nor transient and whose question the read confirms is still pending; `undeliverableDecisionContent` is the one sentence for each way the close-out ended. The guard covers the golden status/code table and near-miss codes, values that are not a verdict, guarded reads (throwing getters, prototype accessors, revoked proxies; the reading of the question itself takes every machine-readable slot of the failure from own data properties only: a value found only on the prototype chain is absent, and an own accessor makes the reading unreadable and is never run; the outcome note and the flattened outcome that also feed the reading take their status and error code from own data properties too, so a value on the prototype chain cannot be laundered into evidence), the outcome note shape, codes unsafe for display, the full close-out truth table and the exact sentences, and that the code literal and the classifier exist in exactly one place; it also checks that the classifier, the interrupt-deny check and the park-resume check give the same settled answer across 6912 failure shapes (the park-resume check also re-reads the stream when the approval it looked at is no longer current), the two shapes that used to get different answers, and that all three read the one shared reading. The failure-note composer `decideFailureNote` is on the root entry as the same function the in-package decision exits use, and the sentence for an answer that does not fit its question has a sentence reader with English and Chinese catalog entries. |
520
+ | `scripts/run-no-allow-cause-test.mjs` | Why an automatic lane did not allow a call, and the refusal line for the two automatic-refusal lanes. `noAllowCauseOf(card, ladder)` combines the card's standing-rule verdict with the host's rule-ladder evidence (only an own, strictly `true` property counts): allowed or denied by a rule gives no cause; a local ask rule gives `ask_rule`; an exhausted automatic-approval budget gives `budget`; an ask rule the card names (its class or origin) gives `engine_ask_rule` when the ladder evidence shows that none of the user's own ask rules matched (the end-of-ladder no-match flag, or the dedicated flag for the case where an allow rule matched but the card's question class overruled it), and stays `ask_rule` when that evidence is absent, because the user's own rule cannot be ruled out; complete no-match evidence on a card a rule could clear gives `no_allow_rule`; everything else gives `other`, so no lane suggests adding an allow rule when that would not help. `policyDenyReason` mints the single line for the lane without an approval UI and for the dontAsk mode, one form per cause, naming only this one call and offering host-specific remedies (an allow-rule flag, a budget setting) only when the host supplies them. The guard covers the golden cases, pairwise ordering, strict reads, all ten sentences verbatim (the engine-side ask rule line says the user's settings cannot approve it and offers neither removing a rule nor adding an allow rule), unsafe tool names kept out of the sentence, and the single mint site. Since 0.109.0 the cause reads the verdict's question-level reading (whether an allow rule could clear this question at all), so it is byte-identical to the previous release over every card and ladder combination except one cell: a question an allow rule could clear, where the host's ladder did not prove that no allow rule matched, now gets its own cause and its own refusal line on both automatic-refusal lanes (only an allow rule for the whole tool can approve such calls ahead of time on this client; rules that name arguments apply only where the engine checks the real arguments), instead of the generic one; the guard fails when the previous release is not supplied. |
521
521
  | `scripts/run-wiring-instructions-attribution-test.mjs` | Which project instruction file the engine actually loaded for a run, attributed to the client's own directory. The capabilities bit `callerCwd` gets its own reader (the same four ports as every capability reader; `callerCwdAdmitted` accepts only an explicit `true`), and a host that notes the bit inside its probe callback reads the new value in the same tick. The working directory a submitted request carried is recorded per run in the same in-process ledger and with the same frame-binding rule as the file-history opt-out (`legCwdOfBody`, `declaredLegCwd`, `legCwdOf`). `engineSelectedInstructionsFile` then returns the selected member only when the run's directory equals the client's current directory, the member is a plain relative path (no `..`, no absolute or drive form, no backslash, no control, bidi or invisible characters), and the host's real-path function confirms it stays inside that directory (a symlink pointing outside is refused). |
522
522
  | `scripts/run-layering-direction-test.mjs` | The package's layering table (`docs/LAYERING.md`) and its direction gate. Every source file sits on exactly one internal layer (wire readers and injected ports, projection, host-facing orchestration, the two entry points) and may import only downward, type-only imports included; the exemption table only shrinks. Every import that leaves the package must match a row of the cross-repository table and the kind that row allows: the engine package is read for types only and never at runtime, and the shipped declaration files reference only the packages listed in `dependencies` and `peerDependencies`. |
523
523
  | `scripts/run-sdk-witness-compile-test.mjs` | The package source compiled against every SDK generation the peer range promises: each installed SDK at or above the declared peer floor (read from `package.json`, not hand-pinned in the guard) is pointed at through the compiler's `paths`, with the package's own `tsconfig.json` options, and `src` must produce zero diagnostics against each one. The ruler is proven to speak on every generation: the program covers every `src` file found by an independent directory walk, every SDK import in `src` resolves into the generation under test rather than the development dependency, and an in-memory planted call that omits the required `baseVersion` and a status word outside the closed union must both be reported while the corrected call stays silent. A probe tree installed on disk but missing from the witness list is red; an absent probe set is an honest skipped section, never green. Type pins that need an SDK newer than the floor live outside `src` in a pin file named after the lowest SDK version they need; every witness at or above that version compiles the pin file in the same program, each named pin group must be free of diagnostics, and an in-memory false pin written with that generation's real types must be reported while its true control stays silent. The published declaration files, emitted in memory from the development SDK, must type-check with zero diagnostics against the witness at exactly the floor version (absent: an honest skipped section), so the public surface never names a type the floor generation lacks; a planted declaration that names a type only the newer SDK exports must be reported against the floor and stay silent against the development SDK. |
524
524
  | `scripts/run-hooks-managed-policy-reading-test.mjs` | The managed-policy reading (`HostSettingsSource.managedPolicyReading()`; formerly the optional `SettingsPort.managedPolicyReading()`, retired in 0.101.0) that tells the hooks projection whether the managed policy layer could be read. Before it, only a read that threw counted as an unreadable governance layer, while a host's per-source reader usually does not throw: a managed file that cannot be read comes back as null and the projection treated that as no managed policy, so user, project, local and flag hooks went out to the engine exactly when an administrator's restriction had disappeared. The reading has three words. `absent` (readable, nothing configured) and `in-effect` (readable, has content) project exactly as before — the governance gates in the managed document still decide — while `unreadable` closes the whole leg: no hooks from any source and no session-level goal hook are sent, and enabled plugins are listed as excluded for an unreadable governance layer. The change only tightens: for the three readable answers (reader missing, `absent`, `in-effect`) the projection is byte-identical on the same managed document, and `absent` never overrides a document that says hooks are disabled. A reader that throws, or answers anything that is not one of the three words (including a different letter case, an empty object, or a property where a method is expected), counts as `unreadable`; only a missing member means the host did not wire the port, which keeps today's behaviour and logs nothing. The http-hook lists read from the settings layers follow the same rule: for the three readable answers the plan is byte-identical, and an unreadable managed layer closes the leg before any list is read. |
525
- | `scripts/run-park-expiry-judgements-test.mjs` | The judgements the approval flow needs once a streamed approval's window has closed and the engine has moved the call into its durable queue instead of refusing it. `classifyApprovalCompleteFrame` reads a closing frame as one of three outcomes (moved to the queue, no queue evidence so retract the card, decided by a person) or none for anything it does not recognise; only a literal `true` counts as queue evidence, and a new outcome word is never mistaken for an expiry. `parkRowDeadlineState` reads a durable row's `deadline` into none, pending or past, treating a missing, null or absurd value as no deadline rather than clamping it. `lateDecideRoute` picks the endpoint for a late decision (the durable decide endpoint only when the window had closed and the row was actually found). `runReapedByDeadline` reads the run record and answers true only for the reaped-by-deadline error code; anything unreadable is false, so an unreadable record is never reported as a step that was not run. The guard carries the terminal client's offline cases over one by one and runs a frozen copy of that client's original implementations side by side over more than fifteen hundred inputs; the registered differences, both on the conservative side, are that a host client whose property read throws is answered with false instead of a rejection, and that a park bit sitting on the prototype chain or behind an accessor is not read as moved-to-the-queue (it is read through the same single reader the desktop seat uses, which only accepts own data properties). |
526
- | `scripts/run-usage-analytics-projection-test.mjs` | Reads of the engine's usage analytics (`GET /v1/usage/summary`, `/series` and `/breakdown`) and the card view built from them. Each reader never throws and answers ok, unavailable (the engine says it has no such face: no run store, no such route, or the feature is switched off) or failed (any other read failure, which is never reported as the backend lacking the feature). A missing number is simply absent from the view, never zero, and a total counts as exact only when the engine says `estimated: false`: otherwise it is an estimate and the view says so — a lower bound for money, an approximation for tokens, each with a shared prefix for the host to render. The card view has three states: available, estimated, and unavailable with one sentence for a backend that has no analytics and a different one for a read that failed; an unavailable card carries no numbers at all. The heat map and the active-day count exist only when the day series was both read and read completely, so a failed read, a series with unreadable points or invalid dates, or one the engine reports as truncated is never drawn as a grid of zero-activity days; the peak hour and the favourite model follow the same completeness rule, and a failed per-model breakdown is different from a window with no model usage. A point's timestamp is read once for both the day and the hour series: the same date-and-time grammar, a real calendar date, an explicit time zone for date-time forms (offsets are converted to UTC first), and a point that cannot be read is dropped and counted as unreadable in either series. The guard also runs a real SDK client against a stubbed fetch and checks the four requests it makes. |
525
+ | `scripts/run-park-expiry-judgements-test.mjs` | The judgements the approval flow needs once a streamed approval's window has closed and the engine has moved the call into its durable queue instead of refusing it. `classifyApprovalCompleteFrame` reads a closing frame as one of three outcomes (moved to the queue, no queue evidence so retract the card, decided by a person) or none for anything it does not recognise; only a literal `true` counts as queue evidence, and a new outcome word is never mistaken for an expiry. `parkRowDeadlineState` reads a durable row's `deadline` into none, pending or past, treating a missing, null or absurd value as no deadline rather than clamping it. `lateDecideRoute` picks the endpoint for a late decision (the durable decide endpoint only when the window had closed and the row was actually found). `runReapedByDeadline` reads the run record and answers true only for the reaped-by-deadline error code; anything unreadable is false, so an unreadable record is never reported as a step that was not run. The guard carries the terminal client's offline cases over one by one and runs a frozen copy of that client's original implementations side by side over more than fifteen hundred inputs; the registered differences, both on the conservative side, are that a host client whose property read throws is answered with false instead of a rejection, and that a park bit sitting on the prototype chain or behind an accessor is not read as moved-to-the-queue (it is read through the same single reader the desktop seat uses, which only accepts own data properties). Since 0.109.0 the same three-way reading also covers the single-card closing frame of the presented-card frame family (`approval_complete`): the frame's own data properties are first copied into one snapshot (accessors, proxy traps and inherited keys are never run or read), the snapshot is narrowed with the type package's v1 predicate and then read by the very same judgement, a frame that does not narrow is read as none so no outcome is guessed, and the old-family reading is unchanged. |
526
+ | `scripts/run-usage-analytics-projection-test.mjs` | Reads of the engine's usage analytics (`GET /v1/usage/summary`, `/series` and `/breakdown`) and the card view built from them. Each reader never throws and answers ok, unavailable (the engine says it has no such face: no run store, no such route, or the feature is switched off) or failed (any other read failure, which is never reported as the backend lacking the feature). A missing number is simply absent from the view, never zero, and a total counts as exact only when the engine says `estimated: false`: otherwise it is an estimate and the view says so — a lower bound for money, an approximation for tokens, each with a shared prefix for the host to render. The card view has three states: available, estimated, and unavailable with one sentence for a backend that has no analytics and a different one for a read that failed; an unavailable card carries no numbers at all. The heat map and the active-day count exist only when the day series was both read and read completely, so a failed read, a series with unreadable points or invalid dates, or one the engine reports as truncated is never drawn as a grid of zero-activity days; the peak hour and the favourite model follow the same completeness rule, and a failed per-model breakdown is different from a window with no model usage. A point's timestamp is read once for both the day and the hour series: the same date-and-time grammar, a real calendar date, an explicit time zone for date-time forms (offsets are converted to UTC first), and a point that cannot be read is dropped and counted as unreadable in either series. The guard also runs a real SDK client against a stubbed fetch and checks the four requests it makes. Since 0.110.0 it also covers `centerUsageCostOf`, which reads the money field of a cloud control plane's usage body (`usage/me`, `usage/summary`) into either an amount with its bound (the same `exact` / `lower` words the engine card uses, plus a flag that says the body itself reports unpriced usage) or an unknown with one of four causes and no number at all. The control plane's `costBasis` word (`exact`, `lowerBound`, `unknown`) is read from own data properties only, with accessors never run; a word outside the set, or one that cannot be read, is unknown, and an `exact` word is lowered to a lower bound when the body itself raises `estimated` or `truncated`. A body without the key (an older control plane) is judged from `estimated` and `truncated` through the same bound derivation the engine card uses, and an estimated amount of zero is unknown rather than zero. A malformed amount is unknown, never zero, and a hostile body never makes the reader throw. |
527
527
  | `scripts/run-interactive-interrupt-emitter-test.mjs` | The interactive interrupt sender (`fireInteractiveInterrupt`): one bare `POST /v1/runs/:id/interrupt` through the client's runs resource, with the method called on its receiver, the session parameter sent only when non-empty and no idempotency key, so exactly one request goes out. Every outcome is a value, never a throw: a receipt yields `halted` with `turnCut` read only from an own boolean data property; a thrown error is classified by the same at-most-once judge the steer and cancel paths use, so a definite refusal becomes `refused` with its status and code, while a 5xx, a network failure or an abort becomes `transport` (no verdict). A missing resource yields `unarmed` with zero requests. The suite cross-checks every thrown sample against the shared judge, drives a real SDK client with a stubbed fetch and feeds the outcomes to `planInteractiveHalt`. |
528
528
  | `scripts/run-device-rebind-judgement-test.mjs` | The device rebind flow (`listDevices`, `readSessionBinding`, `rebindSession`, `reconcileRebindOutcome`, `rebindProvesRequestNotApplied`): each verb goes through the client's devices resource (the binding read goes through the sessions resource: the execution placement read, a writer on the device lane with a readable device is the only prior state, an unknown device on that lane is unreadable rather than unbound, and an older server without the read is unsupported) and returns a discriminated outcome, never a throw. Refusal codes are matched as status-and-code pairs from one closed table (device lane absent, run store absent, not found, active run, revision conflict, service token required); any other failure of the rebind write is classified by the shared at-most-once judge into `refused` or `unconfirmed`. A rebind success is accepted only when the echoed session, the target device and the revision step all hold, and the workspace carryover disclosure is carried only for the literal `none`. Response fields are read as own data properties. Includes a real SDK client with a stubbed fetch and a full grid of the read-back reconciliation. The nine device row types from the SDK (`DeviceSummary`, `DeviceRebindResult`, and the rest) are re-exported as types from the package root, and the guard checks each one is identical to the SDK's; the two binding-read types the SDK removed are no longer exported. |
529
529
  | `scripts/run-engine-env-sourcing-test.mjs` | The local-engine environment group sourcing judge (`resolveEngineEnvSourcing`, `engineEnvView`): when a settings source wrote half of an all-or-nothing group and that half is ignored, the group is either skipped (its own switches are dropped) or, for safety groups and groups declared not skippable, left for the engine to refuse with an explanation. Group declarations, the ignore rule and the alias table come from the host; the module holds no retired-name table and reads no platform state. The suite runs a seeded differential test against a frozen transcription of the previous host-side loop, fixed cases for alias precedence, source precedence and case folding, and scans the built module for every retired environment name in the settings contract. |
@@ -535,7 +535,7 @@ guard still cross-checks the table by name).
535
535
  | `scripts/run-rewind-engine-action-test.mjs` | The two pure judgements the terminal client used to keep for its rewind command. The dispatch gate says whether, right now, the engine has explicitly said that a given rewind mode cannot be done: each of the three modes (rewind with code restore, rewind the conversation only, restore code only) reads the same availability judges the package already ships for the menu, only an explicit off refuses, and a missing, unreadable or unobserved reading never refuses. The action judgement takes the chosen option, the target position the host has already resolved (session root, after the root, or undecidable, with the engine entry handle if there is one) and the capability reading, and answers whether to arm the one-shot rewind keys, to reopen the engine session (the two modes that carry a resume point, chosen at the session root, which the server will not rewind past) or to do nothing, with a reason; an undecidable position never acts in either direction, and the code-only mode looks only at the handle. The preview used by the confirmation screen is the same answer's kind. The guard checks both judgements against hand-written truth tables over every bit combination and every target shape, that the dispatch gate and the menu can never disagree, that the default reading follows the current engine target, that hostile inputs never throw and never act, and that the module keeps no mode list or resume-point list of its own. A live comparison against the terminal client's own copies runs in the layering guard. |
536
536
  | `scripts/run-tool-result-content-projection-test.mjs` | How a tool's result body becomes the content of the tool result in the transcript. A result that carries images keeps them: each image whose data is a string and whose type is supported (JPEG, PNG, GIF or WebP) appears as an image block with exactly the bytes the engine sent (the data is not re-validated, matching the reference client), in order, in both the interactive and the print output, while the structured result next to it stays the same text as before. A block that cannot be shown (an image without string data or with an unsupported type, or a block of an unknown kind) is left out and the content ends with one notice saying that unsupported content was omitted, so nothing disappears silently. Plain text and text-only results are unchanged strings, a body the engine already shortened stays a string with the truncation note, and an error result's content is always text. The text readings of a result body (the flattening helpers and the subagent viewer's result slot) come from the same projection and agree with each other. That projection is also the one text reading behind the public `toolEndOutputText` (an empty block list reads as an empty string, an image as `[image]`, an object that is not a string or a block list as absent) and behind the classifier-denial and aborted-call checks, so a frame that carries an image or an unreadable block is never mistaken for one of those engine-minted bodies, and a classifier denial is recognised only when it arrives as a bare string or a single block (a signature split across blocks is not a denial). Whether a result is an error is decided once for both the interactive and the print output: a structured result reporting a failing exit code is an error in both, whatever the tool's name and even when the result carries no body. The task-output tool's content is the model-facing text in both outputs, with the structured task record kept beside it. A result block that reports a media item as not delivered (a descriptor with the media type, an optional MIME type and the decoded byte count, or a summary block with the number of folded blocks and their total bytes) is shown as one sentence at its own position instead of being swallowed by the single unsupported-content notice, and a block whose fields are malformed stays an unreadable block. The same judgement is public: `toolOutputMediaAbsenceOf` reads such a block and `toolOutputMediaAbsenceSentence` gives its sentence in English or Chinese. |
537
537
  | `scripts/run-refused-park-family-test.mjs` | The refused-resume family (`refusedParkFromError`, `refusedParkContent`, `refusedParkBlockedContent`): three 409 codes whose refusal comes back every time the same decision is sent again (the stored park is not supported by this engine version, its resource ledger carries a retired wall-clock allowance, or the engine's gate refuses its stored task configuration or permission-mode seed). The suite checks the reader on SDK error objects and on flattened failure facts (status policy, field token, still-pending reading, getters that throw), compares every sentence byte for byte against a frozen copy of the terminal's previous implementation over each combination of code, field, pending state, lane and run id, pins the deliberate differences one by one, and checks that the sentences are correct per lane, never print an unknown code, pass the package hygiene list and do not trip the already-resolved word scan. Since 0.108.0 the interactive way-out sentence holds for both engine generations: send a message and the client offers to cancel the run, answering the card first if it comes back instead (en / zh). |
538
- | `scripts/run-prompt-blocked-sentence-projection-test.mjs` | The sentence for a run whose first prompt was held back by a deployment's `userPromptSubmit` hook. The engine frames its own explanation for the model in an envelope that carries the session's reminder mark, and the terminal row used to print that envelope verbatim (`Run blocked: <system-reminder mark="…">…`) while the machine-readable cause beside it went unread. The package now has one sentence-maker for the `blocked` terminal, shared by the terminal row and the result frame's first error: a hook that refused the prompt reads `UserPromptSubmit operation blocked by hook:` followed by the hook's own words (treated as untrusted text — control and bidirectional characters shown as visible escapes, credentials washed, folded to one line, envelope tags removed even when spelled with zero-width characters — and since 0.104.0 also when the engine hands them over in its defused entity spelling `&lt;system-reminder>`, when they are `task-notification` tags, or when the engine's length cut left half a tag glued to its truncation mark `…` (half a tag name without that mark is ordinary text and stays, so a trailing comparison such as `elapsed < t` is kept); only the display copy is decoded, and only the two entities the installed engine actually emits, which the suite pins by running the engine's own defusal functions); a hook that crashed, timed out, or was cancelled mid-check gets one fixed sentence each, saying the prompt was not submitted; an unrecognised cause word prints only the lead and never echoes the word. When the machine-readable cause is missing or malformed, the old `Run blocked: <reason>` line stays, with a whole engine envelope unwrapped so no tag or mark reaches the screen, and an agent's own free-text reason is byte-for-byte what it was. The new line counts as a governance stop (it is not an `API Error:`) and is recognised by the same row predicate as `Run stopped` / `Run blocked`. The suite also pins the single mint site in source, covers the interactive lane, and checks the tag remover is linear on near-miss input. |
538
+ | `scripts/run-prompt-blocked-sentence-projection-test.mjs` | The sentence for a run whose first prompt was held back by a deployment's `userPromptSubmit` hook. The engine frames its own explanation for the model in an envelope that carries the session's reminder mark, and the terminal row used to print that envelope verbatim (`Run blocked: <system-reminder mark="…">…`) while the machine-readable cause beside it went unread. The package now has one sentence-maker for the `blocked` terminal, shared by the terminal row and the result frame's first error: a hook that refused the prompt reads `UserPromptSubmit operation blocked by hook:` followed by the hook's own words (treated as untrusted text — control and bidirectional characters shown as visible escapes, credentials washed, folded to one line, envelope tags removed even when spelled with zero-width characters — and since 0.104.0 also when the engine hands them over in its defused entity spelling `&lt;system-reminder>`, when they are `task-notification` tags, or when the engine's length cut left half a tag glued to its truncation mark `…` (half a tag name without that mark is ordinary text and stays, so a trailing comparison such as `elapsed < t` is kept); only the display copy is decoded, and only the two entities the installed engine actually emits, which the suite pins by running the engine's own defusal functions); a hook that crashed, timed out, or was cancelled mid-check gets one fixed sentence each, saying the prompt was not submitted; an unrecognised cause word prints only the lead and never echoes the word. When the machine-readable cause is missing or malformed, the old `Run blocked: <reason>` line stays, with a whole engine envelope unwrapped so no tag or mark reaches the screen, and an agent's own free-text reason is byte-for-byte what it was. The new line counts as a governance stop (it is not an `API Error:`) and is recognised by the same row predicate as `Run stopped` / `Run blocked`. The suite also pins the single mint site in source, covers the interactive lane, and checks the tag remover is linear on near-miss input. Since 0.110.0 that sentence-maker, `runBlockedText`, is also exported from the package root as the very same function (not a copy), so a host that renders a blocked run itself calls it instead of writing its own line; the suite pins the identity, the unchanged signature, that the English lead constant stays out of the root surface, and that the root call returns the same bytes as the terminal row and the result frame's first error across nine blocked shapes. |
539
539
  | `scripts/run-active-run-busy-read-projection-test.mjs` | How the package reads a rejected submission (the session already has a run in flight) in its terminal row, in its result frame and in the public discriminator `activeRunBusySignal`: one discriminator body and one carrier rule, shared by all three. The server's rejected and conflict terminal frames carry `status: 'failed'` since 7.125.0; the older idempotent-replay form — a cached copy of the original 409 conflict body wrapped as `done{ result, replay: true }`, with neither `status` nor a terminal word — is no longer recognised, since the package's support floor moved past the servers that sent it. The suite pins what such a body reads as today (the discriminator answers null, the row is the single `Outcome unknown` line, the result frame carries the unknown-outcome flag, the public projector agrees, with or without the replay flag, code only, id only), checks over a pool of sixteen frames that the discriminator and the row agree in both directions (including a cause-form frame that carries the busy code), pins which objects count as the carrier, pins that the retired folding function and its flag are gone from the source and the build, and pins in source that the discriminator and the carrier rule are each defined once. |
540
540
  | `scripts/run-fleet-terminal-retain-test.mjs` | The fleet ledger's rule for terminal rows has one home and one function. A terminal row (completed, failed, killed) lives only in the retention pool and never in the active set, whichever way it arrives — a task frame or a reconnect snapshot — and both arms go through the same admission function. A row brought back by a reconnect snapshot therefore expires with the retention window exactly like one that arrived as a frame; before, the snapshot arm left it in the active set for good, and a lost removal frame did the same to a frame-delivered row, and clearing the pool could not clear it. The suite refutes the change on three axes: timing (snapshot before or after the terminal frame, interleaved with removal, repeated delivery, resurrection, a later snapshot that does not carry the row, a row being viewed, pool clearing, and every terminal word through both paths with identical answers), concurrency (twin rows of one task in either order, a notification-retired entry meeting a snapshot row, notifications against a snapshot-carried row, two sessions' ledgers), and failure class (a status outside the terminal vocabulary, absent, or null is never turned into a terminal row; rows without a usable id, with a non-string id, or null entries are dropped and counted, never thrown on). It also pins that the removal hook still reports a known row for a settled terminal row, because downstream bookkeeping credits a removal only when neither the stale mark nor the unknown-row mark is present. Finally it pins the terminal evidence the ledger keeps past the window (status word plus cycle, per task tail, bounded): after a row has aged out or the pool was cleared, a same-cycle completed notification cannot turn a known failure or kill into a success, while a newer cycle, a revival, and the upgrade direction still pass; the evidence itself is cycle-monotonic, so an older generation's terminal row, or one arriving late after a revival, neither overwrites nor re-plants it. Since 0.97.0 it also pins the ledger's single per-task rule, applied in the one admission function to task frames and snapshot rows alike: a reading from an older cycle than the task has already shown is ignored (it neither revives the row, nor withdraws the terminal evidence, nor lets a snapshot drop the task's newer in-flight row); a newer cycle retires older in-flight aliases of the same task; within one cycle, or when the order cannot be told, an in-flight alias wins over a terminal row; otherwise the terminal row settles the task and twin rows collapse into one. Every arrival order of a set of readings, through either arm, gives the same projection and the same in-flight reading. A notification from an older cycle than the task has shown leaves the rows alone even when the running row's latest frame carried no cycle, and the bounded per-task cycle record never evicts a task that still has rows. |
541
541
  | `scripts/run-bg-subagent-row-test.mjs` | The background-subagent identity predicate every end shares. The fleet read surface lists every non-terminal task row — and since server 7.125.0 a session-scoped stream also brings back the session's own run row and its foreground delegation rows — so "is this row a background subagent" needs exactly one answer. `isBackgroundSubagentRow` applies four rules in order: a row with no id, a composite id, or a `run-leg` lane mark is never a subagent (the last two are foreground delegation rows); a non-agent handle (background shell or monitor) never is; a single-segment row with a readable parent is; a row without a readable parent is only when its id is an agent-family handle. `isBackgroundWorkRow` adds the non-agent handles for the "N background tasks" counts, and `isOwnBackgroundSubagentRow` adds ownership — a session-scoped stream replaces the ownership half only, never the row-kind half. The handle shapes are not retyped here: the durable agent-handle pattern is the installed engine's own, and the non-agent prefix letters are read from the engine's task registry when the tables are generated. The read-surface row now carries `sourceLane` (only when the wire row has a non-empty string), and task frames and reconnect snapshots reach the predicate through the same row shape. The suite checks each rule with a positive and a negative example, the absence cases (no id, unreadable parent, unusable lane mark), the 7.125.0 stream shape against the older shape, and that the lane word and the prefix table each have exactly one home in the source. |
@@ -546,19 +546,19 @@ guard still cross-checks the table by name).
546
546
  | `scripts/run-seat-apply-config-reason-test.mjs` | The seat's model-configuration receipt now carries a closed set of reasons instead of a free-form string. When a host refuses to write the configuration it answers with one of five words — the model catalog on this device shadows the write, the cheap model asks for a different provider, the request was not valid, the file could not be written, or the app cannot change the configuration right now — and the one-line human sentence for each word is minted in exactly one place, so a client renders it instead of keeping its own table. Free-text diagnostics moved to an optional `detail` field that is display-only. The outbound gate refuses any other word, including prototype-chain names and case variants, and refuses a failed receipt with no reason. The guard also reads the built declaration for the exact member list, compiles probes that must fail to type-check, and pushes each word through the gate, a structured clone and the sentence reader to show the renderer reads the same sentence the host chose. |
547
547
  | `scripts/run-seat-stream-history-test.mjs` | The seat's streaming-preview chunk and the engine session history projection each have one definition now. Live preview text used to be a shape the desktop host built by hand and the web client unpicked by hand; the package now mints it (`seatStreamDeltaMessage`) and reads it (`readSeatStreamDelta`, which tells a chunk, an unknown inner event, a malformed chunk and an ordinary message apart, so a chunk is never mistaken for a committed message). Rebuilding a transcript from the engine's stored session rows used to be written once per client with diverging rules; `projectEngineSessionMessages` is the single projection: it cuts out the engine's own prepended content using the provenance marks the engine stamps on each user row (never by guessing from text shape, so a user's own text that merely looks like a reminder tag is kept), keeps tool results as tool-result blocks, maps the engine's block vocabulary to the chat-message vocabulary, never invents a timestamp the row does not carry, copies tool arguments and unknown blocks as plain JSON so what it returns can always be cloned and sent, and reports every row or block it could not read instead of dropping it silently. The guard checks the minted bytes against the previous hand-built shape, runs a real adapter stream through mint, gate, structured clone and read, pins ten stored rows to an exact expected transcript, checks the provenance rules case by case (segments across blocks, malformed ranges, absent marks), and feeds hostile rows (getters, throwing proxies, sparse arrays, functions and cycles inside tool arguments) to show one bad row never takes the others down. A replayed tool-result row that the engine archived as parked at an approval gate now carries a machine-readable parked flag on its tool-result block, so a replay does not render it as a failure. |
548
548
  | `scripts/run-seat-recents-test.mjs` | The seat's recent folders. The recent-folders list is read in exactly one way: the folders of the session records (derived, never registered by the host) joined with the folders the host registered (the ones the user picked, whether or not a session ever opened there), de-duplicated by exact path, most recent first, and a path present on both sides counts once at its later time. `recentFoldersOf` is the pure reader; `readRecentFolders` is the orchestrating reader that calls `listRecentFolders` only when the seat capability table says `recents` is available, and that keeps the session-record half when the host half is not offered or cannot be read. The seat gets two optional verbs, `listRecentFolders` and `noteRecentFolder`, and one capability bit `recents` that governs both (an absent bit means the host did not say, never "none"). The guard checks the registration, the outbound gates row by row (plain objects only, so what passes the gate is what survives structured cloning), the capability reading and its three distinct sentences, the reader's absence and ordering rules (a reply whose rows are all unreadable is reported as unreadable, never as an empty list), and an end-to-end chain from the host's capability publication through the renderer's reading to the host's verb double. |
549
- | `scripts/run-approval-card-v1-projection-test.mjs` | Projection of the v1 approval card (the `card` of an `approval_request` frame) into approval-card request facts through `approvalCardFactsOfV1`. For every key the v1 card shares with the live approval frame, the same value, well-formed or malformed, fed to both paths yields the same card request key and value; rule offers land in the read-only slot because the ask-decision route carries no rule; the real-approval bit is read through the single real-approval reader (top level or `risk`); every key in the SDK card key list has a recorded disposition, so a new SDK card key fails the gate; keys outside the card contract are not read, host positions are not minted and the session-answer route fact is not stamped; the grant-binding state matches the live path; refusal evidence that is present on the card but unreadable (malformed, inherited or throwing) is recorded as `unreadableEvidence` identically on both paths; non-objects yield undefined, one throwing key drops only that key and records it, and the result is frozen; the v1 card, live-frame and generic-card projections answer every shared key identically; the two who-asked positions (`agentId`, `askReasons`) are read by one reader on the live frame and on the card, covering well-formed, absent and malformed shapes (bad items are dropped one by one, an unreadable array drops the whole position), open-ended words kept as they are, getters never run and the output frozen, and the generic card does not read them. |
550
- | `scripts/run-approval-card-reading-projection-test.mjs` | Single reading of approval-card facts. The live-frame projection `approvalCardFactsOfFrame` yields, key for key, the facts the live approval path hands to the card port (well-formed and malformed frames alike), including the route, grant-binding and unreadable-evidence facts `standingRowsOf` needs, and a live card whose must-ask evidence could not be read is never answered automatically; the generic-card projection `approvalCardFactsOfGeneric` reads the version-independent keys with the same readers as the v1 projection, reads nothing else, marks the payload unreadable and leaves every automatic-answer lane at unknown; the approval-window reader `approvalWindowKeysOf` accepts only non-negative finite values, needs a current frame, stamps the arrival anchor only when a window is present, requires a coherent envelope for unknown frame shapes, and matches the window on the live card; `probeCauseRowsOf` renders the probe-cause roots and further rows or nothing when the shape does not match, with untrusted text neutralised; `approvalCardReasons` orders the single-sentence reasons and the engine's own message, each line equal to its source sentence, deduplicated, within a line budget that reports what it hides; `readRequiresRealApproval` reads the top level or `risk` and every package reading point agrees with it; `countOwnedPendingRows` counts owned rows this client can present and throws instead of answering zero when the list cannot be read. On the durable parked-approval path the card's tool input is the pending row's `input` — the bytes that will run — and the event stream's `tool_start.args` is used only when the row carries no real input; when both are present and differ as JSON values (compared node by node, stopping at the first difference, within a node and depth budget) the card carries `argsRewritten: true` with an explanatory note, a comparison that cannot be decided adds nothing, and when they match the card is byte-identical to the one built without the stream copy. The live approval card reads its tool input through the same single reader: the frame's own `args` (the bytes that will run) are used whenever they are a plain object, the stream copy is only a comparison partner then, a difference adds the same `argsRewritten` flag and note, and the stream copy is shown only when the frame carries no real input, with a note saying it is the model's original call; the suite also checks from the source syntax tree that both paths call that one reader and that no other function repeats its order or comparison. |
549
+ | `scripts/run-approval-card-v1-projection-test.mjs` | Projection of the v1 approval card (the `card` of an `approval_request` frame) into approval-card request facts through `approvalCardFactsOfV1`. For every key the v1 card shares with the live approval frame, the same value, well-formed or malformed, fed to both paths yields the same card request key and value; rule offers land in the read-only slot because the ask-decision route carries no rule; the real-approval bit is read through the single real-approval reader (top level or `risk`); every key in the SDK card key list has a recorded disposition, so a new SDK card key fails the gate; keys outside the card contract are not read, host positions are not minted and the session-answer route fact is not stamped; the grant-binding state matches the live path; refusal evidence that is present on the card but unreadable (malformed, inherited or throwing) is recorded as `unreadableEvidence` identically on both paths; non-objects yield undefined, one throwing key drops only that key and records it, and the result is frozen; the v1 card, live-frame and generic-card projections answer every shared key identically; the two who-asked positions (`agentId`, `askReasons`) are read by one reader on the live frame and on the card, covering well-formed, absent and malformed shapes (bad items are dropped one by one, an unreadable array drops the whole position), open-ended words kept as they are, getters never run and the output frozen, and the generic card does not read them. Since 0.110.0 the card-only entry does not read `requestedBy` (the upstream card keys do not carry it); only the frame-level entries do. |
550
+ | `scripts/run-approval-card-reading-projection-test.mjs` | Single reading of approval-card facts. The live-frame projection `approvalCardFactsOfFrame` yields, key for key, the facts the live approval path hands to the card port (well-formed and malformed frames alike), including the route, grant-binding and unreadable-evidence facts `standingRowsOf` needs, and a live card whose must-ask evidence could not be read is never answered automatically; the generic-card projection `approvalCardFactsOfGeneric` reads the version-independent keys with the same readers as the v1 projection, reads nothing else, marks the payload unreadable and leaves every automatic-answer lane at unknown; the approval-window reader `approvalWindowKeysOf` accepts only non-negative finite values, needs a current frame, stamps the arrival anchor only when a window is present, requires a coherent envelope for unknown frame shapes, and matches the window on the live card; `probeCauseRowsOf` renders the probe-cause roots and further rows or nothing when the shape does not match, with untrusted text neutralised; `approvalCardReasons` orders the single-sentence reasons and the engine's own message, each line equal to its source sentence, deduplicated, within a line budget that reports what it hides; `readRequiresRealApproval` reads the top level or `risk` and every package reading point agrees with it; `countOwnedPendingRows` counts owned rows this client can present and throws instead of answering zero when the list cannot be read. On the durable parked-approval path the card's tool input is the pending row's `input` — a redacted copy of the bytes that will run — and the event stream's `tool_start.args` is used only when the row carries no real input; when both are present and differ as JSON values (compared node by node, stopping at the first difference, within a node and depth budget) the card carries `argsRewritten: true` with an explanatory note, a comparison that cannot be decided adds nothing, and when they match the card is byte-identical to the one built without the stream copy. The live approval card reads its tool input through the same single reader: the frame's own `args` (a redacted copy of the bytes that will run) are used whenever they are a plain object, the stream copy is only a comparison partner then, a difference adds the same `argsRewritten` flag and note, and the stream copy is shown only when the frame carries no real input, with a note saying it is the model's original call; the suite also checks from the source syntax tree that both paths call that one reader and that no other function repeats its order or comparison. Since 0.109.0 a read-root candidate whose covers key is hidden by a lying has trap is dropped whole instead of being read as the directory form. On the durable parked-approval path, when the row carries no usable input and the card falls back to the stream copy, the card says so, is marked as not carrying the input that will run, and an edited approval sends nothing (plain approvals and denials are unaffected); the live-frame fallback is unchanged. |
551
551
  | `scripts/run-fail-sentence-test.mjs` | The wording of failure sentences that reach the user, in two parts. (1) A scenario refusal now has one sentence owned by the package: `scenarioNotAllowedContent` renders the subject, the allowed list (names escaped but never truncated, empty list omitted) and, for a fresh request only, the way out of omitting the scenario; the resumed-work form leaves that way out off, since a decision has no scenario to omit. The four decision exits (approve, deny, question card, plan review) append the sentence after the resume-policy sentence when the engine folded the refusal into `resume_blocked_by_policy`, and the failed outcomes of the two approval legs carry the folded original code as `blockedBy` next to the outer `errorCode` (present only when the outer code is the fold code and the body names a non-empty original). (2) The remaining user-facing failure sentences read a thrown value the same single way as the earlier seven: the four write and read exits (persisted rules, session policy, memory governance, MCP probe) no longer fall back to the class name or `[object Object]`; the subagent steer exit and the plan-review outcome sentence use the same reading and send the original thrown value's log form to the host log; the parked-row wait and the local rule parse take an optional `onFailure(log)` so their pure modules can hand the original to the host log; and the retry row's stalled `detail` goes through the same display exit as the four error arms. |
552
552
  | `scripts/run-engine-account-keys-projection-test.mjs` | The projection of five engine keys that older packages dropped on the way to the screen: a tool call's `recovered` start frame and `notExecuted` end frame (literal-true marks, read from own data properties only — never from a getter — and never turned into a claim about whether the call ran: absence means nothing), the completed run's machine-readable detail (the provider ended the last turn with a stop reason the engine does not read), the provider's account-condition word on a refused run, and the two notices that report an unknown stop reason and a stream that kept talking after its first ending. The suite drives the built package through the projection, the transcript adapter and the print-mode frame, and checks that absent, falsy, malformed and hostile values produce no claim and never throw, that the terminal reader keeps the run's kind whatever its detail looks like and isolates each key, and that the single reader for the marks is the only way the sources read them. |
553
553
  | `scripts/run-seat-devices-test.mjs` | The seat's device face. Whether the device face is usable, and what it can do, is read in exactly one way: `devicesFaceOf` takes whether the host wired the device verbs (the seat capability `devices`, or on a direct host whether it holds the SDK slice), the engine's device-lane reading when there is one, and the outcome of the last device list, and answers one of unavailable (with a closed reason), probe pending, probe failed, verbs off, or manage. On the seat lane there is no capability reading, so the first device list is the lane probe: until it answers, only that one read-only request is allowed and every write stays closed (`devicesWritesAllowed`). An unarmed verb and a host that reports `false` are the same face with the same sentence. The five seat verbs return the package emitters' outcome shapes, and the outbound gates check every arm as a closed shape (a missing required key, a bad value, an extra key or an unknown arm is refused, naming the path); the one-time enrollment token passes only at the top of the successful enrollment-token outcome. The guard also compares the direct-lane answers with the browser client's previous judgement cell by cell, walks a 140-cell seat matrix against an independent specification, and replays the cold-start sequence. The seat's credential-shaped key-name judge is exported from the package root and pinned to be the very one the seat's second guard uses (one declaration, one internal call site, one word list); its fixed list of judged names and near-misses such as metering keys is checked, and so is the absence of a judge of the same kind at the settings-schema package root. |
554
- | `scripts/run-seat-engine-target-test.mjs` | The seat's engine target (local or Cloud). The engine target has one state, owned by the host and read on the seat through the store `engineTargetStateStore`; six optional verbs switch the mode, run the registry device sign-in, connect or configure a Cloud engine, and clear it. Credentials never cross the seat: the state and every verb outcome are closed shapes at every level (an extra key is refused, naming its path), credential-shaped key names are refused as a second line, and the one inbound credential is the pasted engine token on the configure request, which the inbound judge checks for shape only and never echoes. Failures carry words, not sentences: `engineTargetRefusalText` and `cloudLoginFailureText` render them, and `cloudLoginFailureOf` reads a sign-in failure from the error's own data fields only (the server's error description is never shown; a client-side deadline counts as an expired code). "The engine changed" has one signal, the state's `generation`: `watchEngineTarget` reports each push with a `switched` flag and an `isCurrent(generation)` check, so a refetch started before a switch is dropped when it returns. `seatMcpReadingOf` turns the package's MCP reading into the seat view (an unreadable reading is never shown as an empty list). The guard covers all of this, including the minimal counterexamples for leaked credential keys and the three restart sequences. The refusal list also carries `cloud_unreachable` (a Cloud engine is configured but cannot be reached right now); its sentence never mentions saving, since a new session has no save step. |
554
+ | `scripts/run-seat-engine-target-test.mjs` | The seat's engine target (local or Cloud). The engine target has one state, owned by the host and read on the seat through the store `engineTargetStateStore`; six optional verbs switch the mode, run the registry device sign-in, connect or configure a Cloud engine, and clear it. Credentials never cross the seat: the state and every verb outcome are closed shapes at every level (an extra key is refused, naming its path), credential-shaped key names are refused as a second line, and the one inbound credential is the pasted engine token on the configure request, which the inbound judge checks for shape only and never echoes. Failures carry words, not sentences: `engineTargetRefusalText` and `cloudLoginFailureText` render them, and `cloudLoginFailureOf` reads a sign-in failure from the error's own data fields only (the server's error description is never shown; a client-side deadline counts as an expired code). "The engine changed" has one signal, the state's `generation`: `watchEngineTarget` reports each push with a `switched` flag and an `isCurrent(generation)` check, so a refetch started before a switch is dropped when it returns. `seatMcpReadingOf` turns the package's MCP reading into the seat view (an unreadable reading is never shown as an empty list). The guard covers all of this, including the minimal counterexamples for leaked credential keys and the three restart sequences. The refusal list also carries `cloud_unreachable` (a Cloud engine is configured but cannot be reached right now); its sentence never mentions saving, since a new session has no save step. The local half carries the local engine's lifecycle reading (six phases: waiting with a reason, attached, pending with an optional replace-engine prompt, swapping, down with a reason, behind with whether the host offers a self-upgrade); since 0.110.0 the reading is required, and a local half without it is refused with `missing key` naming `local.lifecycle`. The last boot error is derived from the reading and appears exactly in the down phase, and the local MCP reading is withdrawn whenever the engine is not usable. Two more verbs in the same group answer the replace-engine prompt and start a self-upgrade; their outcomes are closed sets of words. Every phase, notice and non-interactive failure has a sentence in both languages. |
555
555
  | `scripts/run-onboard-judgement-test.mjs` | Adding a model, judged in one place. The decisions for onboarding a model live only in this package: the probe verdict (`probeModelEndpoint` — the verdict request is a streamed one-token request and `ok` is earned only by the model's first token within the deadline; a provider turning requests away is `busy`, headers without a token is `no_token`, a 2xx reply that is not a model answer is `shape`; the model list only feeds a similar-id hint; the key goes only to its own base URL and redirects are never followed), whether to write (`onboardWriteVerdict` — refused when the endpoint proved the setup wrong, and written as unverified only on explicit request when no verdict could be had), the write plan (`onboardWritePlan` — entry name, the entry's own key slot, update in place or create, the default model, and the other catalog entries' problems, with the engine's route-pairing rule through the core value ports), and the two naming algorithms. Writing files, keys, locks and restarts stay with the host. The seat gets three optional verbs under one capability `onboard`, with closed outcome shapes; the API key travels only inbound, at the top of the request, and is never echoed. The guard drives the probe with a fake fetch over both lanes, checks key redaction in five echo forms, the write rule, the write plan cell by cell, the naming algorithms against a frozen reference, and the seat shapes. |
556
556
  | `scripts/run-session-sync-file-history-test.mjs` | The user-facing facts for a session sync that moved the conversation but not its file history. One sentence says it (only the conversation was synced, why, what it means for restoring files with /rewind, and that syncing again will not add it), and a reader decides whether it is said at all: only when the sync result's own file-history outcome is "skipped", and not when the source manifest states outright that the file-history envelope is empty (no tracked files and no rewind boundaries); anything unreadable is said rather than swallowed. A second reader gives the occurrence identity (the session and the source's last entry) so a client can avoid repeating the sentence when nothing new was synced, and a judge tells the 404 for a missing file-history blob (status 404 and exactly the code not_found.blob) apart from every other 404. The guard pins the sentence byte for byte, walks each outcome of the real SDK push, pull and import calls against fake peers, and checks that hostile input (accessors, revoked proxies, inherited properties) never throws. |
557
557
  | `scripts/run-file-history-tighten-test.mjs` | What a client does when the user has turned file checkpointing off but the engine's file-history capture capability cannot be judged. A four-way verdict reads the capability the same way the request fragment does: no restriction requested, declare the per-run opt-out (the engine says it can be switched off), already off (the engine does not capture), or unjudged (not observed, not reported, a word this client does not know, or a malformed reading), and only the last refuses the send, with a sentence chosen by cause (not read yet, so try again in a moment; read but not readable, or not reported, so trying again will not change it) that says nothing was sent, why, and the way out. A single gate answers before a submission: anything judged costs nothing, while an unjudged reading triggers exactly one single-flight re-probe of the engine's capabilities (a body that already landed but is unreadable for this one field is fixed in place; a failed or off-generation re-probe leaves the previous reading untouched for every other reader) and the verdict is taken again. The guard pins the verdict table, the three refusal sentences, the one-probe and single-flight counts against a real capability cache, and the failure and ordering cases. |
558
558
  | `scripts/run-plan-review-outcome-verdict-test.mjs` | The plan-review decision outcome has one reading rule: each outcome word is given only on positive evidence, otherwise `unconfirmed`. A decision reply that says the review is still pending (`retriable: true`) reads `still_parked` whatever the follow-up status read says; `completed` reads `took_effect`, and so does `running` after a rejection (the rejected plan goes back to planning, so the resumed task is expected to be running; the text then says `status`, not `final status`); `suspended` reads `advanced` only when the follow-up read shows the new pause and that pause has a decision card (an approval or a plan review); `needs_review` reads `still_parked` when the review's instance identity before and after the decision can both be read and match, `advanced` when they differ and the new pause has a decision card, and `unconfirmed` otherwise; a malformed status on the follow-up read falls back to the decision reply like a failed read; every other status (`failed`, `blocked`, `timeout`, `running` after an approval, unknown words) reads `unconfirmed` and the sentence states the reading instead of claiming completion. `decidePlanReview` takes an optional `parkedResult` (the paused result the card answers) to supply the before-identity. Pinned with a structural injected client: failed approvals and rejections, the still-pending reply with and without a follow-up read, changed / same / missing / malformed identity, completed with and without the follow-up read, the other status words, the callback and the queue item carrying the same outcome word, and the new option on the shipped declaration. |
559
559
  | `scripts/run-engine-caps-landing-projection-test.mjs` | How a client reads the whole capabilities answer of an engine, and how it swaps the probe used for later re-reads. The whole-answer reader has three states: landed (carrying the frozen snapshot that every narrow capability reader and every per-key reader also read), not landed (no probe yet, still in flight, failed, or invalidated), and unreadable (the probe returned a body whose root cannot be read). A key the engine did not report is unknown and never counts as on: the per-key readers are pinned to a written rule applied to the whole answer, and every narrow reader gives its own not-observed reading on an unreadable root. The same JSON parsed in another realm reads exactly like one parsed here: landing and the snapshot share one plain-record rule, and class instances stay non-records in both realms. A rotation entry for the same engine with a new probe (for example a client rebuilt after a credential refresh) starts that probe when nothing is landed or in flight, and otherwise only replaces the probe used by later single-flight re-reads without invalidating anything; the guard pins that the next re-read calls the new probe and never the old one, together with the in-flight, re-entrant and invalidation orderings. It also pins the root-level nested-value reader for group capabilities (a group is either off or an object of flags), the change-subscription entry (one beat exactly when the whole-answer reader starts returning a different record, per base address, callbacks isolated per subscriber, no replay, idempotent unsubscribe, a seeded random operation sequence proving no extra and no missing beats), and the invalidate-and-reprobe entry that reuses the registered probe. |
560
560
  | `scripts/run-durable-decide-leg-test.mjs` | Which durable decision leg a pending row belongs to has one table and one reading. The approval-card gate kinds are `human` and `irreversible_ask` (the only kinds the approval decide route accepts), the plan-review gate kind is `plan_review`, and the review wording set is `plan_review` and `needs_review`; each table is pinned at compile time in both directions against the engine's pause registry and checked again at run time against the installed engine. `durableDecideLegOf(row)` answers one of five words: `approval`, `plan_review`, `other_gate` (a known gate neither decide route accepts), `unreadable` (a gate word outside the registry) and `absent` (no readable gate word on the row: missing, null, empty, not a string, or a throwing read); it never throws. `askParkForeignGateKind` derives from it. Pinned with the twenty-shape verdict matrix, the derived foreign-kind answer, the row classifier, the pending-row lookup (including refusing to fall back to another row when the row a park names is not an approval), the busy-session recovery on non-gate words, the five-word union on the shipped declaration, and no engine-package reference in the shipped output. Since 0.97.0 the same table also decides which approval arm a parked frame goes to: `isToolApprovalGate` agrees cell by cell with `durableDecideLegOf` (an approval gate kind takes any tool that is not the question tool, an unreadable kind falls back to the file-write and shell name families, any other kind takes neither arm); a real-wire park on an approval gate kind for a tool outside those families raises the approval card and sends the decision end to end, while the non-gate word `tool_approval`, a plan-review kind with a write tool, and a non-gate word with the question tool raise nothing. |
561
- | `scripts/run-rule-tool-name-judge-test.mjs` | One reading for "does the engine accept this rule tool name": the engine's own tool-name check, installed as a value port and read through `ruleToolNameVerdictOf` (current / retired with its current name / unprefixed namespace / unmintable segment / unknown). The session-rule deliverability check asks it once for every name that survives the plain-text steps (an ordinary name the installed engine has newly retired is withheld as `legacy_tool_name`; a check that cannot answer makes the batch unreadable), the refused-entry removal removes every entry it does not call current and answers `judge_failed` without writing when it cannot answer, the sub-agent tool-list pre-check reports retired, unprefixed and unmintable names from it, reports an entry it cannot check as unjudged (sent as written with a warning, like any other deny-list problem) and judges case twins only against a supplied roster, and the request-body assembly rewrites a retired name to the current name it gives only in the deny and ask buckets (allow keeps it). No build-time retired-name table remains in the source. A real engine run that mounts no tool of that name, reading a stored session record, refuses to start exactly for the names this reading does not call current in the deny and ask buckets; in the allow bucket, from engine 7.60.0, a retired name no longer refuses the start: the run starts and reports it in exactly one unmatched-names advisory that names it, while an unprefixed or unmintable name there still refuses. |
561
+ | `scripts/run-rule-tool-name-judge-test.mjs` | One reading for "does the engine accept this rule tool name": the engine's own tool-name check, installed as a value port and read through `ruleToolNameVerdictOf` (current / retired with its current name / unprefixed namespace / unmintable segment / unknown). The session-rule deliverability check asks it once for every name that survives the plain-text steps (an ordinary name the installed engine has newly retired is withheld as `legacy_tool_name`; a check that cannot answer makes the batch unreadable), the refused-entry removal removes every entry it does not call current and answers `judge_failed` without writing when it cannot answer, the sub-agent tool-list pre-check reports retired, unprefixed and unmintable names from it, reports an entry it cannot check as unjudged (sent as written with a warning, like any other deny-list problem) and judges case twins only against a supplied roster, and the request-body assembly rewrites a retired name to the current name it gives only in the deny and ask buckets (allow keeps it). No build-time retired-name table remains in the source. A real engine run that mounts no tool of that name, reading a stored session record, refuses to start exactly for the names this reading does not call current in the deny and ask buckets; in the allow bucket, from engine 7.60.0, a retired name no longer refuses the start: the run starts and reports it in exactly one unmatched-names advisory that names it, while an unprefixed or unmintable name there still refuses. In the deny and ask buckets, from engine 7.65.0, a retired name with no successor no longer refuses the start either: the run starts and one `config.legacy_tool_name` notice names it, while a retired name that has a successor still refuses. |
562
562
  | `scripts/run-tool-card-outcome-test.mjs` | One reading, one sentence, for what a tool result card in the transcript actually says. Three marks decide it: the placeholder mark on the record (the engine sent no body, so the content is an empty placeholder), the engine's positive evidence that the call never ran, and the block's error flag. `readToolCardOutcome(record)` reads them as `normal` (a real result; success or failure still comes from the error flag and is never rewritten), `degraded` with a cause and one sentence (`not-executed` — takes precedence and is independent of the error flag, so a card that looks successful but never ran is caught; `error-without-detail`; `no-result`, where the error flag false is a placeholder and not a success; `unrecognised`, a placeholder value from another version) or `unreadable` (not a tool result record, or the placeholder position cannot be read — never read as normal). The placeholder has two minting points that cannot be told apart on the record (a call closed at the end of the turn with no end frame, and a real end frame without a body), so the no-result sentence states only what both share. Pinned on records produced by the real pipeline in both lanes (the transcript row and the print-lane frame answer the same), a read-failure matrix that never throws and never reads as normal (accessors are not run, prototype-chain keys do not count), four distinct sentences with no internal names or tool-family names, and the source (the sentence table is typed on the cause union, the evidence mark goes through the existing strict-true reader, the placeholder constant is imported not re-written, and each sentence has exactly one minting site in the shipped output). |
563
563
  | `scripts/run-sentence-tables-projection-test.mjs` | The sentences a person reads for the same fact now have one source, shaped as closed key tables checked against the upstream value sets. The checks cover: persistent-rule origin, scope, match form and revoke outcome (the status by liveness grid, with an unknown-status third outcome that never says whether anything was removed, and liveness words other than the three read as unknown); the card-line readers (the origin line with its quiet table, the reason a card has no remember option, the reason a rule offer still asks); crashed-run leftovers (per-row clauses sharing one table with the counted sentences, whose output is frozen byte for byte, and a not-started row never described as partly landed); the one-line ending of an approval the engine closed without an answer (fourteen causes, never described as a refusal), the accepted-decision line that only claims delivery, and the parked-card note shared by the replacement and reopened cards. A parked frame is never described as an ending; a crashed-run row sentence takes the projection and the row and trusts only the projection's bucket. Every table is a literal that carries its own `satisfies Record<…>` clause; a word outside a table, an absent word or a non-string yields a neutral sentence or `undefined` and never echoes the word; readers do not run accessors or throw on proxies. It also pins that no local copy of the server's rule-count cap is minted and that the permission-mode default half of the `policy` origin sentence is present. |
564
564
  | `scripts/run-engine-url-transport-test.mjs` | Engine address handling in one place. `normalizeEngineBaseUrl` trims outer whitespace, strips every trailing slash and keeps the user's spelling; it refuses, with one of five closed reasons (`empty`, `scheme`, `unparseable`, `userinfo`, `query_or_fragment`), anything that is not a plain http(s) base address. A query or fragment is refused even when empty, because request paths are appended to the base as plain text and would end up inside the query. `engineUrlTransportVerdict` lets `https:` through and `http:` only for a host the settings contract's narrow local-host check accepts, the same check that decides whether outbound credentials are dropped, so the credential fence and the plaintext gate cannot disagree about one host; anything else is refused as plaintext to a remote host or as not an http(s) address, and an address it cannot read is refused. Neither function returns message text or echoes the input. The gate checks every refusal reason and success form, the two addresses the terminal and the desktop shell used to answer in opposite ways, that the local-host reading has a single call site over the settings contract's own function and never touches the wider routing check, that the verdict matches the installed settings contract on a host table, that hostile input never throws and a very long run of slashes stays linear, and it compares the package with copies of both shells' former logic on a generated corpus: the package accepts exactly what both former implementations accepted, with the only disagreements between them falling into two registered classes. The cloud configuration's session-mirror address is read through the same normaliser: well-formed addresses come out byte-identical to the former trailing-slash trim, an address the normaliser refuses is passed on as read with its `required` flag intact rather than dropping the mirror, and a long run of slashes stays linear. |
@@ -596,7 +596,7 @@ guard still cross-checks the table by name).
596
596
  | `scripts/run-hitl-decision-send-test.mjs` | One sender for a human decision across three answer endpoints: the live tool approval, the durable in-stream ask and a form prompt from an MCP server (`sendHitlDecision`). The body is built once (verdict, edited input, the deny note behind its capability and length gates, the attribution key chosen by capability, and for the durable ask an idempotency key that is the same for the same ask and verdict and differs across asks); the same body goes through the attribution resend ladder (a retired key is renamed once, a direct-door refusal drops the key once, at most three sends, all before the decision is judged) and a bounded transport retry (the first send plus three, within eight seconds, only for failures without a server verdict and for 5xx other than 501); a durable ask that is mid-park is re-sent at most twice to learn where it landed; only the ask-ledger-disabled code falls back, once, to the live endpoint when an approval id is known; a live 202 parking answer is re-sent exactly once, with the same body, after the interval the server names (only when that interval is within five seconds and the budget allows; a second parking answer is reported as parking and a later not-pending answer is classified on its own). The live card path calls the same delivery segment, so both share one attribution ladder, one bounded transport retry and one parking retry. The outcome is always one of five frozen arms with closed cause sets, and a second send to the same gate while the first is in flight sends nothing. Each case is checked on timing (the outcome is returned after the ledger entry settles; an abort during backoff stops further sends), concurrency (same-gate latch across families, independent gates, a conflicting first decision is reported as such and does not mark the gate as still pending) and every failure class, including malformed form answers that send nothing; the package's two existing decision paths and the sender share one attribution ladder. The machine code of a failure is read once: retries, fallbacks, the attribution ladder and the attached refusal reading all use that single reading, and function-shaped errors read the same as plain objects. A session-wide allow answered on a card goes out only when the card was fully readable and its route can carry it; otherwise both card legs and the decision sender allow once and the sender reports the dropped remember, including on the lane-closed fallback. |
597
597
  | `scripts/run-mcp-tool-toggles-test.mjs` | Per-tool switches for connected MCP servers. A switched-off tool is keyed as `{ server, tool }`, where `tool` is the engine's full tool name exactly as the session's MCP panel lists it, so a key is built straight from a panel row and nobody parses or rebuilds tool names. `checkMcpToolToggleKey` is the only shape check for a key, and `mcpToolTogglesToExcludeTools(keys, mountedServers)` is the only mapping from keys to the request's `excludeTools`: only servers mounted this turn produce entries, a host that cannot tell what is mounted passes `'unknown'` so every switched-off tool is excluded, each key is read once and the value that was checked is the value that is used, and no engine port is needed. The seat gains `listMcpToolToggles` / `setMcpToolToggle` behind the `mcpToolToggles` capability; the host keeps the switches and adds the mapped names to `excludeTools` on every turn, and the renderer never carries an exclusion list on `sendMessage`. |
598
598
  | `scripts/run-host-turn-settings-intake-test.mjs` | The host-settings assembly entry takes raw material and judges it itself. `memory` and `fileHistory` are host-supplied keys like the four settings sub-keys: the assembly input no longer has them (a hand-filled one is refused by name), the entry reads the `--memory` flag word and the settings `memory` section through `memoryOffDeclared`, and turns the file-checkpoint switch plus the engine's capability reading into a row through `fileHistoryTightenVerdict` (an undecidable reading with the switch off means the turn is not sent, with the same sentence as `fileHistoryUnjudgedRefusal`). The web search source is the raw settings section plus the environment, judged by `resolveWebSearchVerdict` inside the entry (a malformed configuration refuses the turn with the existing sentence). `refilterHostSettings(request, fresh, lane)` is the one subtract-only recheck between assembly and dispatch: hooks are intersected with the fresh product, nothing is added, other host keys are left alone, and a fresh refusal stops the send. Every host-supplied key on the request the assembly builds comes from a product row that carries it, and the assembly reads the product once from its own data (the permission snapshot first, turned into its request form at that read; every other value copied once in its serialized form) (an inherited or getter-backed top-level value is refused, never used; for a getter nested inside a value, what it returned when the copy was taken is what is checked and sent) and uses only that read for the permission snapshot, the carried values and the refusal; the live-defaults layer can still put an output style into `settings.outputStyle`, which is a later step. |
599
- | `scripts/run-text-segment-authority-test.mjs` | The **authoritative segment replacement** on `text_end` (server >=7.75.3). `text_end.content` now goes through the same redactor as `result` and the ledger while `text_delta` stays verbatim, so the two **may differ** — an answer that quoted a credential used to be committed to the local transcript in its unredacted form, because the arm only forwarded the boundary signal. Six timing shapes are pinned, two of which an adversarial review reproduced against the installed engine's real bytes and which the first design got wrong in both directions: a second boundary in the same turn (the per-block case on one provider lane) used to make the first segment's prose vanish, and a boundary that arrives *after* the tool card (the other lane emits it at finalize) used to be read as "this package never handled that segment" and reported nothing at all. Three additive keys, all never-false; the two shapes that look alike are told apart by the second one, because the host's action in them is the opposite. The end-to-end legs drive the real pipeline without hand-inserting a segment commit — doing so is exactly what hid the first defect. A second review round then found two combination timings on top of the first fix — a tool card followed by *more* deltas in the same segment, and a byte count that had been documented as a message count — and both are pinned here too. A third round caught a length that the prose called bytes while the code returned UTF-16 units — harmless in ASCII, and on CJK text enough to leave the credential on screen — plus a backfill ledger that had to be kept in step, so the terminal frame does not re-render the segment a second time — kept in step only where the whole stretch sits in one message, because those ledgers are per-message and a fourth round showed that writing across them charges one message's prose to another. A fifth round settled the whole class into one invariant the guard now checks against the previous release's behaviour: this package only rewrites bytes it is still holding in the current message — once a segment has crossed a package-side boundary it emits the three keys and changes nothing else **0.68.2 (CC-01):** the segment identity is now minted here, not by the host: every committed assistant text row carries a top-level `_sema_segment_id` (stamped once at the `adapt()` exit, so the durable whole-message leg and the streamed-segment leg are covered alike; thinking blocks, tool_use-tailed rows and chrome events are left byte-for-byte), `text_segment_end` carries the same value as `segmentId` before rotating, subagent boundaries never rotate, and a replayed stream yields the same identities. Three mutations (no rotation / no stamping / stamping tool_use rows) each turn the guard red **0.69.0 (CC-02):** the same authority replacement now covers the reasoning face (`reasoning_end`, server >=7.77.0): a thinking block still buffered is swapped whole and its live tail recomputed; one already committed at a boundary (the usual timing, since the first text delta commits it) is left untouched and the host is told the row to replace by its uuid, never re-emitted. Subagent boundaries are ignored and the text-segment identity does not rotate **0.69.1 (CC-09):** the run-stream replay guard still drops a frame whose event id was already seen, but it now reports the drop through the host's dropped-frame sink as `duplicate_seq` instead of vanishing silently (server 7.77.0 reuses the first reasoning delta's id for `reasoning_end`, so that authoritative segment is lost on the print lane until 7.78.1); the interactive adapter has no such guard and keeps receiving it **0.69.1 (CC-10/CC-11):** subagent segment-end frames are fenced on all three identity keys (a frame carrying only `sourceTaskId` no longer masquerades as the leader's), and a reasoning segment that spans tool cards now hands the host every committed row it covers (`committedUuids`) so nothing unredacted is left behind. **0.96.0 (CC-645):** the tail of a segment that the authoritative text had not yet streamed is now emitted *before* the segment-end event, so nothing for the same segment follows it; a host that replays the sequence (append deltas, replace with `content` when `diverged`) shows the segment exactly once, on both the text and the reasoning channel **0.100.0:** one committed transcript row now holds exactly one engine segment. The segment's remaining bytes are committed as its own row right after its segment-end event (no more carrying a finished segment into the next segment's row), every row of a segment carries the same `_sema_segment_id` as that segment's end event, the identity is derived from the segment's first text frame (so a projection that joins at the next segment computes the same value), and a model-round end starts a new identity. A segment that genuinely crosses a tool card keeps one identity across its rows. |
599
+ | `scripts/run-text-segment-authority-test.mjs` | The **authoritative segment replacement** on `text_end` (server >=7.75.3). `text_end.content` now goes through the same redactor as `result` and the ledger while `text_delta` stays verbatim, so the two **may differ** — an answer that quoted a credential used to be committed to the local transcript in its unredacted form, because the arm only forwarded the boundary signal. Six timing shapes are pinned, two of which an adversarial review reproduced against the installed engine's real bytes and which the first design got wrong in both directions: a second boundary in the same turn (the per-block case on one provider lane) used to make the first segment's prose vanish, and a boundary that arrives *after* the tool card (the other lane emits it at finalize) used to be read as "this package never handled that segment" and reported nothing at all. Three additive keys, all never-false; the two shapes that look alike are told apart by the second one, because the host's action in them is the opposite. The end-to-end legs drive the real pipeline without hand-inserting a segment commit — doing so is exactly what hid the first defect. A second review round then found two combination timings on top of the first fix — a tool card followed by *more* deltas in the same segment, and a byte count that had been documented as a message count — and both are pinned here too. A third round caught a length that the prose called bytes while the code returned UTF-16 units — harmless in ASCII, and on CJK text enough to leave the credential on screen — plus a backfill ledger that had to be kept in step, so the terminal frame does not re-render the segment a second time — kept in step only where the whole stretch sits in one message, because those ledgers are per-message and a fourth round showed that writing across them charges one message's prose to another. A fifth round settled the whole class into one invariant the guard now checks against the previous release's behaviour: this package only rewrites bytes it is still holding in the current message — once a segment has crossed a package-side boundary it emits the three keys and changes nothing else **0.68.2 (CC-01):** the segment identity is now minted here, not by the host: every committed assistant text row carries a top-level `_sema_segment_id` (stamped once at the `adapt()` exit, so the durable whole-message leg and the streamed-segment leg are covered alike; thinking blocks, tool_use-tailed rows and chrome events are left byte-for-byte), `text_segment_end` carries the same value as `segmentId` before rotating, subagent boundaries never rotate, and a replayed stream yields the same identities. Three mutations (no rotation / no stamping / stamping tool_use rows) each turn the guard red **0.69.0 (CC-02):** the same authority replacement now covers the reasoning face (`reasoning_end`, server >=7.77.0): a thinking block still buffered is swapped whole and its live tail recomputed; one already committed at a boundary (the usual timing, since the first text delta commits it) is left untouched and the host is told the row to replace by its uuid, never re-emitted. Subagent boundaries are ignored and the text-segment identity does not rotate **0.69.1 (CC-09):** the run-stream replay guard still drops a frame whose event id was already seen, but it now reports the drop through the host's dropped-frame sink as `duplicate_seq` instead of vanishing silently (server 7.77.0 reuses the first reasoning delta's id for `reasoning_end`, so that authoritative segment is lost on the print lane until 7.78.1); the interactive adapter has no such guard and keeps receiving it **0.69.1 (CC-10/CC-11):** subagent segment-end frames are fenced on all three identity keys (a frame carrying only `sourceTaskId` no longer masquerades as the leader's), and a reasoning segment that spans tool cards now hands the host every committed row it covers (`committedUuids`) so nothing unredacted is left behind. **0.96.0 (CC-645):** the tail of a segment that the authoritative text had not yet streamed is now emitted *before* the segment-end event, so nothing for the same segment follows it; a host that replays the sequence (append deltas, replace with `content` when `diverged`) shows the segment exactly once, on both the text and the reasoning channel **0.100.0:** one committed transcript row now holds exactly one engine segment. The segment's remaining bytes are committed as its own row right after its segment-end event (no more carrying a finished segment into the next segment's row), every row of a segment carries the same `_sema_segment_id` as that segment's end event, the identity is derived from the segment's first text frame (so a projection that joins at the next segment computes the same value), and a model-round end starts a new identity. A segment that genuinely crosses a tool card keeps one identity across its rows. A text block that the engine delivers only as its end frame, with no per-chunk frames before it, is read as if one chunk equal to its content had arrived just before the end frame: it lands in the transcript as one row that carries the same segment identity as the segment end event (one live chunk before the event, then the event with an empty list of earlier rows, then the row), exactly like a block delivered chunk by chunk. A block that was already delivered as a whole-block row is never added a second time, including when several end frames follow one whole-block row that merged several blocks; the marker resets at the next tool card, model-round end, compaction boundary or end of stream, so an end-frame-only block after a tool card or after a compaction boundary is a new block. |
600
600
  | `scripts/run-terminal-owner-projection-test.mjs` | Terminal ownership on the busy-reject path. When a submission is refused because another run holds the session, the synthetic terminal row and the result frame now carry the same machine-readable owner: `request-not-sent` (with the host's own request id echoed back and the holding run's id), and a stream the host declares as following another run marks every terminal it produces as `followed-run` (with that run's id). Hosts can therefore tell "my message was not sent" from "the run I am watching finished" without guessing from order. Absence keeps the previous meaning (this terminal belongs to the submission's own run). The reader accepts only the closed set, reads own data properties only and never throws. |
601
601
  | `scripts/run-row-kind-projection-test.mjs` | Row kind on synthetic terminal rows. Every synthetic terminal row the package mints (busy-session reject, governance stop, model-output error, any other terminal error, outcome unknown) now carries a top-level `_sema_row_kind` stamped at the mint site, drawn from a closed set of six words that each name an owner, so consumers read the kind instead of parsing the sentence prefix. Rows for provider errors also carry the CC `error` classification, but only when the provider status falls in the package's single status table (a 529 is `server_error`, not `overloaded`; an out-of-table or missing status leaves the key absent). Three readers (`rowKindOf`, `isSyntheticRow`, `inferLegacyRowKind`) read own data properties only, never throw and never fall back to the text; legacy rows are inferred once, through the one prefix table, and only on a positive match. `classifySubmitFailure` projects a thrown submit or stream failure onto the same kind, code and classification. The gate drives the real stream and adapter and pins the single mint site. |
602
602
  | `scripts/run-selfheal-exit-slot-test.mjs` | Exit slots for a held session. Every sentence in the busy-session family that tells the user how to end the run holding the session now reads one host slot (`endRun`), and the parked-for-wake sentence reads a second one (`wakeSession`, `null` to omit), so a host with its own command no longer has to override whole rows to remove the engine's HTTP path; without a slot the sentences name the engine endpoint as before. A refused steer now carries the engine's error code, and when that code says the run is no longer running the package reads the run once more and, if it has finished, says so in a sentence that agrees with the earlier busy reading instead of quoting the refusal verbatim. No retry and no cancel are ever issued on that path. |
@@ -606,12 +606,12 @@ guard still cross-checks the table by name).
606
606
  | `scripts/run-injection-lane-notice-test.mjs` | A single pure function that says, in one sentence, when the local MCP servers and hooks sent with a task will actually run somewhere else. It reads the engine's own execution-lane report (the same reading the doctor line uses): only when that report says tools run on a different machine than the engine, and the lane has hands at all, does it produce a sentence, naming the lane and whichever of three injected things are present — local stdio MCP servers, MCP servers that use credentials from this machine, and hooks — in a fixed order regardless of input order. A local lane, a deployment with no execution lane, a missing, unreadable or malformed report, or nothing injected all yield no sentence rather than a guess. The lane word is shown escaped and capped, the sentence exists in both catalog languages, and the source is pinned not to hand-copy lane names |
607
607
  | `scripts/run-rule-refusal-sentence-test.mjs` | The single place that words a refused-rule notice. Given the rows of rules that were not sent (a rule plus the reason it was dead: its tool name is not one the engine recognizes, it uses a path starting with the home marker that cannot be anchored, or the engine's rule compiler refuses it, in which case the engine's own code and sentence are used as is), it returns one sentence that names up to five rules each with its own reason and reports how many more there were; the leading count is the number of distinct dead rules, a rule listed twice is named once, and rows whose reason has its own separate notice (empty, duplicate, capability off, and so on) are not part of it. Rule text is shown escaped and capped and is never echoed inside a reason; malformed input yields no sentence rather than a throw, and the sentence exists in both catalog languages. Its request-face companion reads the server's refusal body for a request whose rules include a form the engine does not support (the unprocessable-request refusal with a per-entry problem list, or the blocked-resume form of it for work that was parked) and words one sentence that names the offending rules, up to five, escaped and capped, each with the client's own reason sentence; entries whose reason is not one the client knows are never claimed |
608
608
  | `scripts/run-result-route-projection-test.mjs` | The route a refused request took, carried on the error envelope of a run result frame under a superset key. When a run ends because the model provider rejected the request with a 4xx and the terminal detail names the route (which credential source, optionally the carrier and bag, and the root), the result frame carries those words as a fresh object; extra fields an engine might add are never copied, unknown words pass through as written, and nothing is carried when the detail, the route or its required parts are missing or malformed, so absence never reads as having taken no route. Everything else on the frame — subtype, error flag, error list, error code, terminal reason — is unchanged whether or not the route is present, and only a provider-rejected terminal ever carries it. The key has a single minting site and a row in the superset ledger |
609
- | `scripts/run-additive-key-passthrough-test.mjs` | The one disease shape behind two legs: a **closed whitelist / flattening arm** dropping a fact that is already on the wire, while both sides of the seam look correct. (1) The `task_progress` projection carries a registered **key ledger** — a frame populated with every key the service really projects is pushed through the shipped `eventToSdkMessage`, and the set of wire keys that survive must equal the registered pass-through list **name for name in both directions**, so quietly forwarding one more key is as red as quietly dropping one. `model` (the child run's model id, minted by core as `prepared.model.id` and projected by the server since 7.52.1) is the key this batch adds, with the same conditional the server itself applies: a non-empty string or no key at all — an empty string is neither a model id nor "unknown". The ledger is also checked against the fenced list in `docs/INTEGRATION-CLIENTS.md` §3d, so a doc that still says seven keys while the code forwards eight is red rather than merely stale. (2) The decide-failure arms carry the server's S-02 `currentPending` pointer key from a 409 `approval_stale` refusal onto the outcome the host reads. The reader is structural rather than `instanceof`, because the client is host-injected and the class identity is not this package's to assume; a half triple never mints (half a pointer cannot relocate anything), an empty string is not presence, and `checkpointToken` never transits. Both the allow and the deny leg are driven end to end through the real durable approval path — as is the accept-session leg, where a refusal carrying the pointer key must now re-raise instead of silently re-sending the human's answer for the **old** card as a plain approve (one decide call, pointer preserved), while a legacy 400 still falls back exactly as before — and all three flattening points must call the one shared reader — the same-shape residue check that makes "fixed one arm and left the twin" red instead of invisible. (3) The same disease growing on the REQUEST side: the `.mcp.json` → server-spec projection rebuilds each server key by key, and the settings schema deliberately leaves some keys parse-transparent — whatever JSON the file carries reaches the engine untouched, because validating them where the whole domain parses all-or-nothing would let one bad declaration take every server down silently. The whitelist had no row for the newest of them, so an operator's per-tool declarations — the ones the write fence reads — were stripped at the package boundary while both sides looked correct. The criterion is not "is that key handled" but the transparent-key table read out of the INSTALLED schema at runtime, reconciled name-for-name against this leg's ledger, so the day upstream adds a third one this turns red and forces an explicit decision. Behaviour is pinned on both transports, by object identity rather than deep equality (a rebuild would be a second judge), and malformed values must transit UNCHANGED rather than be refused here — the engine refuses them loudly and names the server, whereas a package-side judge can only swallow a declared protection quietly. Absence still mints no key, unknown keys still never reach the wire (the fix is the dropped key, not the gate), and the one transparent key this leg deliberately does not forward is a ledger entry with its own exit condition: it belongs to the deployment plane, and the day the request-plane type declares it the entry's premise is gone and the gate says so The prose probe reason on approval asks (`probeReason`: live frames from server 7.115.0, and the durable pending row's risk descriptor) lands on the same card field from both legs, byte for byte, never as an empty string, read from own keys only. A display field that throws when read (a getter or a proxy trap) is treated as absent: the card is still shown and the decision is still sent. The remember-refused flag (`rememberRefused`, the server saying an allow-for-this-session answer will not be remembered on this card) reaches the same card field from the live frame and from the pending-list row, only as an own strictly-true key; absent, malformed, inherited or throwing values leave it off without blocking the card. The single reader of the card’s remember-refused declaration is a root export — an own key holding exactly `true`, never throwing on hostile input, agreeing with both card legs over the same pool — and no private copy remains. Since 0.96.0 the retry-status leg also carries the provider condition word and its evidence source verbatim to the top of the retry status; no quota reading is derived from them. Since 0.96.0 the configuration-key ledger also accounts for the three transparent connector keys of the 12.2 settings contract: none of them is forwarded on either leg, two because the request shape has no such key, and the per-tool axis override because its meaning on a centrally configured entry is not yet defined by its consumer; each entry carries the condition that would end it. Every withheld key is probed on both transports and with nine value shapes on both legs, the request shape is read with the type checker, and the per-tool axis override's end condition is read by running the current server package's own central projection. Since 0.100.0 the check that an explicitly empty tool allow-list reaches the engine as written is read per server generation (the older generation's accept-list constant and the newer generation's reader assignment are each recognised only in their own form) and its behaviour is run for real on both generations: the server's own request reader keeps the empty list, also when the package's output is fed to it, and the same generation's engine, mounting a stub tool server, mounts both tools when the list is absent, one when it names one, and none when it is empty. The progress frame now forwards its cycle number. |
609
+ | `scripts/run-additive-key-passthrough-test.mjs` | The one disease shape behind two legs: a **closed whitelist / flattening arm** dropping a fact that is already on the wire, while both sides of the seam look correct. (1) The `task_progress` projection carries a registered **key ledger** — a frame populated with every key the service really projects is pushed through the shipped `eventToSdkMessage`, and the set of wire keys that survive must equal the registered pass-through list **name for name in both directions**, so quietly forwarding one more key is as red as quietly dropping one. `model` (the child run's model id, minted by core as `prepared.model.id` and projected by the server since 7.52.1) is the key this batch adds, with the same conditional the server itself applies: a non-empty string or no key at all — an empty string is neither a model id nor "unknown". The ledger is also checked against the fenced list in `docs/INTEGRATION-CLIENTS.md` §3d, so a doc that still says seven keys while the code forwards eight is red rather than merely stale. (2) The decide-failure arms carry the server's S-02 `currentPending` pointer key from a 409 `approval_stale` refusal onto the outcome the host reads. The reader is structural rather than `instanceof`, because the client is host-injected and the class identity is not this package's to assume; a half triple never mints (half a pointer cannot relocate anything), an empty string is not presence, and `checkpointToken` never transits. Both the allow and the deny leg are driven end to end through the real durable approval path — as is the accept-session leg, where a refusal carrying the pointer key must now re-raise instead of silently re-sending the human's answer for the **old** card as a plain approve (one decide call, pointer preserved), while a legacy 400 still falls back exactly as before — and all three flattening points must call the one shared reader — the same-shape residue check that makes "fixed one arm and left the twin" red instead of invisible. (3) The same disease growing on the REQUEST side: the `.mcp.json` → server-spec projection rebuilds each server key by key, and the settings schema deliberately leaves some keys parse-transparent — whatever JSON the file carries reaches the engine untouched, because validating them where the whole domain parses all-or-nothing would let one bad declaration take every server down silently. The whitelist had no row for the newest of them, so an operator's per-tool declarations — the ones the write fence reads — were stripped at the package boundary while both sides looked correct. The criterion is not "is that key handled" but the transparent-key table read out of the INSTALLED schema at runtime, reconciled name-for-name against this leg's ledger, so the day upstream adds a third one this turns red and forces an explicit decision. Behaviour is pinned on both transports, by object identity rather than deep equality (a rebuild would be a second judge), and malformed values must transit UNCHANGED rather than be refused here — the engine refuses them loudly and names the server, whereas a package-side judge can only swallow a declared protection quietly. Absence still mints no key, unknown keys still never reach the wire (the fix is the dropped key, not the gate), and the one transparent key this leg deliberately does not forward is a ledger entry with its own exit condition: it belongs to the deployment plane, and the day the request-plane type declares it the entry's premise is gone and the gate says so The prose probe reason on approval asks (`probeReason`: live frames from server 7.115.0, and the durable pending row's risk descriptor) lands on the same card field from both legs, byte for byte, never as an empty string, read from own keys only. A display field that throws when read (a getter or a proxy trap) is treated as absent: the card is still shown and the decision is still sent. The remember-refused flag (`rememberRefused`, the server saying an allow-for-this-session answer will not be remembered on this card) reaches the same card field from the live frame and from the pending-list row, only as an own strictly-true key; absent, malformed, inherited or throwing values leave it off without blocking the card. The single reader of the card’s remember-refused declaration is a root export — an own key holding exactly `true`, never throwing on hostile input, agreeing with both card legs over the same pool — and no private copy remains. Since 0.96.0 the retry-status leg also carries the provider condition word and its evidence source verbatim to the top of the retry status; no quota reading is derived from them. Since 0.96.0 the configuration-key ledger also accounts for the three transparent connector keys of the 12.2 settings contract: none of them is forwarded on either leg, two because the request shape has no such key, and the per-tool axis override because its meaning on a centrally configured entry is not yet defined by its consumer; each entry carries the condition that would end it. Every withheld key is probed on both transports and with nine value shapes on both legs, the request shape is read with the type checker, and the per-tool axis override's end condition is read by running the current server package's own central projection. Since 0.100.0 the check that an explicitly empty tool allow-list reaches the engine as written is read per server generation (the older generation's accept-list constant and the newer generation's reader assignment are each recognised only in their own form) and its behaviour is run for real on both generations: the server's own request reader keeps the empty list, also when the package's output is fed to it, and the same generation's engine, mounting a stub tool server, mounts both tools when the list is absent, one when it names one, and none when it is empty. The progress frame now forwards its cycle number. Since 0.110.0 the prose reason-for-asking bit crosses to the seat request through `toolPermissionRequestAskBits` (the old pin that it never crosses is replaced). |
610
610
  | `scripts/run-cloud-mcp-dropped-notice-test.mjs` | The sentences shown to a person when the cloud configuration leaves MCP server entries out. Each of the eleven reasons an entry is left out (switched off, a required setting missing, an unsupported transport, both identity-header settings set, an invalid `toolAxes` declaration, an `elicitation` value that is not a boolean, a name the settings contract's reader rejects, a transport the engine cannot use as written, an invalid `toolFaces` declaration, an `allowTools` that is not a list of tool names, an entry that could not be read at all) has its own sentence that names the entries — the last two name a bad-name summary and a key path — and the switched-off sentence is informational while the other ten are warnings. An entry that stays on the wire without an optional part of its transport adds one warning that names the entry and the key path, never a value, and nothing is left out for exceeding 32 entries any more. The sentences are part of the single wording source `cloudEffectiveNotices`, whose input now also carries the list of left-out entries. A reason outside the known set and an unreadable row (not an object, a reason that is not a string, a throwing getter, a reason or name that only exists on the prototype chain, an index that is an accessor or a hole) get a plain warning that names what can be read instead of being skipped, a list that is not an array or cannot be read (an accessor that is never run, a value of undefined, a proxy that throws) gets one warning saying it could not be read, a missing key adds nothing, and an empty list adds nothing. Names are capped at five with a `+N more` tail, repeated names are shown once, rows without a readable name are counted, and the order is degradation warnings, catalog references, left-out MCP entries (warnings first), budget, runtime caps. Pinned on the shipped declaration (the reason union is exactly the eleven words) and on the source (the sentence table is typed on that union and looks reasons up by own key). |
611
- | `scripts/run-approval-card-face-projection-test.mjs` | Three card positions added in 0.94.0. `boundGrants` (what approving a Skill card also grants: the skill name or null, the entries that then run without asking, and the entries refused for the rest of the run) is read once from the live approval frame through a strict reader; any malformed part drops the whole position, the copy is frozen, the digest is not put on the card, and the durable pending-row path never carries it; `approvalBoundGrantsDetail` gives one sentence that says how many tool entries run without asking and how many are refused, and (from 0.106.0) names each of them — up to six per list, then "and N more"; every name goes through the display sanitizer and is quoted, entries the engine reads as nothing are neither named nor counted, and the skill name is never put in the sentence. `classifierNoRuling` (why the auto-mode classifier gave no ruling) is read by word shape as an open set from the live frame only; `classifierNoRulingDetail` has a phrase for each of the engine's words and a generic sentence otherwise. `priorCardsForCall` lists, for the same tool call, the earlier cards that asked about something else (a different class set, origin or mandate word), each with its approval id, origin, mandate word and, when answered here, whether it was allowed or denied; replays and parked twins of the same question do not count, both card paths share one bounded ledger, and `approvalPriorCardsDetail` says this is a separate check that needs its own answer. Each line of the card's "why is this asking" block is rendered from the package's own sentence; only the values interpolated into a sentence (a limit name, a rule entry, probe text, an engine message, a cause word or code) go through the display sanitizer, so a fixed sentence such as "only its basic facts are shown" is no longer rewritten by the credential filter, while a credential-shaped value in any interpolated slot is still masked and never shown in the clear. |
611
+ | `scripts/run-approval-card-face-projection-test.mjs` | Three card positions added in 0.94.0. `boundGrants` (what approving a Skill card also grants: the skill name or null, the entries that then run without asking, and the entries refused for the rest of the run) is read once from the live approval frame through a strict reader; any malformed part drops the whole position, the copy is frozen, the digest is not put on the card, and the durable pending-row path never carries it; `approvalBoundGrantsDetail` gives one sentence that says how many tool entries run without asking and how many are refused, and (from 0.106.0) names each of them — up to six per list, then "and N more"; every name goes through the display sanitizer and is quoted, entries the engine reads as nothing are neither named nor counted, and the skill name is never put in the sentence. `classifierNoRuling` (why the auto-mode classifier gave no ruling) is read by word shape as an open set from the live frame only; `classifierNoRulingDetail` has a phrase for each of the engine's words and a generic sentence otherwise. `priorCardsForCall` lists, for the same tool call, the earlier cards that asked about something else (a different class set, origin or mandate word), each with its approval id, origin, mandate word and, when answered here, whether it was allowed or denied; replays and parked twins of the same question do not count, both card paths share one bounded ledger, and `approvalPriorCardsDetail` says this is a separate check that needs its own answer. Each line of the card's "why is this asking" block is rendered from the package's own sentence; only the values interpolated into a sentence (a limit name, a rule entry, probe text, an engine message, a cause word or code) go through the display sanitizer, so a fixed sentence such as "only its basic facts are shown" is no longer rewritten by the credential filter, while a credential-shaped value in any interpolated slot is still masked and never shown in the clear. Since 0.110.0, `requestedBy` (which extension registration started the tool call being asked about) is read from the top level of the live approval frame and of the presented-card frame through one strict reader and lands on the card request as a frozen `{ kind: 'extension', name }` copy; malformed or hostile shapes drop the position, while the card body, the generic card, the suspended blind row and the durable pending row never carry it, and it never changes the standing-row or automatic-answer verdicts. `approvalRequestedBySentence` gives the sentence ('Requested by ...' / zh) with the name through the display sanitizer, and `approvalAskReasonsOf` is exported at the package root. From 0.110.0 an approval card request also carries an optional `taskId`: the id of the leg the card belongs to — the wire run id for a main-leg card, the delegated child's session id for a child-leg card — and a card only ever has that one value (it is the same identity value the package uses to recognise the same approval). It is present only when the package can read it from the card's own source (for a main-leg frame, the run binding registered for the frame first, otherwise the run the caller passes in, read as own data slots so getters are never run; for a child-leg frame, the source session position on the frame; the parked-row path uses the task id it was driven with, the same value as `parkRow.taskId`); it is absent when it cannot be read, never an empty string, and the package never guesses (no latest-binding fallback, the frame's own `taskId` is not read, and the host run never stands in for a child-leg value that cannot be read). It takes no part in the decision, is not sent on the wire, and the pure projection exits do not carry it. From 0.110.0 an approval card request (and the card facts of the pure projection exits) also carries an optional `inputSource`: `'record'` when the arguments on the card come from the record side (the pending row's input, or the frame's arguments) and `'stream'` when the record side had no usable arguments and the package fell back to the model's original call from the event stream. It is left out when there are no arguments to read (the card shows an empty object or a path recovered from the question), when the stream value is not an object, and on a suspended card the host declared blind; it is read from the card's input reading only, so a same-named key lying on the frame, the card face or the row is never copied. It sits alongside `argsRewritten` and `argsUnavailable` rather than replacing them, takes no part in the decision and is not sent on the wire. |
612
612
  | `scripts/run-retry-status-notice-test.mjs` | The retry-line sentence. `retryStatusNotice(status, ctx?)` turns a retry status (or `null` when the row should be cleared, which gives no sentence) into one sentence, a machine-readable way out (`switch_model`, `wait`, `fix_request`, or none) and the phase of the line. The gate pins the full truth table (arm × terminal × provider-condition word × HTTP status × attempt counts) against an independent oracle and a set of literal sentences; the first-token wait reads as a wait and never as an error; absent or invalid positions are read as absent (no zeros, no invented reasons, no quota wording); words outside the engine's closed set read like `unknown` and are never echoed; the way out is given only where it applies; unknown arms read as the default arm; per-key throwing proxies, a single read per key, a frozen result and purity; the sentence table's keys equal the engine's `PROVIDER_CONDITIONS` and no second table exists; sentence hygiene; and an end-to-end run through the real wire projection. `retryStatusParts(status, ctx?)` returns the same facts as separate segments instead of one string: the model name, the condition phrase, the HTTP status, the attempt number and its cap, the wait and limit of a first-token wait, the engine's own words, the retry-at time as a wall-clock millisecond value for the host to count down, a one-sentence note on what the condition was judged from, and the way out. A segment is present only when the engine gave it, and nothing is ever filled in with zero. The one-line sentence is assembled from the same segments, and the gate checks that the line equals an independently written join of the segments across thousands of combinations. |
613
613
  | `scripts/run-tool-row-terminal-projection-test.mjs` | A tool call held by a park gets no terminal row from its park-time close frame: the call is not settled yet, so the projection takes its card off the open-card table, records it on the adapter instance (with the parked run and checkpoint id, delivered by an internal terminal-fact arm that the stream emits before the result frame) and emits one `tool_parked` chrome event naming the call and the transcript uuid of its start row. Its terminal row is minted only by the executing leg: a later frame for the same call id claims the original card only with a witness (a continuation the question bridge spliced into the same projection); without one it is a new call with its own start row. A claim also needs the parked start row to still be the latest start row the instance minted for that call id (whichever of the two was recorded first), refuses a frame naming a different tool or bound to a run outside the park, and keeps the original row identity while taking the continuation's input. Collateral calls, unreadable frames and a park the package itself settled (Esc on a parked card) keep their terminal rows; the print lane uses the same judge; the registry travels with `exportLedger` / `importLedger`; a run that ends without parking retires its entries; and results pair with the nearest preceding unpaired start row of the same call id. The host-declared continuation witness no longer exists: only a continuation spliced into the same projection reclaims the parked row, and across projections every continuation is a new call until the engine marks the frame itself. |
614
- | `scripts/run-standing-rows-test.mjs` | Single judge `standingRowsOf(card, host?)` for the three standing rows on an approval card: allow for this session, don't ask again, and the deployment-list session row. Every row widens what runs without asking, so each one fails closed. An unreadable card, `rememberRefused` from the engine (own or inherited) or a present-but-unreadable value closes all three rows, even when rule offers are present. The session row needs the route fact `allowSessionCarried`, which the live-frame and pending-row paths stamp and the v1 path does not. The deployment-list row needs every ask class to be the deployment-list class, checked against the installed engine's `classifyAsk`. Engine rule offers in either slot give `engine_offer`; an unreadable offer key gives none. A local whole-tool rule is offered only when no engine lane closed the door, the arguments are available, the grant binding reads `none`, a standing allow rule could clear the ask, and the host reports a generic card face and that the rule would be sent; each precondition is flipped on its own. The live-frame, pending-row and v1 paths are exercised end to end, the grant-binding reading (since 0.98.0 the engine's own `boundGrantAbsence` word, not the tool name) is compared form by form on all three paths with the server's three grant-binding functions when a 7.129.0-or-later server fixture is installed, and may only be stricter where the card cannot tell which of them the server applies, malformed, inherited or throwing refusal evidence on the live-frame, pending-row and v1 carriers is recorded on the card as `unreadableEvidence` and closes the rows instead of reading as absent (checked end to end through the real paths); results are frozen, and hostile host facts never throw. Since 0.97.0 the seat request's allow-for-this-session bit `sessionRow` is minted by `toolPermissionRequestAskBits(card, requestId)` from the same premise as the judge's session row and the request id's domain (`live:` and `durable:` carry a session answer; `question:`, `elicit:` and `plan:` do not); an unreadable or refused remember flag never lets the seat show that row, and the crossing never copies the bit from its input — a seat request, or any input carrying a request id, passed back through it gets no bit. The deployment-list question class names come only from the generated ask-class table. The session row and the deployment-list session row are withheld when the card carries unreadable evidence or an unreadable card payload. |
614
+ | `scripts/run-standing-rows-test.mjs` | Single judge `standingRowsOf(card, host?)` for the three standing rows on an approval card: allow for this session, don't ask again, and the deployment-list session row. Every row widens what runs without asking, so each one fails closed. An unreadable card, `rememberRefused` from the engine (own or inherited) or a present-but-unreadable value closes all three rows, even when rule offers are present. The session row needs the route fact `allowSessionCarried`, which the live-frame and pending-row paths stamp and the v1 path does not. The deployment-list row needs every ask class to be the deployment-list class, checked against the installed engine's `classifyAsk`. Engine rule offers in either slot give `engine_offer`; an unreadable offer key gives none. A local whole-tool rule is offered only when no engine lane closed the door, the arguments are available, the grant binding reads `none`, a standing allow rule could clear the ask, and the host reports a generic card face and that the rule would be sent; each precondition is flipped on its own. The live-frame, pending-row and v1 paths are exercised end to end, the grant-binding reading (since 0.98.0 the engine's own `boundGrantAbsence` word, not the tool name) is compared form by form on all three paths with the server's three grant-binding functions when a 7.129.0-or-later server fixture is installed, and may only be stricter where the card cannot tell which of them the server applies, malformed, inherited or throwing refusal evidence on the live-frame, pending-row and v1 carriers is recorded on the card as `unreadableEvidence` and closes the rows instead of reading as absent (checked end to end through the real paths); results are frozen, and hostile host facts never throw. Since 0.97.0 the seat request's allow-for-this-session bit `sessionRow` is minted by `toolPermissionRequestAskBits(card, requestId)` from the same premise as the judge's session row and the request id's domain (`live:` and `durable:` carry a session answer; `question:`, `elicit:` and `plan:` do not); an unreadable or refused remember flag never lets the seat show that row, and the crossing never copies the bit from its input — a seat request, or any input carrying a request id, passed back through it gets no bit. The deployment-list question class names come only from the generated ask-class table. The session row and the deployment-list session row are withheld when the card carries unreadable evidence or an unreadable card payload. Since 0.109.0 the persistent row reads the verdict's question-level reading (whether a whole-tool rule could clear this kind of question), so `standingRowsOf` and `rememberOfferable` are byte-identical to the previous release; the guard fails when the previous release is not supplied. Since 0.109.0 a presence check that a host proxy answers falsely (the has trap says the key is not there while an own slot is) no longer reads as absence: presence and value come from one observation, anything that cannot prove absence closes all three rows like a throwing has trap does, and a truly absent key reads exactly as before. |
615
615
  | `scripts/run-steer-tier-test.mjs` | Steer priority goes through one gate. The capability reader for `fleet.steerPriority` keeps three states, takes the word as an open set and treats `fleet: false` as not reported; its judge answers `engine` only for the exact word. The steer body has one constructor, `steerBodyOf`, used by the router and by clients that build their own request: `next` and no priority never enter the body (byte-identical to the previous releases on six call shapes), `now` and `later` enter it only when the reading is `engine`, and otherwise the constructor throws a refusal with a stable code and no request is sent. Against the 7.116.0 server package the guard runs the capabilities route, the steer route with the engine's own steer verb and an in-memory checkpoint store: the words the client admits are accepted, the words it refuses are rejected with the same field error, same-key replays stay idempotent, a real tier change is refused, and a `now` steer's cut notice carries the receipt's message id. It also pins the one sentence for a same-key replay (it does not tell the user to resend under a new key unless the text changed), the rewritten refusal sentences, the single receipt reader, the cut-notice verdict and sentence through both projection layers, the first-beat order (bounded capability wait, then reading, then send) and the engine-switch chain where an invalidated reading sends nothing |
616
616
  | `scripts/run-session-policy-grammar-test.mjs` | Reading and rewriting a session’s own rule record never changes which rule grammar its entries are read in. The record carries a grammar stamp: the read exposes it (absent means a record stored before the stamp, which the engine reads in its earlier grammar — never folded into a number), and both rewriting paths — tightening and removing refused entries — send the same stamp back. When a record has no stamp and a rewrite would carry back one of its allow entries written in a tool’s own rule form (such as Bash(git:*)), which the earlier grammar reads as allowing nothing and the current grammar reads as a working allow rule, the rewrite sends nothing and returns a machine-readable refusal naming those entries — for every caller identity alike, since an identity allowed to widen rules would otherwise have the write accepted. An engine refusal of the same kind maps to the same outcome, and a write receipt that comes back without the stamp it was sent with, or with an older one, is never reported as written. The guard runs each case for both caller identities against a modelled engine and, when the matching server build is installed, against the real engine write judge and the server’s error mapping, recording the pre-fix widening as its failing baseline; over a generated corpus it checks that every entry the engine reads differently in the earlier grammar is one the package refuses to carry back, and that every extra entry it refuses is one the engine would refuse to write anyway. Malformed stamps, accessors, per-key throwing proxies and a polluted prototype are refused rather than read as absent. |
617
617
  | `scripts/run-hooks-merged-disable-projection-test.mjs` | The fourth governance leg of the hooks projection: `disableAllHooks` set in a non-managed settings source. The value that counts is the **merged** one, computed by the package from the per-layer raw documents handed over through `HostSettingsSource.readLayer` (the former optional `SettingsPort.mergedDisableAllHooks()` host answer retired in 0.101.0), because a per-source approximation ("any source says true") reads user `true` with local `false` backwards — the merged value there is `false` and every source's hooks ship. When the merged value is `true`, only managed-settings hooks are sent to the engine: non-managed settings can switch off their own hooks, never the managed ones, and a managed `disableAllHooks` is still judged first and sends nothing at all. A full matrix over the four sources, each true, false or absent, is merged with the reference rule (later sources override earlier ones, managed settings last) and every cell's projection is asserted. The reader is the only authority: per-source values never second-guess it, and only a strict `true` counts. A host that does not implement it keeps the previous behaviour and gets exactly one warning per installed settings port, never one per request, and none on paths where the reader would not have been consulted; a reader that throws is treated as `true`, so managed hooks still ship. The session goal's Stop hook and the final-verification yield rule, which reads the projected hooks, follow the same verdict, and the trust gate and the three managed gates are evaluated before the reader is ever called. The last leg pins the member's declared shape in the built declarations: optional, no parameters, returning a boolean or `undefined`. The flag settings source (a settings file or inline settings given at startup) is projected after the local source, and settings hooks stay concatenated managed first (managed → user → project → local → flag): the engine runs hooks in request order, and three of its budgets go to whoever comes first — the per-event wall-clock budget, the per-event cap on model-backed entries, and the total cap on added context — so a managed hook placed after the others could be crowded out and silently not run. The flag source moves with user, project and local under every managed or merged gate; its exec-form entries are dropped and warned about like any other settings source, and the not-run notice names it `Flag settings`. An optional leg feeds the request body to an installed server's hook runner and requires the managed deny, block and context to take effect while non-managed hooks exhaust each of those budgets, and pins the known cost of that order: a non-managed `PreToolUse` hook placed after the managed one can still rewrite the input after the managed check allowed it. Without an installed server the leg reports that it did not run. Safe and bare mode: the host reports its startup mode through the optional `SettingsPort.hooksStartupMode()`, which both the plain and the plan path read and which answers `'safe'` or `'bare'`; the flag already carried by the plugin hooks reading, which only the plan path reads and which cannot tell the two modes apart, counts as safe mode unless the reader named a mode. In either mode only managed-settings hooks are sent from the settings sources: bare mode is handled like safe mode, so managed hooks are still sent, also under the managed-only gates. In both modes plugin hooks are excluded as before and the session goal's Stop hook is still sent. Only the two exact words count; a host that does not implement the reader, returns `undefined`, returns any other value or throws keeps the previous request byte for byte, checked for every such reply, both flag states, both paths and with or without a session goal; any other value and a throw each leave one debug line, and so does a reader written as a property instead of a method, which narrows nothing. The reader is called at most once per request and not at all when hooks are already switched off entirely. The optional server leg requires the left-out sources' hooks not to run while the managed ones still run and still deny, in either mode. A hook written in more than one settings place is sent once: entries on the same event, in groups that are identical apart from their hooks (same matcher), with the same identity — command, shell, arguments and condition for command hooks; type, prompt and condition for prompt and agent hooks; URL for HTTP hooks; server, tool and input for MCP tool hooks — are sent once, at the position of their first occurrence in the concatenation order and with the fields of the copy from the highest-precedence source (managed, then flag, local, project, user; within one source the later copy), so a managed copy keeps both its place and its values and a user copy duplicated in flag settings keeps its place but uses the flag copy's timeout; the other entries keep their order, groups without duplicates are sent as they were, and a group left empty is not sent. Different matchers, events, a one-byte command difference, a different condition, an absent versus explicit shell, different types or extra group keys are not merged; malformed entries and entries whose identity cannot be computed are left alone; plugin groups and the session goal hook do not take part, and hooks removed by safe or bare mode never come back. The optional server leg requires such a duplicate to run once with its added context appearing once, and a user copy with a one-second timeout duplicated by a flag copy with a ten-second timeout to run to completion on a two-second command. When the managed settings turn all hooks off, the not-run notice list gains exactly one line saying so; in every other case that line is absent. |
@@ -647,14 +647,14 @@ guard still cross-checks the table by name).
647
647
  | `scripts/run-reattach-window-test.mjs` | The pure decisions behind a windowed reconnect loop. A wall-clock window (300 s by default) bounds the whole attempt; backoff starts at one second, doubles up to a 30 s cap and honours a server-sent wait; a run the engine does not recognise is only declared unknown after several consecutive not-found answers spread over a minimum elapsed time, and any frame from the stream resets that count. A poisoned stream frame is tolerated once (resume from the last good frame) and raised the second time at the same position. The suite drives the pure judgments in a loop and requires the same outcome, beat count, per-beat wait and confirmation count as a transcription of the reference terminal loop over seeded random scripts and tunings, and pins that malformed inputs stay bounded. |
648
648
  | `scripts/run-tool-history-accepted-test.mjs` | The accepted-history bit on a result frame. After a tool-history mismatch parks a session, the next ending that the engine actually accepts is flagged once with `_sema_tool_history_accepted`, so a way-out picker does not keep pre-selecting the broken turn. The suite checks the key and its reader, the ledger semantics (reported once; a rate-limit, failure or blocked ending in between does not lose the bit; a replay is not an acceptance), an independent oracle over seeded random sequences, the real stream pipeline end to end, and that the key is minted from exactly one place and registered in the type-superset table. |
649
649
  | `scripts/run-attachments-credential-word-test.mjs` | What happens when the attachments setting holds something that looks like a credential. A single unrecognised word used to fall back silently to the defaults, while naming a retired key was refused loudly. Now a single word, or any list entry, that has the shape of a known credential is refused: the thrown error and the on-screen sentence both leave the value out (the token field carries the redaction marker, never the text) and say that a credential does not belong in this setting. Ordinary unrecognised single words such as a typo keep their historical fallback to the defaults, and every recognised spelling answers byte for byte as before. |
650
- | `scripts/run-engine-health-facts-test.mjs` | The self-reported facts an engine publishes on its unauthenticated health body, read in one place: which centre it is attached to, which identity sources it accepts, whether an operator exists, and whether it says it is draining. Each of the four is read on its own, from own data properties only, and a malformed one is absent rather than guessed; the others are unaffected. Absence never means the negative: a missing centre is not no centre, a missing drain object is not not draining. Draining is read only from the engine signal object, never from the deprecated flat boolean, and the guard compares it against the settings contract's own signal reader on a pool of well-formed signals. |
650
+ | `scripts/run-engine-health-facts-test.mjs` | The self-reported facts an engine publishes on its unauthenticated health body, read in one place: which centre it is attached to, which identity sources it accepts, whether an operator exists, and whether it says it is draining. Each of the four is read on its own, from own data properties only, and a malformed one is absent rather than guessed; the others are unaffected. Absence never means the negative: a missing centre is not no centre, a missing drain object is not not draining. Draining is read only from the engine signal object, never from the deprecated flat boolean, and the guard compares it against the settings contract's own signal reader on a pool of well-formed signals. The same reader also returns, each on its own, the engine version as written, the in-flight count (zero is a reading, a missing count is not zero), the data root exactly as the engine reports it, and a marker that is present only when the engine says its in-flight count already includes background shells left running in sessions; that marker is never false. |
651
651
  | `scripts/run-delegated-prompt-absence-test.mjs` | Why a delegated subagent's prompt could not be shown. The fetch used to answer only skipped, unreadable or absent, so a page that could not show a prompt had no way to say why. It now carries a closed reason with a ready sentence: this process has no record of where the conversation lives, the session store has no conversation for it, access was refused, the conversation was read but holds no delegated prompt, or the reason is unknown. The reasons come from the status of the request, never from the text of an error, an already-running fetch carries no reason because it is not an absence, and a settled absence keeps answering with the same reason. |
652
652
  | `scripts/run-error-supplement-roles-test.mjs` | Who decides whether a one-sentence explanation can stand in for the raw error line. When a request fails, the engine's machine-readable error line may be followed by a sentence this package wrote; whether that sentence fully replaces the line or only adds one thing to it is declared by the package for each sentence reader, and the host reads the declaration instead of guessing from sentence length. A reader is declared a replacement only when its sentence says on its own what happened, what was left untouched and what to do, never points at a neighbouring line, and never leaves the key fact to the engine's own message; a reader whose sentence depends on the raw line is declared a supplement, and a reader nobody has declared is treated as a supplement as well, so an unknown or newer reader can never hide the raw line. The lookup goes by the reader function itself rather than its name, so renaming or wrapping a reader cannot silently inherit a role. The suite samples every declared sentence in both languages and checks the evidence for its declared role, and it fails when a new reader ending in `Content` or `Supplement` appears on the root entry without either a declaration or a written reason for staying undeclared. |
653
653
  | `scripts/run-argv-values-equivalence-test.mjs` | The headless `-p` lane no longer scans the command line itself. Its resolvers for permission mode, scenario, limits, final verification, interactive tools, sandbox and the live tool face now take the values the terminal's own option parser already settled — a small object per flag group — and the old shared last-flag-wins scanner plus the six argv readers built on it are gone from the public surface, so exactly one layer reads a command line and hosts without a command line (web, desktop) pass the same values. The behaviour of the `-p` lane is unchanged: the suite replays more than a thousand argument vectors — hand-written edge cases for every flag (missing value, flag-shaped value, `=` form, repeats, tokens after `--`, both permission flags, empty strings) plus a seeded random mix — under three environments through a stand-in for the terminal parser, and requires the full outcome of all seven resolvers to match, byte for byte as a digest, what the last release that still read argv produced for the same vectors. It also pins the input discipline of the new value shapes (hostile objects count as absent flags and never throw; wrongly typed fields count as absent; the empty string is a value), the exact meaning of a flag that is present but has no value for each resolver, the three-state reader of the deployment knob (absent / a word / invalid with its notice, no one-shot latch, frozen results) and the independently exported downgrade notice for the `dontAsk` mode, and that none of the eight modules mentions an argument vector in code. |
654
654
  | `scripts/run-slash-command-gate-test.mjs` | The slash-command gate and the command roster, shared by every host. Whether a slash command is usable on this deployment used to be decided three times — once in the terminal client against its own roster file, and again, from capability readers, in each of the web and desktop command surfaces. The package now owns both halves: a roster of command names in three buckets (local interface state, mappable to the SDK, not applicable to a deployment) pinned to the Claude Code version it was taken from, with per-command overrides, and one judge that takes a command reference and a capabilities snapshot and answers allowed, degraded (visible, with a gap to render), hidden (loaded and callable by exact name but left out of help and completion) or denied, with the reason from a closed set of seven words that hosts can show as the disabled reason. The judge reads capability bits as an open set and fails closed: a missing snapshot, a missing bit or a false bit never counts as present, though a degradable command is kept visible; the host's own two gates are applied first and a missing answer from them means they were not asked, not that they failed. Everything read from the host is read as plain own data (accessors are not run, prototype-only keys do not count, a hostile object counts as absent), command names are matched exactly and through a map so that names like `__proto__` never reach a table entry, and verdicts are frozen and freshly minted. The suite compares the roster and overrides with the terminal's own file when it is available, checks a hand-written verdict matrix, and replays a line-by-line transcription of the terminal's own gate steps over every name, fourteen snapshot shapes, three sources and four host-gate combinations. |
655
655
  | `scripts/run-mcp-status-superset-keys-test.mjs` | Who mints the two extra keys on the headless machine-readable MCP answers. When a headless run answers an MCP status request (one row per server) or a re-dial request on the engine lane, the row carries two keys beyond the Claude Code shape: the connection state of that server on the engine's own leg, and a separate annotation for the host's local connection, which only serves MCP prompts and resources and is neither passed off as the engine nor hidden. The terminal client used to mint both keys itself; the package now owns the key names, the value shapes and the rules for when each is present, so every host that answers these requests produces the same keys. The engine-leg value is a frozen copy of the reading the package already derives from the engine's roster (not shared with the caller's object, copied field by field so a new field in the reading is carried along without a second list of names); the key is absent off the engine lane, which is not a statement that the server is down. The local-connection annotation carries the local state word as given, always says it serves prompts and resources, and carries the failure text only when the state is `failed` and the text is non-empty; when the state word cannot be read the key is simply absent rather than invented. A re-dial answer carries only the local-connection key. The suite replays a transcription of the terminal's existing minting over every engine-leg reading arm, twelve state-word shapes and five failure-text shapes and requires the same keys with the same values, and pins the absence rules, the freezing, and the plain-own-data reading of hostile inputs. |
656
656
  | `scripts/run-task-notification-entry-sampling-test.mjs` | Notification payloads are read from one frozen sample taken at the entry, and the completion identity is carried through. A background-task or workflow-completion notification used to be read field by field at several places (the validity check, normalisation, the rendered text, the de-duplication key, the two ledger verbs), so an object with accessors or proxy traps could show one value to the check and another to the renderer. Each entry — the interactive frame projection, the workflow-completion frame, a host handing frames straight to the adapter, the public helpers, the `-p` output builder, the fleet background notification, the idle watcher's probe answers, the structured body of a TaskOutput result, and transcript replay — now takes exactly one sample, before its first read of the payload (the replay cursor read in the approval path is deliberately outside this and unchanged): own data properties only, accessors never executed (their value is simply absent), proxy traps that throw leave that key absent, prototype-chain keys do not count, cycles and over-deep subtrees are cut, and the result is a deeply frozen copy carrying a package-private brand so a sample passed to another entry is not sampled again. Ordinary JSON round-trips value for value. Notification facts additionally carry the engine-reported completion id and cycle number when the engine reported them (absent means the engine did not say; nothing is defaulted to zero or guessed from the task id), the cycle number the idle watcher keeps for its own bookkeeping is never presented as the engine's, and a finished TaskOutput card (background command, background agent or generic task) carries the completion id under an extra key only when the card is final by its own structured status and the engine supplied it; the cycle number is an integer from 1 up and only when the source reported it. The suite counts reads through instrumented proxies at every entry, compares two independent samples of equal input cell by cell, and proves the counting rule itself turns red on a second sample. |
657
- | `scripts/run-host-settings-source-test.mjs` | The per-turn host settings source (`HostSettingsSource`) that replaced the process-wide settings slot, and the decisions the assembly entry now makes from it: every one of its six members is required and is called exactly once per turn (the hooks plan reuses that single read), a member that throws or answers malformed degrades only its own reading, and an out-of-set workspace trust reading counts as unreadable rather than accepted. The single read is a detached capture: records come back without a prototype, so a value someone later installs on `Object.prototype` cannot change a governance decision; carrier and issue lists are read once by index from own data properties; revoked proxies, shared references and oversized readings are refused as malformed, with bounded work. The suite also pins the six-word managed-settings table cell by cell, the stale-read adoption gate (truncated or rewritten files keep the last trusted read and keep sending its deny and ask rules, deliberate `{}` is adopted, allow rules only follow a clean read, and a changed set of files clears the allow history), the session-state update rule (a state carries its version and the version it was derived from; a host commits only when the stored state is that predecessor, so a concurrent lane or a repeated confirmation cannot overwrite it), asking before an interactive turn proceeds without a settings file that did not load, the output-style decision, and the per-layer permission merge, including the one deliberate loosening: allow rules from the launch-flag settings layer are now admitted. The hooks plan also reports, layer by layer, the repository-controlled settings layers whose hooks were withheld because the host reports no workspace-trust step (`settingsWithheld`, with the sentence listed among the not-run notices); the suite replays one five-layer document under all three trust readings, checks that layers without hooks or narrowed away by managed-only governance are not listed, and pins that the admission judge answers only `trust_withheld` for the hooks category. The per-layer merge also pins the order inside a trust group: a rule written by several sources keeps the strongest source and sits where that source wrote it (a rule repeated within one source keeps its first position, and a weaker source listing it again does not move it), while the order between groups, between the three buckets, and the request body of any input with no rule shared across sources stay byte-identical. A host that hands in a layer document already folded with extra tightening rules (for example a deny or ask list taken from the repository environment) is read the same way as one that hands in the file text: each rule is judged by layer, lands in the trust group of the layer it was folded into, and reaches the request body like a rule written in a file, while a rule folded into the allow direction is admitted no more than a file-written one, and the folded tightening rules are kept when the workspace-trust reading is declined. |
657
+ | `scripts/run-host-settings-source-test.mjs` | The per-turn host settings source (`HostSettingsSource`) that replaced the process-wide settings slot, and the decisions the assembly entry now makes from it: every one of its six members is required and is called exactly once per turn (the hooks plan reuses that single read), a member that throws or answers malformed degrades only its own reading, and an out-of-set workspace trust reading counts as unreadable rather than accepted. The single read is a detached capture: records come back without a prototype, so a value someone later installs on `Object.prototype` cannot change a governance decision; carrier and issue lists are read once by index from own data properties; revoked proxies, shared references and oversized readings are refused as malformed, with bounded work. The suite also pins the six-word managed-settings table cell by cell, the stale-read adoption gate (truncated or rewritten files keep the last trusted read and keep sending its deny and ask rules, deliberate `{}` is adopted, and so is a file the host reports as confirmed gone (`absent`: its rules stop applying from that turn, while a file that is merely unreadable, not yet loaded, truncated or missing from the carrier list is still held), allow rules only follow a clean read, and a changed set of files clears the allow history), the session-state update rule (a state carries its version and the version it was derived from; a host commits only when the stored state is that predecessor, so a concurrent lane or a repeated confirmation cannot overwrite it), asking before an interactive turn proceeds without a settings file that did not load, the output-style decision, and the per-layer permission merge, including the one deliberate loosening: allow rules from the launch-flag settings layer are now admitted. The hooks plan also reports, layer by layer, the repository-controlled settings layers whose hooks were withheld because the host reports no workspace-trust step (`settingsWithheld`, with the sentence listed among the not-run notices); the suite replays one five-layer document under all three trust readings, checks that layers without hooks or narrowed away by managed-only governance are not listed, and pins that the admission judge answers only `trust_withheld` for the hooks category. The per-layer merge also pins the order inside a trust group: a rule written by several sources keeps the strongest source and sits where that source wrote it (a rule repeated within one source keeps its first position, and a weaker source listing it again does not move it), while the order between groups, between the three buckets, and the request body of any input with no rule shared across sources stay byte-identical. A host that hands in a layer document already folded with extra tightening rules (for example a deny or ask list taken from the repository environment) is read the same way as one that hands in the file text: each rule is judged by layer, lands in the trust group of the layer it was folded into, and reaches the request body like a rule written in a file, while a rule folded into the allow direction is admitted no more than a file-written one, and the folded tightening rules are kept when the workspace-trust reading is declined. |
658
658
  | `scripts/run-mcp-server-name-collision-test.mjs` | MCP server names that collide once the engine builds tool names from them. Two declared servers whose names the engine reads as one — `a.b` and `a_b`, or one name written twice — are refused by the engine as a pair, at mount, before any server is started, and the run ends with the engine's refusal code. The client passes that code through unchanged on every carrier a refusal arrives on (a 400 body, a terminal failure, a result frame, the folded resume refusal) and gives it one sentence, in English and Chinese, that says what the engine said and what to change; it does not say which of the two is still used, because neither is. A code the table does not know passes through as it came and gets no sentence. The client never judges this collision itself: the local MCP configuration projection puts both servers on the wire, names byte for byte and in order, with nothing reported as dropped. The suite runs the installed engine on a colliding pair (both spellings, both orders, and one name written twice) and pins that the code is the engine's own, byte for byte, and that no server was started. |
659
659
  | `scripts/run-project-memory-projection-test.mjs` | Request key `projectMemory` and its manifest reader. The request port takes one closed-set word, `disabled` (this run does not read the deployment's project instruction files); an absent value means the request carries no such key at all — the package never mints an "on" word, and a bad value (any other string, `false`, a number, an array) is refused loudly at assembly time on every lane without echoing it. The reader `projectMemoryOf(manifest)` answers one of the manifest's three words — `on`, `disabled`, `absent` — and answers `undefined` for a missing member, an out-of-table word, a non-string value or an unreadable object; it never guesses `absent`, and it reads own data properties only. The gate cross-checks both word sets against the installed engine's declaration files and change record, mints manifests with the engine's own deriver, and runs the same cells against four hand-made broken implementations (mint-on-absence, guess-absent, fold-false-into-absence, read-the-prototype-chain) that must each turn red. A capability reader, `observedProjectMemoryCapability(baseUrl?)`, reads the server's top-level `projectMemory` capability bit from the shared capabilities cache (own data property, strictly `true` to confirm; absent / declined / unreadable / unobserved otherwise), and a fragment helper, `projectMemoryDeclaration(intent, reading)`, hands the request key over only when that reading is confirmed (older servers refuse an unknown top-level request key as a whole): otherwise it returns an empty fragment plus a machine-readable cause (`capability_absent` / `capability_declined` / `capability_unreadable`), and a bilingual sentence says the project instruction files are still sent to the model. There is no version floor anywhere in the helper. A fixture leg and a server-source leg record that the capability bit is minted at the top level by the servers that accept the key and not by older ones. The capability reader's default anchor is read inside the guarded section: a default engine target whose address getter throws yields `unobserved`, as it does for the other readers of the same family. |
660
660
  | `scripts/run-decide-row-unrevivable-projection-test.mjs` | A stored approval card the engine can no longer revive. When a parked approval cannot be brought back — its row has no agent name or no root-session anchor — the engine answers the decision with one refusal code, before it claims anything, and neither approving nor denying the card will ever take effect. The client reads that code with one strict equality test (no prefix, case or whitespace folding) and puts a single optional flag on the refusal reading, present only when the engine said so and never written as false; the flag rides the same code on every carrier a refusal arrives on (a thrown error, a plain response body, and the settled outcome of both decision legs and of the send machine). The flag is distinct from the sibling refusal that asks for a corrected answer, which has the same standing and needs the flag to be told apart, and a body that calls itself retriable does not override it. The client also gives one sentence, in English and Chinese, that says the card cannot be recovered, that nothing chosen on it will apply and that it has to be cleared on the server; it never suggests trying again. The suite runs the installed server's own decision function on rows without a name and without a root anchor, feeds the real bodies through the real SDK error mapping, and swaps in deliberately wrong judges (prefix, substring, case-folding, trimming, any prefixed code) to prove the truth tables turn red. |
@@ -668,11 +668,11 @@ guard still cross-checks the table by name).
668
668
  | `scripts/run-approval-frame-families-declaration-test.mjs` | Request key `settings.approvalFrameFamilies`, the approval-card frame families this client renders. The package mints the declaration itself at assembly time from a reading the host hands in (`approvalFrameFamiliesReading`, taken for the engine the request goes to): the families the server presents (the top-level `approvalFrameFamilies` capability array) intersected with the families the package can render. Without a reading no key is sent, and assembly never reads the default engine target. Two bits must both be well-formed — the acceptance bit `taskSettings.approvalFrameFamilies` (the server takes the key) and the presented-families array — otherwise no key is sent and the request is byte-identical to the one built before this key existed: absent, declined, unreadable and unobserved readings never mint a key, an out-of-table word declines the whole bit, and a server that presents the array but does not accept the key is never sent it. An empty intersection is declared as `[]` rather than skipped. The reader `observedApprovalFrameFamilies(baseUrl?)` and the judge `approvalFrameFamiliesDeclaration(reading)` are pure over the shared capabilities cache and answer five reading states. The gate replays 32 capability body shapes, accessor and throwing-proxy bodies, prototype-chain pollution (of the key and of descriptor values), a throwing engine-target getter and unprobed / failed / invalidated caches, pins the assembled request byte for byte against the pre-key output on the older server shapes and, in a two-session host, for the session whose server does not accept the key, cross-checks the server's key list, tuple and capability mint site in the installed server packages of every generation, and compiles the server's own admission judge and approval-leg resolver verbatim: the declaration the package assembles must keep the leg from falling to the unattended path, while no declaration, or the other family, must make it do so. Hand-made broken readers, judges and assemblers must each turn red. |
669
669
  | `scripts/run-session-background-projection-test.mjs` | Session hand-off to the background: the capability reading `readSessionBackgroundable` (five states), one engine wire per engine generation, the detach posture and the package's own opening of the interactive stream `streamInteractiveRun` (request and headers snapshotted once at entry; every spelling of the detach header replaced by a single canonical value; one header-less reopen after the exact durable-off 400 unless the idempotency key is pinned; one opening record per stream), the single eligibility judge `backgroundHandoffEligibility` (fifteen ordered rows, three outcomes) with its sentence, and the hand-off verb `handoffInteractiveRun` (it tears only the stream the package opened, never interrupts or cancels the run, and the background view rereads the durable ledger from the start). Pinned: the five stream shapes that return a run id without running for this submission never become eligible; a parked terminal frame ends the original stream; hand-off racing completion settles exactly once; an approval or question handed to the host but not yet processed, a pending question and an approval hold on the run keep the answer in the foreground; a capability generation change or a retired wire refuses; Esc after hand-off is withheld; every reader never throws; a second copy of the package cannot recognize the wire; every judge row is pinned with all later rows' conditions also true. |
670
670
  | `scripts/run-thinking-latch-projection-test.mjs` | Session-level extended-thinking switch. The request's `reasoningEffort` is minted on exactly one line of the assembly entry, from one projection that reads the lane, the switch reading the host passes (`thinkingLatch`), the effort word, an optional per-turn floor (`reasoningFloor`) and whether this request really carries `settings.ultracode: true`. Once set, the switch alone answers on or off: off sends `"off"`, on only lifts an effort word of `"off"` and never picks a level for the user; with Ultracode on the same request both go on the body and the engine decides which applies. Unset with no floor leaves the request body byte-for-byte unchanged against the previous release. The transition table, stored form and read classification exist once in the package; a seat session keeps its switch in the desktop host (verb `setThinkingLatch` with an `OkAck` receipt, capability `thinkingLatch`, a four-word row field read back only through `readSeatThinkingLatch`, so a failed read is never reported as never set). |
671
- | `scripts/run-mods-face-projection-test.mjs` | The three user-facing mod seats. A registration record is read one way wherever it comes from (the run receipt's `mods` section, the refused-registration notice's detail, the in-process kernel's records): own data properties only, each key isolated, the two shapes told apart by member presence, no derived disposition word. `modRowsOf` answers `undefined` for anything that is not an array or where no row survives (unreadable is not zero) and `[]` for an empty list; `readModRegistrationRefusedNotice` turns the refused-registration notice into the refused row, or answers `undefined` so the host keeps the engine's own line. On tool result rows the package adds `_sema_answered_by`, the registration that answered a call at the `tool.call` station in place of the tool, from one criterion evaluated once at the single row mint against the same gate view it writes to `_sema_gate`: the engine's own fail-closed marker `engine` never counts as an answering registration, a refusal needs a `denied` / `hook` gate record and `isError: true`, an answer in place needs a gate record that is not a denial and `isError: false`, and an unreadable gate record or an `isError` that cannot be read yields nothing. `modCardSentence` renders the registration card in its two shapes and the refused-by / answered-in-place line; `modLoaderFailureSentence` renders a load failure from whatever the host caught. The gate drives the real kernel in process, the installed server package's receipt and notice projections, and frames derived from the engine's `tool_end` type through both the interactive and the print outputs; it compares the receipt projection byte for byte with the previous reading over the existing samples and a generated pool, and checks that every outward string goes through the display sanitizer. |
671
+ | `scripts/run-mods-face-projection-test.mjs` | The three user-facing mod seats. A registration record is read one way wherever it comes from (the run receipt's `mods` section, the refused-registration notice's detail, the in-process kernel's records): own data properties only, each key isolated, the two shapes told apart by member presence, no derived disposition word. `modRowsOf` answers `undefined` for anything that is not an array or where no row survives (unreadable is not zero) and `[]` for an empty list; `readModRegistrationRefusedNotice` turns the refused-registration notice into the refused row, or answers `undefined` so the host keeps the engine's own line. On tool result rows the package adds `_sema_answered_by`, the registration that answered a call at the `tool.call` station in place of the tool, from one criterion evaluated once at the single row mint against the same gate view it writes to `_sema_gate`: the engine's own fail-closed marker `engine` never counts as an answering registration, a refusal needs a `denied` / `hook` gate record and `isError: true`, an answer in place needs a gate record that is not a denial and `isError: false`, and an unreadable gate record or an `isError` that cannot be read yields nothing. `modCardSentence` renders the registration card in its two shapes and the refused-by / answered-in-place line; `modLoaderFailureSentence` renders a load failure from whatever the host caught. The gate drives the real kernel in process, the installed server package's receipt and notice projections, and frames derived from the engine's `tool_end` type through both the interactive and the print outputs; it compares the receipt projection byte for byte with the previous reading over the existing samples and a generated pool, and checks that every outward string goes through the display sanitizer. Since 0.110.0 `approvalRequestedBySentence` shares the same name-display path, so one name reads identically in both sentences. |
672
672
  | `scripts/run-display-untrusted-phase-ladder-test.mjs` | The ladder behind the credential side of the display outlet. Since 0.102.0 the rules added in a release run only after the older rules have settled on their own, and then alternate with them until nothing changes, so, for every input, the masked text before any length cap covers at least what the older rules (everything outside the table, including this release's changes to them) mask on their own; not masking less than the previous release is measured on frozen pools, not guaranteed by construction. The gate checks that there is one rule table, that rows are only additive candidate generators read through a single emit point, and that the single-pass reading is gone (the upload check and the settings judge read the same ladder). Against the previous published release (a tarball path in `SEMA_DU_BASELINE_TGZ`; any other version reports a skipped section, not a pass) it checks on frozen pools that the first ladder step, taken on the production path, is byte-identical to the previous release in text and position map wherever this release's base rules did not change, and identical to a variant with an empty table everywhere; that no original character becomes visible again, at the scanner and at the address-reading level (where this release reads a configured address differently from the previous one, it either withholds the whole string or every original character the previous release's reading hid stays hidden, checked by flipping one character at a time and watching which positions of the rebuilt address change, not by the package's own alignment); that exit idempotence does not get worse in seven option groups, nor on any single row in six of them; that clean text is collected once; that folding this release's rows into the first step would expose values on constructed rows; and that the nested value judge and the machine-readable wash agree. Pinned counterexamples: for addresses whose own text already has something to hide (a backslash that would shorten the authority, an encoded `@`, a host the rebuild would lower-case), every credential reading (machine-readable wash, upload check, settings judge, settings notice) answers that it carries a credential and the configured-address output is pinned byte for byte; across the pools, a `scheme://` value whose userinfo the wash hides is never judged clean; the widened scheme-less userinfo reading only adds candidates; both families are idempotent; an anchored form inside a control string stays as-is in the first step; whole-string withholding of an ambiguous or unparseable address is not switched off by marker-shaped or escape-shaped text in the value; a backslash address keeps its own text whenever that reading already hides everything the rebuilt reading would. Also: the same text written twice in one address is never taken as the source of the other copy, a marker or a literal escape outside the authority no longer decides how a backslash address is read (one inside the authority marks it as already displayed and keeps its own text), every pool row's chosen reading is checked against the other one by flipping characters (both directions for backslash addresses; for single-slash and no-slash forms every hidden run must land whole and verbatim in one URL field), the package's own position map between the text and the rebuilt address agrees with that flipping test (surrogate pairs, non-ASCII, upper-case hosts, ports), placeholders and the outlet's own escapes in a backslash address stay where they were, and an anchored form inside a control string stays as-is in the first step in its raw, escaped and percent-encoded spellings. Nine in-gate negative controls (restarting the second step from the original text, dropping the step tags, letting the judge read only the last step, no longer accepting a marker inside the anchored tail, dropping the step tags at each of three local copy points: nested control-string content, nested on-screen and decoded content, a bare query part, dropping them for decoded content alone, and rebuilding a backslash address without first asking whether its own text already hides enough) each turn a cell red. The backslash-pairing switch, including its `'auto'` value, never reaches the scanner's options, and the decision-body profile's blank characters scan exactly like the full blank switch wherever neither U+3000 nor its escape form appears, with the profile idempotent over the frozen pools. |
673
673
  | `scripts/run-session-namespace-key-test.mjs` | Engine namespace key of the session map. The key under which a connected engine's continuity record (engine session id, last task id, sync watermark) is stored is its lowercased origin plus its normalized path: trailing slashes are dropped, path case and percent-escapes are kept as written, and the query, fragment and userinfo are not part of it, so two engines behind one relay origin no longer share a record. A target with an empty path keeps the earlier origin-only key and reads its older entry unchanged; an entry stored under the bare origin is never read for a target that has a path (one reading, no fallback). The self-spawned engine stays `local`; a malformed url, and a url whose origin is opaque, falls back to its raw lowercased string. Cells cover the key shape and spelling variants, role and origin separation, the same-engine comparison an auto-sync no-op check builds on, reading of older records, boundaries (IPv6 hosts, default ports, dot segments, percent-escapes, doubled slashes), the single exported reading, and a never-throws sweep. |
674
674
  | `scripts/run-core-7590-pick-projection-test.mjs` | Readings that follow the engine's 7.59 changes. An MCP server that answers 401 or 403 is now reported with the failure kind `unauthorized` and an `authorization` record (`reason`, optional trusted `authorizeUrl`, optional untrusted `resourceMetadataUrl`); `readMcpAuthorization` reads it in three ways (absent, present but unreadable, a record) from own data properties only, and the wiring-receipt row, the per-server connection reading and the reconnect result carry the record as given or a `authorizationUnreadable: true` flag, never folding unreadable into absent. The `mcp.authorization_required` notice has a fact reader (the `context.image_removed` reader left when the engine renamed that notice to `context.media_removed`; the renamed notice's reader, `readContextMediaRemoved`, still reads the old name for servers that send it); the status frame's `sleptMs` and `waitSegment` reach the retry status; a server name starting with lowercase `remote-` is refused on both config paths while other letter cases are accepted; the ask class `write_protection_on_disk` reads with the same sentence as the `write_protection` mandate; the extension kernel contract is 5; a hook denial carries the denying extension's registered name when known; an MCP server's `startupTimeoutMs` reaches the request on both config paths as given. Names the engine added that this package does not read are pinned to zero code reads. An erase receipt the engine marks partial carries its cause (entries not erased, only leftover stored records that refer to erased entries, or unknown), and the session erase outcome no longer says entries were not erased when only leftover records remain; the decision-conflict reader takes one snapshot of the error's `extra` bag at entry. |
675
- | `scripts/run-server-7137-pick-projection-test.mjs` | Readings that follow the server's 7.137 additions. A `delegation.ask_unresolvable` notice's `remedies` list is read into the existing fact view (kinds from the engine's closed set, an unknown kind or a malformed member dropped on its own, an empty list never minted), with one sentence per way out from `askUnresolvableRemedyContent`; `invalid_session` (a stored session log whose parent chain does not resolve) is treated like `corrupt_session` on every carrier: no retry offer, the same damaged-session sentence, the original code kept; a `resume.constraint_rejected` refusal carries `acceptableForm` as given whenever the refusal includes it; edited rule text is trimmed at its outer edges (and between the tool name and the parenthesis) by one function, `trimEditedRuleText`, before both the live card arm and the precheck port; a plan-review decision carries `checkpointId` only when the gate identity is known and the engine reports `planReviewCheckpointId: true` (through an injected connection, only under the connection's own `capsBaseUrl`, never borrowing the installed target's evidence); `observedSandboxGitCredentialPlane` reads `credentialPlane.sandboxGit` in five states and never reads an absent bit as off; the device lane reading carries `relayUrl` when present; an `inheritEnv` refusal of a request that carried the field, with any value, carries the engine's `reason` word when the field is named, and its sentence states which values the engine accepts instead of claiming the request asked for the whole environment. Session-sync retries stay with the client's retrier, which stops on a terminal permanence. |
675
+ | `scripts/run-server-7137-pick-projection-test.mjs` | Readings that follow the server's 7.137 additions. A `delegation.ask_unresolvable` notice's `remedies` list is read into the existing fact view (kinds from the engine's closed set, an unknown kind or a malformed member dropped on its own, an empty list never minted), with one sentence per way out from `askUnresolvableRemedyContent`; `invalid_session` (a stored session log whose parent chain does not resolve) is treated like `corrupt_session` on every carrier: no retry offer, the same damaged-session sentence, the original code kept; a `resume.constraint_rejected` refusal carries `acceptableForm` as given whenever the refusal includes it; edited rule text is trimmed at its outer edges (and between the tool name and the parenthesis) by one function, `trimEditedRuleText`, before both the live card arm and the precheck port; a plan-review decision carries `checkpointId` whenever the approval card names its gate and does not read the engine's `planReviewCheckpointId` bit (an approval needs its card and is refused locally, with nothing sent, when it has none; a rejection without a card goes out without the key; a gate the engine refuses to recognise is not carried); `observedSandboxGitCredentialPlane` reads `credentialPlane.sandboxGit` in five states and never reads an absent bit as off; the device lane reading carries `relayUrl` when present; an `inheritEnv` refusal of a request that carried the field, with any value, carries the engine's `reason` word when the field is named, and its sentence states which values the engine accepts instead of claiming the request asked for the whole environment. Session-sync retries stay with the client's retrier, which stops on a terminal permanence. The same readings are also checked end to end through errors the real client library mints: the three session-sync pull legs, `invalid_session` on a read and on a submit, and `acceptableForm` on all five decision legs. |
676
676
  | `scripts/run-pkg-policy-registration-test.mjs` | Package policy registration. The package's own `package.json` is checked, item by item and by equality, against its row in the cross-package policy table: the package-name sets of the four dependency kinds (version ranges are not compared; one name more or fewer is red and the difference is printed), the `exports` key set, the pinned version (equal to the `version` field or to the highest released CHANGELOG section, so both sides of a version bump are legal, and a bump the table did not follow is red), and the first sentence of `description`. The table is taken from `SEMA_PKG_POLICY_PATH` when that is set (no fallback), otherwise from a sibling checkout found by walking up from the package root; when it cannot be reached the whole gate reports a skipped section and exits 0, which is not a pass. A negative control adds one unlisted dev dependency and expects the dependency check to go red |
677
677
  | `scripts/run-core-7600-pick-projection-test.mjs` | Readings that follow the engine's 7.60 changes. The MCP failure kinds include `host_refused` (a host-supplied pipe did not send the request; safe to retry, no liveness reading), and the wiring-receipt row and the per-server connection reading carry it like any other failure word. A `status` frame's `seat` (which seat's model call the frame is about; absent means the main loop's own call) reaches the retry status through both projection layers, open set, malformed values dropped on their own, never minted when absent; a frame about another seat is not labelled with the session's model, offers no switch-model recourse, and in `-p` stream-json takes the `_sema_model_status` frame with `seat` instead of the `api_retry` frame. The single-image size limit used by the human-turn encoder is generated from the engine's image admission table, with the same value as before. The cost breakdown type no longer names the line the engine removed, while older engines' line still passes through. The extension kernel identity reading reports contract 6 for the 7.60 kernel; the kernel the `./mods` subpath passes through and the engine's own kernel carry the same contract number and event table, with the four new stations defined and `tool.call` failing as `refuse`. Engine names this package does not read are pinned to zero code reads. |
678
678
  | `scripts/run-core-7610-pick-projection-test.mjs` | Readings that follow the engine's 7.61 changes. The notice `context.image_removed` is renamed `context.media_removed` (images and documents read the same way): `readContextMediaRemoved` reads its facts (`reason` as given, the image and document counts, the session and run ids) or nothing, never throwing, each field read once; the old name is no longer in the engine's own catalog, but servers up to 7.142 still send it, so this package keeps recognizing it as a user notice and reads it with the same reader (cause `rejected_by_provider`, no documents). Four new notice codes are in the catalog with the engine's audiences, and `classifier.seat_rate_limited` is now a user notice. The extension kernel contract is 7 (the four `session.*` rows gain stations). The engine no longer ships the TaskOutput tool; a `task-output` card from an older engine still reads as a rich result. Names the engine added, renamed or removed that this package does not read are pinned to zero code reads. |
@@ -686,7 +686,7 @@ guard still cross-checks the table by name).
686
686
  | `scripts/run-background-task-table-projection-test.mjs` | Background-task table rows after a compaction, read the way the engine mints them: a row is the `- [<id>] Task "<description>" ` prefix, a status phrase, and at most one optional tail that starts with ` — ` and runs to the end of the line. The tail is projected verbatim onto the optional `read` field of the task-status attachment (absent means the engine sent none — never an empty string), rows without a tail stay byte-identical to the previous release (a frozen copy of the old reader and a seeded random pool of 4000 batches prove it), a `parked` row is recognised with or without a tail, a second ` — ` inside the tail stays part of it, and unknown phrases and the `(+N more …)` footer line are still not recognised. The status phrase is always read after the last `" ` (quote plus space) on the line, so a description that itself contains a phrase never turns an unknown status into a known one; a table whose row description breaks across lines (an opening quote never closed on a line that is not the last non-empty one) is not read as rows at all and falls back to the generic row. The engine's own table renderer is driven for real, status by status and read form by read form. |
687
687
  | `scripts/run-permission-mode-verb-projection-test.mjs` | Switching the permission mode of a running turn. `setRunPermissionMode()` sends the switch only when the permission-mode write capability reading is present and true; otherwise it answers `unavailable` without sending anything. The outcome is always a value, never a throw, and only `switched` carries a mode: the mode now in force is the engine receipt's `permissionMode.effective`, not the word that was sent. A fold by this deployment's floor or the task's settings is reported in `admission`, kept apart from the engine's own fold. `persisted: false` comes with a reason and a notice that a later resume returns to the earlier mode. Rejections are classified by status and machine code (not found / not running / refused / not available); a 500 `corrupt`, which the server sends before the switch reaches the engine, is a refusal, while other server errors and lost connections stay unconfirmed; an answer that does not match the documented seven-key shape, or contradicts itself, is unconfirmed rather than read as a switch. `runPermissionModeSwitchLine()` gives one sentence per outcome in English or Chinese. A 501 answer's machine code rides on the `unavailable` outcome (open set, carried verbatim) and the two known causes each get their own sentence; `permissionModeTitle()` maps an engine mode word to the name shown in the interface, and the switched sentence uses it. |
688
688
  | `scripts/run-run-ledger-snapshot-test.mjs` | Run-ledger snapshot reader. `readRunLedgerSnapshot` reads, for a run parked at a gate, the highest ledger sequence number, the parked call's own `tool_start` frame and, optionally, a fold over the numbered frames. It answers only when three pieces of evidence agree — the reader proved the end of the stream, the tail sits next to a park marker, and two concurrent passes end on the same number — and the run is parked both before and after the read; otherwise it answers `null`. A pass cut short by the server's draining frame is not a completed read and is re-read on the package's draining ladder. A fold that depends on frame order rejects a pass whose numbers go backwards or repeat, or that carries an unnumbered frame the fold cares about, and both passes must fold to the same answer. All readers are injected, nothing throws, and the budget is a hard wall-clock limit: a parked-state read or a ledger iterator that hangs and ignores the abort signal still yields `null` when the budget runs out. |
689
- | `scripts/run-approval-frame-live-facts-projection-test.mjs` | Approvals replayed after a disconnect or adopted after a server restart arrive without the three out-of-card live facts (mandated flag, mandate word, origin word). When all three are provably absent on a v1 opening frame, the card records them as unreadable, so the automatic-answer verdict says unknown on every lane and the hook scope is deny-only; a session-allow rule can no longer answer such an approval. Frames that carry any of the three, legacy-family frames, and frames the type guard rejects are unchanged. Evidence that already proves the question must be asked is read before the unreadable record, so such cards still answer must-ask and the hook still skips. Cells run real server captures through the whole-frame export, the live-card entry and the suspended-row entry and expect the same facts and verdicts from each. |
689
+ | `scripts/run-approval-frame-live-facts-projection-test.mjs` | Approvals replayed after a disconnect or adopted after a server restart arrive without the three out-of-card live facts (mandated flag, mandate word, origin word). When all three are provably absent on a v1 opening frame, the card records them as unreadable, so the automatic-answer verdict says unknown on every lane and the hook scope is deny-only; a session-allow rule can no longer answer such an approval. Frames that carry any of the three, legacy-family frames, and frames the type guard rejects are unchanged. Evidence that already proves the question must be asked is read before the unreadable record, so such cards still answer must-ask and the hook still skips. Cells run real server captures through the whole-frame export, the live-card entry and the suspended-row entry and expect the same facts and verdicts from each. Since 0.109.0 the hook lane on those frames answers `unknown` with scope `deny_only`, as on every other card. |
690
690
  | `scripts/run-project-seed-projection-test.mjs` | Seeding a handed-off session with a project package (a gzip tar of a project folder that has no git repository). `projectSeedGateOf()` decides whether a submission carries the package: only the hand-off arm, only when the target deployment reports an isolated execution lane, and only once an upload id is in hand; the mirror and side-car arms never carry it, and a lane the deployment does not describe is not treated as isolated. `projectSeedRefusalOf()` reads the four refusal codes (an invalid package at upload; a lane that is not isolated, a session that already has its seed, or an upload that belongs to another session at submission) with their machine-readable fields as sent, and `projectSeedRefusalContent()` / `projectSeedGateContent()` give one sentence per outcome in English or Chinese, with no codes or ids in the text and the problem path shown as untrusted text. Checked against the server's own lane table, bundle reader and refusal bodies through the real client. |
691
691
  | `scripts/run-support-range-admission-test.mjs` | A request key reaches the wire only when every server inside the declared support range accepts it. The check reads the support floor, takes every fixture server at or above it as a witness (the floor server itself and the current one must both be present), fills every input of both MCP entry paths and of the request assembler, and requires the keys actually sent, including the transport sub-keys, to sit inside every witness's own acceptance tables, while the request's top-level keys must pass every witness's body-key check. A key that only newer servers accept must instead be gated on a capability flag the server mints, and such keys are proved to be omitted while the flag is absent. Missing or empty acceptance tables, a missing floor witness and a planted extra key all fail the check. |
692
692
  | `scripts/run-server-7143-pick-projection-test.mjs` | What the client reads from the server's newest approval and memory-sync facts. Approval decisions always echo the two binding fields of the pending row, and the session-scoped remember fallback only answers to refusals that are specific to remembering. A sub-agent approval card names its source session under the current key, falling back to the older key only when the current one is provably absent. A replayed approval whose live facts are unknown to the server is treated as unreadable, whichever frame family carries it. Rows in the operator approval queue are split by family first: only checkpoint rows feed the snapshot, counts, routing and decisions, while held cross-session messages appear separately as read-only rows; a row whose family cannot be read is never decided, yet it is not treated as absent either, so it cannot make a waiting approval look settled. `sessionMemorySyncStateOf()` and `sessionMemorySyncNotice()` turn a session memory-sync trigger outcome into not configured, reported, or unobtainable with a reason, never an empty report; a report body that cannot be read is no answer at all. Checked against the server's own card, replay-frame, queue-kind, refusal-body and memory-report mints through the real client. |
@@ -699,16 +699,53 @@ guard still cross-checks the table by name).
699
699
  | `scripts/run-core-7630-pick-projection-test.mjs` | Readings that follow the engine's 7.63 changes. The notice catalog matches the installed engine code for code (at least the 117 codes of 7.63): three new codes are listed with the engine's audience and no reader of their own, and `session.records_skipped`, which the engine dropped from its catalog while servers inside the supported range still send it, is still treated as listed with audience `user`, through a single table of such codes that the engine catalog never overlaps. The two memory-harvest notices count as one occurrence per harvest report. A parked row the engine cannot read is listed without a gate kind and with a damaged verdict: the verdict is read as the engine gave it, the row never becomes an approval card, and no default gate kind is invented. A tool approval parked before the engine bound the tool name into its hash is read as damaged, a row that does not match its reading rather than an engine fault. The six workspace refusal codes keep the families they already belong to: the two `config.` codes are configuration refusals, none of the six offers a retry, and a failed agent card carries its code and sentence unchanged. A background fork child's delegation link is read like any other background child's. |
700
700
  | `scripts/run-core-7640-pick-projection-test.mjs` | Readings that follow the engine's 7.64 changes. The notice catalog matches the installed engine code for code (118 codes); `delegation.delivery_discarded` is listed with the engine's audience (operator) and no reader of its own. The four refusal notices that can come from a delegated sub-agent (`delegation.ask_unresolvable`, `permission.ask_released_by_mode`, `permission.read_deny_unapplied`, `classifier.unavailable`) say where the call ran: in this session, in a named sub-agent session, or unreadable when the sub-agent fields are present but malformed, in which case the call's session is unknown rather than taken to be the tree root; `noticeCallSessionOf()` answers that session. A gate record's denial carries the engine's echoed request segments verbatim, in order, with a segment dropped only when a required field is unreadable. `catalogDefaultModelVerdict()` asks the engine whether the default model the catalog resolves to can run a task that names no model, handing over the caller's model face by reference without reading it, and refuses with `config.default_model_missing` carrying the engine's own code; with the engine port missing it answers unknown. Checked against the installed engine's own catalog, gate-record screen and model-face question. |
701
701
  | `scripts/run-seat-computer-use-switch-test.mjs` | Host switch for computer use. Whether the host currently allows computer use, where that value came from, whether an environment variable locks it, and when a change takes effect are answered once, by the host that carries the feature. The seat capability table gains `computerUseSwitch`, reported `true` only by a host that really carries computer use; a browser lane (no seat store) and the terminal never report it, and an absent capability means the switch is not rendered at all, not that it is off. `getComputerUse()` returns `{ enabled, source, lockedByEnv, appliesOn }`; `setComputerUse()` takes `{ enabled: false }` or `{ enabled: true, confirmed: true }` and returns `{ ok: true, changed, state }` or `{ ok: false, reason, state }`, a refusal being a receipt rather than an exception. The words are three frozen tuples: sources `env` / `settings` / `default`, refusal reasons `locked-by-env` / `confirmation-required` / `invalid-request` / `write-failed`, and the effect point `next-engine-start`. The request is untrusted input: `checkSetComputerUseRequest` accepts exactly the two shapes, and `planSetComputerUse(request, lockedByEnv)` decides, in the order malformed shape, then lock, then missing confirmation, whether the host may write; a lock flag that is not strictly `false` counts as locked, and the function never throws. Both outbound gates are closed shapes that also refuse two contradictions: a state locked by the environment must name `env` as its source, and a `locked-by-env` refusal must carry a locked state; the converse, and the locked flag on a successful receipt, are left to the host. Each key of the request is read once and the value judged is the value used. The suite checks registration, both outbound gates key by key, the inbound judge, the planner against an independent reference over a generated pool, the capability reading (including the browser lane and the bare name `computerUse`, which is not the capability), and an end-to-end host double that passes every receipt through the same gate. |
702
- | `scripts/run-server-7144-pick-projection-test.mjs` | What the client reads from the server's newest session, approval-queue and live-text facts. Each retired, renamed or added server fact has exactly one reading: an older notice or error code that servers in the supported range still send keeps its own reader until the support floor moves past it, and the new code beside it is never folded into the old view or the other way round. Two new error codes are recognised and passed through unchanged. A cross-session message held for review shows an expiry time only when the server sends one; it is never computed from the hold time. A resume refusal tells the engine's row judge tripping over a row (an engine fault) apart from a row that does not read the way it says it should. Text that arrives in several blocks lands as one transcript row per block, and a credential split across two blocks stays out of the transcript on servers that mask it. The user-facing sentence for a refused fork or log replacement on a session archive that still carries an earlier version's owner stamp is read by code only (the generic conflict of earlier servers does not match), is worded in English and Chinese word for word from the server's fixed sentence (compared against the server fixture when present), and carries no machine code. |
702
+ | `scripts/run-server-7144-pick-projection-test.mjs` | What the client reads from the server's newest session, approval-queue and live-text facts. Each retired, renamed or added server fact has exactly one reading: an older notice or error code that servers in the supported range still send keeps its own reader until the support floor moves past it, and the new code beside it is never folded into the old view or the other way round. Two new error codes are recognised and passed through unchanged. A cross-session message held for review shows an expiry time only when the server sends one; it is never computed from the hold time. A resume refusal tells the engine's row judge tripping over a row (an engine fault) apart from a row that does not read the way it says it should. Text that arrives in several blocks lands as one transcript row per block, and a credential split across two blocks stays out of the transcript on servers that mask it. A block the engine delivers only as its end frame shows up in the live transcript between its neighbours, with the same text a reloaded session shows on servers that include it in their stored rows (older servers leave it out of the stored rows). The user-facing sentence for a refused fork or log replacement on a session archive that still carries an earlier version's owner stamp is read by code only (the generic conflict of earlier servers does not match), is worded in English and Chinese word for word from the server's fixed sentence (compared against the server fixture when present), and carries no machine code. |
703
703
  | `scripts/run-frame-arm-completion-projection-test.mjs` | Two frames the server sends in normal operation no longer count as dropped frames. The single-card close frame of the in-stream approval family (`approval_complete`) goes through the same envelope reading as the open and revoke frames and reaches the host as an `approval_complete` chrome event carrying the frame as-is, whose stream context the existing approval frame context reader answers. The session title notice (`session_titled`) becomes a `session_titled` chrome event with the session id and title, read as own non-empty string fields; the session row stays the only source of the title. Approval-card frames (open, revoke and close) never count as proof that a submission created a run: the delivery ledger and the event leg's refusal detection read one shared check, so a busy refusal that follows a replayed card still reaches `onRefused`. Malformed copies are still reported as `malformed`, frames the projector does not know are still reported, and both events stay on the host side of a seat. |
704
704
  | `scripts/run-plan-review-decide-card-projection-test.mjs` | Plan-review decisions answer the card that was shown. The gate a plan-review decision answers has one source: the card facts that come with it. The package's own card and a reopened card hand over their own facts; the public decision call reads the `card` option. That gate identity goes into the decision body and is the gate the result is compared with afterwards. Approving needs a card that offers approval (a plan shown as written, with a gate identity); without one nothing is sent and the result says why, in the card's own words when the card withheld approval. If the gate moved on before the decision arrived, the engine refuses it and the result says the plan on the card is no longer the one waiting, without reopening anything. If the engine cannot recognise a gate identity that is still current, the result says so and the next decision on that gate is sent without it. The gate drives the public decision call, the package card and a reopened card against an injected client: missing and malformed cards, a card for another task, withheld cards, a moved gate, the unrecognised-gate path and its alternatives, the bounded set of unrecognised gates, the public types, and that the reason is set at most once per decision and never reaches the public result object. |
705
- | `scripts/run-session-engine-lane-projection-test.mjs` | Which engine a session lives on, decided once when the session starts. `sessionEngineBindingFor` turns the engine a host is wired to into a fixed identity key (a versioned JSON tuple for the host's own engine, for a declared engine by center, space and name, or for a direct address) and fails with a reason instead of guessing when a piece is missing; `commitSessionEngine` is the only writer of the session's engine snapshot (first write, identical replay, a different value refused, an older record restored only on proof) and raises a notice when the host's own engine is recorded as a Cloud session; `readSessionEngine` only reads the snapshot; `sessionDispatchTarget` picks the target for a session from that snapshot and the host's wiring snapshot and refuses with one of four reasons instead of falling back to the current default; `foldSessionPlacement` keeps a session's execution lane ordered by the device revision. The seat session row carries `engine` and `executionLane`, and the seat capability `engineModeDefaultOnly` says that switching the engine only changes the default for new sessions. The dispatch refusals also include `session-ambiguous` (the same session id sits on more than one engine's ledger); the host judges it and takes the refusal from `sessionDispatchRefusedFor`, and the dispatch judgement itself never answers it. A new session's lane intent (`start({ engine })`) uses exactly the two words a session's engine `kind` carries and the binding commit accepts. |
705
+ | `scripts/run-session-engine-lane-projection-test.mjs` | Which engine a session lives on, decided once when the session starts. `sessionEngineBindingFor` turns the engine a host is wired to into a fixed identity key (a versioned JSON tuple for the host's own engine, for a declared engine by center, space and name, or for a direct address) and fails with a reason instead of guessing when a piece is missing; `commitSessionEngine` is the only first writer of the session's engine snapshot (first write, identical replay, a different value refused, an older record restored only on proof; the one other writer is `repointSessionEngine`, the session half of the one-time engine re-point) and raises a notice when the host's own engine is recorded as a Cloud session; `readSessionEngine` only reads the snapshot; `sessionDispatchTarget` picks the target for a session from that snapshot and the host's wiring snapshot and refuses with one of four reasons instead of falling back to the current default; `foldSessionPlacement` keeps a session's execution lane ordered by the device revision. The seat session row carries `engine` and `executionLane`, and the seat capability `engineModeDefaultOnly` says that switching the engine only changes the default for new sessions. The dispatch refusals also include `session-ambiguous` (the same session id sits on more than one engine's ledger); the host judges it and takes the refusal from `sessionDispatchRefusedFor`, and the dispatch judgement itself never answers it. A new session's lane intent (`start({ engine })`) uses exactly the two words a session's engine `kind` carries and the binding commit accepts. |
706
706
  | `scripts/run-park-row-card-leg-projection-test.mjs` | Durable pending rows that the engine says it cannot continue: a row whose `rowVerdict` is not `ok` (written by a newer engine, bytes this engine does not resume, an unknown verdict word, or an unreadable verdict) is not shown as an approvable or answerable card and no decision is sent, on both card legs (durable fetch and question-card reopen triage) and in the pending-row count; the outcome carries the verdict and the engine's own sentence, and the fail-soft closing sentence no longer says to decide on the card. Rows with an `ok` verdict and rows from servers that do not send a verdict behave as before. Also pinned: the one way-out sentence shared by the unbound-row refusal and the local `binding_missing` stop, and the synthesized card frame carrying `sourceSessionId`. The same boundary applies when a host hands the decide or answer entry a row it fetched itself, when the verdict position is an accessor (it is not run), and to rows whose gate the engine cannot read (listed by newer engines with a non-`ok` verdict), while rows on a gate this leg does not own are still left to their own leg. Pinned on rows read by the real checkpoint store of two server generations and on rows summarized by the installed engine. When a reopen is refused because this engine cannot continue the parked question row, the self-heal row carries the engine sentence and the cannot-be-decided-here way out, never the resume or decide-on-the-engine way outs. Since 0.108.0 the `settled` verdict of the question-card triage and of the bounded re-check loop also carries `rowTaskId` (the judged row's own run id, present only together with `rowVerdict`; a sole child-run row resolved by fallback yields the child's id, never the requester's; `unborn` / `row` verdicts never carry it), and the root entry exports `checkpointRowBarOf` / `checkpointRowBarSentence` / `CheckpointRowBar` (engine sentence first, package sentence when unreadable). Since 0.108.0 the way-out sentence follows the pending row's own expiry position: with a finite `deadline` it still says to cancel the run or let the approval expire, with `deadline: null` (no expiry timer) it says to cancel the run and that this approval has no expiry timer, and when the position is absent or unreadable it only says to cancel the run; the decide-refusal receipt and the local `binding_missing` stop have no row in hand and always use the cancel-only sentence, the bridge stop reads the row's `deadline`, and the self-heal row reads the optional `_sema_rowDeadline` the host hands next to `_sema_rowVerdict`. Pinned on three states in both languages, a reader table that never runs accessors, and the host verdict in eight shapes. |
707
707
  | `scripts/run-steer-input-ids-projection-test.mjs` | Steered input ids reach the host, and inputs the engine reports as unused are matched by id. The only id a steered input has is the `messageId` the server puts on the steer receipt; the client never mints one. Every steer path in the package that holds a receipt hands that id to the host: the control router's `steer()` returns the receipt as received for live (200) and parked (202) deliveries, and the self-heal path puts it on the `running-steered` outcome (absent when the receipt carries none). `readUserInputUndrained()` reads `detail.inputIds` only when the list names exactly the counted inputs, and `undrainedInputsVerdict()` matches it against the host's ids: an id the host sent and has not yet resent under the same id is resent, an id already resent once is handed to the user, and everything else (an unnamed notice, the server's crash report, a sub-agent copy, or ids the host never sent) is reported by count only. If the host's record of resent ids cannot be read, nothing is resent. When the child-session field on an undrained-input notice is present but unreadable, the id list is withheld and nothing is resent; a notice for this session is still resent by id. |
708
708
  | `scripts/run-hooks-unread-keys-projection-test.mjs` | Hook settings carry only the keys the server's hook reader reads. A matcher group or hook entry that also holds a key the reader does not read (a failure policy, rewake text, a cloud placement, arguments on a non-command hook, a misspelled group key) is not sent: the server refuses a whole new request for such a key and would otherwise drop it on replay, so the key never silently stops meaning what it says. Which keys the reader reads comes from the same settings-contract classifier the server uses (its projection of each piece) plus the server's own wire keys, with no field list of the package's own. Settings hooks are recorded with the reason `key_not_read` and the coordinates of every unread key, plugin hooks get the same reason; a hook that will then run nowhere is listed in the hooks-not-run notice with coordinates in the same form as the server's `unknownKeys` and the way out (remove the key, or use a server that reads it). The turn is never refused for this, managed settings included. When the build-time copy of the classifier is newer than the connected server's own, a per-generation table of the server's reader also removes the entry keys that generation does not read (a server version that is unreported, unreadable or newer than the table is treated as not reading them), and a dropped failure policy adds a sentence that a hook set to block on failure will not block. Checked against a real server admission path: the projected body is accepted, the body as written is refused, and the package's coordinates equal the server's list. |
709
709
  | `scripts/run-task-notification-admission-test.mjs` | Wake-up submissions for task notifications are confirmed only on positive evidence. `taskNotificationAdmissionOf` classifies one read-back of the run bound to a submission key as `admitted`, `pending`, `not_found` or `unreadable`, with a reason from a closed set of eight; the verdict follows from the reason. The read-back is passed in one of three explicit forms: the 200 record, a not-found form the caller passes only after recognising the endpoint's own 404, or an error form for everything else. `admitted` needs either an ISO date-time in the record's `admittedAt` (a field the service has not shipped yet; read when present, and `null` is not taken as a no) or an integer `result.stats.turns` above zero, the same reading the service uses to latch acceptance. A running row however long it has run, zero-turn completed, failed, parked or blocked rows, rows with token usage but no turns, and missing or unknown status words or codes are all `pending`; the status word, error code and token count are never read. Errors are never inspected: any error form, a raw error object, a malformed body or an unrecognised input is `unreadable`, never `not_found`. Only own data properties are read, accessors never run, and the function never throws. `batchCompatible` now refuses to batch a task notification whose fact carries a completion id, whether it is the head or the follower, so each such notification goes in its own submission; the host passes the queue item's notification fact through on the batching entry. The suite covers the two pause points before placement and before engine output, the point between a zero-work refusal being written and its key being released, reads counted through instrumented accessors and proxies, the reason and verdict sets in both directions at compile time and at run time, deliberately broken judges that must fail the matching cells, and the record shape against the type package's declarations. |
710
710
  | `scripts/run-hooks-authored-source-projection-test.mjs` | Hook group sources on the wire are set only by this package. The server runs each hook matcher group in a phase chosen by the group's `source` (a managed-policy source runs in the final managed phase), so a `source` written in a settings file or in a plugin's hook manifest would let a project, user, local or unmanaged-plugin hook claim a managed phase. Any `source` written there, whatever its value (even one that matches where it came from), keeps that matcher group off the request: settings hooks are recorded with the reason `source_not_authored`, plugin hooks get the same reason, and a hook that will then run nowhere is listed in the hooks-not-run notice with the coordinate `settings.hooks.<event>.<group>.source` and the way out (remove the key). The key is never stripped or replaced with another value. A group whose `source` cannot be read plainly (an accessor, a proxy trap) counts as written. Pieces the settings classifier does not accept are left to it and to the pre-send check, so a malformed guard still refuses the turn. Every request body the suite builds is swept: no matcher group carries a `source`. Checked against a real server's hook reader: the body as written puts the hook in a managed phase, the projected body leaves every group unmanaged. |
711
711
  | `scripts/run-seat-actions-test.mjs` | The seat action channel. The host can ask the window it hosts to do one of a closed set of things over one one-way event, `onSeatAction`: open the new-project dialog (`new-project`), or open a fresh new-session page with text placed in the prompt box and, optionally, a folder for the next session (`prefill-composer`), which is never sent on the user's behalf and never replaces a draft the user has already typed. Focusing a session is not an action word: it already travels as the `focusedSessionState` store push, and there is no route-opening word. The window first claims the words it really handles with `claimSeatActions`, and the host offers an entry point (a menu item, a link route) only for claimed words, so a host never shows an entry that does nothing. A claim may only name words from the closed set: one word outside it refuses the whole claim and names that entry, repeated words are harmless and are collapsed in closed-set order, and a malformed claim reads as an empty claim. The outbound gate is a closed shape per action (an unknown action word, an extra key, an over-long or control-character text, a bidirectional-override character or a lone surrogate is refused by name; accessors are never run and the gate never throws), and the window-side reader `readSeatAction` reads an unknown word as `unknown` (dropped, leaving a trace) rather than as malformed, and never throws. One optional capability bit `seatActions` governs the claim verb and the event; a host that does not report it reads as `not_reported` and the window neither claims nor subscribes. The suite checks the registration, both gates and the reader key by key, the capability reading, and an end-to-end host double that sends only claimed words. |
712
+ | `scripts/run-approval-frame-reading-test.mjs` | Why an approval request was not read as the format this client knows. `approvalRequestFrameReadingOf(frame)` answers `{ v1: true }` when the stream's approval request passes the SDK's own v1 guard, and otherwise `{ v1: false, why, schemaVersion?, kind? }` with one of three causes in the guard's own order of checks: `version_unknown` (the schema version is not the one this client knows), `kind_unknown` (the version is known but the request's kind is a word outside this client's vocabulary), or `unreadable_fields` (the discriminating fields are known but a required or present optional field is malformed). The version and kind values are read once, cleaned for display and length-capped; reading never throws, and a frame that cannot be read at all is `unreadable_fields`. The generic card produced for such a frame now carries the cause, and its first reason line says which of three things happened instead of one catch-all sentence; the cause is presentation only and never changes the automatic-answer verdict. This gate checks the three causes and their order against the SDK guard, the single read of each discriminating field, the sanitised values, the never-throw contract, the card key and the three sentences in both languages, and the live approval path end to end. |
713
+ | `scripts/run-parked-unresumable-projection-test.mjs` | A session row says which pending rows this engine cannot continue: the seat session record carries `parkedUnresumable`, one entry per pending row that belongs to the session, whose engine verdict is not `ok` and that would otherwise have gone to the approval card leg. It is read together with `parked` (`parked: true` means another decidable card is open; `parked: false` asserts no pending row at all, so the outbound gate refuses the pair), and its absence asserts nothing. There is one minting entry (`parkedUnresumableRowsOf`, fed the approvals list body as returned: family and gate are read the same way as the durable fetch, ownership by the same judge as the card, an unreadable body throws and a single unreadable row is skipped alone), one reader (`readSeatParkedUnresumable`, the same closed shape as the outbound gate, never throws) and one sentence entry (`parkedUnresumableLine`: the verdict sentence, then the way out, in English or Chinese). Pinned: the outbound gate shape by shape with the field path named, a minting matrix row by row, complementarity with the durable fetch, the identity-named fetch and the question triage over sixteen row forms, golden sentences, the subscription digest now including the gate kind, and an end-to-end host double in which the seat lane and the browser lane produce the same line. |
714
+ | `scripts/run-core-7660-pick-projection-test.mjs` | Readings that follow the engine's 7.65 and 7.66 changes. The notice catalog matches the installed engine code for code (119 codes); `config.legacy_tool_name` is listed with the engine's audience (user) and no reader of its own. `readDeliveryDiscarded()` reads a discarded-delivery notice: the task, its type and the session it was addressed to, plus the seat it would have reached only when that word is in the engine's own list, never guessed. An input that a person did not write carries the engine's author word and, for an extension, its registration name, on both the projected frame and the chrome event; with no author word the input is the person's and no author is invented. An attachment an extension refused names that extension, cleaned for display. An erasure receipt is read over the engine's nine carrier classes, and a class the receipt does not list reads as unverified, the same way the engine's own status rule reads it. The mods kernel contract read by `modsKernelIdentity()` is 9. Checked against the installed engine's own catalog, notice mint, carrier list and status rule. |
715
+ | `scripts/run-local-engine-judge-projection-test.mjs` | The local engine judge. On one machine and one configuration folder there is at most one local engine, and every local client attaches to it. One reader takes the discovery record by its shape only; a damaged record counts as no record, and the time the record was written is never taken as the time the engine process started. One judge then gives a closed verdict: start one, attach, attach while a newer client waits to replace it, this client is behind and must update itself, the version cannot be read, the engine is draining, something else answers on the port, or the answer cannot be verified. Versions are compared by the package's one release-version comparison and instances by its one identity comparison, so a pre-release or malformed version never reads as newer, and an identity that cannot be compared never authorises anything. The guard also covers the verdict-to-lane projection, the per-group configuration drift reading and the verdict sentences in both languages. |
716
+ | `scripts/run-local-engine-occupancy-projection-test.mjs` | Who is using the local engine, and what that means. Other local clients are counted from the shared registry folder; an unreadable record or folder, or a record whose status is present but not one of the three known words, makes the count unknown rather than smaller. Background work is read only from a fleet-wide observation stream bound to the current engine instance; a stream filtered by owner or by session, one that has not delivered its first snapshot, or a row with an unknown status makes the reading unknown. Durable parked work does not count as busy; open approval cards on screen do. Background shells kept in sessions are counted from the engine's per-session registry, excluding services explicitly kept running, and an unreadable, superseded or capped listing makes the reading unknown. Idle requires every reading to be known and zero, so unknown never reads as idle. Whether this client needs the local engine is derived per lane, and never while the principal it would use against that engine cannot be read. The guard also covers the replace-engine prompt (its facts, its fingerprint, which always includes the target instance, and its sentences, which always explain what happens to parked work), the automatic-replacement rule, and the exit outcome, which never stops the shared engine. |
717
+ | `scripts/run-local-engine-spawn-env-projection-test.mjs` | How a local engine is started, whichever client starts it. One table fixes the engine's environment: each row is a literal, a deletion, a path the host supplies for the configuration folder, or a value derived from the rows before it, and only rows marked overridable let a non-empty inherited value win. The host may add only five named start-up bits; anything else it passes is refused and listed. The same assembly yields four wait budgets (data-root lock wait, health wait, stop bound, start-lock wait), 80, 260, 120 and 420 seconds by default. The single-user operator declaration is derived by one function, and the table's derived row injects exactly what that function returns. Settings are layered from the start-up environment through the user, start-up-file and managed layers in that order, with start-up-pinned keys never changed by settings and, on Windows, every key the assembly reads or writes by name (table keys, host bits and the keys the declaration is derived from) stored in upper case whatever spelling a layer used, and a managed layer that cannot be read stops the start unless a last good copy is supplied. The principal used against the local engine comes from settings only; an unreadable layer makes it unknown rather than the default. Start-up refusal reasons are read from structured log lines, scrubbed before parsing and again on each whole parsed field before any prefix is stripped, and truncated last, so a secret split by truncation never leaks. An environment block that exists but whose keys cannot be listed is treated as an unreadable layer (a managed-policy block refuses the start), never as an empty block. |
718
+ | `scripts/run-local-engine-anchor-shape-test.mjs` | The shape of the local engine's discovery record, checked against the host library that writes it. The package reads that record by structure, without importing the host library, so this guard compares the two: every key the package reads must exist in the host library's record type with a compatible type and must not be optional there if the package requires it; keys the package already reads ahead of the host library are listed and turn red once the host library adds them; and the package's older, deprecated copy of the record type must not drift from the host library's until it is removed. The list of keys the package reads is taken from its source, so a new read without a matching entry fails. When the terminal source tree is not available the comparison is reported as skipped, not passed. |
719
+ | `scripts/run-server-7147-pick-projection-test.mjs` | What the client reads from the server's two newest releases. `readEngineHealthFacts()` adds three optional readings to its existing pieces: the engine's first-attach grace window under `engineLease`, an `inflight` piece holding the server's total and, key by key, its per-lane breakdown (a server that sends only the total yields `{ total }`; the `sessionResident` key being present is what says the total already counts session-resident shells, since the bare total of older servers meant something narrower), and whether a service credential without a principal acts for the deployment under `auth`; a piece whose new field is present but malformed is dropped whole, like its existing optional fields. `readCcImportRedeemCounts()` reads the engine's refusal code and sentence on refused rows with the same row reader as rows whose outcome is unknown. Three submit refusals get readers and sentences matched by code and machine fields only: credentials in `settings.env` (naming the variables, never their values), a deleted session id that now runs only for its earlier owner, and the per-deployment cap on runs in flight (with the wait in seconds). Exercised against the server's own `/health` route, its rule-import lane redeeming a ticket minted by an older server, its credential judge and its cap refusal, through the real SDK error mapper. For the 7.147.0 release build the gate also pins the fixture to the registry bytes (lock integrity, no repack marker, bundled engine and settings-contract pins, the shipped wire-change files, the module only the release has), and checks that a request member the server's readers cannot read is refused (400 on submit, 409 on resume) with a reader and a sentence that name which part it is (hook settings, environment variables, another setting, an MCP server's connection settings, an A2A peer's headers), never claiming the neighbouring refusals that carry their own readers; that the plan-review prepare step applies a restatement before judging the stored body while this client never sends one; that the `none` execution-lane word is read at each place the server reports a lane and nowhere it does not; and that all 79 additions in the shipped wire-change file have a disposition. |
720
+ | `scripts/run-seat-subagent-alias-projection-test.mjs` | The additive `subagent_alias` chrome event — the cross-process carrier for the "delegating card id → subagent task id" alias. Subagent content frames carry only the delegating card id, so when the adapter runs in a seat host's main process the renderer's own ledger never learns which task a card belongs to and the subagent detail view reads empty. The adapter registers the alias in exactly two places (the async-launch receipt and the per-subagent progress tick); each now emits the event in the same step with the same arguments on every call that the registration port did not refuse (never consulting the ledger, so adapter output stays a pure function of the frame sequence), and the renderer registers the same half into its own ledger through `readSeatSubagentAlias` / `noteSeatSubagentAlias` — no second copy of the receipt/tick judgement. The guard drives the real adapter, then feeds the emitted events into a second, empty ledger (a separately instantiated ledger module) and checks that content published under the card id lands under the task id, that the `content` scope never writes the steer-target half while `full` does, that content arriving before the alias is migrated, and that a malformed event, an unknown scope word, a degenerate pair, a workflow-lane tick or a non-receipt frame registers and emits nothing; feeding the same frames twice yields byte-identical output, and de-duplication happens only on the renderer side (a second registration of the same pair is ignored). The renderer-side reader also checks the lane proof (it must name the subagent lane and the same delegating card as the payload — the seat's outbound gate only checks the proof's general shape), and the progress-tick form carries the frame's own cycle number as order evidence so that, after a renderer reload, a late tick of an older engine id cannot displace the newer card as the steer target. |
721
+ | `scripts/run-engine-notice-user-codes-test.mjs` | Notice sentences for thirteen user-facing engine notices, and one shared sentence for the durable-approval notice. `durableGateUnavailableSentence(facts, lang?)` turns the facts read from the durable-approval-unavailable notice into one sentence for all three clients. The notice records whether a live approver is attached but not how permission requests are handled when there is none (the engine may approve them without judgment or refuse them), so the sentence says that a live approver answers them only when the facts say so, and otherwise says only that the outcome follows the engine's configured posture; it never states a direction it cannot read. Thirteen notices (memory capture and harvest, cross-session messages, the classifier denial limit, late approvals, an unconsumed stop, a blocked parked input, a model whose catalogue entry declares no image input) each get a reader `read<Code>(notice)` that reads own data fields strictly (a missing or malformed required field gives `undefined`, a malformed optional field is dropped and never folded to zero) and a sentence function in English and Chinese whose untrusted strings pass the display filter. Where the notice does not say which of two opposite outcomes happened, the sentence does not guess. The readers are also rows of the dispatch table. This gate checks every reader on good, absent and malformed frames and on hostile input, every sentence arm, the display filter, the direction rule of the durable-approval sentence across all combinations, and the dispatch table and root exports. |
722
+ | `scripts/run-local-engine-stop-ladder-test.mjs` | The local engine's stop ladder. The three durations live in one constant, and the replace-engine prompt's upper bound for an immediate replacement is computed from the same constant. The step function decides each tick: the first termination signal; in the idle form, a kill only after the engine's own drain deadline plus a grace period; in the immediate form, a second termination signal exactly once and only after evidence that the engine processed the first one (a drain started by that signal after it was sent), and a kill at a bound derived from that evidence. Whenever the process's liveness cannot be determined, no signal is sent in that tick, and the ladder gives up at the end of the segment instead. A ladder that gives up takes the engine state machine to a stop-failed state and releases the start lock. The step function is pure, returns frozen results, and treats malformed input as a reason to send nothing. |
723
+ | `scripts/run-local-engine-step-matrix-test.mjs` | The local engine's lifecycle state machine: one pure reducer the host feeds with observations, lock outcomes, ticks and user answers, and whose effects the host carries out. The guard checks every one of the 384 cells of the published state-by-event matrix, each guarded partition separately, against the row it should take: target state and key effects, an unchanged state with no effect where the matrix says so. It also checks the start-lock cases, that withdrawing the only cause takes the prompt down in the same step, and the invariants: signals are sent only from the two ladder rows and only to the target verified under the lock; an older client never signals or starts an engine; an unknown reading never counts as idle; an immediate replacement only follows a confirmed prompt; each cause is tried automatically at most once per target; every lock-holding state has a deadline exit; the non-interactive lane always ends with a bounded failure instead of waiting for a person; and repeated crashes stop automatic restarts within a fixed bound. Finally it covers the seat projection of each state, the outcome of answering the prompt (a changed target or changed readings make the answer stale), and the outcome of a restart request. |
724
+ | `scripts/run-bash-curtailed-projection-test.mjs` | Bash commands that ran and were then cut off — timed out, killed for producing no output, interrupted, or ended by a failed output-stream callback — now carry the fact on every result surface. The card body leads with a one-line family sentence when there is partial output, the result record's `stderr` is prefixed with the same sentence, and a structured `_sema_curtailed` marker names the kind and, when the engine reported one, the number of seconds. The error flag is never decided here: the card's `isError`, the print lane's `is_error` and the record's `interrupted` stay exactly what the engine said (a cut with partial output is not an error unless the engine says so; a cut with no output is), so a client that wants to know whether a command was cut reads the marker, not the error flag. The reader looks only at own data slots of a Bash-shaped result: an accessor is never run, a key found only on the prototype chain does not count, a family flag counts only as the literal `true`, a seconds value that is not a non-negative safe integer is dropped while the family is kept, and any other result shape leaves everything byte-for-byte as before |
725
+ | `scripts/run-credential-adjacent-projection-test.mjs` | Credential values glued straight onto a `Bearer` or `Basic` scheme word with no separator. When the acceptance check reads the scheme word as a whole word, the value is judged exactly like the same string written with a space (same value scanner, same release words, same address rules, same marker); invisible characters between the scheme word and the value, or inside the value, never turn a masked value into a shown one. The gate pins the masked forms, the clean names that must stay byte-identical (English words, camel / snake / dashed identifiers, paths and property access, the 96-character read window edges), the accepted extra masking, idempotence on every outlet, the switch ownership (`credentialWords` / `credentialUrls`), the forms that stay byte-identical to the previous release, the query and fragment readings, the release-step ladder (the new step only adds masking on top of the previous final output), the per-position read budget of the acceptance check, and a bounded running time on long query strings that repeat the glued form thousands of times. |
726
+ | `scripts/run-host-withdrawal-projection-test.mjs` | Host withdrawal of a parked card: once the host has told the user that a pending row cannot be continued by this engine, it can take a package-held card off screen without a decision. Approval cards (the in-stream parked approval card, a reopened parked approval card and a live card) answer `{ kind: 'withdrawn', toldBy: 'host', rowVerdict }` (`WITHDRAWN_CARD_DECISION_KIND`); parked question cards and plan-review cards are taken with `withdrawParkQuestion(questionId, { toldBy: 'host', rowVerdict })`, which refuses with one of `PARK_QUESTION_WITHDRAW_REFUSALS` (`malformed`, `no_card`, `not_withdrawable`) and then leaves the card untouched. A withdrawal sends nothing, says nothing (no parked-failure reason on the terminal frame, no synthesized failure frame, no way-out sentence), does not record a user dismissal, ends the waiting chain normally and gives every joined caller the same value; the outcome is `{ kind: 'not_sent', cause: 'withdrawn', toldBy: 'host', rowVerdict }` and `approvalResolutionDetail` has no sentence for it. A withdrawal whose notice cannot be read is handled as a retraction. |
727
+ | `scripts/run-done-replay-single-source-test.mjs` | Whether a final `done` frame is the replay of an earlier submission (the same idempotency key hit a run that already exists) is answered by exactly one reader in the package, and every place that needs the answer uses it: the result frame's replay marker, the synthesized terminal rows, the submission helper's reading, the session-background opening record and the background-run watcher. The reader looks at two sources, each strictly `true` and each read in isolation: the frame's own data slot (an accessor is never run) and the result body's slot; a source that cannot be read does not take the other one down, and nothing throws. The guard reads the source with the TypeScript tokenizer, plants four helper-plus-literal-key reads to prove its ruler responds, and then requires that no other file reads a slot named `replay` that way. It also reads the published bytes of every supported server release before 7.148.0 to confirm which submit paths carry the replay flag only on the frame: the in-flight merge and the concurrent de-duplication paths pass the first answer through unchanged, so for those the frame flag is the only evidence. |
728
+ | `scripts/run-interrupt-refusal-note-test.mjs` | The sentence an Esc interrupt gets when the engine refuses it. The engine's machine code on a refused interrupt maps to one sentence per code — the run is waiting on a decision so no turn is in flight, this server holds no live turn for the run, the run is no longer running — each in English and Chinese, saying what happened, that the run itself was not changed, and the way out. Anything outside those three codes (absent, empty, a near-miss spelling, a code from another family, a non-string, a hostile object) gets one neutral sentence that says the interrupt was not accepted, does not echo the input, does not say it failed and does not claim the run's state. The match is strict equality that never reads a property of its input, no sentence invites a resend or carries a machine word, the language argument falls back to English, the wording lives only in the message catalog, and the three codes are checked against the server's route bytes and real SDK error classes through the real emitter, with the escalation decision left unchanged. |
729
+ | `scripts/run-seat-notification-levels-test.mjs` | Host notification levels. Which level the host uses for each of its three native notifications (permission requests, questions, task complete) is answered once, by the host's own store. The seat capability table gains `notificationLevels`, reported `true` only by a host that serves both verbs and reads that same store at every native send point; a browser lane and the terminal never report it, and an absent capability means the window keeps its own notification switches. When it is reported, the window keeps no copy and no default of its own, reads and writes only through the verbs, and stops raising its own reminders for those three kinds, so each reminder is sent once. `getNotificationLevels()` returns `{ levels, supported }`: the level each kind uses right now (all three always present; a kind never set reports the host default) and the levels this host can deliver, a subsequence of the closed set in order, without repeats, always starting with `off`. `setNotificationLevel(type, level)` returns `{ ok: true, state }` or `{ ok: false, reason, state }`, a refusal being a receipt rather than an exception. The words are three frozen tuples: kinds `permission` / `question` / `idle`, levels `off` / `badge` / `banner`, and refusal reasons `invalid-request` / `unsupported-level` / `write-failed`. `planSetNotificationLevel(type, level, supported)` is the one precondition check before the host writes: a kind or level outside the closed sets is `invalid-request`, a level the host cannot deliver (or a supported list it cannot read) is `unsupported-level`, and it never throws. Both outbound gates are closed shapes that also refuse a state whose level for some kind is missing from its own supported list. The suite checks registration, both gates key by key, the planner against an independent reference over a generated pool, the capability reading, and an end-to-end host double whose send point reads the same store. |
730
+ | `scripts/run-session-mods-seat-test.mjs` | Which mods a conversation's last run used, and how many operator-declared mod packages the engine loaded. `sessionModsOf` reads one wiring report — the streaming report event or the submit receipt's view — into `{ rows }`, an empty list meaning that run registered nothing, or into `unreadable` when the report has no readable mods section; it never turns a missing section into an empty list and never keeps an earlier run's rows. A desktop-style host caches that reading per conversation and serves it through the seat verb `readSessionMods`, gated by the seat capability `modsView`, answering `unarmed` until the conversation has run on the current engine instance; a browser-lane renderer applies the same reader to the report it receives, so both lanes show the same thing. The guard checks the verb's registration and declared signature, the outbound check against every row shape and failure arm (including that it refuses a transport arm and never runs accessors), the reader against the row reader on a generated pool, an end-to-end path from a wire frame through the adapter to the host cache (with a real server projection when the fixture is installed), the capability reading, and `readModsDeploymentPackages`, which answers `present` with a count, `absent` or `unreadable` from the health body without folding absence into zero or false. |
731
+ | `scripts/run-hook-env-seat-projection-test.mjs` | The request's `settings.env` block, which the engine reads when it fills `$NAME` references in the headers of HTTP hooks it runs. The host-supplied settings entry now has a seventh row and a required source `env` (the host environment, or `'none'`). The names sent are exactly the union of `allowedEnvVars` over the `type: 'http'` hooks the request body carries on events the engine fires (already narrowed by the admitted `httpHookAllowedEnvVars` lists; entries on events the engine never fires stay in the body but do not count) that are present in the host environment, with their values; no name, no key. The suite checks that the engine's reading and the host's own reading of the same hook produce the same header bytes through the contract package's interpolation function, that nothing outside that intersection is sent and credential names are not pre-filtered (the server judges them), that values never appear in hook entries, receipts, rows, notices or any log line even with `SEMA_DEBUG` on, the lane seats, the row for a missing, absent or unreadable source, the dispatch-time recheck (only tighter), forged products, and the server's own credential refusal naming only names this package sent. |
732
+ | `scripts/run-approval-dual-frame-retired-test.mjs` | Since server 7.133.0 an approval ask is presented in one frame family per leg, and the supported server floor is above that, so the package no longer correlates a replayed older-family frame with a card-family frame carrying the same approval id. A replayed older-family frame that the pending list shows as crash-converged is reported to the host without pre-recording the process-level settled table (a host claim for the same id is held, not joined, and a repeat of the same frame is judged against the list again); an opening card-family frame is no longer evidence that a ledger frame is still pending (the frame path reads the list once, and its ledger keeps no per-segment set of such ids); the card frame itself still flows downstream. Positive controls keep the cross-route dedup in place: a host claim waits for a live card to settle and then joins, an engine close-out that is not a park-move joins while a park-move does not, and a simulated double-frame arrival still joins. The seat contract no longer says that one ask emits two frames; the supported server floor is pinned at 7.133.0 or higher; the host predicate `replayedApprovalClosesCardFrame` stays as a registered pending retirement. |
733
+ | `scripts/run-self-knowledge-output-style-merge-test.mjs` | Self-knowledge block versus the caller's own output style. When the engine does not accept the first-class `appendSystemPrompt` seat, the built-in product context rides along in `settings.outputStyle`; the live-defaults layer now merges the request's own style text behind it instead of overwriting it (context first, a blank line, then the caller's text — byte-identical to what a terminal host that pre-merges already sends, and idempotent when the result is fed through again). A merged value longer than the 16384-character server limit is never sent: the context alone goes out when it can be separated from the caller's text, otherwise the key is left off, and one turn-plane notice says the style was not applied. A style value that cannot be read as text — an accessor (never run), an inherited key, a non-string — is never merged and gets its own notice. A second entry point, `applyLiveRequestDefaultsWithNotices`, returns the request together with those notices; the original entry point is its `.request`, byte for byte. The first-class seat follows the very same rule: when the engine does accept `appendSystemPrompt`, the request's own appended prompt is kept behind the context instead of being replaced by it, with the same limit, the same idempotence, the same two notices and the key dropped from the request when nothing can be sent within the limit; a generated pool feeds the same pair of texts to both seats and requires identical outcomes, so the two cannot drift apart |
734
+ | `scripts/run-hooks-engine-owned-wire-projection-test.mjs` | The request body carries only engine-owned hook events. The engine lights nine hook events (PreToolUse, PostToolUse, PostToolUseFailure, PostToolBatch, PreCompact, PostCompact, Stop, StopFailure, UserPromptSubmit); every other event in the settings contract is run by the client itself and never goes on the wire. Hooks written under those other events, in any of the five settings layers, stay out of the merged hooks and out of the request body while the engine events next to them are sent as before; settings that hold only client-run events send no `hooks` key at all. The verdict rows (dropped entries, executor verdicts, exclusions, plugin verdicts, the hooks-not-run notice) match the same settings without those events. Side effects that exist only for entries on the request body (stripping a hand-written `plugin` key, same-entry de-duplication, HTTP allow-list rows) are counted only for entries that really are on it. The set of client-run events is derived from the contract's event table minus the nine, never hand-copied, and every plan and request body the suite builds is swept: its event keys are always a subset of the nine. |
735
+ | `scripts/run-gate-remote-shape-test.mjs` | Remote-shaped runs of the suites. The suites also run on machines that carry only the tracked files: no fixtures, no neighbouring client trees, and a one-commit repository without release tags. A suite that depends on such material must say so and carry on rather than stop its later sections. When the prerequisite material of a fence is missing it prints a skipped-section marker and keeps going (it is red only in the required-fixtures mode), instead of exiting early; a red on an expiry-type cell is recorded and the rest of the suite still runs before the suite fails; and a repository whose history did not travel with the tree is told apart from a ledger that names a commit nobody has. The guard exercises the three shared helpers behind this on throwaway scripts and throwaway repositories (a deferred red still fails the run, the later cells still run, and the strict failure modes keep their exit codes; a fence whose material is present still exits when its control fails; a one-commit or depth-one repository reports no history while a two-commit one reports history), checks that each suite that used to stop on missing material now goes through them (every wiring is paired with a one-character mutation that must break the check), and runs three previously affected suites in a clean environment with no fixture variables set, requiring them to finish. |
736
+ | `scripts/run-session-policy-loosen-sentence-test.mjs` | The wording for a session-rule removal that the engine refuses as a loosening. Since server 7.135.0 / engine 7.55.0 a non-operator write is judged against the operator part of the stored row, not the whole row: a session's owner can take back the entries the owner added, and cannot take back what an operator wrote (a row stored before that release, or one an operator last wrote, counts as the operator's throughout). The refusal code stays the same while the engine's own sentence changed between those generations, so the package reads the code and never the sentence. The support floor sits above that generation line, so there is exactly one meaning inside the supported range and exactly one sentence; the guard ties that claim to the declared floor and goes red if the floor is ever lowered below the line. The sentence says the record is unchanged, that the engine counted at least one entry as written by an operator, and that an owner can only take back the entries the owner added; a deployment without an operator gets a different closing instead of a pointer to someone who does not exist. Its reason lives once, in the message catalog, and both closings embed it. The guard pins the English and Chinese text byte for byte, shows that three generations of engine wording with the same code produce the same sentence while a refusal without the code does not, and runs the real engine store through the removal verb: rows an operator wrote, rows an operator rewrote after the owner, and rows stored before the operator part existed are refused with the record untouched, while an owner's own entry is removed. The old claim that only an operator can remove entries appears nowhere in the sentences or the catalog. |
737
+ | `scripts/run-tool-permission-settled-projection-test.mjs` | A seat's tool-permission receipt can now say that the request was already decided elsewhere: the receipt carries an optional `settledElsewhere` slot (the first decision, and whether it is the opposite of what the user pressed), minted once where the engine reports a replayed answer and read back through a reader and a sentence helper that share the existing message-catalog sentences. The answer-receipt reader also recognises the parked-redemption arm (first redemption and replay), and every case checks that a non-replay answer is unchanged, malformed bodies read as absent without throwing, and accessors are never executed. |
738
+ | `scripts/run-engine-repoint-projection-test.mjs` | One-time engine re-pointing. When an engine's management address changes but the device is the same one, `repointEngine` moves this machine's device record and its session mappings to the new address in a single operation, without enrolling again (the device id does not change, and the engine's own bindings, which are keyed by device id, stay valid). It acts only when two proofs both hold: the device's private key completes the handshake at the new address (`helloAck`; `auth_failed` means unconfirmed, never refused), and a device listing read with a non-operator credential includes that device id (an operator listing is unfiltered and counts for nothing, so it is not even requested). If either proof cannot be read the result is `undetermined`, if either is not confirmed it is `unverified`; in both cases nothing is changed and a machine-readable way out is returned. All reads and stores are injected, the old record is never written to before both proofs hold, an existing record at the new address is never overwritten, the move is read back before sessions are touched, and a half-finished run (`partial`) completes when run again. `deviceHelloReadingOf` is the single reader of the handshake answer frame, `engineRepointVerdictOf` the single verdict, `repointSessionEngine` the session-mapping half (the second writer of a session's engine snapshot, besides the first-bind commit). |
739
+ | `scripts/run-output-style-body-carry-test.mjs` | Output style on the host-settings assembly. The request key `settings.outputStyle` is the style's body — the server appends it verbatim to the system prompt — so the assembled product now carries the body the host read for the configured style name, byte for byte and untrimmed, and never the name itself (the name, with the optional per-turn line, stays in the product's style facts for the top-level per-turn seat, which is unchanged). A style whose body is empty or whitespace-only, such as the built-in default, carries nothing: the key is left off, the row says there was nothing to carry, the style facts are still given, and the request is byte-identical to one built with no style source. A body that cannot be read as text — missing, not a string, an accessor (never run), only on the prototype chain, a malformed per-turn line, a reader that throws or answers a non-object — is never carried blindly: the session gets one notice, the key is left off, and no style facts are given. Safe mode, an absent source and an unset style name behave as before |
740
+ | `scripts/run-hooks-closed-leg-local-skip-test.mjs` | Local hook skip ports follow the request when governance closes the whole hooks leg. When a turn's hooks plan is closed (the workspace is not trusted, the managed policy could not be read, or managed settings turn all hooks off) the request carries no hooks, and neither local skip port lets anything run either: `settingsHooksKeptLocally` answers an empty list for every event, every group shape (with or without a source) and the compaction branches, and the plugin counterpart `pluginHooksKeptLocally` answers an empty list for every plugin, event and index. Whether a plan is closed is read in one place, from its closed-by slot: absent, null or undefined means open (unchanged behavior); any other value, and a present slot that cannot be read plainly (an accessor, a prototype-chain slot, a throwing proxy), counts as closed. A closed plan lists every settings hook entry of every layer in its dropped-hooks table, one row each, with the closed-by word as the reason and indexes pointing into the original document; pieces of a document that cannot be read plainly are skipped one piece at a time and accessors never run. The hooks-not-run notice says one sentence per layer when the policy could not be read, exactly one sentence when managed settings turn all hooks off, and nothing extra for an untrusted workspace. Plans that are not closed behave as before. |
741
+ | `scripts/run-hitl-allow-feedback-projection-test.mjs` | A note written next to Yes on an approval card reaches the model only through the durable decision door. The one assembly point (`answerPayloadFor` for the decide lane) mints the `feedback` key from the note as the original bytes — never trimmed or cut — and mints nothing for empty, whitespace-only or non-string notes, which can never fail the decision. The one judge (`answerCarriage`) answers `model` only for a card that carries the parked-row fact and `none` for everything else; the seat request carries `feedbackCapable` only when that judge says `model` and the request id is in the durable domain. A direct-door refusal of the key drops it and resends exactly once, keeping the session-wide choice, and says so after the decision lands. The live respond door and the ask door never carry the key: their bodies are byte-identical with or without a note, and the one catalog sentence (`allowNoteNotCarriedSentence`, English and Chinese) is shown after delivery, never before; the sender outcome says `allowNoteDropped` only when a real note was written. |
742
+ | `scripts/run-edited-approval-acceptance-test.mjs` | The edited-approval gate. A card whose shown arguments are not the arguments that will run cannot take an edited approval: sending the edit would replace the tool's real input, and dropping the edit would approve something the person never saw. The package exposes one judge for this, `editedApprovalAcceptanceOf`, answering accepts, refuses or unknown with a machine-readable basis word (unreadable input is never folded into accepts; a refusal also says the question stays pending so the same card can be raised again), plus a two-language one-line sentence for the refusal that carries no user-supplied bytes. The guard pins the three states cell by cell including hostile inputs (accessors and trapped proxies are never executed), the sentence in both languages and its single catalog source, and feeds the same fixtures to both sending paths — the live-card path and the parked-approval path — to show that each refuses an edited approval exactly when the judge says refuses and sends it exactly when the judge says accepts, even if the host tampers with the card request while it is shown. A structure check keeps the judge the only reader of that bit. |
743
+ | `scripts/run-inline-task-model-slot-test.mjs` | The model id a delegated sub-run was prepared with now reaches the inline progress ledger from the sub-run's start frame as well as from its progress ticks. A background sub-run's delegating card closes at the launch receipt and its progress ticks may never arrive, so the badge on the card used to stay on the requested name. The start frame's model now lands in the same ledger slot through a write door that touches only the model (counts stay absent rather than becoming zero; a settled row is not reopened), a later tick that carries no model keeps the model already known, and the sub-run tail's start and progress frames use the same door. The gate drives real wire frames through the adapter and a forwarding host, covers the model slot against counts, settle ordering and card shapes (settled, open, never seen), a loopback SSE tail, compile-time pins on the declarations and source anchors on the single write site |
744
+ | `scripts/run-input-author-projection-test.mjs` | An input's author (who wrote it) and run origin (where it came from) are two separate, closed-word facts read through one reader that the `human_input` projection, the chrome arm and the replayed user rows all share. Absent means unsaid (never filled in as the operator, no undefined keys); a word in the engine's closed set passes byte for byte; anything else present (an unknown word, a case or whitespace variant, an empty string, a non-string, null, an accessor) leaves the slot out and sets an unreadable flag, never folded into another word. Replayed rows read the run origin only from user rows (rows carry no author). The gate also pins that the type package does not declare these keys yet, that the engine has no notice code for a dropped undelivered input, and that the retired objective flag has no reader. |
745
+ | `scripts/run-workflow-completion-poll-test.mjs` | Background workflow completions are delivered while the session is idle, once. The host hands the client the run record read as it is; the client decides whether the run has ended (running, completed or failed; anything it cannot read, or a status outside those words, is treated as unknown rather than as still running or finished). When the record shows the run ended and no other channel has delivered that completion yet, one completion notification is queued through the same constructor and the same run-id gate the pushed completion uses, carrying the run's returned result and no pointer to any retrieval tool, since the record carries none. A completion pushed first stops the polling; a push that arrives while a read is in flight stops it too; a push that arrives after the polled completion is recognised and not delivered again. Reads that fail, hang or answer nothing neither deliver nor give up; the two-hour watch limit still ends through the single give-up path. |
746
+ | `scripts/run-sdk-37-pick-projection-test.mjs` | What the client relies on from the newest client library generation, read the same way on every library generation it supports. A refusal reason the library dropped still reaches the caller verbatim. The liveness check sends only the base address (same budget, no credentials, on every generation), while the re-exported health probe sends exactly one bearer header when given a token; a health reading without identity fields gives an unknown identity verdict, never a changed one. The engine's own refusal of a parked row is read as the row's own refusal: it is not treated as a changed binding and not as a transient refusal, and a real binding change still stops safely. A push whose destination is a read-only mirror is reported as pushed, with one sentence saying the title and pin stay with the writer side. Rule-import refusal reasons pass through verbatim, and a parked approval's remember flag is read as a boolean. The new library-only names are never value-imported by the client, so older library generations still load. |
747
+ | `scripts/run-session-box-lanes-projection-test.mjs` | Session-box lanes on `task_notification` frames: the three carriers (`crossSessionMessage`, `crossSessionNotice`, `inboxNotice`) and the attribution record `_sema_provenance`. The lane set has four members, the fourth being `inbox_notice`; all three carriers and the record are read in one classifier, and the carrier that is present first decides the lane (message before notice before inbox), so a record whose kind disagrees with that carrier falls back to the generic card instead of falling through to a later one. An inbox frame needs two non-negative integer counts (`pending`, `delivered`); a malformed or missing one drops the whole frame and never becomes a synthesized zero, while an absent `urgent` or `resupplied` stays absent and is never rendered as zero, and the box handle is never carried anywhere. A receipt or idle notice takes its typed state only from the record's `peerMeta.notice`, never from the carrier's model-facing text; when the record carries none, no chrome event is produced and no state word is invented. The additive `session_box_notice` chrome event carries the typed facts plus a one-line English sentence and the uuid of the accompanying transcript row, is sent just before that row, and the same sentence is available in both interface languages through `sessionBoxNoticeSentence` (unknown states get a neutral sentence that does not echo the word). Inbox notices restart their sequence number in every run, so their de-duplication key carries the run identity and a second run's first notice is no longer swallowed as a replay. The suite drives the real `adapt()` pipeline against the built artefact. |
748
+ | `scripts/run-stream-ended-reason-test.mjs` | Why a stream reader gave up. When the type package's stream reader ends without a terminal event it now says which of its two exits gave up (two empty re-opens in a row, or the failure counter reaching its ceiling) and, when the last leg ended with an error, what that error was. The package exposes one reader, `streamEndedReasonOf`, that answers either a closed two-word reason with the optional cause, or unknown with one of three reasons (not this class, reason unreadable, reason not recognized). It reads own data slots only, never runs an accessor, never folds an unrecognized word into a known one, and never throws. The older class predicate is now derived from that reader and answers exactly as before for every input. The failure presentation picks one of three sentences for a stream that ended without a terminal event, in both languages: two empty re-opens (the run is likely parked waiting for a decision), no progress (no claim that the run is parked, pointing at checking it), or no reason readable (the earlier sentence); the category, the retry decision and the machine-readable fields do not change with the reason. The gate drives real type-package error objects and a hostile pool (accessors, prototype-chain slots, trapped and revoked proxies, coercible objects), checks older single-argument constructors, and plants five broken readers to prove the pool catches each. |
712
749
 
713
750
  Each suite carries a floor that only moves up — a refactor that stops executing a group of
714
751
  assertions is a failure, not a quieter pass. Guards anchor on the **installed artefact's content**