@sema-agent/client-core 0.110.0 → 0.111.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 (176) hide show
  1. package/CHANGELOG.md +169 -0
  2. package/README.md +50 -18
  3. package/dist/abortableSleep.d.ts +7 -0
  4. package/dist/abortableSleep.js +37 -0
  5. package/dist/adapt/arms.js +25 -4
  6. package/dist/adapt/toolCards.d.ts +3 -1
  7. package/dist/adapt/toolCards.js +4 -2
  8. package/dist/adapter/activeRunSelfHeal.d.ts +39 -0
  9. package/dist/adapter/activeRunSelfHeal.js +171 -3
  10. package/dist/adapter/downstream/eventToSdkMessage.js +10 -10
  11. package/dist/adapter/downstream/terminalToSdkResult.d.ts +2 -0
  12. package/dist/adapter/downstream/terminalToSdkResult.js +11 -0
  13. package/dist/adapter/runStream.d.ts +5 -0
  14. package/dist/adapter/runStream.js +15 -3
  15. package/dist/attachmentRoute.d.ts +20 -0
  16. package/dist/attachmentRoute.js +39 -0
  17. package/dist/attachmentsWireCaps.js +1 -1
  18. package/dist/builtinOutputStyles.d.ts +12 -0
  19. package/dist/builtinOutputStyles.js +147 -0
  20. package/dist/compactionStatus.d.ts +9 -0
  21. package/dist/compactionStatus.js +29 -5
  22. package/dist/controlRouter.js +2 -1
  23. package/dist/coreValuePorts.d.ts +39 -1
  24. package/dist/coreValuePorts.js +10 -0
  25. package/dist/displayUntrusted.d.ts +1 -0
  26. package/dist/displayUntrusted.js +6 -0
  27. package/dist/engineHealthFacts.d.ts +11 -0
  28. package/dist/engineHealthFacts.js +21 -0
  29. package/dist/engineHealthProbeVerdict.d.ts +23 -0
  30. package/dist/engineHealthProbeVerdict.js +115 -0
  31. package/dist/engineNoticeCodes.js +5 -7
  32. package/dist/engineRepoint.d.ts +12 -5
  33. package/dist/engineRepoint.js +44 -19
  34. package/dist/engineRowRef.d.ts +3 -0
  35. package/dist/engineRowRef.js +11 -1
  36. package/dist/engineSwapCrossing.d.ts +57 -0
  37. package/dist/engineSwapCrossing.js +297 -0
  38. package/dist/engineUnavailable.d.ts +44 -0
  39. package/dist/engineUnavailable.js +492 -0
  40. package/dist/eventLeg.js +3 -3
  41. package/dist/generated/sdkFactTables.d.ts +1 -1
  42. package/dist/generated/sdkFactTables.js +1 -0
  43. package/dist/generated/settingsSchemaTables.d.ts +4 -0
  44. package/dist/generated/settingsSchemaTables.js +1 -0
  45. package/dist/hitl/approvalDelivery.d.ts +1 -0
  46. package/dist/hitl/approvalDelivery.js +1 -1
  47. package/dist/hitl/approvalResolution.d.ts +1 -1
  48. package/dist/hitl/approvalResolution.js +2 -1
  49. package/dist/hitl/askGateWire.js +5 -6
  50. package/dist/hitl/decisionRefusal.d.ts +2 -0
  51. package/dist/hitl/decisionRefusal.js +7 -8
  52. package/dist/hitl/decisionRefusalCarrier.d.ts +3 -0
  53. package/dist/hitl/decisionRefusalCarrier.js +348 -0
  54. package/dist/hitl/decisionSend.d.ts +7 -0
  55. package/dist/hitl/decisionSend.js +34 -12
  56. package/dist/hitl/editedApprovalAcceptance.js +1 -1
  57. package/dist/hitl/elicitationRefusal.d.ts +4 -0
  58. package/dist/hitl/elicitationRefusal.js +23 -0
  59. package/dist/hitl/frameRouter.d.ts +2 -2
  60. package/dist/hitl/frameRouter.js +8 -15
  61. package/dist/hitl/gateIdentity.d.ts +5 -0
  62. package/dist/hitl/gateIdentity.js +23 -0
  63. package/dist/hitl/hitlBridge.d.ts +6 -0
  64. package/dist/hitl/hitlBridge.js +10 -4
  65. package/dist/hitl/hitlHostSurface.d.ts +2 -1
  66. package/dist/hitl/hitlHostSurface.js +7 -2
  67. package/dist/hitl/localAllowRedemption.d.ts +67 -0
  68. package/dist/hitl/localAllowRedemption.js +256 -0
  69. package/dist/hitl/parkResolver.js +7 -4
  70. package/dist/hitl/persistedRulesWire.d.ts +16 -0
  71. package/dist/hitl/persistedRulesWire.js +70 -0
  72. package/dist/hitl/planReviewWire.d.ts +3 -0
  73. package/dist/hitl/planReviewWire.js +23 -6
  74. package/dist/hitl/policyDenyCause.d.ts +1 -1
  75. package/dist/hitl/policyDenyCause.js +1 -1
  76. package/dist/hitl/resumeRunningCard.d.ts +1 -0
  77. package/dist/hitl/resumeRunningCard.js +1 -0
  78. package/dist/hitl/screenCardCarriers.d.ts +15 -0
  79. package/dist/hitl/screenCardCarriers.js +41 -0
  80. package/dist/hitl/screenDecisionCards.d.ts +98 -0
  81. package/dist/hitl/screenDecisionCards.js +362 -0
  82. package/dist/hitl/standingRows.d.ts +3 -1
  83. package/dist/hitl/standingRows.js +18 -0
  84. package/dist/hitl/toolApprovalWire.d.ts +30 -8
  85. package/dist/hitl/toolApprovalWire.js +264 -121
  86. package/dist/hooksWireCaps.js +3 -3
  87. package/dist/hostSettingNoticeArm.d.ts +3 -0
  88. package/dist/hostSettingNoticeArm.js +12 -0
  89. package/dist/hostTurnSettings.d.ts +3 -0
  90. package/dist/hostTurnSettings.js +38 -5
  91. package/dist/humanTurnRead.js +11 -1
  92. package/dist/i18n/messages.en.d.ts +129 -0
  93. package/dist/i18n/messages.en.js +135 -7
  94. package/dist/i18n/messages.zh.js +142 -6
  95. package/dist/i18n/translate.d.ts +3 -0
  96. package/dist/i18n/translate.js +78 -10
  97. package/dist/i18n/zh.d.ts +1 -0
  98. package/dist/i18n/zh.js +1 -0
  99. package/dist/index.d.ts +43 -17
  100. package/dist/index.js +30 -12
  101. package/dist/interactiveHalt.js +2 -1
  102. package/dist/interactiveInterrupt.js +20 -2
  103. package/dist/liveModelCatalog.js +2 -2
  104. package/dist/localEngineSeatReading.d.ts +2 -2
  105. package/dist/localEngineSeatReading.js +5 -3
  106. package/dist/localEngineSpawn.d.ts +0 -7
  107. package/dist/localEngineSpawn.js +0 -92
  108. package/dist/localEngineStep.d.ts +2 -2
  109. package/dist/localEngineStep.js +2 -0
  110. package/dist/mcpReconnect.js +3 -3
  111. package/dist/mcpWireCaps.js +1 -1
  112. package/dist/model/engineModelWords.d.ts +3 -0
  113. package/dist/model/engineModelWords.js +13 -0
  114. package/dist/model/subagentModelWord.d.ts +21 -2
  115. package/dist/model/subagentModelWord.js +100 -29
  116. package/dist/modelCapabilityProbe.js +3 -40
  117. package/dist/notifications.d.ts +19 -0
  118. package/dist/notifications.js +114 -12
  119. package/dist/ownKey.d.ts +6 -0
  120. package/dist/ownKey.js +31 -0
  121. package/dist/pendingUserRow.d.ts +4 -0
  122. package/dist/pendingUserRow.js +16 -0
  123. package/dist/permissionsDeclaration.d.ts +1 -0
  124. package/dist/permissionsDeclaration.js +11 -1
  125. package/dist/planReviewCardFacts.d.ts +3 -0
  126. package/dist/planReviewCardFacts.js +52 -2
  127. package/dist/pluginHooksWire.d.ts +9 -1
  128. package/dist/pluginHooksWire.js +40 -6
  129. package/dist/printToolResultFrame.js +4 -2
  130. package/dist/promptBlockedCopy.js +11 -6
  131. package/dist/promptSuggestionBatch.d.ts +7 -0
  132. package/dist/promptSuggestionBatch.js +9 -0
  133. package/dist/promptSuggestionsTail.d.ts +39 -0
  134. package/dist/promptSuggestionsTail.js +116 -0
  135. package/dist/request/taskRequest.js +10 -6
  136. package/dist/requestCoordinate.d.ts +1 -0
  137. package/dist/requestCoordinate.js +13 -0
  138. package/dist/rewindArchiveReading.js +4 -4
  139. package/dist/runCancelContext.d.ts +2 -1
  140. package/dist/runCancelContext.js +1 -0
  141. package/dist/runPermissionModeSwitch.js +2 -2
  142. package/dist/scenarioRefusal.js +11 -13
  143. package/dist/scenarioSkills.js +11 -6
  144. package/dist/seam.d.ts +16 -2
  145. package/dist/seam.js +10 -0
  146. package/dist/seatContract.d.ts +109 -9
  147. package/dist/seatContract.js +218 -15
  148. package/dist/sessionCostReading.d.ts +70 -0
  149. package/dist/sessionCostReading.js +150 -0
  150. package/dist/sessionHooksRoster.d.ts +4 -0
  151. package/dist/sessionHooksRoster.js +158 -0
  152. package/dist/sessionMemoryStatus.js +2 -2
  153. package/dist/sessionReadRedaction.d.ts +7 -0
  154. package/dist/sessionReadRedaction.js +26 -0
  155. package/dist/sessionSyncFileHistory.js +8 -9
  156. package/dist/settingsHooksSalvage.js +2 -1
  157. package/dist/sseIdleTriage.js +2 -1
  158. package/dist/storeDurabilityWord.d.ts +15 -0
  159. package/dist/storeDurabilityWord.js +18 -0
  160. package/dist/storePostureCapability.d.ts +7 -3
  161. package/dist/storePostureCapability.js +69 -39
  162. package/dist/subagent/engineSubagentResume.js +2 -2
  163. package/dist/subagent/engineSubagentTail.js +2 -1
  164. package/dist/subagentContentStore.d.ts +11 -0
  165. package/dist/subagentContentStore.js +28 -2
  166. package/dist/toolResult.d.ts +1 -0
  167. package/dist/toolResult.js +2 -0
  168. package/dist/userAttachmentEnvelope.d.ts +2 -1
  169. package/dist/userAttachmentEnvelope.js +10 -0
  170. package/dist/wireErrorTriage.js +2 -1
  171. package/dist/wireFailureShape.d.ts +4 -0
  172. package/dist/wireFailureShape.js +141 -19
  173. package/dist/wireRefusalCopy.d.ts +6 -5
  174. package/dist/wireRefusalCopy.js +49 -92
  175. package/docs/INTEGRATION-CLIENTS.md +2118 -17
  176. package/package.json +6 -2
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.110.0
38
+ **Version:** 0.111.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
@@ -298,7 +298,7 @@ guard still cross-checks the table by name).
298
298
 
299
299
  | Suite | What it guards |
