@sema-agent/client-core 0.97.1 → 0.98.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.
- package/CHANGELOG.md +408 -0
- package/README.md +74 -37
- package/dist/adapt/arms.d.ts +1 -0
- package/dist/adapt/arms.js +128 -34
- package/dist/adapt/ids.d.ts +12 -1
- package/dist/adapt/ids.js +59 -8
- package/dist/adapt/instanceLedger.d.ts +50 -0
- package/dist/adapt/instanceLedger.js +147 -0
- package/dist/adapt/textStream.d.ts +1 -0
- package/dist/adapt/textStream.js +24 -7
- package/dist/adapt/toolCards.d.ts +27 -5
- package/dist/adapt/toolCards.js +39 -6
- package/dist/adapt/wireShapes.js +6 -2
- package/dist/adapt.d.ts +2 -0
- package/dist/adapt.js +8 -3
- package/dist/adapter/activeRunSelfHeal.d.ts +5 -0
- package/dist/adapter/activeRunSelfHeal.js +51 -28
- package/dist/adapter/downstream/eventToSdkMessage.d.ts +2 -5
- package/dist/adapter/downstream/eventToSdkMessage.js +27 -34
- package/dist/adapter/downstream/terminalToSdkResult.js +11 -4
- package/dist/adapter/downstream/turnUsageToModelUsage.d.ts +5 -2
- package/dist/adapter/downstream/turnUsageToModelUsage.js +2 -0
- package/dist/adapter/runStream.js +29 -13
- package/dist/adapter/types.d.ts +4 -4
- package/dist/agentSession/backgroundView.js +2 -1
- package/dist/agentSession/engineAgentRegistryRead.d.ts +2 -2
- package/dist/agentSession/engineAgentRegistryRead.js +12 -13
- package/dist/agentsWireCaps.d.ts +8 -8
- package/dist/agentsWireCaps.js +168 -175
- package/dist/approvalDecidedByCapability.d.ts +0 -4
- package/dist/approvalDecidedByCapability.js +0 -6
- package/dist/approvalsStreamLiveCapability.d.ts +0 -4
- package/dist/approvalsStreamLiveCapability.js +0 -6
- package/dist/assistantInbox.d.ts +34 -0
- package/dist/assistantInbox.js +118 -0
- package/dist/centerWiringCapability.js +9 -1
- package/dist/clientSlice.d.ts +0 -9
- package/dist/clientSlice.js +0 -3
- package/dist/cloudConfigWireCaps.d.ts +4 -2
- package/dist/cloudConfigWireCaps.js +81 -61
- package/dist/compensations.js +1 -1
- package/dist/connectorStatus.d.ts +21 -0
- package/dist/connectorStatus.js +112 -0
- package/dist/controlRouter.d.ts +14 -1
- package/dist/controlRouter.js +68 -17
- package/dist/coreValuePorts.d.ts +3 -3
- package/dist/decideReceipt.d.ts +2 -2
- package/dist/decideReceipt.js +5 -2
- package/dist/declared/supportFloorDeclared.js +3 -3
- package/dist/deviceExecutorManagementCapability.d.ts +0 -4
- package/dist/deviceExecutorManagementCapability.js +0 -6
- package/dist/deviceRebind.d.ts +49 -1
- package/dist/deviceRebind.js +221 -3
- package/dist/displayUntrusted.d.ts +1 -0
- package/dist/displayUntrusted.js +250 -76
- package/dist/engineAgentAbsence.d.ts +1 -1
- package/dist/engineAgentPanelStore.d.ts +5 -4
- package/dist/engineAgentPanelStore.js +3 -12
- package/dist/engineCapReader.d.ts +0 -4
- package/dist/engineCapReader.js +1 -5
- package/dist/engineCapsCache.d.ts +4 -0
- package/dist/engineCapsCache.js +17 -12
- package/dist/engineClosedSets.d.ts +1 -0
- package/dist/engineClosedSets.js +4 -1
- package/dist/engineErrorCodes.d.ts +9 -2
- package/dist/engineErrorCodes.js +9 -1
- package/dist/engineIdentity.d.ts +1 -0
- package/dist/engineIdentity.js +8 -0
- package/dist/engineRowRef.d.ts +28 -0
- package/dist/engineRowRef.js +111 -0
- package/dist/engineSessionMessages.d.ts +67 -0
- package/dist/engineSessionMessages.js +364 -0
- package/dist/engineToolDirectory.d.ts +0 -9
- package/dist/engineToolDirectory.js +1 -94
- package/dist/engineUrl.d.ts +18 -0
- package/dist/engineUrl.js +52 -0
- package/dist/env/uiLanguage.d.ts +9 -0
- package/dist/env/uiLanguage.js +35 -0
- package/dist/eventLeg.d.ts +92 -0
- package/dist/eventLeg.js +285 -0
- package/dist/executionLaneCapability.d.ts +0 -4
- package/dist/executionLaneCapability.js +3 -16
- package/dist/fileHistoryCaptureCapability.d.ts +0 -4
- package/dist/fileHistoryCaptureCapability.js +5 -7
- package/dist/fleet/fleetLedger.d.ts +0 -3
- package/dist/fleet/fleetLedger.js +21 -36
- package/dist/fleet/fleetProjection.d.ts +2 -2
- package/dist/fleet/fleetProjection.js +10 -8
- package/dist/frameBinding.d.ts +5 -0
- package/dist/frameBinding.js +23 -0
- package/dist/gateFailureCodes.d.ts +2 -0
- package/dist/gateFailureCodes.js +15 -0
- package/dist/gateOutcome.d.ts +3 -1
- package/dist/gateOutcome.js +25 -6
- package/dist/gateVocabulary.d.ts +5 -0
- package/dist/gateVocabulary.js +64 -2
- package/dist/generated/engineAskFaceTables.js +2 -2
- package/dist/generated/engineFactTables.d.ts +6 -3
- package/dist/generated/engineFactTables.js +22 -4
- package/dist/generated/engineNoticeTables.js +9 -0
- package/dist/generated/toolNameTables.d.ts +1 -0
- package/dist/generated/toolNameTables.js +1 -0
- package/dist/hitl/allowRuleShadow.d.ts +6 -1
- package/dist/hitl/allowRuleShadow.js +21 -17
- package/dist/hitl/approvalCardReading.d.ts +15 -0
- package/dist/hitl/approvalCardReading.js +154 -0
- package/dist/hitl/approvalCardReasons.d.ts +13 -0
- package/dist/hitl/approvalCardReasons.js +120 -0
- package/dist/hitl/approvalHolds.d.ts +47 -15
- package/dist/hitl/approvalHolds.js +134 -28
- package/dist/hitl/approvalResolution.d.ts +51 -26
- package/dist/hitl/approvalResolution.js +86 -122
- package/dist/hitl/approvalsFeed.js +6 -18
- package/dist/hitl/askClassPair.d.ts +2 -0
- package/dist/hitl/askClassPair.js +1 -0
- package/dist/hitl/askGateWire.d.ts +20 -2
- package/dist/hitl/askGateWire.js +49 -1
- package/dist/hitl/askParkRowRouting.d.ts +5 -0
- package/dist/hitl/askParkRowRouting.js +32 -1
- package/dist/hitl/askSettlementLine.d.ts +18 -0
- package/dist/hitl/askSettlementLine.js +79 -0
- package/dist/hitl/automaticAnswerVerdict.d.ts +1 -1
- package/dist/hitl/automaticAnswerVerdict.js +21 -5
- package/dist/hitl/crashConverged.d.ts +11 -20
- package/dist/hitl/crashConverged.js +56 -0
- package/dist/hitl/crashConvergedNotice.d.ts +3 -1
- package/dist/hitl/crashConvergedNotice.js +48 -11
- package/dist/hitl/decisionConflict.js +3 -10
- package/dist/hitl/decisionRefusal.d.ts +40 -1
- package/dist/hitl/decisionRefusal.js +199 -42
- package/dist/hitl/frameRouter.d.ts +5 -2
- package/dist/hitl/frameRouter.js +20 -16
- package/dist/hitl/gateLedger.d.ts +2 -0
- package/dist/hitl/gateLedger.js +50 -13
- package/dist/hitl/hitlBridge.d.ts +1 -2
- package/dist/hitl/hitlBridge.js +10 -7
- package/dist/hitl/livePendingAsk.d.ts +3 -3
- package/dist/hitl/livePendingAsk.js +7 -18
- package/dist/hitl/parkResolver.d.ts +2 -24
- package/dist/hitl/parkResolver.js +61 -97
- package/dist/hitl/persistedRulesWire.d.ts +24 -0
- package/dist/hitl/persistedRulesWire.js +122 -2
- package/dist/hitl/planReviewWire.d.ts +26 -13
- package/dist/hitl/planReviewWire.js +112 -78
- package/dist/hitl/refusedToolNameProse.d.ts +0 -1
- package/dist/hitl/refusedToolNameProse.js +0 -3
- package/dist/hitl/replayedApproval.d.ts +4 -3
- package/dist/hitl/replayedApproval.js +7 -4
- package/dist/hitl/sessionPolicyDeliverable.js +2 -4
- package/dist/hitl/sessionPolicyWire.d.ts +32 -20
- package/dist/hitl/sessionPolicyWire.js +162 -52
- package/dist/hitl/standingRows.d.ts +20 -2
- package/dist/hitl/standingRows.js +69 -8
- package/dist/hitl/toolApprovalWire.d.ts +87 -152
- package/dist/hitl/toolApprovalWire.js +591 -380
- package/dist/hookMatcherVerdict.d.ts +6 -1
- package/dist/hookMatcherVerdict.js +14 -7
- package/dist/hooksWireCaps.d.ts +2 -2
- package/dist/hooksWireCaps.js +59 -55
- package/dist/i18n/messages.en.d.ts +121 -0
- package/dist/i18n/messages.en.js +119 -0
- package/dist/i18n/messages.zh.d.ts +3 -0
- package/dist/i18n/messages.zh.js +120 -0
- package/dist/i18n/translate.d.ts +25 -0
- package/dist/i18n/translate.js +127 -0
- package/dist/index.d.ts +53 -20
- package/dist/index.js +38 -11
- package/dist/inheritEnvCapability.d.ts +31 -0
- package/dist/inheritEnvCapability.js +104 -0
- package/dist/liveModelCatalog.d.ts +1 -2
- package/dist/liveModelCatalog.js +2 -6
- package/dist/liveQuestionStore.d.ts +2 -21
- package/dist/mcpLiveness.d.ts +2 -1
- package/dist/mcpProbeWire.d.ts +6 -1
- package/dist/mcpProbeWire.js +1 -1
- package/dist/mcpWireCaps.d.ts +13 -2
- package/dist/mcpWireCaps.js +48 -0
- package/dist/memoryEntriesWire.d.ts +4 -9
- package/dist/model/modelSupplyRules.d.ts +0 -2
- package/dist/model/modelSupplyRules.js +3 -12
- package/dist/model/modelsPolicy.d.ts +0 -5
- package/dist/model/modelsPolicy.js +6 -36
- package/dist/model/subagentModelWord.d.ts +31 -0
- package/dist/model/subagentModelWord.js +261 -0
- package/dist/notifications.d.ts +6 -0
- package/dist/notifications.js +71 -23
- package/dist/outputStyleSeatCapability.d.ts +0 -3
- package/dist/outputStyleSeatCapability.js +0 -3
- package/dist/ownKey.d.ts +15 -0
- package/dist/ownKey.js +60 -3
- package/dist/panelReduce.d.ts +15 -9
- package/dist/panelReduce.js +680 -549
- package/dist/panelStateHolder.d.ts +3 -7
- package/dist/panelStateHolder.js +14 -130
- package/dist/peerLaneCapability.d.ts +0 -4
- package/dist/peerLaneCapability.js +0 -6
- package/dist/permissionRulesWriteCapability.d.ts +0 -4
- package/dist/permissionRulesWriteCapability.js +0 -6
- package/dist/permissionWireCaps.d.ts +2 -2
- package/dist/permissionWireCaps.js +2 -0
- package/dist/permissionsAllowGrantCapability.d.ts +0 -4
- package/dist/permissionsAllowGrantCapability.js +0 -6
- package/dist/permissionsDeclaration.d.ts +0 -11
- package/dist/permissionsDeclaration.js +60 -53
- package/dist/pluginHooksWire.d.ts +1 -1
- package/dist/pluginHooksWire.js +3 -6
- package/dist/preemptWire.d.ts +44 -0
- package/dist/preemptWire.js +83 -0
- package/dist/printToolResultFrame.js +15 -4
- package/dist/replayBody.d.ts +3 -0
- package/dist/replayBody.js +14 -0
- package/dist/request/taskRequest.d.ts +0 -1
- package/dist/request/taskRequest.js +0 -3
- package/dist/resumeRefusalCopy.d.ts +2 -1
- package/dist/resumeRefusalCopy.js +38 -2
- package/dist/retryStatus.d.ts +8 -0
- package/dist/retryStatus.js +106 -1
- package/dist/rewindArchiveCapability.d.ts +0 -4
- package/dist/rewindArchiveCapability.js +2 -10
- package/dist/rewindArchiveReading.d.ts +0 -1
- package/dist/rewindArchiveReading.js +0 -3
- package/dist/runCancelContext.d.ts +2 -1
- package/dist/runCancelContext.js +4 -2
- package/dist/runReattachPolicy.d.ts +31 -0
- package/dist/runReattachPolicy.js +46 -0
- package/dist/runTerminal.d.ts +3 -6
- package/dist/runTerminal.js +8 -9
- package/dist/sandboxWire.d.ts +3 -1
- package/dist/sandboxWire.js +76 -27
- package/dist/scenarioWire.js +1 -1
- package/dist/seam.d.ts +24 -2
- package/dist/seam.js +15 -3
- package/dist/seatContract.d.ts +53 -10
- package/dist/seatContract.js +132 -81
- package/dist/sessionPolicyCapability.d.ts +0 -4
- package/dist/sessionPolicyCapability.js +0 -6
- package/dist/sessionSyncFileHistoryCapability.d.ts +0 -3
- package/dist/sessionSyncFileHistoryCapability.js +0 -3
- package/dist/skillToolListsCapability.d.ts +0 -3
- package/dist/skillToolListsCapability.js +0 -3
- package/dist/sqlEngineCapability.d.ts +0 -4
- package/dist/sqlEngineCapability.js +0 -6
- package/dist/steerPriorityCapability.d.ts +0 -3
- package/dist/steerPriorityCapability.js +0 -3
- package/dist/streamDrainingLegs.d.ts +4 -0
- package/dist/streamDrainingLegs.js +6 -1
- package/dist/subagent/engineCompactWire.d.ts +0 -1
- package/dist/subagent/engineCompactWire.js +7 -5
- package/dist/subagent/engineTaskHandleWire.js +4 -7
- package/dist/subagentContentStore.js +1 -11
- package/dist/submitReopen.d.ts +24 -0
- package/dist/submitReopen.js +75 -0
- package/dist/terminalStatuses.d.ts +3 -0
- package/dist/terminalStatuses.js +10 -2
- package/dist/toolResult.d.ts +10 -1
- package/dist/toolResult.js +34 -6
- package/dist/toolRoster.d.ts +0 -2
- package/dist/toolRoster.js +0 -2
- package/dist/turnFailurePresentation.d.ts +28 -0
- package/dist/turnFailurePresentation.js +205 -0
- package/dist/userInvokedSkillsCapability.d.ts +0 -3
- package/dist/userInvokedSkillsCapability.js +0 -3
- package/dist/webSearchBackendCapability.d.ts +0 -4
- package/dist/webSearchBackendCapability.js +0 -6
- package/dist/wireErrorTriage.js +5 -24
- package/dist/wireFailureShape.d.ts +62 -5
- package/dist/wireFailureShape.js +105 -5
- package/dist/wireRefusalCopy.d.ts +12 -2
- package/dist/wireRefusalCopy.js +50 -52
- package/dist/workflowsJournalCapability.d.ts +0 -4
- package/dist/workflowsJournalCapability.js +0 -6
- package/dist/writeProtectionCapability.d.ts +0 -4
- package/dist/writeProtectionCapability.js +0 -6
- package/docs/INTEGRATION-CLIENTS.md +1253 -46
- package/package.json +5 -4
- package/dist/attachmentSeatCapability.d.ts +0 -12
- package/dist/attachmentSeatCapability.js +0 -23
- package/dist/hitl/approvalOutcomeNote.d.ts +0 -10
- package/dist/hitl/approvalOutcomeNote.js +0 -29
- package/dist/sessionReadRedactionCapability.d.ts +0 -13
- package/dist/sessionReadRedactionCapability.js +0 -35
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.
|
|
38
|
+
**Version:** 0.98.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
|
|
@@ -57,6 +57,10 @@ Renamed from **`@sema-agent/wire-cc-adapter`** (0.1.x, deprecated — see *Migra
|
|
|
57
57
|
the T20 client-side hunk computation; core ruled that hunks belong to the client, so keeping it
|
|
58
58
|
out of this package would mean three re-implementations (TUI / web / desktop). The portability
|
|
59
59
|
guard pins the runtime dependency set with an *equality* check, so this list cannot drift.
|
|
60
|
+
`@sema-agent/connectors-contract` (the connector status contract; no dependencies of its own) is
|
|
61
|
+
the source of the two reader-minted connector states behind `connectorStatusOf`. This package
|
|
62
|
+
imports its types only, so nothing from it is loaded at run time; it is a dependency rather than
|
|
63
|
+
a dev dependency because the published declarations reference it.
|
|
60
64
|
`sideEffects: false`.
|
|
61
65
|
- **Peers:**
|
|
62
66
|
- `@sema-agent/agent-types` — the CC session vocabulary (`SDKMessage` and friends). **Type-only**:
|
|
@@ -67,12 +71,12 @@ Renamed from **`@sema-agent/wire-cc-adapter`** (0.1.x, deprecated — see *Migra
|
|
|
67
71
|
against — the tables live upstream precisely so this package does not keep a second copy that can
|
|
68
72
|
fall behind. The browser bundle really bundles the SDK through (the portability guard would
|
|
69
73
|
exit 3 rather than quietly mark it external).
|
|
70
|
-
- The declared floor is `>=21.1.0` (raised from `>=20.0.0` in 0.97.0 with the sdk 21 pick-up, together with the server and engine floors: the two decision doors' gate-kind tables (`APPROVAL_DECIDE_GATE_KINDS` / `PLAN_REVIEW_GATE_KINDS`, 21.1.0 on) are passed through from the sdk and the package's gate-kind judge is derived from them, the run terminal's failure detail reads the sdk's three-arm union and the approval card word comes from the sdk's closed card-word list; before that raised from `>=19.0.0` in 0.96.0 with the sdk 20 pick-up, together with the server and engine floors: the stream terminal reads the sdk's five-arm `done.result` union — idempotent replay body and refusal-after-open included — and fleet rows carry `workspaceSnapshot`; before that raised from `>=18.1.0` in 0.95.0 with the sdk 19 pick-up: the ask-row ending-cause table, the run-death verdict codes and the terminal-frame rule now come from the sdk's own tables; before that raised from `>=12.0.1` in 0.94.0: the request-side permission-mode vocabulary, the decision-attribution key type, the cancellation-cause list, the status-derived coarse-code list and the routing-failure denial word are now taken from the SDK itself, all of which it declares from 17.x or 18.x on; before that raised from `>=11.3.0` in 0.84.0: the per-session background listing (`sessions.background`), its capability bit `capabilities.background.listFace` and the closed background-status vocabulary are typed from 12.x on, and the package reads all three, so it no longer compiles against 11.x; before that raised from `>=11.2.1` in 0.80.0: a deny decision may now name its settler — `settledBy: "policy"` is typed on both the durable decide body and the live respond body from 11.3.0 on, and the gate record's settlement vocabulary carries its thirteenth word `policy_refused`, which this package reads to place a refusal in the policy bucket rather than the person's; the package now compiles against 11.3.0 and no consumer ships 11.2.x any more, so the older floor lost its witness; before that raised from `>=11.0.1` in 0.79.0: `capabilities.mcpProbe`, the MCP probe face (`mcpCapabilities` / `probeMcp` with `McpProbeFace`) and the write receipt's third liveness arm (`stillLive: "unknown"`) are typed from 11.2.x on, 11.2.1 adds `TaskRequest.approverPosture` and the `mandated` approval-frame key to the types and the runtime key anchor, the package now compiles against 11.2.1, and no consumer ships 11.0.x / 11.1.x any more, so the older floor lost its witness; before that raised from `>=9.8.1` in 0.78.0: `DeniedBy` carries its tenth word `read_boundary`, `rules.write` answers a `stillLive`-discriminated body, `RemovalLiveness` / `RuleWriteRequest` / `RuleWriteResult` / `RuleWriteBehavior` are exported from the SDK root and `TaskRequest.excludeAllTools` is typed from 11.x on, the package now compiles against 11.0.1, and no consumer ships 9.8.x any more, so the older floor lost its witness; before that raised from `>=9.7.1` in 0.75.0: `Capabilities.deviceExecutor.management` is typed from 9.8.x on, the package now compiles against 9.8.1, and no consumer ships 9.7.x any more, so the older floor lost its witness; before that raised from `>=9.6.0` in 0.74.0: `Capabilities.approvalsStreamLive` / `.executionLane`, `LivePendingRow.frame`, the `live_*` approval-stream events and `gates[].toolCallId` are typed from 9.7.x on, and no consumer ships 9.6.0 any more, so the older floor lost its witness; before that raised from `>=9.4.0` in 0.71.0: the `tool_disclosure` / `tool_progress` frames and `ToolApprovalFrame.readRootCandidate` are typed there; earlier: raised from `>=8.8.0` in 0.69.0: the `reasoning_end` frame and `McpStatusPanel.lastLegMcp` are typed from 9.4.0 on), and it is *witnessed*: the guard checks that an actually
|
|
74
|
+
- The declared floor is `>=23.0.2` (raised from `>=21.1.0` in 0.98.0 with the sdk 23 pick-up: the live respond receipt is read as the sdk's two-arm union (applied / replayed first decision), the session rule record follows the sdk's three-bucket type, the parked-resume startup cause is the sdk's closed five-word set and the crash-converged row is the sdk's own row type; before that raised from `>=20.0.0` in 0.97.0 with the sdk 21 pick-up, together with the server and engine floors: the two decision doors' gate-kind tables (`APPROVAL_DECIDE_GATE_KINDS` / `PLAN_REVIEW_GATE_KINDS`, 21.1.0 on) are passed through from the sdk and the package's gate-kind judge is derived from them, the run terminal's failure detail reads the sdk's three-arm union and the approval card word comes from the sdk's closed card-word list; before that raised from `>=19.0.0` in 0.96.0 with the sdk 20 pick-up, together with the server and engine floors: the stream terminal reads the sdk's five-arm `done.result` union — idempotent replay body and refusal-after-open included — and fleet rows carry `workspaceSnapshot`; before that raised from `>=18.1.0` in 0.95.0 with the sdk 19 pick-up: the ask-row ending-cause table, the run-death verdict codes and the terminal-frame rule now come from the sdk's own tables; before that raised from `>=12.0.1` in 0.94.0: the request-side permission-mode vocabulary, the decision-attribution key type, the cancellation-cause list, the status-derived coarse-code list and the routing-failure denial word are now taken from the SDK itself, all of which it declares from 17.x or 18.x on; before that raised from `>=11.3.0` in 0.84.0: the per-session background listing (`sessions.background`), its capability bit `capabilities.background.listFace` and the closed background-status vocabulary are typed from 12.x on, and the package reads all three, so it no longer compiles against 11.x; before that raised from `>=11.2.1` in 0.80.0: a deny decision may now name its settler — `settledBy: "policy"` is typed on both the durable decide body and the live respond body from 11.3.0 on, and the gate record's settlement vocabulary carries its thirteenth word `policy_refused`, which this package reads to place a refusal in the policy bucket rather than the person's; the package now compiles against 11.3.0 and no consumer ships 11.2.x any more, so the older floor lost its witness; before that raised from `>=11.0.1` in 0.79.0: `capabilities.mcpProbe`, the MCP probe face (`mcpCapabilities` / `probeMcp` with `McpProbeFace`) and the write receipt's third liveness arm (`stillLive: "unknown"`) are typed from 11.2.x on, 11.2.1 adds `TaskRequest.approverPosture` and the `mandated` approval-frame key to the types and the runtime key anchor, the package now compiles against 11.2.1, and no consumer ships 11.0.x / 11.1.x any more, so the older floor lost its witness; before that raised from `>=9.8.1` in 0.78.0: `DeniedBy` carries its tenth word `read_boundary`, `rules.write` answers a `stillLive`-discriminated body, `RemovalLiveness` / `RuleWriteRequest` / `RuleWriteResult` / `RuleWriteBehavior` are exported from the SDK root and `TaskRequest.excludeAllTools` is typed from 11.x on, the package now compiles against 11.0.1, and no consumer ships 9.8.x any more, so the older floor lost its witness; before that raised from `>=9.7.1` in 0.75.0: `Capabilities.deviceExecutor.management` is typed from 9.8.x on, the package now compiles against 9.8.1, and no consumer ships 9.7.x any more, so the older floor lost its witness; before that raised from `>=9.6.0` in 0.74.0: `Capabilities.approvalsStreamLive` / `.executionLane`, `LivePendingRow.frame`, the `live_*` approval-stream events and `gates[].toolCallId` are typed from 9.7.x on, and no consumer ships 9.6.0 any more, so the older floor lost its witness; before that raised from `>=9.4.0` in 0.71.0: the `tool_disclosure` / `tool_progress` frames and `ToolApprovalFrame.readRootCandidate` are typed there; earlier: raised from `>=8.8.0` in 0.69.0: the `reasoning_end` frame and `McpStatusPanel.lastLegMcp` are typed from 9.4.0 on), and it is *witnessed*: the guard checks that an actually
|
|
71
75
|
installed SDK at that line still exports every value-level symbol this package imports and still
|
|
72
76
|
declares `TaskStats.costMicroUsd` (the key `costOrNull` reads). A floor nobody ever ran is a
|
|
73
77
|
promise, not a contract.
|
|
74
|
-
- **Support floor:** the oldest server, engine and SDK this package still supports — server **7.
|
|
75
|
-
engine **7.49.5
|
|
78
|
+
- **Support floor:** the oldest server, engine and SDK this package still supports — server **7.129.1**,
|
|
79
|
+
engine **7.51.2**, SDK **23.0.2** (server and engine raised in 0.98.0 from 7.126.0 / 7.49.5, withdrawing support for servers 7.126.0 – 7.129.0; SDK raised in 0.98.0 from 21.1.0, with the peer floor; all three raised together in 0.97.0, from 7.125.0 / 7.48.0 / 20.0.0; raised together in 0.96.0 from 7.115.0 / 7.40.1 / 19.0.0; server / engine raised from 7.106.0 / 7.35.2 in 0.95.0 and from 7.104.0 / 7.33.8 in 0.92.0; SDK raised from 18.1.0 in 0.95.0 and from 12.0.1 in 0.94.0). The values are
|
|
76
80
|
declared in `scripts/support-floor.json` and exported at runtime as `SUPPORT_FLOOR`; servers below the floor
|
|
77
81
|
are no longer witnessed, and the readings that existed only for them were retired in the same release.
|
|
78
82
|
- The direction is constitutional: **sdk (wire) + agent-types (vocabulary) → this package**,
|
|
@@ -294,15 +298,15 @@ guard still cross-checks the table by name).
|
|
|
294
298
|
| Suite | What it guards |
|
|
295
299
|
|---|---|
|
|
296
300
|
| `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. |
|
|
297
|
-
| `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 |
|
|
301
|
+
| `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. |
|
|
298
302
|
| `scripts/run-client-core-diff-test.mjs` | Differential equivalence against the CLI reference bridge + replay-id invariant + ledger round-trip |
|
|
299
|
-
| `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, 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) |
|
|
303
|
+
| `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. |
|
|
300
304
|
| `scripts/run-approval-frame-keys-test.mjs` | The tool-approval frame key mirror, element-wise against the SDK's runtime anchor (one carve-out: AHEAD_OF_ANCHOR entries — keys the server already emits but the SDK anchor has not caught up to — may lead by one generation; the gate turns red the day the SDK catches up, forcing the entry's removal — the register is occupied again — this time by the bit that says a saved allow rule cannot retire a given approval card, carrying both the release that minted it and the byte coordinates that prove it, so the lead is a dated record rather than an exemption; its predecessor left the register the other way, by being retired upstream rather than by the anchor catching up). Beside the key mirror it now guards three further faces of that bit: the closed word table the durable leg reads it through must be **the very array object the SDK exports**, not a same-looking copy — reference identity, because an equal-contents check still permits a second table that diverges the day upstream adds a member; the one predicate a client is meant to call answers over both legs — the live card's bit and the parked row's absence word, which is all the row carries, since the row has no such bit at all — and answers `false` for a malformed value exactly as its presence-only siblings do, a strictness the upstream mint shares; and the one sentence minted for it must never point the reader at writing a rule, since a rule written in answer to a mandated question can never take effect where it was written. The same bit's key also has to reach the card request itself, which the SDK's card anchor does not list — a fact that arrives at the package boundary and stops there is the shape of defect this file's guards exist to catch. Since 0.83.2 the frame also mirrors `mandate` (the word a mandated question stands on) ahead of the SDK anchor, with its own exit condition; the released server carries it from 7.102.0, so the allowance for the server fixture was removed by its own exit condition. The installed engine must declare the member with the six-word closed type, and the key must reach the card request. Since 0.91.0 the frame also mirrors `probeReason` ahead of the development SDK anchor, with the same exit condition, and the probe-tree comparison reads the newest configured SDK first. |
|
|
301
|
-
| `scripts/run-segment-authority-single-source-test.mjs` | The authoritative-segment replacement verdict, single-sourced. `text_end.content` and the `text_delta` stream stopped being byte-identical the day the engine started redacting the former through the same filter as the result, so every consumer now has to decide six ways what to do with the segment it has half-emitted — and until this release that decision existed **twice**: once here for the transcript lane, once in the shell for the print lane, hot-fixed a version apart. The verdict is now one pure function both lanes call, and the guard pins it on the quantity that actually decides the outcome: whether the authoritative text still *starts with* the bytes that already left, not whether a flush has happened — the latter is a precondition, and anchoring on it withholds a perfectly ordinary answer. Each of the six forms is checked with its counter-case, the prefix length is pinned to UTF-16 code units against a non-ASCII sample whose UTF-8 byte count differs (slicing by bytes leaves the very thing being redacted on screen), and the withheld-segment ledger is compared by normalised equality rather than substring, because a short redaction marker quoted in an unrelated later answer would otherwise suppress that answer entirely. The same file pins the session-level memory-capture declaration to one mint point — the wire value is a single-member closed set, and a consumer that spells it wrong gets a loud refusal rather than a silently dropped privacy request — and pins the SDK URL/health transit to be the **same function reference**, since wrapping it would discard the one guarantee the transit exists for. A last section strips comments with the TypeScript parser and asserts the second expression has not grown back |
|
|
305
|
+
| `scripts/run-segment-authority-single-source-test.mjs` | The authoritative-segment replacement verdict, single-sourced. `text_end.content` and the `text_delta` stream stopped being byte-identical the day the engine started redacting the former through the same filter as the result, so every consumer now has to decide six ways what to do with the segment it has half-emitted — and until this release that decision existed **twice**: once here for the transcript lane, once in the shell for the print lane, hot-fixed a version apart. The verdict is now one pure function both lanes call, and the guard pins it on the quantity that actually decides the outcome: whether the authoritative text still *starts with* the bytes that already left, not whether a flush has happened — the latter is a precondition, and anchoring on it withholds a perfectly ordinary answer. Each of the six forms is checked with its counter-case, the prefix length is pinned to UTF-16 code units against a non-ASCII sample whose UTF-8 byte count differs (slicing by bytes leaves the very thing being redacted on screen), and the withheld-segment ledger is compared by normalised equality rather than substring, because a short redaction marker quoted in an unrelated later answer would otherwise suppress that answer entirely. The same file pins the session-level memory-capture declaration to one mint point — the wire value is a single-member closed set, and a consumer that spells it wrong gets a loud refusal rather than a silently dropped privacy request — and pins the SDK URL/health transit to be the **same function reference**, since wrapping it would discard the one guarantee the transit exists for. A last section strips comments with the TypeScript parser and asserts the second expression has not grown back Since 0.98.0 the gate also pins the single reading of engine liveness: no string expression in the package source evaluates, after static folding, to a `/health` path, so a second hand-written probe cannot come back however it is spelled, and the package's one liveness helper answers six real HTTP fixtures exactly as the SDK probe does (a self-reported degraded body, a non-JSON body and a 503 are not alive; 200, 201 and 299 with an ok body are). |
|
|
302
306
|
| `scripts/run-print-bash-iserror-test.mjs` | The print lane's Bash `is_error` authority (structured over regex). A second section pins where the denial classification word lands on this lane: on the message envelope, never inside the tool-result block, because that block is forwarded verbatim to the provider on compaction and a self-minted key there is the shape of an old, real defect. A word outside the upstream table — or an empty string, a non-string, or nothing at all — mints no key rather than a guess, and the word never moves the error flag, because attribution does not decide anything |
|
|
303
307
|
| `scripts/run-bash-benign-exit-interpretation-test.mjs` | Benign non-zero Bash exits (`returnCodeInterpretation`) stay non-errors across all three derivation arms, and the annotation transits to the card |
|
|
304
308
|
| `scripts/run-sdk-floor-test.mjs` | The SDK version floor — and, more to the point, that the *installed* type declarations still carry the keys this package reads — including, from 0.80.0, the three declarations that justify the floor itself: the key naming who settled a refusal on both decision legs, and the thirteenth word in the settlement vocabulary. They are found through the syntax tree rather than by searching text, because this guard's own comment stripper blanks string contents and would have made that check permanently, silently green. From 0.84.0 the floor is 12.0.1 and the guard witnesses the declarations the package now reads: the per-session background listing method, the `background.listFace` capability bit, and the closed background-status vocabulary that the registry classification switches over exhaustively. Every installed SDK the guard can see at or above the floor must carry those declarations too, so lowering the floor to a line where they do not exist fails. From 0.94.0 the floor is 18.1.0: the shipped declarations name the approval-attribution type and the six-word request permission-mode type that SDK 17 introduced, and the cancellation-cause, status-derived-code and routing-failure values are generated from the same SDK at build time; the guard reads those values on the floor-line SDK it can see. |
|
|
305
|
-
| `scripts/run-engine-caps-ledger-test.mjs` | A per-key disposition ledger for `GET /v1/capabilities`. The SDK's `Capabilities` grew from 74 keys to 93 in one release and nothing on the board could see it: this package consumes that table through four synchronous readers, and *nineteen new positions arriving while the package does not move* is exactly the disease shape this repo keeps logging on other axes — the fact is already on the wire, the package boundary is the cell that swallows it, and no client can read it however they write their side. So the ledger is reconciled **element-wise against the SDK interface in both directions**: a key the SDK added with no ledger row is red (someone must classify it), and a row for a key the SDK removed is red too (a registration that no longer does anything). Each row then has to survive its own claim — a `read` row names the source file, and the **code** there (comments stripped) must really mention the key, because prose asserting an alignment is the classic way these guards go hollow; a `not_read` row must have **zero** read sites in the tree, so wiring one up while the ledger still says the package ignores it is red rather than invisible. The census behind those two directions recognises five call shapes, each of which really occurs here — a reader whose base argument carries its own parentheses, a direct `caps.<key>`, a narrowing cast, an own-property read helper, and a `*_CAP` constant — and proves it on fabricated samples first, since a census that recognises one shape reports "nothing here" for the other four. What the guard deliberately does **not** judge is whether a position *ought* to be read: that is a design call, and the ledger only pins that every capability was looked at once by a person and that what they wrote down does not contradict the code |
|
|
309
|
+
| `scripts/run-engine-caps-ledger-test.mjs` | A per-key disposition ledger for `GET /v1/capabilities`. The SDK's `Capabilities` grew from 74 keys to 93 in one release and nothing on the board could see it: this package consumes that table through four synchronous readers, and *nineteen new positions arriving while the package does not move* is exactly the disease shape this repo keeps logging on other axes — the fact is already on the wire, the package boundary is the cell that swallows it, and no client can read it however they write their side. So the ledger is reconciled **element-wise against the SDK interface in both directions**: a key the SDK added with no ledger row is red (someone must classify it), and a row for a key the SDK removed is red too (a registration that no longer does anything). Each row then has to survive its own claim — a `read` row names the source file, and the **code** there (comments stripped) must really mention the key, because prose asserting an alignment is the classic way these guards go hollow; a `not_read` row must have **zero** read sites in the tree, so wiring one up while the ledger still says the package ignores it is red rather than invisible. The census behind those two directions recognises five call shapes, each of which really occurs here — a reader whose base argument carries its own parentheses, a direct `caps.<key>`, a narrowing cast, an own-property read helper, and a `*_CAP` constant — and proves it on fabricated samples first, since a census that recognises one shape reports "nothing here" for the other four. What the guard deliberately does **not** judge is whether a position *ought* to be read: that is a design call, and the ledger only pins that every capability was looked at once by a person and that what they wrote down does not contradict the code In 0.98.0 the census reads the syntax tree — call heads, the second argument, constants, property reads, narrow casts and own-property reads are nodes, not text matched by a pattern with bracket balancing and quote pairing — and proves six more shapes on fabricated samples (a closing bracket inside a string, a pattern literal holding brackets and commas, a template with a closing bracket, a string with a comma and both quote kinds, an optional call, call-looking text inside a string) plus a control that removing the whole class empties the census. |
|
|
306
310
|
| `scripts/run-sql-engine-capability-test.mjs` | The SQL-posture read face and the four-state capability reader underneath it. One capability cell here carries **four different things**, and each one points an operator somewhere else: nothing has been observed yet in this process (a one-shot doctor run is always in that state), the response arrived but cannot say (no store posture, or one that cannot be read), the engine reports a store posture without an SQL seat — *this deployment has no SQL backend*, which is a **positive fact** rather than an absence — and a full reading. Fold any two together and the screen states something flatly, confidently, and wrongly, so every positive control here is paired with a control pointing the opposite way, and the four sentences the doctor row can print are checked to be pairwise distinct and non-implying. The reading itself is narrowed no tighter than the mint: `txnMode: null` is a **legal value** — two of the three engines always report it that way, and the upstream type note names reading it as "optimistic" as the error — so treating it as malformed would throw away the entire reading for ordinary deployments, which is the same disease this repo logged when a consumer's domain was narrower than the producer's. A response that cannot be parsed **clears** the cell rather than leaving the previous engine's answer in place, and a separate invalidation port exists for the case the generation latch cannot catch — a same-port respawn whose new probe never succeeded, where the stale reading would otherwise be answered as current fact. Untrusted values (the isolation string is read back from a database server variable) are sanitised and bounded before display, and the bound is applied **before** escaping so a visible escape never gets cut in half. Finally the export names are themselves a guard: the shell still carries a copy that is meant to go red on the package's same-named export and be swapped out, so renaming anything here would silently disarm that lock. Current engines (≥7.106.0) moved the SQL posture under the always-present store posture (`store.sql`) and dropped the top-level key, and from 0.92.0 the reader reads `store.sql` alone (absent there means the engine reports no SQL posture): the top-level position of servers below the support floor is no longer read, a response without a readable store posture is "not reported" — the same answer the other readers sharing that witness give — and an unreadable SQL seat clears the cell; the engine's own projection is replayed to prove every form it mints reads back |
|
|
307
311
|
| `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 |
|
|
308
312
|
| `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. |
|
|
@@ -319,25 +323,25 @@ guard still cross-checks the table by name).
|
|
|
319
323
|
| `scripts/run-tool-disclosure-progress-projection-test.mjs` | The two wire arms sdk 9.6.0 adds — `tool_disclosure` (name-only tool census: open-set `policy`, `thresholdPercent` absent ≠ default, `deferred`/`activated` full snapshots) and `tool_progress` (one frame, two beats: Bash ticks carry an output tail with `totalLines`/`totalBytes` that come and go together; other tools carry only `elapsedSeconds`) — project to neutral internal arms plus chrome arms. Required keys missing ⇒ `malformed`; bad optional keys drop only themselves; the sub-flow three-key gate keeps child frames off the leader lane; both arms are `required: false` in the arm table with duties stated (the output tail is untrusted raw and must never be fed back to the model). |
|
|
320
324
|
| `scripts/run-mcp-panel-projection-test.mjs` | The `GET /v1/sessions/:id/mcp` panel reader (`projectMcpPanel`; server >=7.77.0 adds the optional `lastLegMcp` key) and the single wording mint for its "last leg" line. Absence of `lastLegMcp` is one literal sentence that never blames the engine version (a new session, a leg outside the retention window, a leg without a manifest and an older engine all look the same on the wire); a key that is present but unreadable is a different sentence plus a `lastLegMcpUnreadable: true` mark, never folded into absence. The `mcp[]` roster goes through the same reader as the live `wiring_manifest` third section, so a replayed roster and a live one have one shape. The two faces of the panel (`servers[]` and the last-leg roster) may legitimately differ, so the view carries no agreement flag and none of the five sentences mentions `servers`. Required keys are pinned to the SDK `openapi.yaml` component bytes **0.69.0:** `fetchMcpPanel` fetches the panel through the SDK client's own `sessions.mcp` call (same transport and auth as every other read) and projects it; transport failure, an unreadable body and an empty session id all come back as `undefined`, never as a fabricated empty panel 0.71.0 adds section K: `mcpEngineLegPresence(view)` — the engine-side MCP presence tri-state read only off the panel view (`unknown` when the view could not be read, never rendered as "no MCP configured") |
|
|
321
325
|
| `scripts/run-absence-fold-census-test.mjs` | A package-wide census of the "absence folded into a positive outcome" defect shape, so that fixing the six sites this release does not merely move the shape somewhere else. The defect is defined by position, not syntax: a fallback position (the unconditional tail return, the `default:` arm, the literal minted when there is nothing to pass on, the value returned from an error path) may only say `unknown` or stay absent, never a positive word. Detection walks the syntax tree of every source file, so comments, strings and multi-line spellings cannot hide or fake a hit, and covers five forms: the right arm of `??` / `\|\|`, the else arm of a ternary, the first return of an explicit `default:`, a `catch` block or `.catch(() => …)` arrow returning a healthy value, and a function whose last statement returns a positive word after other returns. Every remaining hit must be registered with a written reason, an unregistered hit fails the gate naming the file and line, the registered count must equal the real count so a cleared site cannot leave a spare allowance behind, and the gate proves its own teeth behind a fence (a failed self-proof refuses to report any count): each form injected into an in-memory copy must add exactly one hit, two correct spellings are pinned as non-hits, and samples inside comments or strings do not count. It also pins the headline site: the fleet panel projection no longer mints an `end` with `isError: false` on absence |
|
|
322
|
-
| `scripts/run-device-executor-management-capability-test.mjs` | The engine's device-lane self-description (`capabilities.deviceExecutor`, with its management bit from engine ≥7.88.0), read by one reader the same four-state way as its sibling capability readers: a lane object is reported as present and carries the management bit as one of three literal states (on, off, or absent — an older engine really ships the lane object without the key, and that is never folded into `false`) plus three optional lane facts (protocol version as a finite number, in-flight cap per device as a finite non-negative number, socket path as a non-empty string), each read on its own so a missing or malformed one only drops that fact; the lane object must carry `enabled: true` as the contract says, and anything else is an unreadable shape rather than an invented disabled-lane state; an absent `deviceExecutor` key is not reported, `deviceExecutor: false` is the lane being absent, presence is judged by own-property not truthiness, every key is read as an own data property (accessors are never run), the
|
|
326
|
+
| `scripts/run-device-executor-management-capability-test.mjs` | The engine's device-lane self-description (`capabilities.deviceExecutor`, with its management bit from engine ≥7.88.0), read by one reader the same four-state way as its sibling capability readers: a lane object is reported as present and carries the management bit as one of three literal states (on, off, or absent — an older engine really ships the lane object without the key, and that is never folded into `false`) plus three optional lane facts (protocol version as a finite number, in-flight cap per device as a finite non-negative number, socket path as a non-empty string), each read on its own so a missing or malformed one only drops that fact; the lane object must carry `enabled: true` as the contract says, and anything else is an unreadable shape rather than an invented disabled-lane state; an absent `deviceExecutor` key is not reported, `deviceExecutor: false` is the lane being absent, presence is judged by own-property not truthiness, every key is read as an own data property (accessors are never run), the reading comes only from the capabilities cache's landed response (there is no separate write port), and the package owns two verdicts — whether the lane is open (`yes` for any lane object, `no` for the lane being absent, otherwise `unknown`) and whether the `/v1/devices` management verbs are usable (`yes` only when the bit is on, `no` when it is off or the lane is absent, otherwise `unknown`) — while the installed SDK's declaration of the lane object is pinned |
|
|
323
327
|
| `scripts/run-run-cancel-context-test.mjs` | The run record's `cancelContext` side-note (engine ≥7.87.3) read structurally, and the cause of a `turn_aborted{engine_error}` classified from machine-readable evidence only: `cancelled` (code `cancelled`, with the cancel-time context when present) / `engine_error` (any other failure code, passed through verbatim) / `run_still_live` (the record is not terminal — a dropped stream is a client-side fact, not the run's cause) / `unknown` (never guessed). An absent `cancelContext` reads as *not reported*, never as "not cancelled"; `elapsedMs` is never folded to 0. |
|
|
324
328
|
| `scripts/run-suspended-reopen-projection-test.mjs` | The durable `suspended` event's `reopened` key read as three distinct states — `reopened` (with the engine's code, verbatim), `not_reopened` (an explicit `null`), `unstated` (key absent or unreadable) — and carried on the HITL bridge's active gate (`currentGateReopen()`), re-read on every `suspended` and cleared with the gate. The stream driver also turns a `suspended` frame that reads `reopened` into the chrome arm `suspended_reopened` (the reading, the single-source reopen sentence when the code is in the reopen family, the frame's event id, and the run id only when the frame itself carries one), pinned end to end: one notice per event id on a stream, emitted in stream order on a replay from the first frame, nothing for a cursor that starts after it, and an unchanged transcript when the host has no chrome sink or the sink fails. |
|
|
325
|
-
| `scripts/run-panel-identity-normalization-test.mjs` | One background subagent has two ids on the wire — the fleet row id tail and the `task_progress` task id (its transcript id). Every panel event goes through one funnel that rewrites the `tick` / `end` task id onto the fleet row's id once a `fleet-row` has
|
|
329
|
+
| `scripts/run-panel-identity-normalization-test.mjs` | One background subagent has two ids on the wire — the fleet row id tail and the `task_progress` task id (its transcript id). Every panel event goes through one funnel that rewrites the `tick` / `end` task id onto the fleet row's id once a `fleet-row` carrying that `transcriptId` has bound the two ids (the delegating card id is not a key: several agents can share one card), carrying the original as `wireTaskId` and marking `taskIdOrigin`; an unbound tick whose row has not arrived yet waits one beat (bounded) and is released verbatim on the next tick / `end`, when the buffer is full, or after `MAX_HELD_WIRE_TICK_BEATS` other fleet-row / end / sweep events (a `sweep` itself leaves it alone: there is no row to settle yet); a normalized `end` that carries no cycle identity borrows the registering row's, so a late close of a revived task is recognized as stale; records are bounded as a whole and evicted least-recently-hit first, finished ones before running ones (a task that keeps ticking is never evicted by newer registrations); the residency mark lives on the agent's one record, so both ids read and clear the same mark — except that a stale (previous-cycle) terminal never clears the revived agent's mark. Once a tick has been delivered verbatim under its UUID, that UUID is the subagent's key: later fleet rows and fleet-side ends are rewritten onto it (the tail kept in `wireTaskId`), so a consumer sees one row in every arrival order, and a revival — even one with a new transcript id — stays on that key; a late tick from a previous cycle is dropped rather than folded into the revived row, and a fleet row that carries only the delegating card id is its own agent. |
|
|
326
330
|
| `scripts/run-prompt-assembled-projection-test.mjs` | The `prompt_assembled` frame (one prepare's prompt-assembly manifest) projected to an internal arm and then to the additive `prompt_assembled` chrome event — the per-section / per-block **character** counts, the mounted tool names and `totalChars`, each key present only when the engine really sent it (the frame's `constitution` is deliberately not carried: no consumer asks for it today, and every published key is a contract to keep). The manifest carries **no token counts** anywhere upstream, so this projection mints none: a token figure derived from characters would be an invented number, and the engine's own estimate lives on `context_usage.sections[].tokens` (same id wordlist, joinable). Bad rows are dropped one by one, and a face that loses every row reads as an absent key rather than an empty array — so an absent face means only "this event carries no readable view of it" (an absent upstream key, an empty array and a fully filtered list all land on the same shape) and is never reported as a diagnosis about the engine. `blocks[].id` and `sections[].id` are two different wordlists with a many-to-one relation, and the token join against `context_usage.sections[].tokens` only holds when both sides carry a section view. A frame with no readable composition key at all is malformed, ids and slots are read as an open set, one chrome event per frame with zero transcript rows, several prepares per task are all handed over (de-duplication — "take the last one" — is the host's move), and the lane is told honestly (`parentToolCallId` ⇒ subagent lane; a frame attributable only by `sourceTaskId` / `bgAgentId` is not surfaced on the main lane). Both entry points obey the same rule: the adapt layer rebuilds every row too, so a host pipeline (or a replayed transcript) that feeds the raw frame straight into `adapt()` cannot smuggle extra keys (`tokens`, digests, aliases), a negative `chars` or a `null` row into the chrome payload, an empty array does not count as a composition face, the identity keys are snapshotted once on both paths (read exactly once each, a throwing accessor rejects the whole frame — reading one twice is what lets an accessor frame land on a different lane on each path), and the two paths are compared verbatim so the two readers cannot drift. |
|
|
327
331
|
| `scripts/run-compaction-outcome-projection-test.mjs` | The `compaction_outcome` frame (a compaction that did **not** end as compacted: mooted by the task ending, failed, …) projected to an internal arm and then to the additive `compaction_outcome` chrome event — `outcome` required and verbatim (open set), `trigger` / `reason` present only when the engine sent a non-empty string, malformed frames dropped, zero transcript rows, the lane told honestly (`parentToolCallId` ⇒ subagent lane; a frame attributable only by `sourceTaskId` / `bgAgentId` is not surfaced on the main lane). |
|
|
328
|
-
| `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),
|
|
332
|
+
| `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. |
|
|
329
333
|
| `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. |
|
|
330
334
|
| `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. |
|
|
331
|
-
| `scripts/run-approval-resolution-test.mjs` |
|
|
332
|
-
| `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 terminal row held back
|
|
335
|
+
| `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-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. |
|
|
333
337
|
| `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 |
|
|
334
338
|
| `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. |
|
|
335
339
|
| `scripts/run-model-facing-anchors-test.mjs` | Five places where model-facing engine text is parsed back into card slots: the task-output cursor semantics machine-readable key wins over the spool marker text (and its absence is never read as `full`), the `--- result (…) ---` line accepts a parenthesised note and reads it in linear time even on crafted input (a note can no longer swallow the start of the next line), the bash header accepts the engine's exit-note and image variants and carries the note into the same benign-exit slot the structured path uses, the notification seed scan treats both producers' terminal words as terminal, and the parked background-task phrase is recognised. |
|
|
336
|
-
| `scripts/run-execution-lane-capability-test.mjs` | The deployment execution-lane self-description (`capabilities.executionLane`, engine ≥7.86.0), read the same four-state way as its three sibling capability readers. An absent key is reported as "not reported" and never as "tools do not run on this host" as a fact about the deployment; since every server at or above the support floor reports the key, the single "do tools run here" predicate answers `false` for a body that reported nothing (from 0.92.0, so no skill `baseDir` is sent on a reading that proves nothing), and keeps using the caller's own inference only while nothing has been observed yet. The implication is one-way (`toolsOnThisHost: false` means `baseDir` is never sent; `true` is only a necessary condition), `provider` is read as an open non-empty string rather than a hand-copied closed set and is sanitised and bounded before display, `toolsOnThisHost` must be a strict boolean,
|
|
340
|
+
| `scripts/run-execution-lane-capability-test.mjs` | The deployment execution-lane self-description (`capabilities.executionLane`, engine ≥7.86.0), read the same four-state way as its three sibling capability readers. An absent key is reported as "not reported" and never as "tools do not run on this host" as a fact about the deployment; since every server at or above the support floor reports the key, the single "do tools run here" predicate answers `false` for a body that reported nothing (from 0.92.0, so no skill `baseDir` is sent on a reading that proves nothing), and keeps using the caller's own inference only while nothing has been observed yet. The implication is one-way (`toolsOnThisHost: false` means `baseDir` is never sent; `true` is only a necessary condition), `provider` is read as an open non-empty string rather than a hand-copied closed set and is sanitised and bounded before display, `toolsOnThisHost` must be a strict boolean, an `isolated` position that can only be read by running a getter (or whose descriptor read throws) makes the whole entry unreadable rather than degrading that one position, extra members never cross, the reading comes only from the capabilities cache's landed response (there is no separate write port), and the single "do tools run here" predicate uses the bit when it is present. Current engines (≥7.106.0) also say whether the lane's tools sit behind an OS isolation boundary (`isolated`, present only when the lane has tools of its own); the reading carries it when it is a strict boolean and otherwise records which of three absences it is — a lane without tools on a current engine, an older engine, or a value that cannot be read (including a current engine that says tools run on this host yet omits the flag it must then send) — none of which is ever read as `false`, and an unreadable value never costs the rest of the lane reading. The single "no OS sandbox on this machine" predicate (`isolated` false and tools on this host) answers yes, no or unknown under three-valued logic, one sentence goes with the yes answer, the operator diagnostics segment is read by the same rule, and the engine's own projection for every lane is replayed: only the host lane answers yes |
|
|
337
341
|
| `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. |
|
|
338
342
|
| `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 |
|
|
339
343
|
| `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 |
|
|
340
|
-
| `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. 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. |
|
|
344
|
+
| `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. 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. |
|
|
341
345
|
| `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 |
|
|
342
346
|
| `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 |
|
|
343
347
|
| `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 |
|
|
@@ -371,7 +375,7 @@ guard still cross-checks the table by name).
|
|
|
371
375
|
| `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. |
|
|
372
376
|
| `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 |
|
|
373
377
|
| `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.
|
|
374
|
-
| `scripts/run-integration-doc-freshness-test.mjs` | The **integration contract** (`docs/INTEGRATION-CLIENTS.md`) and the **changelog** (`CHANGELOG.md`) checked against the code, because a document with no guard rots — this one had a whole nest of drift found on it within a day of being written. Five directions, each a claim a machine can actually evaluate. (1) *Counting discipline*: the version-anchor row for the guard count may no longer carry a hand-copied number at all — it changes every time a guard is added, and writing it down is planting a timer; the export counts that are still hand-copied (the surface total, the test-hook count, the sentence describing the surface's internal composition, the sum of the sixteen domain rows, and the three sub-counts) are each compared against a value **derived** from `public-export-baseline.json`, which is the drift a human reviewer caught last time. (2) *Coordinates alive*: every `src/` `scripts/` `docs/` path the doc quotes must be on disk **and tracked by git** — on disk is not in the repo, and a doc that points readers at a file living only in its author's working tree sends every clone to nothing. A file landing in the same commit takes a named carve-out that **stops applying** the moment the file is really tracked (it can no longer let anything through, and the guard prints a line asking for it to be deleted) — deliberately not a red, since turning red on the very commit that lands the file would just manufacture a break that only a follow-up commit could clear. (3) *Arm tables*: the `hitl_out_of_slice` row and the `not_in_slice` fenced list must equal, name for name and in **both** directions, the case labels that really fall into those two buckets — read through the **TypeScript AST**, since which bucket an arm lands in is decided by the argument to `nothing(...)` and by nothing a comment says. The extractor is anchored to the one production projector: exactly one function named `eventToSdkMessage`, exactly one `switch (ev.type)` inside it, and no repeated case label — anything else is a broken anchor rather than a verdict, because a second same-shaped switch elsewhere in the file would otherwise overwrite the real one's conclusions and leave the doc agreeing with a switch nobody runs. The list is delimited by a machine-readable fence rather than by section headings, because the same section also names the terminal arms as a counter-example and prose boundaries cannot tell a member from a foil. (4) *Released sections are frozen*: an **append-only ledger** carries every version ever published — its number, the commit it was published from, and the sha256 of its section — and each one is checked, not just the current release, since pinning only the latest would set every earlier version free the moment the next one ships. The ledger cannot vouch for itself either: each recorded hash is **re-derived from that release commit** through git, so editing an old section and its constant together no longer passes — and the commit the row names is in turn checked against the `gitHead` npm recorded at publish time, which is the one value this repository cannot rewrite, so pointing an old version at a freshly written commit does not pass either. The *set* of versions that must be frozen comes from the registry too, so deleting an old row together with its section — which would otherwise remove that version from every set the guard looks at — is red rather than invisible. A failed registry call is classified rather than swallowed, and the classification consults the registry's own status code *before* it considers connection-level symptoms, so an auth refusal whose body happens to mention the network is still red rather than a skip. The version set is compared as full SemVer including prereleases — matching only `x.y.z` would silently drop a published `0.30.0-beta.1` and reopen the very hole this direction closes — and section headings are matched on a whole-version boundary so a stable release cannot bind itself to the release-candidate section sitting above it. Publishing itself is a two-phase protocol rather than a paradox: before a release, exactly one row may be marked pending and must name the current `package.json` version, exempt from the checks whose inputs do not exist yet; once the registry has that version the row must be promoted, so the temporary state cannot survive its own release. And because the pending exemption rests entirely on "this version is not out yet," it is refused outright when the registry cannot be reached to confirm that — an unverifiable premise is not a licence. Three reverse directions close the rest: a section claiming to be released but absent from the ledger, a ledger entry whose section has vanished, and a `package.json` version that was never frozen. Publishing appends a row; it never rewrites one. (6) *Sentinels*: the readers §5a hands hosts for "is this port installed" are checked against what the source actually declares it returns — `hasXxx()` is a `boolean`, the card port / HITL surface / wire target return `T \| null`, the `installHost` family returns `T \| undefined`. Testing a `null`-returning reader for `!== undefined` is *always true*, and a self-check that passes whether or not the port is installed is worse than none, because hosts retire their own fallback on the strength of it. Both directions are red: an implementation that changes its sentinel without the doc following, and a doc that names the wrong one. The roster covers the zero-argument readers and their `*For` variants alike — a multi-session host reads the variants, so leaving them off would let exactly the surface desktop depends on drift unwatched — and the §5a table and the §8-B checklist line are each checked against the source, because hosts tick the checklist, and a guard that only watches the prose table misses the line people actually follow. (5) *Packaging*: the README ships with the package and opens by pointing hosts at the integration doc, and the checklist names two more files as required reading before an upgrade — all three must really appear in the `npm pack` manifest, or an npm consumer follows a relative link that npmjs rewrites onto a private repository. Missing tooling never takes the whole verdict down with it: when git, npm or the registry is unreachable those legs print the `SKIPPED-SECTION` marker and the rest still judges, while a release commit the ledger names but git cannot resolve is red rather than skipped. The guard says in its own header what it does **not** do: it judges counts, coordinates, arm sets, released bytes and the packing list — whether a sentence is *right* is still for review and for the hosts to report (7) *Retired names*: every name in the per-version `removed` ledger of `scripts/export-liveness.json` may appear in the live sections of the integration doc only where a retirement note follows the name inside the same clause (or the table row's label cell is itself a retirement label); the scan is by identifier boundary after invisible text (HTML comments, link targets, reference-link labels, tag attributes) has been stripped, so a signature line in a code block, an inline `NAME = 4096`, a hidden note, or a note that belongs to a neighbouring name all count as a bare recommendation and go red. Frozen sections (a numbered section whose heading carries a version already recorded as released) are historical and never rewritten, so a retired name there is allowed only if the live retirement catalog has a row for it: a retirement label, the name, the version it left in (matching the ledger) and what to use instead, next to the sentence stating that frozen sections are historical records and the catalog is authoritative. A section whose version cannot be read, or is not yet released, is judged as live. (9) *Known-limits tables*: every row of every table in `docs/KNOWN-LIMITS.md` must split into as many cells as its header — a pipe character inside inline code still splits a Markdown table cell unless it is escaped, which silently shifts the trigger and upgrade columns. |
|
|
378
|
+
| `scripts/run-integration-doc-freshness-test.mjs` | The **integration contract** (`docs/INTEGRATION-CLIENTS.md`) and the **changelog** (`CHANGELOG.md`) checked against the code, because a document with no guard rots — this one had a whole nest of drift found on it within a day of being written. Five directions, each a claim a machine can actually evaluate. (1) *Counting discipline*: the version-anchor row for the guard count may no longer carry a hand-copied number at all — it changes every time a guard is added, and writing it down is planting a timer; the export counts that are still hand-copied (the surface total, the test-hook count, the sentence describing the surface's internal composition, the sum of the sixteen domain rows, and the three sub-counts) are each compared against a value **derived** from `public-export-baseline.json`, which is the drift a human reviewer caught last time. (2) *Coordinates alive*: every `src/` `scripts/` `docs/` path the doc quotes must be on disk **and tracked by git** — on disk is not in the repo, and a doc that points readers at a file living only in its author's working tree sends every clone to nothing. A file landing in the same commit takes a named carve-out that **stops applying** the moment the file is really tracked (it can no longer let anything through, and the guard prints a line asking for it to be deleted) — deliberately not a red, since turning red on the very commit that lands the file would just manufacture a break that only a follow-up commit could clear. (3) *Arm tables*: the `hitl_out_of_slice` row and the `not_in_slice` fenced list must equal, name for name and in **both** directions, the case labels that really fall into those two buckets — read through the **TypeScript AST**, since which bucket an arm lands in is decided by the argument to `nothing(...)` and by nothing a comment says. The extractor is anchored to the one production projector: exactly one function named `eventToSdkMessage`, exactly one `switch (ev.type)` inside it, and no repeated case label — anything else is a broken anchor rather than a verdict, because a second same-shaped switch elsewhere in the file would otherwise overwrite the real one's conclusions and leave the doc agreeing with a switch nobody runs. The list is delimited by a machine-readable fence rather than by section headings, because the same section also names the terminal arms as a counter-example and prose boundaries cannot tell a member from a foil. (4) *Released sections are frozen*: an **append-only ledger** carries every version ever published — its number, the commit it was published from, and the sha256 of its section — and each one is checked, not just the current release, since pinning only the latest would set every earlier version free the moment the next one ships. The ledger cannot vouch for itself either: each recorded hash is **re-derived from that release commit** through git, so editing an old section and its constant together no longer passes — and the commit the row names is in turn checked against the `gitHead` npm recorded at publish time, which is the one value this repository cannot rewrite, so pointing an old version at a freshly written commit does not pass either. The *set* of versions that must be frozen comes from the registry too, so deleting an old row together with its section — which would otherwise remove that version from every set the guard looks at — is red rather than invisible. A failed registry call is classified rather than swallowed, and the classification consults the registry's own status code *before* it considers connection-level symptoms, so an auth refusal whose body happens to mention the network is still red rather than a skip. The version set is compared as full SemVer including prereleases — matching only `x.y.z` would silently drop a published `0.30.0-beta.1` and reopen the very hole this direction closes — and section headings are matched on a whole-version boundary so a stable release cannot bind itself to the release-candidate section sitting above it. Publishing itself is a two-phase protocol rather than a paradox: before a release, exactly one row may be marked pending and must name the current `package.json` version, exempt from the checks whose inputs do not exist yet; once the registry has that version the row must be promoted, so the temporary state cannot survive its own release. And because the pending exemption rests entirely on "this version is not out yet," it is refused outright when the registry cannot be reached to confirm that — an unverifiable premise is not a licence. Three reverse directions close the rest: a section claiming to be released but absent from the ledger, a ledger entry whose section has vanished, and a `package.json` version that was never frozen. Publishing appends a row; it never rewrites one. (6) *Sentinels*: the readers §5a hands hosts for "is this port installed" are checked against what the source actually declares it returns — `hasXxx()` is a `boolean`, the card port / HITL surface / wire target return `T \| null`, the `installHost` family returns `T \| undefined`. Testing a `null`-returning reader for `!== undefined` is *always true*, and a self-check that passes whether or not the port is installed is worse than none, because hosts retire their own fallback on the strength of it. Both directions are red: an implementation that changes its sentinel without the doc following, and a doc that names the wrong one. The roster covers the zero-argument readers and their `*For` variants alike — a multi-session host reads the variants, so leaving them off would let exactly the surface desktop depends on drift unwatched — and the §5a table and the §8-B checklist line are each checked against the source, because hosts tick the checklist, and a guard that only watches the prose table misses the line people actually follow. (5) *Packaging*: the README ships with the package and opens by pointing hosts at the integration doc, and the checklist names two more files as required reading before an upgrade — all three must really appear in the `npm pack` manifest, or an npm consumer follows a relative link that npmjs rewrites onto a private repository. Missing tooling never takes the whole verdict down with it: when git, npm or the registry is unreachable those legs print the `SKIPPED-SECTION` marker and the rest still judges, while a release commit the ledger names but git cannot resolve is red rather than skipped. The guard says in its own header what it does **not** do: it judges counts, coordinates, arm sets, released bytes and the packing list — whether a sentence is *right* is still for review and for the hosts to report (7) *Retired names*: every name in the per-version `removed` ledger of `scripts/export-liveness.json` may appear in the live sections of the integration doc only where a retirement note follows the name inside the same clause (or the table row's label cell is itself a retirement label); the scan is by identifier boundary after invisible text (HTML comments, link targets, reference-link labels, tag attributes) has been stripped, so a signature line in a code block, an inline `NAME = 4096`, a hidden note, or a note that belongs to a neighbouring name all count as a bare recommendation and go red. Frozen sections (a numbered section whose heading carries a version already recorded as released) are historical and never rewritten, so a retired name there is allowed only if the live retirement catalog has a row for it: a retirement label, the name, the version it left in (matching the ledger) and what to use instead, next to the sentence stating that frozen sections are historical records and the catalog is authoritative. A section whose version cannot be read, or is not yet released, is judged as live. (9) *Known-limits tables*: every row of every table in `docs/KNOWN-LIMITS.md` must split into as many cells as its header — a pipe character inside inline code still splits a Markdown table cell unless it is escaped, which silently shifts the trigger and upgrade columns. Also (0.98.0) every `## §NN` heading carries no repeated topic phrase and the in-flight heading stays under 1200 characters, so a merge that concatenates heading lines is caught. |
|
|
375
379
|
| `scripts/run-type-superset-ledger-test.mjs` | The type/wire **superset ledger** (`docs/type-superset.json`): positions this package adds on top of a CC-shaped contract, each carrying the evidence for what CC's own type surface does or does not have there. Completeness is deliberately uneven and the ledger says so. The `_sema_*` private-key class is checked in **both** directions (a key in the source that never entered the ledger is red, naming key and file; a ledger row whose key left the source is red) — but only for keys written as literals, which is the convention the ledger mandates. A key assembled by string arithmetic is beyond what any static rule can enumerate, so the guard fails closed on every shape it *can* decide (a bare `_sema_` prefix is red wherever it appears, save one pinned guard site) and leaves the rest as a convention violation for review to catch, rather than claiming a completeness it does not have. The two hand-surveyed classes are only checked for coordinate and evidence integrity, never discovered. Both directions read the source through the **TypeScript AST**, not a text scan, and they read two different sets out of it. A *key site* is an identifier, or a string whose whole value is the key — so `'_sema_decision-v2'` is carried whole rather than truncated at the first non-identifier character into some *other* key that happens to be registered. A *mention* is the key appearing inside a longer string, which is prose, not usage. The staleness direction counts key sites only: a comment or a doc sentence left behind after the last real mint site is deleted must not keep the row alive (mutation-proven — with both the comment and the prose string untouched, removing the one real site turns the guard red). And because a prefix can be concatenated or interpolated into a key no static set will ever see, the bare `_sema_` literal is refused outright rather than traced: every occurrence is red except the single inline `startsWith` guard the sanitizer needs, because the set of expressions a bare prefix can travel through on its way to a concatenation is open-ended and enumerating it is always one form behind. Every row's `host` must still resolve, with the key being a real **member of that declaration** rather than a string occurring somewhere in the same file — `governanceForced`/`delegation` each live on two different shapes in one file, and a member commented out is a member deleted, which a text-shaped check happily reads as still present. And the direction worth the most: each machine-form `ccAbsenceEvidence` is re-derived from the row's own `key` — the ledger's recorded string must match that derivation verbatim, since a row quietly witnessing `\bnever_present\b` is green forever while watching nothing (mutation-proven: the same edit passes the unbound form and is caught by the bound one) — and the check runs against the names the installed `@sema-agent/agent-types` `.d.ts` set actually declares, parsed with the TypeScript AST rather than grepped, so a name CC merely mentions in a comment cannot force the row into the manual escape hatch and thereby retire the very witness that was supposed to fire the day CC declares that name for real. That escape hatch is gated by an allowlist living **in the guard**, not the ledger, so claiming it costs a reviewed diff. Missing material never reads as a pass, and the verdict splits by *why* it is missing: no TypeScript parser skips the suite before it starts; a missing `agent-types` still runs and prints the first three directions, then exits **1** when `package.json` declares the mirror but it is not installed — a broken install must not retire the repository's only "the day CC declares this name" alarm, and reporting it as a skip would leave "never evaluated" and "evaluated, no drift" indistinguishable to the runner — and exits 3 only when nothing declares the mirror at all, which is the one case where the direction genuinely does not apply. Either way a run that evaluated no witness is never counted as one that did. When the mirror *is* present its **installed version** is witnessed too (the two declared floors must agree with each other and the installed copy must meet them), since four preflight probes are satisfied by an arbitrarily stale mirror — they prove the extractor speaks, not that it is current. Every direction carries a positive control — known-present CC symbols, a comment-only sample proving the extractor distinguishes declaration from mention, and synthetic corpora fed through the **same** discriminator function the real verdict uses, so a verdict quietly rewritten to return nothing takes its own control down with it |
|
|
376
380
|
| `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. |
|
|
377
381
|
| `scripts/run-park-decision-layer-test.mjs` | The decision layer behind the "stuck behind a card" family, shared by every client. A pending row that is **not in the queue** is three states, not one: a bounded, interruptible re-probe loop distinguishes *a decidable row*, *not born yet* (no positive evidence that anything settled — an empty queue proves nothing) and *settled elsewhere*, always probes at least once so a zero budget keeps the pre-fix semantics verbatim, cuts a hung read face off at the window rather than only noticing afterwards, and reports the honest failure when the window is spent instead of inventing a decision. The decision-note reader is likewise three-state: an explicit `noteRecorded: false` outranks an echoed note body, absence renders **no line at all**, and untrusted note text is flattened and bounded before it ever reaches a renderer. Row routing anchors on the deciding quantity — a row carrying `gateKind: "human"` with `toolName: "Write"` is a tool gate, because `human` is the engine's *generic* "someone must decide", not a synonym for a question — and the queue scan refuses to surface a row it cannot positively prove belongs to this session. A chain that fails after the row vanished is split by whether a card was ever presented: decided-elsewhere, or not-its-turn-yet. A row-level single-flight makes "at most one card per pending item" structural rather than incidental. The resume three-way card pins the option **order** (the zero-effect choice sits at index 0, because the frame carries no default-focus field and a stray Enter must not attach or cancel), renders only options the wired verbs can honour, collapses every ambiguous answer to zero action, omits the liveness line entirely when the engine gave no evidence, and — when there is no card lane at all — prints three real routes and exits on a dedicated code rather than reporting success |
|
|
@@ -390,7 +394,7 @@ guard still cross-checks the table by name).
|
|
|
390
394
|
| `scripts/run-approval-frame-chrome-arms-test.mjs` | The two in-stream approval frames finally reaching every host through the shared pipeline instead of one shell's private branch — the shape of a layering defect: hosts that only consume the package could not rebuild their pending cards after a reconnect, and did not clear a card the engine had withdrawn. The payload is deliberately carried as the **envelope** the upstream types declare rather than the first-version card: the stream parser applies no predicate, so narrowing here would let a legitimately newer frame pass as the older shape and invite consumers to read keys a newer card never promised. The guard therefore pins that every open key survives untouched, that an unknown version still passes through, and that narrowing is left to the host's own predicates — with the fallback being a generic card and a person, **never** an automatic denial. A frame whose version cannot be read at all is reported as malformed rather than dropped in silence, because both frames carry user-visible decisions and state changes. Both arms are registered as **required** host duties, and their duty text names the load-bearing rules a host would otherwise have to rediscover: which predicate to narrow with, that the reconnect preamble — not a replayed historical frame — is the authority on which cards exist, and that a withdrawal frame can be lost entirely. Unlike the sibling arms, these carry **no** sub-stream cutoff: an approval raised under a delegated call still has to reach a person, and filtering it by ownership is the host's job, not a reason to discard it. Finally the upstream bytes that justify the envelope discipline are checked to still be there, since the whole design rests on them |
|
|
391
395
|
| `scripts/run-terminal-status-vocabulary-test.mjs` | One place that decides whether a run has **ended** and whether it ended badly — written because that judgement had already been hand-copied three times, so the day the engine added a word for *the agent itself reported it cannot continue*, every copy missed it and a panel settled a self-reported failure as a success. The distinction the table exists for is pinned from both sides: that word belongs in it, while the two words meaning *waiting for a person to decide* deliberately do **not** — reading those as endings would bury a run that is actively waiting on the reader. A word this client does not know answers *no*, and the guard states plainly that *no* is not evidence of success: proving success means reading the positive side, so negating this predicate is the very mistake that caused two earlier incidents. The fleet lane gets the same treatment from the other direction: a workflow parked on a durable approval used to fall through to *running*, leaving the person with no hint that a card was waiting, and it now lands on the same rendered word the task lane already used — same fact, same word, checked end to end on a real row. Why the word was added directly rather than carried as a private superset key is checked mechanically against the upstream declaration being open, so the day it closes this reds and the decision gets revisited. The residue sweep is the point: the source tree must contain **no** further inlined copy of the judgement, each of the three former sites is checked to really read the single predicate, and the exemption table is empty: every former inlined copy now reads the single predicate, so a new inlined copy turns the gate red instead of being waved through. Comments are stripped with the shared token-level reader rather than a hand-written quote-pairing scanner, so a regular-expression literal that merely contains a comment opener can no longer swallow the code after it; a planted input pins that from both sides |
|
|
392
396
|
| `scripts/run-terminal-word-source-test.mjs` | The dividing lines between the owner-named terminal tables, checked from the public entry point. The word for *something outside stopped it* is not an engine reason and not a run-row ending; it appears only in the fleet, notification and task-registry tables, so reading it through the run-row table is a type error rather than a silent miss. The word for *paused and resumable* is an engine reason but appears in no terminal table, so it can never be read as an ending. The word for *the agent reported it cannot continue* is a run-row ending only, and *cancelled* belongs to the notification and task-registry tables only. Every public table is a frozen literal tuple a consumer can derive types from; every retired name — the two from the first split and the ten renamed when owners were added to the names — is absent rather than aliased; internal-only tables and predicates stay off the public surface. Every predicate tolerates malformed input, answers *no* for unknown words and prototype-chain names, and the guard states plainly that *no* is not evidence of success. The run-terminal reader keeps one arm per engine reason, and an unknown reason falls to the unknown arm with the word carried back verbatim. Word-for-word reconciliation against the upstream packages lives in the parameterized owner guard. |
|
|
393
|
-
| `scripts/run-terminal-table-provenance-test.mjs` | Every table in this package that answers *has this ended* is a machine-read copy of the terminal half of one named upstream union, lives in one owner module, and is named after its owner — and one parameterized guard holds all of them to that rule. Each registered row pairs a table with the upstream type it copies, the explicitly listed non-terminal half of that union, and any named legacy word kept only to read rows stored before the word was retired; a single comparator then requires the table plus the non-terminal half to equal the upstream union exactly, in both directions. Both sides are resolved by the TypeScript checker from the installed packages' public entry points rather than by pattern-matching declaration text, so a word added upstream, a word dropped locally, a word invented locally, an overlap between the two halves, a legacy word that is still live upstream, or an open-ended member each turn the guard red — and planted fakes prove each of those shapes is caught before the real rows are judged. The guard also checks the naming rule mechanically, finds no terminal table declared and no membership test against one anywhere outside the owner module — independent of names: a table may be imported only at registered sites (any alias, namespace, dynamic import or re-export counts), and no string array literal or equality chain built from two or more distinct terminal words may appear outside the owner module — requires each converted read point to call its owner's predicate, checks the shipped values are frozen and equal to their declared member types, checks the projection onto the local task-row words, and replays two behaviours that changed when read points moved to their true owner: a cancelled notification now settles its row as an error and announces the terminal frame, and a cancelled background child is no longer settled as a success. A behavioural regression drives the installed engine's real task registry through stop-then-read and requires both projection paths to mark the run delivered. |
|
|
397
|
+
| `scripts/run-terminal-table-provenance-test.mjs` | Every table in this package that answers *has this ended* is a machine-read copy of the terminal half of one named upstream union, lives in one owner module, and is named after its owner — and one parameterized guard holds all of them to that rule. Each registered row pairs a table with the upstream type it copies, the explicitly listed non-terminal half of that union, and any named legacy word kept only to read rows stored before the word was retired; a single comparator then requires the table plus the non-terminal half to equal the upstream union exactly, in both directions. Both sides are resolved by the TypeScript checker from the installed packages' public entry points rather than by pattern-matching declaration text, so a word added upstream, a word dropped locally, a word invented locally, an overlap between the two halves, a legacy word that is still live upstream, or an open-ended member each turn the guard red — and planted fakes prove each of those shapes is caught before the real rows are judged. The guard also checks the naming rule mechanically, finds no terminal table declared and no membership test against one anywhere outside the owner module — independent of names: a table may be imported only at registered sites (any alias, namespace, dynamic import or re-export counts), and no string array literal or equality chain built from two or more distinct terminal words may appear outside the owner module — requires each converted read point to call its owner's predicate, checks the shipped values are frozen and equal to their declared member types, checks the projection onto the local task-row words, and replays two behaviours that changed when read points moved to their true owner: a cancelled notification now settles its row as an error and announces the terminal frame, and a cancelled background child is no longer settled as a success. A behavioural regression drives the installed engine's real task registry through stop-then-read and requires both projection paths to mark the run delivered. In 0.98.0 the in-flight partition of the run-row status union has one table: it is frozen and equals the union minus the terminal partition, the session-lock holding list is that very object, the abort-cause reader answers still-live exactly when the in-flight predicate does for every status word (including a word the package once copied that neither the SDK nor the server has), and the abort-cause module carries no status-word literal. |
|
|
394
398
|
| `scripts/run-workflow-park-truth-projection-test.mjs` | The read face for *which approvals a workflow run left parked* — and the credential that must never ride along with it. Upstream strips the redemption token from that response, and this package's reader is built so the token **cannot** come back: each row is assembled field by field from the three identity keys, never copied wholesale, so an extra key appearing upstream is structurally unable to reach anything this package hands a UI. The guard proves that rather than asserting it — a poisoned row carrying a secret is read, and the secret is searched for across the **entire** serialized result, with the same search proven to find it in the input so a blind search cannot pass; renaming the credential key does not help it through, because the rule is *only these three*, not a blocklist; and the reader's own source is checked to contain no object spread, since one such line would quietly void all of it. The other half is an absence distinction with opposite consequences: a record with **no** parks field at all was written by an older engine and proves nothing about whether approvals are waiting, while an empty list is a positive statement that none are — collapsing those two would let a run whose parked approvals cannot be proven be resumed anyway, so they are kept literally distinguishable, and a payload whose rows are all unreadable answers *unknown* rather than *none*. The four refusal codes for this family are checked code by code against the engine's real bytes, never matched by name prefix, and the older umbrella code they were split out of is asserted to still be **alive** — treating the whole code as retired would make a family of real refusals vanish silently **0.68.3 (core 7.18.0):** two more keys ride the same projection duty as `parks` itself: `originUnconfirmed: true` on a row (never `false`; absence is the confirmed state) and `resumeAdmissionIncomplete: true` on the run (presence means "not a resume base"). Dropping either would turn a refused record back into an admissible one, so the guard pins both, including that neither folds into the other **0.69.1 (CC-12):** both keys now also ride the projected `WorkflowRunState`, so a host that only sees the projection can render them |
|
|
395
399
|
| `scripts/run-retired-vocabulary-census-test.mjs` | Whether a retirement really happened. When upstream removes a family, a downstream package can cut it out or keep a courteous alias — and the alias is the worse outcome: three clients keep writing branches for something nobody emits, and a status line advertises a state it can never reach. Choosing the clean cut only means something if a guard holds it, since a comment saying *retired* is not an exit code. Each registered entry is held two ways: the name must be gone from **code positions** in this package (comments stripped first, because the explanation is supposed to stay) and off the published surface, and — the half that keeps this from being self-congratulation — it must really be gone **upstream**, since that is the entire reason it was removed here; if it comes back, the disposition deserves reconsideration rather than silence. The scanner proves it can speak by finding a symbol that is genuinely present before any absence is believed, and distinguishes a mention inside a comment from one in a string literal, which is exactly the form being cleared. A closing check runs the other way: the retirement **story** must remain in the comments, including a promise this package made earlier and has now had to withdraw — deleting the history alongside the code is a bad way to satisfy *zero hits*, and leaves the next reader with code that has no reason |
|
|
396
400
|
| `scripts/run-classifier-status-test.mjs` | What state the auto-mode classifier is in **on this session** — the question a doctor line, a model settings page and a permission card’s status row all ask, and a different question from the one the approval card asks (*why am I being asked right now*), so the sentences are pinned mutually distinct from that face’s as well as from each other. The session-level half of this reading — a breaker record the engine used to keep — was **retired upstream**, and the guard now holds that retirement from **both** sides: the engine's own declarations must really no longer carry it (a fact coming back would mean the removal here was the wrong disposition, and that deserves a conversation rather than silence), and this package must carry no alias, no state word and no leftover narrowing for it — a reading kept alive for something nobody emits any more is a promise the interface cannot keep, and it left the doctor line advertising a state it can never reach. What remains is ordered by the quantity that actually decides whether the classifier is running: the fact from **this round** first, then whether this leg is armed — a decider is minted per run, so a later leg can be armed again. Not armed, and a section that never arrived, both answer **undefined** rather than *available*; that arming question has its own field and answering it twice grows a second ledger. Arming and availability are also **two words, not one**: the engine says a decider was minted *for this leg*, which is an assembly-time fact, while whether that decider answers any given round is a **per-call** one — so an armed leg reads `armed` and only a positive per-call fact (an ask whose origin is the classifier's own denial-bound fallback, which by construction stands *after* the classifier ran) reads `available`. Every other ask origin is refused as evidence and for a stated reason rather than out of caution: several are ones the classifier is structurally forbidden to answer, and for the rest a surviving ask is precisely the case where it did **not** resolve one — so reading availability off them would be a guess. The projection is a **whitelist**, so an older engine still sending the retired member loses it at the boundary while the two live facts beside it ride through untouched. Rendering never throws and never impersonates: a state word this client does not know — including the retired one, which a restored view can still carry — reaches an honest fallback that names it verbatim, carries no invented explanation of a mechanism that no longer exists, and is proven distinct from all three real sentences; prototype keys reach that same fallback rather than a function body, checked against a real out-of-table word so the comparison cannot hold vacuously The ask origin is read the one way it is read everywhere else — an own key holding a non-empty string — so an origin that exists only on a prototype no longer counts as proof that the classifier ran. |
|
|
@@ -402,7 +406,7 @@ guard still cross-checks the table by name).
|
|
|
402
406
|
| `scripts/run-assistant-arm-identity-test.mjs` | The identity keys on an assistant row, and an explicit account of the two that are **deliberately not** there. What the renderer received was a bare role-and-content object, so a dozen consumer sites downstream were each estimating what the message envelope should have told them. The id is taken from the engine's own event id rather than minted locally, because it has to be **the same value** on the live leg and on a durable replay — a freshly minted one would make a replayed message look new to a host's dedup and to rewind — and when the wire carries none the key is simply absent rather than filled with a random stand-in wearing an identity it does not have; it is also kept distinct from the envelope's own local render key, which is a different identity. The model name comes from what the host pinned when it opened the stream (the request was the host's to build) and is never guessed, since a wrong model name is worse than none once a billing or capability face looks it up. Usage and stop reason are **not** minted on this arm, and the reason is frame order rather than effort: content arms arrive before the turn's closing frame, so at the moment the arm is emitted the engine has not yet said what the round cost — anything put there would be an estimate, which is the very thing this work exists to remove — and synthesising a follow-up assistant update when the real figure lands is also refused, because that shape does not exist upstream and would place a message in the transcript the engine never sent. Their real values leave through the turn's own neutral arm as two superset keys, the usage one reusing the **same single mint point** the footer rollup already folds so the two faces cannot diverge, and the stop reason passed through verbatim as an open set — the machine signal for *was this turn cut short*, previously blind on both the stream and the trace. The existing behaviours beside them are pinned too: no arm at all when usage is wholly absent, and the sub-flow cut-out that keeps a child's turn from driving the leader's face |
|
|
403
407
|
| `scripts/run-text-segment-authority-test.mjs` | The **authoritative segment replacement** on `text_end` (server >=7.75.3). `text_end.content` now goes through the same redactor as `result` and the ledger while `text_delta` stays verbatim, so the two **may differ** — an answer that quoted a credential used to be committed to the local transcript in its unredacted form, because the arm only forwarded the boundary signal. Six timing shapes are pinned, two of which an adversarial review reproduced against the installed engine's real bytes and which the first design got wrong in both directions: a second boundary in the same turn (the per-block case on one provider lane) used to make the first segment's prose vanish, and a boundary that arrives *after* the tool card (the other lane emits it at finalize) used to be read as "this package never handled that segment" and reported nothing at all. Three additive keys, all never-false; the two shapes that look alike are told apart by the second one, because the host's action in them is the opposite. The end-to-end legs drive the real pipeline without hand-inserting a segment commit — doing so is exactly what hid the first defect. A second review round then found two combination timings on top of the first fix — a tool card followed by *more* deltas in the same segment, and a byte count that had been documented as a message count — and both are pinned here too. A third round caught a length that the prose called bytes while the code returned UTF-16 units — harmless in ASCII, and on CJK text enough to leave the credential on screen — plus a backfill ledger that had to be kept in step, so the terminal frame does not re-render the segment a second time — kept in step only where the whole stretch sits in one message, because those ledgers are per-message and a fourth round showed that writing across them charges one message's prose to another. A fifth round settled the whole class into one invariant the guard now checks against the previous release's behaviour: this package only rewrites bytes it is still holding in the current message — once a segment has crossed a package-side boundary it emits the three keys and changes nothing else **0.68.2 (CC-01):** the segment identity is now minted here, not by the host: every committed assistant text row carries a top-level `_sema_segment_id` (stamped once at the `adapt()` exit, so the durable whole-message leg and the streamed-segment leg are covered alike; thinking blocks, tool_use-tailed rows and chrome events are left byte-for-byte), `text_segment_end` carries the same value as `segmentId` before rotating, subagent boundaries never rotate, and a replayed stream yields the same identities. Three mutations (no rotation / no stamping / stamping tool_use rows) each turn the guard red **0.69.0 (CC-02):** the same authority replacement now covers the reasoning face (`reasoning_end`, server >=7.77.0): a thinking block still buffered is swapped whole and its live tail recomputed; one already committed at a boundary (the usual timing, since the first text delta commits it) is left untouched and the host is told the row to replace by its uuid, never re-emitted. Subagent boundaries are ignored and the text-segment identity does not rotate **0.69.1 (CC-09):** the run-stream replay guard still drops a frame whose event id was already seen, but it now reports the drop through the host's dropped-frame sink as `duplicate_seq` instead of vanishing silently (server 7.77.0 reuses the first reasoning delta's id for `reasoning_end`, so that authoritative segment is lost on the print lane until 7.78.1); the interactive adapter has no such guard and keeps receiving it **0.69.1 (CC-10/CC-11):** subagent segment-end frames are fenced on all three identity keys (a frame carrying only `sourceTaskId` no longer masquerades as the leader's), and a reasoning segment that spans tool cards now hands the host every committed row it covers (`committedUuids`) so nothing unredacted is left behind. **0.96.0 (CC-645):** the tail of a segment that the authoritative text had not yet streamed is now emitted *before* the segment-end event, so nothing for the same segment follows it; a host that replays the sequence (append deltas, replace with `content` when `diverged`) shows the segment exactly once, on both the text and the reasoning channel |
|
|
404
408
|
| `scripts/run-gate-negative-controls-test.mjs` | Whether the registry-shaped guards among the suites above actually turn red when the material they check really breaks — a census found the ones clean enough to rehearse safely (closed sets, mirrors, baselines, floors, a type-shape ratchet) without touching any judgement code. Each is exercised by tampering a disk copy of the real material, spawning the guard's own unmodified script, asserting it exits non-zero and names the disease, then restoring the file byte-for-byte. The guards of the same shape that are too costly to rehearse, and the behaviour/projection suites that red on their own assertions, are catalogued rather than rehearsed — see `docs/GATE-NEGATIVE-CONTROLS.md` for the three tables (all generated from `gates-manifest.json`, which carries each suite's classification), the reasons, and a one-minute manual replay recipe for each blind one. The suite reconciles its case list against that classification by name in both directions, so a case quietly dropped from the array without the manifest following is itself an undeclared blind guard. The backup that makes the restore possible is taken by **exclusive create**: checking for it and then copying are otherwise two steps, and two instances can pass the check together — the later one overwrites the only clean copy with material the earlier one has already tampered, and the rehearsal that promises to leave no trace leaves a permanently corrupted file instead. That interleaving is rehearsed too, in a throwaway directory of its own. A run that is interrupted — by the suite runner's time limit, by Ctrl-C or by its terminal closing — restores what it was in the middle of tampering with before it exits: the rehearsed guard's whole process group is stopped first, so nothing that guard started can write the tampered state back afterwards; the file is restored byte-for-byte from its backup and the backup removed only once the bytes match; the build output is regenerated from the restored sources within the grace period the runner allows (a rebuild that would overrun it is cut short, its compiler stopped, and the run says the build output was not regenerated); and the run exits with 128 plus the signal number rather than passing. Before the first rehearsal the suite also looks for backups left behind by an earlier run that could not clean up — in the trees its targets live in, following a symlinked dependency directory — and if it finds one it refuses to start, names the file and how to check and restore it, and leaves the backup untouched rather than overwriting what may be the only clean copy. |
|
|
405
|
-
| `scripts/run-engine-cap-reader-factory-test.mjs` | The one shared implementation behind every capability reader's ports, exercised as a table. Since 0.95.0 a reading is a lazily computed, frozen projection of the capabilities cache's landed response for that base URL, memoised per landing; the probe tee port
|
|
409
|
+
| `scripts/run-engine-cap-reader-factory-test.mjs` | The one shared implementation behind every capability reader's ports, exercised as a table. Since 0.95.0 a reading is a lazily computed, frozen projection of the capabilities cache's landed response for that base URL, memoised per landing; a reader has a single read port — the probe tee port, the per-reader test reset and the per-reader `forget` port are all gone, invalidation is `invalidateEngineCaps` alone, and a test that wants a clean slate resets the cache. Every reader in the table runs the *same* criteria (the table length is the source of truth, and a roster check fails the gate if any source file calls the factory without having a row): the four states (nothing landed, present, key absent so not reported, malformed or unreadable once a response has landed, so a distinct unreadable state rather than unobserved); a throwing projection or descriptor read treated exactly like an unreadable one and never escaping; a projection that changes the generation mid-flight answering unobserved without being remembered; a getter at the top level or at any nested position never being executed and reading as malformed; a freshly minted "unobserved" reading on every miss (two misses are never the same object, so a consumer that mutates one cannot taint another base URL); one independent memo per reader that never takes another down (two readers built from the same spec each project once on the same landing and each keep handing out their own object); invalidating one base URL leaving every other base URL's reading untouched; `invalidateEngineCaps` being a no-op on an empty or non-string base URL; the retired per-reader `forget*Reading` exports being absent from the root entry; the read anchor being resolved dynamically; and the deliberate split in how presence is judged per reader (own data property only, so a key that exists only on the prototype is not reported). A stored reading is frozen at every level, whether it reports a value or reports that the position was not reported, so a consumer that writes to the object a reader hands out changes nothing for later readers (a strict-mode write throws); the capabilities body passed in is never frozen. A reading follows landings and invalidations rather than any per-reader state: after `invalidateEngineCaps` every reader answers "unobserved" until the next landing, checked against a late response from the previous engine (dropped by the cache), a replacement probe that succeeds, throws, returns a bad shape, hangs or never comes, two readers sharing one landing, two base URLs landing in either order, two in-flight probes landing in either order, and a projection that invalidates midway — and a test reset of the cache never revives an older reading. A host that never invalidates is checked on a seeded random sequence (each read is the projection over the latest landing, one object per landing, getters never executed), and the downstream decisions that depend on these readings are pinned at each moment around a switch. |
|
|
406
410
|
| `scripts/run-mcp-liveness-test.mjs` | The engine's **liveness observation** about each MCP server it hosts (`wiring_manifest.mcp[].liveness`, engine-side from core 7.24.3 / server 7.91.2): one reader, one word list, one leg-level verdict. The cell answers *can this server still be reached* — it is not the connect-time verdict beside it, which the engine deliberately freezes (a server that died mid-run still reads `connected`), and it is not a re-dial's judgement either, so `status: "failed"` next to `liveness.state: "reachable"` is a **real row**: the server answered the handshake and answered with a protocol error — up, and misconfigured. The three words are read as a closed set (an exchange completed / it was lost in transport or the clock / what came back does not answer the question), and a word outside it is malformed rather than rendered, because a word nobody upstream has defined is not a sentence worth putting on screen. **Absence is the fourth reading and is not one of the words**: it means *no liveness record is available*, which on the wire covers a server this leg never reached, a declaration that could not be dialled, an older engine, and a record the projection ahead of us dropped — all indistinguishable, so it is never read as "we looked and could not tell" (a strictly stronger claim), never as healthy and never as off. The failure-class footnote rides the unreachable word only, and one that turns up anywhere else, or that is malformed, loses **just the footnote** while the word and its timestamp stay: the honest reading is then "cannot be reached, reason not given", not "this record is broken". Malformed never becomes healthy: a cell that is present but unreadable marks its row and pulls the leg-level verdict back to *cannot tell*, since letting it sit beside a reachable row would report a leg as reachable on the strength of a record that may well have said the opposite. The verdict takes the worst fact first rather than a majority or the newest reading, carries no server count — so there is no fabricated zero to be read as "no problems" — and its timestamp belongs to **that leg's** observation, not to now: the engine runs no probe and adds no traffic of its own, so this is a per-leg snapshot rather than a heartbeat. The replayed roster on the session panel and the live leg go through the same reader, and the panel's own deployment-side rows carry no liveness position today, so the verdict does not borrow a word from that face. The word list is bitten in both directions where an installed witness exists and the absence of one is itself asserted against the installed engine's version, so the day it ships the comparison starts on its own; since sdk 11.2.0 the wire types declare the cell, and the word list and the presence test are pinned to those types at compile time in both directions, so the two can never drift apart |
|
|
407
411
|
| `scripts/run-peer-lane-rules-write-capability-test.mjs` | Two more engine self-descriptions read the same four-state way as their seven sibling capability readers (`capabilities.peerLane`, `capabilities.permissionRulesWrite`): an absent key is not reported (an older engine that predates the position, never folded into `false`), `true` is present, `false` is a positive absent (the cross-session lane not being mounted on this deployment, or this particular call not being able to reach the tightening-direction write entry), and anything else is unreadable and drops the cell. Each carries its own single-source verdict (`peerLaneAvailable` returns `yes`/`no`/`unknown`; `permissionRulesWriteAvailable` collapses to a plain boolean, present being the only `true`). The write-entry position pairs with a boolean convenience port in the persisted-rules module, and this guard pins that port to derive from nothing but this one reader's own reading — never a conjunction with the lane-reachable position, and never a second read of the deployment-level existence signal the revoke surface uses (the two are documented as reading differently on purpose): a deployment where the lane answers true but the write entry's key is simply absent (an older binary) must still come back `false`, a deployment where the write entry answers true while the lane key is entirely unseen must still come back `true` (proving no silent conjunction crept in), seeding only the general capabilities cache — never this reader's own feed — must still come back `false` (proving the convenience port cannot be satisfied by the wrong table), and passing an explicit `undefined` base URL must still come back `false` even while a different, already-installed engine target answers `true` for the same position (an adversarial pass found the naive forward of that parameter falls through to the reader's own convenience default, silently answering for whichever engine happens to be installed rather than the caller's absent target — the fix routes an explicit absence through the same empty-string path the reader treats as unobserved). |
|
|
408
412
|
| `scripts/run-lane-proof-identity-test.mjs` | The **instance identity of a lane proof**: the main-lane proof is minted fresh on every emission. Previously a single module-level constant object was handed both to `laneOf(an unregistered task id)` and to some fifty main-lane emission points, so two unrelated consumers — across adapter instances, across streams, across turns — held the same object: writing a card id onto one of them was readable on the other, and the four opening main-lane events changed together. Nothing in this package writes to a lane proof and the known consumers only read it, so this is an **aliasing hazard on a published output surface** rather than an observed corruption — a consumer that uses the proof as an identity key, for dedup, or as a view-layer identity would conflate two unrelated rows without writing a single byte, which is precisely the half that freezing the object would not solve. The gate therefore anchors on instance identity: two independent adapter instances, two rows inside one instance, the same id read twice, and two arms in one beat are each distinct references; mutating one leaves the others byte-identical; and the subagent lane, which already minted fresh, is the control that proves the criterion discriminates. The main-lane **value** is unchanged — an unregistered id still answers `{lane:"main"}` with exactly one own key and still emits its events, so absence is not turned into a second kind of absence — with ordering pinned three ways (registered-then-read, read-then-registered with no retroactive edit of an already delivered proof, the same id twice) and the id failure classes pinned four ways (unregistered, empty string, absent, non-string, the last two emitting no panel event at all rather than an ownerless proof). Where one row emits **two** events — the terminal-tick and card-close legs, which each yield a lifecycle stop and a panel end — the attribution is decided once (a consumer binding a card between the two yields must not split one row across two lanes) while each event still gets its own proof, so a host consuming them one at a time cannot poison the second before it is even yielded. The run stream leg is covered as the same shape, and a syntax-tree check forbids reintroducing a module-level lane-proof object literal or a module-level `LaneProof`-annotated binding (judged on the type node, not on text, so a compile-time pin tuple that merely mentions the type is not miscaught), backed by a type-checker pass that also catches an un-annotated module-level cache such as `const x = mainLane()` while letting the callable factory itself through, while the module-private three-state sentinels of the untrusted read are frozen instead — only `Object.freeze` counts, never `Object.seal`, which still permits writes to existing keys — their exposure being confined to one module |
|
|
@@ -410,7 +414,7 @@ guard still cross-checks the table by name).
|
|
|
410
414
|
| `scripts/run-memory-verbs-wire-test.mjs` | The five memory-governance verbs as call ports — entry provenance, compliance erasure, the external-origin listing, the clearance ledger and the un-mark valve — on top of the readers above. One failure judge serves all five, and its first question is **provenance, not status**: the engine stamps a machine code on every refusal it mints, so a 501, 405, 409, 404 or 400 that carries **no code** proves nothing about who answered — a proxy or gateway returning the same status may well have passed the request on first — and every such answer is reported as "no verdict" rather than as "nothing happened". Twenty-one coded refusals each get their own arm, branched on the code alone: the status cannot tell them apart (ten different operator actions ride the same 409 here), and conjoining the status would silently demote a refusal the day the engine moved it. A coded 5xx, a coded answer with no status at all, and a coded 4xx this version does not recognise all land in the "cannot tell" arm, because on a non-idempotent verb the default for "could not classify" must be "do not know", never "did not happen". The judge is called from catch blocks, so each of its own property reads is guarded: an error object whose accessors throw is classified, not re-thrown. The capability gate runs before the call and reads the two bits the engine keeps deliberately separate (one for provenance and erasure, one for the origin faces); only an engine that positively says the face is off stops the request, while "this binary does not report that bit" and "this process never saw a capabilities body" both still send — folding "cannot say" into "is not there" is the dishonest-absence shape this package refuses, and these routes answer the capability gate before touching anything. A missing capability reading is a named, explainable error rather than a silent default that would answer for whichever engine happens to be installed. Each verb returns its own discriminated union whose success arm, refusal arm and cannot-tell arm share no keys, so a consumer cannot express "could not read it" as "it worked". The two write legs reuse the erasure and clearance receipt readers rather than minting a second copy, which keeps "this call erased nothing", "a 200 with an empty body" and "a body that could not be read" three separate things here too; the provenance account is narrowed only to its envelope and discriminants and otherwise passes through verbatim, and an account stamped with a newer envelope version is refused rather than reinterpreted. All three receipt-bearing verbs additionally reconcile identity — the account id, the attestation request id and the clearance receipt entry id must be the ones that were sent — because a readable receipt is not yet a receipt about this call. The origin listing carries the server's own echo of the scopes it actually audited, the type has no store-wide arm, and the coverage port takes a mandatory second argument and has no "clean" arm at all: the strongest thing it will say is which scopes were audited. Neither write leg is ever retried, including the refusal whose documented recovery is to send again, because that resend completes whichever clearance row is already open and the audit attribution on it is a person's signature. Request bodies are handed over verbatim — the degraded-erasure authorization is never injected — while the scope list is sent as the snapshot this port validated, so an array that reports one length while being read and another afterwards cannot make "the list I checked" and "the list I sent" two different things. An empty operator list (501 `capability.operator_principals_required`) is a refusal before anything ran (`operators_unconfigured`), not an unknown outcome. The store-form refusal family (a directory in the store that leads out of it, a scope home that is no longer a real directory) is read as one `cannot tell` arm, because the engine gives no marker for whether the refusal came before or after the commit point. |
|
|
411
415
|
| `scripts/run-dist-orphan-test.mjs` | Every `.js` / `.d.ts` under `dist/` must have a same-named source under `src/`, and every source must have its build output — because the compiler only writes and never deletes, so a module removed from the sources keeps shipping from the previous build (the whole `dist/` directory is on the publish whitelist) while the public-surface gate only looks at what the barrel exports and the hygiene gate only looks at forbidden words. Orphans are named one by one; the pre-publish posture is a clean rebuild, and this gate is the check that the posture was actually followed. |
|
|
412
416
|
| `scripts/run-dist-comments-test.mjs` | **dist ships zero comments.** Since 0.77.2 the build strips comments (`removeComments`); this gate walks every shipped `dist/**/*.js` / `*.d.ts` and counts comments with the TypeScript parser (a comment is whatever sits between two tokens, so string, template and regular-expression literals that merely look like comments, and generator methods, are not comments), failing on the first one (`DIST-COMMENT-FAIL`). Source comments are an internal surface; what still ships is code, string literals and type-level text, which the hygiene gate screens. Negative control: one plain comment appended to `dist/index.js` turns it red. |
|
|
413
|
-
| `scripts/run-task-request-omission-receipt-test.mjs` | Where every key a client hands to the request constructor ends up. The constructor used to answer "not stamped" the same way for four different reasons — value absent, no such row, wrong lane, live gate closed — and a key it had never heard of did not even get that: an unattended run could pass a system prompt, an output schema and a spend cap and receive a body holding the objective and the session id, with nothing anywhere saying what was left out or why. The guard pins the three answers apart. **Seated** keys reach the body verbatim on the unattended lane. Keys the package **knows but did not carry** never throw, never reach the body, and each gets a receipt row with one word from a frozen cause list — every present key is on the body or on the receipt, never both and never neither, checked across both lanes with the live gate open and closed against a key-by-key table written independently of the package's own routing. Keys the package **does not know** are refused loudly and are a separate cell, not a fourth cause: the cause list has no word that could hold them, and the seat reader answers `unknown`, not `none`. The cause list is bitten from both sides (exact, every word producible, nothing produced outside it, the judge table's keys read from source through the TypeScript parser) and no second hand-copied list may exist in `src/`. Upstream claims are read straight off the installed SDK typings: a key seated in this release must be a named request field, a key registered as having no upstream counterpart must not be — the day it appears the guard turns red — and the index signature counts as evidence for nothing. The per-run file-history opt-out word is covered the same way: seated on the two user lanes behind the live gate and absent from the side-channel lane, stamped only for its one legal word, refused loudly for any other value, and read after every existing declaration so that it cannot erase them. The wording shown to end users is pinned alongside the wording for integrators: a second sentence table, keyed by the same frozen cause list and exhaustive over it at compile time (the check sits on a two-column table literal — a lead-in plus a reason clause — from which the full sentences are derived), gives each cause one plain sentence that says the item was not sent and why (the integrator table now carries the same literal-level check — before, a missing sentence failed the build but an extra one did not), with no integrator vocabulary and no claim about whether anything took effect; every sentence differs from the others and from the integrator wording and passes the published-text hygiene list, and an unknown word, a non-string, a throwing object or a prototype-chain name gets one honest generic sentence instead of a throw, an echo or an invented cause. The
|
|
417
|
+
| `scripts/run-task-request-omission-receipt-test.mjs` | Where every key a client hands to the request constructor ends up. The constructor used to answer "not stamped" the same way for four different reasons — value absent, no such row, wrong lane, live gate closed — and a key it had never heard of did not even get that: an unattended run could pass a system prompt, an output schema and a spend cap and receive a body holding the objective and the session id, with nothing anywhere saying what was left out or why. The guard pins the three answers apart. **Seated** keys reach the body verbatim on the unattended lane. Keys the package **knows but did not carry** never throw, never reach the body, and each gets a receipt row with one word from a frozen cause list — every present key is on the body or on the receipt, never both and never neither, checked across both lanes with the live gate open and closed against a key-by-key table written independently of the package's own routing. Keys the package **does not know** are refused loudly and are a separate cell, not a fourth cause: the cause list has no word that could hold them, and the seat reader answers `unknown`, not `none`. The cause list is bitten from both sides (exact, every word producible, nothing produced outside it, the judge table's keys read from source through the TypeScript parser) and no second hand-copied list may exist in `src/`. Upstream claims are read straight off the installed SDK typings: a key seated in this release must be a named request field, a key registered as having no upstream counterpart must not be — the day it appears the guard turns red — and the index signature counts as evidence for nothing. The per-run file-history opt-out word is covered the same way: seated on the two user lanes behind the live gate and absent from the side-channel lane, stamped only for its one legal word, refused loudly for any other value, and read after every existing declaration so that it cannot erase them. The wording shown to end users is pinned alongside the wording for integrators: a second sentence table, keyed by the same frozen cause list and exhaustive over it at compile time (the check sits on a two-column table literal — a lead-in plus a reason clause — from which the full sentences are derived), gives each cause one plain sentence that says the item was not sent and why (the integrator table now carries the same literal-level check — before, a missing sentence failed the build but an extra one did not), with no integrator vocabulary and no claim about whether anything took effect; every sentence differs from the others and from the integrator wording and passes the published-text hygiene list, and an unknown word, a non-string, a throwing object or a prototype-chain name gets one honest generic sentence instead of a throw, an echo or an invented cause. The full sentences and the generic fallback stay byte-identical to the sentences shipped in 0.88.0, every reason clause inside them starts lower-case with no closing full stop, an unreadable cause gets the generic sentence, no full sentence may appear as a literal anywhere in the source, and the separate reason-clause export withdrawn in 0.98.0 is checked to be absent from the root entry. When a consuming client's own copy of the cause table is on disk, the guard also checks that every word in it is one of the package's causes, with no word listed twice (a word only the client's table has fails; a word only the package has is reported, not judged), and reports that check as skipped rather than passed when the table is not there. A request key the engine retired and refuses by name is, from 0.92.0, refused at construction by the same unknown-key rule as any other key the package does not know (it names the key and never echoes the value), on every lane, in every live state and through both doors; the seat reader answers unknown and the wire-side judge names it. |
|
|
414
418
|
| `scripts/run-session-policy-wire-test.mjs` | The per-session tool-rule face: the capability bit that says whether an engine keeps such rules at all, and the narrow read plus tightening orchestration built on it. The bit is read the same four-state way as its sibling capability readers — an absent key is not reported (this binary predates the position itself, which says nothing about whether the face exists), `true` is present, `false` is a positive absent (this deployment keeps no per-session rules), any non-boolean value is unreadable and drops the cell rather than being folded into "absent", and a capabilities body that is not an object at all (an array included) is unreadable rather than "not reported". Its single-source verdict answers whether to show the tightening entry: only an engine that says yes is `yes`, both a positive no and a binary too old to answer are `no`, and never having observed a capabilities body is `unknown`. Whether to put a request on the wire is deliberately a **different** question with a different answer for that last state, and lives with the orchestration. The read narrows three ways that must not collapse into each other: a record that really is empty (present, version zero — what an engine answers for a session no rules were ever written for), a record that cannot be read, and a call that failed with a typed disposition. A half-bad record — one rule bucket well-formed and another the wrong shape — counts as unreadable in full, because the write verb replaces the whole record: dropping the bad bucket and writing the rest back would empty it, which relaxes the rules while the caller sees a 200. An unreadable version stamp is never filled in with a zero, a bucket that reports an implausible number of entries is unreadable rather than walked or truncated, each array's length and each of its indices are read exactly once, and a throwing accessor is unreadable rather than propagated. Every load-bearing key is read as an own property — the envelope, the version stamp, each of the five buckets and each array index — because a prototype lookup would let a polluted prototype put a bucket into the reading that the wire never carried, and since the write replaces the whole record the union would then write that invented restriction back as a real one; a guard pollutes the object and array prototypes in place and proves all four shapes stay out. Because the write replaces the whole record, adding a restriction means writing "what is already there, plus the new entries": the union only ever adds, de-duplicates verbatim, keeps a bucket that is present but empty (present-and-empty and absent are opposite meanings, and dropping it would relax the rules), mints no bucket neither side had, copies entry bytes as they came (no trimming, sorting or path rewriting — those judgements belong to the engine), and takes its bucket names from the engine's own type surface rather than a hand-copied list, so a new bucket upstream is a compile error instead of a silently dropped one. Which differences count as relaxing is the engine's judgement and is never re-implemented here: a refusal on those grounds is reported verbatim, never swallowed and never retried. The orchestration is guarded on three axes. Timing: when the record moves between the read and the write, it re-reads and re-writes **exactly once** — two reads and two writes, no more — and the second attempt's union carries the other writer's entries, which is the entire point of re-reading; a second collision is reported rather than retried a third time, and an uncontended write makes exactly one round trip. Concurrency: an explicit barrier holds both orchestrations first reads at the same version before either may write, and the criterion is how many times the store actually rejected a stale version rather than how many writes it saw — the latter is equally true of two serial successes, so it would stop detecting contention the day the interleaving changed. Under real contention the store rejects exactly once, both writers land on strictly different versions, both writers entries survive in the final record, and the round trips are exactly three reads and three writes; the same two orchestrations run serially are asserted to reject zero times in two reads and two writes, which is what proves those numbers are discriminating. On a store where every write loses the race both report a collision having written exactly twice each. Failure classification: a relaxation refusal, a refusal to stamp a version the store cannot establish (the same status code as the relaxation refusal but a different machine code, and folding it into that arm would send the caller off to edit entries that are not the problem), a missing session, a deployment without the face, an ownerless session, a collision code, a bare conflict with no machine code, a rejected body and an unauthorized call each land on their own arm — the collision arm is matched on the machine code verbatim rather than on the status, because two different situations share that status and only one of them is worth retrying. The remaining split is not "which code is this" but "did the engine answer at all": an answered client-side refusal is allowed to say nothing was written, because every such refusal on this endpoint is emitted before the record is touched, while a throw with no answer at all — a dropped connection, a timeout, a response body the transport itself could not decode, a server fault — can only say "unknown", since that throw may well have happened after the record was already saved. Two guards prove that is not theoretical: a write whose receipt cannot be read, and a write that throws after the fixture store has committed, both leave the record changed. Neither is success nor failure: the only honest answer is "unknown", it carries no version, and it is never retried. The direction of the change is likewise never claimed. The engine’s tighten-only rule is an identity gate, not a field gate — for a principal the deployment treats as an operator it does not run at all, so a union that adds a name to an existing allowlist is accepted and really does widen it. This package does not mint a second copy of that rule, so what it reports is the fact it can stand behind: the record now holds what it already had plus the entries sent here. The sentence for a saved write is pinned to contain no claim of tightening, narrowing or restriction, and a guard reproduces the operator case to prove the widening is real while the wording stays honest. Every sentence the module mints is checked pairwise distinct, with the receipt-unreadable one required to keep its "may already be in effect" and the record-unreadable one required to say nothing was written. When a read comes back without a usable record, one sentence per failure kind and per unreadable position says so in plain words, never naming internal codes, never saying the record is empty, and falling back to one general sentence for anything it does not recognise. A sixteenth sentence covers a session that has not begun on the engine yet — a state only the host knows, since the server gives the same answer for a session that does not exist and for someone else's session — and it is checked like the others: distinct from the other fifteen and from the general fallback, free of machine words, never claiming the record is empty or that nothing is restricted, never saying the contents are unknown (there is nothing to know yet), read from own data properties only, and compared with the previous release's package so that the other fifteen sentences and the general fallback stay unchanged byte for byte. |
|
|
415
419
|
| `scripts/run-persisted-rule-write-test.mjs` | The **single-step tightening write** for persisted permission rules — the dual of the revoke surface, and the half where a hopeful reading is expensive. The two behaviours this entry accepts are **derived** from the three-state vocabulary by subtracting the widening one, never hand-copied: the guard bites in both directions (every word in the derived table is really accepted; every constructed outsider — casing variants, trailing whitespace, the widening word itself — is refused before a single round trip), keeps a word-count canary against the parent table, and pins that the source file contains **exactly one** array literal carrying two or more behaviour words, so a second hand-written table shows up as a boundary failure rather than as drift nobody reads. A standing approval is minted by answering a permission question or by importing settings; this entry is not a third route, and the widening word is unspellable in the type. The outcome is a discriminated union whose two failure arms are **not** interchangeable: ten refusal causes each promise the same single thing — not one byte reached the store — while three separate words say the opposite, that the outcome could not be read at all. The service's own "I cannot tell" (a write that could not be confirmed as standing: store wobble, a redemption leg with no decidable ending, or a write that landed and was revoked concurrently before the read-back) stays in the second group, because announcing "nothing was written" invites a clean retry that is not clean, and announcing success misreports a tightening that may already be gone. Anything the shared failure classifier does not recognise defaults to the same place — this is a non-idempotent verb, so "unclassified" must mean "unknown", never "no write": a 500 can happen after the store commits. A 2xx whose body cannot be read is pinned in the same direction and from both sides: it reads as unknown, and the unknown arm carries **neither** the revision nor the written row, so a consumer cannot even spell the shape that would let "unreadable" pass for "written". `persisted` is guarded against the reading everyone reaches for first: it says *this call wrote*, not *a new rule now exists* — an equivalent rule already in the store still mints a fresh causal point, so the lane honestly reports `persisted`, and the material for judging whether the **logical** rule is new (the approval ledger on the returned row) is handed to the caller rather than folded into the discriminant, since the package does not have the one fact that judgement needs. The returned row goes through the **same single narrower** the listing surface uses — proven by running one row corpus through both legs and asserting the two verdicts agree entry for entry (an adversarial pass that forks the listing leg back into an inline copy reds here immediately), plus a source pin that the predicate is defined once and called from exactly the two legs. That sharing is what keeps a row whose behaviour cell is unreadable **visible in both places** rather than hidden by one of them — and the shared narrower is deliberately followed by a second, *different* question only the write leg can ask: is the row that came back **the rule that was just sent**? A rule's identity is a triple, so a receipt missing its behaviour cell, carrying the sibling state, carrying the widening one, or naming another text or another scope is not evidence that the requested tightening is standing; it reads as unknown with its own word, kept distinct from "unreadable" so the two stay tellable apart, while display and derived cells may vary freely. An adversarial pass found both of the gaps this pins: the receipt check that only looked at whether the row was renderable, and a subtler one — pulling the verb off the injected port and calling it bare drops the receiver, so a host that hands over a real resource object (a class instance whose verbs reach the transport through `this`) would see every write throw and be reported as "could not tell", retry after retry, while the package's three other ports call their verbs as methods and work fine. Both are pinned from the failing side: a shorthand-method facade and a class-instance facade must reach the transport and return a real outcome, with the bare-call throw proven to be a real failure mode first. A 405 is split in two, because only the engine's own bare code is evidence about **the engine**: with it, the path exists and this verb does not, so this worker predates the verb; without it — an absent, empty, or foreign code, which is what a proxy or gateway blocking the method typically returns as HTML or an empty body — what was seen is that the verb was refused, while **who** refused it and **at which hop** is unknown, so it lands in the unreadable-outcome arm with its own word rather than sending someone to upgrade a worker that is fine, hiding an entry that is live, or — the part a second adversarial pass insisted on — promising that nothing was written. That promise is what the refusal group means, and a middlebox is free to forward the request and only then answer 405 on its own policy, so a caller who skipped reconciliation on that word would leave behind a standing refusal the user believes never took effect; the guard pins exactly that shape, with a double that writes the rule and *then* answers 405, and with the engine's own bare code still landing in the refusal group beside it. (The same passes caught the naive status-only reading and the asymmetry where an empty-string code fell through to a different bucket.) The documented recovery for an unreadable outcome — retry, then reconcile — is pinned to be **ledger-safe** rather than merely asserted: against a double that models the engine's own "is this identity already standing?" question, re-sending the same identity comes back as a no-op with the approval ledger and the bucket revision both unmoved, however many times it is repeated, while two concurrent writers each landing a causal point are both honestly reported as having written. Finally the four local gates are pinned to be free: a missing write verb on the injected port, an unwritable direction, an unreadable identity pair, and a principal key that is present but cannot name anyone all refuse **without sending anything** — the last of those because silently degrading a blank target into absence would land a tightening aimed at someone else in the caller's own bucket and return a 200; an empty operator list (501 `capability.operator_principals_required`) is reported as a refusal (`operators_unconfigured`) rather than an unknown outcome |
|
|
416
420
|
| `scripts/run-registrar-tables-test.mjs` | The four registrar table bodies — this Guards table and the three census tables in the repository's negative-control record — are **generated** from `scripts/gates-manifest.json`, the one file that describes a suite. Every row's text must equal what the generator emits, the manifest's suite set must equal the suites on disk, each entry must declare how it is negative-controlled (rehearsed, blind, or behavioural, with the census taker itself declared as such since it does not appear in its own tables), and each row's outward prose is scanned against the published-surface word list — the same list the packaging-hygiene guard uses, shared rather than copied — before the generator may write it into this file. Adding a guard is therefore one manifest entry plus one generator run instead of six hand edits across three files, and a description that drifts in one place and not the others stops being expressible. The row count is no longer what is compared: the earlier arrangement checked the census tables by **length**, so rows naming the wrong suites reconciled green. Positive controls run entirely on in-memory copies — a changed description, a dropped entry, an added entry, a changed class and a hand-edited row on disk each have to make the same judgement speak — and the quieter halves are pinned too: nothing outside a table body may move, a line inside one that is not a recognisable row makes the generator refuse rather than drop it, byte equality is backed by a column-count check (a cell holding a bare pipe splits a row into extra columns, and a code span does not protect it), and the malformed rows kept byte-for-byte as they are found are registered individually, so the registration turns red the day it stops being needed rather than outliving its reason — audited in both directions, since a registration pointing at a row that is no longer malformed and one pointing at a guard that was reclassified or deleted are both exemptions nobody reads |
|
|
@@ -424,15 +428,15 @@ guard still cross-checks the table by name).
|
|
|
424
428
|
| `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 |
|
|
425
429
|
| `scripts/run-file-history-capture-capability-test.mjs` | The engine's file-history-capture self-description (`capabilities.fileHistoryCapture`), read the same four-state way as its sibling capability readers: an absent key is reported as not reported (never folded into `off`), words are taken as an open set so a newer mode is not mistaken for a malformed answer, `fileHistoryCaptureMode` recognises `off` and `on-optional` (captured, and a run may opt out) — any other word, including the older `on-always` that pre-`7.104` engines sent, reads as unknown — and the wording for `off` speaks about capture only — whether code can be rewound is left to the rewind readings; a malformed reading makes neither the verdict nor the wording throw. The request side is covered too: the fragment helper yields the per-run opt-out word only when the reading is `on-optional` and the host asked not to capture — never for an older engine that would refuse the unknown key — and the fragment is driven through the real request constructor on every lane. The recognised words are checked both ways against the capture vocabulary of a `7.104` engine package. |
|
|
426
430
|
| `scripts/run-model-identity-resolvability-test.mjs` | The model-identity judgement a client makes before letting anyone in: can the engine it is about to use start with a model name? Each end reports what it read from each place that can feed a model name to a local engine (complete, partial — a gateway address or a credential but no model name —, absent, or unreadable), or, for an engine that runs elsewhere, whether that engine has been seen answering; `modelIdentityResolvability` answers resolvable, unresolvable or unknown. Having part of an upstream configuration is not having enough of one, so partial lanes never add up to resolvable; a lane that was not reported or could not be read makes the answer unknown rather than unresolvable; an engine that runs elsewhere is never judged unresolvable and local lanes are never consulted for it (it does not start without a model name, so seeing it answer is enough to call it resolvable). `modelSetupDecision` combines that answer with whether this end can configure a model at all: setup is offered only for unresolvable on an end that can configure one, an end that cannot says so and points at whoever runs the engine, and unknown never opens setup. The detail and notice sentences are checked to be pairwise distinct, unknown sentences neither claim a model is configured nor that it is not, and the module is checked to import no platform I/O. The catalog lane counts as complete only when the host reports that the local engine accepts a catalog default; a report of `false` reads the catalog as partial, and no report (an explicit `undefined` included) or an unusable one leaves that lane undetermined — the same answer as before the catalog could be complete. Every sentence about a complete catalog names the catalog, including the undetermined sentence when the report is `false` and another lane cannot be read, and a 25-cell matrix pins that the report changes nothing when the catalog is not complete. |
|
|
427
|
-
| `scripts/run-cloud-effective-projection-test.mjs` | The cloud control plane's effective-configuration response beyond its four configuration domains, and what a locally started engine does with the models document derived from it. Three top-level keys are read with the meaning their producer gives them: `warnings` (degradation warnings from the build that produced the served view — an empty list is a clean build, an absent key is an older server that cannot tell), and `budget` / `runtimeCaps`, where `null` means two different things: nothing resolves for this principal when you view yourself, and values withheld when the response previews another principal. A missing key or a wrong type is a third state, unknown, and none of the three is ever folded into a zero, a `false` or "no budget". A malformed warning row, budget field or cap costs only itself, and a known budget field of the wrong type is named as not shown rather than silently read as "no limit on this axis"; warning kinds are an open set, so a kind this client does not recognise still produces a warning line. The budget and cap readers are reconciled against the installed settings schema. The single wording source puts degradation warnings first and keeps every "not set / withheld / unknown" sentence free of digits, while a zero the server really sent is shown as a zero. On the models side, the document is written in the shape of the installed settings schema: the default model rides `roles.default` and the @-mention policy rides `atMention` (`only` carries its list, `none` is written, and `all` is left to the schema default). Every role goes through one gate: a role that names something outside the catalog is dropped and recorded, and a role that names a catalog model is carried only when the host injects an entry check that accepts that entry — one rejected entry costs only the roles that name it — while without the check no such role is written and each one is recorded with the reason `unvouched` (the missing check is the cause, not the role), because the package cannot tell which entries the local engine will discard and a role naming one makes it discard the whole models domain (a role chosen by capability or price names no entry and is always carried). When the host injects an entry check the document also carries the tier groups
|
|
431
|
+
| `scripts/run-cloud-effective-projection-test.mjs` | The cloud control plane's effective-configuration response beyond its four configuration domains, and what a locally started engine does with the models document derived from it. Three top-level keys are read with the meaning their producer gives them: `warnings` (degradation warnings from the build that produced the served view — an empty list is a clean build, an absent key is an older server that cannot tell), and `budget` / `runtimeCaps`, where `null` means two different things: nothing resolves for this principal when you view yourself, and values withheld when the response previews another principal. A missing key or a wrong type is a third state, unknown, and none of the three is ever folded into a zero, a `false` or "no budget". A malformed warning row, budget field or cap costs only itself, and a known budget field of the wrong type is named as not shown rather than silently read as "no limit on this axis"; warning kinds are an open set, so a kind this client does not recognise still produces a warning line. The budget and cap readers are reconciled against the installed settings schema. The single wording source puts degradation warnings first and keeps every "not set / withheld / unknown" sentence free of digits, while a zero the server really sent is shown as a zero. On the models side, the document is written in the shape of the installed settings schema: the default model rides `roles.default` and the @-mention policy rides `atMention` (`only` carries its list, `none` is written, and `all` is left to the schema default). Every role goes through one gate: a role that names something outside the catalog is dropped and recorded, and a role that names a catalog model is carried only when the host injects an entry check that accepts that entry — one rejected entry costs only the roles that name it — while without the check no such role is written and each one is recorded with the reason `unvouched` (the missing check is the cause, not the role), because the package cannot tell which entries the local engine will discard and a role naming one makes it discard the whole models domain (a role chosen by capability or price names no entry and is always carried). When the host injects an entry check the document also carries the tier groups and the active tier group (without the check neither is written), and every catalog reference the local engine's schema would reject — a role, @-mention entry, tier binding or active group that names something outside the catalog served to this principal — is dropped and recorded, because one dangling reference makes the local engine discard the whole models domain and fall back to its environment catalog; this is proven by reading the produced document with the installed file store. The @-mention policy is narrowed with the settings contract's own narrowing function: an allowlist pruned to empty becomes "nothing may be mentioned" (never "everything") and gets one warning saying so, and a policy this client cannot read is not written (the schema default applies) and gets its own warning. The keys the settings schema retired (`default`, `atModelAllowlist`) are never written and, from 0.98.0, never read: a reply carrying them and the same reply without them give byte-identical documents, issue lists, notices and pool-delete answers. Both generations of the same response produce the same document, byte for byte, and the gate compares the result with what the settings contract's own one-time migration makes of the older shape. Warning kinds with their own sentence are reconciled against the kinds the installed schema can produce. A per-model budget in which no field could be read is never described as having no limits. Registry annotation keys on model entries (`origin`, `overridesTeam`) are removed before the models document is written: the local engine's schema does not accept them, and a configuration refresh would otherwise be rejected as a whole. A model entry the host-injected entry check rejects is left out of the document and references to it are dropped: the local engine drops such an entry at startup, but a refresh rejects the whole configuration over it, so the gate requires a clean read of the produced document; when no entry passes the check, the catalog is kept as sent and gets its own warning, which the gate proves by reading the document back. The check receives a copy, so it cannot alter what is written. Tier words outside the local schema's closed set are dropped and recorded as unsupported, and the package's tier word list is reconciled against the installed schema in both directions; an active tier group is judged against group names, never model names. Since 0.96.0 the MCP leg's account of HTTP transport keys is reconciled against the installed settings contract: the connector assertion header is not forwarded while the request shape has no such key (the guard turns red the day it does), and an entry that names both the principal header and the assertion header is left out with the reason `principal-and-assertion-header` instead of having one of the two picked, answering exactly as the contract's own parse does whenever both are well-formed names; the dropped entry's header references are not reported as missing environment names. The request shape is read with the type checker, so a quoted or inherited declaration counts as declared. From 0.98.0 an MCP http entry's `assertionHeader` is forwarded as written on both the cloud path and the local `.mcp.json` path, through one public reader that also leaves out an entry setting both identity headers, and a notice names every server that declares it, because no supported server sends that header yet. The written shadow models document is a data copy: changing the input afterwards, or a `model` getter that answers differently on a second read, does not change it. |
|
|
428
432
|
| `scripts/run-websearch-verdict-test.mjs` | The per-request web-search configuration a host puts on the wire. Newer servers refuse the whole request when that section is malformed — and a missing or misspelled search provider now counts as malformed, because the section names where the searches go and an unknown destination is refused rather than silently swapped for the deployment's own backend. The old readers in this package dropped such a section without a word, which let the server swap destinations after all. The guard pins the new three-way verdict (absent, honoured, malformed with the field that is wrong) against the server's own judge, vector by vector, whenever that judge is available next to this package; it pins that a half-configured environment is malformed rather than ignored, that a malformed environment never falls back to the settings file (that would change the destination too), and that neither the sentence shown to the user nor the recorded reason repeats an endpoint, a key, or a search-parameter name or value — an unrecognised provider is never echoed either (the sentence lists the valid words instead), so a URL or key pasted into the wrong field does not come back out, even when it happens to be all letters. A host's key store is plugged in through a callback the package calls only after the provider has been recognised, so the precedence between environment and settings stays inside the package. Since 0.83.0 an endpoint that carries a user name or password is malformed as well (the server judges the same way from 7.101.0), the reason sentences match the server's own word for word, and the three older readers that dropped a misspelled provider are gone. Since 0.93.0 the search key is checked in the same pass: a key that cannot be carried as an HTTP header value — a line break, a NUL or another control character, or a character above U+00FF anywhere but at the very ends — is malformed, with the server's own sentence (the server refuses such a key from 7.118.0 on, and a deployment's own start-up configuration is refused too), while a trailing or leading newline, as secret files often carry, is accepted just as the runtime accepts it. The rule is a pure re-implementation of the runtime's two-step header check, so the package stays free of Node built-ins; the guard pins it character by character against the real Node header pipeline, sends the whole Latin-1 range through a real loopback request, compares a large corpus with the previous release byte for byte (legal keys read exactly the same; only keys that could never be sent differ), and pins that the server's refusal is read by its code alone — nothing in the package matches its sentence. |
|
|
429
433
|
| `scripts/run-hooks-merged-disable-projection-test.mjs` | The fourth governance leg of the hooks projection: `disableAllHooks` set in a non-managed settings source. The value that counts is the **merged** one, read from the host through the optional `SettingsPort.mergedDisableAllHooks()`, because a per-source approximation ("any source says true") reads user `true` with local `false` backwards — the merged value there is `false` and every source's hooks ship. When the merged value is `true`, only managed-settings hooks are sent to the engine: non-managed settings can switch off their own hooks, never the managed ones, and a managed `disableAllHooks` is still judged first and sends nothing at all. A full matrix over the four sources, each true, false or absent, is merged with the reference rule (later sources override earlier ones, managed settings last) and every cell's projection is asserted. The reader is the only authority: per-source values never second-guess it, and only a strict `true` counts. A host that does not implement it keeps the previous behaviour and gets exactly one warning per installed settings port, never one per request, and none on paths where the reader would not have been consulted; a reader that throws is treated as `true`, so managed hooks still ship. The session goal's Stop hook and the final-verification yield rule, which reads the projected hooks, follow the same verdict, and the trust gate and the three managed gates are evaluated before the reader is ever called. The last leg pins the member's declared shape in the built declarations: optional, no parameters, returning a boolean or `undefined`. The flag settings source (a settings file or inline settings given at startup) is projected after the local source, and settings hooks stay concatenated managed first (managed → user → project → local → flag): the engine runs hooks in request order, and three of its budgets go to whoever comes first — the per-event wall-clock budget, the per-event cap on model-backed entries, and the total cap on added context — so a managed hook placed after the others could be crowded out and silently not run. The flag source moves with user, project and local under every managed or merged gate; its exec-form entries are dropped and warned about like any other settings source, and the not-run notice names it `Flag settings`. An optional leg feeds the request body to an installed server's hook runner and requires the managed deny, block and context to take effect while non-managed hooks exhaust each of those budgets, and pins the known cost of that order: a non-managed `PreToolUse` hook placed after the managed one can still rewrite the input after the managed check allowed it. Without an installed server the leg reports that it did not run. Safe and bare mode: the host reports its startup mode through the optional `SettingsPort.hooksStartupMode()`, which both the plain and the plan path read and which answers `'safe'` or `'bare'`; the flag already carried by the plugin hooks reading, which only the plan path reads and which cannot tell the two modes apart, counts as safe mode unless the reader named a mode. In either mode only managed-settings hooks are sent from the settings sources: bare mode is handled like safe mode, so managed hooks are still sent, also under the managed-only gates. In both modes plugin hooks are excluded as before and the session goal's Stop hook is still sent. Only the two exact words count; a host that does not implement the reader, returns `undefined`, returns any other value or throws keeps the previous request byte for byte, checked for every such reply, both flag states, both paths and with or without a session goal; any other value and a throw each leave one debug line, and so does a reader written as a property instead of a method, which narrows nothing. The reader is called at most once per request and not at all when hooks are already switched off entirely. The optional server leg requires the left-out sources' hooks not to run while the managed ones still run and still deny, in either mode. A hook written in more than one settings place is sent once: entries on the same event, in groups that are identical apart from their hooks (same matcher), with the same identity — command, shell, arguments and condition for command hooks; type, prompt and condition for prompt and agent hooks; URL for HTTP hooks; server, tool and input for MCP tool hooks — are sent once, at the position of their first occurrence in the concatenation order and with the fields of the copy from the highest-precedence source (managed, then flag, local, project, user; within one source the later copy), so a managed copy keeps both its place and its values and a user copy duplicated in flag settings keeps its place but uses the flag copy's timeout; the other entries keep their order, groups without duplicates are sent as they were, and a group left empty is not sent. Different matchers, events, a one-byte command difference, a different condition, an absent versus explicit shell, different types or extra group keys are not merged; malformed entries and entries whose identity cannot be computed are left alone; plugin groups and the session goal hook do not take part, and hooks removed by safe or bare mode never come back. The optional server leg requires such a duplicate to run once with its added context appearing once, and a user copy with a one-second timeout duplicated by a flag copy with a ten-second timeout to run to completion on a two-second command. |
|
|
430
434
|
| `scripts/run-memory-saved-projection-test.mjs` | Engine memory writes (a successful `Remember` tool call) moved off the transcript onto the additive `memory_saved` chrome event, driven through the real pipeline: zero transcript rows for the write (the transcript is byte-identical to the same frames with the write reported as not successful), exactly one event whose `notes` carry the note text verbatim (notes, not file paths) and whose key set is exactly kind / laneProof / id / notes; no event for a missing, empty or non-string note, a non-`true` `ok`, a tool error, a missing result or another tool name; two writes give two events in order with distinct ids; a sub-agent write rides the sub-agent lane and an empty parent id emits nothing rather than falling back to the main lane; the `id` is derived from the write's wire key (the tool-end event id, else the tool-start event id, else the call id; empty ids count as absent), so projecting the same wire events twice gives the same id, and it never collides with the tool result row of the same or another call; the event sits right after the tool result row; the arm is registered as required. |
|
|
431
435
|
| `scripts/run-result-frame-projection-test.mjs` | Result frames and the synthesized terminal rows. The CC key `terminal_reason` is minted on result frames only where it follows from what the engine reported: `completed` on success, `max_turns`, `budget_exhausted` and `structured_output_retry_exhausted` for the three matching engine codes, on both the done-frame path and the failed-event path. Every other outcome leaves the key absent as an own property rather than present with an undefined value: wall-clock and token-budget limits, the classifier denial limit, cancellation, unknown codes, blocked, paused, unreadable or missing terminal records, and the busy-session refusal. The public reader `terminalReasonForResult` shares the minting predicate and is checked to agree with the minted key on every frame the gate produces. Both the minted key and the reader derive the word from the frame's CC subtype (success with `is_error` strictly false, and the three limit subtypes), not from the error code, so a replayed row whose status is paused, blocked or unrecognised never carries a word that contradicts its subtype. The four words are checked against the mirrored CC union, and the minting file is checked to hold no hand-copied code literals. The renamed superset keys (`_sema_error_code`, `_sema_salvaged_result`, `_sema_model_degraded`, `_sema_selected_model`, and the row flag `_sema_api_error_message`) are driven through the real stream pipeline. Each must be present under its new name, the old name must be absent, and every frame the gate saw is swept for old names. The selected model appears on error envelopes whenever the terminal record carries it, and never on a failed event, which has no record. It stays separate from the provider-reported model name. The two in-package readers still work: the interactive result arm reads the salvaged text under its new name, and the print init gate treats the renamed flag as the run having ended. Since 0.86.0 the result arm no longer reads `result` on an error envelope, meaning a frame whose subtype is a string starting with `error_`, including error subtypes it does not recognise yet; a compile-time check pins that every CC error subtype carries that prefix. Such an envelope that carries the salvaged text only under the old `result` key yields no answer row, no appended suffix and no divergence event; when both names are present, or the new one is present but not a string, the old one is never read. Everything else reads `result` exactly as before, in all three producing cases (whole answer, missing suffix, divergence): success frames, a success frame flagged `is_error: true` (checked equal to the same frame with `is_error: false`), and frames whose subtype is a string without that prefix (empty, unrecognised, `Success`, a bare `error`), absent, or not a string (each checked equal to the same frame with subtype `success`). From 0.94.0 four root readers return the result frame's string superset keys (`resultErrorCodeOf`, `resultSelectedModelOf`, `resultSalvagedResultOf`, `resultParkFailedReasonOf`): they read only the frame's own data properties, never fall back to the retired names, run no getter and never throw. A synthesized terminal row that carries `_sema_api_error_message: true` has its text credential-washed on both projection paths (minted by the stream projection, or rebuilt by `adapt()` from an older transcript row), so the flag itself tells a client the text is already washed; model rows are left byte-for-byte alone. |
|
|
432
436
|
| `scripts/run-layering-shadow-export-test.mjs` | Same-name shadows across the first-party clients that consume this package (terminal, desktop, web and the admin console). Each client's product sources are read at the local clone's `origin/main` (or its HEAD when there is no such ref), without fetching, and parsed with the TypeScript parser; every top-level runtime export the client declares itself is compared with this package's public runtime exports. The guard prints which ref, commit and commit date it read for each client, and warns (without failing) when that commit is more than seven days old, because the result then only describes that older snapshot. A client-side declaration carrying the name of a package export means a piece of shared logic now lives in two places and can drift apart. It fails the guard unless it is listed in `scripts/layering-shadow-exemptions.json`, and a listed row must carry one due-by version no more than three minor lines ahead (it fails once the package reaches it; that single field is the only one the guard reads for expiry, and the older retire-by and review-by fields that still sit on the rows for one more version are only checked to agree with it). It also fails once the client has removed the shadow and the row still stands. Every row also names what kind of duplicate it is, and carries the evidence that kind requires. A client copy that is this package's own function object behind a type assertion, or a thin wrapper that only passes the client's own dependencies into this package's function, must be proven so on every run by reading the client's syntax tree; such rows are due for review within two minor versions, and the guard fails as soon as the proof no longer holds. A wrapper whose extra logic the client has confirmed to be host-specific carries that confirmation (the client's own wording and where it was stated) together with a pinned review record that is due within two minor versions. A wrapper that adds its own decisions, or a same-name function that does something else, carries a review record pinned to a hash of the normalized client declaration (comments and formatting do not count); once the client's copy changes, the guard fails until it is reviewed again. A second implementation of the same logic carries a semantic diff: the client's copy is taken from the commit being read, the named declarations and the client modules they import are extracted with the TypeScript parser, transpiled and run in memory against this package's build on the same inputs, and any disagreement outside the classes registered on that row fails the guard (each class is a fixed predicate over both answers). A separate register lists same-meaning copies that carry a different name (a name-based comparison cannot see those): each row names the client declaration and the package export it duplicates, and fails when it is malformed, past its due-by version, or when the client no longer declares that name; the register only records and checks liveness and expiry, it does not scan for such copies. A row can follow a client-side rename to a new name, and can be registered ahead of a client change for a limited number of versions before the client code exists. Re-exports of this package's own exports are the intended form and never count. A client tree that is not present is reported as a skipped section, not as a pass. The ruler proves itself on an in-memory fake client (planted shadows must be caught, legal forms must not), on a throwaway repository (a missing `origin/main` falls back to HEAD, a broken one is a fault rather than a silent fallback), and refuses to report zero on a client whose scan surface is empty. |
|
|
433
437
|
| `scripts/run-session-policy-deliverable-test.mjs` | Which of a batch of user-written permission rules can be written into a session’s own rule record without changing their meaning, and why each of the others cannot. The record holds whole tool names and command names only, so exactly one class maps across losslessly: a deny rule that names one tool with no qualifier. Everything else is withheld with one word from a closed seven-word list — an ask rule (the record has no ask tier), a deny rule with a parenthesised qualifier (recording just the name could block more), a rule that names a server or agent peer without naming one of its tools (for every protocol namespace the engine knows, checked against the engine package's own table) or contains a wildcard (*) anywhere (an engine that compares exact names would block nothing), an entry that is not a tool name, and a name the engine refuses at the start of every run — a retired tool name, or one containing "__" without a protocol prefix, where the prefix check is case-sensitive (once such a name is in the record, every later run of the session fails at startup until that entry is removed; the retired-name list is checked entry by entry against the engine package's own list, and a withheld retired name carries its current name when the engine says it was renamed), and — only when the caller passes the tool roster of a run — a name that roster does not list as a tool name or alias, compared exactly with letter case counted (recorded as it is it would block nothing on that deployment) — and each word has one sentence, which never echoes the rule itself; asking for the sentence never throws, even with a value that throws when turned into a string. A name with leading or trailing whitespace counts as not a tool name: the record compares exact bytes, so it would block nothing. The guard pins the batch semantics: the deliverable part is either the whole batch or empty, never a subset, so a caller cannot send half a change and report it as saved. An end-to-end check runs the engine package itself: every batch this function would deliver — the recorded vectors and a fixed-seed sample of generated names — is written into an in-memory session rule store and the next run must get past its start-up checks and reach the model, while every name withheld as refused — every retired name included — must indeed make that run fail at start-up, and every string literal in the judgement source that it withholds as refused must be one the engine package's own tables refuse. It also checks that malformed input never throws and never delivers anything (non-arrays, non-string entries, holes, a polluted array prototype, a length or index that throws, a changing index read once), that a batch which cannot be read at all is marked `unreadable: true` while an empty batch is not, so the two stay tellable apart, that each word is produced by some vector and nothing outside the list is produced, and — when a checkout of the previous in-client implementation is present — that this function gives the same answer on every recorded vector and on tens of thousands of generated rules and pairs, except for five deliberately stricter classes (whitespace-padded names; rules with a wildcard anywhere, which the previous implementation sent as exact names unless the wildcard was the whole tool part of a server rule; peer-wide rules outside the MCP namespace, which it did not recognise; names the engine refuses at start-up, which it sent as ordinary names; and names the engine's rule compiler does not accept as one whole tool, such as an escaped parenthesis or a backslash in the name), whose disagreements are counted per class and must match an independent count exactly. The optional roster only ever withholds more: with no roster, or an absent or null one, every answer is byte-identical to the last release before the roster existed, checked against that release's published file over tens of thousands of inputs, apart from the rule-syntax class described below; a name withheld without a roster stays withheld with the same word, and the same current name, when the roster lists it as a name or an alias; a name delivered without a roster is withheld as not in the roster exactly when the roster lacks it; the engine's own namespace-covering forms keep their word; and a roster or options value that cannot be read marks the whole batch unreadable instead of falling back to no roster, while a readable empty roster is a roster. An end-to-end check takes the roster from a real run of the engine package that mounts a tool with an alias: every name delivered with that roster passes the engine's start-up name audit without being reported, and every name withheld as not in the roster is one that audit reports as matching no mounted tool. When the previous in-client implementation carries its own roster check, the two are compared under a given roster and may differ only for an alias (delivered here, withheld there) and for a refused name that the roster lists (withheld here, delivered there). A deny rule is delivered only when the installed engine's rule compiler, asked about that one string the same way the request-body permission assembly asks about whole tool names, accepts it as a rule for one whole tool: a name with an escaped parenthesis, a backslash, a lone closing parenthesis or more characters than a rule may have is withheld as not a tool name, because the record would compare those bytes and match no tool. When the compiler is not installed or cannot answer, a batch that needs it is marked unreadable rather than delivered, while batches that never reach that step answer as before. The guard checks that every delivered name is one the compiler accepts, that nothing delivered now was withheld by the earlier release and the extra withholding is exactly that class, that a roster never makes such a name deliverable, that the compiler of every engine generation in the server fixtures gives the same answers, and that the request-body assembly reaches the same verdict on the same names. |
|
|
434
|
-
| `scripts/run-plugin-hooks-projection-test.mjs` | Plugin hooks: each command hook an enabled plugin declares is decided one by one as running in the engine, running in this client, or not running at all, and the page of hooks sent with a request is built from the same per-turn plan the client uses to skip its own copies, so one hook never runs in two places. Governance is judged first and always wins — a managed hooks switch-off, an untrusted workspace, safe or bare mode, or a governance read that fails sends no plugin hook and does not list it as a gap; managed-hooks-only (set directly, or through a merged non-managed hooks switch-off) keeps only managed plugins; the plugin-only customization lock does not touch plugin hooks. A hook reaches the engine only when this client started the engine on this machine, the engine reports plugin-hook support, the entry is a command, the plugin declares no sensitive option, and the event still fits the engine's per-event limits; the gate walks that matrix cell by cell, including the limit boundaries and a session goal hook counting toward them. A fact that was never read is reported as not known rather than as a fact: a host that does not say where the engine runs gets a "not known whether this client started the engine" reason, an engine whose capabilities have not been read yet gets a "not known yet whether it supports plugin hooks" reason, and the plan's two engine facts are null in those cases, not false. Events the engine never fires run only if the client says it fires them itself, and hooks the upstream behaviour itself refuses (option references in a shell-form command, an unset option in exec form, malformed entries) run nowhere. Exec-form arguments are passed element by element with only saved non-sensitive option references filled in; path placeholders are left for the executor. Sensitive option values never reach the request: with a host that wrongly supplies one, every string in the plan, the request body, the notice, the labels and the log is searched for it across eight cells. A host without the plugin reader keeps the previous request body and gets exactly one warning per settings port; plugin data that throws while it is being read (a throwing getter, a revoked proxy) is treated like a failing reader — no plugin hooks this turn, settings hooks still sent, nothing thrown; the not-running notice names the plugin and events, never a command or an option value, and escapes control characters in names. Command hooks from settings that carry arguments (a non-empty `args` array, which is the exec form, or any other non-null value) are removed from the request until the engine reports support for arguments, because the engine would otherwise drop the arguments and run the bare command through a shell; an empty `args` array is not treated as carrying arguments when the command is made only of letters, digits and `_ . / : + -` (the shell runs the same executable), so such a guard still reaches the engine, while an empty array on a command with spaces or shell characters is removed; `args` on a prompt or http entry, a null `args`, or an entry with no type is left alone, and those go out unchanged. MCP tool hooks, which the engine cannot parse, are removed
|
|
435
|
-
| `scripts/run-display-untrusted-projection-test.mjs` | The single display-safety outlet (`displayUntrusted`) and the credential wash on the end-of-run rows this package mints. The outlet composes two credential nets (URL structure: userinfo, every query value, the fragment, path parameters and path segments that start with a known secret prefix; key/value words such as `Authorization: Bearer ...`, `Authorization: token ...` or `api_key=...`, plus well-known secret literals that appear without a label, such as `sk-...`, `ghp_...`, `AKIA...`, JWTs and the body of a PEM private key) with three character nets (control characters, bidirectional and format characters, whitespace folding). The credential nets match on a view of the text with ANSI sequences, format characters, control characters and the outlet's own escape tokens stripped, and map the result back onto the original, so colouring or an invisible character wedged between a label, its separator and its value cannot hide the value, and no stray marker is left behind. Whitespace of any length around the separator is accepted. Hosts, ports, paths, query key names and surrounding prose stay byte-for-byte, clean text comes back unchanged, the result is idempotent (also with a length cap), a length cap never splits an escape token or a surrogate pair, an invalid cap means no cap, and every net can be switched off on its own. A few narrow shapes are left alone because they name something rather than carry a value (a plain English word after `bearer` or `basic`, a back-quoted credential variable name, a plain integer after `tokens:`, a list of key names after `keys:`), each with a counter-example that is still washed. Regional flag emoji built from tag characters are kept whole. The existing single-line helpers (`escapeDisplayControlChars`, `collapseLabel`, `capForDisplay`, peer sender names and the hook failure banner) run on the same engine and, since 0.96.0, read the same character table as the outlet: each is checked equal to the outlet with both credential nets off (escape without folding, fold then escape, space mark then fold, and the same length cap) over a seeded pool of more than 600 strings; every character class the old sets left alone (zero-width characters, the word joiner, invisible operators, U+FEFF, the soft hyphen, tag characters and other format characters, plus C1 controls, bidirectional controls and lone surrogates in sender names) is marked class by class, while joiners inside emoji sequences and joining or Brahmic scripts and RGI regional flags stay as written; no character the old sets marked is ever left raw, input without the newly marked characters stays byte-identical to the previous output over every BMP code unit plus random strings, all of them are idempotent, and neither `capForDisplay` nor the hook banner's 512-character cap splits a `\u{…}` escape token or a surrogate pair. The approval decision-note echo, the subagent resume receipt (and its failure debug line) and the startup list of plugin hooks that will not run now also drop bidirectional and format characters (and, for the receipt, C1 controls); a note that is empty after cleaning is treated as absent. The synthetic end-of-run rows (`API Error:`, `Run stopped:`, `Model output error:`, `Outcome unknown:`) and the result frame's `errors[]` pass both credential nets before they leave the package, on the print lane and on the interactive lane (which also keeps the row-class flag); this covers a blocked reason whoever wrote it, while assistant text rows, a successful `result` and salvaged output are never touched, and a non-string `errors[]` entry is passed through unchanged. The known-secret-prefix check is a local copy of the configuration package's detector and is compared with the installed one entry by entry. Since 0.83.5 the outlet has two opt-in switches and a position read-out. `escapeBackslashes` (escape form only) writes every literal backslash as a pair, so each output decodes back to exactly one input (a real invisible character and its literal six-character spelling no longer look alike); a reference decoder round-trips thousands of random strings, the output is byte-identical to the default when the input has no backslash, a length cap is measured on the paired output and keeps the longest fitting prefix, credentials are washed exactly as in the default, and the switch is not idempotent by design (use it only at the final render). Zero-width joiners and non-joiners are kept only inside emoji sequences drawn as emoji (so not between symbols such as © or ™ that display as text) and between letters of scripts where they change the shaping (joining scripts such as Arabic, and the Brahmic family), each listed script checked both ways; the one other place a joiner is kept is right after a virama at the end of a word, the older spelling still found in Malayalam and Bengali text. Next to Latin, Cyrillic, CJK and other letters, next to modifier letters shared across scripts, at the start of a word, at the end of a word without a virama before it, or on their own they are now marked. `blanks` marks characters that look like a space but are not an ASCII space (no-break and other width spaces, the ideographic space, Hangul fillers, the blank Braille pattern) before whitespace folding, for names that must never look alike. `displayUntrustedMarks` returns the same text plus the position of every character mark; its text is compared with the outlet over thousands of inputs. With both credential nets off, the character face stays byte-identical to the previous release for input without joiners. The credential nets read escape sequences the way a terminal would when one is cut short: an unfinished colouring or character-set sequence interrupted by another one is dropped as a whole, a sequence never takes the `@` of an address as its final character, and a final character that starts a well-known secret literal (`sk-`, `ghp_`, `AKIA`, a JWT) is also read as the start of that literal; escape tokens this outlet writes are read as one unit, while look-alike text it never writes (an upper-case `\U`, or a code point it never marks) is read as plain text. A URL is cut before a run of non-ASCII blanks followed by a credential label or scheme word, Hangul fillers and the blank Braille pattern count as spaces around a label's separator, and a value that itself starts with a quoted label (`token= "password":"..."`) is left to that inner label. With `blanks` on, a blank written as an escape token right after a label is read as a blank when the value is judged, so `password:` followed by a no-break space and `missing` stays unmasked and an empty value gets no marker; the credential nets also read the text the way it looks after default whitespace folding and combine what each reading masks, so whatever the default form masks stays masked with `blanks` on (checked over a seeded corpus for the escape form, with paired backslashes and without folding; the exceptions are text that itself contains a literal six-character blank escape, which cannot be told apart from one the outlet wrote, and the dot and space marks, which cannot tell a marked blank from a real dot or space). A lone surrogate wedged between a label, its separator and its value no longer hides the value: the credential nets treat it exactly like a format character, in the machine-readable wash, in a single pass of the display outlet, and on the end-of-run rows and the result frame's `errors[]` on both lanes, while lone surrogates anywhere else are left byte-for-byte. Since 0.84.1 an address-shaped value (`scheme://…`) that sits right after an invisible single character — a format character, a lone surrogate, a non-whitespace control character, or the outlet's own escape token for one — is no longer let through as an address: the scheme stays and the host and path are replaced, with the query and fragment masked as before; `user:<password>@` followed only by stripped units and then a boundary, a port or another `@` is masked as userinfo. A value after real whitespace or after a colour sequence is still treated as an address, and clean text stays unchanged; forms that the previous release masked are checked not to leak on the same outlets. A `user:<password>@` candidate that is the value of a credential label or scheme word — including a label or scheme word split by stripped characters, and a quoted value that goes on past whitespace — is left to the label pass, so the whole value is masked as in the previous release; both reported shapes and frozen samples from the targeted pools are checked on every outlet. A scheme-less `user:password@host` whose password holds a double quote, backtick or angle bracket is still masked when a credential label or scheme word sits right before the user name, or when the user name is a plain word that is not itself a label, the host has a real shape and the address is not wrapped by that quote; quoted JSON values, quoted or bracketed e-mail addresses and quoted arrays stay byte-identical. When a label inside a quoted value ends right at that value's closing quote, the quote and separator stay outside the mask, and an address that begins the rest of a quoted value counts as an address only up to the next blank, so washing the output again never changes it. The credential stage now repeats its single scan on its own output until nothing changes (the cap counts rewrites, at most eight; clean text takes exactly one pass), so masking the output again never changes it for every input that settles within the cap — the random, exhaustive and grammar pools and the two known step-by-step constructions settle in at most three passes counting the confirming one, and the searched constructions kept in the pool in at most six; a deliberately constructed input can need more rewrites as it grows, and at the cap the last rewrite is returned, which a further pass can only mask more of. The checks cover the pass cap and how reaching it is reported, single-pass clean text, a grammar sample of the busiest shapes, constructions that once advanced one step per pass, the mark positions on the final output and how positions compose across rewrites. A username that is itself a credential label with a quote or angle bracket in the password is masked as userinfo (also with only the URL net on) unless the label's own value really reaches the `@`; a URL parameter such as `;token=...` or `,token=...` stays inside the URL piece; a private-key header the outlet cannot recognise does not hide the labels inside its block; the tail glued after the host of a label's address value is masked to the value's end; and a value this package's own transcript wash split at a carriage return is masked when the display joins it back. A bare `Token <value>` / `token <value>` (no label, no separator) is now washed like `Bearer`, with a wider allow-list of ordinary English words after `token` so prose such as `token count: 1234` or `token expired` stays readable; a `/token` URL path tail followed by a status code is not a scheme word and stays as is, and neither is `token` right after `unexpected` in a parser error such as `SyntaxError: Unexpected token '<'`, which stays readable. The `token` scheme word only adds masking of its own value: label values and `bearer` / `basic` values are read exactly as in the previous release, so a value that starts with another character, or a `bearer` value, followed by `token`, a non-ASCII space and an address is still masked whole. When a credential label or scheme word is followed by a colon and a YAML block scalar header (`\|` or `>`, optionally with `-`, `+` or digits, invisible characters allowed between them, and nothing else on that line but blanks, invisible characters or a comment, ending in a line break), the whole indented block becomes a single marker: the block runs until the first line with visible content that is not indented deeper than the label's line, blank lines included, and the line break before that line and the line itself stay as they were on outlets that keep line breaks (outlets that draw line breaks as escapes or dots merge the next word into the marker, as they already do after any value). Raw and JSON-escaped line breaks are both read; blanks inside escape sequences or control strings never count as indentation, and a block line holding only a control string counts as content and is masked whole. Forms where the marker is followed by more text on the same line, `=`-separated forms such as `key = \|` or the `=>` arrow, and a header at the very end of the text read exactly as in the previous release. From 0.94.0 the private-key header has one definition, generated from the settings contract: the whole-string check finds it anywhere in the text (as the contract's own write-side check does from its 10.0.0 release), and the key-block cutter builds its header line from the same definition, so a lowercase header or one without a space after BEGIN has its key body masked like the canonical form, while public keys and certificates are left as they were. Since 0.94.0 the credential words also read through YAML node properties (type tags such as `!!str` and anchors such as `&name`) between the colon and the value: the properties and the value after them get one marker each, block scalars and block-sequence items included, and a property with no value after it on the same line reads exactly as before. A list after a credential label (a block sequence, a flow sequence including JSON arrays, or a same-line comma list after a plural label) is judged item by item with the same exemptions as a single value: a list of plain key names after a bare `keys`, or of integers after a bare `tokens`, is left alone, and any value-like item masks every non-neutral item. A dash right after the colon on the same line is a list bullet, not a value. The retry overlay's error text (`error.formatted` on the four error arms) passes through the same outlet, and the label vocabulary is pinned as it stands (members masked, known out-of-family keys such as `passphrase` left as written). From 0.95.0 the credential words also read a value wrapped in round or square brackets after a scheme word or label (placeholder-shaped contents such as `(API_KEY)` or `(your-token)` and bracketed prose stay as written), a JSON-escaped tab between a scheme word or label and its value, a same-line associative-array arrow with a quoted label and a quoted value, and a `token` scheme word with its value after a credential label as one value; a word with no label at all is masked when it is shaped like a random token (length, three character classes, entropy and run structure), while hex hashes, UUID lists, base64 image headers and data URIs, integrity strings, provider object ids and ordinary identifiers stay as written; a known secret prefix glued after an underscore or hyphen inside an identifier or a URL path segment is masked from the prefix on; and a masked value no longer swallows a closing backquote at its end, or a closing round or square bracket at the end of a URL that has no matching opener, while a backquote inside a value never cuts it short; an address after `Bearer` or `Basic` that is itself the value of a credential label (including `Authorization:`) is masked whole, keeping the scheme word, while an address with no label stays as written. Credential masking also covers three shapes that used to print as written: a name glued onto a credential word (PGPASSWORD=, CUSTOMSECRET=, dbPassword:, secretAccessKey=; singular only, so token-count names such as maxTokens or inputTokens and English words such as monkey stay visible), Bearer or Basic glued to a preceding word character (_Bearer, xBearer), and a known secret prefix followed by one unbroken run of six or more letters and digits (sk-abcdefghijk). The new readings only add masks: a value that was already masked whole stays masked whole when it contains one of these shapes. The configured-address display and the upload check read the same label rule, so a glued name used as a query key counts as a credential in both. A frozen copy of a pattern net that a downstream host used to run locally is replayed against the output as a no-regression check. A length cap never splits a redaction marker: the cut backs off to before it on every capped path (plain, position-tracking, paired backslashes, and the shared cap of the field-escaping helper); strings that are not complete markers are cut as before. |
|
|
438
|
+
| `scripts/run-plugin-hooks-projection-test.mjs` | Plugin hooks: each command hook an enabled plugin declares is decided one by one as running in the engine, running in this client, or not running at all, and the page of hooks sent with a request is built from the same per-turn plan the client uses to skip its own copies, so one hook never runs in two places. Governance is judged first and always wins — a managed hooks switch-off, an untrusted workspace, safe or bare mode, or a governance read that fails sends no plugin hook and does not list it as a gap; managed-hooks-only (set directly, or through a merged non-managed hooks switch-off) keeps only managed plugins; the plugin-only customization lock does not touch plugin hooks. A hook reaches the engine only when this client started the engine on this machine, the engine reports plugin-hook support, the entry is a command, the plugin declares no sensitive option, and the event still fits the engine's per-event limits; the gate walks that matrix cell by cell, including the limit boundaries and a session goal hook counting toward them. A fact that was never read is reported as not known rather than as a fact: a host that does not say where the engine runs gets a "not known whether this client started the engine" reason, an engine whose capabilities have not been read yet gets a "not known yet whether it supports plugin hooks" reason, and the plan's two engine facts are null in those cases, not false. Events the engine never fires run only if the client says it fires them itself, and hooks the upstream behaviour itself refuses (option references in a shell-form command, an unset option in exec form, malformed entries) run nowhere. Exec-form arguments are passed element by element with only saved non-sensitive option references filled in; path placeholders are left for the executor. Sensitive option values never reach the request: with a host that wrongly supplies one, every string in the plan, the request body, the notice, the labels and the log is searched for it across eight cells. A host without the plugin reader keeps the previous request body and gets exactly one warning per settings port; plugin data that throws while it is being read (a throwing getter, a revoked proxy) is treated like a failing reader — no plugin hooks this turn, settings hooks still sent, nothing thrown; the not-running notice names the plugin and events, never a command or an option value, and escapes control characters in names. Command hooks from settings that carry arguments (a non-empty `args` array, which is the exec form, or any other non-null value) are removed from the request until the engine reports support for arguments, because the engine would otherwise drop the arguments and run the bare command through a shell; an empty `args` array is not treated as carrying arguments when the command is made only of letters, digits and `_ . / : + -` (the shell runs the same executable), so such a guard still reaches the engine, while an empty array on a command with spaces or shell characters is removed; `args` on a prompt or http entry, a null `args`, or an entry with no type is left alone, and those go out unchanged. MCP tool hooks, which the engine cannot parse, are removed from every request: a plan records each one and its not-running notice lists them — the gate checks request bodies against the engine's own hooks schema, and the raw settings against it as a positive control. Malformed entries still pass through for the engine to reject loudly. Since 0.98.0 the page of hooks is built only from a plan: `hooksForWire` takes `{ plan }` and throws a TypeError for anything else (nothing passed, null, a value that is not a plan, a near-plan, a plan passed directly, an options object whose `plan` cannot be read, or a host value that throws while it is checked), naming the right call and never echoing the value or logging what the host value threw; the gate walks those shapes one by one and checks the built output for leftovers of the retired path. A `plugin` key written by hand on a settings hook is stripped before sending (even when its value is undefined), because only hooks that come from the plugin reader may carry plugin context; the settings document itself is left untouched and a debug line records the count. The two hand-copied tables, the engine-fired event list and the engine limits, are checked against their owners. Since 0.89.0 the same per-turn decision also gives every settings hook on an engine-fired event exactly one executor: a command, HTTP, prompt or agent hook from settings goes to the engine when the engine reports that it runs hooks sent with requests and either this client started the engine on this machine or this client cannot run that hook itself; otherwise this client runs it and the hook is left out of the request. A hook the engine is known to skip (a PowerShell command, or hooks the engine ignores entirely) that this client cannot run either is listed as not running; one the engine may or may not reach (capabilities not read yet, or a prompt or agent hook after the fourth one on an event) is still sent and not listed. The client learns which of its own settings hooks to skip from `settingsHooksKeptLocally`, which matches hooks by content against the request body only, returns the same array when nothing is skipped, and for the two compaction events follows whether the engine actually compacted. When the prompt was not written by the user, prompt-submit hooks from settings and plugins are left out on both sides. A host that reports events must also report which hook types its own executor runs on each event: with that table, a plugin hook it cannot run is reported as not running rather than as running in this client; without it, prompt and agent hooks from settings on engine events are treated as hooks the client cannot run and stay in the request for the engine. A settings hook whose required field is unusable (a command, URL or prompt that is not a non-empty string) is not assigned to either side and is sent unchanged, so the service still rejects it; malformed rows in a plan handed back by the host never make the not-running notice throw and never authorise skipping a hook; and a missing, empty or `*` matcher counts as the same match-all matcher when the client decides what to skip. A matrix over source, event, hook type, capability, engine ownership, host shape, prompt origin and compaction outcome checks that every hook runs at most once and exactly once whenever either side can run it, while the print lane and hosts without a local executor keep the previous request byte for byte. An optional leg runs the request body through an installed server's hook runner, current and previous release, together with the client-side skip, and counts one run per prompt. A plan in the previous release's six-key shape is read the same way by the request side and the skip side, so every hook it sends runs once; the client side of the skip reads groups and entries the way the client's executor does (class instances and inherited getters included); and on the two compaction events a prompt or agent hook past the engine's per-event limit runs in the client when the client can run it. |
|
|
439
|
+
| `scripts/run-display-untrusted-projection-test.mjs` | The single display-safety outlet (`displayUntrusted`) and the credential wash on the end-of-run rows this package mints. The outlet composes two credential nets (URL structure: userinfo, every query value, the fragment, path parameters and path segments that start with a known secret prefix; key/value words such as `Authorization: Bearer ...`, `Authorization: token ...` or `api_key=...`, plus well-known secret literals that appear without a label, such as `sk-...`, `ghp_...`, `AKIA...`, JWTs and the body of a PEM private key) with three character nets (control characters, bidirectional and format characters, whitespace folding). The credential nets match on a view of the text with ANSI sequences, format characters, control characters and the outlet's own escape tokens stripped, and map the result back onto the original, so colouring or an invisible character wedged between a label, its separator and its value cannot hide the value, and no stray marker is left behind. Whitespace of any length around the separator is accepted. Hosts, ports, paths, query key names and surrounding prose stay byte-for-byte, clean text comes back unchanged, the result is idempotent (also with a length cap), a length cap never splits an escape token or a surrogate pair, an invalid cap means no cap, and every net can be switched off on its own. A few narrow shapes are left alone because they name something rather than carry a value (a plain English word after `bearer` or `basic`, a back-quoted credential variable name, a plain integer after `tokens:`, a list of key names after `keys:`), each with a counter-example that is still washed. Regional flag emoji built from tag characters are kept whole. The existing single-line helpers (`escapeDisplayControlChars`, `collapseLabel`, `capForDisplay`, peer sender names and the hook failure banner) run on the same engine and, since 0.96.0, read the same character table as the outlet: each is checked equal to the outlet with both credential nets off (escape without folding, fold then escape, space mark then fold, and the same length cap) over a seeded pool of more than 600 strings; every character class the old sets left alone (zero-width characters, the word joiner, invisible operators, U+FEFF, the soft hyphen, tag characters and other format characters, plus C1 controls, bidirectional controls and lone surrogates in sender names) is marked class by class, while joiners inside emoji sequences and joining or Brahmic scripts and RGI regional flags stay as written; no character the old sets marked is ever left raw, input without the newly marked characters stays byte-identical to the previous output over every BMP code unit plus random strings, all of them are idempotent, and neither `capForDisplay` nor the hook banner's 512-character cap splits a `\u{…}` escape token or a surrogate pair. The approval decision-note echo, the subagent resume receipt (and its failure debug line) and the startup list of plugin hooks that will not run now also drop bidirectional and format characters (and, for the receipt, C1 controls); a note that is empty after cleaning is treated as absent. The synthetic end-of-run rows (`API Error:`, `Run stopped:`, `Model output error:`, `Outcome unknown:`) and the result frame's `errors[]` pass both credential nets before they leave the package, on the print lane and on the interactive lane (which also keeps the row-class flag); this covers a blocked reason whoever wrote it, while assistant text rows, a successful `result` and salvaged output are never touched, and a non-string `errors[]` entry is passed through unchanged. The known-secret-prefix check is a local copy of the configuration package's detector and is compared with the installed one entry by entry. Since 0.83.5 the outlet has two opt-in switches and a position read-out. `escapeBackslashes` (escape form only) writes every literal backslash as a pair, so each output decodes back to exactly one input (a real invisible character and its literal six-character spelling no longer look alike); a reference decoder round-trips thousands of random strings, the output is byte-identical to the default when the input has no backslash, a length cap is measured on the paired output and keeps the longest fitting prefix, credentials are washed exactly as in the default, and the switch is not idempotent by design (use it only at the final render). Zero-width joiners and non-joiners are kept only inside emoji sequences drawn as emoji (so not between symbols such as © or ™ that display as text) and between letters of scripts where they change the shaping (joining scripts such as Arabic, and the Brahmic family), each listed script checked both ways; the one other place a joiner is kept is right after a virama at the end of a word, the older spelling still found in Malayalam and Bengali text. Next to Latin, Cyrillic, CJK and other letters, next to modifier letters shared across scripts, at the start of a word, at the end of a word without a virama before it, or on their own they are now marked. `blanks` marks characters that look like a space but are not an ASCII space (no-break and other width spaces, the ideographic space, Hangul fillers, the blank Braille pattern) before whitespace folding, for names that must never look alike. `displayUntrustedMarks` returns the same text plus the position of every character mark; its text is compared with the outlet over thousands of inputs. With both credential nets off, the character face stays byte-identical to the previous release for input without joiners. The credential nets read escape sequences the way a terminal would when one is cut short: an unfinished colouring or character-set sequence interrupted by another one is dropped as a whole, a sequence never takes the `@` of an address as its final character, and a final character that starts a well-known secret literal (`sk-`, `ghp_`, `AKIA`, a JWT) is also read as the start of that literal; escape tokens this outlet writes are read as one unit, while look-alike text it never writes (an upper-case `\U`, or a code point it never marks) is read as plain text. A URL is cut before a run of non-ASCII blanks followed by a credential label or scheme word, Hangul fillers and the blank Braille pattern count as spaces around a label's separator, and a value that itself starts with a quoted label (`token= "password":"..."`) is left to that inner label. With `blanks` on, a blank written as an escape token right after a label is read as a blank when the value is judged, so `password:` followed by a no-break space and `missing` stays unmasked and an empty value gets no marker; the credential nets also read the text the way it looks after default whitespace folding and combine what each reading masks, so whatever the default form masks stays masked with `blanks` on (checked over a seeded corpus for the escape form, with paired backslashes and without folding; the exceptions are text that itself contains a literal six-character blank escape, which cannot be told apart from one the outlet wrote, and the dot and space marks, which cannot tell a marked blank from a real dot or space). A lone surrogate wedged between a label, its separator and its value no longer hides the value: the credential nets treat it exactly like a format character, in the machine-readable wash, in a single pass of the display outlet, and on the end-of-run rows and the result frame's `errors[]` on both lanes, while lone surrogates anywhere else are left byte-for-byte. Since 0.84.1 an address-shaped value (`scheme://…`) that sits right after an invisible single character — a format character, a lone surrogate, a non-whitespace control character, or the outlet's own escape token for one — is no longer let through as an address: the scheme stays and the host and path are replaced, with the query and fragment masked as before; `user:<password>@` followed only by stripped units and then a boundary, a port or another `@` is masked as userinfo. A value after real whitespace or after a colour sequence is still treated as an address, and clean text stays unchanged; forms that the previous release masked are checked not to leak on the same outlets. A `user:<password>@` candidate that is the value of a credential label or scheme word — including a label or scheme word split by stripped characters, and a quoted value that goes on past whitespace — is left to the label pass, so the whole value is masked as in the previous release; both reported shapes and frozen samples from the targeted pools are checked on every outlet. A scheme-less `user:password@host` whose password holds a double quote, backtick or angle bracket is still masked when a credential label or scheme word sits right before the user name, or when the user name is a plain word that is not itself a label, the host has a real shape and the address is not wrapped by that quote; quoted JSON values, quoted or bracketed e-mail addresses and quoted arrays stay byte-identical. When a label inside a quoted value ends right at that value's closing quote, the quote and separator stay outside the mask, and an address that begins the rest of a quoted value counts as an address only up to the next blank, so washing the output again never changes it. The credential stage now repeats its single scan on its own output until nothing changes (the cap counts rewrites, at most eight; clean text takes exactly one pass), so masking the output again never changes it for every input that settles within the cap — the random, exhaustive and grammar pools and the two known step-by-step constructions settle in at most three passes counting the confirming one, and the searched constructions kept in the pool in at most six; a deliberately constructed input can need more rewrites as it grows, and at the cap the last rewrite is returned, which a further pass can only mask more of. The checks cover the pass cap and how reaching it is reported, single-pass clean text, a grammar sample of the busiest shapes, constructions that once advanced one step per pass, the mark positions on the final output and how positions compose across rewrites. A username that is itself a credential label with a quote or angle bracket in the password is masked as userinfo (also with only the URL net on) unless the label's own value really reaches the `@`; a URL parameter such as `;token=...` or `,token=...` stays inside the URL piece; a private-key header the outlet cannot recognise does not hide the labels inside its block; the tail glued after the host of a label's address value is masked to the value's end; and a value this package's own transcript wash split at a carriage return is masked when the display joins it back. A bare `Token <value>` / `token <value>` (no label, no separator) is now washed like `Bearer`, with a wider allow-list of ordinary English words after `token` so prose such as `token count: 1234` or `token expired` stays readable; a `/token` URL path tail followed by a status code is not a scheme word and stays as is, and neither is `token` right after `unexpected` in a parser error such as `SyntaxError: Unexpected token '<'`, which stays readable. The `token` scheme word only adds masking of its own value: label values and `bearer` / `basic` values are read exactly as in the previous release, so a value that starts with another character, or a `bearer` value, followed by `token`, a non-ASCII space and an address is still masked whole. When a credential label or scheme word is followed by a colon and a YAML block scalar header (`\|` or `>`, optionally with `-`, `+` or digits, invisible characters allowed between them, and nothing else on that line but blanks, invisible characters or a comment, ending in a line break), the whole indented block becomes a single marker: the block runs until the first line with visible content that is not indented deeper than the label's line, blank lines included, and the line break before that line and the line itself stay as they were on outlets that keep line breaks (outlets that draw line breaks as escapes or dots merge the next word into the marker, as they already do after any value). Raw and JSON-escaped line breaks are both read; blanks inside escape sequences or control strings never count as indentation, and a block line holding only a control string counts as content and is masked whole. Forms where the marker is followed by more text on the same line, `=`-separated forms such as `key = \|` or the `=>` arrow, and a header at the very end of the text read exactly as in the previous release. From 0.94.0 the private-key header has one definition, generated from the settings contract: the whole-string check finds it anywhere in the text (as the contract's own write-side check does from its 10.0.0 release), and the key-block cutter builds its header line from the same definition, so a lowercase header or one without a space after BEGIN has its key body masked like the canonical form, while public keys and certificates are left as they were. Since 0.94.0 the credential words also read through YAML node properties (type tags such as `!!str` and anchors such as `&name`) between the colon and the value: the properties and the value after them get one marker each, block scalars and block-sequence items included, and a property with no value after it on the same line reads exactly as before. A list after a credential label (a block sequence, a flow sequence including JSON arrays, or a same-line comma list after a plural label) is judged item by item with the same exemptions as a single value: a list of plain key names after a bare `keys`, or of integers after a bare `tokens`, is left alone, and any value-like item masks every non-neutral item. A dash right after the colon on the same line is a list bullet, not a value. The retry overlay's error text (`error.formatted` on the four error arms) passes through the same outlet, and the label vocabulary is pinned as it stands (members masked, known out-of-family keys such as `passphrase` left as written). From 0.95.0 the credential words also read a value wrapped in round or square brackets after a scheme word or label (placeholder-shaped contents such as `(API_KEY)` or `(your-token)` and bracketed prose stay as written), a JSON-escaped tab between a scheme word or label and its value, a same-line associative-array arrow with a quoted label and a quoted value, and a `token` scheme word with its value after a credential label as one value; a word with no label at all is masked when it is shaped like a random token (length, three character classes, entropy and run structure), while hex hashes, UUID lists, base64 image headers and data URIs, integrity strings, provider object ids and ordinary identifiers stay as written; a known secret prefix glued after an underscore or hyphen inside an identifier or a URL path segment is masked from the prefix on; and a masked value no longer swallows a closing backquote at its end, or a closing round or square bracket at the end of a URL that has no matching opener, while a backquote inside a value never cuts it short; an address after `Bearer` or `Basic` that is itself the value of a credential label (including `Authorization:`) is masked whole, keeping the scheme word, while an address with no label stays as written. Credential masking also covers three shapes that used to print as written: a name glued onto a credential word (PGPASSWORD=, CUSTOMSECRET=, dbPassword:, secretAccessKey=; singular only, so token-count names such as maxTokens or inputTokens and English words such as monkey stay visible), Bearer or Basic glued to a preceding word character (_Bearer, xBearer), and a known secret prefix followed by one unbroken run of six or more letters and digits (sk-abcdefghijk). The new readings only add masks: a value that was already masked whole stays masked whole when it contains one of these shapes. The configured-address display and the upload check read the same label rule, so a glued name used as a query key counts as a credential in both. A frozen copy of a pattern net that a downstream host used to run locally is replayed against the output as a no-regression check. A length cap never splits a redaction marker: the cut backs off to before it on every capped path (plain, position-tracking, paired backslashes, and the shared cap of the field-escaping helper); strings that are not complete markers are cut as before. Since 0.98.0 a value glued to a semicolon, a value wrapped in braces or angle brackets (template placeholders stay readable), a bare `Basic` / `Bearer` assigned to an environment-style credential name, and an ordinary word right after `Bearer` / `Basic` in a credential header are masked on every outlet, under a 65 KB linear-time bound; the diagnostic-word predicate answers from the same word list the outlet uses. |
|
|
436
440
|
| `scripts/run-engine-agent-absence-projection-test.mjs` | Absent background agents: when the engine stops reporting a background agent and no final state has arrived, the row is marked absent and this package owns every decision about it, so all clients agree. One predicate says whether a row is absent (the mark, not the status, decides). An absent row keeps its last known status, never counts as running, and is never counted as completed, failed or stopped; its elapsed time stops at the last moment it was seen, and its sentence says it may still be running. The end-of-turn sweep never settles an absent row (or a resident one). A row that comes back, or a real final state for the current cycle, clears the mark; a late final state from an earlier cycle does not. Absent rows are never removed at the short grace window. After the hard limit (30 minutes from the last time they were seen) the host is asked for the background-agent registry reading of each row: only a reading that the agent has ended or is not listed lets the row go, and each removal is returned as a fact the host must act on and announce; a reading of running, unknown, missing or unrecognised keeps the row and schedules nothing, so no standing poll is created. Until a registry reading is available every absent row stays. A row someone is viewing is held and reported separately only once the registry confirms it is gone. The row sentence, the removal sentence and the late-result sentence come from one place, never state an outcome or that the agent finished, and escape control characters in names, in the engine's removal word and in the late-result status. An end-to-end cell drives the real fleet projection and the real absence channel through every decision. From 0.84.0 the package also reads the registry itself: a reader lists the session's background registry only when the server advertises the listing, classifies its failures as unavailable (a failed read keeps the HTTP status and error code when the error carries them, and never invents either), and refuses a partly readable listing as a whole; a classifier turns the listing into the per-row reading, treating a registry status it cannot place as unknown and reading "not listed" only for keys shaped like registry handles and only when the host states the engine is a single process and names the row's session, because a key from another identity space proves nothing by being absent and, on a multi-replica deployment, a listing answered by another replica does not contain this session's agents at all (without that statement a missing row reads unknown and the row stays); and a predicate returns the agents the registry still counts as live that the host has no row for. A listing at the server's 500-row cap cannot show that an agent is gone either, so a missing row there reads unknown; the cap is checked against the engine's own list clamp and, when a server build is supplied, against the server's route. Each listing carries its session and a per-client sequence number, and a listing superseded by a later delivered read, or read for another session than the one the host names, counts for nothing. Only the exact listing object the reader returned can authorize removing a row or adding one: a spread copy, a structured clone, a filtered or a hand-built listing reads unknown and adds nothing, the listing, its row array and every row are frozen, the cap check uses the row count recorded when the page was read, and rewriting a listing's sequence number fools nothing. The fill predicate holds back agents first seen within the absence settle window — timed on one monotonic local clock from when this client's reader first saw the id, so a skew between the server's clock and this one, or reconnecting to a long-running agent, cannot skip the window — agents without a readable registration time and agents the host saw end since the read went out; the registry key of a row, progress or absence event is whichever of its two ids is shaped like a registry handle; and none of the reader, the classifier or the predicate throws. |
|
|
437
441
|
| `scripts/run-plan-review-dismissal-test.mjs` | An automatic reopen does not put back a plan-review card the user closed (the first-presentation path neither checks nor clears that record, so a replayed park frame still presents its card). A plan-review card the user dismissed (Esc, abort, or any answer that is not approve or reject) is recorded per session and run at the moment of dismissal, synchronously, before anything queued behind the card can be released; a reopen marked `trigger: 'automatic'` then refuses with `{ reopened: false, dismissedByUser: true }` instead of minting a new card the user's next keystroke would land on, while the user's own next action (`trigger: 'user'`) reopens it and clears the record. The record is keyed by gate instance when the host supplies an instance reader: a new plan gate on the same run is still surfaced, and anything that cannot prove the gate is new (no reader, a failed, empty, thrown or timed-out read) refuses on the conservative side. The asynchronous form re-checks after its reads and before minting — a decision handed over meanwhile (seen by the package, or reported by the host's optional hand-over predicate) answers as "your answer is on its way"; a record that changed meanwhile makes the stale evaluation mint nothing and answer from the current record: another close refuses as the user's close and keeps the newer record, a card already back on screen (the user's own action or a concurrent automatic reopen, waited for within the receipt window and re-read once the wait is over) answers `reopened: true`, and a record that is gone (session change, ledger overflow) answers a plain refusal without `dismissedByUser`; a hand-over predicate that throws refuses with a plain `{ reopened: false }`. Each run has at most one reopen on its way: a reopen that arrives while an earlier one's card is published but not yet settled joins it instead of minting a second card and retiring the first card's answer path. Instance readers are snapshotted when they resolve, so a host that hands over its own live set still gets a new gate recognised; and a decisive-looking host answer note does not clear the record for the very card the package's own responder already judged non-decisive (the label was not on that card). A successful reopen replaces only the record taken before the card was minted (with an on-screen marker, not a deletion), a decisive answer clears it, the per-session ledger is bounded, and a session change clears its bucket. Without `trigger` the reopen answers exactly as before, apart from joining a reopen already on its way. A reopen for a session with no view subscribed to question frames (`onQuestionFrameFor(sessionKey, handler)`; `onQuestionFrame` for the default session) refuses with `{ reopened: false, _sema_noPresentationSurface: true }` — in the synchronous and the receipt form alike, per session key, and also when the view goes away while the asynchronous check is reading — and that answer comes before the user-closed check, at the entry and after the asynchronous read alike, whether or not the read proved a newer gate (after the read, a decision handed over while the read was in progress, and a dismissal record that was replaced or cleared meanwhile, still take precedence over the missing view); a host log or probe port that throws changes none of the reopen answers (the suite runs the same arrangement with a quiet and with a throwing log port and requires the same answer and the same card frames); every other refusal (receipt window lapsed, decision in flight, user-closed card, non-default slot without a delivery path, record gone) keeps its own keys without the flag. The first-presentation call does not check for a view: without one it still answers `true` and the card frame goes nowhere, which the suite records as today's behaviour, not as a guarantee. A free-text answer on the plan card (no option picked, a non-blank note) counts as a decisive answer: it clears the record and delivers exactly one rejection; a blank note, or a contradictory multi-select that carries a note, does not. |
|
|
438
442
|
| `scripts/run-gate-interrupt-safety-test.mjs` | The two safety properties of running the suites themselves. The collecting runner no longer kills a suite outright when its time limit expires: it sends a termination signal first, waits a grace period for the suite to clean up, and only then kills it — and it records the suite as timed out however it exits, so a suite that exits cleanly after the signal is still not counted as passing. The limit and the grace period can be widened for a single suite in `gates-manifest.json` (an optional entry with a written reason; a malformed entry, including one with a misspelled field, stops the runner before any suite starts instead of silently falling back to the default, and an entry filed under a misspelled name is reported by this guard rather than ignored), and the negative-control suite is widened there. Every property is exercised on byte-for-byte copies of the real runner and the real negative-control suite in a throwaway directory: a suite that honours the signal finishes within the grace period, one that ignores it is killed when the grace period ends, and the summary lines are unchanged; once a suite has exited the runner waits at most a short drain window for its output pipes, so a child process that inherited them and outlives the suite neither turns a passing suite into a timeout nor holds the runner past the limit, the grace period and that window; the negative-control suite, interrupted in the middle of a rehearsal by any of the three signals or by the runner's own time limit, restores the file byte-for-byte, leaves no backup behind, stops the rehearsed guard together with anything it started, regenerates the build output (checked on a small project in the throwaway directory), and exits with 128 plus the signal number; a rebuild that would overrun the grace period is cut short and its compiler stopped; a backup left behind by an earlier run — next to a later target, loose in the source tree, or inside a symlinked dependency directory — makes it refuse to start without touching anything, naming the file and how to restore it. |
|
|
@@ -452,7 +456,7 @@ guard still cross-checks the table by name).
|
|
|
452
456
|
| `scripts/run-session-integrity-refusal-test.mjs` | Three refusals a session can meet on newer servers, read the same way on every client. When a session's saved history cannot be read, the server answers with one code whether the refusal arrives as an HTTP error, as the failure recorded at the end of a streamed run, or on the background run's record; one predicate decides it for all three carriers, it keys on the code alone — never on the status or on the engine's wording — and one sentence explains it: the history is damaged, retrying will not help, whoever operates the engine has to repair it, and meanwhile a new session works. The package's own end-of-run error line and the error of the result frame both carry that sentence, identically, and nothing changes for any other code. When a delete is refused because background agents the session started are still running somewhere this server cannot stop them, the refusal is read with the ids it names — each id checked on its own, a bad entry dropped rather than invented, and a list with no usable entry reported as absent rather than as an empty list, which would read as "no agents left"; a refusal because a run is still in progress is read with that run's id, and any other conflict falls back to a sentence that does not guess. When a decision reaches an approval that moved in the meantime (stopped, decided elsewhere, parked again, claimed by another decision, or — newly — belonging to a session that was deleted), the package reads the code rather than the error class, says the decision was not applied and that the pending approvals should be fetched again, and the approval legs treat it as "the approval is no longer where it was" and keep reading the run, instead of letting the wording of the server's sentence decide. When the server package is supplied, the guard drives the server's own code for each of these answers and pushes the result through the SDK's error mapping before the package reads it. |
|
|
453
457
|
| `scripts/run-mode-release-projection-test.mjs` | The permission-mode fields on a tool call's gate record. Newer engines record, on an allowed call, that a permission mode answered a question nobody was asked (which mode, which question class, the whole class set, and the origin and mandate words the card would have shown), and on a refused call that the session's permission mode itself refused it (which mode and which question class). The package's gate reader carries both into its view — the allowed-call account whole or not at all, the refusal fields one by one, unfamiliar mode or class words passed through as written — and exposes a named reader for the allowed-call account. Absence is not a negative: an older engine, or a server that does not forward these fields, produces a record without them, so the reader answers `undefined` rather than claiming no mode was involved; when a server package is supplied, the guard runs the same records through that server's own projection to show exactly that. The fixtures are first passed through the engine's own record screen, the view's fields are checked against the engine's declared shapes in both directions, the fields survive the projection onto the transcript arm unchanged, and a mode refusal is filed in the transcript as a permission-rule denial. From 0.88.0 the release reason word has a closed-set mirror: the public word list is the build-time generated table taken from the engine package's entry point (the same object, frozen, equal to the engine's list word for word), the membership predicate answers exactly as the engine's own predicate on members, unknown words, case and whitespace variants, empty strings, non-strings and prototype keys, and the reader still passes an unfamiliar reason through unchanged; a release minted by the installed engine's own mode arbitration reads back field for field with its reason recognised. From 0.96.0 the release reason also has one user-facing sentence per known word (an unknown word gets a generic sentence that does not repeat it, and an absent, non-string or empty reason gets none), and a per-session ledger answers whether this call's disclosure has already been shown (read-only `has`, monotonic `mark` after the line is really on screen; a call is identified by its tool-call id scoped to the session generation, else by its card key, and with neither the answer is always 'not shown'; bounded, first-in-first-out, no module-level state). |
|
|
454
458
|
| `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. |
|
|
455
|
-
| `scripts/run-rules-legacy-tool-name-projection-test.mjs` | The session
|
|
459
|
+
| `scripts/run-rules-legacy-tool-name-projection-test.mjs` | The retired session-rule write refusal for tool names, pinned as retired. Servers before 7.129.0 refused a session rule write naming a retired tool or a name with "__" but no protocol prefix with a dedicated code; from 7.129.0 that code is gone and the same names are refused with the inert-entry code, which this package reads as its own named outcome. With the support floor at 7.129.1 the old code is no longer read: its constant is gone from the root entry, and when an older server still sends it the failure classifier answers the generic answered-refusal arm (no list, name, verdict or current name is carried) for any 4xx status, while other statuses follow the usual source rules; the family prefix test still recognises the code. Against a model of the old refusing write gate, the tightening flow answers unknown with a refused write, writes exactly once and leaves the stored record untouched, and the removal verb answers a refused write; the before-write withholding of such names is unchanged. The run-startup refusal sentence is still given for the startup code only, the old write sentence is no longer given for either the old code or a hand-made old outcome, and the cross-copy body-slot read is pinned on the live inert-entry arm instead. |
|
|
456
460
|
| `scripts/run-store-center-wiring-capability-test.mjs` | The two cloud-mode capability positions (engine ≥7.106.0), read the same four-state way as the other capability readers. The store posture (`capabilities.store`: declared backend, assembled backend, whether it is durable, an optional degradation reason, and the SQL engine seat) is always present on a current engine, so an absent key means an older engine and is never read as “not durable”; words are carried as reported rather than folded into known ones, the SQL seat is judged by the very same rule as the SQL-posture reader, and one malformed seat discards the whole reading. The center wiring (`capabilities.centerWiring`) is present only when the engine is connected to a configuration center: with no quota lease configured its three lease-dependent keys are legitimately absent, and an absent key is split by the same response — a current engine (it carries the store posture) is reported as not connected, an older one as not reported. Two predicates live in the package (does this replica persist; is the caller's budget lease-enforced — known words matched exactly, everything else unknown), and each reader has one table of doctor sentences in which no two states read alike, the older-engine sentence never claims “not durable” or “not connected”, the two failure postures cannot be said the wrong way round, and unrecognised words are shown escaped and bounded. Both are checked against the engine's own projections: every form they mint reads back exactly, every word in their vocabularies lands on a known arm, and the member lists the engine declares for these two positions equal the reading's own keys. |
|
|
457
461
|
| `scripts/run-inherit-env-wire-test.mjs` | The request field that lets the model-driven shell inherit this machine's whole environment (`"all"`) or keep the default scrubbed environment (`"scrub"`). Both words go out exactly as given on the two user lanes, an absent value is never filled in, and any other value (including a list of variable names) is refused before anything is sent. The value reaches a request only through a helper that checks the engine's version: a server whose version cannot be read gets nothing, and the caller receives the reason plus one sentence saying what the run's shell gets instead, including that leaving the field out also clears an earlier whole-environment choice on engines that know it; from 0.92.0 a readable version always gets the field (a server below the support floor refuses the request loudly rather than being skipped quietly). An invalid-field refusal of a request that asked for `"all"` is read by code with one conditionally worded sentence, because the same code has other causes. |
|
|
458
462
|
| `scripts/run-submit-refusal-projection-test.mjs` | Four kinds of refusal that mean the request itself needs fixing, read by code with one sentence each. Permission rule lists that are too long or hold a bad entry are named precisely (which list, how many entries against the limit, or which entry by position) for a new request and, with different wording, when parked work cannot continue with the request stored for it; the same sentence is appended to a decision that fails for that reason. A tool declaration the engine refuses at the start of a run is read by its code rather than its HTTP status, so a status that looks like a temporary outage is not presented as one: the sentence says the same request will be refused the same way. A deployment refusing a single-user-only setting is attributed from the request the caller sent (the whole-environment shell setting, bypassPermissions, both, or unknown) and always says nothing ran and the local settings are not at fault. A body-shape refusal exposes its two lists of unrecognized and unsupported keys exactly as sent (only well-formed lists are read; a missing list is not treated as empty), with a three-way check of whether any listed key is a settings path. |
|
|
@@ -464,9 +468,9 @@ guard still cross-checks the table by name).
|
|
|
464
468
|
| `scripts/run-prompt-delivery-projection-test.mjs` | Whether a user message actually reached the engine, judged once for every client. A ledger records, per message id and per submitted batch, that a submission entered the submit path and whether the engine created a run for it: a stream frame from the new run counts, a later accepted steer into the running run counts, while the session-busy refusal does not, and neither do the frames a server writes before creating the run (the stream-head `meta` frame and replays of approval cards still pending from earlier runs), which arrive ahead of that refusal. Both sets only grow, so the answers do not depend on arrival order, replays or repeats; a message never seen on the submit path is reported as unknown rather than undelivered. Two row-level readers filter side-channel payloads (only messages known to be undelivered are removed, everything else is kept in order). Records serialize one per line in the shape hosts already persist. The check also replays the exact frames an installed server package writes for a busy session through the real SDK stream parser when one is available and reports that part as skipped otherwise. The idempotent-replay form of the busy-session refusal (the raw conflict body without a status, sent when a host reuses an idempotency key while the first submission is still in flight) does not count as delivered either. Since 0.90.0 the check also feeds the delivery frames a steer now produces (a queued frame and an applied frame on one input id, which equals the message id on the receipt): the first records delivery, the second changes nothing, replays and reversed order give the same records, and the answer does not depend on whether the input id equals the message id, differs from it, or is absent. The ledger object itself also carries the two row-level readers as methods bound to it, `knownUndeliveredMessage(message)` and `withoutUndelivered(messages)`, which answer exactly what the two free functions answer for that ledger on every input shape: a bare string is not a message row, so it is judged false, and that is why the row-level method does not reuse the name of the id-keyed `knownUndelivered(uuid)`. They keep answering when taken off the ledger, when the ledger is copied, and after it is frozen, they read the ledger as it is now, and a ledger rebuilt from stored records carries them too. The check also compiles the installed declaration file against positive and negative type examples, including a hand-written nine-member ledger stand-in that must still be accepted by the two free functions (their ledger parameter only needs the id-keyed reader). Since 0.91.0 every delivered record the ledger writes names its source (`by`: a frame from the new run, or the accepted steer receipt) and may carry a share (`full` or `partial`, absent meaning full); a delivered record without a source, as written by earlier versions, is read as unknown rather than delivered, a record whose extension fields cannot be read falls back to that unknown form and is stored without them, and keys the reader does not know are dropped and counted. Merging takes the highest of unknown, partial and full for each message, whatever the order: the check replays 2400 random sequences in original, shuffled, duplicated, split-and-merged and persisted forms against an independent model, and feeds records written by this version to the previous release's reader, which must give the same answers. A submission can also carry the engine generation it was sent on; the ledger then lists, once per generation, the messages submitted there that are still known undelivered, without changing the recorded facts, and a malformed generation is ignored while the submission itself is still recorded. Revoked proxies passed as batches, outcomes, record lists or message lists are treated as unreadable instead of throwing. Both write ports also take the two tiers of an encoded submission, `{ full, partial }` as returned by `deliveredUuidsOf`: the partial tier is recorded with `share: 'partial'`, so a message that was only partly sent is never read back as known undelivered, and an unreadable batch records nothing without throwing. On read-back, once a record's `u` and `s` are read, a throwing extension field or key enumeration keeps the record as legacy instead of dropping it. |
|
|
465
469
|
| `scripts/run-session-memory-erase-projection-test.mjs` | The session owner's memory erase (`eraseSessionMemory`, with the pure judge `sessionMemoryEraseOutcome`): one call sends exactly one request to erase what a session contributed to memory, with a body of exactly the request id plus the unevidenced-erase consent only when the caller gives it as a boolean (never added when absent, and no other key the caller passes reaches the wire); a dot-segment session id is refused locally. The connection takes the same configuration as the engine client factory, and the outbound headers are compared byte for byte with a client that factory builds for every credential form (string, per-request value function, a value function that returns nothing or throws, empty, same-origin relay, loopback without a token, blank or missing principal); a token-bearing connection in a browser host sends nothing. Malformed arguments, options and connections never throw and send nothing. A request that may have reached the engine without a readable answer (time limit, a connection dropped after the request went out or while the body was read, a cancellation after sending, an unreadable response) is reported as no answer, while a connection that never opened, a cancellation before sending or a synchronous fetch failure is reported as not sent. The judge sorts each answer into erased, partly erased, refused (nothing was erased: not sent, or a named engine code issued before any change) or not confirmed (no answer, an unreadable receipt or one for a different request or session, a refusal with two possible meanings, an unrecognised code, or a non-success status without the engine's own error code; a code found only on the prototype chain does not count), by machine code before HTTP status, with one fixed sentence per outcome and a retry posture; a caller who gave the unevidenced consent always gets a person decides on the not confirmed outcomes. Receipts are read with the existing erase receipt reader. The closed lists are frozen, derived from the wording tables and equal to what the judge produces; sentences differ, refusals always say nothing was erased, and none copies a server sentence. The check also boots installed server packages of the current and the previous generation, when available, with their real dispatcher and handler and the engine's real file memory store, and drives the call against them: real receipts and replays, a request id reused on another session, a store without erase evidence with and without consent, unknown and foreign sessions, missing credentials, missing faces, malformed bodies and the coded refusals; the previous generation's untyped internal errors are judged not confirmed. The unevidenced-erase consent counts only when it is the caller's own property (an inherited one is never sent), a session id that cannot be encoded into the path is refused locally without a request and without a rejection, and a signal-shaped object that is not a real abort signal is refused as bad input. |
|
|
466
470
|
| `scripts/run-disable-skills-wire-test.mjs` | The request field that turns off every skill for one request (the deployment's skills, skills the client sends and skills listed on agent definitions). It is carried on the two user lanes only as the literal true; false or an absent value sends nothing, and any other value is refused before anything is sent. A request that also carries a non-empty skill list, or an agent definition with skills, is refused by name both when it is built and again after agent definitions are added later, because the engine rejects that combination. The value reaches a request only through a helper that reads the engine's capability report: when the engine has not reported the capability, reports it as unavailable, has not been read yet, or reports it in a form that cannot be read, nothing is sent (an engine that does not know the field rejects the whole request) and the caller receives the reason plus one sentence saying skills stay on for this run. Requests that do not use the field are byte-identical to the release before it was introduced, apart from images on the one-shot lane, which gained a seat in a later release and are checked by the request-receipt and human-submission guards instead. |
|
|
467
|
-
| `scripts/run-permissions-declaration-test.mjs` | The request-side assembly of permission rules (`permissionsDeclaration`): the deny, ask and allow lists the client resolved from its settings become the `settings.permissions` fragment of a request. Every list is ordered by where each rule came from (managed policy first, then command-line flags, the settings flag, the session, user, local and project settings, and rules of unknown origin last) and filtered by the same judges the engine uses, so a rule the engine would refuse does not turn a whole request into an error — the judges are the client's installed engine functions, so the server can still refuse the whole request when its engine version differs from the client's, when a judge fails on one rule and that deny or ask rule goes through unchecked, or when a list is longer than the engine's limit. A whole-server or whole-peer rule (`mcp__<server>`, `a2a__<peer>`; one rule for every protocol prefix the installed engine knows) is rewritten to the same name followed by `__*`, after checking, protocol by protocol, that the engine judges both forms alike and that every tested server generation accepts the rewritten form. A whole tool name that the engine's rule compiler refuses (for example one containing a closing parenthesis or a backslash) is kept on the client as well, whether or not the rule was rewritten, so every rule sent has passed the same judge; this is checked on generated rule strings and differs from the previous terminal's behaviour in exactly that one registered class. Allow rules are sent only when the engine reports, through one capability position read by `observedPermissionsAllowGrant`, that it reads allow rules as rules rather than as an exclusive list; an engine that does not say so may read an allow list as the only tools permitted, so nothing is sent and approvals keep being answered on the client side. A sent allow rule records that someone answered for the tool; it does not waive the prompts the engine raises on its own, so the client keeps answering matching approvals from all of its allow rules, and allow rules with a content pattern are never sent. Allow rules that come from settings files (or whose source is unknown) are also held back whenever the host does not report that the settings read which produced them was clean (`settingsRead: 'clean'`; a partial or unreadable read, or no report at all, holds them back), while allow rules from the command line or the session are still sent, so every lane follows one rule decided in one place. The capability position is the one servers publish from 7.115.0 on, checked against the capabilities route of each server generation the gate is given. An empty list is never sent, missing rule judges raise an error instead of sending unchecked rules (a judge that is present but fails on a rule is the exception above), the diagnostic sentence is derived from the same judgement as the sending decision and never throws, and inputs with inherited or malformed fields, or rule lists whose gaps are filled from a modified array prototype, are refused rather than silently losing a rule. A companion report, `permissionsDeclarationReport`, comes out of the same single pass: its `permissions` are byte-identical to the sending decision, every rule string lands exactly once either in what is sent (with a table mapping each rewritten form back to the rule that produced it) or in a `kept` list with one reason from a closed vocabulary, and deny or ask rules sent while the rule compiler could not answer are listed as unchecked; the gate keeps one witness per reason, checks that both entry points make the same sequence of compiler calls and throw the same errors, and compares the sending decision with the previous release on two independent rule pools. From 0.92.0 the allow list's whole tool names are checked under the tightening reading, because the engine's rule compiler no longer takes an allow reading at all: the guard proves the swap changes nothing on the wire — the published 0.90.0 build running on the engine generation that receives this list and the current build running on either engine generation emit byte-identical requests over two rule pools, every name the receiving generation refuses under the allow reading is refused by the current compiler under the deny reading and no other, and the mirrored rule shape's behavior set equals the installed compiler's. It also records why the engine's own allow-list grammar is not used as the judge yet: on the names this assembly checks, that grammar and the receiving generation's compiler disagree in exactly five registered classes (names with a backslash, which the receiving generation refuses and would reject the whole request for, are never sent), and a sixth class appearing turns the guard red. From 0.92.0 it also pins how path rules relative to the task root (`src/**`, `./x`, `/x`) are handled: they are sent exactly as written, byte-identical to every published build from 0.90.0 on (0.91.0 included, whose companion report is compared in full as well whenever the capability that turns allow rules on is not reported as true), and never rewritten to an absolute `//` form; for every server generation the gate is given, it records verbatim how that server's settings intake answers such deny and ask rules with and without a task root (refused without one, accepted with one, absolute `//` rules accepted either way), so a change on the server side turns the guard red. From 0.92.0 two of those classes (protocol names whose server segment carries a backslash or a wildcard) are held back before the name is checked at all, so three classes reach that judge, and the guard checks each held witness was never asked. Protocol rule names whose server segment the engine never mints as written (a wildcard segment, or one the engine's own naming function would rewrite) match no tool on any server generation: in the allow list they are held back with the reason `unminted_protocol_segment`, while deny and ask rules are still sent exactly as the previous release sent them (a tightening rule must not quietly disappear; from 7.116.0 the server refuses the whole request and names the rule) and are listed in the report's `unreachable` table with the prefix the engine mints for that server, so a client can warn before sending; the guard compares the deny and ask lists with the previous release byte for byte on a pool full of such names, and checks that the table lists exactly the sent rules the server's judge would refuse. Without the naming function installed the allow copies are held as not judged and only wildcard segments are listed. Since 0.94.0 the allow bucket follows the engine generation that receives it: with the allow-layer capability (and the engine's allow-list reader installed) content-form allow rules are sent as written, and an entry the engine would read as voiding the whole allow list is held back and reported; older engines keep the previous projection byte for byte. The one-sentence notice for the print lane when settings-file allow rules were held back because the settings read was not clean (`permissionsAllowKeptNotice`) is pinned here: the wording per read-health state, the managed-policy variant, no rule text in the sentence, malformed input treated as absent. |
|
|
471
|
+
| `scripts/run-permissions-declaration-test.mjs` | The request-side assembly of permission rules (`permissionsDeclaration`): the deny, ask and allow lists the client resolved from its settings become the `settings.permissions` fragment of a request. Every list is ordered by where each rule came from (managed policy first, then command-line flags, the settings flag, the session, user, local and project settings, and rules of unknown origin last) and filtered by the same judges the engine uses, so a rule the engine would refuse does not turn a whole request into an error — the judges are the client's installed engine functions, so the server can still refuse the whole request when its engine version differs from the client's, when a judge fails on one rule and that deny or ask rule goes through unchecked, or when a list is longer than the engine's limit. A whole-server or whole-peer rule (`mcp__<server>`, `a2a__<peer>`; one rule for every protocol prefix the installed engine knows) is rewritten to the same name followed by `__*`, after checking, protocol by protocol, that the engine judges both forms alike and that every tested server generation accepts the rewritten form. A whole tool name that the engine's rule compiler refuses (for example one containing a closing parenthesis or a backslash) is kept on the client as well, whether or not the rule was rewritten, so every rule sent has passed the same judge; this is checked on generated rule strings and differs from the previous terminal's behaviour in exactly that one registered class. Allow rules are sent only when the engine reports, through one capability position read by `observedPermissionsAllowGrant`, that it reads allow rules as rules rather than as an exclusive list; an engine that does not say so may read an allow list as the only tools permitted, so nothing is sent and approvals keep being answered on the client side. A sent allow rule records that someone answered for the tool; it does not waive the prompts the engine raises on its own, so the client keeps answering matching approvals from all of its allow rules, and allow rules with a content pattern are never sent. Allow rules that come from settings files (or whose source is unknown) are also held back whenever the host does not report that the settings read which produced them was clean (`settingsRead: 'clean'`; a partial or unreadable read, or no report at all, holds them back), while allow rules from the command line or the session are still sent, so every lane follows one rule decided in one place. The capability position is the one servers publish from 7.115.0 on, checked against the capabilities route of each server generation the gate is given. An empty list is never sent, missing rule judges raise an error instead of sending unchecked rules (a judge that is present but fails on a rule is the exception above), the diagnostic sentence is derived from the same judgement as the sending decision and never throws, and inputs with inherited or malformed fields, or rule lists whose gaps are filled from a modified array prototype, are refused rather than silently losing a rule. A companion report, `permissionsDeclarationReport`, comes out of the same single pass: its `permissions` are byte-identical to the sending decision, every rule string lands exactly once either in what is sent (with a table mapping each rewritten form back to the rule that produced it) or in a `kept` list with one reason from a closed vocabulary, and deny or ask rules sent while the rule compiler could not answer are listed as unchecked; the gate keeps one witness per reason, checks that both entry points make the same sequence of compiler calls and throw the same errors, and compares the sending decision with the previous release on two independent rule pools. From 0.92.0 the allow list's whole tool names are checked under the tightening reading, because the engine's rule compiler no longer takes an allow reading at all: the guard proves the swap changes nothing on the wire — the published 0.90.0 build running on the engine generation that receives this list and the current build running on either engine generation emit byte-identical requests over two rule pools, every name the receiving generation refuses under the allow reading is refused by the current compiler under the deny reading and no other, and the mirrored rule shape's behavior set equals the installed compiler's. It also records why the engine's own allow-list grammar is not used as the judge yet: on the names this assembly checks, that grammar and the receiving generation's compiler disagree in exactly five registered classes (names with a backslash, which the receiving generation refuses and would reject the whole request for, are never sent), and a sixth class appearing turns the guard red. From 0.92.0 it also pins how path rules relative to the task root (`src/**`, `./x`, `/x`) are handled: they are sent exactly as written, byte-identical to every published build from 0.90.0 on (0.91.0 included, whose companion report is compared in full as well whenever the capability that turns allow rules on is not reported as true), and never rewritten to an absolute `//` form; for every server generation the gate is given, it records verbatim how that server's settings intake answers such deny and ask rules with and without a task root (refused without one, accepted with one, absolute `//` rules accepted either way), so a change on the server side turns the guard red. From 0.92.0 two of those classes (protocol names whose server segment carries a backslash or a wildcard) are held back before the name is checked at all, so three classes reach that judge, and the guard checks each held witness was never asked. Protocol rule names whose server segment the engine never mints as written (a wildcard segment, or one the engine's own naming function would rewrite) match no tool on any server generation: in the allow list they are held back with the reason `unminted_protocol_segment`, while deny and ask rules are still sent exactly as the previous release sent them (a tightening rule must not quietly disappear; from 7.116.0 the server refuses the whole request and names the rule) and are listed in the report's `unreachable` table with the prefix the engine mints for that server, so a client can warn before sending; the guard compares the deny and ask lists with the previous release byte for byte on a pool full of such names, and checks that the table lists exactly the sent rules the server's judge would refuse. Without the naming function installed the allow copies are held as not judged and only wildcard segments are listed. Since 0.94.0 the allow bucket follows the engine generation that receives it: with the allow-layer capability (and the engine's allow-list reader installed) content-form allow rules are sent as written, and an entry the engine would read as voiding the whole allow list is held back and reported; older engines keep the previous projection byte for byte. The one-sentence notice for the print lane when settings-file allow rules were held back because the settings read was not clean (`permissionsAllowKeptNotice`) is pinned here: the wording per read-health state, the managed-policy variant, no rule text in the sentence, malformed input treated as absent. 0.98.0: the deny / ask buckets now carry every non-empty rule (only equivalent rewrites; the report has no unchecked / unreachable tables; deny / ask kept rows are only empty_rule / duplicate), the allow bucket is byte-identical to the previous build, every throw from the assembly carries a machine-readable refusal identity (read from own data slots, identical in a minified bundle), and the live server fixture refuses / accepts each passed-through rule as expected. |
|
|
468
472
|
| `scripts/run-file-history-leg-notice-test.mjs` | What a client says after the engine reports, for one run, whether it is capturing file history. A five-way verdict combines the engine's capture capability as it was read when the request was assembled (not as it reads when the report arrives), whether this run's submitted request actually carried the per-run opt-out, whether the user turned file checkpoints off, and the word reported for the run; only one outcome produces a sentence (the run carried the opt-out and the engine still captures), with its conclusion first so a truncated status line still reads. The guard pins every one of the 54 cells of the verdict table, sweeps alternative spellings and malformed values for each input, checks that an unreadable capability never produces a sentence while a carried opt-out is judged without consulting the capability, and that inherited input properties are ignored. A process-local ledger records, per submission, whether the outbound request body carried the opt-out (only a plain own data property counts; a body with its own serialisation hook or an accessor for that key is read as unknown, so a fact is never recorded that the sent JSON could contradict) and binds the submission to its run only on a frame proving the run was created, using the same rule as the prompt-delivery ledger, so session-busy refusals and replayed approval cards carrying another run's id never attach this submission's fact to that run; answers do not depend on call order, conflicting records read as unknown, and runs this process never bound read as unknown. An end-to-end section sends real requests through the SDK stream parser to a local stub and drives the reported word through the projection and adapter layers to the verdict. A body whose property descriptor and property read disagree (a proxy), or that carries any enumerable getter, reads as unknown, because serialization runs those getters and may drop the field; a value that is a non-null object (a wrapped string, an object with its own serialisation hook) also reads as unknown, since what it serialises to cannot be proved. |
|
|
469
|
-
| `scripts/run-support-floor-projection-test.mjs` | The package's machine-readable support floor: the oldest server, engine and SDK versions it still supports, kept in `scripts/support-floor.json` with the source of each value and exported at runtime as `SUPPORT_FLOOR` (with `supportFloorOf` and the engine version reading `engineVersionTier`). Since 0.95.0 the constant is not a hand-kept copy of the declaration but its build-time product: a generator writes one comment-free, import-free file from the declaration, and the check fails when the checked-in file differs from a fresh generation, carries a comment, when the runtime constant is not that very object, or when the source holds a literal version string; it proves itself against an unfaithful generator (a dropped key, a changed key order, a stale file, an injected comment, an orphan or missing file) and against twelve malformed declarations. The check also fails when the declaration cannot be read or is malformed, when the exported values differ from it, when the SDK value differs from the SDK peer range, and (when the full fixture set is required) when no server fixture in the list matches the declared server version or that server pins and installs a different engine than declared; every fixture in the list is printed with whether it sits below, at or above the floor. The version reading answers engine at and above the floor, legacy below it and unknown for anything that is not a plain release version (pre-releases, build metadata, a leading v, whitespace, wrong segment counts, leading zeros, the word unknown, non-strings); a caller-supplied threshold can only raise the floor, a malformed one is refused before any version is read, and hostile inputs never throw. The same reading exists for the version of the engine install's core package (`coreVersionTier`): the same parser, the same three answers and the same result type, differing only in where the lower bound comes from (the declared engine floor rather than the declared server floor); the check compares the two readings on one pool of inputs, pins the golden cases, and fails when either reading carries its own version comparison instead of calling the one shared function. Since 0.93.0 the check also pins that exactly one function in the whole source tree reads a version string (a planted version-shaped regular expression or split-and-parse is detected by the same scan); the per-feature version checks that used to carry their own thresholds — the limits and detach checks, the goal stop-hook tier, the plan-review three-choice tier and the inherited-environment server tier — no longer exist. Since 0.96.0 the check also covers the effective threshold read: the one value the engine version reading actually applies — the declared server floor or the caller's own floor, whichever is higher — is exposed as a single read, so a message that names the threshold prints the very version the check used. It pins that the read gives the same answer as the check on a pool of floors (the answer is exactly the lowest version the check lets through, including across segment boundaries), that a malformed caller floor is refused identically by both, that both come from one shared function whose body is a single delegation, that a version segment above 2^53 − 1 is treated as unreadable (a floor with one is refused, a version string with one reads as unknown) so the printed threshold and the admitted minimum cannot disagree, and that the generated declaration object is not part of the public surface. |
|
|
473
|
+
| `scripts/run-support-floor-projection-test.mjs` | The package's machine-readable support floor: the oldest server, engine and SDK versions it still supports, kept in `scripts/support-floor.json` with the source of each value and exported at runtime as `SUPPORT_FLOOR` (with `supportFloorOf` and the engine version reading `engineVersionTier`). Since 0.95.0 the constant is not a hand-kept copy of the declaration but its build-time product: a generator writes one comment-free, import-free file from the declaration, and the check fails when the checked-in file differs from a fresh generation, carries a comment, when the runtime constant is not that very object, or when the source holds a literal version string; it proves itself against an unfaithful generator (a dropped key, a changed key order, a stale file, an injected comment, an orphan or missing file) and against twelve malformed declarations. The check also fails when the declaration cannot be read or is malformed, when the exported values differ from it, when the SDK value differs from the SDK peer range, and (when the full fixture set is required) when no server fixture in the list matches the declared server version or that server pins and installs a different engine than declared; every fixture in the list is printed with whether it sits below, at or above the floor. The version reading answers engine at and above the floor, legacy below it and unknown for anything that is not a plain release version (pre-releases, build metadata, a leading v, whitespace, wrong segment counts, leading zeros, the word unknown, non-strings); a caller-supplied threshold can only raise the floor, a malformed one is refused before any version is read, and hostile inputs never throw. The same reading exists for the version of the engine install's core package (`coreVersionTier`): the same parser, the same three answers and the same result type, differing only in where the lower bound comes from (the declared engine floor rather than the declared server floor); the check compares the two readings on one pool of inputs, pins the golden cases, and fails when either reading carries its own version comparison instead of calling the one shared function. Since 0.93.0 the check also pins that exactly one function in the whole source tree reads a version string (a planted version-shaped regular expression or split-and-parse is detected by the same scan); the per-feature version checks that used to carry their own thresholds — the limits and detach checks, the goal stop-hook tier, the plan-review three-choice tier and the inherited-environment server tier — no longer exist. Since 0.96.0 the check also covers the effective threshold read: the one value the engine version reading actually applies — the declared server floor or the caller's own floor, whichever is higher — is exposed as a single read, so a message that names the threshold prints the very version the check used. It pins that the read gives the same answer as the check on a pool of floors (the answer is exactly the lowest version the check lets through, including across segment boundaries), that a malformed caller floor is refused identically by both, that both come from one shared function whose body is a single delegation, that a version segment above 2^53 − 1 is treated as unreadable (a floor with one is refused, a version string with one reads as unknown) so the printed threshold and the admitted minimum cannot disagree, and that the generated declaration object is not part of the public surface. Since 0.98.0 the check also pins that the two outward copies of the floor — the README support-floor line and the support-floor row of the integration guide's version table — carry exactly the declared server, engine and SDK versions, so raising the floor in one place and not the others fails. |
|
|
470
474
|
| `scripts/run-server-7113-fixture-test.mjs` | Adoption checks against the published server package, run once per server generation (the one that carries the change and the one just before it). Session reads, live steer and policy settlement are driven through the package's real routes and modules; the attachment checks read the package's shipped source text instead. Attachments: the keys the server's shipped shape description lists are exactly the keys this package knows, the server's shipped refusal for the retired plan-mode reminder key names its retirement, every value this package's attachment switch can produce stays inside the accepted set, and naming the retired key is refused locally instead of being sent. Session reads: a non-owner reading a session's detail, head or event stream gets a not-found whose code names the session (the previous generation named a run), and this package's session read answers the same way on both. Live steer: without an idempotency key the engine's input id equals the message id on the receipt, a queued frame and an applied frame on one input id each project as their own event with every field as written, and the delivery ledger records delivery once. Policy settlement: an unattended answer by class is recorded under the policy party as an allow settled by policy, read back verbatim, no longer read as a person's approval, and a tool-end frame carrying it still projects. |
|
|
471
475
|
| `scripts/run-server-7114-fixture-test.mjs` | Adoption checks against the published server package's real task admission, run on every configured server package (the newest one and each older one in the list), each held to an exact expectation for its generation. From 7.114.0, a tool-event hook matcher that selects a tool's alias but not its current name is refused with a field-invalid error naming both names, in every spelling the server reads (literal, pattern, padded, comma list, pipe list, empty segment), and a matcher that also selects the current name is accepted; 7.113 refuses only the literal and the pipe-list spellings; 7.111 and older accept all nine. A server generation outside the table fails the check instead of being guessed, and with fixtures required each generation band must be covered. This package passes the matcher through byte for byte, the request it assembles gets the same answer from the server as the hand-written one, and its local skip check keys on the matcher as written. From 7.115.0 the refusal additionally carries a machine-readable `field` and a `detail` naming the event, the alias and the current name, with the sentence and the error code unchanged; older generations carry neither. |
|
|
472
476
|
| `scripts/run-steer-refusal-advice-test.mjs` | Refusals of a mid-run steer have one speaker. The root entry's `steerRefusalAdviceOf(error)` returns the steering refusal's safety code and disposition sentence, and `ControlRouter.steer` builds its `ControlSafetyError` from that very call, so a client that steers without the router (and therefore holds the raw SDK error) shows the same words the router would instead of keeping its own sentence table. The guard runs a pool of 180 thrown values (every steering code under ten status shapes, every SDK class name, non-steering errors, throwing getters, proxies, inherited fields and non-objects) through both paths and requires the same code, sentence, message, status and cause on every row; it checks every message against the bytes the previous release produced on the same pool, recorded from that build rather than hand-written, with the four rows whose field reads throw registered as the one intended change: the original error now propagates instead of the getter's. It also pins the error's two new read-only fields (`advice`, `status`), absent on the old two- and three-argument constructor calls and on the cancel 404 and local refusals that carry no disposition; the call-site roster of the three steering legs; that each sentence exists exactly once in the source; and that the open-set sentence splits on the same 4xx line the delivery verdict uses. A last section compares each sentence with the table a client kept, so a change on either side forces a fresh reading. From 0.92.0 the sentences are rewritten so a client can show them as they are, each is built from two halves the result also carries, and the cancel 404 gains one: the guard keeps comparing the code and the message prefix with the previous recorded bytes and requires the message to be that prefix followed by the new sentence |
|
|
@@ -478,7 +482,7 @@ guard still cross-checks the table by name).
|
|
|
478
482
|
| `scripts/run-replayed-approval-disposition-test.mjs` | Approval requests read back from a run's event log are only raised as questions when the pending-approval list says they are still open: `replayedApprovalDisposition(frame, basis)` answers `live` / `decided` / `unknown` (a frame without an event-log position is live; one listed by this connection's opening snapshot or by a pending list read after the frame arrived is live; one missing from a complete list is decided, with the crash-convergence cause when the same response carries one; an incomplete, missing or unreadable list, or no read, is unknown and never treated as open), and the in-stream approval leg applies the same verdict before raising a card — a decided frame raises no card and sends no decision, an unknown one raises no card, sends nothing, hands the frame to `onReplayedApproval` and shows one notice, the list is re-read for every such frame (never cached across frames), the read is bounded, and the opening-snapshot evidence is reset on every new connection. The test for 'does this frame carry an event-log position' is public (`hasLedgerPositionKey`: either an `id` or a `seq` key that is present and not null counts, whatever its value; it is the exact opposite of the verdict's live-leg shortcut), and the one sentence a host shows for a decided or unknown frame is minted in one place (`replayedApprovalSentence`): a decided frame is described only as no longer listed by the engine as waiting (a tool name, when given, is sanitised and shown inside quotation marks), a crash-convergence cause says the engine denied it automatically (it stopped unexpectedly, or was shut down in an orderly way), nothing is ever described as approved or as having run, an unknown frame says it could not be confirmed, and every sentence says no decision was sent. A question the bridge has already seen settled (answered, dismissed with an interrupt, or closed by an engine closing frame) is never raised again by a repeated frame — with or without an event-log position, whether or not the opening snapshot lists it, and even when the pending list read afterwards still lists it: the verdict is `decided` with a `settled` word (never together with a crash-convergence cause), no card, no decision, no list read, no notice, and one hand-off to `onReplayedApproval`. Only certain outcomes are recorded (a card the host withdrew or a decision that failed to send is not, so a repeated frame still raises a card then), the record is first-outcome-wins, survives new connections, is bounded, and the sentence for it is minted in the same one place without saying what the outcome was. A host that keeps its own set of already-closed approvals asks one public predicate (`replayedApprovalClosesCardFrame`) whether a handed-back disposition should make later card frames for the same approval count as closed: only a decided disposition carrying a known crash-convergence cause does; a decided one without a cause (the answering replica may simply not have it in memory), a settled one, a live or unknown one, an unfamiliar cause and any malformed value do not, so a card that might still be answerable stays visible. |
|
|
479
483
|
| `scripts/run-server-7116-fixture-test.mjs` | Checks against the published 7.116.0 server package and every older package in the list, each held to an exact per-generation table. Answering a stream approval that the server has already closed without a human decision (a 410 `gone.ask`, which from 7.116.0 also covers an approval left pending by a process that crashed or shut down) is produced by the server's own convergence step and decision route, and through both SDK error mappings is read by `askGoneFromError` and turned by `askGoneContent` into a three-part sentence whose way out depends on the cause; the decision-failure note and the already-resolved check claim it by code; the two process-ended causes are the same closed list as crash-converged rows, the sentence carries no machine words, and the one denied-group word the reader keeps equals the server's table. Run death codes from the server's run-detail route reach the abort-cause reader and the synthesized terminal unchanged, with one way-out sentence per class. An empty operator list now answers 501 `capability.operator_principals_required`, read as `operators_unconfigured` (nothing was done) by the memory and rules readers and the rule write, while 403 stays `operator_only`; a plan-review decision hitting an unavailable-face 501 is `not_applied`; the four new capability bits are read by their readers. Protocol rule names whose server segment the engine never mints are held back from the allow list, while deny and ask rules are sent unchanged and listed in the report's `unreachable` table; the server's own judge refuses exactly the listed rules (naming the same minted prefix), the request without them is accepted, and the whole request is refused by 7.116.0 and accepted by older servers. The remember-refused card flag reaches the card from both paths. |
|
|
480
484
|
| `scripts/run-automatic-answer-verdict-test.mjs` | The single verdict `automaticAnswerVerdict(card, lane, facts?)` that decides whether an automatic lane (the standing permission mode, or a standing allow rule) may answer an approval card on the user's behalf. The only `may_answer` exit is a well-formed class set whose classes are all known to this build, whose folded row does not block on that lane, with no must-ask evidence on the card and, for the mode lane, a host reading that its own ask rules did not match; every other card is `must_ask` (with the road that decided it) or `unknown` (with why). The guard derives every single-class card and every ordered pair of classes on both lanes from the engine's own arbitration table, walks every row of the decision table on both sides (including positive evidence when the class set is absent or malformed, class sets that change while being read, and class names this build does not know), compares the package's fold against the engine's fold function over all singles, pairs, triples and a random sample, checks the mode lane against the engine's bypass-mode arbitration (including the release disclosure), confirms on a real engine run that a matched ask rule always puts `ask_rule` in the class set, checks that the two older predicates never answer more loosely than this verdict nor than the previous release, and, when a terminal reference tree is available, that the terminal's own decision chain auto-approves nothing new when it is built against this package. Since 0.94.0 it also covers the class-aware form of the mandated-question sentence: the clause naming an automatic check appears only when the card's question classes let the classifier or the sandbox answer, checked class by class against the engine's own table, and any unreadable card falls back to the generic sentence. Since 0.94.0 the mode lane reads the bypass column on the tool face of the card (the face comes only from the generated shell-face name list, matched exactly, and releasable classes are read per face from the generated table): a write whose protection could not be resolved is answerable on the shell face and still asked on file tools, while lookalike tool names (case, whitespace, zero-width or homoglyph characters, protocol prefixes, rule syntax, inherited or accessor names, non-strings, absence) always read as the other face; when the installed engine exposes faces, the fold and the bypass arbitration are compared per face against the engine and every catalog name is checked against its face. A third lane, `permission_request_hook`, reads the hook column of the same table with the same must-ask evidence first, ignores allow-rule shadow evidence and host facts, and `permissionRequestHookScope` turns its verdict into allow-or-deny, deny-only or skip. The terminal-chain comparison accepts exactly the shell-face release as the one intended widening. |
|
|
481
|
-
| `scripts/run-allow-rule-shadow-projection-test.mjs` | The reader for the fact that a standing allow layer matched a call but could not clear its ask (`readApprovalAllowRuleShadow`, `approvalAllowRuleShadowed`, and the structured `allowRuleShadow` disclosure stamped on the approval card).
|
|
485
|
+
| `scripts/run-allow-rule-shadow-projection-test.mjs` | The reader for the fact that a standing allow layer matched a call but could not clear its ask (`readApprovalAllowRuleShadow`, `approvalAllowRuleShadowed`, and the structured `allowRuleShadow` disclosure stamped on the approval card). Servers send a list of members with their matched entries, and that list is the only thing read. Since 0.98.0 the single text that older servers and older parked rows sent is no longer read as a disclosure: a frame or row that carries only that older text has no disclosure and no sentence, while its presence still counts against automatic answers — the card paths record it as unreadable evidence and the automatic-answer check answers "not known" for it, also when a bare frame is handed straight to the check — so retiring the reading never lets a remembered rule answer an ask it could not answer before. The guard covers every member, several members in order, empty entry lists, the count of omitted entries, member names this build does not know, four kinds of malformed values (all of which still count as evidence), absence, the older text on both card paths and the parked-ask path through the real card port (including a row whose risk descriptor is inherited or comes from a getter), a check against the previous release that every older-shaped frame and row it treated as evidence is still not answered automatically, real frames and durable rows minted by the server fixtures when available, and the effect on every automatic lane. The one sentence a card shows for that fact is minted in one place (`allowRuleShadowSentence`): an absent disclosure renders nothing (it never says there is no rule), an unreadable one says so, and entries and member words arrive display-sanitised. |
|
|
482
486
|
| `scripts/run-steer-tier-test.mjs` | Steer priority goes through one gate. The capability reader for `fleet.steerPriority` keeps three states, takes the word as an open set and treats `fleet: false` as not reported; its judge answers `engine` only for the exact word. The steer body has one constructor, `steerBodyOf`, used by the router and by clients that build their own request: `next` and no priority never enter the body (byte-identical to the previous releases on six call shapes), `now` and `later` enter it only when the reading is `engine`, and otherwise the constructor throws a refusal with a stable code and no request is sent. Against the 7.116.0 server package the guard runs the capabilities route, the steer route with the engine's own steer verb and an in-memory checkpoint store: the words the client admits are accepted, the words it refuses are rejected with the same field error, same-key replays stay idempotent, a real tier change is refused, and a `now` steer's cut notice carries the receipt's message id. It also pins the one sentence for a same-key replay (it does not tell the user to resend under a new key unless the text changed), the rewritten refusal sentences, the single receipt reader, the cut-notice verdict and sentence through both projection layers, the first-beat order (bounded capability wait, then reading, then send) and the engine-switch chain where an invalidated reading sends nothing |
|
|
483
487
|
| `scripts/run-abort-cause-projection-test.mjs` | The cause a server records when it ends a run itself — the stream client went away, the connection dropped while a question was pending and the run could not be parked in time, or a cancel request arrived. One reader handles all four places the word appears (the run record's result, the streamed `done` result, the synchronous response body and the event log's `failed` row) and reads it only on a cancelled terminal: a word on any other outcome is contradictory evidence and is not read. A missing or malformed word reads as "not recorded" rather than as any of the three, and a word this version does not know stays distinct — reported as unrecognised with the original word, never folded into a known one. The same reading drives the cancelled branch of the run-cause classifier, the `_sema_abort_cause` key on the result frame, and the single sentence appended on a new line to both the synthetic terminal row and the result frame's first error; without a cause both stay byte-identical to a cancelled terminal that carries none. The word list and the reading are checked against every server package in the fixture list — generations that mint the cause and older ones that do not — and against the client SDK's own type for it |
|
|
484
488
|
| `scripts/run-transport-failure-scene-test.mjs` | The reader that turns a failed transport call (a thrown value of any shape) into a machine-readable scene: up to four layers of the cause chain, each carrying only name, code, errno, syscall and the socket byte counters — never message text, addresses, headers or request bodies — together with whether the request is provably not sent and which kind of no-answer it is. The not-sent judgement is checked against a frozen copy of the previous release's over generated corpora (code table, syscalls, evidence depth, top-level shapes, merged multi-address errors, throwing getters and proxies); the layer records are checked against a frozen copy of the terminal's own five-layer reader on the same corpus, with three registered divergence classes and none outside them; the source is pinned to a single cause-chain walker; and real undici errors from a loopback server (connection refused, peer closed after the request, peer reset) are read end to end. |
|
|
@@ -494,7 +498,6 @@ guard still cross-checks the table by name).
|
|
|
494
498
|
| `scripts/run-no-allow-cause-test.mjs` | Why an automatic lane did not allow a call, and the refusal line for the two automatic-refusal lanes. `noAllowCauseOf(card, ladder)` combines the card's standing-rule verdict with the host's rule-ladder evidence (only an own, strictly `true` property counts): allowed or denied by a rule gives no cause; a local ask rule gives `ask_rule`; an exhausted automatic-approval budget gives `budget`; an ask rule the card names (its class or origin) gives `engine_ask_rule` when the ladder evidence shows that none of the user's own ask rules matched (the end-of-ladder no-match flag, or the dedicated flag for the case where an allow rule matched but the card's question class overruled it), and stays `ask_rule` when that evidence is absent, because the user's own rule cannot be ruled out; complete no-match evidence on a card a rule could clear gives `no_allow_rule`; everything else gives `other`, so no lane suggests adding an allow rule when that would not help. `policyDenyReason` mints the single line for the lane without an approval UI and for the dontAsk mode, one form per cause, naming only this one call and offering host-specific remedies (an allow-rule flag, a budget setting) only when the host supplies them. The guard covers the golden cases, pairwise ordering, strict reads, all ten sentences verbatim (the engine-side ask rule line says the user's settings cannot approve it and offers neither removing a rule nor adding an allow rule), unsafe tool names kept out of the sentence, and the single mint site. |
|
|
495
499
|
| `scripts/run-approval-card-face-projection-test.mjs` | Three card positions added in 0.94.0. `boundGrants` (what approving a Skill card also grants: the skill name or null, the entries that then run without asking, and the entries refused for the rest of the run) is read once from the live approval frame through a strict reader; any malformed part drops the whole position, the copy is frozen, the digest is not put on the card, and the durable pending-row path never carries it; `approvalBoundGrantsDetail` gives one sentence with counts only, never the deployment text. `classifierNoRuling` (why the auto-mode classifier gave no ruling) is read by word shape as an open set from the live frame only; `classifierNoRulingDetail` has a phrase for each of the engine's words and a generic sentence otherwise. `priorCardsForCall` lists, for the same tool call, the earlier cards that asked about something else (a different class set, origin or mandate word), each with its approval id, origin, mandate word and, when answered here, whether it was allowed or denied; replays and parked twins of the same question do not count, both card paths share one bounded ledger, and `approvalPriorCardsDetail` says this is a separate check that needs its own answer. |
|
|
496
500
|
| `scripts/run-hitl-words-readout-test.mjs` | The five wording and reader fixes of the HITL surface. (1) Seven user-facing failure sentences (pending-row fetch, approve and deny decisions on approval cards, the question-card decision, the parked-row wait, local rule parsing, background stop) carry the thrown value's own message and never the error class name — a thrown value with no usable message becomes one fixed sentence instead of a bare class name, `[object Object]` or `null`; the original thrown value goes only to the host log. (2) `resumeContextUnavailableContent` is a single English source (three ` · ` segments, with and without a run id, the retention seconds only when the engine gave them) and a plan-review decision that meets a 409 `conflict.resume_context_unavailable` says it. (3) The plan-review `permissionModeAfter` pre-check no longer mirrors the server's decision pairing: only the value set is checked locally, and the engine's `request.field_conflict` speaks for the rest. (4) `requestEngineCompact` reports how the fire settled through `onSettled` (accepted / refused / failed / offline), with a 409 `compact.not_running` also surfaced on the host surface. (5) Every exported `project*Capability` reads only own data descriptors: an accessor is never run and reads as `undefined`, and a throwing descriptor read is swallowed the same way. Pinned with throwing and class-named values at all seven sites, both plan-review entry points, the compact outcomes under 409 / 404-retry / 5xx / the 60 s injected deadline, and a per-projector throwing-proxy row plus a source scan. The decision exits treat hostile thrown values uniformly: a throwing message getter, a throwing toString, a revoked proxy, or throwing status, error-code or name getters all end in the thrown value's own message or the fixed fallback sentence, never in the getter's own error text, and the transient-failure classification reads each slot independently. |
|
|
497
|
-
| `scripts/run-session-read-redaction-capability-test.mjs` | The session-history redaction capability reader. From 7.120.0 the server's capabilities body carries a constant bit saying that model and tool output read back from a session's history is redacted by the same pass as the live frames; the reader keeps three states (the literal `true` is present, a missing key is an older worker — including the release that already redacted but did not report the bit — anything else is dropped as unreadable and reads as not observed), ignores keys that exist only on the prototype, reads only own data properties (an accessor is never run and reads as unreadable) and never throws, and its stored reading is discarded automatically when the engine's capability cache changes generation. One judge, `sessionReadRedactionAdmitted`, answers true only for a reading whose own data properties say present and `true` (accessors are never run). The bit stays true when the history route itself is absent, since it answers whether history is redacted, not whether it can be read; and the package source contains no version-number threshold for the same question. |
|
|
498
501
|
| `scripts/run-output-style-seat-test.mjs` | The per-turn output-style seat. From 7.121.0 the request accepts a top-level `outputStyle` object (`name` and an optional `turnReminder`) that the engine states once per attachment cycle, and the capabilities body carries a constant top-level bit for it; the reader keeps the same three states as its constant-bit siblings and does not confuse it with the nested bit for the style body. The live-defaults layer places the host's `outputStyleSeat` on the request only when that reading is admitted, copies it as given (an empty name or extra keys are passed through for the engine to refuse at prepare time with `config.output_style_invalid`, which the configuration-refusal table recognises), never overwrites a caller's own top-level value, never invents one, and leaves the style body seat unchanged; the field is registered in the live-defaults list, survives the unregistered-key check on both lanes, and is reported as belonging to another channel when passed to the request assembler. |
|
|
499
502
|
| `scripts/run-request-uplift-test.mjs` | Six request-assembly and reader pieces shared by every client. The per-run declaration that nobody on an unattended run can answer approval requests is carried only on the unattended lane, only when the engine reports it accepts that field (an older engine rejects the whole request), with one plain sentence for each reason it was held back; the headless permission mode is always stated explicitly, falling back to `default` when neither flag nor environment sets it; a predicate tells apart the denial an engine makes when nobody is reachable and no queue can hold the request; a host-reported route problem (no gateway address, or the anthropic channel with no credentials) turns a model that has a name but cannot start into a stated configuration problem; a reader gives, per model, which thinking levels the engine accepts; attachments from MCP resources and the new not-attached reasons are written as data envelopes rather than as tool calls; and servers the client mounts itself state their own allow rule, audited separately from the user's rules, with the rule-source trust order exported once. |
|
|
500
503
|
| `scripts/run-server-7122-pick-test.mjs` | Checks for the 7.122.0 server pickup. The new refusal for a leader run on a deployment whose autonomy is plan is recognised by its code alone and given one plain sentence; the parked-approval answer's new `message` is read only as an own non-empty string, never placed in the package's own sentence, across both the older and newer body shapes; the end-of-stream frame a draining server sends on read-only subscriptions is told apart from failures and from the pre-stream refusal with the same code, the reconnect wait comes from the same back-off as the pre-stream retry, the package's own stream readers stop treating that frame as a change or as an unknown frame, and a real published server is drained over its own endpoint to show the frame and the reconnect; and agent tool lists are checked for retired spellings and letter-case twins, entry by entry in agreement with the engine's own deny-list judge. |
|
|
@@ -502,7 +505,7 @@ guard still cross-checks the table by name).
|
|
|
502
505
|
| `scripts/run-seat-wave2-test.mjs` | The seat capability table, the folder picker and the session folder, added in 0.94.0. Whether a renderer may offer an optional seat action is answered only by the capability table the host publishes (`seatCapability`: reported, not reported, or unreadable — reading own data properties only, never running a getter), never by calling the action and watching it fail; "not reported" is neither folded into "available" nor into "cannot", and the disabled sentence comes from one place (`seatCapabilityUnavailableDetail`). The subscription helper subscribes before it reads, so a renderer that subscribes after the host has published and then withdrawn a capability sees the withdrawn state on its first callback. `pickFolder` returns a non-empty path or `null` for a cancel, and the session record's `cwd` is the folder the session was started in, absent when none was chosen. The same gate pins `deviceFlowPollingPlan`: the device-code response's own positive finite numbers win, anything missing or malformed falls back to the contract numbers generated from the settings schema at build time (no number is written by hand), and the poll interval is never below one second |
|
|
503
506
|
| `scripts/run-wiring-instructions-attribution-test.mjs` | Which project instruction file the engine actually loaded for a run, attributed to the client's own directory. The capabilities bit `callerCwd` gets its own reader (the same four ports as every capability reader; `callerCwdAdmitted` accepts only an explicit `true`), and a host that notes the bit inside its probe callback reads the new value in the same tick. The working directory a submitted request carried is recorded per run in the same in-process ledger and with the same frame-binding rule as the file-history opt-out (`legCwdOfBody`, `declaredLegCwd`, `legCwdOf`). `engineSelectedInstructionsFile` then returns the selected member only when the run's directory equals the client's current directory, the member is a plain relative path (no `..`, no absolute or drive form, no backslash, no control, bidi or invisible characters), and the host's real-path function confirms it stays inside that directory (a symlink pointing outside is refused). |
|
|
504
507
|
| `scripts/run-layering-direction-test.mjs` | The package's layering table (`docs/LAYERING.md`) and its direction gate. Every source file sits on exactly one internal layer (wire readers and injected ports, projection, host-facing orchestration, the two entry points) and may import only downward, type-only imports included; the exemption table only shrinks. Every import that leaves the package must match a row of the cross-repository table and the kind that row allows: the engine package is read for types only and never at runtime, and the shipped declaration files reference only the packages listed in `dependencies` and `peerDependencies`. |
|
|
505
|
-
| `scripts/run-panel-reduce-test.mjs` | The panel row reducer: one record per background agent, one
|
|
508
|
+
| `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 (which carries no generation) moves the numbers only for an agent without cycle numbers, 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. |
|
|
506
509
|
| `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. |
|
|
507
510
|
| `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`. |
|
|
508
511
|
| `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 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 eleven device row types from the SDK (`DeviceSummary`, `DeviceBinding`, and the rest) are re-exported as types from the package root, and the guard checks each one is identical to the SDK's. |
|
|
@@ -513,11 +516,11 @@ guard still cross-checks the table by name).
|
|
|
513
516
|
| `scripts/run-hooks-managed-policy-reading-test.mjs` | The optional `SettingsPort.managedPolicyReading()` 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. |
|
|
514
517
|
| `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). |
|
|
515
518
|
| `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. |
|
|
516
|
-
| `scripts/run-engine-caps-single-source-test.mjs` | Engine capability reads have one store: the shared capabilities snapshot. The subagent tail, subagent report and manual-compact paths read it (probing once through it when nothing has landed, with a bounded wait) instead of keeping private per-address tables, so a capability follows `invalidateEngineCaps`; a connection without an identity key is probed once and nothing is stored. The suite checks the internal read path, the three migrated entry points against a host-kicked snapshot and across invalidation, an AST scan that every capabilities call in the source is a probe handed to the shared cache (
|
|
519
|
+
| `scripts/run-engine-caps-single-source-test.mjs` | Engine capability reads have one store: the shared capabilities snapshot. The subagent tail, subagent report and manual-compact paths read it (probing once through it when nothing has landed, with a bounded wait) instead of keeping private per-address tables, so a capability follows `invalidateEngineCaps`; a connection without an identity key is probed once and nothing is stored. The suite checks the internal read path, the three migrated entry points against a host-kicked snapshot and across invalidation, an AST scan that every capabilities call in the source is a probe handed to the shared cache (no exceptions: the custom-agent capability verdict reads the same snapshot, follows invalidation at the same address and re-prepares on a new capability generation), single readers for the task-handle capability and the agent-registry list face, a center-wiring reading from the cache that never carries the per-caller lease, and the retirement of the store-based two-generation readings and of the second argument of `toolsRunHereFromExecutionLane`. |
|
|
517
520
|
| `scripts/run-mcp-engine-leg-connection-test.mjs` | The engine-hosted MCP connection reader (`mcpEngineLegConnectionOf`): whether a server is connected on the engine's leg is read only from that leg's `wiring_manifest.mcp[]` status, never from the tool roster or a client's own connection. The suite checks the three states and their causes (connected with an optional tool count, not connected because it failed, was skipped or is not listed, and unknown because the section was not received, the name matches several rows, the status is unreadable or the word is not recognised), that the row lookup is shared with the liveness reader, that the liveness record is not read, and the same answers after the package's own section projector, including rows the projector drops because their status is unreadable (counted by name on the projected array, invisible to JSON and key enumeration). |
|
|
518
521
|
| `scripts/run-model-switch-fallback-test.mjs` | The model-switch judgments (`modelSwitchProbeOutcomeOf`, `runModelSwitchProbe`, `createModelSwitchFallbackLedger`): the probe outcome for each lane (allowed without probing on a mock lane, not switched when the probe is not wired, threw, was missing or rejected the name, with the probe's own words as the reason), the orchestration that only calls the validator when it is needed, and a per-session fallback ledger that names the last model that answered when the newly selected one fails its first turn with an empty stream. The suite checks every arm, the negative cases (other error codes, other sessions, other models, a model already proven), that judging never changes the ledger and that a stale sequence number cannot release a newer switch. |
|
|
519
522
|
| `scripts/run-approval-cross-leg-holder-test.mjs` | One holder per approval across the live card and the durable paths. While the package holds the live card for an approval (the card is on screen or its answer is being sent), a durable path for the same approval (the package's own parked-row decision path, or a host's own durable route that asks `joinApprovalHolder` first) presents no second card and sends no decision; once the holder lets go, an approval it settled is joined (the holder's outcome marked `joinedInFlight: true`), and one it did not settle (the answer was not delivered, the row is being parked or was parked, the host withdrew the card) is taken over by the durable path, never while the live card is still on screen. Identity is the approval id when the caller has one; otherwise the same run and the same gated call (and the same session when both sides know it), matched only while the live card still waits for an answer, and any component that cannot be read means no match. Cells cover the host timing that produced the second card (an answer after more than the 750 ms grace window: zero second card, zero durable decision, the next approval's card shows at once), the negative controls (nobody holds it; the live answer fails with 404 and the durable card is requested only after the live card was answered), unreadable or different identities, aborts from either side (one interruption deny), the parked-row path joining an allow and a deny, a second approval of the same call while the first answer is in flight, a parked close-out handing over to the parked row, and the scope: a host that picks the route by frame type with no timed fallback is not on this path |
|
|
520
|
-
| `scripts/run-engine-cap-snapshot-readers-test.mjs` | Capability readers derive their reading only from the capabilities cache's landed response, never from a second table
|
|
523
|
+
| `scripts/run-engine-cap-snapshot-readers-test.mjs` | Capability readers derive their reading only from the capabilities cache's landed response, never from a second table, and before a landing, after an invalidation, after a failed probe, or for a malformed or non-object body the reading is "unobserved". A reading is memoised per landing and frozen, a second landing of the very same body re-projects, and re-entering a reader while it is projecting answers unobserved without being remembered. Every projection (all 29, plus the attachment-seat placeholder and the shared store-seat reader) reads only own data descriptors: a recording proxy sees zero `get` and `has` traps, an accessor at the top level or at any nested position that would return a valid value is never run and reads as malformed, a key that exists only on the prototype is not reported, and a throwing descriptor read is malformed rather than escaping. The seat gates follow the same law for every validated position of every checker (an accessor returning a valid value is refused without running, a recording proxy sees no `get` trap, a read that throws is a refusal and never a throw), and a payload with an accessor anywhere in its tree — validated or passed through — or a validated position that is not enumerable is refused with a dotted path, because the structured clone a host performs afterwards would run the accessor and rewrite a position that had already passed. Finally the retired probe tee ports (`noteEngineCapsFor*`) and per-reader test resets (`__reset*ReadingsForTests`) appear nowhere — not in a source file's code, not among the root entry's exports, not in the build output — and the factory hands out a reader whose only port is its read port; the judge that decides whether the "edit and rewind" hint is spoken reads the same landed capability reading as the rewind menu, directly, with no registration step that depends on which module happened to load first (checked in a fresh process that loads only the capabilities cache and the hint module, and by a scan that no source file runs a statement when it loads apart from compile-time checks). The cache's keyed readers (state, value, true, nested true, string, and the bounded asynchronous state reader) read through the same own-data slot reader as the projections: an accessor is never run and reads as unreadable (never as a value, never as absent), a key found only on the prototype is absent, a key present with the value undefined is absent, and a throwing descriptor read is unreadable rather than escaping; a recording proxy sees no `get` or `has` trap. |
|
|
521
524
|
| `scripts/run-server-7124-pick-test.mjs` | Checks for the 7.124.0 server pickup. The reason an approval ended without a human answer is read from the `cause` body key against one closed list taken from the SDK, grouped by end state (since 0.96.0 the older key that servers before 7.124.0 sent is no longer read: those servers are below the support floor); the live approval card's ending reason is read as a closed list and its absence is never filled in; the decision endpoint's conflict refusals are split into a binding change, a damaged row, an unsupported stored format and an invalid answer, and an invalid answer is no longer reported as "the pending action changed" (since 0.96.0 the older servers' answer-seat form is no longer recognised and is treated like any other unclassifiable binding refusal); the two version fields on the unsupported-format refusal are read together or not at all and change the way out only when the stored work is newer; interrupting a question that is no longer pending (already settled, parked or gone) no longer shows the "session may stay locked" warning, while real failures still do; the run-retired code list matches the SDK's; the SDK itself ends a subscription on its final frame; the engine's registered worker name is projected only when sent; errors minted by the real SDK (its error class and a real client over an injected transport) are read through one shared reader for the extra-fields slot, so a late-answer 404 that carries an ended-cause word is recognised as already ended, while an unreadable or hostile slot reads as no evidence and never throws. |
|
|
522
525
|
| `scripts/run-run-events-drain-reopen-test.mjs` | A draining server ends each read-only subscription with a final frame, and from SDK 19 the run-event stream simply ends there instead of reconnecting. The two run-event legs (the stream that resumes a run after an approval decision, and the headless lane's continuation) now reopen the same run from the last event they saw, after the same back-off as the other subscriptions (1, 2 and 4 seconds, three times), without sending a second decision, without counting the drain as a connection failure and without showing the drain frame; the run carries on to its normal end. The drain frame is never handed on. When the back-off is used up or the run handle is unknown, the leg ends the way a dropped stream does (the decision leg ends cleanly with no final frame and without inventing a failure, so the host's own handling of a dropped stream takes over; the headless leg first looks up the run's stored result, then falls back to its failure path); a caller that has stopped gets the usual interrupt exit. A thrown error is still a lost connection and takes the failure path, and an SDK that reconnects by itself never gets a second stream. All four subscription readers share one drain bookkeeping helper, checked by a syntax-tree roster. |
|
|
523
526
|
| `scripts/run-skill-tool-lists-projection-test.mjs` | Two tool lists on a request skill, `allowedTools` and `disallowedTools`, are only put on the request when the engine reports that it reads them. `skillCommandsToSpecs` takes the capability reading as an option, asks the one capability judge, and sends the lists only on a clear yes; with no option, an unobserved, missing or negative reading, or a value that is not a reading, the request is exactly what earlier versions built, so an older engine never sees the keys (it ignores them on a top-level skill and rejects a delegated agent's skill outright). When they are sent they go as given: no trimming, de-duplication, reordering or truncation, an empty list stays an empty list, and the caller's array is copied rather than shared. The only local check is the shape: each list must be an array of strings or absent (an array that cannot state a valid length, or a read that throws, is refused too), otherwise the call is refused with a sentence naming the skill and the list (never echoing entries or the thrown value), and once the option is passed that check runs whether or not the lists would be sent; without the option the lists are not read at all. Entry-level limits (the per-list cap, deny entries the engine reads as nothing, empty strings) are left to the server, which refuses loudly; the suite runs the server's own admission functions for every fixture generation from 7.116.0 on and shows the package output accepted, the over-cap list still carried in full and refused with the server's own sentence, and the unreadable deny entry still carried and named by the server's judge, and for older generations shows the output carrying no list keys and accepted, while a delegated agent's skill with the keys is rejected. The same output fed to the delegated-agent projection gives the same shape. A source scan pins that there is exactly one reader of the capability and no copy of the server's limits. |
|
|
@@ -530,7 +533,7 @@ guard still cross-checks the table by name).
|
|
|
530
533
|
| `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); 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. |
|
|
531
534
|
| `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. |
|
|
532
535
|
| `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. |
|
|
533
|
-
| `scripts/run-display-credential-reach-test.mjs` | The check for how far the display credential masking reaches. Percent-encoded text is read through decoded views inside the same single scan (up to three layers, on the raw text and on the painted screen text), so an encoded separator such as `token%3D<key>`, an encoded label pair, an encoded separator followed by whitespace before the value, or a whole URL that was percent-encoded once or twice is masked exactly like its plain form, with the mask placed on the original escaped bytes and the surrounding diagnostic words kept byte for byte. A known key prefix right after a slash (`GET /sk-…`, a multi-segment request path, or its encoded form) is now masked too, while segments inside a `file:` address that starts its own token (a `file:` buried inside a request path or query value does not count), short directory names that merely start with a prefix, and random-looking path segments are left alone. The configured-address display and the upload check read one value judge: a query value is masked or reported when its key is a credential label, its value is credential-shaped or carries a nested credential (including encoded layers and `+` as a space), and readable word chains under non-secret keys such as `deployment` stay visible on screen but still count as credentials for upload. A sixty-eight address pool (including single-slash spellings, which are rebuilt from the parsed components first, also when a query value holds a nested address), a seventeen query pool, the rule that anything the configured-address display would mask is also reported as a credential for upload, idempotence on dot, space and unfolded exits and the absent-input rules are pinned, and when a terminal reference tree is present both functions are compared with the terminal's own implementation byte for byte. |
|
|
536
|
+
| `scripts/run-display-credential-reach-test.mjs` | The check for how far the display credential masking reaches. Percent-encoded text is read through decoded views inside the same single scan (up to three layers, on the raw text and on the painted screen text), so an encoded separator such as `token%3D<key>`, an encoded label pair, an encoded separator followed by whitespace before the value, or a whole URL that was percent-encoded once or twice is masked exactly like its plain form, with the mask placed on the original escaped bytes and the surrounding diagnostic words kept byte for byte. A known key prefix right after a slash (`GET /sk-…`, a multi-segment request path, or its encoded form) is now masked too, while segments inside a `file:` address that starts its own token (a `file:` buried inside a request path or query value does not count), short directory names that merely start with a prefix, and random-looking path segments are left alone. The configured-address display and the upload check read one value judge: a query value is masked or reported when its key is a credential label, its value is credential-shaped or carries a nested credential (including encoded layers and `+` as a space), and readable word chains under non-secret keys such as `deployment` stay visible on screen but still count as credentials for upload. A sixty-eight address pool (including single-slash spellings, which are rebuilt from the parsed components first, also when a query value holds a nested address), a seventeen query pool, the rule that anything the configured-address display would mask is also reported as a credential for upload, idempotence on dot, space and unfolded exits and the absent-input rules are pinned, and when a terminal reference tree is present both functions are compared with the terminal's own implementation byte for byte. Since 0.98.0 the configured-address display and the upload check read the whole string as one address: a credential value keeps its tail after quote, bracket, pipe or backslash characters inside the mask, a token split by a tab or line break is judged as the URL parser sees it, a relative reference starting with a slash, question mark or hash has its query values masked, and a host name that looks like a credential label next to a numeric port is shown as written. |
|
|
534
537
|
| `scripts/run-notice-presentation-test.mjs` | Presentation facts for engine notices, shared by every client. The cut primitive (`cutForDisplay`) shortens a sentence to a budget at a sentence boundary, then a clause, then a word, then a character, keeping at least half of the usable length at each step, counting the mark inside the budget, never splitting a surrogate pair or an escape token, and leaving the text untouched when the budget is not a finite number. The facts reader (`noticePresentationFacts`) answers who a notice is for (user and operator from the generated audience table; unknown for any code outside the catalogue), whether it carries something the person can do (present only when `detail.remedy` is an own non-empty string, otherwise unknown; absent is never invented), the remedy text, whether repeats are the same occurrence (only for the seven codes whose contract names an identity key, checked against the installed engine contract; every other case is an event), and a seat class (persistent with a hint, persistent, transient or diagnostic; `NOTICE_SEAT_CLASS`, `noticeSeatClassOf`). `outletReachable` and `planOutletRendering` decide which ways out a sentence may name in a given lane and deployment: reachable ones are written, unreachable ones are dropped, unreadable ones are not asserted. `noticePrintLine` builds the one-line print-mode form `[sema] <sentence> [<code>]` with a per-audience budget, sentence-boundary cut, a full-text pointer only when a session transcript exists, no cut when there is nowhere to point to, a single line, and the shared credential and control-character wash. The suite runs the real built package on each reader, probes hostile inputs and getters, checks the identity table against the installed engine contract, and reads the source to confirm there is one implementation of the surrogate and escape boundary. A cut never lands inside a redaction marker (the marker is removed whole), including on the 8000-character defence bound. |
|
|
535
538
|
| `scripts/run-print-permission-outcome-test.mjs` | The one-line permission outcome for print mode (`printPermissionOutcomeOf`). It reads only the engine's own outcome for a tool call (the gate disposition and its settlement) and answers one of three forms from a single sentence table: the permission mode denied the call (the dontAsk sentence names the mode and offers an allow rule; the bypassPermissions sentence says it could not tell whether the write lands on a protected path; an unknown or missing mode word gets a generic sentence that never echoes the word), nobody could be asked (print mode has no approval channel; the sentence no longer claims a mode), or the mode let a call run without asking (the sentence for the recorded reason, shared with `modeReleaseReasonSentence`). Every other outcome, a missing record and hostile input answer nothing. The notice is declared for the user with or without a way out, and its seat class is read from the shared seat-class table; combined with `noticePrintLine` it forms the line `[sema] <sentence>`. |
|
|
536
539
|
| `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 |
|
|
@@ -538,9 +541,9 @@ guard still cross-checks the table by name).
|
|
|
538
541
|
| `scripts/run-credential-provenance-test.mjs` | The credential-source verdict for a cloud engine target (`credentialProvenance`). A client hands it plain lifecycle facts — what kind of target this is, whether the user had exported a service token before the first write, whether the user is signed in, whether a loopback source was explicitly approved, and whether the local sidecar's ownership was verified — and gets back one of four words (local sidecar, service token, registry, none) or an explicit "could not read". The facts carry only yes/no flags and closed words, never a credential value. Several things are guarded because each one is a way to be confidently wrong. A local engine is judged by the sidecar ownership check alone — never by the token, the sign-in state, or whether the engine is still alive, and the identity facts are not read at all, so a dead engine does not change where its credential came from. A service token wins on a remote or loopback target whenever it was there first, and the verdict records only that it was there: a token string passed in place of the flag is refused and never echoed. The sign-in credential is attached only when the target was pinned through the connect step, and on a loopback source only after explicit approval; a manually exported base address never gets it automatically, even with an approval on record. "None" is a positive fact (nothing is attached) and is never used for "unknown": a missing, malformed, out-of-set, accessor-backed or inherited input reads as unreadable and names the field that could not be read, and a field the decision never reaches is not read at all, so garbage in an irrelevant field cannot change the answer. The check pins every row of the carrying table as a literal (plus two literal grids for the local and the pinned kinds), records exactly which input fields each decision path reads, walks the full product of the closed inputs (384 combinations) against independent if-and-only-if properties for every outcome, throws nothing on hostile inputs, and confirms that the four words are written in exactly one source file. |
|
|
539
542
|
| `scripts/run-seat-settings-default-model-test.mjs` | The seat settings state no longer carries the deployment's default model. That fact has one reader, `liveDefaultModelOf`, applied to the model catalog the seat's `listModels()` returns; a second copy published through the settings store (a different wire field, taken at a different moment) could only disagree with it. The settings state now holds just the engine version and the data root, and a host that still publishes a `defaultModel` key is refused at the outbound gate, with the reason pointing at the catalog. The guard also reads the built declaration for the exact member list, and pushes six catalog bodies (default found by name, by id, absent from the catalog, key missing, key malformed, two rows sharing an id) through the body reader, the seat gate and both default-model readers to show they agree byte for byte. |
|
|
540
543
|
| `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. |
|
|
541
|
-
| `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
|
|
542
|
-
| `scripts/run-standing-rows-test.mjs` | Single judge `standingRowsOf(card, host?)` for the three standing rows on an approval card: allow for this session, don't ask again, and the deployment-list session row. Every row widens what runs without asking, so each one fails closed. An unreadable card, `rememberRefused` from the engine (own or inherited) or a present-but-unreadable value closes all three rows, even when rule offers are present. The session row needs the route fact `allowSessionCarried`, which the live-frame and pending-row paths stamp and the v1 path does not. The deployment-list row needs every ask class to be the deployment-list class, checked against the installed engine's `classifyAsk`. Engine rule offers in either slot give `engine_offer`; an unreadable offer key gives none. A local whole-tool rule is offered only when no engine lane closed the door, the arguments are available, the grant binding reads `none`, a standing allow rule could clear the ask, and the host reports a generic card face and that the rule would be sent; each precondition is flipped on its own. The live-frame, pending-row and v1 paths are exercised end to end, the grant-binding reading is compared form by form with the server's
|
|
543
|
-
| `scripts/run-refusal-machine-reads-test.mjs` | Machine-readable causes on refusal outcomes, shared by every client. A rule the engine's rule compiler refuses is reported in `permissionsDeclarationReport().kept[]` with the compiler's own `code` and `message` for that rule (present only when readable as non-empty text; other reasons never carry them), so a host does not ask the compiler a second time. The refusal note on a live approval outcome (`respondRefusal`) keeps every machine-readable part the shared refusal readers use: the server's `retriable: true`, the not-pending cause word and the refused field from the refusal body (`extra`), and an `unreadable` mark when the thrown error itself cannot be read, so `classifyDecisionRefusal`, `decisionRefusalAskReading` and the close-out check answer the same for the thrown error and for the note. A binding safety stop says where it came from in its code: `binding_missing` when the pending row lacks the binding fields and nothing was sent, `binding_mismatch` only when the engine answered with a binding conflict; the closed set of safety stop codes is frozen, and the shared readers treat the new code as a local stop (not settled, never read as already resolved). |
|
|
544
|
+
| `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. |
|
|
545
|
+
| `scripts/run-standing-rows-test.mjs` | Single judge `standingRowsOf(card, host?)` for the three standing rows on an approval card: allow for this session, don't ask again, and the deployment-list session row. Every row widens what runs without asking, so each one fails closed. An unreadable card, `rememberRefused` from the engine (own or inherited) or a present-but-unreadable value closes all three rows, even when rule offers are present. The session row needs the route fact `allowSessionCarried`, which the live-frame and pending-row paths stamp and the v1 path does not. The deployment-list row needs every ask class to be the deployment-list class, checked against the installed engine's `classifyAsk`. Engine rule offers in either slot give `engine_offer`; an unreadable offer key gives none. A local whole-tool rule is offered only when no engine lane closed the door, the arguments are available, the grant binding reads `none`, a standing allow rule could clear the ask, and the host reports a generic card face and that the rule would be sent; each precondition is flipped on its own. The live-frame, pending-row and v1 paths are exercised end to end, the grant-binding reading (since 0.98.0 the engine's own `boundGrantAbsence` word, not the tool name) is compared form by form on all three paths with the server's three grant-binding functions when a 7.129.0-or-later server fixture is installed, and may only be stricter where the card cannot tell which of them the server applies, malformed, inherited or throwing refusal evidence on the live-frame, pending-row and v1 carriers is recorded on the card as `unreadableEvidence` and closes the rows instead of reading as absent (checked end to end through the real paths); results are frozen, and hostile host facts never throw. Since 0.97.0 the seat request's allow-for-this-session bit `sessionRow` is minted by `toolPermissionRequestAskBits(card, requestId)` from the same premise as the judge's session row and the request id's domain (`live:` and `durable:` carry a session answer; `question:`, `elicit:` and `plan:` do not); an unreadable or refused remember flag never lets the seat show that row, and the crossing never copies the bit from its input — a seat request, or any input carrying a request id, passed back through it gets no bit. |
|
|
546
|
+
| `scripts/run-refusal-machine-reads-test.mjs` | Machine-readable causes on refusal outcomes, shared by every client. A rule the engine's rule compiler refuses is reported in `permissionsDeclarationReport().kept[]` with the compiler's own `code` and `message` for that rule (present only when readable as non-empty text; other reasons never carry them), so a host does not ask the compiler a second time. The refusal note on a live approval outcome (`respondRefusal`) keeps every machine-readable part the shared refusal readers use: the server's `retriable: true`, the not-pending cause word and the refused field from the refusal body (`extra`), and an `unreadable` mark when the thrown error itself cannot be read, so `classifyDecisionRefusal`, `decisionRefusalAskReading` and the close-out check answer the same for the thrown error and for the note. A binding safety stop says where it came from in its code: `binding_missing` when the pending row lacks the binding fields and nothing was sent, `binding_mismatch` only when the engine answered with a binding conflict; the closed set of safety stop codes is frozen, and the shared readers treat the new code as a local stop (not settled, never read as already resolved). 0.98.0: compiler refusal codes are pinned on the allow bucket, the only bucket that still asks the compiler. |
|
|
544
547
|
| `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. |
|
|
545
548
|
| `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. |
|
|
546
549
|
| `scripts/run-seat-session-patch-test.mjs` | The seat's rename and pin verb. The seat gains one optional verb, `patchSession`, that writes the two user-writable fields of a session row — `title` (a string renames; `null` clears back to the automatic title, and an empty string is not a way to clear) and `pinned` (a boolean) — and answers with the row as it is after the write; the host forwards it to the engine, and the seat contract fixes only the shape. The inbound judge `checkSessionRowPatchRequest` accepts exactly the three keys, requires a non-empty `sessionId` and at least one of the two fields that is not `undefined`, and refuses anything else naming the key; accessors are never run; a non-enumerable own key is refused naming the key, whatever its value (a structured clone would drop it, so the gate does not let it pass unread); the request must be a plain record (prototype `Object.prototype` or `null`), so `Date`, `Map`, `Set`, class-instance and custom-prototype carriers are refused; a key found only on the prototype chain does not count as carried. It deliberately does not copy the server's title rules (length and surrounding whitespace): the server judges them and reports a typed error, and the guard pins that choice with two cells so that copying them later is a visible decision. The receipt row goes through the existing outbound gate, which now accepts `pinned` only as a boolean (absent means not pinned or unknown, never an error). Whether the verb can be used is answered only by the seat capability `patchSession`: three readings (reported, not reported, unreadable), three distinct disabled sentences that differ from the other capabilities', and no cross-talk between capabilities. The guard also reads the built declaration file with the TypeScript parser for the verb signature, the request members (only `sessionId` required), `LocalSessionRecord.pinned` and `SeatCapabilitiesState.patchSession`, drives a host stand-in end to end through the public entry points (publish the capability, subscribe, judge, call, outbound gate, structured clone, withdraw), and poisons every key of the new coverage-table row. |
|
|
@@ -549,13 +552,47 @@ guard still cross-checks the table by name).
|
|
|
549
552
|
| `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. |
|
|
550
553
|
| `scripts/run-plan-review-outcome-verdict-test.mjs` | The plan-review decision outcome has one reading rule: each outcome word is given only on positive evidence, otherwise `unconfirmed`. A decision reply that says the review is still pending (`retriable: true`) reads `still_parked` whatever the follow-up status read says; only `completed` reads `took_effect`; `suspended` reads `advanced` only when the follow-up read shows the new pause and that pause has a decision card (an approval or a plan review); `needs_review` reads `still_parked` when the review's instance identity before and after the decision can both be read and match, `advanced` when they differ and the new pause has a decision card, and `unconfirmed` otherwise; a malformed status on the follow-up read falls back to the decision reply like a failed read; every other status (`failed`, `blocked`, `timeout`, `running`, unknown words) reads `unconfirmed` and the sentence states the reading instead of claiming completion. `decidePlanReview` takes an optional `parkedResult` (the paused result the card answers) to supply the before-identity. Pinned with a structural injected client: failed approvals and rejections, the still-pending reply with and without a follow-up read, changed / same / missing / malformed identity, completed with and without the follow-up read, the other status words, the callback and the queue item carrying the same outcome word, and the new option on the shipped declaration. |
|
|
551
554
|
| `scripts/run-durable-decide-leg-test.mjs` | Which durable decision leg a pending row belongs to has one table and one reading. The approval-card gate kinds are `human` and `irreversible_ask` (the only kinds the approval decide route accepts), the plan-review gate kind is `plan_review`, and the review wording set is `plan_review` and `needs_review`; each table is pinned at compile time in both directions against the engine's pause registry and checked again at run time against the installed engine. `durableDecideLegOf(row)` answers one of five words: `approval`, `plan_review`, `other_gate` (a known gate neither decide route accepts), `unreadable` (a gate word outside the registry) and `absent` (no readable gate word on the row: missing, null, empty, not a string, or a throwing read); it never throws. `askParkForeignGateKind` derives from it. Pinned with the twenty-shape verdict matrix, the derived foreign-kind answer, the row classifier, the pending-row lookup (including refusing to fall back to another row when the row a park names is not an approval), the busy-session recovery on non-gate words, the five-word union on the shipped declaration, and no engine-package reference in the shipped output. Since 0.97.0 the same table also decides which approval arm a parked frame goes to: `isToolApprovalGate` agrees cell by cell with `durableDecideLegOf` (an approval gate kind takes any tool that is not the question tool, an unreadable kind falls back to the file-write and shell name families, any other kind takes neither arm); a real-wire park on an approval gate kind for a tool outside those families raises the approval card and sends the decision end to end, while the non-gate word `tool_approval`, a plan-review kind with a write tool, and a non-gate word with the question tool raise nothing. |
|
|
552
|
-
| `scripts/run-engine-caps-landing-projection-test.mjs` | How a client reads the whole capabilities answer of an engine, and how it swaps the probe used for later re-reads. The whole-answer reader has three states: landed (carrying the frozen snapshot that every narrow capability reader and every per-key reader also read), not landed (no probe yet, still in flight, failed, or invalidated), and unreadable (the probe returned a body whose root cannot be read). A key the engine did not report is unknown and never counts as on: the per-key readers are pinned to a written rule applied to the whole answer, and every narrow reader gives its own not-observed reading on an unreadable root. A rotation entry for the same engine with a new probe (for example a client rebuilt after a credential refresh) starts that probe when nothing is landed or in flight, and otherwise only replaces the probe used by later single-flight re-reads without invalidating anything; the guard pins that the next re-read calls the new probe and never the old one, together with the in-flight, re-entrant and invalidation orderings. |
|
|
553
|
-
| `scripts/run-rule-tool-name-judge-test.mjs` | One reading for "does the engine accept this rule tool name": the engine's own tool-name check, installed as a value port and read through `ruleToolNameVerdictOf` (current / retired with its current name / unprefixed namespace / unmintable segment / unknown). The session-rule deliverability check asks it once for every name that survives the plain-text steps (an ordinary name the installed engine has newly retired is withheld as `legacy_tool_name`; a check that cannot answer makes the batch unreadable), the refused-entry removal removes every entry it does not call current and answers `judge_failed` without writing when it cannot answer, the sub-agent tool-list pre-check reports retired, unprefixed and unmintable names from it, reports an entry it cannot check as unjudged (
|
|
554
|
-
| `scripts/run-agents-wire-directory-test.mjs` | Agent tool lists
|
|
555
|
+
| `scripts/run-engine-caps-landing-projection-test.mjs` | How a client reads the whole capabilities answer of an engine, and how it swaps the probe used for later re-reads. The whole-answer reader has three states: landed (carrying the frozen snapshot that every narrow capability reader and every per-key reader also read), not landed (no probe yet, still in flight, failed, or invalidated), and unreadable (the probe returned a body whose root cannot be read). A key the engine did not report is unknown and never counts as on: the per-key readers are pinned to a written rule applied to the whole answer, and every narrow reader gives its own not-observed reading on an unreadable root. The same JSON parsed in another realm reads exactly like one parsed here: landing and the snapshot share one plain-record rule, and class instances stay non-records in both realms. A rotation entry for the same engine with a new probe (for example a client rebuilt after a credential refresh) starts that probe when nothing is landed or in flight, and otherwise only replaces the probe used by later single-flight re-reads without invalidating anything; the guard pins that the next re-read calls the new probe and never the old one, together with the in-flight, re-entrant and invalidation orderings. |
|
|
556
|
+
| `scripts/run-rule-tool-name-judge-test.mjs` | One reading for "does the engine accept this rule tool name": the engine's own tool-name check, installed as a value port and read through `ruleToolNameVerdictOf` (current / retired with its current name / unprefixed namespace / unmintable segment / unknown). The session-rule deliverability check asks it once for every name that survives the plain-text steps (an ordinary name the installed engine has newly retired is withheld as `legacy_tool_name`; a check that cannot answer makes the batch unreadable), the refused-entry removal removes every entry it does not call current and answers `judge_failed` without writing when it cannot answer, the sub-agent tool-list pre-check reports retired, unprefixed and unmintable names from it, reports an entry it cannot check as unjudged (sent as written with a warning, like any other deny-list problem) and judges case twins only against a supplied roster, and the request-body assembly rewrites a retired name to the current name it gives only in the deny and ask buckets (allow keeps it). No build-time retired-name table remains in the source. A real engine run that mounts no tool of that name, reading a stored session record, refuses to start exactly for the names this reading does not call current. |
|
|
557
|
+
| `scripts/run-agents-wire-directory-test.mjs` | Agent tool lists carry what the agent definition says. Since 0.98.0 the supported floor's engine (server 7.129.1, engine 7.51.2) only warns about a name in an agent's tool list that nothing answers to, so the agents projection sends `tools` and `disallowedTools` as written — it no longer holds back an agent, drops an allow-list entry the engine's tool-name check flags, or drops an `mcp__` entry whose server the run does not declare — and the run-level exclude-list check `excludeToolsAgainstDirectory` is gone; whether a tool answers is answered by the engine alone. The tool-name check still reports its findings for both lists; an allow entry also gets a warning line, conditional on purpose ("gives the agent nothing unless this run has a tool with exactly that name" — when the run does have such a tool, mounted or in the delegated pool, the engine accepts the entry silently and it takes effect), while a deny entry gets no warning line at all, whether or not a JavaScript host still passes the old withhold switch. `engineToolDirectoryOf` still reads the MCP servers a run declares from two halves the host holds — the request's own `mcpServers` and the servers the deployment declares itself — minted into tool-name prefixes by the engine's own naming function; either half missing reads as not landed, an unreadable half or a missing naming port as unreadable, and an empty pair as an empty directory; the projection no longer reads it, so any directory, or none, yields the same payload. The gate checks every case against the engine judge of the supported floor (the installed engine package, which must be at or above the declared engine floor, and the live server fixture's engine when it is within the floor): every list sent as written must be accepted, with a warning where nothing answers, while an entry with a grammar flaw is still refused. 0.98.0: agent tool lists, names and maxTurns go on the wire as written (the live server fixture names the field), a written `tools` always yields `allowTools`, a retired deny entry is rewritten only when a roster is in hand and does not contain it, and the readiness gate returns an outcome (projection failure / not ready / ok) that recovers once the projection works again. |
|
|
555
558
|
| `scripts/run-cloud-mcp-dropped-notice-test.mjs` | The sentences shown to a person when the cloud configuration leaves MCP server entries out. Each of the five reasons an entry is left out (switched off, a required setting missing, an unsupported transport, more entries than this client passes to the engine, both identity-header settings set) has its own sentence that names the entries; the switched-off sentence is informational and the other four are warnings. The sentences are part of the single wording source `cloudEffectiveNotices`, whose input now also carries the list of left-out entries. A reason outside the known set and an unreadable row (not an object, a reason that is not a string, a throwing getter, a reason or name that only exists on the prototype chain, an index that is an accessor or a hole) get a plain warning that names what can be read instead of being skipped, a list that is not an array or cannot be read (an accessor that is never run, a value of undefined, a proxy that throws) gets one warning saying it could not be read, a missing key adds nothing, and an empty list adds nothing. Names are capped at five with a `+N more` tail, repeated names are shown once, rows without a readable name are counted, and the order is degradation warnings, catalog references, left-out MCP entries (warnings first), budget, runtime caps. Pinned on the shipped declaration (the reason union is exactly the five words) and on the source (the sentence table is typed on that union and looks reasons up by own key). |
|
|
556
559
|
| `scripts/run-tool-card-outcome-test.mjs` | One reading, one sentence, for what a tool result card in the transcript actually says. Three marks decide it: the placeholder mark on the record (the engine sent no body, so the content is an empty placeholder), the engine's positive evidence that the call never ran, and the block's error flag. `readToolCardOutcome(record)` reads them as `normal` (a real result; success or failure still comes from the error flag and is never rewritten), `degraded` with a cause and one sentence (`not-executed` — takes precedence and is independent of the error flag, so a card that looks successful but never ran is caught; `error-without-detail`; `no-result`, where the error flag false is a placeholder and not a success; `unrecognised`, a placeholder value from another version) or `unreadable` (not a tool result record, or the placeholder position cannot be read — never read as normal). The placeholder has two minting points that cannot be told apart on the record (a call closed at the end of the turn with no end frame, and a real end frame without a body), so the no-result sentence states only what both share. Pinned on records produced by the real pipeline in both lanes (the transcript row and the print-lane frame answer the same), a read-failure matrix that never throws and never reads as normal (accessors are not run, prototype-chain keys do not count), four distinct sentences with no internal names or tool-family names, and the source (the sentence table is typed on the cause union, the evidence mark goes through the existing strict-true reader, the placeholder constant is imported not re-written, and each sentence has exactly one minting site in the shipped output). |
|
|
557
560
|
| `scripts/run-approval-settled-single-source-test.mjs` | Whether an approval already has an outcome is recorded once per process, in the package's approval-holder registry, at two points only: when the live card's holder settles (the decision was delivered and acknowledged, or the interruption denial it sent was accepted) and when the engine's close-out frame for that approval arrives. Every path that could put up a card for the same approval reads that one record first: the in-stream frame path, the suspended-ask path (same entry point), and a host's own durable route through `joinApprovalHolder`, which now answers `joined` for an approval that was already answered or closed by the engine even after the holder let go (an approval the engine moved to the parked queue still answers `free`, since it remains open there). Cells cover the host's durable route after a delivered answer and after a close-out with no card ever shown, the parked negative control, the suspended-ask path writing and reading the same record, a new bridge still recognising a settled approval, the replay verdict reading only this record (a host-supplied map is ignored), the outcomes that are not recorded (a delivery failure including 404, a withdrawn card, a refused edit, an interruption denial that failed, threw or was answered with the parking reply, or that a parked close-out overtook), the write happening at the acknowledgement rather than when the card is answered, and the 4096-entry bound |
|
|
558
561
|
| `scripts/run-server-7126-pick-test.mjs` | Checks for the 7.126.0 server and SDK 21.1 pickup. A submission refused because its idempotency key was already bound to a different request body is classified by one code check on every leg (thrown errors and the refusal in the stream's final frame), and the result frame carries the package's own sentence. The engine's approval card word is read through one closed-set reader and reaches the approval card from both the live frame and the pending row; a missing or unknown word stays missing. The run's failure detail is read as the server's three-arm union plus an unrecognized arm. The two decision doors' gate-kind tables are the SDK's own objects, and the retired hand-written copy is gone from the root entry. The retired team-review request keys are refused at assembly, and allowTools is sent as given. The models list's rejected-candidate record has a three-state reader. Against the 7.126.0 server package it checks the request key closure, the 409 mint site, the engine's card words and the failure-detail registry. |
|
|
562
|
+
| `scripts/run-sentence-tables-projection-test.mjs` | The sentences a person reads for the same fact now have one source, shaped as closed key tables checked against the upstream value sets. The checks cover: persistent-rule origin, scope, match form and revoke outcome (the status by liveness grid, with an unknown-status third outcome that never says whether anything was removed, and liveness words other than the three read as unknown); the card-line readers (the origin line with its quiet table, the reason a card has no remember option, the reason a rule offer still asks); crashed-run leftovers (per-row clauses sharing one table with the counted sentences, whose output is frozen byte for byte, and a not-started row never described as partly landed); the one-line ending of an approval the engine closed without an answer (fourteen causes, never described as a refusal), the accepted-decision line that only claims delivery, and the parked-card note shared by the replacement and reopened cards. A parked frame is never described as an ending; a crashed-run row sentence takes the projection and the row and trusts only the projection's bucket. Every table is a literal that carries its own `satisfies Record<…>` clause; a word outside a table, an absent word or a non-string yields a neutral sentence or `undefined` and never echoes the word; readers do not run accessors or throw on proxies. It also pins that no local copy of the server's rule-count cap is minted and that the permission-mode default half of the `policy` origin sentence is present. |
|
|
563
|
+
| `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. |
|
|
564
|
+
| `scripts/run-engine-url-transport-test.mjs` | Engine address handling in one place. `normalizeEngineBaseUrl` trims outer whitespace, strips every trailing slash and keeps the user's spelling; it refuses, with one of five closed reasons (`empty`, `scheme`, `unparseable`, `userinfo`, `query_or_fragment`), anything that is not a plain http(s) base address. A query or fragment is refused even when empty, because request paths are appended to the base as plain text and would end up inside the query. `engineUrlTransportVerdict` lets `https:` through and `http:` only for a host the settings contract's narrow local-host check accepts, the same check that decides whether outbound credentials are dropped, so the credential fence and the plaintext gate cannot disagree about one host; anything else is refused as plaintext to a remote host or as not an http(s) address, and an address it cannot read is refused. Neither function returns message text or echoes the input. The gate checks every refusal reason and success form, the two addresses the terminal and the desktop shell used to answer in opposite ways, that the local-host reading has a single call site over the settings contract's own function and never touches the wider routing check, that the verdict matches the installed settings contract on a host table, that hostile input never throws and a very long run of slashes stays linear, and it compares the package with copies of both shells' former logic on a generated corpus: the package accepts exactly what both former implementations accepted, with the only disagreements between them falling into two registered classes. The cloud configuration's session-mirror address is read through the same normaliser: well-formed addresses come out byte-identical to the former trailing-slash trim, an address the normaliser refuses is passed on as read with its `required` flag intact rather than dropping the mirror, and a long run of slashes stays linear. |
|
|
565
|
+
| `scripts/run-submit-reopen-test.mjs` | A streaming submit whose connection closes before the run is bound is reopened exactly once with the same idempotency key. `submitStreamWithBoundedReopen` calls the SDK's streaming submit itself, so the key it mints and the response hook that reads the run id header cannot be forgotten by the host; the host passes the request and its own stream options (its own response hook is called after the package's). It reopens only when every one of these holds, and anything it cannot read counts as a reason not to reopen: no run id header was seen (an unreadable header counts as seen) and no frame was delivered; the caller has not cancelled (an unreadable signal counts as cancelled) and the consumer has not asked the stream to stop; the engine did not answer with a status code; the transport-failure reading is readable; and the failure is a lost connection with no answer according to the package's own reading. Cancellation is checked again after the failure is classified, just before the second send. A second failure is thrown as it is, so there is never a third send, and a close after the run is bound is left to the run-event continuation. A reading reports the key, how many sends were made, and whether the terminal frame was a replay of the earlier request under the same key. The gate drives the real SDK stream through a recording fake fetch and checks the key header on every POST, the POST count (also with an SDK client configured to retry, since its streaming leg is single-shot), each case that must not reopen, the replay reading from both of its sources, and that a consumer stopping the stream before its first frame prevents the second send. |
|
|
566
|
+
| `scripts/run-protocol-name-reads-test.mjs` | Reads of sdk error class names when the machine code is gone, checked in real bundles. The sdk gives every error instance a name taken from its class, and a production bundle's minifier renames the class, so comparing that name to a literal never matches there. The judges now treat a class name as family evidence only on rebuilt errors (plain objects that kept just a `name`), compared against one closed table of literal protocol names through one reader; a real sdk instance is judged by its machine code or status, never by its name. The gate bundles the sdk errors and this package three ways (plain, minified, minified with kept names) and runs real instances, literal rebuilt forms and real instances with the code stripped through eight judge outputs, expecting identical answers in all three bundles; it also pins that the literal names live in one place in the shipped output, that they equal the installed sdk class names, the three-state name reader, and the order in which code and name are consulted. |
|
|
567
|
+
| `scripts/run-thrown-data-reads-test.mjs` | Machine-read slots on thrown errors, read the same way everywhere: only an own data property counts. Four readers (the effective code behind a folded refusal, the scenario refusal reader, the blocked-by reader and the deny-attribution refusal reader) and two siblings used to read a status or code along the prototype chain and run accessors. The gate drives every reader with each trigger shape and each slot placed five ways (own data, prototype chain, accessor returning, accessor throwing, a proxy whose descriptor trap throws) and expects the answer for the slot being absent (an explicit unreadable state for the blocked-by reader), no accessor ever run and no reader ever throwing; real sdk errors read exactly as before, and a syntax-tree pin keeps ordinary property reads of those slots out of the four function bodies. |
|
|
568
|
+
| `scripts/run-turn-failure-presentation-test.mjs` | The failure sentence entry point, the turn failure presentation and the reattach budget. `wireFailureTextsOf` turns anything thrown into a sentence that is safe to show (never empty, never a class name or an object tag, never throwing) plus a log form that is not for the screen. `turnFailurePresentation` classifies a failed turn from wire facts only: ten categories, whether to offer a retry (the server's own retriable flag first), how long to wait (only when the server gave a window, rounded up, never zero), and a host-neutral sentence for the failures that carry no engine message. `runReattachPolicy` decides whether and when to re-attach a run whose stream ended, spending one explicit budget. The gate pins each absence shape, a table of real sdk errors, the own-data-slot reading, and compares the budget against reference copies of the web and desktop loops over a thousand generated sequences. 0.98.0: the seat `error` event superset key is minted only by `seatTurnFailureOf` (field-for-field the turn-failure presentation) and read only through `readSeatTurnFailure`, which validates its shape and returns undefined for anything malformed. |
|
|
569
|
+
| `scripts/run-sandbox-hint-host-test.mjs` | Sandbox degradation hints per host. Each of the five hints is a fact sentence (what the engine refused and why, the same for every host) followed by a way out that depends on the host: a terminal can re-run without the sandbox flag or list profiles with a command, while a graphical host has neither and points at keeping the default environment or choosing another one. With no host given the hint is byte-for-byte what it was before; an unrecognised host gets the fact sentence only. The gate checks the terminal wording against the previous release verbatim, that graphical hosts get the fact sentence and no terminal-only words, that detection does not depend on the host, and that status and code are read as own data properties without ever throwing. |
|
|
570
|
+
| `scripts/run-transcript-identity-projection-test.mjs` | Transcript identity: every id this package hands out for a transcript row, a projected frame, a segment, an attachment event or a per-response message id is a deterministic function of the engine identity it carries, so projecting the same wire events again (a second process, a reconnect replay without a cursor, a resume replay, a desktop replay) yields the same ids and a uuid-keyed transcript gets no duplicates. Driven through the real pipeline with two independent adapter instances and random sources: live and durable event sets each projected twice give equal row uuids, segment ids, segment-end committed uuids, per-response ids and engine references for every row class; a projection joined mid-stream matches the full one for every row whose opening event it saw; a streamed text or thinking row is identified by its first and last contributing events, so two projections that cut a segment at different points (an idle split, a stream ending mid-segment, a rewritten authoritative segment) never share a row id and a uuid-keyed transcript keeps both instead of losing text, while a child process projecting the same events yields the same ids; the `_sema_engine_ref` reference on frames, rows and print-mode tool result frames carries the opening event id plus the run binding only when the host supplies it, and is absent for host rows, old rows and events without identity; `sameEngineRef` answers same / different / unknown and can only answer same when both sides carry an event id; a frame carrying only a stream cursor (`seq`) no longer gets a derived id; terminal rows and result frames are derived from the run id when present; a print-mode tool result frame and the interactive tool result row share one id, the engine event id. An optional segment compares against the previous release build and fails on any unregistered id change or any change to row count, order, class or body. |
|
|
571
|
+
| `scripts/run-inherit-env-intent-projection-test.mjs` | Whether the interactive lane may ask for the user's whole environment in tool shells. The engine says, through one capability word on its capabilities answer, that it keeps its own credentials out of shells it starts; this guard pins the four readings of that word (confirmed only for a literal top-level true; absent, declined with the value's shape, unreadable, or not yet observed otherwise, never read from a getter), the one decision built on it (interactive lane, project context confirmed, word confirmed and identity verified, all four, or the request omits the field with one named reason), and the single sentence clients show when the word is not confirmed. Looser spellings, nested groups and stringly true values must never open it, and the server fixture is checked by generation: one before 7.129.0 does not publish the word anywhere in its release, while 7.129.0 and later mint it at the top level of the capabilities answer, and the real minting function's answer, landed through the capabilities cache, is read by this reader as confirmed or declined by its boolean value. |
|
|
572
|
+
| `scripts/run-approval-hold-scope-test.mjs` | One holder per approval across every card-opening path. The live-card entry, the parked-row decision path and the host's own paths (registered through `claimApprovalCard`) all register in one table and see each other through one symmetric matching rule: same approval id when both sides have one, otherwise the same run and the same call while the holder is still waiting for the human. A later arrival waits for the holder and, when the holder settled the question, sends nothing and gets the holder's outcome. Also checks the superseding-card position (`supersedes`, with the earlier answer disclosed only when the engine did not take it), the parked-row position (`parkRow`, deadline from the row's own SLA wall) and the parking notice function. |
|
|
573
|
+
| `scripts/run-hitl-answer-outlets-projection-test.mjs` | The new decision-chain outlets. `answerPayloadFor` builds the base decision body for both lanes from the one answer object (decision word, session approval, edited input, the deny note behind its capability and length gates) and the package's own lanes use it; a Yes note never reaches the wire while `answerCarriage` answers `none`; the answer-committed event fires exactly once per answered card before the first send. `rememberOfferable` is checked card by card against `standingRowsOf`; `localAllowRuleEnforceable` against the permissions declaration judge. `lateDecideOutcomeOf` is checked against a frozen copy of the previous host-side classification. `RESPOND_DECISIONS` is frozen with exactly three words. |
|
|
574
|
+
| `scripts/run-seat-apply-config-reason-test.mjs` | The seat's model-configuration receipt now carries a closed set of reasons instead of a free-form string. When a host refuses to write the configuration it answers with one of five words — the model catalog on this device shadows the write, the cheap model asks for a different provider, the request was not valid, the file could not be written, or the app cannot change the configuration right now — and the one-line human sentence for each word is minted in exactly one place, so a client renders it instead of keeping its own table. Free-text diagnostics moved to an optional `detail` field that is display-only. The outbound gate refuses any other word, including prototype-chain names and case variants, and refuses a failed receipt with no reason. The guard also reads the built declaration for the exact member list, compiles probes that must fail to type-check, and pushes each word through the gate, a structured clone and the sentence reader to show the renderer reads the same sentence the host chose. |
|
|
575
|
+
| `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. |
|
|
576
|
+
| `scripts/run-assistant-inbox-projection-test.mjs` | One reading of the per-user inbox of parked questions. `readAssistantInbox(assistant)` returns `present` (the rows in exactly the order the server sent them — the package neither sorts nor re-sorts; a missing severity stays missing, it is not a lowest tier and not zero; marks that are only ever sent as true are read as true only, so an absent bidi mark is never "checked clean"; malformed rows are dropped and counted; an empty inbox is a real empty list), `unavailable` (unauthorized; unreadable — a body without the array is never rendered as an empty inbox; or failed, with the status and code) or `not-reported` (the route itself answered 501, or no assistant connection was handed in — the whole panel stays hidden). Whether the lane exists is asked of the call's own answer, never of a capability flag, and the background view's assistant leg uses the same judge. Pinned on the real client against stubbed fetches, on fixed order cases with a syntax-tree scan that the source never sorts, and against the fixture server's key list and sort law. |
|
|
577
|
+
| `scripts/run-control-preempt-test.mjs` | Graceful yield for a running run. `ControlRouter.preempt()` asks the engine to pause the run at its next clean turn boundary (the run can be resumed later), unlike cancel, which kills it. A 202 is not a success by itself: the answer's status word decides — preempting means asked; running means this run cannot yield and nothing changed. At most once, never re-sent; failures use the same judge as steer and cancel. When the engine's yield route is unavailable (it answers 501, or there is no assistant connection) the default falls back to cancel — a hard stop, reported as such — and the host can instead choose to just report it. Each outcome has one neutral sentence, and none says the run has yielded. Pinned on the real client (even with retries enabled there is exactly one send), on a pool of thrown values judged by the shared at-most-once judge, on import-cycle smoke runs and on the fixture server's minting points. |
|
|
578
|
+
| `scripts/run-device-registration-test.mjs` | Device registration verbs and the identity-mismatch refusal. `issueEnrollToken`, `enrollDevice` and `revokeDevice` go through the client's devices slice with the same closed refusal table and at-most-once judge as the rebind calls, and every outcome is a value, never a throw. The one-time enrolment token appears only in the issue call's ok outcome — never in another outcome, in a sentence, or in a log call (a refusal sentence that echoes the token is dropped from the outcome). The enrol body is exactly the six closed keys in fixed order and nothing else the caller passed; a missing token, public key, platform or workspace root is refused locally with no request; the returned device id is accepted only if the host's own shape judge says so. A revoke is ok only if the answer echoes the same device id and the revoked status. The identity-mismatch refusal has one reader for its two forms (the machine code, and the bracketed prefix in a tool error text), and the bound device id is read only from structured fields, never from prose. Pinned on the real client with stubbed fetches (writes are sent exactly once even with retries enabled), on a plain-object pool of thrown values, on a syntax-tree scan, and on the fixture server's minting points. |
|
|
579
|
+
| `scripts/run-durable-seq-reading-test.mjs` | One reading of a frame's resume position. `durableSeqOf(frame)` returns the cursor value of an event: the stream id comes first (a string or a number; a malformed id hides a valid sequence number, so the cursor does not move), and only when there is no id does a numeric sequence number count — except on progress, notification and task-start events, whose sequence number is a different axis (a sub-agent generation or a stop-cycle batch counter) and never a cursor; a frame with neither stays at the previous cursor. The host app's own copy of this reading answers differently on nine shapes; the guard freezes that copy and pins the exact disagreement set, extracts the real function from the host app tree when it is present, and pins that the gate ledger's cursor is fed by this one function only. |
|
|
580
|
+
| `scripts/run-task-notification-xml-read-test.mjs` | The single-notification read for transcript task-notification XML. The package already had a batch reader that turns a saved transcript into de-duplication keys; a host that needed the task id and status of one queued notification wrote its own scan. Both now read through one block parser: a block is a `<task-notification …>…</task-notification>` pair, the id and status are the first `<task-id>` / `<status>` pair inside it, un-escaped, and a field that is missing, empty or unterminated is simply absent. The parser is linear: unterminated openers, openers with whitespace and no closing bracket, and look-alike tags no longer cost time proportional to the square of the input. The gate pins the single read against the render function over a generated pool (with escapes, whitespace and newlines), compares the batch reader with a frozen copy of the previous regular-expression implementation over thirty thousand generated texts in three message shapes, runs four adversarial inputs of hundreds of thousands of candidates in a child process with a timeout, and checks by syntax tree that neither entry point contains a regular expression of its own. |
|
|
581
|
+
| `scripts/run-narrow-readers-export-test.mjs` | Four narrow readers for foreign objects, exported from the package root. Hosts used to write their own copies: a three-field reader for thrown errors (status, error code, message) existed three times with identical text, and the one-line checks for a non-empty string or a finite number appeared three to eight times per host. The package already had one reading rule for this, reading only the object's own data properties without running accessors, but it was internal. The four exports are the own-data string reader, the own-data finite-number reader (zero is a value, NaN and infinities are absent), a guard that turns a throwing read into an absent one, and a three-field reader for thrown values whose message is never empty and is produced by the same function as the user-facing failure sentence. The gate drives each reader with every absence shape (missing, inherited, accessor, proxy trap, wrong type), compares the three-field reader with a frozen copy of the host-side code over six thousand generated JSON-shaped errors (the only deliberate difference is the sentence used when no message exists), checks by syntax tree that the three-field reader is a projection of the existing snapshot reader, and compiles a small consumer under the strict settings to prove that an `unknown` from a catch clause can be passed straight in. |
|
|
582
|
+
| `scripts/run-plan-review-offered-mode-test.mjs` | The offered-mode read for plan-review cards, and the retirement of the synchronous reopen overload. A plan-review card is shown either as the old two-choice card or as a three-choice card whose two Yes options each carry a mode to switch to after approval. Reading the user's answer correctly needs the labels that were actually shown, otherwise a three-choice Yes is read as dismissed. The package knew what it had shown but cleared it on the first answer and exposed no reader, so a host kept its own table of what it had armed and re-derived the rules around it (reopened cards are always two-choice, a missing entry means two-choice, an armed card is not rewritten by a replay). One reader now answers `three_choice`, `two_choice` or `unknown` from positive evidence only: the labels on the card or frame when given, then the question id (reopened identities are two-choice by construction, otherwise a bounded record the package keeps when it publishes the card, not cleared by an answer). `unknown` is never read as two-choice by the reader itself. Reopening a card now always returns a promise and requires the presentation-receipt window; a call without it presents nothing and resolves as not reopened. The gate drives the real arm, answer, dismiss and reopen paths, compares the reader with a frozen model of the host-side table over a hundred and sixty random steps across three tasks, and pins by syntax tree and a strict compile that the overload is gone. |
|
|
583
|
+
| `scripts/run-context-usage-projection-test.mjs` | The one narrow reader for the engine's context-usage payload. The payload (used tokens, window, compaction threshold, and the prompt's section rows) was read in several places: the projection filtered section rows, but the next stage passed any array through, so a hand-built frame fed straight to the adapter put injected keys and invalid rows on the event; the terminal re-implemented the finite-number checks on raw frames to serve both the interactive and the print lane; the web client re-narrowed the rows on the already-projected event. The reader accepts the raw frame, the internal arm or the chrome event, because the four fields have the same names and shapes in all three, and returns each field independently: numbers must be finite (absent is never turned into zero), rows need a non-empty id and a finite token count, rows that fail are dropped one by one, an all-bad list leaves the key absent, and a payload with nothing readable returns nothing. The projection and the chrome arm both call it. The gate checks the table of shapes, accessors and proxies, compares the reader with a frozen copy of the web client's reader over eight thousand generated payloads, runs the same frames through the projection and straight through the adapter to prove the two entries agree, and checks by syntax tree that neither entry narrows rows itself. |
|
|
584
|
+
| `scripts/run-tool-gate-view-projection-test.mjs` | The gate-record view on tool-result rows. The engine's tool_end frame carries a gate record (the disposition: allowed, or denied by which layer; how the wait was settled; who asked), and the package has one reader and a set of judges for it, but the projection only folded it into a seven-word category on the transcript row, so the ten denial layers and the settlement words stopped before the card and a host had to intercept the raw frame ahead of the adapter to read them. The tool-result row, in both the interactive transcript and the print-mode frame, now carries the same view under one envelope key, present only when the reader can read it and never inside the content block that is sent back to the provider. Absence says nothing: a missing view does not mean allowed. The gate runs five gate records through the real projection and adapter and compares the key with the reader's own result, checks the absent shapes (no record, malformed, unreadable disposition, parked), the print-mode frame, hand-built arms in both view and raw shape, idempotence of the reader over six hundred generated records, and that the five public judges answer identically on the row's view and on the raw frame's view. |
|
|
585
|
+
| `scripts/run-connector-status-projection-test.mjs` | Reader-minted connector status. `connectorStatusOf({ name, mountFace })` reads one answer of `readMcpCapabilities` (this deployment's default MCP server list, each server dialed live) through the same per-name row lookup the MCP detail card uses: a fully read list without the name means unmounted; exactly one row whose engine reachability observation is unreachable means unreachable; exactly one reachable row means the connector speaks for itself (its own authorization state is not read here); everything else is unknown, with the step where the reading stopped. The dial verdict on the row is never read, because a failed dial includes servers that did answer (a protocol error, a 401). Pinned: the two states and their shape against the real bytes of the contract package's status schema, both directions; every combination of list shapes, reachability shapes and dial verdicts against an independent oracle; an unreadable list or name never becomes unmounted (the guard proves it catches a reader that folds them); per-key throwing inputs read once each; the ten distinct English sentences of `connectorStatusLine`, the same for every language tag; an end-to-end run through the real client, including that a caller-probe answer is never read as this deployment's list (it is rejected at compile time and reads as unknown at run time); a single minting point in source; and the supported server's and engine's minting points. |
|
|
586
|
+
| `scripts/run-hitl-single-reading-projection-test.mjs` | One reading per fact on the approval surface. When a park carries the identity of the call it holds, the collateral marking of aborted sibling calls reads that identity directly, so two calls of the same family are told apart, while frames from a sub-agent lane or naming a different gate are left unmarked; without the identity the earlier inference is unchanged. The session-approval fallback re-sends a plain approval only for the dedicated refusal codes. Reopening a plan-review card with the reused question id and an injected delivery path cannot be expressed in types and is refused at runtime. The suspended-ask tracker reports only appeared and gone. The sub-agent test reads the explicit marker only. An unreadable pending section escapes into the poll-leg breaker while a partly readable one commits with a dropped count and still owes a complete read. Tool-start argument snapshots live in one table. A panel row keeps its first binding. The fleet description filter no longer hides tools named Task or Fork. The gate record view carries which seats were present but unreadable, so the denial-kind reader answers the same on the view as on the raw frame. Both tool-output text readers share one reading. |
|
|
587
|
+
| `scripts/run-row-sidecar-projection-test.mjs` | Row sidecar facts. A terminal that the server marks as an idempotent replay (this request did not start a new run) carries `_sema_replay: true` on the result frame, on synthesized terminal rows and on rows derived from them, read through one guarded reader; `isReplayMarked` reads the fact; exactly one required `result_replayed` chrome event carries the user-facing sentence per replayed terminal, whatever the row count. Tool result rows carry four more facts on the envelope (collateral abort, never started, approval window expired, denial-limit window expired), minted by one function shared by the interactive transcript and the print frame and read with `toolResultRowFlagsOf`; `TRANSCRIPT_ROW_SIDECAR_KEYS` lists every row key a host must keep when it rebuilds rows. The gate pins the readers by type checker and syntax tree, the key order, the one-event count, the sentence, the consumer contract, the shared mint and the identity range. |
|
|
588
|
+
| `scripts/run-tool-row-terminal-projection-test.mjs` | A tool call held by a park gets no terminal row from its park-time close frame: the call is not settled yet, so the projection takes its card off the open-card table, records it on the adapter instance (with the parked run and checkpoint id, delivered by an internal terminal-fact arm that the stream emits before the result frame) and emits one `tool_parked` chrome event naming the call and the transcript uuid of its start row. Its terminal row is minted only by the executing leg: a later frame for the same call id claims the original card only with a witness (the host declaring `continuesPark` for the stream it is projecting, or a continuation the question bridge spliced into the same projection); without one it is a new call with its own start row. A claim also needs the parked start row to still be the latest start row the instance minted for that call id (whichever of the two was recorded first), refuses a frame naming a different tool or bound to a run outside the park and its declared continuation, and keeps the original row identity while taking the continuation's input. Collateral calls, unreadable frames and a park the package itself settled (Esc on a parked card) keep their terminal rows; the print lane uses the same judge; the registry travels with `exportLedger` / `importLedger`; a run that ends without parking retires its entries; and results pair with the nearest preceding unpaired start row of the same call id. |
|
|
589
|
+
| `scripts/run-session-policy-grammar-test.mjs` | Reading and rewriting a session’s own rule record never changes which rule grammar its entries are read in. The record carries a grammar stamp: the read exposes it (absent means a record stored before the stamp, which the engine reads in its earlier grammar — never folded into a number), and both rewriting paths — tightening and removing refused entries — send the same stamp back. When a record has no stamp and a rewrite would carry back one of its allow entries written in a tool’s own rule form (such as Bash(git:*)), which the earlier grammar reads as allowing nothing and the current grammar reads as a working allow rule, the rewrite sends nothing and returns a machine-readable refusal naming those entries — for every caller identity alike, since an identity allowed to widen rules would otherwise have the write accepted. An engine refusal of the same kind maps to the same outcome, and a write receipt that comes back without the stamp it was sent with, or with an older one, is never reported as written. The guard runs each case for both caller identities against a modelled engine and, when the matching server build is installed, against the real engine write judge and the server’s error mapping, recording the pre-fix widening as its failing baseline; over a generated corpus it checks that every entry the engine reads differently in the earlier grammar is one the package refuses to carry back, and that every extra entry it refuses is one the engine would refuse to write anyway. Malformed stamps, accessors, per-key throwing proxies and a polluted prototype are refused rather than read as absent. |
|
|
590
|
+
| `scripts/run-message-catalog-test.mjs` | The package's message catalog. Every user-facing sentence the package mints is meant to live in exactly one place, a key in the catalog: `EN_MESSAGES` holds the English text and defines the key set, `ZH_MESSAGES` holds the Chinese text, and both tables are frozen. `translate`, `createTranslator`, `msg` and `renderMessage` pick one language per call; a host only chooses the language, and `resolveUiLanguageFrom({ saved, hostTags })` resolves it from a saved choice, then the host's ordered locale tags (the first non-empty tag decides, including `C`), then English. The gate checks that both languages have the same keys and the same placeholders, that no value is empty or left untranslated, the key grammar and domains, that no key collides with an upstream code, that backticked commands and registered terms are kept verbatim, that every value survives the display sanitizer, that the keys taken over from the terminal are byte-identical to a pinned snapshot, that the tables are frozen, that translation never throws (unknown language, inherited names, missing keys, missing parameters, a failing miss ledger), that nested references render in the outer language and are recognised across two copies of the package, and that the language resolver matches the terminal's previous resolution chain on six thousand generated inputs. |
|
|
591
|
+
| `scripts/run-bare-sentence-ratchet-test.mjs` | A ratchet on bare English sentences in the package source. New user-facing sentences belong in the message catalog, where they can be rendered per language; the existing ones are being moved in batches. The gate evaluates string expressions statically before counting words (string literals, templates, `+` chains, `.join` on literal or constant arrays, array spreads and `const` bindings followed across modules), so the same sentence written nine different ways is counted as one sentence, and it records, per file, how many times each normalized sentence occurs. A new sentence, or one more occurrence of an existing sentence, fails; fewer occurrences are reported so the baseline can be lowered. Developer diagnostics (thrown errors, console output), property names and type positions are not counted. A second check keeps the set of exported English sentence constants from growing: new sentences must be functions, so a language can be passed. |
|
|
592
|
+
| `scripts/run-subagent-model-word-test.mjs` | The subagent model word resolver. `resolveSubagentModelWord({ word, liveCatalog?, local? })` returns the word to send as `agents[].model` and a closed reading, never a sentence: catalog names are sent as written, tier words, provider aliases and `best` are sent in canonical lower case so the target engine expands them with its own tier table, a provider model id is replaced by its catalog name, a case-insensitive match is accepted only when it is unique, and anything else is passed through for the engine to judge. When the host asserts that it started the engine itself and passes that engine's active tier group (`local`), and that engine's catalog is also known, the reading also previews where a tier word lands, or why it cannot land (inactive tiers, nothing bound down the chain, a binding missing from the catalog); an unreadable tier table or a missing catalog means no verdict, never an inactive one, and the preview never changes the request. `subagentModelWordProblemLine` renders the one sentence for those cases through the message catalog. The gate compares the preview with the installed engine's own tier expansion on two thousand generated cases, proves the comparison catches five known wrong orders, and checks canonical spelling, absence, hostile inputs, frozen readings and the sentences in both languages. |
|
|
593
|
+
| `scripts/run-retry-status-notice-test.mjs` | The retry-line sentence. `retryStatusNotice(status, ctx?)` turns a retry status (or `null` when the row should be cleared, which gives no sentence) into one sentence, a machine-readable way out (`switch_model`, `wait`, `fix_request`, or none) and the phase of the line. The gate pins the full truth table (arm × terminal × provider-condition word × HTTP status × attempt counts) against an independent oracle and a set of literal sentences; the first-token wait reads as a wait and never as an error; absent or invalid positions are read as absent (no zeros, no invented reasons, no quota wording); words outside the engine's closed set read like `unknown` and are never echoed; the way out is given only where it applies; unknown arms read as the default arm; per-key throwing proxies, a single read per key, a frozen result and purity; the sentence table's keys equal the engine's `PROVIDER_CONDITIONS` and no second table exists; sentence hygiene; and an end-to-end run through the real wire projection. |
|
|
594
|
+
| `scripts/run-event-leg-compose-test.mjs` | Event leg composer: one table decides, for each of the three leg kinds (live stream, durable attach, continuation opened by the question bridge), which stages a frame passes between the SDK event stream and the projection, and the composer only takes host ports. The frame-level stages (run and session adoption, sub-agent owner stamping, in-stream question / elicitation / tool approval frames) have pairwise disjoint frame-type domains (recomputed from the source) and run once on every stream, including each continuation the bridge opens. The run a frame belongs to is recorded per frame object and taken as a pair from one source: on a live stream the terminal frame's run wins over the header and the first frame (an idempotent replay found at row creation), a refused submission is reported with the candidate run id it never created and leaves its frame unbound, frames before a run id is known and frames a host copies without the inheritance helper are unbound, and nothing ever falls back to the most recent binding. A type-checker scan traces every place in the bridge modules where a frame flows (yield, return, frame arrays, frame tables, frame parameters) back to its source and accepts only an untouched reference or the inheritance helper; nine planted shapes (literal copies, Object.assign, structuredClone, helpers with inferred return types, concrete frame subtypes, a frame escaping through a local object, a file in a subdirectory) prove it bites. Driven through the real question bridge: a continuation's frames are stamped and swallowed stage by stage, the approval countdown is shown on the live stream and fully dropped on replayed streams, a hand-off moves frame bindings, owner stamps and the bound callback to the new run, rewritten, collateral and synthesized frames answer the run they came from, the terminal port fires exactly once per released terminal frame and never for the parked one, the bridge dependencies are read from the host object as-is (lane accessors not executed, a throwing accessor does not break the stream, private-field getters keep their receiver), and nothing is persisted once the host says the leg is stale or its staleness check cannot be read. Missing question / elicitation / tool-approval ports fail at assembly with a named host_port_missing error; non-HITL frames pass through unchanged and in order. |
|
|
595
|
+
| `scripts/run-upstream-type-shadow-test.mjs` | Same-name types across this package and its runtime upstreams. Every type name exported from the package root that is also exported as a type by a runtime upstream (the packages listed under `dependencies` and `peerDependencies`, including the typed subpath entries of their `exports` maps, with wildcard entries expanded from the files on disk) must resolve to that upstream's own symbol, re-exported. A local declaration under the same name fails the guard even when its shape matches today, because two declarations under one name drift apart on the next upstream change, and so does a re-export of a different upstream symbol under that name; readings this package mints carry a different name (suffix `Reading`). Module augmentations of a runtime upstream from the package sources fail as well. The only registered exception is an upstream that lags behind the package that owns the vocabulary: the row names the owner, which must be a declared dependency other than this package and whose same-name type must be declared inside that package, and the guard re-proves on every run that this package's type equals the owner's while the upstream's type is a strict subset of it. Rows cover one name and one upstream entry each, fail once they are past their due-by version (at most two minor lines ahead), once the premise no longer holds, or once the name no longer collides. Two types are the same only if each is assignable to the other and their structural fingerprints match, so one extra optional member, a readonly modifier or a wider return shape counts as a difference. The ruler proves itself on an in-memory corpus of package and upstream modules and on two planted declarations checked against the real upstream name set. |
|
|
559
596
|
|
|
560
597
|
Each suite carries a floor that only moves up — a refactor that stops executing a group of
|
|
561
598
|
assertions is a failure, not a quieter pass. Guards anchor on the **installed artefact's content**
|