300
300
  |---|---|
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. |
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. The park arm of the turn-error classifier now asks the same stream-ended reader as the predicate and the reason reader: a thrown object whose name exists only on its prototype chain or behind an accessor is no longer taken for a parked run, a real instance still is even when renamed, and the arm and the predicate answer the same for every shape in the pool. |
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
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. |
@@ -312,7 +312,7 @@ guard still cross-checks the table by name).
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. 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. |
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. Since 0.110.0 the durable-approval reader takes the source word as an open set, like the cause word: a word the package does not know reads as present and comes back as written, while an empty or non-string value reads as absent. |
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. |
@@ -333,7 +333,7 @@ guard still cross-checks the table by name).
333
333
  | `scripts/run-approval-card-retract-test.mjs` | The approval card's **decision-free retraction** and the in-stream frame leg's **outcome hand-back**: a host that must withdraw a card that no longer has a decision channel (session switch, engine switch, a tracker reporting the ask gone) answers `{ kind: 'retracted' }` and the package sends nothing on any of the three legs (in-stream frame, suspended ask, durable park); the outcome is the `ApprovalResolution` `{ kind: 'not_sent', cause: 'retracted' }` (`isApprovalDecided` answers false; the pre-0.98.0 reading of `decision: 'unresolved'` plus a `retracted` flag is withdrawn), `aborted` resolves `unsettled` / `interrupted` after exactly one posted deny, `failed` keeps its fail-closed deny, and `onToolApprovalOutcome` hands every in-stream outcome back to the host exactly once, tolerating a throwing or rejecting callback. The former approval-outcome note helper (`approvalOutcomeNoteOf`) is retired with CC-77: read `isApprovalDecided`, `approvalNotSentCause` and `approvalResolutionDetail` instead. |
334
334
  | `scripts/run-memory-spec-wire-test.mjs` | The per-agent **memory spec** (`agents[].memory`) read once for every client, plus the judge for the engine's **closed** key list. Two states are kept apart that clients habitually collapse: an absent `scopes` means *no layers were specified*, never "zero layers", and an explicit `writeScope: null` is a positive fact — this run has memory **read-only** (no remember tool, no consolidation write; recall still works) — which is neither "unspecified" nor "memory off". Each of the four keys is read once, on own properties only (an inherited key never reaches the wire, so reading one would report a value the engine cannot see), and a key that is present but unreadable stays in its own slot instead of collapsing into "unspecified"; `enabled` must be a strict boolean and `scopeContract` is an open-set verbatim word. A spec that cannot be read at all answers *undefined*, kept distinct from an agent that simply has no spec. The judge earns its keep on the consequence: the engine checks this spec against a closed list, so one unlisted key — most often the retired singular `scope` — is refused together with the **whole agent definition**, not just that key, and the single sentence minted here says so. What counts as "on the wire" is decided by the bytes, not by the shape of the in-process object: both the reader and the judge work off a `JSON` snapshot of the spec taken **in its property position** (wrapped under the same key, never serialized as a root value — otherwise a `toJSON(key)` that branches on the key hands us one shape and the engine another: one such input made the snapshot say *read-only, no violations* while the real bytes carried the retired key and a writable scope), because `Object.keys` and `hasOwn` disagree with the serializer in ways that change the answer — a non-enumerable `writeScope: null` would otherwise be reported as "memory is read-only for this run" while the engine receives *unspecified* and may still write; a key whose value is `undefined` would be reported as a violation that never leaves the process; a `toJSON` (even inherited) adds keys that `Object.keys` cannot see, including the retired singular one; and a throwing getter would let the judge claim it had looked when the spec cannot be serialized at all. A spec that fails to serialize is reported as unreadable by both ports, and the snapshot is taken once, so every getter runs exactly once. The judge answers in three states, never two: `[]` is an assertion (*looked, nothing unlisted* — including an agent that carries no spec at all), a non-empty list is what it saw, and *undefined* means it could not read the spec (a non-object item, an array, an unreadable `memory`, a throwing getter) — an unreadable spec never poses as a clean one, and an array is not a spec so its index keys are noise rather than findings. It reports only the snapshot's string keys, sorted and bounded, so a prototype, symbol, non-enumerable or `undefined`-valued key is never blamed while a `__proto__` that really does serialize is; the empty-string key is kept rather than dismissed as noise, because it does serialize and dropping it left a non-empty violation list with nothing said about the consequence; every key name in the sentence is quoted and escaped one code point at a time, so no escape is ever cut in half (a half-cut escape used to make the closing quote itself look escaped) and an empty name, a key literally named `""`, a key containing a backslash and a real control character versus a literal `\uXXXX` all read as different violations; a name too long to show is marked `(truncated)` outside the quotes with a pointer to the judge's verbatim list, so a prefix is never presented as the whole key — two long names sharing a prefix do show the same, which is why the mark and the pointer are there; key names are sanitized and bounded on the way into the sentence while the judge itself hands back the verbatim key, because sanitizing belongs in prose and never in a verdict. The announced future key `projectKey` is still unlisted today and is reported as such, with a sentence saying it is not a typo. The two construction-time refusals (`config.memory_project_key_spelling` — a spelling, 400; `config.memory_write_scope_mismatch` — a conflict with the scope already in force, 409) join the existing `config.` recognition table rather than a second word list, and each gets one sentence stating that the refusal landed **before the run started**, so nothing ran; the engine owns the triage and an unrecognised code gets no sentence at all. The write face is widened in the same batch so the package can actually mint what the reader can read: `TaskAgentWireMemory.writeScope` is now an optional `string \| null`, since a reader that understands "memory is read-only for this run" while the writer cannot express it is worse than no reader at all — it makes the support look real. Minting `null` survives serialization and reads back as read-only, minting `undefined` drops the key and reads back as unspecified, and the projector still pins `writeScope` explicitly every time. The accepted key list is reconciled against an upstream witness rather than a second local copy: the guard reads the SDK's own declaration comment for this key, requires the two sets to match in both directions, requires that comment to still name the singular `scope` as retired, and requires it to still not mention `projectKey` — so the day upstream admits that key, the guard goes red instead of the package quietly continuing to promise a 400. Each port takes its own snapshot, so a consumer that wants one self-consistent answer about a spec that can still change under it should read `spec.unknownKeys` off the reader — which comes from the same snapshot as the four slots — and send that materialized data rather than the live object. |
335
335
  | `scripts/run-approvals-feed-unknown-test.mjs` | The approvals feed tells three states apart: **N items waiting**, **nothing waiting**, and **this fetch did not come back, so we do not know**. Every way a fetch can fail (the call throwing or rejecting, a body that is not an object, a `livePending` section that is not an array — including the `null` seen in the field, a `pending` that is not an array or holds a malformed row) publishes `{kind:'unknown', why, at, mode}` on the subscription — never an empty snapshot and never silence. Real snapshots carry `kind:'snapshot'`; `snapshot()` still answers only with the last real one (a fact about the past) while `reading()` answers whether it is current (`unobserved` / `present` / `unknown`). Recovery always publishes a real snapshot again, even when the contents are byte-identical to before the failure. An unknown reading is never counted as zero: the awaiting-decision counts read `null`, the view is empty, and the tracker reports no removals, so cards on screen are not retracted for a failed fetch. Retry, backoff and circuit-breaking are unchanged. |
336
- | `scripts/run-approval-resolution-test.mjs` | How an approval decision ended is a single value, `ApprovalResolution` (`decided` / `not_sent` / `unsettled`, with the shared `lane`, `gatedCallId` and `joinedInFlight` fields), and all three decision entry points return it directly: the durable approval leg, the live approval-card leg and the question leg. The suite drives the real entry points and checks every outcome lands on one arm and one cause from the two frozen cause vocabularies; that the same thrown error yields the same cause on both decision lanes (a 503 with a capability code is an engine error, a 501 is a refusal, a local safety stop is not sent, an engine binding mismatch is an engine refusal carrying the original 409); that a joined call receives the holder's value; that the host outcome callback receives the union; that the four refusal judges answer the same for the union as for the original error, reading the frozen refusal reading recorded where the decision failed; the one-sentence detail for each cause; the standard `cause` on the package's safety error; the single delivery judge (status code plus a table of prior-refusal evidence keyed by error code and route) and that plan review, manual compaction and the union all follow it; and the two new park-moved conflict codes plus both forms of the resume preflight refusal. |
336
+ | `scripts/run-approval-resolution-test.mjs` | How an approval decision ended is a single value, `ApprovalResolution` (`decided` / `not_sent` / `unsettled`, with the shared `lane`, `gatedCallId` and `joinedInFlight` fields), and all three decision entry points return it directly: the durable approval leg, the live approval-card leg and the question leg. The suite drives the real entry points and checks every outcome lands on one arm and one cause from the two frozen cause vocabularies; that the same thrown error yields the same cause on both decision lanes (a 503 with a capability code is an engine error, a 501 is a refusal, a local safety stop is not sent, an engine binding mismatch is an engine refusal carrying the original 409); that a joined call receives the holder's value; that the host outcome callback receives the union; that the four refusal judges answer the same for the union as for the original error, reading the frozen refusal reading recorded where the decision failed; the one-sentence detail for each cause; the standard `cause` on the package's safety error; the single delivery judge (status code plus a table of prior-refusal evidence keyed by error code and route) and that plan review, manual compaction and the union all follow it; and the two new park-moved conflict codes plus both forms of the resume preflight refusal. The sentence for a refused edited approval is now the catalog stem; the retracted-card sentence is unchanged. |
337
337
  | `scripts/run-panel-cycle-identity-test.mjs` | The **cycle identity** on agent-panel events and the fleet ledger's **departure read-out**: a background agent may be revived under the same id, so `fleet-row` and `end` events now carry the wire's own `cycleSeq` / `startedAt` when present (absent means the row has no notion of generations, never "generation one"), `isStaleEngineAgentPanelEnd` is the single rule for ignoring a late `end` from a previous cycle (only when both sides carry a comparable identity; absence never drops a real terminal), a changed `cycleSeq` is a new cycle for usage stickiness and buffer coalescing, the notification lane carries `cycleSeq` only when the wire really sent `seq`, and `task_remove` frames reach the host through `onTaskRemoved` with `removeReason` / `cycleSeq` verbatim, a stale previous-generation removal leaving the newer row in place. A first-seen terminal row is held back only when this process has never seen that agent — it never ran, ended or showed here, and its delegating card was never seen either; a held row binds its own ids when it is held, so any terminal fact that resolves to the same agent releases it, through the one funnel, so the agent stays one row; a running frame arriving after such a held terminal row is a stale snapshot when both sides carry a comparable generation and it matches (no event, the held row keeps its final usage), a revival when the frame is provably newer (the held row is dropped), and is treated as a revival when neither side can be compared. A subagent lifecycle event carries `agentType` only from an honest source — the fleet row's own agent type, recorded before it is folded into the row label — and omits the key when there is none, never substituting the display name Since 0.95.0 the fleet ledger also ends the display of a stopped sub-agent early: when a row that the row-frame lane settled as killed leaves with a settled-level removal (no reason word on the frame), it is dropped from the retention window at once. The rule only shortens the display of a row that is already terminal — it never settles anything and never touches the terminal facts the detail view reads. A statement-level removal, any other terminal word, a late removal for an older cycle, and a row only inferred from a notification are all left alone, a removal that arrives before its terminal frame leaves no trace, and a by-id removal call (per session) follows the same single implementation. |
338
338
  | `scripts/run-workflow-size-warning-test.mjs` | The **workflow size warning** verdict shared by every host footer / panel: a three-state result (`warn` / `ok` / `unknown`) read off the optional fleet view keys, where an unknown size is never reported as a normal one (absent `totalCount` / `tokens` without positive over-cap evidence is `unknown`, naming the missing keys), positive evidence on either axis wins regardless of absent keys, the per-agent denominator uses the engine's started count only when it is not below done+failed (a smaller value is a stale reading), otherwise falls back to the done+failed lower bound only when both keys are present — and a lower-bound denominator only yields an upper bound of the projection, which can prove *within cap* (`ok`, flagged) but never *over cap* (`unknown`, with the upper bound exposed) — and the prior is used only when the engine itself reports zero started agents; cap precedence env > explicit guideline > default, prototype keys never act as a guideline, the env reader is pure and does not fall through to the second name on a bad first value; caps, guideline table, env names and the three copy variants are single-sourced |
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. |
@@ -342,7 +342,7 @@ guard still cross-checks the table by name).
342
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
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
- | `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. |
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. Since 0.111.0 the port bag has two more members, the engine's account-line constructor and its single account fold. Unset they read as absent (never an empty account); set they pass arguments and answers through by reference and let the engine's refusal of malformed lines propagate. The engine's own functions type-check into the port under all three option sets, a bag built for the previous twenty-five ports stops compiling and names both new members, and the widths ledger records both ports as equal to the engine's types. |
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 |
348
348
  | `scripts/run-display-cap-order-test.mjs` | The order in which untrusted text is sanitised and length-capped, across every mint point that puts an engine- or database-supplied string on a screen. The sanitiser rewrites each invisible character as a six-character escape, so capping the **raw** string first and escaping afterwards hands the screen six times the width that was budgeted — a forty-character allowance becomes two hundred and forty. The guard does not hardcode that allowance, because each mint point wraps its field in different fixed prose and the prose moves: it anchors on the deciding quantity instead, feeding one benign and one control-character input of the same length through the same mint and requiring the second not to come out longer. That criterion is immune to wording changes and stays sensitive to the expansion, and it is `<=` rather than `==` on purpose — a correct escape-then-cap backs the cut off a partially-consumed escape token, so the control-character line is legitimately the shorter of the two, and demanding equality would score that avoidance as a regression. Each mint is bracketed by two positive controls (the input really reaches the screen; the cap really engages) and the expansion predicate is shown to turn red against a deliberately cap-then-escape reference, so an all-green run cannot mean the guard simply measured nothing. The shared mint point is checked directly for the two avoidances it owes — never splitting an escape token in half, which would leave something on screen that looks like the beginning of a complete answer, and never splitting a legal surrogate pair, which would manufacture the very lone surrogate the sanitiser exists to catch |
@@ -368,20 +368,20 @@ guard still cross-checks the table by name).
368
368
  | `scripts/run-registry-test.mjs` | Every ratchet number the suites compare against lives in exactly one place, `scripts/registry.json`: each cell must be a finite non-negative integer, and each one carries a dated ledger of every change (`from` / `to` / `when` / `why`) whose last entry for that cell must equal the number in force — a number moved without an entry, or an entry written without moving the number, is red in both directions. Every suite that reads it is checked to read it: no ratchet name may sit next to a numeric literal in their own source (an AST check, so an accounting comment or a test string mentioning the number is fine), each names the cell it reads, and each keeps the step-down channel — when a measurement comes in *under* a ceiling the suite prints one `RATCHET-SLACK` line instead of passing in silence, so slack cannot quietly accumulate under a ceiling nobody lowered. Negative control: a cell rewritten as a string, a fractional or negative cell, a missing cell, a cell with no ledger entry, a ledger entry missing a field, and a ledger tail that disagrees with the number in force each have to make the check speak. The ledger is append-only twice over: against the previous commit, and against the last release tag, so an entry inserted before older ones cannot pass once it has been committed |
369
369
  | `scripts/run-client-core-failloud-test.mjs` | §C1/§C2: an empty `catch` with no comment anywhere inside it, a pure-swallow `catch` nobody reasoned about, and `void <write>` that really returns a Promise with no `.catch`. The exemption instrument is a comment saying why *this* failure may die; the documented-swallow count is a ratchet that only goes down |
370
370
  | `scripts/run-client-core-typeshape-test.mjs` | Type discipline as a guard rather than a build side effect: the set of enabled strict knobs (one silently switched off is red), `tsc --noEmit`, and export-surface ratchets for inline anonymous shapes (≥3 members), `unknown` leaving the surface, and bare `unknown` returns — zero slack in either direction |
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 |
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; and no subpath entry point's runtime import closure may hold a `dupRisk: high` singleton (state that must exist exactly once lives behind the root entry only) |
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
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 |
378
- | `scripts/run-package-hygiene-test.mjs` | Everything `package.json` `files` ships — dist JS/typings and the Markdown docs — is screened line-by-line against a deny-list of strings that must never reach a public tarball (internal hostnames, codenames, person names, collaboration-process words, other repos' ledger ids and repo names; opaque ticket ids `CC-nnn` and post numbers `[nnnn]` are allowed as traceability references). Since 0.77.2 the build strips comments (`removeComments`; enforced by `run-dist-comments-test.mjs`), so what this gate screens in dist is code, string literals and type-level text. Markdown docs are enforced forward-only (CHANGELOG from 0.77.2, the integration doc from §81) because published sections are frozen. From 0.88.0 every shipped file is also scanned in full, frozen sections included, for merge-conflict markers at the start of a line (`<<<<<<< `, a bare `=======`, `>>>>>>> `, and the seven-bar base marker of three-way conflicts). From 0.88.0 a short local list of release-process phrasings (integration-branch and review-round wording) is screened as well, forward-only from the 0.88.0 sections. From 0.101.0 internal finding labels (a single capital letter, a hyphen and a number, such as the forms an internal review uses, and another repository's ledger form) are screened in the Markdown docs, forward-only from the 0.101.0 sections; the integration doc's own row prefixes are two letters and are not matched. From 0.102.0 post-number aliases (a hash sign followed directly by two to five digits, as opposed to the allowed `CC-nnn` and `[nnnn]` forms) and fix-batch labels (`FIX` followed by a number) are screened in the shipped JS, typings and README in full, and forward-only in the CHANGELOG and the integration doc; hex colour codes, anchors such as `file.md#129a` and HTML entities are not matched.
378
+ | `scripts/run-package-hygiene-test.mjs` | Everything `package.json` `files` ships — dist JS/typings and the Markdown docs — is screened line-by-line against a deny-list of strings that must never reach a public tarball (internal hostnames, codenames, person names, collaboration-process words, other repos' ledger ids and repo names; opaque ticket ids `CC-nnn` and post numbers `[nnnn]` are allowed as traceability references). Since 0.77.2 the build strips comments (`removeComments`; enforced by `run-dist-comments-test.mjs`), so what this gate screens in dist is code, string literals and type-level text. Markdown docs are enforced forward-only (CHANGELOG from 0.77.2, the integration doc from §81) because published sections are frozen. From 0.88.0 every shipped file is also scanned in full, frozen sections included, for merge-conflict markers at the start of a line (`<<<<<<< `, a bare `=======`, `>>>>>>> `, and the seven-bar base marker of three-way conflicts). From 0.88.0 a short local list of release-process phrasings (integration-branch and review-round wording) is screened as well, forward-only from the 0.88.0 sections. From 0.101.0 internal finding labels (a single capital letter, a hyphen and a number, such as the forms an internal review uses, and another repository's ledger form) are screened in the Markdown docs, forward-only from the 0.101.0 sections; the integration doc's own row prefixes are two letters and are not matched. From 0.102.0 post-number aliases (a hash sign followed directly by two to five digits, as opposed to the allowed `CC-nnn` and `[nnnn]` forms) and fix-batch labels (`FIX` followed by a number) are screened in the shipped JS, typings and README in full, and forward-only in the CHANGELOG and the integration doc; hex colour codes, anchors such as `file.md#129a` and HTML entities are not matched. From 0.111.0 a further short list of collaboration-process phrasings (in-branch mutation runs, implementation branches, internal ticket-queue and ledger-status bookkeeping) is screened in the shipped JS, typings and README in full, and forward-only from the 0.111.0 sections; the package's own product terms for its in-memory ledgers are not matched.
379
379
  | `scripts/run-terminal-coded-detail-projection-test.mjs` | The structured account of a coded failure, carried on the synthesized API error row and on the run result frame under one superset key. When a run ends in a failure whose throw site stated structured detail (for example the seat and arm of a refused model binding, or the code and permanence of an unusable rewind boundary), both the row and the result frame carry that detail under `_sema_coded_detail` (key names and structure unchanged; credential-shaped substrings in string values are replaced by redaction markers, the same net the error list goes through), taken from a single reading of the terminal so the two carriers always agree, and the interactive lane's rebuilt row keeps the key. Nothing is carried when the terminal has no such account, the account is malformed, empty or over budget, the terminal is not a failure, or the event is a bare failure event: absence never reads as having no structured reason, no placeholder word is ever written, and a permanence that was not stated is not guessed. `terminalCodedDetailOf` reads the key back from either carrier and `runTerminalCodedDetail` reads the account from a terminal reading, both returning a frozen copy and neither throwing on hostile shapes. Everything else on the row and the frame is unchanged, the key has one minting site per carrier, and it has a row in the superset ledger |
380
380
  | `scripts/run-sdk-registry-transit-test.mjs` | The package's cloud control-plane surface (`sdkRegistryTransit`), which lives behind its own `./registry` subpath entry point rather than on the root barrel, and this guard holds both halves of that decision. Upstream publishes the same surface behind a subpath of its own, because the subject differs: the engine-wire surface speaks for one engine's service credential, this one for a person's rotating token, and their refresh and error semantics were deliberately never merged. Keeping it behind a second entry point means a client that never touches the control plane neither resolves nor type-checks it. Every value re-export is therefore read from the file the subpath entry actually resolves to, and must be the **same reference** as upstream's own (the control-plane client class, the three config reads, the health probe, the feedback call, the auth-path constant, the content-address helper, the two typed error classes a host recognises with `instanceof`, and — since 0.90.0 — the five sign-in and scope calls: device-code request, polling until approved, token refresh, scope listing and scope switch); every type re-export is compared with the emitted declaration file in both directions; the gate's list and the source file's export lists are likewise compared both ways; and the root entry is checked to carry none of these names, with the root barrel's own source checked to reference the entry file nowhere — a name leaking onto the root would put the cost of this surface back on clients that never asked for it. The subpath is then verified end to end: the installed SDK must really publish its own `./registry` entry and declare every transited name inside it, and this package's own `exports` must point that subpath at exactly the files the gate just judged. Portability is two checks rather than one, done with a parser rather than a text search: the upstream subpath's emitted JavaScript, walked recursively, must contain neither a `node:` specifier (static, side-effect, dynamic, `require` and re-export forms all exercised) nor a Node **global** — because the same package's third entry point is a Node-only surface that imports nothing at all and reaches for the `Buffer` global, so a specifier check alone would pass it as isomorphic, while a byte-level search of it reports two `node:` hits that live entirely inside a documentation example. A text scanner that merely strips comments first gets both directions wrong on ordinary JavaScript — a regular-expression literal containing a slash pair swallows the rest of its line, and the word in a string reads as a reference — so both scanners run off the syntax tree and are checked against fixtures for each failure direction as well as against that real material — including a dynamic import written with a template literal, which a check that accepts only quoted strings misses entirely, and a dynamic import whose target cannot be determined statically, which is refused rather than read as no edge at all. Zero-processing is likewise enforced with a syntax-tree allowlist rather than a keyword search: every top-level statement must be a named re-export carrying that one specifier, so an import followed by an in-place edit of the upstream prototype is refused with a file and line — that shape leaves the name lists untouched and even keeps the same-reference check green, since both sides are then the one object that was damaged. The last section measures, rather than merely notes, one declaration-level gap: two of the upstream subpath's declaration files reference a package the SDK lists only among its own dev dependencies. Moving such a check to a scratch directory is not isolation, because package resolution walks the ancestor directories, so the gate builds a sandbox served by a restricted compiler host and proves the isolation both ways — a decoy copy of the missing package placed one level above the sandbox must silence the errors for an unrestricted host and must not silence them for the restricted one. Then it installs this package into that same sandbox as a real consumer would, and pins the two readings that justify the entry-point split: a consumer that imports only from the root sees no unresolved-module errors at all, while a consumer that imports the subpath sees exactly the two, reported honestly rather than swallowed by this layer. The day upstream ships those declarations, that section turns red; the registered count is then updated and the section stays as a guard against a regression. The separation itself rests on the root closure being computed correctly, so the portability guard that computes it was extended in the same change: a template-literal dynamic import is followed like any other edge, and an edge whose target cannot be resolved statically is refused on every one of the four graphs — without that, a single line in a third file already reachable from the root would put this surface back into the root runtime while every guard stayed green. Since 0.84.0 the upstream subpath ships those declarations itself: the measured count is now zero, the section stays as a guard (a new missing declaration package turns it red again), the isolation proof uses a synthetic probe that imports only the decoy, and the gate also checks that no upstream subpath declaration references the missing package any more. Since 0.90.0 the five sign-in, token-rotation and scope calls are re-exported as well, with the same untouched re-export form, and the declaration file of the re-export is compared with the gate's value list in both directions. The problem that once kept those five out lived in the type surface, and reference equality holds just as well for a function whose result type has silently collapsed to `any`, so the gate reads each of three upstream generations separately (the installed one plus two probe trees named by environment variables): the five names must be functions in the generation's emitted JavaScript and value-level named exports in its declaration entry; the closure of relative edges (imports, re-exports, import types and path references) from the declaration files that declare them, including a hop out of the registry directory, must contain no bare module specifier; the re-export file, loaded in a scratch directory where the package resolves to that generation, must expose the same function objects as upstream and exactly the gate's list of names, with the neighbours upstream really exports beside them (logout, the grant-type constant, the in-memory token store, the admin class and the like) absent; and a typed probe in the clean-install sandbox must see real return and parameter types, using suppression comments that stay valid only if there is a genuine error to suppress. A missing name in any generation fails the gate rather than skipping; only an unset probe tree is reported as a skipped section, and a set one that does not resolve stops the gate as broken material. The three generations must also be three distinct versions, each at least the major its slot is named for: one SDK filled into two slots, or an older one into a newer slot, stops the gate as broken material instead of counting as extra coverage. Synthetic fixtures (a generation missing a name, a bare specifier in the declaring file and one a hop away, a path reference that points at nothing or pulls in a file with a bare specifier, a wrapper function, one name too many, an export entry pointing at nothing, the generation registry itself) prove the reader answers each case. |
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. 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. |
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. The import preview now has one presentation reader for its skipped rows. It reports one of three states: absent (the list was not sent), malformed (present but not readable as a list) or read. Rows are grouped by the engine sentence byte for byte (no normalisation), groups and entries keep the engine order, a code is attached only when the sentence has a code prefix, and entries and sentences go through the display filter. A row that cannot be read is only counted, never guessed, filled in or folded into a group, so an empty list, a list of unreadable rows and an absent list stay distinguishable; no accessor runs and the result is frozen. |
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 |
@@ -523,7 +523,7 @@ guard still cross-checks the table by name).
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
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. |
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. Since 0.111.0 the same guard covers the session cost reading built on the engine's account lines. With the account-fold port absent every reading is unknown with no numeric fields at all (never a known zero). The result-frame total reads the run-origin fold, resumed history included and side queries excluded; the cumulative reading is the whole fold and side queries are listed apart; this is checked against an independent summing oracle over three hundred line sets. An unpriced line makes only the readings that contain it unknown. Lines flagged as missing usage, a partial or unread history declaration and a fork seed all read as lower bounds with a plus mark, a lower bound of zero is unknown, and a missing declaration is never read as complete. A literal zero turn cost is not free until the run's terminal cost fact says it was priced. Malformed lines, misbehaving ports and hostile objects never throw, and the module's source contains no summing or reducing. |
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. 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. |
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 now goes through the same display path as the hook's words (control and bidirectional characters shown as visible escapes, credentials washed, folded to one line, capped at 1000 units without splitting an escape), so clean text is byte-for-byte what it was and an empty or blank reason reads as the generic sentence. 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. |
@@ -547,7 +547,7 @@ guard still cross-checks the table by name).
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
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. |
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); since 0.111.0 the live approval card treats the same fallback the same way (same note, same mark, an edited approval sends nothing, plain approvals and denials unaffected), both paths take that note and mark from one shared place that the suite checks from the source syntax tree, and the card's standing-rule rows close identically on both paths. |
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. |
@@ -625,7 +625,7 @@ guard still cross-checks the table by name).
625
625
  | `scripts/run-catalog-routing-test.mjs` | A gate for the message-catalog migration. Sentences that move into the catalog must read byte for byte the same in English as before the move, and must exist in Chinese. Each corpus entry (one sentence producer called with one set of inputs) is compared in English, with and without an explicit language argument, against digests taken from the pre-migration build; a sentence the changelog names as reworded is pinned to its new digest instead of being waived. In Chinese every entry must contain Chinese characters, differ from the English text, and contain no run of three English words outside registered protocol terms, quoted foreign values and code spans; no placeholder may be left over and the message ledger must stay empty after the whole corpus has run. A syntax-tree check requires every function that takes a language argument to pass it on to every other language-taking function it calls, and every exported language-taking function must be covered by a corpus (or be listed as pinned by another gate or as a not-yet-migrated placeholder, which the gate verifies). The gate also proves its own detectors on planted cases. Read-only modes print the digests of a build and compare two builds entry by entry. |
626
626
  | `scripts/run-host-turn-settings-test.mjs` | The single assembly entry for host-supplied request settings (`hostTurnSettings`): hooks, the resolved permission snapshot, the web-search setup and the output-style name reach the request body only through its product, which always carries one row per key (supplied, explicitly absent, or not wired at all, the last one with a session notice and a self-check that names it), a turn verdict, and notices. The request builder takes the product and refuses these keys when they are written straight into the input settings. The suite checks that the request bodies match the previous release byte for byte on inputs whose projection did not change, the missing-source and explicit-none rows, the refusal when managed hooks cannot be loaded (thrown only on a lane that carries the refused key behind an open live gate: the one-shot side-channel lane never carries hooks, so it is sent as before), the final check of the hooks plan against the settings-contract classifier before sending, partial layers, and, against the pinned server fixture, that a configuration the classifier accepts but the server's own limits refuse still fails through the generic invalid-request path. A permission-rule projection failure (for example missing core value ports) now refuses the turn instead of sending without any permission rules, and the thrown refusal carries the underlying assembly refusal as its `cause`. The host may declare its own memory switch with the same literal as the command-line flag: only exactly `off` turns memory off for the request (checked before the settings section), an absent flag leaves memory on and the request body without the key, and any other value is refused at the entry instead of being read as absent or as a spelling of `off`. |
627
627
  | `scripts/run-settings-credential-key-paths-test.mjs` | The one judge of which keys in a settings document hold credentials. It returns key paths only, never values, as findings that carry an optional `numericByNameOnly` flag for count-style names whose value is a plain integer (the flag is present only when true, so a restrictive file-mode decision can still count them while a user-facing notice leaves them out). Key names are judged on two deliberately wide tables, one for environment-style names and one for structured-config names; values are judged only through the display layer's known-secret shapes, URL userinfo and credential-bearing query strings, and an all-string array is read as a command line (`--password <value>`, `--token=<value>`, `NAME=<value>`, `Authorization: …`). Empty strings, null and false do not count. The golden cases, the residual boundaries (a single-letter short option is not judged, credentials embedded in command strings match only by known shape, an unprefixed random value under an ordinary name does not match) and the never-throws / never-leaks behaviour on cycles, getters, throwing proxies and very deep nesting are pinned, together with a differential against the settings package's own secret-token check and the reference client's name table when they are installed Since 0.101.0 it also pins that percent-encoded YAML block scalars and block sequences inside a string value answer exactly as their plain form does: a known credential shape inside an encoded block is found at every layer of encoding, malformed byte sequences next to it do not hide it, a label with a low-entropy value inside a string value is not found in either form, a block inside a URL query string is found through the query check, a whole address that is itself percent-encoded (credentials in its userinfo or query) is found as the plain address is while an encoded address without credentials is not, and three in-gate negative controls (without the known-shape check, without the query check, reading the address only unencoded) turn the cells red. Since 0.102.0 a string value is judged against the configured-address display outlet's own scan: whatever that outlet would mask as a credential (a scheme-less Slack webhook, a key in an address path, a single-slash address, `access_token=` in a fragment, a labelled value inside prose or a command string) is found, and an address whose only value is already a redaction marker is no longer found; the cells for those shapes, the labelled-value changes to the residual boundary and to the low-entropy block cells, and an in-gate negative control without that scan are pinned. The differential against the settings package's own secret-token check now pins one direction only: every value that check finds is found here too, and the values found only here (shapes the display outlet masks, such as a short `Bearer` value after a prefix) are listed one by one, so a new difference in either direction turns the cell red. |
628
- | `scripts/run-mcp-wire-caps-test.mjs` | The local MCP config projection. A server name goes on the wire byte for byte: a name with leading or trailing whitespace, an empty name, or a name that would collide with another entry once the whitespace is stripped is left out (not silently trimmed, which let ` blocked ` reach the wire as `blocked` and slip past a disabled-server list) and is reported by `projectMcpConfigs`, whose `dropped` rows carry a sentence for the user. `elicitation` is judged by one reader for both the local and the cloud-configuration leg (`mcpElicitationVerdict`): a boolean passes through as written (`true` and `false` alike), absence adds no key, and any other value, `null` included, leaves that one server out with a sentence naming the server and the key. A `toolAxes` declaration is judged by the engine's own reader through the core value port (`mcpToolAxesVerdict`): a bad shape, `null` included, leaves that one server out with a sentence naming the server, the key and the fix, the other servers are unaffected, and with no judge installed the declaration passes through unchanged. The suite feeds the products to the live server fixture's request-entry check: the 400 for a bad `toolAxes` is gone, and a non-boolean `elicitation` sent as written would still be refused by the server, naming the key, which is why neither leg sends it. Since 0.100.0 the name, transport and identity-header readers are the settings contract's own judges, copied at build time and read by both the local and the cloud-configuration leg, so the two legs answer every input the same way. A name that is empty, longer than 128 characters or has leading or trailing whitespace leaves its entry out with a sentence built on a bad-name summary that never echoes the whole name; a transport that cannot be used at all (no command, an address that is not an http(s) URL) leaves the entry out and names the key path; and a transport whose optional parts cannot be used (an argument list with a non-string member, an environment or header record that is not plain string data, a member literally named `__proto__`, a non-string principal header) keeps the entry on the wire as the judge's independent copy without those parts and reports what was left out — key paths and member names only, never a value. Setting both identity headers leaves the entry out with a sentence (it used to be skipped silently). The suite feeds these products to the live server fixture's request-entry check on both generations: every product is accepted, while the same declarations sent as written are refused. Nothing is capped at 32 servers any more: forty servers and a skill body above one mebibyte go on the wire as written, servers from 7.132.0 refuse the whole list or body with a field-invalid answer that names the limit, and older servers keep the first 32 on submission as before. A server start timeout passes through as written and a null server list is never produced. A standalone server-name verdict, `mcpServerNameVerdict(name, siblings?)`, answers whether one name can go on the wire byte for byte (usable, or one of four reasons: blank, edge whitespace, collides after trim, too long) with the same judge as the whole-table projection: on one table the two agree name by name. A collision needs the sibling names; without them only the other three reasons are judged. Non-string names count as blank, a non-iterable sibling list throws a type error, and the result is a fresh frozen object. The `toolFaces` declaration of a server entry is judged the same way: its shape is asked of the engine's own reader through the core value port, a malformed declaration keeps that one server off the wire (the other servers go as before) and is reported with the engine's own sentence, a well-formed one goes out as the engine's plain copy, and with no judge installed the value passes through untouched. A pool of three thousand seeded declarations is compared with the engine's reader sentence by sentence, and the published server's request-acceptance door shows which malformed shapes it refuses itself and which only the engine's own door would. |
628
+ | `scripts/run-mcp-wire-caps-test.mjs` | The local MCP config projection. A server name goes on the wire byte for byte: a name with leading or trailing whitespace, an empty name, or a name that would collide with another entry once the whitespace is stripped is left out (not silently trimmed, which let ` blocked ` reach the wire as `blocked` and slip past a disabled-server list) and is reported by `projectMcpConfigs`, whose `dropped` rows carry a sentence for the user. `elicitation` is judged by one reader for both the local and the cloud-configuration leg (`mcpElicitationVerdict`): a boolean passes through as written (`true` and `false` alike), absence adds no key, and any other value, `null` included, leaves that one server out with a sentence naming the server and the key. A `toolAxes` declaration is judged by the engine's own reader through the core value port (`mcpToolAxesVerdict`): a bad shape, `null` included, leaves that one server out with a sentence naming the server, the key and the fix, the other servers are unaffected, and with no judge installed the declaration passes through unchanged. The suite feeds the products to the live server fixture's request-entry check: the 400 for a bad `toolAxes` is gone, and a non-boolean `elicitation` sent as written would still be refused by the server, naming the key, which is why neither leg sends it. Since 0.100.0 the name, transport and identity-header readers are the settings contract's own judges, copied at build time and read by both the local and the cloud-configuration leg, so the two legs answer every input the same way. A name that is empty, longer than 128 characters or has leading or trailing whitespace leaves its entry out with a sentence built on a bad-name summary that never echoes the whole name; a transport that cannot be used at all (no command, an address that is not an http(s) URL) leaves the entry out and names the key path; and a transport whose optional parts cannot be used (an argument list with a non-string member, an environment or header record that is not plain string data, a member literally named `__proto__`, a non-string principal header) keeps the entry on the wire as the judge's independent copy without those parts and reports what was left out — key paths and member names only, never a value. Setting both identity headers leaves the entry out with a sentence (it used to be skipped silently). The suite feeds these products to the live server fixture's request-entry check on both generations: every product is accepted, while the same declarations sent as written are refused. Nothing is capped at 32 servers any more: forty servers and a skill body above one mebibyte go on the wire as written, servers from 7.132.0 refuse the whole list or body with a field-invalid answer that names the limit, and older servers keep the first 32 on submission as before. A server start timeout passes through as written and a null server list is never produced. A standalone server-name verdict, `mcpServerNameVerdict(name, siblings?)`, answers whether one name can go on the wire byte for byte (usable, or one of four reasons: blank, edge whitespace, collides after trim, too long) with the same judge as the whole-table projection: on one table the two agree name by name. A collision needs the sibling names; without them only the other three reasons are judged. Non-string names count as blank, a non-iterable sibling list throws a type error, and the result is a fresh frozen object. The `toolFaces` declaration of a server entry is judged the same way: its shape is asked of the engine's own reader through the core value port, a malformed declaration keeps that one server off the wire (the other servers go as before) and is reported with the engine's own sentence, a well-formed one goes out as the engine's plain copy, and with no judge installed the value passes through untouched. A pool of three thousand seeded declarations is compared with the engine's reader sentence by sentence, and the published server's request-acceptance door shows which malformed shapes it refuses itself and which only the engine's own door would. The toolAxes and toolFaces disclosure sentences are pinned to the new wording (started to read its tool list and then dropped). |
629
629
  | `scripts/run-panel-reduce-test.mjs` | The panel row reducer: one record per background agent, found through one identity index of engine ids — the fleet row id tail and the sub-run id (the id ticks and transcript anchors carry) are bound together only when a single frame states both, and two rows later proven to be the same agent are merged, the duplicate leaving with a `remove` that names the surviving row (`supersededBy`). A delegating card id never decides which agent a frame belongs to: it is a display field and an "already seen in this process" hint only. One generation rule, one place that constructs terminal facts and one place that emits a `terminal` row delta. Usage is monotonic within one known generation: a tick moves the numbers when its own cycle number matches the row (for an agent without cycle numbers the sub-run id decides), a row frame that binds a new sub-run id to the same tail starts a new generation, a terminal row freezes the phase but not the usage, and finality is reported per counter (`usage_delta.final`), a value-unchanged change of finality included. `reducePanelFrame(state, frame)` is pure — no I/O, no clock read (the projection frame carries its own `nowMs`), no module state, the input state is never mutated, the same input always yields the same deltas and replaying a frame yields none. The delta set is closed (`upsert` / `usage_delta` / `terminal` / `remove`); `subscribeEnginePanelRows` replays a snapshot on subscribe. Twelve end-to-end sequences cover interleaved lanes, the first-published id, revival, reconnect/absence, late subscription, short-lived agents in both orders, historic rows, absent usage never filled with zero, the turn-end sweep scope, the transitional event channel versus the row channel, bounded eviction and snapshot fidelity. The generation criterion now lives in a dependency-free leaf module that both the panel record and the fleet ledger share; each layer keeps its own state (the panel's per-row watermark, the ledger's per-task watermark) but the comparison and the notion of a valid cycle number exist once. A data-flow scan keeps cycle numbers and watermark readings out of every comparison operand in the ledger (with planted violations to prove the scanner speaks, and no false alarm on presence checks or shape tests), and the numeric view is pinned against an independently written integer comparison over a grid that includes malformed values. A background agent terminal notification may carry the child session id; the reducer treats it as binding evidence, so a host that feeds only task events (no fleet rows) gets one row per child with the engine's final numbers, and a revived child no longer keeps the previous generation's tool count beside the new generation's token count. A tick carries its engine cycle number: a background child usage is driven by ticks again when the tick cycle matches the row, a tick from a previous cycle is dropped whole, a tick from a newer cycle neither raises the watermark nor counts, and a terminal tick marks exactly the counters it really carried as final. |
630
630
  | `scripts/run-provider-preset-baseurl-mirror-test.mjs` | Mirror ledger for the one hand-copied engine default. The provider preset for the first-party hosted API carries a `baseUrl` that is a hand copy of the engine's own exported default root (the constant the engine falls back to when neither a model entry nor the deployment names a root). The guard reads the constant out of every readable copy of the engine package (the one this package is built against, an explicitly pinned one, and the fixture copy when present; an absent fixture is reported as a skipped section, and no readable copy at all skips the gate) with a TypeScript syntax-tree reader that accepts exactly one exported string-literal declaration, checks that the package root re-exports that very name, and requires the preset's `baseUrl` to match it byte for byte (a trailing slash, a changed letter or a different case is red). A census then requires that the only source file in this package that carries the hosted API's host name is the preset data file, so a second hand copy cannot appear unnoticed. Every ruler is proven against planted samples before it is trusted |
631
631
  | `scripts/run-engine-roster-peers-projection-test.mjs` | The leg's tool roster grouped by peer. For one source word (`mcp` or `a2a`; the word is an open set and is matched exactly), the rows the engine says came from that source are grouped by the peer name each row itself declares — byte for byte, never normalized and never recovered from the tool's name — so two servers whose names differ only in characters that a minted tool name flattens (`a.b` and `a_b`) stay two groups even when their tool names share a prefix. A row of that source that carries no declared peer is reported in a separate bucket instead of being guessed into a group. Each group lists its tools in roster order with their aliases and card identity where the row has them, the set of declaring routes its rows state (a peer declared through more than one route lists all of them), and a count; groups are ordered by peer name in UTF-16 code-unit order, independent of locale. Three kinds of absence stay apart: no roster, an unreadable one, or an unusable source word answers undefined (not known); a readable roster with no row of that source answers an empty result (stated zero, a fresh object each time); and rows without a declared peer land in the bucket. A row that cannot be read drops the whole answer rather than yielding a group that is one tool short, because a short group would report a mounted server as having fewer tools; the roster is read through the package's one roster reader, so a hand-built roster with a bad digest or count, or an array that skips rows while being iterated, answers undefined exactly as the roster projection does, and the result never shares a mutable array with its input. The projection never throws, including on a hand-built roster, throwing getters, proxy traps and peer names such as `__proto__`. One convenience entry point fixes the source word to `a2a`; the `mcp` case goes through the general entry point. The guard drives declared-name pairs that collide once flattened, an end-to-end pass from the raw assembly receipt, a seeded pool of 600 rosters compared against an independently written reference with a conservation identity, and a syntax-tree pin that the grouping function never reads a tool's name, calls a string transform, or uses a locale-dependent comparison. |
@@ -662,7 +662,7 @@ guard still cross-checks the table by name).
662
662
  | `scripts/run-session-rules-namespace-forms-test.mjs` | Session-rule coverage forms behind a capability bit. When the server reports, in its capabilities body, that the per-session rule record reads a whole server or agent peer (`mcp__srv`), a tool wildcard (`mcp__srv__*`) and a tool glob (`mcp__srv__get_*`) as one group of tools — and accepts them on write — `sessionPolicyDeliverable(behavior, rules, { namespaceForms })` lets exactly those three forms through, for the `mcp__` and `a2a__` namespaces alike. Every other reading of the bit (absent, false, anything other than `true`, unreadable, not yet observed) keeps them withheld, under a new cause word `namespace_covering_unconfirmed` when the reading was passed in; every other rule shape (`*`, `Bash*`, `mcp__*`, `mcp__*__x`, a partial wildcard in the peer part, a trailing separator, parentheses, edge whitespace, other globs in the tool part, case variants) keeps its previous answer; and not passing the reading at all gives answers byte-identical to the previous release. The reader `observedSessionRulesNamespaceForms` answers five states and only `confirmed` relaxes anything; the option reads only an own data field holding exactly `'confirmed'`. Every rule that is let through is accepted by the engine's session-rule write check and blocked by the engine's real session-rule gate for the tools it covers, and the roster check does not apply to these forms because they are not tool names. Seven hand-built broken implementations (relaxing on an absent reading, on no reading, on an unknown word, on non-confirmed states, one form too many, the whole-protocol name, or changing the answer when no reading is passed) must fail the same matrix; the server's own bytes are read when a 7.135.0 or later fixture is available. |
663
663
  | `scripts/run-cloud-skill-tool-lists-test.mjs` | The two tool lists on a skill from the cloud configuration, `allowedTools` and `disallowedTools`, go through the same single judge as the local skill projection (`skillToolListsVerdict`), so the two legs answer identically: with the capability reading passed to `projectEffectiveBody` (or `cloudSkillsToSpecs`), an absent list adds no key, a well-formed list goes on as written (an empty list stays an empty list, the caller's array is copied), and a malformed list (not an array, a non-string entry, a hole, an array that cannot state a valid length, or a read that throws) leaves that one skill off the request and adds a row to `droppedSkills` naming the skill and the key path, never echoing entries, while sibling skills are unaffected; a later skill of the same name does not take the dropped one's place. Without the reading option nothing is read and the output is exactly what earlier versions built. `cloudEffectiveNotices` turns the rows into one sentence. The suite also compares both legs for identical output across every reading and list shape, runs the real settings-contract manifest through the cloud leg, scans the source to pin that the cloud leg has no reader of its own, and checks every available server fixture for the pass-through point (present from 7.135.0, absent before). |
664
664
  | `scripts/run-web-search-sources-projection-test.mjs` | The search-source reader for web-search results. When the engine runs its web search tool, the tool-end event carries one structured card with the query, the duration and every hit that was shown to the model. The client rebuilds that card in the Claude Code `WebSearchOutput` shape (`query`, `results`, `durationSeconds` and an optional `searchCount`; no engine-only keys and no page text) as the structured tool result on both the interactive lane and the print lane, and exposes `webSearchSourcesOf(toolEnd)`, which flattens the hits into a frozen `{ title, url }[]`. Three answers are kept apart: `undefined` when the event carries no readable card (not this card, no `results`, a non-array, an unreadable or hostile carrier, or a non-empty list with nothing readable in it) — never an empty array; `[]` only when the engine itself reports zero hits (a failed search or a backend that answered nothing); otherwise the hits. Items are isolated one by one (a hit whose title is not a string, or whose URL is empty or not a string, is dropped alone and counted on a non-enumerable `droppedCount`), reads are bounded, and only own data properties are read. Titles and URLs go through the same display exit as every other untrusted wire text, so credentials inside addresses never reach the screen, while the card itself is carried verbatim. The suite runs the installed engine's real search tool to mint success, failure and zero-hit cards, parses the Claude Code interface declaration with the TypeScript syntax tree, drives both lanes, and feeds twelve hand-built bad implementations that must each be judged red. |
665
- | `scripts/run-mcp-config-entry-shape-test.mjs` | Shape of a configured MCP server entry, and a transport-only verdict. A server table handed to `projectMcpConfigs` may hold entries that are not objects (`null`, a string, a number, a list) or that cannot be read; each such entry now becomes one disclosure row (`entry_not_object` / `entry_unreadable`) while every other server is projected exactly as before, and nothing throws. `mcpTransportVerdict(entry)` judges only the transport half of one entry (`type`, `command`, `args`, `env`, `url`, `headers` and the two identity-header keys) with the same function the table projection uses, reads no other key, and answers `ok` with the transport and the sub-keys it dropped, or `bad` with the key path and the reason. The suite pins every non-object form at every table position, the order in which unreadable slots are judged, a golden of the released 0.100.0 output for a legitimate table (byte-identical, also with non-object entries interleaved), 3000 generated entries answered identically by the transport-only verdict, the single-entry export and the table projection, the single-source pin on the source syntax tree, and in-suite negative controls. A further section covers the cloud-configuration leg with the same entry cases: non-object entries, and getters or revoked or fully trapped proxies on the row-level slots and on `toolAxes`, `elicitation` and `allowTools`, each drop only that server (the new reason `entry-unreadable` for unreadable ones), transport-side outcomes and a disabled entry are answered first, each getter runs once, and the two legs answer the same input with the same word. |
665
+ | `scripts/run-mcp-config-entry-shape-test.mjs` | Shape of a configured MCP server entry, and a transport-only verdict. A server table handed to `projectMcpConfigs` may hold entries that are not objects (`null`, a string, a number, a list) or that cannot be read; each such entry now becomes one disclosure row (`entry_not_object` / `entry_unreadable`) while every other server is projected exactly as before, and nothing throws. `mcpTransportVerdict(entry)` judges only the transport half of one entry (`type`, `command`, `args`, `env`, `url`, `headers` and the two identity-header keys) with the same function the table projection uses, reads no other key, and answers `ok` with the transport and the sub-keys it dropped, or `bad` with the key path and the reason. The suite pins every non-object form at every table position, the order in which unreadable slots are judged, a golden of the released 0.100.0 output for a legitimate table (byte-identical, also with non-object entries interleaved), 3000 generated entries answered identically by the transport-only verdict, the single-entry export and the table projection, the single-source pin on the source syntax tree, and in-suite negative controls. A further section covers the cloud-configuration leg with the same entry cases: non-object entries, and getters or revoked or fully trapped proxies on the row-level slots and on `toolAxes`, `elicitation` and `allowTools`, each drop only that server (the new reason `entry-unreadable` for unreadable ones), transport-side outcomes and a disabled entry are answered first, each getter runs once, and the two legs answer the same input with the same word. The disclosure sentences now say what actually happened: a server dropped for a bad per-tool setting (allowTools, toolAxes or toolFaces) was started to read its tool list and then dropped, while the shapes decided before any connection still say not started. The gate pins the six sentences byte for byte, checks every local-leg catalog sentence belongs to exactly one of the two groups, and drives the real projection in both languages. |
666
666
  | `scripts/run-scenario-skills-projection-test.mjs` | Scenario-card skill list reader. From server 7.135.0 each scenario card carries an always-present `skills: [{ name, description, source }]` — the deployment skills handed to every run of that scenario, `source` being `factory` (the deployment's skill directory) or `center` (the configuration source), no bodies — and the capabilities body carries an always-true `scenarioSkills` flag (absent on older servers). `scenarioSkillsOf(card)` reads the list: an absent, non-array or unreadable `skills` answers `undefined` (never an empty list — `[]` means the server said the scenario has none), entries are isolated one by one (a malformed entry drops only itself), a `source` outside the two words is marked `unknown-source` without dropping the entry, nothing is trimmed, de-duplicated or reordered, and the text the engine injects into prompts is never scraped. `observedScenarioSkillsCapability(baseUrl?)` reads the flag from the landed capabilities (confirmed / absent / declined / unreadable / unobserved); it is evidence only and never a precondition for reading a card. The gate checks the shapes against the 7.135.0 server bytes, feeds the package's own list function's real output through the reader, and runs hand-made broken readers that must each turn it red. |
667
667
  | `scripts/run-mcp-allow-tools-fail-closed-test.mjs` | MCP server `allowTools` fail-closed. `allowTools` narrows which tools of one MCP server are mounted (an empty list mounts none, an absent key mounts all), so a malformed value must never be silently dropped. Both projection legs — the local `.mcp.json` leg and the cloud-config leg — now read it through one judge, `mcpAllowToolsVerdict`: a value that is not an array of strings (a non-array, a non-string element, a sparse array, or something that cannot be read) keeps that one server from starting and discloses a row that names the key path (`allowTools` or `allowTools[2]`) without echoing the value; an empty list is still sent, an absent key is still omitted, and what goes on the wire is a copy taken at read time. The gate runs both legs over every malformed shape, checks the two legs agree with the judge value by value, and runs hand-made legacy implementations (silent drop of a non-array, silent filtering of non-string members, holes accepted, shared reference, empty list folded to absent, value echoed, disclosure before the transport check, double read) that must each turn it red. |
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. |
@@ -718,7 +718,7 @@ guard still cross-checks the table by name).
718
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
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
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. |
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. Since 0.111.0 it also checks that a source word the package does not know still yields the reading and one cautious sentence in both languages (no echo of the word, no alternative exit, the way out chosen by the cause), and that an empty or non-string source word still reads as absent. |
722
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
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
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 |
@@ -731,21 +731,53 @@ guard still cross-checks the table by name).
731
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
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
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. |
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. When this client runs those events itself, the verdict rows (dropped entries, executor verdicts, exclusions, plugin verdicts, the hooks-not-run notice) match the same settings without those events; when it does not, each such entry adds exactly one not-running row (see the suite for client-run events without a runner). 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
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
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. |
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. The receipt reader observes every slot of a receipt exactly once: each slot's own property descriptor is read once (getters and proxy get traps never run, and a slot that exists only on the prototype chain does not count), nested rule lists and anchors are captured in the same pass, and every arm judges and copies only from that one observation. The guard checks that a getter answering a valid value first and a bad one afterwards never puts the bad value in the view, that an unreadable discriminating slot makes the whole receipt read as absent, that a proxy whose traps throw never makes the reader throw, that the view's rule list is a fresh array rather than the caller's, and that three thousand seeded JSON receipts read byte for byte as before. |
738
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
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
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
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. |
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. The three sentences that used to describe an edited approval that was not sent now share one catalog stem: the transcript line and the park-leg failure text each nest it, and the sentence port returns the stem itself. The transcript line keeps the bytes it shipped with in both languages; the gate checks the three derive byte for byte, that the failure text avoids the words the already-decided check looks for, and that no second copy of the stem lives in the source. |
743
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
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
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. |
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. Four additions from the newest library generation now have an explicit final state: the two lease lane and handover bits stay pending until the server ships its handover verbs, and the two workflow list-row bits (budget overshoot, timeout interruption) are declined because the status judge never receives them. The gate checks the facts behind that choice: the judge answers identically with and without them, the view and row key lists do not contain them, and the health reading ignores the lease lane and handover bits. |
747
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
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. |
749
+ | `scripts/run-package-subpath-exports-test.mjs` | The package's entry points and its Chinese catalog subpath. `package.json` `exports` is a closed set equal to the one registered in `scripts/_package-entries.mjs` (no wildcard keys; each entry exactly `types` + `default`, pointing at the compiler's per-file output), and `./i18n/zh` exports exactly `ZH_MESSAGES`. The root entry carries no Chinese catalog: its static import closure and a real browser bundle of it contain none of the catalog's modules or sentences (the bundle size is printed). A UI language whose catalog the host has not installed renders English, never an empty string or a key, records `lang:<language>` in the miss ledger and announces itself exactly once; the installer's refusal words, install-time snapshot and accessor-free reads are pinned. Bundled together with three resolution-condition sets (browser, Electron main process, Node), the root entry and `./i18n/zh` carry one copy of the catalog registry and the installed catalog renders key for key; a two-bundle control proves the check can tell two copies apart |
750
+ | `scripts/run-elicitation-leg-ownership-test.mjs` | Who reopens a stream that ends on the server's draining frame, and the line for an MCP form that cannot be shown. A stream that can be reopened as the same subscription (a run event stream the ask bridge holds a run handle for: an attached run, or a continuation the bridge opened after a decision) is reopened by the bridge on the package's one draining ladder, and the break is handed up once only after that ladder runs out. A live submit stream cannot be reopened as itself, so on a draining frame the bridge opens nothing: it hands the break up exactly once, naming the leg's bound run when known and never inventing one, and the leg ends like a dropped stream so the host's re-attach is the only reader; there is no host switch for this, and extra fields on the bridge dependency object do not change it. `elicitationRefusedLine` gives one line, in English or Chinese from the same catalog key, when a host could not show an MCP form and its decline took effect; any other outcome, a completion frame, another family or a malformed input gives no line, the server name passes through the untrusted display path, and the line never says the user declined. |
751
+ | `scripts/run-session-hooks-roster-test.mjs` | Two seat verbs. The Quick Entry shortcut: the seat capability `quickEntryShortcut` gates `getQuickEntryShortcut` and `setQuickEntryShortcut`; the state says which shortcut is saved, which one is registered right now (`null` when none, never an empty string) and why a saved shortcut failed to register; a refused change is a result with a closed reason (invalid request, unsupported combination, registration refused by the system, or the preference could not be written) that carries the unchanged state, never an error and never a false success. The session hooks roster: the seat capability `hooksRoster` gates `readSessionHooksRoster`, whose answer is produced only by `sessionHooksRosterOf(plan)`, a row-by-row projection of the same hooks plan that assembles the turn — event, matcher and hook index, entry type, status (engine, shell, not run, not sent) and reason, plus per-layer outcomes and HTTP allowlist drops. Every string in a roster is a closed-set word, so commands, URLs and user-written key names cannot appear. The guard checks both verbs' registration and declared signatures, both outbound checks (including the false-success and closed-leg contradictions), the shortcut pre-check, a host stand-in for each verb, a planted-secret sweep over a pool of plans, and a row-by-row comparison between roster and plan. |
752
+ | `scripts/run-subagent-model-ports-test.mjs` | The subagent model word ports share one reading. One vocabulary of engine words that catalog rows do not list (tier words, provider aliases, `best`, `default`) is read by both the word resolver and the catalog acceptance judge. `subagentModelAdmissionOf({ word, liveCatalog?, local? })` judges whether this engine admits an agent entry carrying the word the reading sends: it asks the same acceptance judge, except that the mention list does not gate agents and an engine word the catalog does not serve is reported as `not_served`; with no catalog from the engine the answer is unknown, never refused. `subagentModelAdmissionLine` renders the sentence for a refusal and `subagentModelCallRefusalReason` the sentence for a call-time refusal, both from closed reasons through the message catalog. The gate compares the admission verdict with the installed engine's tier expansion and the server's exact-key agent check on generated catalogs, and checks the vocabulary, `default`, both host assertion words, absence, hostile inputs and the sentences in both languages. |
753
+ | `scripts/run-thrown-slot-reader-test.mjs` | Two readers for the fields of a thrown value, both of which never throw. `thrownSlotOf(value, name, kind?)` reads one field of whatever was thrown — an error object, a proxy, a revoked proxy, `null` or a primitive. By default it reads along the prototype chain (where a built-in error's `name` lives); with `kind: true` it reads only the value's own keys; with `kind: 'data'` it reads only its own data fields, so an accessor is never run and a field planted on `Object.prototype` is never seen. A field that is absent and a field that cannot be read (a getter or proxy trap that throws, a revoked proxy, `null` or `undefined` as the thrown value) both answer `undefined`, so the result can be defaulted or printed directly; narrow with `typeof` before using it as a string or number. `errnoCode(value)` is the fixed call for the error code of a system error: the own, non-empty string `code`, and `undefined` for everything else. |
754
+ | `scripts/run-engine-unavailable-line-test.mjs` | What a client says when the engine cannot serve the session, and what the way out is. One reader takes the engine's start-up log lines: structured lines are read first and plain echoed text only as a fallback, and a refusal code is recognised only where the engine states it as a code. A data-folder refusal additionally yields its variant, the recorded engine version, the engine's own version and the marker path when the engine names them, and nothing when it does not. One line names the case, carries a machine-readable prefix and the exit details (none when unknown), and gives the way out for that case; the lane only changes how retrying is phrased. Ways out for a refused data folder follow how the client is installed: an update command is offered only to global and managed installs, a client started by the host never suggests overriding the data folder and instead names the configuration-folder variable the start-up table pins, and with no install shape the engine's own words are shown unchanged. Headless runs exit with 69. |
755
+ | `scripts/run-self-heal-conversation-left-test.mjs` | Conversation-left handling for the busy-session self-heal. A host can answer one optional question, `stillHere()` (is the conversation that issued this action still the one on screen?), and the package reads it one way only, as three states: a strict `true` means still here, a strict `false` means the conversation was left, and a reader that is supplied but answers anything else or throws means it cannot be confirmed; no reader at all keeps the previous behaviour. When a resend is about to be authorised but the conversation was left or cannot be confirmed, the outcome becomes `resend-withheld-left` or `resend-withheld-unconfirmed` (not delivered either way, so the host never sends the message into a different conversation) with its own sentences in both languages; the deferred-batch wait `waitForInjectedResubmit` asks the same question every round and settles `left` or `unconfirmed`; and ending a run after the conversation was left maps to six result classes with their own sentences. |
756
+ | `scripts/run-refusal-noted-projection-test.mjs` | Refusal notes on tool results. When a person refuses a tool call on the approval card and attaches a note, the engine records the note and the turn continues with that direction; a refusal without a note is a bare refusal. Tool result rows (interactive transcript and print frames) carry `_sema_refusal_noted: true` when the same row's denial kind is `user-rejected` and its gate record is a person's refusal whose note is present (empty or non-string notes count as absent), read with `toolResultRowFlagsOf(row).refusalNoted`; the key name is exported as `SEMA_REFUSAL_NOTED_KEY` and listed in `TRANSCRIPT_ROW_SIDECAR_KEYS`. Subagent content events, items and view slots carry the same facts as `denialKind` (the same closed vocabulary as the main transcript) and `refusalNoted`, on the live stream, the background tail and replay alike, and the seat validator accepts only those values. The gate checks every refusal and note shape on both lanes, row self-consistency, key order, replay equivalence, the reader's isolation, the three subagent views, the seat contract, and by syntax tree that the key name and the rule each live in one place. |
757
+ | `scripts/run-decide-ack-outcomes-test.mjs` | Tool-permission receipts on the seat answer one question, whether the user's decision is in effect right now, with exactly three answers: yes (`ok: true`), no (`ok: false`), or not known (`ok: false` with an `unconfirmed` slot naming the cause). The cause words are the package's existing words for a decision that got no verdict (`transport`, `server_error`, `local_error`, `aborted`, `gone_after_retry`) plus `registry_unreadable` for a host that could not read the pending-request list when it settled the receipt. The `unconfirmed` and `settledElsewhere` slots never appear together, `unconfirmed` always comes with `ok: false`, and when `settledElsewhere` is present `ok` must say the opposite of `contrary`; the receipt's key set is closed. A receipt with neither slot is still valid and reads as before. One reader turns a receipt into applied, not applied or unconfirmed and judges with the same check the outbound gate runs, a sentence helper gives the unconfirmed line in both interface languages (the after-retry cause reuses the existing sentence), and a mint helper turns a thrown decision error into the slot using the same classification the decision sender already applies, returning nothing when the engine gave a verdict. The guard covers every arm accepted and every malformed or contradictory form refused with the slot named, accessors never run, and the three counterexamples: a server error folded into a refusal, an unreadable pending-request list folded into success, and both slots in one receipt. |
758
+ | `scripts/run-engine-swap-crossing-test.mjs` | One judgement before any client stops or replaces the local engine (`engineSwapCrossingVerdict`). Whether the running engine is newer than this client is taken only from the local engine judge's verdict, never compared a second time: a newer running engine is never stopped, and no confirmation changes that. When this client is newer, work still waiting on the running engine that may not survive the version change holds the replacement while it can still clear (waiting approval cards counted per session, or a reading that failed this time); a class the engine cannot report at all is listed but does not hold. When the direction cannot be read, the engine is not replaced automatically. A confirmation releases a hold only if it names the fingerprint of the exact verdict the person saw, so one more waiting card or a different engine instance asks again, and the replacement then says what may have been lost. The fallback reading (`parkedToolApprovalsBySession`) counts, from the approvals feed as emitted, only rows on the approval decision path whose engine verdict is absent or ok; anything it cannot read fully is a failed reading, never zero. Sentences come in English and Chinese (`engineSwapCrossingText`). Accessors are never executed and hostile inputs never throw. |
759
+ | `scripts/run-attachment-route-test.mjs` | The attachment-route gate. Whether a file's contents may be read on this machine and handed to the model depends on whether the session's tools work on this machine's files. The package exposes one judge, `attachmentRouteOf`, which takes the kind of attachment (an @-mention, a changed-file note, a paste, an editor selection) and the host's reading of where the tools run (this machine, elsewhere, or unconfirmed) and answers: attach, attach with a note saying the content is this machine's copy, or withhold with one of two reasons. A location that cannot be read is treated as unconfirmed, never as this machine and never as a confirmed elsewhere, and accessors are never executed. The guard pins every cell of the judge including hostile inputs, the two reasons in both languages for the person and the model-facing clause in the attachment envelope (both drawn from the message catalog, so the two faces cannot disagree), the note for this machine's copy, the six attachment kinds a host routes through the judge, and that no other module reads the location or spells the two reasons. |
760
+ | `scripts/run-resupply-truncated-projection-test.mjs` | Resupplied notifications that arrive in their minimal form. When the server cannot fit a resupplied `task_notification` or `workflow_complete` frame into its size budget it sends only the minimal form and marks it with `payloadTruncated` (the full byte count and the names of the keys it left out). The client turns a well-formed mark into one additive chrome event, `resupply_truncated`, carrying the source frame, the task or run id, the byte count, the dropped key names and one sentence in the interface language: the notice was shortened, the full record is that many bytes and over the notice size limit, so that many fields were left out, and the full content can still be viewed in the run record. The sentence counts the dropped keys and never names them. An absent mark (a full frame, a live frame, an older server) says nothing; a malformed one (a byte count that is not a positive integer, a key list that is empty, too long or holds anything but non-empty names, an accessor, a trap) is dropped as a whole, never filled in with a zero or half kept. The event is sent once per delivered frame, before the transcript row for a task notification and after the enqueue event for a workflow completion (a consumer that stops reading at the event has already been handed the enqueue event, so a completion is never marked as delivered without it), and it passes the seat gate. The gate drives real frames through the adapter and the projection door for both frames and both languages. |
761
+ | `scripts/run-screen-decision-cards-test.mjs` | Facts of a decision card on screen and the engine-generation card plan. Each card has three facts with one reader per carrier: its kind (`SCREEN_CARD_ORIGINS`, read from an approval card with `approvalCardOriginOf` and from a question id with `questionCardOriginOf`), the gate it asks about (`approvalCardGateOf`, `questionFrameGateOf`: a tool gate is the run plus the gated call, a plan gate is the run plus the checkpoint) and the engine generation it came from (`sourceGeneration` on the approval card request, `questionFrameSourceGenerationOf` on a question frame; read before the pending list is read, or when the stream was opened, and absent when it cannot be read). `engineGenerationCardPlan` judges both beats with the same card type: when a generation ends only a live card whose window is certainly open is retracted (the existing convergence judge decides), and once the next generation is ready a card is withdrawn only when it is a withdrawable kind, has a gate, is from an earlier generation, matches a row owned by this session and that row's verdict is not ok; every other card is kept with one of `SCREEN_CARD_KEEP_REASONS`. The suite pins each reader against parked and live cards of both gate kinds and their absence, the generation timing, the golden cases exported with the package, a seeded comparison against an independent oracle, the recheck-due predicate, the engine-decision truth table and the declaration files. |
762
+ | `scripts/run-decision-refusal-carrier-test.mjs` | Cross-process carrier for refused decisions. `decisionRefusalCarrierOf` turns the error a refused approval decision threw into a plain object that is safe to send through structured clone and carries exactly the fields the package's own failure readers read on that error (or hand back unchanged): no field whose name looks like a credential at any depth, and nothing nobody reads. Before returning it, the package checks that every one of its failure readers gives the same answer for the carrier as for the original error; when it cannot prove that (an unreadable field, a reader that depends on the error class, or a credential-named field a reader needs) it returns nothing, and the host keeps its generic failure path. The guard compares every root failure reader, with every closed-set argument, on the original error, the carrier and its structured clone across the decision-refusal families raised by real SDK errors, and checks that credential-named keys and their values never appear, that unread body fields are left out, that every carrier path exists on the original, that hostile shapes give nothing without running getters, that the carrier is deeply frozen, that a seat renderer reading the cloned carrier reaches the same verdict and sentence as the host, and that every root failure reader is on the carrier's reader list. |
763
+ | `scripts/run-plan-review-frame-claim-test.mjs` | Plan-review claim for hosts that intercept the parked terminal frame. `openPlanReviewFrameClaim()` gives such a host one claim leg per stream: it feeds the raw frames in arrival order, and on the terminal frame of a run parked at a plan gate the claim commits and returns the same frozen plan-review card facts the stream adapter would have committed, into the same registry, synchronously and without I/O, subscriptions, chrome events or question frames; both entries call one per-frame step. The guard runs a twelve-shape matrix through the stream adapter and through the claim and requires byte-identical facts, covers the browser shape where only the plan-call frames reach the stream adapter, checks that a read with the gate identity then answers from the registry without reading the run ledger, that other gate kinds and completed runs give nothing on both entries with no registry write, that a repeated durable frame id does not take a second reading, that bad input never throws, and that both entries call the one step in source. |
764
+ | `scripts/run-pending-user-row-projection-test.mjs` | The row-id shape of a queued user message. A queued message appears in the transcript as a row whose id is the fixed prefix `pending-` followed by a version-4 UUID. One minting function builds that id from a UUID (it returns undefined for anything that is not a well-formed version-4 UUID, without guessing or normalising case); one reader hands the UUID back from an id, byte for byte, only when the id is exactly what the minting function can produce, and returns undefined for anything else; and one predicate answers yes or no and is defined through the reader, so there is no second reading of the shape. Hostile input (accessors, throwing proxies, objects with their own `toString`) is never touched and never throws. The suite pins golden cases, twenty-two malformed UUIDs, twenty malformed ids, a three-thousand-case agreement run between the three functions, and a scan showing the prefix literal appears at exactly one place in the sources |
765
+ | `scripts/run-builtin-output-styles-test.mjs` | The table of built-in output styles: the five names `default`, `Proactive`, `Concise`, `Explanatory` and `Learning`, in the order of the reference implementation, as a deeply frozen read-only table plus one lookup function. `default` maps to null (no style text); the others carry a name, a source marker, a one-line description, the keep-coding-instructions flag and the full style prompt, and the first two also carry the short per-turn reminder sentences that the reference attaches to them. The lookup returns the very table object for a known name and undefined for everything else (other letter case, surrounding whitespace, prototype-chain names, non-strings) and never executes anything on its input. When the reference implementation's decoded source is on disk the suite compares every prompt and reminder byte for byte against it (the only substitution is the product name at the start of each description), checks that the reference's set of built-in names equals this table's names exactly, and checks that the prompt text lives in exactly one source file; without that source the comparison is skipped honestly, and a required-fixtures run turns the skip red |
766
+ | `scripts/run-url-query-label-stop-test.mjs` | The stop point of a credential label's bare value inside a URL query. Two layers read the text independently. The URL-structure layer, when it runs alone, does not look at labels: it only replaces userinfo, query values and fragments, and prose after the end of the URL stays byte for byte. The word layer recognises a credential label and reads its value up to its own stop characters: inside a query the value stops at the first `&` or `#`, the same as the region boundary, and otherwise at the usual bare-value stops; a vertical bar is not one of them, so a label value may run past the end of the URL and the part past it is redacted as a secret together with the rest. The direction is to redact more, never less, because a value such as an application id and a secret joined by a vertical bar must not leave its second half on screen. The suite pins the bytes of both layers on the `&`, `#`, past-the-end, quote and non-credential-key forms and pins that the written statement of the layer-one guarantee is scoped to that layer |
767
+ | `scripts/run-compaction-stop-verdict-test.mjs` | The stop verdict for a compaction in progress. When the user stops a turn while a compaction runs, `compactionStopVerdict(tracker.reading())` answers `compaction_canceled` only when exactly one main-lane pass is open and its trigger is exactly `manual`; every other reading (an automatic or forced pass, an unknown trigger word, no open pass, two passes open at once, or a reading that cannot be read) answers `interrupted`, so the host keeps its generic interrupted line. The reading gains `openTriggers`, the triggers of the open main-lane passes in opening order, present only while compacting and kept on the tracker's single pass ledger. The canceled sentence is checked byte for byte against the upstream reference line when that corpus is on disk, and the frame order (the outcome frame arrives after the stop) is checked with the server's own frame builders |
768
+ | `scripts/run-host-setting-notice-projection-test.mjs` | The host settings notice arm. Every sentence a `hostTurnSettings()` product says (turn, session or diagnostic plane) becomes one `host_setting_notice` chrome event through the single minting function `hostSettingNoticeEventsOf(notices)`, whose payload is the notice itself; the arm is on the seat list, so a seat host forwards it unchanged and both renderer lanes read one arm. A sentence minted for one row of the product carries that row's `key` and `cause`, and the refusal sentence of the verdict is the very same object as the one in the list. The arm is not a terminal event: it carries no error or outcome field. Malformed entries are skipped one by one, malformed row bits drop only themselves, and accessors are never run |
769
+ | `scripts/run-prompt-suggestions-tail-test.mjs` | The follow-up for prompt suggestions after a completed turn. A suggestion batch has one minting rule, shared by the stream frame and the run record, and one arm. The main path is the stream frame, which servers from 7.149.0 put before the terminal frame; `followUpPromptSuggestions` asks the run record only when the main path cannot already have delivered: not when suggestions were not requested, not when the frame was seen on the stream, and not when the stream reached its terminal frame on a server that sends the frame first (`promptSuggestionsOnStream`, which reads the capability bit together with the server version because older servers report the bit as true on every leg). Polling is bounded by six requests and twelve seconds (no request starts once the budget is spent, and a request in flight waits only for what is left of it), stops on an empty batch or on a run that did not complete, and mints the same arm the stream would have produced |
770
+ | `scripts/run-approval-card-request-assembly-test.mjs` | The single assembly path for an approval card request. `approvalCardRequestOf(facts, host)` turns a card-facts reading (the output of `approvalCardFactsOfV1Frame`, `approvalCardFactsOfFrame`, `approvalCardFactsOfGeneric` or `approvalCardFactsOfV1`) plus the host positions `ApprovalCardHostBits` (`callKey`, and optionally `taskId`, `signal` and the four window positions read by `approvalWindowKeysOf`) into an `ApprovalCardRequest`, and the package's own live-card path uses the same assembly. The guard reads the key sets of the request, facts and host-position types from the shipped declarations with the type checker and requires every request key to be classified as a fact, a host position, or one of the five positions only the package's own card paths stamp, so a new request key that nobody classified fails the gate. Every fact key and every host key is fed alone and all together and must arrive as the same value; keys outside each closed set never cross (a host cannot overwrite a fact, and a facts reading cannot smuggle a host or package-only position); only own data properties are read, so getters never run, inherited and undefined-valued keys are left out and a throwing read drops only that key; malformed inputs answer undefined and nothing throws. Real readings from the three projection entries equal the earlier spread form key for key and in the same order, the four positions a hand-picked copy used to drop arrive intact, and the request the live-card path hands to the card port equals the assembly of the same frame's projection with the card's host positions, with a syntax-tree check that the live path calls the assembly instead of spreading the facts inline. |
771
+ | `scripts/run-session-read-redaction-projection-test.mjs` | Session-history redaction and store durability. `sessionReadRedactionOf` is the one reader for the capabilities bit `sessionReadRedaction`: the boolean `true` of earlier servers, or a non-empty list of leaf names (passed through exactly as sent), reads as `redacted`; an absent bit, an empty list, `false` or any malformed form reads as `unconfirmed` and is never taken to mean the history is unredacted. `readEngineHealthStore` reads `/health.store.durability` into `reported` (durable, ephemeral, or the engine's own statement unknown, which is never folded into either of the others), `unreadable` (sent but not understood; a word outside the three comes back verbatim) or `absent` (not sent: an older server, or an anonymous reader). Checked against the real bytes of two server generations; both readers never throw and never run an accessor. |
772
+ | `scripts/run-store-posture-durability-projection-test.mjs` | How the store posture reads whether a replica is durable across engine generations. Older engines report a durable boolean; newer ones report one three-state durability word (durable, ephemeral, unknown) that is the same assembled value as the health report's. Exactly one generation's key must be present: a word outside the three is carried as reported and reads as unknown rather than being folded into a known word, both keys at once or neither discards the whole reading, and "unknown" is never read as "not durable". The doctor line and its head word come from the message catalog in both languages, with the older-engine sentences unchanged byte for byte; the ephemeral sentence does not claim the state is held in memory. Checked against both engine generations' own projections and member lists, and the three words are recognised in exactly one place in the package. |
773
+ | `scripts/run-local-allow-redemption-test.mjs` | Redeeming a "don't ask again" choice made on a read-only approval path (a parked pending row, or the v1 ask frame, whose answer body has no rule slot). A local-settings allow line never takes effect on its own: the settings contract places that layer below the floor for allow rules, so the only consent that counts is a live rule of the same identity (allow, exact text, the same project root or global) in the engine's rule store. One judge, `localAllowRedeemability`, answers for a line already on disk and for an option about to be shown, in three states: redeemable (by the layer itself, or by consent in the store), not redeemable with a closed reason, or unknown with a closed cause; it never throws and never runs accessors. A repository-shipped allow line with no matching consent is judged not redeemable, whatever the workspace trust reading says. The redemption call, `redeemReadOnlyRuleOffer`, asks the same judge first, applies the five shape narrowings, then sends a settings import that contains only the chosen rule, never the settings file itself, and redeems the ticket only when the preview is exactly that one rule at that project root. Its receipt is one of four closed arms: applied, refused (certainly not stored), unavailable (the route is closed or cannot be reached), or unknown (the redemption was sent and the outcome cannot be told). The approval card's don't-ask-again row for the read-only slot reads the same judge. |
774
+ | `scripts/run-health-probe-verdict-test.mjs` | The verdict on one liveness probe of an engine's health endpoint, read in one place: the probe was not answered at all (it timed out or could not connect), was answered healthy, or was answered but not healthy (a non-success status code, or a success status whose body does not report that the engine is ok). One function folds the four arms of the SDK probe result into those three states and never reports an unreadable result as "did not answer"; one function turns a verdict into a short clause in English or Chinese that, for the answered-but-not-healthy state, names only the HTTP status code; one selector picks, from the observations of one unhealthy stretch, the most recent that was answered, ignoring observations older than the stretch and skipping malformed ones. The guard drives the real SDK probe against local servers (healthy, a failing status, a success status whose body reports draining, no answer, nothing listening), a hand-written table of the four arms with the status-code boundaries, unreadable and hostile inputs (accessors, prototype-chain values, trapping proxies) that must neither throw nor run a getter, the exact sentences in both languages, and a seeded random pool compared with an independently written reference selector. A syntax-tree scan then pins that the only place in the package that reads the probe result's liveness flag is the verdict function, that every call of the SDK probe hands its result straight to it, that the package's own liveness helper is derived from it, and that verdict objects for the two answered states are built in that one module only; a planted violation proves the scanner can see. Finally the liveness helper is compared with the SDK's own flag on real servers, so deriving it from the verdict changed no answer. |
775
+ | `scripts/run-hooks-shell-owned-no-executor-projection-test.mjs` | Hooks written under client-run events say so when nothing runs them. Hook events the engine never fires (every event in the settings contract except the nine engine events) never go on the wire, so whether such a hook runs depends only on whether this client's own hook runner runs that event and hook type. When it does, nothing is recorded; when it does not, the plan's dropped-hooks table carries exactly one row for the entry with the reason `no_executor` (the same word and sentence plugin and session hooks already use for that case), marked as running nowhere, and the hooks-not-run notice says one sentence per settings layer. The engine-contract reasons (exec-form commands, `mcp_tool`, unread keys, an authored group `source`) are judged on engine events only. Governance still comes first: a closed hooks leg, managed-only hooks, safe mode and workspace-trust withholding keep their own rows and produce no `no_executor` rows. The suite covers all five settings layers times every client-run event, the runner's event and type tables (absent, listed, unlisted, malformed), the three hook sources, malformed documents and misspelled event names, pins the single place the row is minted, and sweeps every plan it builds. |
776
+ | `scripts/run-attachment-name-cc-form-test.mjs` | Entity-form display for attachment names and paths. `showAttachmentNameCc(name)` writes `<` and `>` as `&lt;` and `&gt;` and every C0, DEL, C1, line-separator and paragraph-separator code unit as a decimal `&#N;` reference, and leaves everything else (including `&`, quotes, backslashes, format characters and lone surrogates) as it is. The gate walks a 103-row rule table, 4000 deterministic random strings against an independent per-code-unit reference, idempotence, a two-form output table (the existing JSON-style form is untouched and shares no code with the entity form), and source pins that keep the two forms independent. |
777
+ | `scripts/run-single-reading-doubles-test.mjs` | Two former double readings, each now a single source. The note a person writes on a Yes answer is judged by one reader (it counts as written when it is a string that is not all whitespace), and the decision sender's entry snapshot asks that reader instead of keeping an inline copy of the rule. The suite compares the snapshot's outcome fact against the reader over sixteen shapes (strings with outer whitespace, empty and all-whitespace strings including full-width and no-break spaces, a zero-width space, non-strings, absent, an accessor that must not run, an inherited name, a proxy that throws on read) and runs a syntax-tree census over the source: only the reader's own file reads the note slot, and the snapshot function calls the reader and never trims. The session-push row-bits word list is derived from one table that also says which word speaks, so a word added upstream cannot be recognised by one list and missed by the other: each of the four words appears exactly once in code outside type positions, only the writer-side word speaks, and names inherited from the object prototype are neither words nor errors. No behaviour changes. |
778
+ | `scripts/run-interrupt-deny-delivery-test.mjs` | Interrupt denials take each leg's ordinary decision delivery: when a turn is stopped while an approval card is open, the tool-level deny that settles the pending question is sent like any other answer on that leg — on the live card leg through the bounded transport retry and the one invited re-send after a 202 parking reply, on the durable approval and question legs through the decide bridge's own retry — with a lifetime of its own (the turn's signal is already aborted) equal to one delivery budget. The warning that the session may stay locked is shown when that delivery has failed after its last attempt, or when the budget runs out with no answer; `CANCEL_DENY_BUDGET_MS` is that budget (8000 ms). A retry that finds the question already settled stays silent, at most one of the sends is accepted, and a second caller for the same card joins the delivery in flight instead of presenting another card. |
779
+ | `scripts/run-p2-clearance-gate-test.mjs` | A release-closing ledger gate. Before the release that closes the clearance window, every earlier committed P2+ item must be closed or deferred with a falsification note. The gate reads the clearance queue and the ledger status column, counts the items still open (open, in progress, blocked, resolved but unverified, or deferred only on cost), prints one machine-readable count line plus a per-item list, and turns red only when the package version is the gate version or later and the gate version's changelog section already carries a release date; before that it reports the count and passes. When the ledger files are not reachable (for example a remote container without the sibling repository) it skips honestly and says so instead of passing. Self-checks cover every status form, the version and date matrix, and negative controls on throwaway ledger copies. |
780
+ | `scripts/run-server-7148-pick-projection-test.mjs` | What the client reads from the newest server release and client library generation. A refusal's machine-readable facts are read from one place, the root of the refusal body, for every server in the supported range: older servers that nested some facts under `detail` are normalized once where the error is first read, so hook-setting refusals, conflicting-field refusals, rule-entry problems, attachment refusals and scenario refusals read the same on both shapes, and a hook refusal names the exact setting coordinate on both. The anonymous `/health` reading only yields the facts the server shows to callers without credentials; everything else reads as absent, never as zero or false. A 404 for an approval question reads as not settled. The Esc interrupt sends exactly one request on both library generations, and a cancelled run whose heartbeat had lapsed is read and worded as such. Exercised against the server's own refusal minters and health route on two server generations, through the real library error mapper and run resource. |
749
781
 
750
782
  Each suite carries a floor that only moves up — a refactor that stops executing a group of
751
783
  assertions is a failure, not a quieter pass. Guards anchor on the **installed artefact's content**