@sema-agent/client-core 0.112.0 → 0.113.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 +153 -0
- package/README.md +37 -24
- package/dist/adapt/arms.js +7 -1
- package/dist/adapt/textStream.d.ts +2 -0
- package/dist/adapt/textStream.js +27 -1
- package/dist/adapter/activeRunSelfHeal.d.ts +7 -5
- package/dist/adapter/activeRunSelfHeal.js +38 -66
- package/dist/adapter/downstream/eventToSdkMessage.js +4 -0
- package/dist/adapter/downstream/terminalToSdkResult.d.ts +8 -0
- package/dist/adapter/downstream/terminalToSdkResult.js +36 -1
- package/dist/adapter/runStream.d.ts +2 -12
- package/dist/adapter/runStream.js +46 -19
- package/dist/agentSession/engineAgentRegistryRead.js +2 -1
- package/dist/agentSession/engineSessionBackgroundStop.js +2 -2
- package/dist/attachmentOutcome.d.ts +35 -0
- package/dist/attachmentOutcome.js +192 -0
- package/dist/attachmentPathPlan.d.ts +67 -0
- package/dist/attachmentPathPlan.js +203 -0
- package/dist/attachmentRoute.d.ts +9 -1
- package/dist/attachmentRoute.js +23 -2
- package/dist/declared/supportFloorDeclared.d.ts +1 -1
- package/dist/declared/supportFloorDeclared.js +1 -0
- package/dist/deviceRebind.js +2 -3
- package/dist/displayUntrusted.js +126 -19
- package/dist/engineAddress.d.ts +1 -1
- package/dist/engineAddress.js +9 -6
- package/dist/engineErrorCodes.d.ts +3 -0
- package/dist/engineErrorCodes.js +4 -0
- package/dist/engineNoticeCodes.d.ts +34 -4
- package/dist/engineNoticeCodes.js +133 -6
- package/dist/engineUnavailable.d.ts +4 -2
- package/dist/engineUnavailable.js +99 -17
- package/dist/fleet/fleetLedger.d.ts +9 -3
- package/dist/fleet/fleetLedger.js +112 -16
- package/dist/fleet/fleetProjection.d.ts +5 -0
- package/dist/fleet/fleetProjection.js +9 -1
- package/dist/gateVocabulary.d.ts +7 -0
- package/dist/gateVocabulary.js +40 -14
- package/dist/generated/engineFactTables.d.ts +4 -2
- package/dist/generated/engineFactTables.js +198 -2
- package/dist/generated/engineNoticeTables.js +56 -8
- package/dist/generated/settingsSchemaFns.d.ts +7 -0
- package/dist/generated/settingsSchemaFns.js +11 -0
- package/dist/generated/settingsSchemaHash.d.ts +1 -0
- package/dist/generated/settingsSchemaHash.js +58 -0
- package/dist/generated/settingsSchemaOwnKeyRead.js +6 -1
- package/dist/generated/settingsSchemaTables.d.ts +3 -2
- package/dist/generated/settingsSchemaTables.js +14 -0
- package/dist/generated/settingsSchemaTightenVerdict.d.ts +1 -0
- package/dist/generated/settingsSchemaTightenVerdict.js +125 -0
- package/dist/headlessPermissionModeWire.d.ts +7 -2
- package/dist/headlessPermissionModeWire.js +77 -8
- package/dist/hitl/approvalDecisionDoor.d.ts +24 -0
- package/dist/hitl/approvalDecisionDoor.js +77 -0
- package/dist/hitl/approvalDelivery.d.ts +6 -0
- package/dist/hitl/approvalDelivery.js +57 -7
- package/dist/hitl/approvalResolution.d.ts +6 -3
- package/dist/hitl/approvalResolution.js +24 -2
- package/dist/hitl/approvalsFeed.js +1 -1
- package/dist/hitl/automaticAnswerVerdict.d.ts +1 -1
- package/dist/hitl/decisionSend.d.ts +7 -4
- package/dist/hitl/decisionSend.js +90 -41
- package/dist/hitl/frameRouter.d.ts +3 -3
- package/dist/hitl/frameRouter.js +5 -0
- package/dist/hitl/hitlBridge.d.ts +1 -0
- package/dist/hitl/hitlBridge.js +17 -4
- package/dist/hitl/hitlHostSurface.d.ts +3 -2
- package/dist/hitl/hitlHostSurface.js +5 -3
- package/dist/hitl/legacyApprovalShapes.d.ts +142 -0
- package/dist/hitl/legacyApprovalShapes.js +1 -0
- package/dist/hitl/livePendingAsk.d.ts +4 -4
- package/dist/hitl/livePendingAsk.js +5 -3
- package/dist/hitl/parkResolver.js +1 -1
- package/dist/hitl/persistedRulesWire.js +2 -2
- package/dist/hitl/planReviewCard.d.ts +4 -2
- package/dist/hitl/planReviewCard.js +66 -1
- package/dist/hitl/planReviewWire.d.ts +34 -2
- package/dist/hitl/planReviewWire.js +326 -150
- package/dist/hitl/sessionPolicyWire.js +16 -7
- package/dist/hitl/subagentSource.d.ts +1 -0
- package/dist/hitl/subagentSource.js +11 -0
- package/dist/hitl/toolApprovalWire.d.ts +34 -10
- package/dist/hitl/toolApprovalWire.js +182 -33
- package/dist/hookMatcherVerdict.d.ts +2 -1
- package/dist/hookMatcherVerdict.js +3 -2
- package/dist/hooksReaderGenerations.d.ts +8 -0
- package/dist/hooksReaderGenerations.js +49 -0
- package/dist/hooksWireCaps.js +43 -18
- package/dist/hostSettingsSource.d.ts +1 -1
- package/dist/hostSettingsSource.js +2 -2
- package/dist/hostTurnSettings.d.ts +2 -2
- package/dist/hostTurnSettings.js +87 -47
- package/dist/humanTurn.d.ts +2 -1
- package/dist/humanTurn.js +44 -13
- package/dist/humanTurnRead.d.ts +7 -0
- package/dist/humanTurnRead.js +23 -14
- package/dist/humanTurnSentences.js +3 -0
- package/dist/i18n/messages.en.d.ts +93 -5
- package/dist/i18n/messages.en.js +100 -12
- package/dist/i18n/messages.zh.js +110 -15
- package/dist/index.d.ts +29 -15
- package/dist/index.js +17 -9
- package/dist/liveModelCatalog.js +1 -0
- package/dist/liveQuestionStore.d.ts +2 -0
- package/dist/liveQuestionStore.js +7 -2
- package/dist/localConfig/kernel.d.ts +2 -1
- package/dist/localConfig/kernel.js +12 -18
- package/dist/localConfig/mcp/edit.js +25 -27
- package/dist/localConfig/mcp/servers.js +5 -8
- package/dist/localConfig/modelPool.d.ts +6 -1
- package/dist/localConfig/modelPool.js +104 -27
- package/dist/localConfig/modelPoolWords.d.ts +2 -0
- package/dist/localConfig/modelPoolWords.js +1 -0
- package/dist/localConfig/settingsWrite.js +1 -14
- package/dist/localConfig/skills.js +9 -16
- package/dist/localEngineSpawn.d.ts +29 -1
- package/dist/localEngineSpawn.js +62 -1
- package/dist/mcpLiveness.d.ts +1 -1
- package/dist/mcpProbeWire.js +2 -2
- package/dist/mcpReconnect.js +2 -2
- package/dist/model/catalogLoader.js +3 -17
- package/dist/model/onboardJudgement.d.ts +8 -0
- package/dist/model/onboardJudgement.js +8 -3
- package/dist/model/onboardSteps.d.ts +19 -0
- package/dist/model/onboardSteps.js +76 -0
- package/dist/notifications.js +20 -12
- package/dist/ownKey.d.ts +4 -0
- package/dist/ownKey.js +61 -0
- package/dist/pathShape.d.ts +14 -0
- package/dist/pathShape.js +44 -0
- package/dist/planReviewCardFacts.d.ts +3 -0
- package/dist/planReviewCardFacts.js +20 -3
- package/dist/planReviewOutcomeCause.d.ts +2 -0
- package/dist/planReviewOutcomeCause.js +12 -0
- package/dist/pluginHooksWire.d.ts +20 -8
- package/dist/pluginHooksWire.js +204 -86
- package/dist/prepareRefusal.d.ts +2 -0
- package/dist/prepareRefusal.js +11 -1
- package/dist/request/taskRequest.js +3 -1
- package/dist/resultFrameKeys.d.ts +1 -0
- package/dist/resultFrameKeys.js +3 -0
- package/dist/resumeRefusalCopy.js +3 -1
- package/dist/retryStatus.d.ts +11 -1
- package/dist/retryStatus.js +15 -4
- package/dist/runTerminal.d.ts +1 -0
- package/dist/runTerminal.js +15 -1
- package/dist/sdkWireTransit.d.ts +1 -1
- package/dist/seatContract.d.ts +12 -3
- package/dist/seatContract.js +24 -13
- package/dist/sessionEngine.js +1 -1
- package/dist/sessionHooksRoster.js +8 -6
- package/dist/sessionIdSegment.d.ts +0 -1
- package/dist/sessionIdSegment.js +1 -3
- package/dist/sessionMemoryErase.js +2 -2
- package/dist/sessionMemoryStatus.js +2 -1
- package/dist/sessionOccupant.d.ts +29 -0
- package/dist/sessionOccupant.js +78 -0
- package/dist/settingsHooksSalvage.d.ts +4 -1
- package/dist/settingsHooksSalvage.js +22 -9
- package/dist/skillOverrideChain.d.ts +3 -0
- package/dist/skillOverrideChain.js +31 -4
- package/dist/skillsWireCaps.d.ts +16 -2
- package/dist/skillsWireCaps.js +47 -20
- package/dist/subagent/agentStateDetail.js +32 -8
- package/dist/subagent/engineCompactWire.js +48 -36
- package/dist/subagent/engineDelegatedPrompt.d.ts +3 -2
- package/dist/subagent/engineDelegatedPrompt.js +65 -28
- package/dist/subagentEditOutcomes.d.ts +9 -0
- package/dist/subagentEditOutcomes.js +43 -0
- package/dist/supportFloor.d.ts +2 -1
- package/dist/supportFloor.js +1 -1
- package/dist/toolResult.js +4 -2
- package/dist/toolRoster.d.ts +1 -0
- package/dist/toolRoster.js +1 -0
- package/dist/usageAnalytics.js +2 -1
- package/dist/userAttachmentEnvelope.d.ts +11 -0
- package/dist/userAttachmentEnvelope.js +7 -2
- package/dist/userAttachmentSeat.d.ts +7 -1
- package/dist/userAttachmentSeat.js +44 -7
- package/dist/wiringInstructionsAttribution.js +8 -2
- package/docs/INTEGRATION-CLIENTS.md +1654 -16
- package/package.json +5 -5
package/README.md
CHANGED
|
@@ -35,7 +35,7 @@ Renamed from **`@sema-agent/wire-cc-adapter`** (0.1.x, deprecated — see *Migra
|
|
|
35
35
|
|
|
36
36
|
## Scope
|
|
37
37
|
|
|
38
|
-
**Version:** 0.
|
|
38
|
+
**Version:** 0.113.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
|
|
@@ -315,7 +315,7 @@ guard still cross-checks the table by name).
|
|
|
315
315
|
| `scripts/run-engine-notice-catalog-test.mjs` | The engine-notice catalog and its audience table. Whether a notice deserves a person's attention is not decided by whether this end happens to have a phrasing for it — that drifts with each client's build order — but by whether the engine minted the code into its own written catalog; the audience row answers the separate question of *who* the fact is for, since an operations fact pushed at an end user is noise and a user-facing fact buried in an operator log is something withheld from the person who could act on it. Both tables are reconciled against the installed engine's own artefacts in both directions and pinned in lockstep with each other, unknown codes fall back to the conservative operator side, and catalog membership is tested on the raw value so a code carrying control characters cannot impersonate a registered one after sanitizing. The reader for a dropped MCP injection keys on its own code alone and treats a missing session, server or reason as absence rather than throwing at a read site. A reverse pin enforces the upstream's single-mint contract: the engine composes those sentences from the host's facts, so a copy of them appearing in this package's source or build is a second source that would drift, and fails. From 0.84.0 it also covers the reader for the two read-directory grant notices: it recognises only those two codes, needs the tool call id to match a card, passes the rejection reason through as written, and treats only the granted notice as evidence that a directory was added; a granted notice without both the directory and the spelling the engine now holds, or with a scope other than `exact`, is not read at all, and the scope word is pinned to the engine's type at compile time. The server also mints a few notices of its own through the same channel; those codes live in a second table with their own audiences, kept apart from the engine mirror (which must stay equal to the engine's catalog) and reconciled against the server's published package when one is supplied, so a user-facing server notice is no longer filed under operations. One dispatcher returns the typed facts for every code that has a reader, discriminated by code and tagged with its audience — only the user-audience codes belong on a user surface — and the guard ties the dispatch table to the module's own exported readers in both directions, so a reader cannot be exported without a row and a row cannot be dropped without the guard failing. From 0.85.0 it also covers a third server-minted notice, the one saying that part of a session's saved history could not be read when the session was reopened: it is a user-audience notice, its two counts are read one by one and anything that is not a non-negative integer reads as `unknown` rather than zero (neither "nothing was skipped" nor "nothing is left" may be invented), and one extra sentence — the context is empty but the turn runs — is given only when the count of entries left is exactly zero. When the server package is supplied, the guard drives the server's own emitter for that notice and reads what it emits back through the dispatcher. From 0.86.0 the catalog grows by six engine notices — a personal rule store that could not be read and was skipped, an auto-mode classification that could not decide and where the call went, a requested permission mode that did not take effect, a permission mode answering a question on the person's behalf, a mode release past a read-deny table, and a legacy checkpoint's mode keys being migrated — each with its own typed reader: branching words (the classifier's cause and destination, the reason a mode did not take effect) are read as closed sets, descriptive words pass through as written, and a malformed notice reads as absent; two of them are checked against notices produced by the engine's own emitters. The unresolvable-ask notice's optional remedy sentence is carried verbatim when it is a non-empty string — never trimmed, rewritten or filtered by cause — and its absence (older engines, other causes, an empty or non-string value) leaves the rest of the view unchanged; a notice without it reads byte-for-byte as before, and the engine's own mint function drives three cases through the dispatcher. A fourth server-minted code, the user-facing notice that a model endpoint could not be connected to, is registered with its audience and has its own reader: the failure code, the endpoint, the remedy sentence and the session are required, the name of the proxy variable is optional, the code and remedy pass through as written, and the server's sentences are never copied into the package. With server fixtures, the server's own decorator mints the notice for a direct and a proxied connection and for every connect-failure code it knows, and stays silent when a response arrived or the call did not end in error; the audience table is reconciled against both fixture generations, a row newer than the older fixture being required to be absent there. From 0.88.0 the catalog grows by one operator notice — a provider request whose tool turn broke the pairing rules was repaired on its way out — with a typed reader: each repair's form word passes through as written (the engine does not publish that word list from its entry point and has announced more words), the call ids are read as a non-empty list, any malformed entry makes the whole notice read as absent, and notices produced by the engine's own repair law and emitter read back field for field. The model-unreachable reader reads only the notice detail's own properties, like the other server-minted readers. From 0.89.0 the engine's own catalog registers those server-minted codes as well — each code carries an audience and a record of who mints it — so the package's separate table for them is retired and both the membership check and the audience lookup answer from the engine's tables alone, the four codes keeping the audiences they had; the guard reconciles the engine's record of server-minted codes against the published server's stream allow-list on the newer server generation and against the older server's own table on the older one, and the generator refuses an engine whose minter table is not in lockstep with its catalog. Two user-facing notices gain typed readers: an incomplete tool turn (cause, count, session and run are required, the call ids are optional and copied, and a cause word the package does not know passes through as written) and user input a run may not have used — one reader for the steer and follow-up codes, the kind decided by the code, the count required, the cause passed through as written and never filled in when absent — plus one user-facing sentence that names only the count, does not claim the input went unused when the server could not tell, and falls back to a cautious sentence for any cause it does not recognise. Notices produced by the engine's and the server's own emitters read back field for field, and every reader answers the same for frozen and unfrozen input. Every reader and the by-code entry point read a notice's code and detail as own properties only: a notice whose code or detail sits on its prototype, or on a polluted global prototype, reads as nothing through all three doors alike. From 0.90.0 the catalog is eighty-five notices: the engine retired the notice for the one-off migration of old parked rows, so the catalog, audience and minter tables lose a row, its fact reader and named type are gone from the public surface, and the code is treated like any code outside the catalog (the dispatcher answers nothing, the catalog check answers false, the audience stays the conservative default). The unused-input reader also reads the child-session field carried by the copy a parent session receives when a sub-agent ends with inputs unused — taken from the engine's real minting site — and the user-facing sentence says so for the two definite shapes, with no identifier in it; every malformed form of the field drops only that field. The register now holds the allow-list notice under its 0.92.0 engine name with the user audience; the earlier engine name is not registered and falls to the operator default, which is the audience it had on the engines that minted it. Since 0.94.0 the user-input-not-used reader takes its lane and count key from the generated lane table rather than from two hand-written codes, and the catalogue expectations follow the installed engine generation. From 0.94.0 the allow-list notice has a fact reader: each entry that reads as nothing comes back with its list source, its machine value exactly as the server sent it (control characters included), a display form produced by the package's single untrusted-text exit, its reason and the optional skill, list, verdict, current-name and value-type positions; a malformed entry list makes the whole reading absent. The model-unreachable reader carries the leg's run id as an optional `taskId` when the server sends one, and drops only that key when it is unusable. Since 0.96.0 the dropped-tool-turn reader also carries the optional outcome word (absent means the engine did not say), and the catalog follows the renamed path-view notice and three new route notices. Since 0.99.0 the catalog follows the newer engine at 95 codes, and the audience of each new code is the engine's own. Since 0.101.0 the catalog follows the engine at 100 codes: two new codes (a detached routing seat, and an advisor seat that was asked for but not attached) are registered with the engine's own audience and minter, and no reader is added for them. Since 0.107.0 the catalog also recognizes the old name `context.image_removed` as a user notice, because servers up to 7.142 still send it; the reader of `context.media_removed` reads it too, and a malformed new-name notice is never read through the old shape. The legacy-code table retires row by row: each row has exactly one retirement entry anchored on that row, and retiring one row at its floor leaves the other row in place. Since 0.109.0 a read-root grant notice whose covers key is hidden by a lying has trap is not recognised instead of being read as the directory meaning. Since 0.110.0 the durable-approval reader takes the source word as an open set, like the cause word: a word the package does not know reads as present and comes back as written, while an empty or non-string value reads as absent. |
|
|
316
316
|
| `scripts/run-tool-roster-projection-test.mjs` | The leg's tool roster — what the engine says it actually mounted and what face each tool wears — replacing three word lists that were only ever an estimate taken from one traffic capture against one pinned engine. The reader copies the engine's own all-or-nothing discipline: a roster whose row cannot be read, or whose declared count disagrees with the rows, is dropped whole rather than handed over short, because a consumer reading a short roster concludes the missing tools are not mounted — the upstream says in as many words that this is worse than sending nothing. A malformed *face* on a row (path target, render hints) drops only that face, since a face is not an identity. Shims are built strictly from roster rows and never guessed from a tool's name, and an axis that cannot be read stays absent rather than defaulting to `false` or `never`, which would render "unknown" as "safe". For run-time changes the guard pins the one hard rule in the contract: a digest that does not match is **not** a rejection — the carried roster is the new state regardless and only the summary becomes unusable, because refusing the swap would leave the consumer holding a stale roster forever. One reading here answers a question that the terminal state structurally cannot: whether this run was assembled with any file-and-shell tools at all. The engine's terminal vocabulary says a run finished, not whether the work got done, so an orchestrator that waits for the end and then guesses has nothing to guess from — while the assembly manifest already said it at the start, one row per mounted instance with the single condition that mounted it. The reading is three-state and both folds are refused: a roster that is readable and carries no such row is the engine stating a fact, while no roster at all is not that fact — the static half of a manifest never carries one, and an older engine reports rosters without naming the mount condition at all, where an empty count would be a statement about the reader rather than about the run. Those two are kept apart in the reason the reading carries, and the wording for every unknown case is checked never to claim the run had no tools. The same roster now decides the tool list on the first line of a non-interactive run: the host holds that line until the roster arrives and lists exactly what the engine mounted at the start of the run, in mount order. The guard runs a real assembly frame through the projection into the decision, and pins that the host falls back to the estimate only once the roster is known not to be coming — a manifest without one, an unreadable one, model output or the run's end arriving first — rather than on a timer alone (model activity counts, including a model call that is still waiting or retrying; an error line the stream synthesizes when a run fails before assembly counts as the run ending), that a sub-run's manifest is never mistaken for the run's own, that an empty roster is taken as the engine's answer rather than as silence, and that the wait bound covers both sequential default budgets the engine gives an external tool server to connect and list its tools. The holding logic itself lives in the package as a small per-run gate — buffer, decide once, release the held messages in arrival order, then pass through — and the guard drives real stream output through it to pin that the release happens exactly once, at the manifest, releasing exactly the held prefix. The ordering itself also lives in the package as a stream wrapper, and the guard checks the final output a consumer reads: the first line is always the tool-list line, a message that arrives while that line is still being built comes after it, a timer firing races nothing out of order, a source that ends or fails before the decision still gets its first line and held messages out before the error, and an early exit closes the source. From 0.84.0 the roster-derived sentence source no longer throws on a value it does not recognise, including a reading of the manifest's `hands` section passed by mistake: it answers the same "not stated" sentence as the `hands` reader, from one shared source, and its six known sentences do not change. From 0.86.0 that first line also says where its tool list came from, in two added keys the wrapper writes onto the host's own line object in place: a source of `engine-roster`, `estimate` or `host-static`, and, only for an estimate, which of the four reasons made the roster known not to be coming. The guard checks every decision path end to end, that the host's object keeps its identity and every other key byte for byte, that stale values of the same two keys left by the host are replaced (a stale reason is removed rather than left behind), and that a host line that cannot be written is passed through untouched instead of failing the first line. |
|
|
317
317
|
| `scripts/run-permission-rule-issue-codes-test.mjs` | The rule-lint refusal codes an engine reports when it will not compile a permission rule. The SDK publishes neither a schema nor a type for them, so the package mints the table from the engine's own bytes and the guard pays the cost of that copy instead of leaving it to somebody remembering: it parses the codes the engine actually mints and reconciles them against the table in both directions, so a code added upstream (the user would see a bare code) and a code only the package believes in (a branch that can never fire) both fail. It also reconciles the table plus a small retired ledger against the engine's declared union, which is deliberately not the same set — one member was renamed and its old name is still declared — so reviving a code the engine will never mint again is impossible and a future stale member shows up immediately. Sentences are pinned one per code, mutually distinct, and split by family: a rule that is wrong and a rule that is legal but unsupported on this lane are different next steps and may not share a sentence. The engine's own message rides along as prose — sanitized and capped after escaping, never matched on The set of codes the engine actually mints now also counts declared codes minted outside the rule compiler (the allow layer reports an entry that is not a string with one). Codes an engine release retired but older engines still mint are found by comparing the receiving server's bundled engine with the installed one rather than by a hand-kept list; each keeps the exact sentence it had before, and the codes that replaced them each get their own sentence with no machine identifiers in it. Since 0.94.0 the base-path refusal sentence is checked to hold on both engine generations (base missing, or declared but not a usable absolute path), with the premise run on the installed engine. Since 0.99.0 the tool-name refusal code minted by the newer engine is in the table with its own sentence, and the two-way equality with the engine's minted set covers it. |
|
|
318
|
-
| `scripts/run-gate-vocabulary-test.mjs` | The two gate vocabularies — who denied a call (`DeniedBy`, the installed engine's ten words) and who asked about it (`AskOrigin`, eleven) — together with the one place their sentences are minted, so the same denial does not read three different ways across three clients. The tables are copies, not opinions: the asker table is reconciled in both directions against the lists the installed SDK and engine packages publish, and the denier table is exactly the installed engine's own list (from 0.92.0 the one word only engines below the support floor still sent is no longer in it), checked so that every word the SDK declares is present except that retired word, and every word ahead of the SDK comes from the engine — a word added upstream (nobody renders it, the user sees a bare code) and a word only the package believes in (a branch that can never fire) both fail. Every word must carry its own literal sentence and no two may collide, including the sibling pairs the upstream deliberately split apart — an organization store and a personal rule store being unreadable send you to different people, and the two tighten origins exist precisely to name which layer of engine logic asked. The two fallbacks are pinned distinct because an unknown word means different things in each: for a denial layer this build does not know, the sentence says only that this build cannot say which layer refused the call or why — it no longer guesses at a newer engine or a damaged record, since the most likely unknown word is the retired one replayed from an old session; the asker vocabulary is genuinely open (the server only checks for a non-empty string, so an unknown word just means the client is older than the engine). Alongside them sits an **uplift anchor** rather than a third table: the reason a call was decided the way it was is a distinct semantic face from who denied it and who asked, one upstream has not mirrored into the SDK at all, and one whose newest member — a shell command allowed because it only reads — has no sentence anywhere yet. Minting the union here would create the second drifting source the day upstream publishes it, so the guard instead asserts the **absence** from both ends: the SDK declarations carry no such union near that word, and the installed engine’s own list does not carry the word either. The engine end fires first, on the batch that raises the dependency, which is exactly when the ownership question should be answered; the SDK end fires when the mirror lands. Either red is the work order to mint the sentence, never a reason to delete the anchor. A fourth mint now sits beside the three tables and is not a table at all: a single presence-only fact — that no saved rule and no standing posture can retire this question — earns one sentence, taking no argument precisely so a caller cannot mistake it for a second kind of mandate, pinned distinct from every sentence the tables mint, pinned never to point at rule-writing, and pinned not to overclaim the stronger neighbouring demand that a person rather than a configuration must answer; it must not say the question is asked every time — an answer for this one call may come from the person, a hook or an automatic check the deployment runs — and its wording is checked against the engine package's own description of the mandate. A fifth table joins them from 0.80.0: the words for **how a wait ended** (thirteen from 0.80.0, fourteen from 0.90.0), mirrored in both directions from the engine's own declarations — the table's owner — with the wire SDK's copy held alongside as a second witness that must match it word for word and in order, so the day the SDK falls a generation behind, that is what turns red rather than the mirror silently following the wrong source. The newest of them says a deployment's own policy answered the card — not a person, and not “nobody could be asked” — so the guard pins it apart from both neighbours by behaviour, feeding every one of the words through all five named predicates and checking which word makes which one speak, rather than what any predicate returns. Two of them also decide how a refusal is filed in the session transcript; that mapping is minted once and reused by both of the package's own entry points, and anything outside those two words yields nothing rather than a guess. Since 0.83.2 a sixth list covers the word a mandated question stands on (`APPROVAL_MANDATE_WORDS`, six words): it must equal the engine's own list word for word and in order, membership is exact, the card reader `readApprovalMandate` answers only for an own key holding one of the six words, and each word has one fixed sentence explaining why the question must be confirmed — six distinct sentences that never point the reader at writing a rule, never promise a question every time, never claim only a person may answer, and repeat no other sentence the package mints. The list is also pinned against the engine's type at compile time in both directions, while the published build references no engine package at all: every `.js` and `.d.ts` file in the build is scanned, and the same scan is first shown to fire on references planted in a scratch directory. From 0.90.0 the settlement table carries `policy_allowed` (a machine allow, listed before `policy_refused`) and the mirror is checked against the installed engine byte for byte, in order; the installed SDK (12.x) predates the word, so its list is compared as "the engine's list minus one registered lagging word", and every SDK witness at 16.0.0 or later must carry exactly the engine's set of words. The party that settled is the engine's five-word set, read open (any word, including an unknown one, comes back as written); a policy allow is no person's approval and no refusal (all five named predicates stay silent on it, and it files no denial class in the transcript), and the refusal word reads the same under both party spellings of the two server generations. The one lagging word that loosens the compile-time check against the installed SDK is itself guarded: it must stay a non-empty union of string literals (widening it to `string` compiles cleanly, so only this check catches it), the check's declared type must stay exactly as written, the word list must equal the list this check registers, and every lagging word must be absent from the SDK at the peer floor yet present in the newest witness SDK and in the installed engine's list. The origin word on a gate record and on a released-by-mode record is read as an own key only, and a structural pin holds that every source file reading an ask origin goes through the one reader. A retired mandate word that an older server still sends reads as the sentence of the word that replaced it (one sentence per fact), the sentence table holds the current words only (a leftover sentence for a retired word fails to compile), and the modules that read the rename table are pinned to a named roster. |
|
|
318
|
+
| `scripts/run-gate-vocabulary-test.mjs` | The two gate vocabularies — who denied a call (`DeniedBy`, the installed engine's ten words) and who asked about it (`AskOrigin`, eleven) — together with the one place their sentences are minted, so the same denial does not read three different ways across three clients. The tables are copies, not opinions: the asker table is reconciled in both directions against the lists the installed SDK and engine packages publish, and the denier table is exactly the installed engine's own list (from 0.92.0 the one word only engines below the support floor still sent is no longer in it), checked so that every word the SDK declares is present except that retired word, and every word ahead of the SDK comes from the engine — a word added upstream (nobody renders it, the user sees a bare code) and a word only the package believes in (a branch that can never fire) both fail. Every word must carry its own literal sentence and no two may collide, including the sibling pairs the upstream deliberately split apart — an organization store and a personal rule store being unreadable send you to different people, and the two tighten origins exist precisely to name which layer of engine logic asked. The two fallbacks are pinned distinct because an unknown word means different things in each: for a denial layer this build does not know, the sentence says only that this build cannot say which layer refused the call or why — it no longer guesses at a newer engine or a damaged record, since the most likely unknown word is the retired one replayed from an old session; the asker vocabulary is genuinely open (the server only checks for a non-empty string, so an unknown word just means the client is older than the engine). Alongside them sits an **uplift anchor** rather than a third table: the reason a call was decided the way it was is a distinct semantic face from who denied it and who asked, one upstream has not mirrored into the SDK at all, and one whose newest member — a shell command allowed because it only reads — has no sentence anywhere yet. Minting the union here would create the second drifting source the day upstream publishes it, so the guard instead asserts the **absence** from both ends: the SDK declarations carry no such union near that word, and the installed engine’s own list does not carry the word either. The engine end fires first, on the batch that raises the dependency, which is exactly when the ownership question should be answered; the SDK end fires when the mirror lands. Either red is the work order to mint the sentence, never a reason to delete the anchor. A fourth mint now sits beside the three tables and is not a table at all: a single presence-only fact — that no saved rule and no standing posture can retire this question — earns one sentence, taking no argument precisely so a caller cannot mistake it for a second kind of mandate, pinned distinct from every sentence the tables mint, pinned never to point at rule-writing, and pinned not to overclaim the stronger neighbouring demand that a person rather than a configuration must answer; it must not say the question is asked every time — an answer for this one call may come from the person, a hook or an automatic check the deployment runs — and its wording is checked against the engine package's own description of the mandate. A fifth table joins them from 0.80.0: the words for **how a wait ended** (thirteen from 0.80.0, fourteen from 0.90.0), mirrored in both directions from the engine's own declarations — the table's owner — with the wire SDK's copy held alongside as a second witness that must match it word for word and in order, so the day the SDK falls a generation behind, that is what turns red rather than the mirror silently following the wrong source. The newest of them says a deployment's own policy answered the card — not a person, and not “nobody could be asked” — so the guard pins it apart from both neighbours by behaviour, feeding every one of the words through all five named predicates and checking which word makes which one speak, rather than what any predicate returns. Two of them also decide how a refusal is filed in the session transcript; that mapping is minted once and reused by both of the package's own entry points, and anything outside those two words yields nothing rather than a guess. Since 0.83.2 a sixth list covers the word a mandated question stands on (`APPROVAL_MANDATE_WORDS`, six words): it must equal the engine's own list word for word and in order, membership is exact, the card reader `readApprovalMandate` answers only for an own key holding one of the six words, and each word has one fixed sentence explaining why the question must be confirmed — six distinct sentences that never point the reader at writing a rule, never promise a question every time, never claim only a person may answer, and repeat no other sentence the package mints. The list is also pinned against the engine's type at compile time in both directions, while the published build references no engine package at all: every `.js` and `.d.ts` file in the build is scanned, and the same scan is first shown to fire on references planted in a scratch directory. From 0.90.0 the settlement table carries `policy_allowed` (a machine allow, listed before `policy_refused`) and the mirror is checked against the installed engine byte for byte, in order; the installed SDK (12.x) predates the word, so its list is compared as "the engine's list minus one registered lagging word", and every SDK witness at 16.0.0 or later must carry exactly the engine's set of words. The party that settled is the engine's five-word set, read open (any word, including an unknown one, comes back as written); a policy allow is no person's approval and no refusal (all five named predicates stay silent on it, and it files no denial class in the transcript), and the refusal word reads the same under both party spellings of the two server generations. The one lagging word that loosens the compile-time check against the installed SDK is itself guarded: it must stay a non-empty union of string literals (widening it to `string` compiles cleanly, so only this check catches it), the check's declared type must stay exactly as written, the word list must equal the list this check registers, and every lagging word must be absent from the SDK at the peer floor yet present in the newest witness SDK and in the installed engine's list. The origin word on a gate record and on a released-by-mode record is read as an own key only, and a structural pin holds that every source file reading an ask origin goes through the one reader. A retired mandate word that an older server still sends reads as the sentence of the word that replaced it (one sentence per fact), the sentence table holds the current words only (a leftover sentence for a retired word fails to compile), and the modules that read the rename table are pinned to a named roster. Since 0.113.0 it also pins the one-sentence 'why am I being asked' headlines for approval cards: nine sentences in two languages (eight non-quiet origins plus the unknown-origin case; the four quiet origins carry no headline), checked for length, a banned-word list of internal mechanism terms, distinctness from every other sentence the package mints, per-origin fidelity to the engine's own definition (including the old failure shapes), and a hash of the engine's definition text so an upstream rewording turns the guard red until someone rereads the sentence; and it pins that the origins with no headline are exactly the origins the detail layer keeps quiet. |
|
|
319
319
|
| `scripts/run-engine-identity-test.mjs` | The engine generation anchors on `/health` (`pid`, `instanceId`, `startedAt`; engine >=7.67.0). `/health` is the one unauthenticated door and its heartbeat is always green, so "another host restarted the shared engine" used to be discoverable only by having some authenticated request hit a 401 first — a path that misreads a restart as a network fault. The reader narrows each anchor independently (one malformed field never hides the other two) and always hands back a reading object rather than an absence, because the caller is asking which anchors answered, not whether there was a response. The comparison is a three-word verdict, not a boolean: `unknown` when the two readings share no comparable anchor at all — an empty intersection means nothing could be compared, never that nothing changed — and the boolean convenience is pinned so that only `true` is an assertion. Any comparable anchor differing decides `changed`, so a reading whose `startedAt` matches while its `instanceId` does not cannot be waved through as the same life; precedence only decides which anchor gets named in the diagnosis |
|
|
320
320
|
| `scripts/run-posture-knob-projection-test.mjs` | The three deployment knobs on the operator face (`serverGates.durableApproval` / `streamAskWindowMs` / `sessionAutoTitle`, engine >=7.67.0), each read as a value **plus who set it plus one operator-facing pointer** rather than a bare value — a bare boolean cannot answer why this particular machine is on this setting or how to pin it back, and a default that flips with the deployment shape is invisible without that. A worker too old to report readings still sends a bare boolean; the reader folds it into the same shell so consumers keep one branch, but raises a `legacy` bit, answers `undefined` from the machine-readable source accessor, and mints a sentence that contains no source word at all — claiming a source nobody reported is worse than admitting the worker cannot say. The other two knobs are honestly absent on such a worker rather than defaulted, a malformed side knob drops only itself while the anchor knob drops the whole reading, and the four sentences are pinned literally distinct so an operator can tell "not observed" from "not reported" from a real value. The last leg reads the installed SDK's `openapi.yaml` and `types.d.ts` directly, including a pin that exactly one knob on this face is numeric — the premise the millisecond-to-prose rendering rests on |
|
|
321
321
|
| `scripts/run-terminal-facts-projection-test.mjs` | The four unconsumed terminal-receipt facts: `TaskResult.effectiveReasoning` / `effectiveMemoryScopes` are narrowed into `_sema_effective_reasoning` / `_sema_effective_memory_scopes` on the CC-shaped `result` (success and error envelopes alike; a malformed value mints nothing, never a default tier), the resume **reopen** family (`resume.env_failed` / `tool_unavailable` / `tool_contract_mismatch`) is a frozen closed set with a reader and three-sentence copy that is disjoint from the refusal and retry-later sets, and `routePairingVerdict` reads `ModelInfo.routePairing` as ok / broken / unknown without policing the open set. A fifth section pins the structured-output key on the success result: the CC-spelled `structured_output` is the only home for the value the wire calls `structuredOutput`. The camelCase spelling this package used to mint on its own — a misspelling of the CC field, not an additive field of our own — rode alongside it for exactly one release (0.79.1) and is **absent from 0.80.0 on**, pinned both by own-key and by `in`, so a consumer still reading the old name sees `undefined` rather than a stale copy. The wire position is read exactly once, so a value-changing accessor is only ever asked for its first answer; absence stays absence; a wire key that is present but `undefined` mints nothing, since a key whose value is `undefined` makes a consumer that tests presence read "the engine produced nothing" as "the engine produced an empty result"; falsy-but-present values such as `null`, `0`, `""` and `false` are still minted, and so are shapes that are not records at all — an empty array, a populated array, a string, a number, a boolean — each carried through by the same reference, because the shape of that value is decided by the caller's own schema and the package does not get to filter it; and the error envelope carries no such key, because the CC error arm has no such field. Which spelling CC itself declares is witnessed from the mirror's own syntax tree rather than a constant copied into the guard, so the day that field is renamed upstream the guard says so. A seventh section covers the two keys the memory-scope reading used to leave behind: the mounted arm's `projectKey` (exactly one of `{ scope }` or `{ absent }`; a missing key means the deployment declared nothing, which is not the same as having looked and found none, and the reason word is read as an open set so a word added upstream is never dropped) and each row's `shadowed` (a positive integer only, never on the write home's own row, never a zero or a coerced value). A malformed or unreadable value mints that one key and nothing else, so a throwing accessor never takes the arm down; the other arms never carry either key; and inputs without the two keys read byte for byte as the previous release does, checked against that release's published artifact on a large generated pool and on a pool of live objects (throwing accessors, revoked proxies) where even throwing versus not throwing must match. The upstream facts are pinned on every generation of server fixture the guard is given: the mint sites and declarations in the engine each one bundles, the server's terminal shaping run for real so both keys cross the wire unchanged, and the server's own project-identity minting fed into the reader. The SDK types are pinned as not modelling the face at all, and the two keys that were declined are pinned as having no mint site on the wire, so the day either changes the guard goes red instead of the decision going stale. The key set compared across the success and error envelopes is read from the type declaration of the shared fact parts by the compiler's checker rather than from a hand-kept list or a name prefix: every declared key must have an input fixture and every fixture a declared key, and a planted extra key or a removed key is shown to change the derived set. Since 0.99.0 the route-pairing verdict table is pinned at compile time in both directions to the engine's pairing-status type, the two new broken states read as broken, and anything outside the known words, prototype key names included, reads as unknown. |
|
|
@@ -339,7 +339,7 @@ guard still cross-checks the table by name).
|
|
|
339
339
|
| `scripts/run-approvals-stream-live-capability-test.mjs` | The engine's live-approval-push self-description (`capabilities.approvalsStreamLive`, engine ≥7.87.1), read the same four-state way as its four sibling capability readers: an absent key is reported as not reported (never folded into `false`), the value must be a strict boolean, and the one decision the feed consumer needs — whether it must keep pulling suspended asks itself — is answered by `livePendingNeedsReconcile`, which only says no when the engine explicitly says it pushes. |
|
|
340
340
|
| `scripts/run-model-facing-anchors-test.mjs` | Five places where model-facing engine text is parsed back into card slots: the task-output cursor semantics machine-readable key wins over the spool marker text (and its absence is never read as `full`), the `--- result (…) ---` line accepts a parenthesised note and reads it in linear time even on crafted input (a note can no longer swallow the start of the next line), the bash header accepts the engine's exit-note and image variants and carries the note into the same benign-exit slot the structured path uses, the notification seed scan treats both producers' terminal words as terminal, and the parked background-task phrase is recognised. |
|
|
341
341
|
| `scripts/run-execution-lane-capability-test.mjs` | The deployment execution-lane self-description (`capabilities.executionLane`, engine ≥7.86.0), read the same four-state way as its three sibling capability readers. An absent key is reported as "not reported" and never as "tools do not run on this host" as a fact about the deployment; since every server at or above the support floor reports the key, the single "do tools run here" predicate answers `false` for a body that reported nothing (from 0.92.0, so no skill `baseDir` is sent on a reading that proves nothing), and keeps using the caller's own inference only while nothing has been observed yet. The implication is one-way (`toolsOnThisHost: false` means `baseDir` is never sent; `true` is only a necessary condition), `provider` is read as an open non-empty string rather than a hand-copied closed set and is sanitised and bounded before display, `toolsOnThisHost` must be a strict boolean, an `isolated` position that can only be read by running a getter (or whose descriptor read throws) makes the whole entry unreadable rather than degrading that one position, extra members never cross, the reading comes only from the capabilities cache's landed response (there is no separate write port), and the single "do tools run here" predicate uses the bit when it is present. Current engines (≥7.106.0) also say whether the lane's tools sit behind an OS isolation boundary (`isolated`, present only when the lane has tools of its own); the reading carries it when it is a strict boolean and otherwise records which of three absences it is — a lane without tools on a current engine, an older engine, or a value that cannot be read (including a current engine that says tools run on this host yet omits the flag it must then send) — none of which is ever read as `false`, and an unreadable value never costs the rest of the lane reading. The single "no OS sandbox on this machine" predicate (`isolated` false and tools on this host) answers yes, no or unknown under three-valued logic, one sentence goes with the yes answer, the operator diagnostics segment is read by the same rule, and the engine's own projection for every lane is replayed: only the host lane answers yes |
|
|
342
|
-
| `scripts/run-live-pending-ask-test.mjs` | Suspended in-stream asks (the `livePending` section of `GET /v1/approvals`): an absent key reads as not reported (never an empty list or a zero count), an unreadable section leaves the previous snapshot in place, rows are narrow-read (malformed rows dropped and counted, optional flags only-if-true, extra members never cross), the feed digest covers the section so an ask appearing or settling produces a snapshot, the host-gated reconcile cadence re-lists while the push leg is connected and backs off when nothing changes, the tracker surfaces each `approvalId` once and reports it gone once, and the decision runs through the same respond chain as the in-stream frame leg — with the card stating that the tool arguments are not visible and edits on that card never forwarded. Since 0.73.4 a push-side fetch that did not get a readable body (thrown, or an unreadable `livePending` section) is owed a look: it is retried with doubling back-off until one fetch succeeds (never abandoned by count), cancelled by any successful fetch, cleared on stop, and switchable off. The session scope of the suspended-ask view, count and tracker must be chosen explicitly (one session, all sessions, or identity pending): an unresolved or malformed scope answers null counts, an empty view and a tracker that never emits, and the three entry points refuse to compile without a scope. On servers that present approvals as `approval_request` frames (engine ≥7.133.0), a suspended row carries that frame instead of a `tool_approval` one; either opening-frame form is accepted when its `approvalId` matches the row, both go through the same card, holder table and respond call keyed by the row's `approvalId`, the card face is read from `card` and the out-of-card facts (mandated, origin, …) from the frame's top level, and anything outside the two forms still yields the blind card; the `tool_approval` card is byte-identical to the previous release. A malformed top-level `requiresRealApproval` on such a card is recorded in `unreadableEvidence` (the automatic-answer verdict stays `unknown`), and a parked row without an approval id still joins the same live card by run, call id and ask signature — call id, sub-agent run and ask classes read from the card face, origin and mandate from the frame's top level, and none of the out-of-card facts for an unrecognised version — so no second card or second decision appears. The per-session waiting count includes parked rows of background children (and grandchildren) this session launched, using the same session test as card ownership. The decision identity of a suspended row (its `approvalId`) is read once: the row reader takes the own data descriptor of the row id and of the frame id one time each (a getter is never run, an id that lives only on the prototype chain does not count; a bad row id drops the row and counts it, a bad or mismatching frame id drops only the frame position so the row surfaces as a blind card), the live card entry takes the own data descriptor of the frame id once before its first await (an unreadable id surfaces no card and sends nothing — `not_sent` / `card_unavailable`), and the suspended-row leg hands the row reader's reading to the entry as the card identity, so a frame changed after the read, or a proxy that answers the descriptor read differently each time, cannot move a decision to another approval. |
|
|
342
|
+
| `scripts/run-live-pending-ask-test.mjs` | Suspended in-stream asks (the `livePending` section of `GET /v1/approvals`): an absent key reads as not reported (never an empty list or a zero count), an unreadable section leaves the previous snapshot in place, rows are narrow-read (malformed rows dropped and counted, optional flags only-if-true, extra members never cross), the feed digest covers the section so an ask appearing or settling produces a snapshot, the host-gated reconcile cadence re-lists while the push leg is connected and backs off when nothing changes, the tracker surfaces each `approvalId` once and reports it gone once, and the decision runs through the same respond chain as the in-stream frame leg — with the card stating that the tool arguments are not visible and edits on that card never forwarded. Since 0.73.4 a push-side fetch that did not get a readable body (thrown, or an unreadable `livePending` section) is owed a look: it is retried with doubling back-off until one fetch succeeds (never abandoned by count), cancelled by any successful fetch, cleared on stop, and switchable off. The session scope of the suspended-ask view, count and tracker must be chosen explicitly (one session, all sessions, or identity pending): an unresolved or malformed scope answers null counts, an empty view and a tracker that never emits, and the three entry points refuse to compile without a scope. On servers that present approvals as `approval_request` frames (engine ≥7.133.0), a suspended row carries that frame instead of a `tool_approval` one; either opening-frame form is accepted when its `approvalId` matches the row, both go through the same card, holder table and respond call keyed by the row's `approvalId`, the card face is read from `card` and the out-of-card facts (mandated, origin, …) from the frame's top level, and anything outside the two forms still yields the blind card; the `tool_approval` card is byte-identical to the previous release. A malformed top-level `requiresRealApproval` on such a card is recorded in `unreadableEvidence` (the automatic-answer verdict stays `unknown`), and a parked row without an approval id still joins the same live card by run, call id and ask signature — call id, sub-agent run and ask classes read from the card face, origin and mandate from the frame's top level, and none of the out-of-card facts for an unrecognised version — so no second card or second decision appears. The per-session waiting count includes parked rows of background children (and grandchildren) this session launched, using the same session test as card ownership. The decision identity of a suspended row (its `approvalId`) is read once: the row reader takes the own data descriptor of the row id and of the frame id one time each (a getter is never run, an id that lives only on the prototype chain does not count; a bad row id drops the row and counts it, a bad or mismatching frame id drops only the frame position so the row surfaces as a blind card), the live card entry takes the own data descriptor of the frame id once before its first await (an unreadable id surfaces no card and sends nothing — `not_sent` / `card_unavailable`), and the suspended-row leg hands the row reader's reading to the entry as the card identity, so a frame changed after the read, or a proxy that answers the descriptor read differently each time, cannot move a decision to another approval. It also pins that the live and sub-agent waiting counts leave out a listed row once this process holds a settled outcome for that ask (a decision delivered and answered, an interrupt refusal accepted, a host hand-off settled, or the engine closing the approval), that parking does not count as settled, that the counts never go below zero, and that the durable count and the unknown readings are unchanged. |
|
|
343
343
|
| `scripts/run-leader-conflict-test.mjs` | The leader-run terminal `needs_human` + `result.conflict` (engine ≥7.83.0), read once for all three shells. A `result` without a `conflict` key is reported as `none` and worded as "no conflict details" rather than "no conflict": on the wire it is indistinguishable from an older engine that never reports one, and the reader does not pick a side. A `conflict` that is present but unreadable is a third word, never folded into `none`, because a tree that carries conflict markers must not be rendered as clean. `filesTruncated` is honoured only as `true` (absent means the list is complete; any other value makes the section unreadable), `workers[].applied` is passed through as the boolean fact it is, `salvaged[].patch` is handed to the save path byte-for-byte (an artifact, not screen text; a malformed row is dropped alone, and a salvage list that is present but cannot be fully read is flagged rather than rendered as "no patches"), and `rejHead` — the one on-screen diagnostic — is escaped and bounded before display. The record never throws on hostile input, extra members do not cross, arrays are fresh copies, and the single sentence minted here names the base, the files, which branches landed and which did not, and how many patches can be saved, without ever suggesting a retry: `needs_human` is a run waiting for a person, and the no-details sentence says plainly that it is not proof of a merge |
|
|
344
344
|
| `scripts/run-mcp-reconnect-test.mjs` | The in-session MCP re-dial verb (`POST /v1/sessions/:id/mcp/reconnect`, engine ≥7.85.0), consumed. The single discriminant is `outcome` and all three answers are HTTP 200, so the reader branches on the word and never on the status; the `unsupported` answer carries exactly five keys and the reader refuses to invent a zero or an empty list for the four fields the engine did not produce, while `accepted` / `refused` treat those four as required and go malformed when one is missing. The tool roster follows the **presence** of `toolNames` (absent = untouched, empty = withdrawn), the connection record passes `errorCode` through as an open set, and every remote-authored string is sanitised and bounded before display. Failures are classified by `errorCode` alone, a missing code is reported as unknown rather than guessed, the verb never throws, and the request-side guard (non-empty name, ≤190 chars) stops a call that the contract would reject anyway. The capability bit reads absent as "cannot tell" rather than "unavailable", and the one sentence the contract insists every UI carries — that re-dialing is a transaction, not a refresh — is minted here once. The engine's answer-deadline reply — a 503 whose machine code says the request stopped waiting before the re-dial settled — is its own verdict arm: it is read by that code alone and never by the status, carries no outcome, and is never rendered as a failure, because the re-dial was not cancelled and may still be running; the one sentence for it is minted here once, and the gate pins that no server release up to 7.117.0 contains the code at all Since 0.109.0 a toolNames or listingIncomplete key hidden by a lying has trap makes the whole result malformed instead of being read as absent. Since 0.110.0 the same gate pins the one place that words a re-dial verdict that did not succeed (nine verdicts times a server name times both languages, the name painted as visible escapes and bounded, remote error text never rendered, malformed input — including a successful verdict whose result cannot be read — answering nothing and never throwing) and the pure reader that says whether a failure record names the engine's own authorization-required class (only that exact class word counts: HTTP status numbers, near-miss spellings and unreadable input are never read as a yes, and unreadable input is never folded into a no). The small status fragment that follows a failure class on a connector row has one maker too: only the authorization-required class carries the re-authorize hint, a 5xx status says the server is down, and every other status — 401 and 403 under the plain HTTP-status class included — is shown as the bare number. |
|
|
345
345
|
| `scripts/run-core-value-ports-test.mjs` | The port-injection seam for ten **engine value-level** facilities (autonomous-loop prompt assembly, permission-rule loosening, tool-policy composition, protocol/retired-name/grammar lookups, rule compilation; the discussion-workflow name port left in 0.96.0 together with that engine feature). This package cannot re-export them (the engine barrel drags Node built-ins into the browser bundle), so it declares the ports and honest-absence readers; a Node host installs the engine's own functions verbatim. The guard pins: every reader returns `undefined` when nothing is installed (never a fabricated empty array or default policy), arguments and results pass through by reference, engine errors propagate unchanged, partial installs read partially, restore functions unwind to the previous bag, and the module source has zero engine imports From 0.90.0 the mirror of the allow/deny policy options is also compared with the engine's own declaration, both ways: the same set of option keys (the engine added an optional `scope` — whether an allow-list hit is a stated grant or only a bound on which tools a lane may see), with `scope` optional and its two-word union and the invalid-name union spelled identically; the value handed to the installed function is passed through untouched. From 0.101.0 the engine retires that option (passing it is refused by name when the policy is built), so the mirror has no `scope` member and the check asserts it is absent on both sides, that the engine refuses it, and that a stray one is still handed to the installed function untouched rather than dropped. Since 0.94.0 there are twelve ports: the engine's allow-list reader joins them, with the same absence and pass-through rules. From 0.94.0 the seam also carries the engine's five route-adjudication functions (`routeKeyOf`, `joinRequestUrl`, `resolveRouteCredential`, `adjudicateModelRoute`, `routePairingStatus`): a host that builds the full `CoreValuePorts` bag fails to compile until it adds them (the gate compiles that bag in memory and checks each missing member is named), the engine's own functions type-check against the mirrors, and each reader answers `undefined` while its port is not installed. Since 0.96.0 the whole engine bag also assigns into the full port type with no cast (the rule-grammar mirror follows the engine). Also in 0.96.0 the engine's rule tool-name judge (`judgeRuleToolName`, reader `ruleToolNameJudgement`) joins the seam and the retired-name table port leaves it (its one reader now asks the judge): the gate pins the swapped name list, pass-through and error propagation for the judge, and compiles the full bag both ways — missing the judge, or still carrying the retired-name table, fails and names the member. In 0.98.0 the full-bag assembly check compiles under three option sets — plain strict, strict plus exact optional property types and unchecked indexed access, and non-strict — and all sixteen engine functions must install into the full type with no error in each (before, the two strict sets rejected the combine-policies and validate-rules members, which were function-typed properties checked contravariantly), with a sabotage control that goes red under all three. In 0.101.0 the nineteenth port is the engine's default root address for the first-party hosted API: a value rather than a function, installed from the engine's package-root constant. Its reader returns `undefined` while nothing is installed (never the literal root), returns exactly what was installed otherwise (no trimming, no shape check, an empty string stays an empty string), the engine's own constant type-checks into the port type under all three option sets, a full-type bag built for the previous eighteen ports stops compiling and names the new member, and the reader body is a single read of the port with the host name absent from the module source. The twentieth port is the engine's reader for an MCP server's `toolFaces` declaration: absent when not installed (never an invented ok), passed through with the same references when installed, the engine's own function type-checks into the port under all three option sets, a full-type bag built for the previous nineteen ports stops compiling and names the new member, and the reader's signature takes a narrow input type rather than an unknown one. The twenty-first port is the engine's covering-rule matcher (whether a rule name the engine reads as covering every tool of a server covers a given tool name): absent when not installed (never an invented false), both arguments passed through and the answer returned as given when installed, a throw from the engine propagates, the engine's own function type-checks into the port under all three option sets, a full-type bag built for the previous twenty ports stops compiling and names the new member, the real engine answers twelve fixed pairs identically through the port, and the reader's signature has no unknown in it. The twenty-second port is the engine's frozen tuple of the three settings-layer names the importer reads (a value, installed from the engine's package-root constant; the types package carries the matching type but no matching value, so the seam carries the value): absent when not installed (never the three literals), and when installed the very same tuple is returned (no copy, no re-freeze, no shape check, so the engine's frozen tuple stays frozen at the read site). The values are checked element by element with a syntax-tree reader against the engine's own declaration, its compiled initializer, its runtime value and the tuple declared on the port member in this package's shipped declaration file, and the tuple and its element type are checked both ways against the engine's own type and the types package's type with the real type checker; every reader and comparison is first proven against planted samples (a wrong word, a missing member, a swapped order, a missing declaration, an unfrozen initializer, a renamed re-export, a copying reader, a reader with a literal fallback). The engine's own constant type-checks into the port under all three option sets, a full-type bag built for the previous twenty-one ports stops compiling and names the new member, and the reader body is a single read of the port. Since 0.111.0 the port bag has two more members, the engine's account-line constructor and its single account fold. Unset they read as absent (never an empty account); set they pass arguments and answers through by reference and let the engine's refusal of malformed lines propagate. The engine's own functions type-check into the port under all three option sets, a bag built for the previous twenty-five ports stops compiling and names both new members, and the widths ledger records both ports as equal to the engine's types. |
|
|
@@ -351,7 +351,7 @@ guard still cross-checks the table by name).
|
|
|
351
351
|
| `scripts/run-subagent-durable-divert-test.mjs` | The side-channel that keeps a **sub-agent's** content out of the leader's transcript, on the replay leg. A content frame stamped with a parent tool-call id belongs to a child, and rendering a child's tokens as the leader's own text is the pollution this divert exists to prevent — but the predicate only listed the four **live** frame shapes, while the durable leg replays the same segment in its **aggregated** form. Those frames fell straight through onto the main projection path, which is how a reconnect or a resumed session ended up with the child's answer printed as the leader's. The anchor is unchanged and shared: the parent tool-call id is what says whose frame this is, and whether the frame is an increment or a whole segment has nothing to do with whose it is — judging the two shapes separately is exactly how one of them got missed. Folding the aggregate into a synthetic increment would have been the smaller diff and the wrong one: an increment means *append*, so a segment that already streamed live and then replays whole would be counted **twice**. The two are kept distinct and the aggregate absorbs instead — a whole segment whose prefix is what the buffer already holds replaces it, which also makes a redelivery of the same frame idempotent, and a prefix that does not match falls back to appending both rather than deciding on the engine's behalf which version counts. Segment boundaries stay with the tool frames rather than moving into the aggregate arm, since closing there would turn a second replay of one segment into a second entry, and the increment arm is pinned to keep appending so a token run that happens to be a prefix of the next does not silently lose characters. When the host declares the non-interactive lane, a sub-agent's tool calls and results are also forwarded into the main output with their parent tool-use id (the sub-agent's text and thinking still stay out, as in the reference CLI); without that declaration the output is unchanged. On that same non-interactive lane the tool-call blocks of the main output carry only the reference shape (type, id, name, input): the display name and the two identity keys the interactive bridge strips before rendering are removed through the very function the bridge uses, the input object itself is left untouched, and the interactive lanes keep the three keys. |
|
|
352
352
|
| `scripts/run-subagent-content-budget-test.mjs` | The **byte** budget on the sub-agent transcript ledger. It used to be bounded only by *counts* — so many entries per child, so many children — and a count is not a budget when a single entry has no ceiling of its own: one tool result carrying an inlined attachment, or one long model answer, and a single slot sits on tens of megabytes. The guard anchors on how many bytes are **still held** after over-filling, not on whether truncation fired, because an implementation that flags the overflow without actually dropping anything satisfies the second and not the first. Dropping is required to leave a record — how much went and where the retained content now starts — and that record has to reach the render plan, because content that vanishes with no marker gives the reader a transcript shorter than what happened with nothing to say so; the record is one per child, updated in place, pinned to the front, and excluded from the budget it describes. Order matters and is checked: oldest entries go first and the live tail is trimmed only as a last resort, since taking the text the user is watching stream while older history survives is the wrong end. The total budget evicts a whole least-recently-used child rather than shaving every child, and the configuration surface is fail-loud on zero, negatives, non-finite and non-integer values — a silently ignored budget is the exact failure this exists to remove — with the rejection proven atomic so a bad second field cannot leave half a configuration behind. The defaults are checked to be a magnitude that can really be reached, since a number too large to hit is a field rather than a budget |
|
|
353
353
|
| `scripts/run-subagent-usage-projection-test.mjs` | Per-subagent usage, split by task. The engine's final accounting carries the delegated spend as **one total** — tokens, turns, task count — and no per-task breakdown, while every sub-flow turn on the stream carries its own usage. This package used to fold that away at the leader/sub-flow divide (a child's output tokens must never reconcile the leader's response length), so a client showing a subagent's detail pane had nothing to print. The split table can therefore only be accumulated from the stream, and this guard pins what that costs. The two existing leader-only arms stay **byte-for-byte unchanged** — the new arm is additive and always carries the sub-flow's own lane proof, so a host cannot mistake a child's numbers for the session window. Attribution is by the engine's own originating-task id — deliberately not a second `taskId`, which the event identity does not carry and whose absence would silently collapse every child under one parent call — falling back to the parent call id; a turn that answers neither is dropped rather than filed under an invented row, because merging two children's ledgers is worse than missing one. Cache-read tokens are read from the **engine's own shape** rather than the mirrored one, since the mirror fills that member with zero when the wire omits it and reading it there would erase the difference between *not reported* and *no cache hit*. A turn that reported no usage at all still counts as a turn and still adds its zeros — the numbers are a lower bound, and dropping the round would make the bound less true, so the honesty bit rides on the row instead and is never spelled `false`; such a round still emits its live arm, because the frame that says "this round has no account" is the one a real-time consumer most needs and the easiest one to drop. The same honesty bit also survives a terminal that carries no statistics at all: what the stream observed is unioned with what the final record says, so a run that already reported an unmeasured round cannot come out the other end looking like an exact zero. Finally the table says whether it is **partial**, and that verdict is anchored on the quantity that actually decides it: the engine's own totals. Turn count and row count must both reconcile before the table claims to cover the whole run; anything else — including totals that cannot be read — marks it partial, so the failure direction is always the safe one (a complete table called partial, never the reverse). The two accounts are kept separate and are never added together or used to correct each other. One more thing the totals cannot settle: the row key has **two namespaces** — the originating-task id and the parent call id it falls back to — and nothing upstream promises they are disjoint, so the same literal can name one child's identity and another child's parent call. Accumulation therefore keys on the origin as well as the id; the delivered table still keys on the bare id, and a cross-namespace clash is merged into one row that says so, with the partial verdict forced, because a row count and a turn count can both reconcile while the attribution behind them is wrong. The table itself is likewise a **per-stream snapshot** handed to the terminal projector by value rather than left on the caller's context: the three terminal projectors are public, so a host may drive one run through the stream and project another's terminal directly on the same context, and a table left behind would be attributed to whoever projects next — silently called complete whenever that run's own totals happen to match. Without a snapshot, both table keys are simply absent |
|
|
354
|
-
| `scripts/run-result-text-backfill-test.mjs` | What happens when the terminal frame's answer text and the text already on screen do not match. A turn's answer normally streams in and the terminal frame carries the same words again, so the two agree — but when the connection drops mid-answer and the reconnect brings the finished version, "this turn already produced assistant text" is true, the terminal fallback is skipped entirely, and the screen stays permanently short of whatever arrived while the stream was down, with nothing to say so. Four cases are pinned. Nothing on screen yet: render the terminal text whole, byte for byte the previous behaviour. On-screen text is a **prefix** of the terminal text: emit only the missing tail, and the guard measures the deciding quantity — the total bytes that reached the screen must equal the terminal text, which fails both for a missing tail and for a re-render that would print the first half twice; when the two are already equal, nothing is emitted at all. Terminal text is a prefix of what is on screen (an engine-side trim): touch nothing, since there is nothing missing and overwriting with the shorter version would erase what the reader already saw. Neither is a prefix of the other: emit **nothing** and raise a fact instead — which version counts is the engine's to say, and appending the terminal version after the streamed one composes a passage nobody ever wrote. That fact carries lengths rather than text, so a renderer is not handed a third version to choose from, and its declared duty is to *reword* the transcript line, never to render more. A cross-segment case proves the comparison reads the whole committed answer rather than the last segment, and the whole thing is driven through the real two-stage path rather than hand-built messages. The comparison only holds if both sides are the same kind of thing — one assistant message against one assistant message — and that depends on the client knowing where a message ends. A tool card is one place a message ends, but not the only one: when a model round finishes and the next one starts writing prose straight away, with no tool call in between, the two passages belong to two different assistant messages even though nothing visible separates them on the wire. Reading them as one used to glue the two passages into a single transcript line, running the end of one sentence into the start of the next, and then handed the terminal comparison a concatenation to check against the engine's **last** message — so a perfectly ordinary multi-part answer was reported to the reader as *the final answer does not match the text streamed above*. The end of a model round is therefore treated as the end of an assistant message: the pending prose is committed and a new message begins. That holds even when the round reports no spend at all, because *this round is over* and *this is what it cost* are two different facts and only the first one decides a boundary. A round belonging to a **sub-flow** decides nothing for the leader, and the test pins all three identity members, empty strings included, against a control that proves the same shape really does divide when no identity is present. The rest of the section is regression: a boundary landing immediately after a tool card must not lose the fallback that lets a card-ends-the-turn answer compare against the prose before the card; repeated boundaries with no prose between them must not mint empty messages or lose track of which message the comparison should read; a reconnect that brings the finished answer still backfills only the missing tail; the leg that already carries whole messages is not divided twice; and a segment boundary that arrives **after** the round ended still replaces the segment authoritatively and hands back the row attribution, without minting a second copy of the passage. A last group covers where a message boundary meets a segment that the engine announces late or not at all, and it pins only the half that is unambiguous. A round whose prose the engine never announces, followed by one it does, used to have the first passage **silently overwritten** by the second one's authoritative text — no transcript line for it and an empty attribution list, so a host had nothing to correct; it now keeps its own line and the attribution is handed over in full, with the offset measured on that passage rather than on the authoritative text. Which of the two passages survives a compliant host's rewrite is deliberately **not** asserted: the row named belongs to an earlier message, exactly as it already did at a tool-call boundary, and the underlying cause — the client splitting messages on its own boundaries while the engine announces segments on content blocks — is recorded as a known limit rather than pinned as a desired outcome. Where the second round's prose arrives as a whole block instead, there is no segment announcement at all and both passages keep their own line **in the order they happened** — previously the whole block was written first and the earlier passage only landed at the close, so the reader saw them reversed. An announcement that arrives after its round has already ended, and whose authoritative text merely extends what was shown, is pinned on the two things that are not in question: the bytes on screen add up to the authoritative text exactly once, and the divergence signal with its offset is still emitted so a host can reword. That ordering is unreachable on the installed engine — the announcement is pushed while the message is still being assembled and the round end only after it is complete, both through one synchronous dispatcher onto one queue — so the case is defensive; the late-announcement path exists because a tool call can be admitted before assembly finishes, which a round end cannot. Finally, one guard names a layer seam rather than a behaviour: the round-end frame is folded into a neutral usage arm that carries **none** of the three identity members, so a round belonging to a forwarded background child arrives with nothing to judge and the sub-flow cutoff cannot reach it. That guard reddening is the signal that identity now survives the fold and the cutoff has become effective |
|
|
354
|
+
| `scripts/run-result-text-backfill-test.mjs` | What happens when the terminal frame's answer text and the text already on screen do not match. A turn's answer normally streams in and the terminal frame carries the same words again, so the two agree — but when the connection drops mid-answer and the reconnect brings the finished version, "this turn already produced assistant text" is true, the terminal fallback is skipped entirely, and the screen stays permanently short of whatever arrived while the stream was down, with nothing to say so. Four cases are pinned. Nothing on screen yet: render the terminal text whole, byte for byte the previous behaviour. On-screen text is a **prefix** of the terminal text: emit only the missing tail, and the guard measures the deciding quantity — the total bytes that reached the screen must equal the terminal text, which fails both for a missing tail and for a re-render that would print the first half twice; when the two are already equal, nothing is emitted at all. Terminal text is a prefix of what is on screen (an engine-side trim): touch nothing, since there is nothing missing and overwriting with the shorter version would erase what the reader already saw. Neither is a prefix of the other: emit **nothing** and raise a fact instead — which version counts is the engine's to say, and appending the terminal version after the streamed one composes a passage nobody ever wrote. That fact carries lengths rather than text, so a renderer is not handed a third version to choose from, and its declared duty is to *reword* the transcript line, never to render more. A cross-segment case proves the comparison reads the whole committed answer rather than the last segment, and the whole thing is driven through the real two-stage path rather than hand-built messages. The comparison only holds if both sides are the same kind of thing — one assistant message against one assistant message — and that depends on the client knowing where a message ends. A tool card is one place a message ends, but not the only one: when a model round finishes and the next one starts writing prose straight away, with no tool call in between, the two passages belong to two different assistant messages even though nothing visible separates them on the wire. Reading them as one used to glue the two passages into a single transcript line, running the end of one sentence into the start of the next, and then handed the terminal comparison a concatenation to check against the engine's **last** message — so a perfectly ordinary multi-part answer was reported to the reader as *the final answer does not match the text streamed above*. The end of a model round is therefore treated as the end of an assistant message: the pending prose is committed and a new message begins. That holds even when the round reports no spend at all, because *this round is over* and *this is what it cost* are two different facts and only the first one decides a boundary. A round belonging to a **sub-flow** decides nothing for the leader, and the test pins all three identity members, empty strings included, against a control that proves the same shape really does divide when no identity is present. The rest of the section is regression: a boundary landing immediately after a tool card must not lose the fallback that lets a card-ends-the-turn answer compare against the prose before the card; repeated boundaries with no prose between them must not mint empty messages or lose track of which message the comparison should read; a reconnect that brings the finished answer still backfills only the missing tail; the leg that already carries whole messages is not divided twice; and a segment boundary that arrives **after** the round ended still replaces the segment authoritatively and hands back the row attribution, without minting a second copy of the passage. A last group covers where a message boundary meets a segment that the engine announces late or not at all, and it pins only the half that is unambiguous. A round whose prose the engine never announces, followed by one it does, used to have the first passage **silently overwritten** by the second one's authoritative text — no transcript line for it and an empty attribution list, so a host had nothing to correct; it now keeps its own line and the attribution is handed over in full, with the offset measured on that passage rather than on the authoritative text. Which of the two passages survives a compliant host's rewrite is deliberately **not** asserted: the row named belongs to an earlier message, exactly as it already did at a tool-call boundary, and the underlying cause — the client splitting messages on its own boundaries while the engine announces segments on content blocks — is recorded as a known limit rather than pinned as a desired outcome. Where the second round's prose arrives as a whole block instead, there is no segment announcement at all and both passages keep their own line **in the order they happened** — previously the whole block was written first and the earlier passage only landed at the close, so the reader saw them reversed. An announcement that arrives after its round has already ended, and whose authoritative text merely extends what was shown, is pinned on the two things that are not in question: the bytes on screen add up to the authoritative text exactly once, and the divergence signal with its offset is still emitted so a host can reword. That ordering is unreachable on the installed engine — the announcement is pushed while the message is still being assembled and the round end only after it is complete, both through one synchronous dispatcher onto one queue — so the case is defensive; the late-announcement path exists because a tool call can be admitted before assembly finishes, which a round end cannot. Finally, one guard names a layer seam rather than a behaviour: the round-end frame is folded into a neutral usage arm that carries **none** of the three identity members, so a round belonging to a forwarded background child arrives with nothing to judge and the sub-flow cutoff cannot reach it. That guard reddening is the signal that identity now survives the fold and the cutoff has become effective. Since 0.113.0 it also covers the engine's continuation chain: when the final frame names exactly two committed entries and the last two messages of this run that carry text, joined in order, equal the final text (each trimmed), the turn is treated as one answer and no divergence line is emitted; a 32-case golden fixture taken from the previous build pins every other outcome byte for byte, runs are never joined across a run boundary, tool-only or thinking-only messages do not count, and a run whose screen text was rewritten in place is never let through. |
|
|
355
355
|
| `scripts/run-engine-vocab-floor-test.mjs` | Engine-mirrored vocabularies (structured card whitelist, self-reported tool face, control verbs, recogniser sets) against the *installed* `@sema-agent/core` |
|
|
356
356
|
| `scripts/run-limits-env-failloud-test.mjs` | `SEMA_HEADLESS_*` env-lane limits reject invalid values as loudly as the flag lane (no silent "no budget" runs) The sentence about whether an attached engine honors an explicit limit flag (`limitsSupportNotice`) and the helper naming the explicitly given limit flags (`explicitLimitFlagNames`) are pinned here: no sentence when the capability probe was not observed or the version is at or above the support floor, a "may ignore" sentence below it, a "cannot be confirmed" sentence when no readable version was reported, and only the three limit flag names are ever echoed. |
|
|
357
357
|
| `scripts/run-streamjson-timing-honesty-test.mjs` | Stream timing & terminal honesty ([2084]): held errored fs-write results release on model progress; a wall-clock stop maps to `error_during_execution` with a truthful salvage note; the synthetic API-error assistant row carries the `<synthetic>` in-message sentinel. Also ([2489], core 5.8.0): the run-limit `errorCode` -> CC subtype map is pinned code by code (`limits.max_{cost,turns,tokens,walltime}_exceeded`), token/wall-clock stops keep the text the engine already produced, and the `failed` event arm shares the one mapping point. The 5.7 dual-vocabulary legs retired with server 6.0.0 (which bundles core 5.8.0); four **retirement negative controls** stand in their place — the retired `status:'timeout'` and the retired codes must fall to the honest fallback subtype and must never drop back to an empty success, so putting any of them back turns the gate red |
|
|
@@ -387,12 +387,12 @@ guard still cross-checks the table by name).
|
|
|
387
387
|
| `scripts/run-engine-directory-test.mjs` | The read faces for the cloud control plane's two engine endpoints: the directory of engines visible in a scope, and the "my engine" resolution a client asks for right after login. Four things are guarded because each one is a way to be confidently wrong. An unreadable response is not an empty directory: only an explicit empty list means "no engines here", and a body that is missing the list, or is not an object at all, voids the reading rather than reporting zero. A row that cannot be read is dropped without taking its neighbours with it, but the roster then says how many rows it could not read, so nobody treats it as complete. The relay path on a row is the one string the client appends to the center's origin and sends the user's token to, so it is accepted only as a same-origin relative path — an absolute URL, a protocol-relative form, one that only becomes protocol-relative once the URL parser has eaten a tab or newline, or one with a dot segment (plain or percent-encoded) that the parser would fold into a different path makes the whole row unreadable instead of being cleaned up and let through. And the words the server may add later are read as an open set: an unlisted kind, posture or resolution rule is passed through verbatim rather than thrown on or folded into a known word, while a missing online flag stays "not reported" instead of becoming "offline". The decision on top of the reading never guesses: a target the server named is used only when the candidate roster is present and agrees with it, a target the client could not read is never turned into "no engine", a resolution rule this version does not know with no target neither claims "no engine" nor "ambiguous", and a body whose rule contradicts its target or roster is not acted on. The explicit engine override is checked against the candidate roster only, by exact bytes; a name that also appears on a row that could not be read counts as a duplicate rather than disappearing with the filtered row; and when the roster itself cannot be read the override is refused rather than falling back to connecting by name |
|
|
388
388
|
| `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. |
|
|
389
389
|
| `scripts/run-rewind-archive-capability-test.mjs` | The read face for whether a rewind can restore the code archive, and the honest three-state answer the ends render from it. The shells used to decide this from a local backup table that only their own in-process tools ever fill, so on any session where the engine runs the tools it stayed empty and the two code-restoring rewind modes simply never appeared, while the engine had been keeping a file history the whole time. The judgement now comes from what the engine itself advertises, and each of the four bits it advertises answers a different question: whether the conversation can be forked at a message at all, whether a fork can restore the tracked set, whether a code-only restore is possible, and whether this deployment understands the current spelling of the request key. The mode that rewinds the conversation and restores the code together needs both of the first two, and the engine offers no single bit for that combination, so the combination is made here: one bit stated off is enough to rule the mode out, both stated on make it available, anything else stays unknown — reading the file-history bit alone would offer that mode on a deployment that keeps file history but has no conversation anchors, where the request can only fail. A bit that is absent means an older engine that never spoke about it, which is not the same as an engine that said no, and a bit reported in a shape this reader cannot read is a third thing again — it is recorded as unreadable rather than quietly filed under "not reported", because those two send an operator to different places. One unreadable bit does not discard its siblings; only a response that is not a capability object at all clears the cell. What the ends get is available, unavailable or unknown, and unknown stays unknown: folding it into unavailable would hide the mode again, which is the mirror image of the bug this replaces. The sentences the doctor row can print are checked to be pairwise distinct and to avoid implying a refusal the engine never made, and the spelling of the outgoing request is deliberately not made to follow the epoch bit, since the older spelling is rejected outright by current engines |
|
|
390
|
-
| `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),
|
|
390
|
+
| `scripts/run-hitl-words-readout-test.mjs` | The five wording and reader fixes of the HITL surface. (1) Seven user-facing failure sentences (pending-row fetch, approve and deny decisions on approval cards, the question-card decision, the parked-row wait, local rule parsing, background stop) carry the thrown value's own message and never the error class name — a thrown value with no usable message becomes one fixed sentence instead of a bare class name, `[object Object]` or `null`; the original thrown value goes only to the host log. (2) `resumeContextUnavailableContent` is a single English source (three ` · ` segments, with and without a run id, the retention seconds only when the engine gave them) and a plan-review decision that meets a 409 `conflict.resume_context_unavailable` says it. (3) The plan-review `permissionModeAfter` pre-check no longer mirrors the server's decision pairing: only the value set is checked locally, and the engine's `request.field_conflict` speaks for the rest. (4) `requestEngineCompact` reports how the fire settled through `onSettled` (accepted / refused / failed / offline); manual compaction is sent on the run bind; a 404 is retried on a five-step ladder (six requests at most, last within 3.5 s); a 409 `compact.not_running` carries each request over once (will-retry notice, no callback) and only a second one settles it with the final notice; a second request during the carry-over merges into the same send and each settles on its own allowance; other refusals are not carried over. (5) Every exported `project*Capability` reads only own data descriptors: an accessor is never run and reads as `undefined`, and a throwing descriptor read is swallowed the same way. Pinned with throwing and class-named values at all seven sites, both plan-review entry points, the compact outcomes under 409 / 404-retry / 5xx / the 60 s injected deadline, and a per-projector throwing-proxy row plus a source scan. The decision exits treat hostile thrown values uniformly: a throwing message getter, a throwing toString, a revoked proxy, or throwing status, error-code or name getters all end in the thrown value's own message or the fixed fallback sentence, never in the getter's own error text, and the transient-failure classification reads each slot independently. |
|
|
391
391
|
| `scripts/run-hitl-wave2-test.mjs` | The second HITL batch of this release. (1) On the live approval leg an aborted card sends exactly one interrupt deny unless the engine's closing frame has already withdrawn the card (then, like a retracted card, nothing is sent); the resolution is `unsettled` / `interrupted`, the same as the durable leg, and the CC-514 regression (durable leg already decided ⇒ closing frame withdraws ⇒ nothing sent) is pinned. (2) The stream is no longer held while a card waits for a person: with a card on screen, later frames of the same stream leave the bridge within a bounded wait; a settlement frame for the same approval withdraws the card through its abort signal and nothing is sent from it; a host that ignores the signal still sends nothing after that point, and when the settlement says the approval was parked the user gets the existing "answer it again on the parked card" sentence; an answer given before the settlement is sent as before; a refusal frame that arrives while the answer is in flight still carries the refusal stamp; the in-stream frame and the suspended-ask path share one single-holder table with the parked leg, so the same approval shows one card and sends at most one answer, and the second caller gets the holder's outcome marked as joined; leaving the bridge does not withdraw a waiting card. (3) One reader produces the "why was I asked" sentence: when the structured cause is a record, the package's own sentence comes first (naming the check code only when it has the engine's code shape), followed by one space and the probe's own prose when present, passed through the untrusted-text display exit; the prose alone when there is no structured cause; nothing when neither is present or the card cannot be read. (4) One reader turns the engine's structured subagent state into a sentence, keeping status and approval words verbatim and computing the last-activity age from the engine's own read time. |
|
|
392
392
|
| `scripts/run-park-decision-layer-test.mjs` | The decision layer behind the "stuck behind a card" family, shared by every client. A pending row that is **not in the queue** is three states, not one: a bounded, interruptible re-probe loop distinguishes *a decidable row*, *not born yet* (no positive evidence that anything settled — an empty queue proves nothing) and *settled elsewhere*, always probes at least once so a zero budget keeps the pre-fix semantics verbatim, cuts a hung read face off at the window rather than only noticing afterwards, and reports the honest failure when the window is spent instead of inventing a decision. The decision-note reader is likewise three-state: an explicit `noteRecorded: false` outranks an echoed note body, absence renders **no line at all**, and untrusted note text is flattened and bounded before it ever reaches a renderer. Row routing anchors on the deciding quantity — a row carrying `gateKind: "human"` with `toolName: "Write"` is a tool gate, because `human` is the engine's *generic* "someone must decide", not a synonym for a question — and the queue scan refuses to surface a row it cannot positively prove belongs to this session. A chain that fails after the row vanished is split by whether a card was ever presented: decided-elsewhere, or not-its-turn-yet. A row-level single-flight makes "at most one card per pending item" structural rather than incidental. The resume three-way card pins the option **order** (the zero-effect choice sits at index 0, because the frame carries no default-focus field and a stray Enter must not attach or cancel), renders only options the wired verbs can honour, collapses every ambiguous answer to zero action, omits the liveness line entirely when the engine gave no evidence, and — when there is no card lane at all — prints three real routes and exits on a dedicated code rather than reporting success. Ownership is three-way too: a pending row is *owned*, *provably foreign* or *unknown*, and "not mine" needs positive proof just like "mine" — a readable own-session id, a row session id read from an own data slot that differs from it, no delegation link back to this session, and no own-run hit. Anything unreadable (accessors, proxy traps, non-string slots, a missing own-session id) stays *unknown*. The owned-pending count skips a malformed row of another session instead of failing as a whole, and still fails on a malformed row that is ours or cannot be placed |
|
|
393
393
|
| `scripts/run-selfheal-reopen-test.mjs` | The 409 active-run self-heal decision chain: `governanceForced` narrows on strict `true` only; triage prefers the wire's `pendingGate.kind` and falls back to the status table (an off-table kind is never guessed into a card arm — hands-off plus the honest wording); a first-sight card makes zero closed/reopened claims and a host presentation receipt of `presented: false` demotes the outcome to reopen-failed; park-row ownership is a fail-closed positive proof (own-run ledger or session id — unprovable is not owned); the three gate-identity key literals live in exactly one mint (`hitl/gateIdentity.ts`, AST string-token scan); the armed-gate presentation ledger is per-session; and the `plan_review` reopen arm shares the arm arm's card body, three-state verdict and delivery pipe, consuming the presentation history once a decision is delivered. The same chain also carries the `running` three-way card: both plan-family gate kinds route to the plan arm and all four ask-family kinds to the ask arm (an off-table kind still never gets guessed into either); the card is offered only for verbs that can actually be honoured and a missing presenter means zero action rather than a silent cancel; a steer is sent **exactly once** with its three delivery outcomes worded apart (a `queued` receipt is the wire correcting the triage input, so the named park word decides which card gets reopened, and an unrecognised park word drives neither arm), and a steer failure is split into *provably not delivered* (4xx) and *delivery unknown*, because telling a user to resend a non-idempotent instruction that may already have landed is how duplicates get made. After a user-chosen cancel, "the session is free" is asserted only from a whitelist of terminal states — park states hold the claim, an unrecognised state word is not a release, a failed read is *unknown* rather than a release, and only a 404 counts as one — and the honest timeout line quotes how long it really waited. The two "card could not be reopened" rows can carry a host-declared way to keep the conversation, which says the card comes back on resume only if it is still waiting: it is placed before the route that abandons it, never offered for an injected submission, while a decision is still on its way, or once the pending approval has been proven gone (the outcome then carries a flag saying so; the proof only counts before the cleanup card is shown, so a fallback after the card carries no flag unless a fresh read finds the run finished, and a recheck that finds the approval back clears it), the host function is not even called in those cases, and it is treated as unavailable when it throws or returns an empty value; a host can also switch off the engine decide route on the interactive rows while the cancel route stays, and with neither given all four rows are pinned byte-for-byte to the text the previous release produced. When the plan reopen refuses because the user closed that card (`dismissedByUser`), the outcome carries the flag and one package-owned line says so — you closed it, a message brings it back — in the typed, the injected and the queued-steer forms, with no way-to-keep and no engine decide route; the reopen port receives `{ trigger }` mapped from the submission origin (typed ⇒ `user`, injected ⇒ `automatic`, absent ⇒ the old single-argument call). When the reopen refuses because no view is subscribed to question frames for that session (`_sema_noPresentationSurface`), the plan and ask outcomes carry `noPresentationSurface`, and the five lines that talk about the card not coming back — both arms' typed line, the queued-steer typed and injected lines, and the injected not-delivered line — add one package-owned clause saying the session has no view that can show decision cards, byte-identical to the plain refusal otherwise; loose values and the other refusals (answer in flight, user-closed card, plain refusal) carry neither the flag nor the clause. For a host that reopens under its own session key with a presentation receipt, the suite drives the whole sequence end to end: subscribing for that key, reopening, and registering the receipt through the per-session call `registerArmedGateFromQuestionIdFor` gives `reopened: true` with the right first-sight reading, a second reopen reads as seen before, a decisive answer (with no extra bookkeeping from the host) makes the next reopen read as first sight again, and a dismissal does not; registering through the default-session call instead leaves the receipt window to run out with `{ reopened: false }`. Every bit the package reads off a host's reopen verdict is read once, inside the guard: a bit whose getter throws (or a proxy that throws) is treated as absent, so the self-heal still settles — with exactly the outcome it would give if that bit were missing — and the rows never throw, including for an outcome the host built itself; a plain verdict lands in the queued-steer outcome byte for byte, key order and any extra keys the host added included, a key whose getter throws is the only one left out, and an own key literally named `__proto__` is copied as a key rather than changing the copy's prototype; symbol keys are copied too and a verdict with a null prototype stays one (compared key by key, value by value and by prototype, not through JSON); and an outcome whose `reopened` property itself cannot be read is phrased as if it had none. The two reopen-failed outcomes carry `errorCode` only when the reopen gave up because a request failed — the outer wire code of that request (a folded policy code is read through `blockedBy`), taken from a reopen port that threw, from the host verdict's `_sema_errorCode`, or from the stale-park arm's own three requests; a later request that changed what the reopen gave up on (the poll finally answered, the row appeared), a failure with no wire code, a reopen port that failed after the caller had already aborted, and the three judgment refusals (answer in flight, user-closed card, no view) all leave it absent, while a read that only confirms (the row is still pending after a failed decision) keeps it; only a request that failed counts, never a field that cannot be read on a record that did come back; and every row is byte-identical with or without it. The error objects the self-heal leg reads (thrown by the run lookup, the steer and the cancel) are read inside the guard too: a `status` or `message` whose getter throws, or a proxy that throws on any read, is treated as absent, so the self-heal still settles instead of rejecting, and the at-most-once failure class reads such an error as delivery unknown; the same goes for the run record a successful lookup returns — when its `status` cannot be read, the self-heal treats it as a record without a status rather than rejecting — and for a steer receipt, whose two readers answer `null` for a field they cannot read. When the delivery of a steer is missing, unreadable or an unknown word, the injected-submission line says the message was accepted but its delivery was not reported and to watch that reply before deciding whether anything needs sending again, instead of the handed-over wording, and it never says nothing is needed, with or without a follow declaration. The `running` three-way card also gives a fourth way out, *attach*, under the same single rule as the resume card (the run is still running and can be taken over): one shared predicate decides for both cards, only a strict `true` supply flag counts, and a host that does not state the flag gets the request byte-for-byte as before (no `canAttach` key at all, which is not the same as `false`). Choosing it hands the decision to the host without sending any write request (no steer, no cancel), the refused message is reported as not sent, and an `attach` answer on a card that never offered it is zero action. The resume card's two cancel closing rows, confirmed and unconfirmed, are mutually exclusive and come from one private mint. A follow-up message carrying a plan-review outcome is never passed into a reply already in progress: when every queued item in that submission is such an outcome naming the very run that holds the session, the outcome is dropped as redundant (whatever effect it recorded) and the host is told to follow that run; otherwise it is held and released for a new message exactly once, when that run is read as finished or paused. Submissions without such an item — and every typed submission — behave byte for byte as before. The same holds when the run that holds the session has already ended: an outcome naming that run is dropped as redundant and the host follows the end of that run, instead of the outcome being reported as not delivered — on the in-progress path and on the stale-pause path alike; the redundant sentence no longer says the reply is still in progress. The plan-review carrier list is read exactly once per self-heal on every path. |
|
|
394
394
|
| `scripts/run-terminal-identity-copy-test.mjs` | Terminal-state **identity**, in both lanes where a stop gets a name. A run stopped by this deployment's own governance knobs — the open-set `limits.*` family, `output.invalid`, and the `blocked` contract terminal a ReportBlocked agent produces — is not a provider failure, and labelling it `API Error:` sends the reader to check the network, the key and the quota when the handle is the `--max-turns` they passed themselves. Those terminals now render a neutral row; the reverse direction is guarded just as hard, because asserting "this is *not* an API error" on a code the package does not recognise is the same misfiling pointed the other way — a real `gateway HTTP 502`, a `conflict.session_active_run` and any unknown code all keep the `API Error:` prefix, and the row keeps its `_sema_api_error_message` class flag (spelled `isApiErrorMessage` before 0.83.0) so brief-mode visibility filtering does not silently drop it. The second half is who the rejected submission belonged to: the self-heal copy told every caller "Your message was NOT sent … send it again", which is three separate untruths for a system injection (a plan-review outcome, a cron wake-up, a task notification) — not the user's message, and not re-sendable, since a host queue marks those non-editable and non-recallable. The injected form says so instead, and the one sentence that promises re-delivery is pinned to the single disposition that earns it: `selfHealSubmissionDisposition` is the same function the host consults before putting the item back on its queue, so the promise and the behaviour cannot drift apart, and the arms where no card could be surfaced state plainly that nothing was delivered and nothing will retry. Since 0.72.6 the same gate pins the **follow intent** after a steer (): a message handed to a live run only pays off if someone tails that run's own event stream, so `steerFollowIntent` decides from the delivery word whether to tail now, after the pending decision, or only after a wake — and the "watch that run" sentence ("watch that reply" on the rows about follow-up messages sema sent on its own) turns into a factual "sema is following that run" ("… that reply") **only** when the host declares it attached that tail, so a shell that did not wire it can never claim it did. Since 0.83.0 the rows about follow-up messages sema sent on its own carry no engine-internal words and say what happened per delivery shape, promising a resend or "nothing for you to do" only where the code guarantees it |
|
|
395
|
-
| `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 |
|
|
395
|
+
| `scripts/run-seat-wave2-test.mjs` | The seat capability table, the folder picker and the session folder, added in 0.94.0. Whether a renderer may offer an optional seat action is answered only by the capability table the host publishes (`seatCapability`: reported, not reported, or unreadable — reading own data properties only, never running a getter), never by calling the action and watching it fail; "not reported" is neither folded into "available" nor into "cannot", and the disabled sentence comes from one place (`seatCapabilityUnavailableDetail`). The subscription helper subscribes before it reads, so a renderer that subscribes after the host has published and then withdrawn a capability sees the withdrawn state on its first callback. `pickFolder` returns a non-empty path or `null` for a cancel, and the session record's `cwd` is the folder the session was started in, absent when none was chosen. Since 0.113.0 the file picker `pickFile` follows the same rules: a non-empty path returned byte for byte or `null` for a cancel, judged by the same single path check as `pickFolder` with a refusal that names `pickFile`; it has its own capability name (reporting `pickFolder` says nothing about `pickFile`) and its own three disabled sentences. 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 |
|
|
396
396
|
| `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 |
|
|
397
397
|
| `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. (0.99.0) The per-table length numbers (`ENGINE_NOTICE_CODES: … // N`) now live only in a standing section of the integration contract (`### 2x`, the table-length ledger) and are checked against the source tables there; a length line anywhere else, released sections included, fails the gate, so adding or removing one word edits one live line and never a released section. And (CC-784) in the in-flight changelog section, every `§NN XX-` entry prefix named by a `> 主题:` topic line must have at least one entry line in the same section, so a merge that silently drops entry lines while the topic line survives is caught. |
|
|
398
398
|
| `scripts/run-esc-halt-plan-test.mjs` | The Esc stop decision every client shares: fire the **turn-level** halt first, and escalate to a **run-level** cancel in exactly two cases — the engine itself answered with a 409 from the closed code set (it is saying "there is no in-flight turn here; use cancel for a run-level stop"), or that shot came back with no verdict at all *and* the shell can independently prove a permission card was on screen. Everything else does not escalate. The asymmetry is the whole point and every negative control guards the same direction — deciding *not* to escalate costs the user one more choice on a busy-session card (recoverable), deciding to escalate wrongly tears down a run that was alive and takes every in-flight tool with it (not). So: the closed code set is a **frozen** value, not a `ReadonlySet` — type-level immutability does not stop a consumer's `.add()`, and the guard proves it by really trying to mutate the exported value and then checking the verdict did not drift; the escalation gate is the **conjunction** of that closed set and the 409 status, since honouring the code alone lets a 500 that merely quotes it drive a destructive call; `interrupt.not_held` and `steering.not_running` are deliberately outside the set (the first means *this replica* has no live face — the run may be perfectly alive on another); an unreadable code falls to the no-escalation side; a `parked` flag never overrides a verdict the engine did give, and only strict `true` counts when it did not. The first shot is unconditional by construction — it does not consult `parked`, because the 409 it earns is exactly the verdict the gate wants — and the verdict itself is a closed machine-readable reason word, not display copy. A third escalating case was added once tearing the stream stopped reaping the run: with detach armed, a shot that never lands leaves the run going all the way to the end of the turn, so the Esc the user pressed has no effect at all and nothing on screen says so — the old behaviour had a silent backstop (tearing the stream ended the run) and that backstop is gone. The new fact is held to the same three disciplines as `parked`: it is read only where the engine gave no verdict, it is judged **after** `parked` so an existing host's reason word does not change under it, and only strict `true` counts. Absence is proven to be a no-op rather than asserted — the guard carries its own reference implementation of the previous version's table, runs the full grid through both, requires zero divergence when the new field is omitted, and first shows the comparison really does report a difference on the one cell where the two versions are meant to differ |
|
|
@@ -455,7 +455,7 @@ guard still cross-checks the table by name).
|
|
|
455
455
|
| `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. From 0.108.0 the projection carries the names of the A2A peers the response declares (`a2a`), judged on the raw response before any parsing: a response without that domain reads as absent (an older server), one that is there but cannot be read reads as unreadable, and neither is ever an empty peer list, which only a present domain gives; the cell that parses first with the settings contract shows the absence turning into an empty list, names the wire-name rule rejects are counted as unreadable rows rather than shown, a real response built by the settings contract reads back with its peer names, and the warning row for keys the settings contract stripped from a peer goes through the existing warning reading and sentence. |
|
|
456
456
|
| `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. The managed http-hook lists (allowed URLs and allowed environment variables) are read when the hooks plan is assembled and applied to every http hook that would be sent to the engine: absent means no restriction, an empty list allows nothing, a malformed list is read as empty and named, a credential in a URL never appears in the plan, and the lists are read from every settings layer the contract admits (the managed, user and flag layers; the project and local layers are never read), combined by intersection when several layers write one, with each dropped hook naming the layer whose list dropped it; a layer whose settings cannot be read counts as an unknown list and no http hook is sent that turn. |
|
|
457
457
|
| `scripts/run-memory-saved-projection-test.mjs` | Engine memory writes (a successful `Remember` tool call) moved off the transcript onto the additive `memory_saved` chrome event, driven through the real pipeline: zero transcript rows for the write (the transcript is byte-identical to the same frames with the write reported as not successful), exactly one event whose `notes` carry the note text verbatim (notes, not file paths) and whose key set is exactly kind / laneProof / id / notes; no event for a missing, empty or non-string note, a non-`true` `ok`, a tool error, a missing result or another tool name; two writes give two events in order with distinct ids; a sub-agent write rides the sub-agent lane and an empty parent id emits nothing rather than falling back to the main lane; the `id` is derived from the write's wire key (the tool-end event id, else the tool-start event id, else the call id; empty ids count as absent), so projecting the same wire events twice gives the same id, and it never collides with the tool result row of the same or another call; the event sits right after the tool result row; the arm is registered as required. |
|
|
458
|
-
| `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. |
|
|
458
|
+
| `scripts/run-result-frame-projection-test.mjs` | Result frames and the synthesized terminal rows. The CC key `terminal_reason` is minted on result frames only where it follows from what the engine reported: `completed` on success, `max_turns`, `budget_exhausted` and `structured_output_retry_exhausted` for the three matching engine codes, on both the done-frame path and the failed-event path. Every other outcome leaves the key absent as an own property rather than present with an undefined value: wall-clock and token-budget limits, the classifier denial limit, cancellation, unknown codes, blocked, paused, unreadable or missing terminal records, and the busy-session refusal. The public reader `terminalReasonForResult` shares the minting predicate and is checked to agree with the minted key on every frame the gate produces. Both the minted key and the reader derive the word from the frame's CC subtype (success with `is_error` strictly false, and the three limit subtypes), not from the error code, so a replayed row whose status is paused, blocked or unrecognised never carries a word that contradicts its subtype. The four words are checked against the mirrored CC union, and the minting file is checked to hold no hand-copied code literals. The renamed superset keys (`_sema_error_code`, `_sema_salvaged_result`, `_sema_model_degraded`, `_sema_selected_model`, and the row flag `_sema_api_error_message`) are driven through the real stream pipeline. Each must be present under its new name, the old name must be absent, and every frame the gate saw is swept for old names. The selected model appears on error envelopes whenever the terminal record carries it, and never on a failed event, which has no record. It stays separate from the provider-reported model name. The two in-package readers still work: the interactive result arm reads the salvaged text under its new name, and the print init gate treats the renamed flag as the run having ended. Since 0.86.0 the result arm no longer reads `result` on an error envelope, meaning a frame whose subtype is a string starting with `error_`, including error subtypes it does not recognise yet; a compile-time check pins that every CC error subtype carries that prefix. Such an envelope that carries the salvaged text only under the old `result` key yields no answer row, no appended suffix and no divergence event; when both names are present, or the new one is present but not a string, the old one is never read. Everything else reads `result` exactly as before, in all three producing cases (whole answer, missing suffix, divergence): success frames, a success frame flagged `is_error: true` (checked equal to the same frame with `is_error: false`), and frames whose subtype is a string without that prefix (empty, unrecognised, `Success`, a bare `error`), absent, or not a string (each checked equal to the same frame with subtype `success`). From 0.94.0 four root readers return the result frame's string superset keys (`resultErrorCodeOf`, `resultSelectedModelOf`, `resultSalvagedResultOf`, `resultParkFailedReasonOf`): they read only the frame's own data properties, never fall back to the retired names, run no getter and never throw. A synthesized terminal row that carries `_sema_api_error_message: true` has its text credential-washed on both projection paths (minted by the stream projection, or rebuilt by `adapt()` from an older transcript row), so the flag itself tells a client the text is already washed; model rows are left byte-for-byte alone. Since 0.113.0 the success arm also carries `_sema_result_entry_ids`, a verbatim copy of the engine's `resultEntryIds` (order kept, no de-duplication, an empty list copied as is); a missing or malformed list, or any other arm, leaves the key out. |
|
|
459
459
|
| `scripts/run-layering-shadow-export-test.mjs` | Same-name shadows across the first-party clients that consume this package (terminal, desktop, web and the admin console). Each client's product sources are read at the local clone's `origin/main` (or its HEAD when there is no such ref), without fetching, and parsed with the TypeScript parser; every top-level runtime export the client declares itself is compared with this package's public runtime exports. The guard prints which ref, commit and commit date it read for each client, and warns (without failing) when that commit is more than seven days old, because the result then only describes that older snapshot. A client-side declaration carrying the name of a package export means a piece of shared logic now lives in two places and can drift apart. It fails the guard unless it is listed in `scripts/layering-shadow-exemptions.json`, and a listed row must carry one due-by version no more than three minor lines ahead (it fails once the package reaches it; that single field is the only one the guard reads for expiry, and the older retire-by and review-by fields that still sit on the rows for one more version are only checked to agree with it). It also fails once the client has removed the shadow and the row still stands. Every row also names what kind of duplicate it is, and carries the evidence that kind requires. A client copy that is this package's own function object behind a type assertion, or a thin wrapper that only passes the client's own dependencies into this package's function, must be proven so on every run by reading the client's syntax tree; such rows are due for review within two minor versions, and the guard fails as soon as the proof no longer holds. A wrapper whose extra logic the client has confirmed to be host-specific carries that confirmation (the client's own wording and where it was stated) together with a pinned review record that is due within two minor versions. A wrapper that adds its own decisions, or a same-name function that does something else, carries a review record pinned to a hash of the normalized client declaration (comments and formatting do not count); once the client's copy changes, the guard fails until it is reviewed again. A second implementation of the same logic carries a semantic diff: the client's copy is taken from the commit being read, the named declarations and the client modules they import are extracted with the TypeScript parser, transpiled and run in memory against this package's build on the same inputs, and any disagreement outside the classes registered on that row fails the guard (each class is a fixed predicate over both answers). A separate register lists same-meaning copies that carry a different name (a name-based comparison cannot see those): each row names the client declaration and the package export it duplicates, and fails when it is malformed, past its due-by version, or when the client no longer declares that name; the register only records and checks liveness and expiry, it does not scan for such copies. A row can follow a client-side rename to a new name, and can be registered ahead of a client change for a limited number of versions before the client code exists. Re-exports of this package's own exports are the intended form and never count. A client tree that is not present is reported as a skipped section, not as a pass. The ruler proves itself on an in-memory fake client (planted shadows must be caught, legal forms must not), on a throwaway repository (a missing `origin/main` falls back to HEAD, a broken one is a fault rather than a silent fallback), and refuses to report zero on a client whose scan surface is empty. |
|
|
460
460
|
| `scripts/run-display-untrusted-projection-test.mjs` | The single display-safety outlet (`displayUntrusted`) and the credential wash on the end-of-run rows this package mints. The outlet composes two credential nets (URL structure: userinfo, every query value, the fragment, path parameters and path segments that start with a known secret prefix; key/value words such as `Authorization: Bearer ...`, `Authorization: token ...` or `api_key=...`, plus well-known secret literals that appear without a label, such as `sk-...`, `ghp_...`, `AKIA...`, JWTs and the body of a PEM private key) with three character nets (control characters, bidirectional and format characters, whitespace folding). The credential nets match on a view of the text with ANSI sequences, format characters, control characters and the outlet's own escape tokens stripped, and map the result back onto the original, so colouring or an invisible character wedged between a label, its separator and its value cannot hide the value, and no stray marker is left behind. Whitespace of any length around the separator is accepted. Hosts, ports, paths, query key names and surrounding prose stay byte-for-byte, clean text comes back unchanged, the result is idempotent (also with a length cap), a length cap never splits an escape token or a surrogate pair, an invalid cap means no cap, and every net can be switched off on its own. A few narrow shapes are left alone because they name something rather than carry a value (a plain English word after `bearer` or `basic`, a back-quoted credential variable name, a plain integer after `tokens:`, a list of key names after `keys:`), each with a counter-example that is still washed. Regional flag emoji built from tag characters are kept whole. The existing single-line helpers (`escapeDisplayControlChars`, `collapseLabel`, `capForDisplay`, peer sender names and the hook failure banner) run on the same engine and, since 0.96.0, read the same character table as the outlet: each is checked equal to the outlet with both credential nets off (escape without folding, fold then escape, space mark then fold, and the same length cap) over a seeded pool of more than 600 strings; every character class the old sets left alone (zero-width characters, the word joiner, invisible operators, U+FEFF, the soft hyphen, tag characters and other format characters, plus C1 controls, bidirectional controls and lone surrogates in sender names) is marked class by class, while joiners inside emoji sequences and joining or Brahmic scripts and RGI regional flags stay as written; no character the old sets marked is ever left raw, input without the newly marked characters stays byte-identical to the previous output over every BMP code unit plus random strings, all of them are idempotent, and neither `capForDisplay` nor the hook banner's 512-character cap splits a `\u{…}` escape token or a surrogate pair. The approval decision-note echo, the subagent resume receipt (and its failure debug line) and the startup list of plugin hooks that will not run now also drop bidirectional and format characters (and, for the receipt, C1 controls); a note that is empty after cleaning is treated as absent. The synthetic end-of-run rows (`API Error:`, `Run stopped:`, `Model output error:`, `Outcome unknown:`) and the result frame's `errors[]` pass both credential nets before they leave the package, on the print lane and on the interactive lane (which also keeps the row-class flag); this covers a blocked reason whoever wrote it, while assistant text rows, a successful `result` and salvaged output are never touched, and a non-string `errors[]` entry is passed through unchanged. The known-secret-prefix check is a local copy of the configuration package's detector and is compared with the installed one entry by entry. Since 0.83.5 the outlet has two opt-in switches and a position read-out. `escapeBackslashes` (escape form only) writes every literal backslash as a pair, so each output decodes back to exactly one input (a real invisible character and its literal six-character spelling no longer look alike); a reference decoder round-trips thousands of random strings, the output is byte-identical to the default when the input has no backslash, a length cap is measured on the paired output and keeps the longest fitting prefix, credentials are washed exactly as in the default, and the switch is not idempotent by design (use it only at the final render). Zero-width joiners and non-joiners are kept only inside emoji sequences drawn as emoji (so not between symbols such as © or ™ that display as text) and between letters of scripts where they change the shaping (joining scripts such as Arabic, and the Brahmic family), each listed script checked both ways; the one other place a joiner is kept is right after a virama at the end of a word, the older spelling still found in Malayalam and Bengali text. Next to Latin, Cyrillic, CJK and other letters, next to modifier letters shared across scripts, at the start of a word, at the end of a word without a virama before it, or on their own they are now marked. `blanks` marks characters that look like a space but are not an ASCII space (no-break and other width spaces, the ideographic space, Hangul fillers, the blank Braille pattern) before whitespace folding, for names that must never look alike. `displayUntrustedMarks` returns the same text plus the position of every character mark; its text is compared with the outlet over thousands of inputs. With both credential nets off, the character face stays byte-identical to the previous release for input without joiners. The credential nets read escape sequences the way a terminal would when one is cut short: an unfinished colouring or character-set sequence interrupted by another one is dropped as a whole, a sequence never takes the `@` of an address as its final character, and a final character that starts a well-known secret literal (`sk-`, `ghp_`, `AKIA`, a JWT) is also read as the start of that literal; escape tokens this outlet writes are read as one unit, while look-alike text it never writes (an upper-case `\U`, or a code point it never marks) is read as plain text. A URL is cut before a run of non-ASCII blanks followed by a credential label or scheme word, Hangul fillers and the blank Braille pattern count as spaces around a label's separator, and a value that itself starts with a quoted label (`token= "password":"..."`) is left to that inner label. With `blanks` on, a blank written as an escape token right after a label is read as a blank when the value is judged, so `password:` followed by a no-break space and `missing` stays unmasked and an empty value gets no marker; the credential nets also read the text the way it looks after default whitespace folding and combine what each reading masks, so whatever the default form masks stays masked with `blanks` on (checked over a seeded corpus for the escape form, with paired backslashes and without folding; the exceptions are text that itself contains a literal six-character blank escape, which cannot be told apart from one the outlet wrote, and the dot and space marks, which cannot tell a marked blank from a real dot or space). A lone surrogate wedged between a label, its separator and its value no longer hides the value: the credential nets treat it exactly like a format character, in the machine-readable wash, in a single pass of the display outlet, and on the end-of-run rows and the result frame's `errors[]` on both lanes, while lone surrogates anywhere else are left byte-for-byte. Since 0.84.1 an address-shaped value (`scheme://…`) that sits right after an invisible single character — a format character, a lone surrogate, a non-whitespace control character, or the outlet's own escape token for one — is no longer let through as an address: the scheme stays and the host and path are replaced, with the query and fragment masked as before; `user:<password>@` followed only by stripped units and then a boundary, a port or another `@` is masked as userinfo. A value after real whitespace or after a colour sequence is still treated as an address, and clean text stays unchanged; forms that the previous release masked are checked not to leak on the same outlets. A `user:<password>@` candidate that is the value of a credential label or scheme word — including a label or scheme word split by stripped characters, and a quoted value that goes on past whitespace — is left to the label pass, so the whole value is masked as in the previous release; both reported shapes and frozen samples from the targeted pools are checked on every outlet. A scheme-less `user:password@host` whose password holds a double quote, backtick or angle bracket is still masked when a credential label or scheme word sits right before the user name, or when the user name is a plain word that is not itself a label, the host has a real shape and the address is not wrapped by that quote; quoted JSON values, quoted or bracketed e-mail addresses and quoted arrays stay byte-identical. When a label inside a quoted value ends right at that value's closing quote, the quote and separator stay outside the mask, and an address that begins the rest of a quoted value counts as an address only up to the next blank, so washing the output again never changes it. The credential stage now repeats its single scan on its own output until nothing changes (the cap counts rewrites, at most eight; clean text takes exactly one pass), so masking the output again never changes it for every input that settles within the cap — the random, exhaustive and grammar pools and the two known step-by-step constructions settle in at most three passes counting the confirming one, and the searched constructions kept in the pool in at most six; a deliberately constructed input can need more rewrites as it grows, and at the cap the last rewrite is returned, which a further pass can only mask more of. The checks cover the pass cap and how reaching it is reported, single-pass clean text, a grammar sample of the busiest shapes, constructions that once advanced one step per pass, the mark positions on the final output and how positions compose across rewrites. A username that is itself a credential label with a quote or angle bracket in the password is masked as userinfo (also with only the URL net on) unless the label's own value really reaches the `@`; a URL parameter such as `;token=...` or `,token=...` stays inside the URL piece; a private-key header the outlet cannot recognise does not hide the labels inside its block; the tail glued after the host of a label's address value is masked to the value's end; and a value this package's own transcript wash split at a carriage return is masked when the display joins it back. A bare `Token <value>` / `token <value>` (no label, no separator) is now washed like `Bearer`, with a wider allow-list of ordinary English words after `token` so prose such as `token count: 1234` or `token expired` stays readable; a `/token` URL path tail followed by a status code is not a scheme word and stays as is, and neither is `token` right after `unexpected` in a parser error such as `SyntaxError: Unexpected token '<'`, which stays readable. The `token` scheme word only adds masking of its own value: label values and `bearer` / `basic` values are read exactly as in the previous release, so a value that starts with another character, or a `bearer` value, followed by `token`, a non-ASCII space and an address is still masked whole. When a credential label or scheme word is followed by a colon and a YAML block scalar header (`\|` or `>`, optionally with `-`, `+` or digits, invisible characters allowed between them, and nothing else on that line but blanks, invisible characters or a comment, ending in a line break), the whole indented block becomes a single marker: the block runs until the first line with visible content that is not indented deeper than the label's line, blank lines included, and the line break before that line and the line itself stay as they were on outlets that keep line breaks (outlets that draw line breaks as escapes or dots merge the next word into the marker, as they already do after any value). Raw and JSON-escaped line breaks are both read; blanks inside escape sequences or control strings never count as indentation, and a block line holding only a control string counts as content and is masked whole. Forms where the marker is followed by more text on the same line, `=`-separated forms such as `key = \|` or the `=>` arrow, and a header at the very end of the text read exactly as in the previous release. From 0.94.0 the private-key header has one definition, generated from the settings contract: the whole-string check finds it anywhere in the text (as the contract's own write-side check does from its 10.0.0 release), and the key-block cutter builds its header line from the same definition, so a lowercase header or one without a space after BEGIN has its key body masked like the canonical form, while public keys and certificates are left as they were. Since 0.94.0 the credential words also read through YAML node properties (type tags such as `!!str` and anchors such as `&name`) between the colon and the value: the properties and the value after them get one marker each, block scalars and block-sequence items included, and a property with no value after it on the same line reads exactly as before. A list after a credential label (a block sequence, a flow sequence including JSON arrays, or a same-line comma list after a plural label) is judged item by item with the same exemptions as a single value: a list of plain key names after a bare `keys`, or of integers after a bare `tokens`, is left alone, and any value-like item masks every non-neutral item. A dash right after the colon on the same line is a list bullet, not a value. The retry overlay's error text (`error.formatted` on the four error arms) passes through the same outlet, and the label vocabulary is pinned as it stands (members masked, known out-of-family keys such as `passphrase` left as written). From 0.95.0 the credential words also read a value wrapped in round or square brackets after a scheme word or label (placeholder-shaped contents such as `(API_KEY)` or `(your-token)` and bracketed prose stay as written), a JSON-escaped tab between a scheme word or label and its value, a same-line associative-array arrow with a quoted label and a quoted value, and a `token` scheme word with its value after a credential label as one value; a word with no label at all is masked when it is shaped like a random token (length, three character classes, entropy and run structure), while hex hashes, UUID lists, base64 image headers and data URIs, integrity strings, provider object ids and ordinary identifiers stay as written; a known secret prefix glued after an underscore or hyphen inside an identifier or a URL path segment is masked from the prefix on; and a masked value no longer swallows a closing backquote at its end, or a closing round or square bracket at the end of a URL that has no matching opener, while a backquote inside a value never cuts it short; an address after `Bearer` or `Basic` that is itself the value of a credential label (including `Authorization:`) is masked whole, keeping the scheme word, while an address with no label stays as written. Credential masking also covers three shapes that used to print as written: a name glued onto a credential word (PGPASSWORD=, CUSTOMSECRET=, dbPassword:, secretAccessKey=; singular only, so token-count names such as maxTokens or inputTokens and English words such as monkey stay visible), Bearer or Basic glued to a preceding word character (_Bearer, xBearer), and a known secret prefix followed by one unbroken run of six or more letters and digits (sk-abcdefghijk). The new readings only add masks: a value that was already masked whole stays masked whole when it contains one of these shapes. The configured-address display and the upload check read the same label rule, so a glued name used as a query key counts as a credential in both. A frozen copy of a pattern net that a downstream host used to run locally is replayed against the output as a no-regression check. A length cap never splits a redaction marker: the cut backs off to before it on every capped path (plain, position-tracking, paired backslashes, and the shared cap of the field-escaping helper); strings that are not complete markers are cut as before. Since 0.98.0 a value glued to a semicolon, a value wrapped in braces or angle brackets (template placeholders stay readable), a bare `Basic` / `Bearer` assigned to an environment-style credential name, and an ordinary word right after `Bearer` / `Basic` in a credential header are masked on every outlet, under a 65 KB linear-time bound; the diagnostic-word predicate answers from the same word list the outlet uses. Since 0.102.0 the configured-address outlet's seven arms are pinned cell by cell (a userinfo whose boundary with the query or fragment cannot be told apart, and a single-slash address that does not parse with a host, show one marker; a leading scheme-less `user:pass@host` keeps host and path; a scheme-less relative request keeps its path and masks every parameter value; a backslash inside a special scheme's authority is read through whichever of the text and the parsed-component reading hides everything the other hides, matched by position, else the whole string is withheld), the reference client's golden inputs are checked to be present verbatim when its tree is available, the general nets' wider scheme-less userinfo and the boundary rule on addresses in prose are pinned with neighbouring negatives, path segment names and values in configured addresses and the Slack webhook tail are pinned over forty shapes (free-text path segments stay as written), non-string input to the three outlets shows one marker without running any caller code, and an in-gate negative control on the boundary rule turns those cells red; pass counts are read as `collections`. Since 0.107.0 the guard also covers a third value of the backslash-pairing switch, `'auto'`, which decides per string: a string whose shown form carries a mark, or a literal sequence that reads as an escape (a pair of backslashes, `\u` with four hex digits, or `\u{…}`), is shown in the injective paired form and any other string exactly as written — so the real U+2800 and the literal six characters `\u2800` always come out different, clean Windows paths stay untouched, and every output decodes back to its input on its own, including under a length cap, where a mark that only appears after the cut (a joiner whose right-hand neighbour was cut off) also switches the string to the paired form; the marks read-back reports which form was taken (`exact`) from the same scan, with mark positions checked against an independent decoder; and the decision-body profile marks one-cell-wide space characters but not U+3000, identically for plan text, diffs and new-file bodies. The label and capped-length helpers wash credentials first and cap the washed text, so a credential-shaped name never appears in full or as a leftover prefix at the cut. |
|
|
461
461
|
| `scripts/run-display-untrusted-baseline-diff-test.mjs` | The credential side of the display outlet (`displayUntrusted` and `washCredentials`), diffed byte for byte against the previous published release: five input pools (seeded random strings, exhaustive shapes around labels, quoted values, userinfo, schemes and invisible units, and URL parameters separated by `;` or `,`) run through fifteen outlets. A secret the previous release masked must stay masked wherever it sits in a credential position; the outlet with only the URL net enabled is judged within that net's own reach. Clean lines from the package's own documents pass through unchanged, and every outlet is idempotent (masking the output again changes nothing). The previous release tarball is supplied through `SEMA_DU_BASELINE_TGZ`; without it the diff sections report as skipped and the run counts as partial, not green. The minimal-shape pool also carries scheme-less userinfo whose password holds a quote or angle bracket after a credential label, and the shapes that once washed differently on a second pass. A full-product grammar pool of 64,512 strings joins the idempotence check, and every pool is also checked for the credential stage reaching its pass cap (it must never do so). The pools now include the bare `token` scheme-word forms and a full `token` grammar product, so the new wash is held to the same idempotence and no-regression bars against the previous release. They also include label and `bearer` / `basic` values that run into a `token` scheme word, a non-ASCII space and an address, with or without a leading character, and quoted values where `token` and an address follow `bearer`. A YAML block-scalar pool (block headers with every chomping and indentation indicator, comments, raw and JSON-escaped line breaks, non-ASCII and tab indentation, invisible characters at line starts, credentials, control strings and bidi characters inside the block, and seven kinds of line after the block) joins all checks: no secret inside a block may show on any outlet that runs the word net, the previous release must show some on the same pool, and the non-block forms in that pool must stay byte for byte as in the previous release. A sixth pool covers lists and YAML node properties after a credential label: block, flow and comma lists plus tag and anchor prefixes, alongside look-alike non-list forms. The secrets in the list pool must stay masked on every outlet, the look-alike forms must read byte for byte as in the previous release, and the three forms whose reading changed on purpose (a lone dash after the colon, a value after a dash, a block sequence of plain key names) are pinned as registered divergences. A seventh pool covers the 0.95.0 credential-net shapes (bracketed values after scheme words and labels, a JSON-escaped tab before the value, unlabelled random-looking tokens built inside the shape rule, associative-array arrows, a label followed by a `token` scheme word and an address, and known prefixes glued after an underscore or hyphen) alongside normal long strings and placeholders: the secrets must stay masked on every outlet while the previous release shows some, the normal strings must read byte for byte as in the previous release, eleven readings that changed on purpose are pinned as registered divergences, and the share of purely random tokens the shape rule catches is read at four lengths with a floor of 0.9. A nested-shape pool pins that the three new credential readings only add masks: a value that the previous release masked whole must stay masked whole when it contains one of the new shapes. At the capped exit, differences against the previous release are accepted only when the old output ended in a truncated redaction marker and the new output is the old one without that fragment. Since 0.101.0 a pool of percent-encoded YAML block scalars and block sequences (166 plain shapes in one to three layers of encoding) is pinned: no secret shows on any outlet while the previous release shows some, benign encoded multi-line text reads byte for byte as in the previous release, six readings that changed on purpose are registered as divergences, and washing twice equals washing once. Since 0.102.0 two more sections compare with the previous release: the wider scheme-less userinfo and the boundary rule on addresses in prose (registered rows byte for byte on the machine wash, no password byte on any outlet with a credential net on, unchanged with both nets off, neighbouring negatives unchanged, idempotent), and a pool of capability addresses judged by zero residue of the secret on the configured-address outlet and in prose (Slack and known-prefix rows masked, the other rows in prose unchanged); the pass histogram reads `collections`. The `'auto'` value of the backslash-pairing switch is checked against the earlier release too: wherever both releases show a string the same way, `'auto'` must equal the earlier release's paired or unpaired form, chosen from that release's own marks, and undoing the pairing must give back the unpaired form across every pool, so the credential side cannot move. |
|
|
@@ -515,7 +515,7 @@ guard still cross-checks the table by name).
|
|
|
515
515
|
| `scripts/run-multicause-wording-test.mjs` | Wording guard for sentences that put several possible causes side by side. When a reader folds several causes into one reading, the sentence may only be written one of two ways: split into one sentence per cause (when this end can tell which applies), or ending with a plain admission that this end cannot tell which (when it cannot). Each site is checked twice: every cause is reached through the real entry point and renders its own sentence, or the causes are shown to collapse into the same reading and the sentence admits it. Covered: the two posture sentences that used to name an engine version, the dropped-frame sentences for an unknown frame type and a malformed one, the seventeen capability readers' not-observed sentence (none received, last one unreadable, or last one discarded when the engine connection was reset), the permission-rule cap code with its engine-minted triggers, the four session-rule not-found sentences, the three allow-rule read-health sentences, and the two sentences whose engine bit encodes two causes. A source-wide census of string literals keeps a newly added reader from copying the old shape without the admission. A second pass covers twenty-two more sites, plus two of the same shape: the per-task agents warning, the cloud model-entry sentences, the decision-receipt codes, the run-death and resume-refusal sentences, the gate-vocabulary sentences, the approval-row routing sentences, the store-posture degraded sentence, the three drop paths of the "don't ask again" check (each now names only its own cause), and the unreadable-body sentences of the persisted-rule, MCP probe and memory-provenance reads (each now names the part that failed). It also checks that the two not-reported sentences no longer read a missing key as an engine version, and that the dropped-frame default sentence keeps its opening verbatim while admitting it cannot tell which. |
|
|
516
516
|
| `scripts/run-workflow-decide-refusal-test.mjs` | Reader for the two refusals the decide route returns when a workflow sub-agent's pending approval cannot be handed to a single run record: `decide.workflow_park_forked` (the same run was resumed more than once and each copy waits on this same approval; the body's `runIds` come back exactly as sent — not reordered, not deduplicated, as a frozen copy — and a malformed list drops only that one field) and `decide.workflow_parks_unreadable` (the run record that owns the approval is unreadable or predates the owner field; the body's `runId` and the optional `readableHolderRunId` are read, the latter being a hint rather than a way out). Both join the existing workflow-lane refusal reader and are read by code, never by sentence; each has one user-facing sentence that carries no machine words, says nothing was delivered and the approval is still pending, and does not promise that resuming makes the decision deliverable. Both codes also count as "the approval is still waiting" by code, so a reworded server sentence containing a word like "already" is never mistaken for an already-decided approval. The guard drives the reader with real SDK error mappings from every witnessed SDK generation, with the bodies each server generation mints (older packages are pinned to "no fork code; the unreadable code carries only `runId`"; a package that mints the new bodies is run when present and reported as skipped otherwise), and compares plain inputs that carry neither new code byte for byte against the previous release. The reader never throws on a hostile error object: a field whose getter throws, or a list that is revoked part-way through validation, is dropped on its own while the recognised refusal is still returned. |
|
|
517
517
|
| `scripts/run-output-style-seat-test.mjs` | The per-turn output-style seat. From 7.121.0 the request accepts a top-level `outputStyle` object (`name` and an optional `turnReminder`) that the engine states once per attachment cycle, and the capabilities body carries a constant top-level bit for it; the reader keeps the same three states as its constant-bit siblings and does not confuse it with the nested bit for the style body. The live-defaults layer places the host's `outputStyleSeat` on the request only when that reading is admitted, copies it as given (an empty name or extra keys are passed through for the engine to refuse at prepare time with `config.output_style_invalid`, which the configuration-refusal table recognises), never overwrites a caller's own top-level value, never invents one, and leaves the style body seat unchanged; the field is registered in the live-defaults list, survives the unregistered-key check on both lanes, and is reported as belonging to another channel when passed to the request assembler. |
|
|
518
|
-
| `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,
|
|
518
|
+
| `scripts/run-request-uplift-test.mjs` | Six request-assembly and reader pieces shared by every client. The per-run declaration that nobody on an unattended run can answer approval requests is carried only on the unattended lane, only when the engine reports it accepts that field (an older engine rejects the whole request), with one plain sentence for each reason it was held back; the headless permission mode is always stated explicitly, in the order flag, environment, settings layer, then `default` (the settings layer is read only from a plain object, only tightens toward the session's current mode — an unreadable session reading tightens as if it were `default` — never supplies `bypassPermissions`, and never turns `auto` on without an explicit eligibility reading; an unrecognized environment value is shown through the display exit and reported with the mode the run actually uses); a predicate tells apart the denial an engine makes when nobody is reachable and no queue can hold the request; a host-reported route problem (no gateway address, or the anthropic channel with no credentials) turns a model that has a name but cannot start into a stated configuration problem; a reader gives, per model, which thinking levels the engine accepts; attachments from MCP resources and the new not-attached reasons are written as data envelopes rather than as tool calls; and servers the client mounts itself state their own allow rule, audited separately from the user's rules, with the rule-source trust order exported once. |
|
|
519
519
|
| `scripts/run-decision-refusal-standing-test.mjs` | Shared decision-refusal reading for every client: `decisionRefusalAskReading` is the one reading of whether the question a refused decision was aimed at is already settled (settled, not settled, or unreadable), and settled comes only from positive evidence (a local no-pending stop, a 409 `conflict.ask_decided`, a `gone.ask` or a 410 other than a parked ask, a 404 whose body names an ending cause, or the matching error class when the code was stripped); a 404 without an ending cause, a not-found error class, a parked ask, the codes that only say the approval you looked at is no longer current, and wording in the failure text are not evidence. When the engine refuses an approval decision, `classifyDecisionRefusal` reports settled elsewhere exactly when that reading is settled, otherwise ambiguous (5xx except 501) or refused while the question may still be pending (every other 4xx and 501), returns nothing when no server verdict can be read, and marks transient refusals (408, 425, 429, 502, 503, 504, a body that says the request is retriable, or a 404 whose body says the id belongs to another replica); `liveAskPendingReading` reads whether the question is still in the engine's live pending list as pending, not pending or unknown (an unreported, malformed or partly dropped list is unknown); `decisionRefusalClosesOut` closes out only a refusal that is neither settled elsewhere nor transient and whose question the read confirms is still pending; `undeliverableDecisionContent` is the one sentence for each way the close-out ended. The guard covers the golden status/code table and near-miss codes, values that are not a verdict, guarded reads (throwing getters, prototype accessors, revoked proxies; the reading of the question itself takes every machine-readable slot of the failure from own data properties only: a value found only on the prototype chain is absent, and an own accessor makes the reading unreadable and is never run; the outcome note and the flattened outcome that also feed the reading take their status and error code from own data properties too, so a value on the prototype chain cannot be laundered into evidence), the outcome note shape, codes unsafe for display, the full close-out truth table and the exact sentences, and that the code literal and the classifier exist in exactly one place; it also checks that the classifier, the interrupt-deny check and the park-resume check give the same settled answer across 6912 failure shapes (the park-resume check also re-reads the stream when the approval it looked at is no longer current), the two shapes that used to get different answers, and that all three read the one shared reading. The failure-note composer `decideFailureNote` is on the root entry as the same function the in-package decision exits use, and the sentence for an answer that does not fit its question has a sentence reader with English and Chinese catalog entries. |
|
|
520
520
|
| `scripts/run-no-allow-cause-test.mjs` | Why an automatic lane did not allow a call, and the refusal line for the two automatic-refusal lanes. `noAllowCauseOf(card, ladder)` combines the card's standing-rule verdict with the host's rule-ladder evidence (only an own, strictly `true` property counts): allowed or denied by a rule gives no cause; a local ask rule gives `ask_rule`; an exhausted automatic-approval budget gives `budget`; an ask rule the card names (its class or origin) gives `engine_ask_rule` when the ladder evidence shows that none of the user's own ask rules matched (the end-of-ladder no-match flag, or the dedicated flag for the case where an allow rule matched but the card's question class overruled it), and stays `ask_rule` when that evidence is absent, because the user's own rule cannot be ruled out; complete no-match evidence on a card a rule could clear gives `no_allow_rule`; everything else gives `other`, so no lane suggests adding an allow rule when that would not help. `policyDenyReason` mints the single line for the lane without an approval UI and for the dontAsk mode, one form per cause, naming only this one call and offering host-specific remedies (an allow-rule flag, a budget setting) only when the host supplies them. The guard covers the golden cases, pairwise ordering, strict reads, all ten sentences verbatim (the engine-side ask rule line says the user's settings cannot approve it and offers neither removing a rule nor adding an allow rule), unsafe tool names kept out of the sentence, and the single mint site. Since 0.109.0 the cause reads the verdict's question-level reading (whether an allow rule could clear this question at all), so it is byte-identical to the previous release over every card and ladder combination except one cell: a question an allow rule could clear, where the host's ladder did not prove that no allow rule matched, now gets its own cause and its own refusal line on both automatic-refusal lanes (only an allow rule for the whole tool can approve such calls ahead of time on this client; rules that name arguments apply only where the engine checks the real arguments), instead of the generic one; the guard fails when the previous release is not supplied. |
|
|
521
521
|
| `scripts/run-wiring-instructions-attribution-test.mjs` | Which project instruction file the engine actually loaded for a run, attributed to the client's own directory. The capabilities bit `callerCwd` gets its own reader (the same four ports as every capability reader; `callerCwdAdmitted` accepts only an explicit `true`), and a host that notes the bit inside its probe callback reads the new value in the same tick. The working directory a submitted request carried is recorded per run in the same in-process ledger and with the same frame-binding rule as the file-history opt-out (`legCwdOfBody`, `declaredLegCwd`, `legCwdOf`). `engineSelectedInstructionsFile` then returns the selected member only when the run's directory equals the client's current directory, the member is a plain relative path (no `..`, no absolute or drive form, no backslash, no control, bidi or invisible characters), and the host's real-path function confirms it stays inside that directory (a symlink pointing outside is refused). |
|
|
@@ -560,7 +560,7 @@ guard still cross-checks the table by name).
|
|
|
560
560
|
| `scripts/run-durable-decide-leg-test.mjs` | Which durable decision leg a pending row belongs to has one table and one reading. The approval-card gate kinds are `human` and `irreversible_ask` (the only kinds the approval decide route accepts), the plan-review gate kind is `plan_review`, and the review wording set is `plan_review` and `needs_review`; each table is pinned at compile time in both directions against the engine's pause registry and checked again at run time against the installed engine. `durableDecideLegOf(row)` answers one of five words: `approval`, `plan_review`, `other_gate` (a known gate neither decide route accepts), `unreadable` (a gate word outside the registry) and `absent` (no readable gate word on the row: missing, null, empty, not a string, or a throwing read); it never throws. `askParkForeignGateKind` derives from it. Pinned with the twenty-shape verdict matrix, the derived foreign-kind answer, the row classifier, the pending-row lookup (including refusing to fall back to another row when the row a park names is not an approval), the busy-session recovery on non-gate words, the five-word union on the shipped declaration, and no engine-package reference in the shipped output. Since 0.97.0 the same table also decides which approval arm a parked frame goes to: `isToolApprovalGate` agrees cell by cell with `durableDecideLegOf` (an approval gate kind takes any tool that is not the question tool, an unreadable kind falls back to the file-write and shell name families, any other kind takes neither arm); a real-wire park on an approval gate kind for a tool outside those families raises the approval card and sends the decision end to end, while the non-gate word `tool_approval`, a plan-review kind with a write tool, and a non-gate word with the question tool raise nothing. |
|
|
561
561
|
| `scripts/run-rule-tool-name-judge-test.mjs` | One reading for "does the engine accept this rule tool name": the engine's own tool-name check, installed as a value port and read through `ruleToolNameVerdictOf` (current / retired with its current name / unprefixed namespace / unmintable segment / unknown). The session-rule deliverability check asks it once for every name that survives the plain-text steps (an ordinary name the installed engine has newly retired is withheld as `legacy_tool_name`; a check that cannot answer makes the batch unreadable), the refused-entry removal removes every entry it does not call current and answers `judge_failed` without writing when it cannot answer, the sub-agent tool-list pre-check reports retired, unprefixed and unmintable names from it, reports an entry it cannot check as unjudged (sent as written with a warning, like any other deny-list problem) and judges case twins only against a supplied roster, and the request-body assembly rewrites a retired name to the current name it gives only in the deny and ask buckets (allow keeps it). No build-time retired-name table remains in the source. A real engine run that mounts no tool of that name, reading a stored session record, refuses to start exactly for the names this reading does not call current in the deny and ask buckets; in the allow bucket, from engine 7.60.0, a retired name no longer refuses the start: the run starts and reports it in exactly one unmatched-names advisory that names it, while an unprefixed or unmintable name there still refuses. In the deny and ask buckets, from engine 7.65.0, a retired name with no successor no longer refuses the start either: the run starts and one `config.legacy_tool_name` notice names it, while a retired name that has a successor still refuses. |
|
|
562
562
|
| `scripts/run-tool-card-outcome-test.mjs` | One reading, one sentence, for what a tool result card in the transcript actually says. Three marks decide it: the placeholder mark on the record (the engine sent no body, so the content is an empty placeholder), the engine's positive evidence that the call never ran, and the block's error flag. `readToolCardOutcome(record)` reads them as `normal` (a real result; success or failure still comes from the error flag and is never rewritten), `degraded` with a cause and one sentence (`not-executed` — takes precedence and is independent of the error flag, so a card that looks successful but never ran is caught; `error-without-detail`; `no-result`, where the error flag false is a placeholder and not a success; `unrecognised`, a placeholder value from another version) or `unreadable` (not a tool result record, or the placeholder position cannot be read — never read as normal). The placeholder has two minting points that cannot be told apart on the record (a call closed at the end of the turn with no end frame, and a real end frame without a body), so the no-result sentence states only what both share. Pinned on records produced by the real pipeline in both lanes (the transcript row and the print-lane frame answer the same), a read-failure matrix that never throws and never reads as normal (accessors are not run, prototype-chain keys do not count), four distinct sentences with no internal names or tool-family names, and the source (the sentence table is typed on the cause union, the evidence mark goes through the existing strict-true reader, the placeholder constant is imported not re-written, and each sentence has exactly one minting site in the shipped output). |
|
|
563
|
-
| `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-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. The no-remember-option sentence for `lane_cannot_speak` is pinned whole in both languages, and the engine's own baseline sentences for the four absence words are anchored byte for byte. |
|
|
564
564
|
| `scripts/run-engine-url-transport-test.mjs` | Engine address handling in one place. `normalizeEngineBaseUrl` trims outer whitespace, strips every trailing slash and keeps the user's spelling; it refuses, with one of five closed reasons (`empty`, `scheme`, `unparseable`, `userinfo`, `query_or_fragment`), anything that is not a plain http(s) base address. A query or fragment is refused even when empty, because request paths are appended to the base as plain text and would end up inside the query. `engineUrlTransportVerdict` lets `https:` through and `http:` only for a host the settings contract's narrow local-host check accepts, the same check that decides whether outbound credentials are dropped, so the credential fence and the plaintext gate cannot disagree about one host; anything else is refused as plaintext to a remote host or as not an http(s) address, and an address it cannot read is refused. Neither function returns message text or echoes the input. The gate checks every refusal reason and success form, the two addresses the terminal and the desktop shell used to answer in opposite ways, that the local-host reading has a single call site over the settings contract's own function and never touches the wider routing check, that the verdict matches the installed settings contract on a host table, that hostile input never throws and a very long run of slashes stays linear, and it compares the package with copies of both shells' former logic on a generated corpus: the package accepts exactly what both former implementations accepted, with the only disagreements between them falling into two registered classes. The cloud configuration's session-mirror address is read through the same normaliser: well-formed addresses come out byte-identical to the former trailing-slash trim, an address the normaliser refuses is passed on as read with its `required` flag intact rather than dropping the mirror, and a long run of slashes stays linear. |
|
|
565
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
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. |
|
|
@@ -589,7 +589,7 @@ guard still cross-checks the table by name).
|
|
|
589
589
|
| `scripts/run-print-model-status-test.mjs` | The print-lane model-service status frames. `retryStatusFromArm` reads the internal retry arm (the same reader the interactive lane uses; sub-agent frames and recovered are told apart), and `createPrintModelStatusTracker` keeps the per-turn status ledger for the `-p` stream-json lane: a retryable failure with both counters becomes a `system/api_retry` frame in the reference client's shape (its keys keep their meaning, every extra key is `_sema_`-prefixed, the category word follows the reference client's table), waiting for the first token, counter-less reconnects, give-ups and replays of the same back-off become `system/_sema_model_status` frames, and an error result carries `_sema_last_model_status`. The sentence comes from the retry-line sentence source. The suite drives the real stream projector end to end, checks the reader against the interactive lane, the replay de-duplication, the category table, the key set, the turn lifecycle (including that a synthetic failure row does not clear the ledger), hostile input, and that the new module holds no module-level state. |
|
|
590
590
|
| `scripts/run-subagent-started-projection-test.mjs` | The delegated sub-agent start frame. The stream projector turns the engine's per-cycle `task_started` frame into a neutral internal arm and the adapter turns that into a `subagent_started` chrome event (no transcript row, no adapter state; the event hangs on the delegating card's lane when the frame names the card, otherwise on the main lane), and `publishEngineAgentPanelStart` feeds the start time into the panel reducer as evidence: it only fills a missing `startedAt` and never creates a row or changes how identities merge. The suite checks the projection key by key (absent keys stay absent, bad shapes drop only the key), the end of the dropped-frame trace line for this frame type (a real unknown frame still gets one), the chrome event end to end through the real stream projector and through the real SDK's SSE parser, the reducer scenarios, and, over 600 random frame sequences, that adding start frames changes no row delta except the `startedAt` field. |
|
|
591
591
|
| `scripts/run-settings-hooks-salvage-test.mjs` | The settings-file hooks salvage reader (`salvageSettingsHooksEntries`): one reading of whether a settings document's hooks load as written, load with some pieces dropped, or are rejected as a whole, with the upstream settings-contract classifier as the only judge. A partial load never hands back the original object: the dropped pieces are removed from the document as written and every kept piece is the original object, so fields outside the contract (such as exec-form arguments) still reach the checks that guard them; the result always classifies as loaded. The suite compares the reader with the upstream classifier over a corpus transcribed from the terminal's own salvage tests, checks the pruning rules and the one-sentence-per-file notice (no values echoed, untrusted names made visible and credential-like names masked), and checks that the hooks plan reads every settings layer through it: a rejected layer contributes no hooks, a rejected managed layer closes the whole hooks leg, and row indices still point into the document the host supplied. For a managed layer that lost a piece, the notice now says requests are not sent until the managed settings are fixed, matching the turn's outcome. |
|
|
592
|
-
| `scripts/run-user-attachment-seat-test.mjs` | The request-level attachment seat. Its capability reader has four states (not observed, unreadable, not reported, seat) and one predicate decides whether the seat may be used: it must accept entries on the objective, carry text content and accept client text. The suite checks the reader against malformed shapes and against the installed server's own seat description, then checks the encoder: with no usable seat the encoding is byte-for-byte the same as the previous release's encoder on a fixed corpus; with one, whole @-mentioned text files, complete notebooks and fully listed directories move into seat entries named by their full path (partial selections, restored material and other kinds stay in the text envelope), within the seat's per-entry and count limits, never on an otherwise empty message, and inside the request body budget. Entries the encoder emits are run through the engine's own screening in process, the engine's whole-window refusal is read back through the refusal reader, and the engine's per-attachment outcomes are projected back with ordinals matching the encoding. |
|
|
592
|
+
| `scripts/run-user-attachment-seat-test.mjs` | The request-level attachment seat. Its capability reader has four states (not observed, unreadable, not reported, seat) and one predicate decides whether the seat may be used: it must accept entries on the objective, carry text content and accept client text. The suite checks the reader against malformed shapes and against the installed server's own seat description, then checks the encoder: with no usable seat the encoding is byte-for-byte the same as the previous release's encoder on a fixed corpus; with one, whole @-mentioned text files, complete notebooks and fully listed directories move into seat entries named by their full path (partial selections, restored material and other kinds stay in the text envelope), within the seat's per-entry and count limits, never on an otherwise empty message, and inside the request body budget. Entries the encoder emits are run through the engine's own screening in process, the engine's whole-window refusal is read back through the refusal reader, and the engine's per-attachment outcomes are projected back with ordinals matching the encoding. The predicate also answers per use: a path needs both the seat and the scenario card to accept paths, and a missing or unreadable card means unknown. |
|
|
593
593
|
| `scripts/run-user-invoked-skills-slot-test.mjs` | The `userInvokedSkills` request slot. Naming skills widens what a run may do without a card, so the value is only given through `userInvokedSkillsDeclaration`, which returns the key when the capability reading is present and otherwise returns nothing plus the reading that withheld it. The suite checks that truth table, the lanes that seat the key (interactive and print, behind the live gate; never the side channel), that an empty list is not sent and a malformed one is refused at assembly, the path from a landed capability report to the request body, and the refusal reader and sentence for an unknown skill name, read from an error the engine itself raises in process. |
|
|
594
594
|
| `scripts/run-agents-wire-caps-test.mjs` | The custom agent projection reads each tool list once and sends the description, the system prompt and skill content as written. The three hand-copied server caps are gone from the root entry (and the truncation and skip logic with them): an over-long field now reaches the server, which answers with a 400 that names the field (`agent.whenToUse`, `agent.systemPrompt`, `agent.skills`). A tool list (`tools` or `disallowedTools`) that is written but is not an array, for example an unsplit "Read, Grep" string, is refused instead of silently dropped: the projection throws a `TypeError` that names the agent and the key and carries the assembly-refusal identity, so the readiness gate keeps the turn from being submitted. |
|
|
595
595
|
| `scripts/run-decision-in-flight-ledger-test.mjs` | One ledger answers whether a human decision has been handed to the engine and not yet settled (or settled after a read started), for tool approvals, plan reviews, form prompts from MCP servers and whole runs alike, through one public reading `decisionInFlightOf(identity, readMark?)` and one read mark `decisionReadMark()`. Every writer lives in the package: the plan-review delivery path and its hand-over registration, the durable decide operation, the host's card hold when it sends, the live card while its answer is being sent, and the decision sender. The plan-review entries keep their rules byte for byte (the old and new readings answer every checkpoint of the shared vector table identically, also with the other partitions filled to capacity); a read that started before a decision settled reports a stale snapshot, one that started after does not, and a decision proven not to have taken effect never makes a snapshot stale; positive evidence that the run left the gate settles what is open while the duplicate-delivery latch still holds. Three real writers on one run (a durable decide, a host-held card that is sending, a live card that is answering) are each visible at run level and at their own identity, and the run reads clear only after all three settle. The capacity bound never evicts the package's own in-flight entries; malformed identities and read marks never throw. A refused plan-review reopen that used to carry no reason now names one cause from a closed, frozen set of five (`PLAN_REVIEW_REOPEN_REFUSALS`). |
|
|
@@ -608,13 +608,13 @@ guard still cross-checks the table by name).
|
|
|
608
608
|
| `scripts/run-result-route-projection-test.mjs` | The route a refused request took, carried on the error envelope of a run result frame under a superset key. When a run ends because the model provider rejected the request with a 4xx and the terminal detail names the route (which credential source, optionally the carrier and bag, and the root), the result frame carries those words as a fresh object; extra fields an engine might add are never copied, unknown words pass through as written, and nothing is carried when the detail, the route or its required parts are missing or malformed, so absence never reads as having taken no route. Everything else on the frame — subtype, error flag, error list, error code, terminal reason — is unchanged whether or not the route is present, and only a provider-rejected terminal ever carries it. The key has a single minting site and a row in the superset ledger |
|
|
609
609
|
| `scripts/run-additive-key-passthrough-test.mjs` | The one disease shape behind two legs: a **closed whitelist / flattening arm** dropping a fact that is already on the wire, while both sides of the seam look correct. (1) The `task_progress` projection carries a registered **key ledger** — a frame populated with every key the service really projects is pushed through the shipped `eventToSdkMessage`, and the set of wire keys that survive must equal the registered pass-through list **name for name in both directions**, so quietly forwarding one more key is as red as quietly dropping one. `model` (the child run's model id, minted by core as `prepared.model.id` and projected by the server since 7.52.1) is the key this batch adds, with the same conditional the server itself applies: a non-empty string or no key at all — an empty string is neither a model id nor "unknown". The ledger is also checked against the fenced list in `docs/INTEGRATION-CLIENTS.md` §3d, so a doc that still says seven keys while the code forwards eight is red rather than merely stale. (2) The decide-failure arms carry the server's S-02 `currentPending` pointer key from a 409 `approval_stale` refusal onto the outcome the host reads. The reader is structural rather than `instanceof`, because the client is host-injected and the class identity is not this package's to assume; a half triple never mints (half a pointer cannot relocate anything), an empty string is not presence, and `checkpointToken` never transits. Both the allow and the deny leg are driven end to end through the real durable approval path — as is the accept-session leg, where a refusal carrying the pointer key must now re-raise instead of silently re-sending the human's answer for the **old** card as a plain approve (one decide call, pointer preserved), while a legacy 400 still falls back exactly as before — and all three flattening points must call the one shared reader — the same-shape residue check that makes "fixed one arm and left the twin" red instead of invisible. (3) The same disease growing on the REQUEST side: the `.mcp.json` → server-spec projection rebuilds each server key by key, and the settings schema deliberately leaves some keys parse-transparent — whatever JSON the file carries reaches the engine untouched, because validating them where the whole domain parses all-or-nothing would let one bad declaration take every server down silently. The whitelist had no row for the newest of them, so an operator's per-tool declarations — the ones the write fence reads — were stripped at the package boundary while both sides looked correct. The criterion is not "is that key handled" but the transparent-key table read out of the INSTALLED schema at runtime, reconciled name-for-name against this leg's ledger, so the day upstream adds a third one this turns red and forces an explicit decision. Behaviour is pinned on both transports, by object identity rather than deep equality (a rebuild would be a second judge), and malformed values must transit UNCHANGED rather than be refused here — the engine refuses them loudly and names the server, whereas a package-side judge can only swallow a declared protection quietly. Absence still mints no key, unknown keys still never reach the wire (the fix is the dropped key, not the gate), and the one transparent key this leg deliberately does not forward is a ledger entry with its own exit condition: it belongs to the deployment plane, and the day the request-plane type declares it the entry's premise is gone and the gate says so The prose probe reason on approval asks (`probeReason`: live frames from server 7.115.0, and the durable pending row's risk descriptor) lands on the same card field from both legs, byte for byte, never as an empty string, read from own keys only. A display field that throws when read (a getter or a proxy trap) is treated as absent: the card is still shown and the decision is still sent. The remember-refused flag (`rememberRefused`, the server saying an allow-for-this-session answer will not be remembered on this card) reaches the same card field from the live frame and from the pending-list row, only as an own strictly-true key; absent, malformed, inherited or throwing values leave it off without blocking the card. The single reader of the card’s remember-refused declaration is a root export — an own key holding exactly `true`, never throwing on hostile input, agreeing with both card legs over the same pool — and no private copy remains. Since 0.96.0 the retry-status leg also carries the provider condition word and its evidence source verbatim to the top of the retry status; no quota reading is derived from them. Since 0.96.0 the configuration-key ledger also accounts for the three transparent connector keys of the 12.2 settings contract: none of them is forwarded on either leg, two because the request shape has no such key, and the per-tool axis override because its meaning on a centrally configured entry is not yet defined by its consumer; each entry carries the condition that would end it. Every withheld key is probed on both transports and with nine value shapes on both legs, the request shape is read with the type checker, and the per-tool axis override's end condition is read by running the current server package's own central projection. Since 0.100.0 the check that an explicitly empty tool allow-list reaches the engine as written is read per server generation (the older generation's accept-list constant and the newer generation's reader assignment are each recognised only in their own form) and its behaviour is run for real on both generations: the server's own request reader keeps the empty list, also when the package's output is fed to it, and the same generation's engine, mounting a stub tool server, mounts both tools when the list is absent, one when it names one, and none when it is empty. The progress frame now forwards its cycle number. Since 0.110.0 the prose reason-for-asking bit crosses to the seat request through `toolPermissionRequestAskBits` (the old pin that it never crosses is replaced). |
|
|
610
610
|
| `scripts/run-cloud-mcp-dropped-notice-test.mjs` | The sentences shown to a person when the cloud configuration leaves MCP server entries out. Each of the eleven reasons an entry is left out (switched off, a required setting missing, an unsupported transport, both identity-header settings set, an invalid `toolAxes` declaration, an `elicitation` value that is not a boolean, a name the settings contract's reader rejects, a transport the engine cannot use as written, an invalid `toolFaces` declaration, an `allowTools` that is not a list of tool names, an entry that could not be read at all) has its own sentence that names the entries — the last two name a bad-name summary and a key path — and the switched-off sentence is informational while the other ten are warnings. An entry that stays on the wire without an optional part of its transport adds one warning that names the entry and the key path, never a value, and nothing is left out for exceeding 32 entries any more. The sentences are part of the single wording source `cloudEffectiveNotices`, whose input now also carries the list of left-out entries. A reason outside the known set and an unreadable row (not an object, a reason that is not a string, a throwing getter, a reason or name that only exists on the prototype chain, an index that is an accessor or a hole) get a plain warning that names what can be read instead of being skipped, a list that is not an array or cannot be read (an accessor that is never run, a value of undefined, a proxy that throws) gets one warning saying it could not be read, a missing key adds nothing, and an empty list adds nothing. Names are capped at five with a `+N more` tail, repeated names are shown once, rows without a readable name are counted, and the order is degradation warnings, catalog references, left-out MCP entries (warnings first), budget, runtime caps. Pinned on the shipped declaration (the reason union is exactly the eleven words) and on the source (the sentence table is typed on that union and looks reasons up by own key). |
|
|
611
|
-
| `scripts/run-approval-card-face-projection-test.mjs` | Three card positions added in 0.94.0. `boundGrants` (what approving a Skill card also grants: the skill name or null, the entries that then run without asking, and the entries refused for the rest of the run) is read once from the live approval frame through a strict reader; any malformed part drops the whole position, the copy is frozen, the digest is not put on the card, and the durable pending-row path never carries it; `approvalBoundGrantsDetail` gives one sentence that says how many tool entries run without asking and how many are refused, and (from 0.106.0) names each of them — up to six per list, then "and N more"; every name goes through the display sanitizer and is quoted, entries the engine reads as nothing are neither named nor counted, and the skill name is never put in the sentence. `classifierNoRuling` (why the auto-mode classifier gave no ruling) is read by word shape as an open set from the live frame only; `classifierNoRulingDetail` has a phrase for each of the engine's words and a generic sentence otherwise. `priorCardsForCall` lists, for the same tool call, the earlier cards that asked about something else (a different class set, origin or mandate word), each with its approval id, origin, mandate word and, when answered here, whether it was allowed or denied; replays and parked twins of the same question do not count, both card paths share one bounded ledger, and `approvalPriorCardsDetail` says this is a separate check that needs its own answer. Each line of the card's "why is this asking" block is rendered from the package's own sentence; only the values interpolated into a sentence (a limit name, a rule entry, probe text, an engine message, a cause word or code) go through the display sanitizer, so a fixed sentence such as "only its basic facts are shown" is no longer rewritten by the credential filter, while a credential-shaped value in any interpolated slot is still masked and never shown in the clear. Since 0.110.0, `requestedBy` (which extension registration started the tool call being asked about) is read from the top level of the live approval frame and of the presented-card frame through one strict reader and lands on the card request as a frozen `{ kind: 'extension', name }` copy; malformed or hostile shapes drop the position, while the card body, the generic card, the suspended blind row and the durable pending row never carry it, and it never changes the standing-row or automatic-answer verdicts. `approvalRequestedBySentence` gives the sentence ('Requested by ...' / zh) with the name through the display sanitizer, and `approvalAskReasonsOf` is exported at the package root. From 0.110.0 an approval card request also carries an optional `taskId`: the id of the leg the card belongs to — the wire run id for a main-leg card, the delegated child's session id for a child-leg card — and a card only ever has that one value (it is the same identity value the package uses to recognise the same approval). It is present only when the package can read it from the card's own source (for a main-leg frame, the run binding registered for the frame first, otherwise the run the caller passes in, read as own data slots so getters are never run; for a child-leg frame, the source session position on the frame; the parked-row path uses the task id it was driven with, the same value as `parkRow.taskId`); it is absent when it cannot be read, never an empty string, and the package never guesses (no latest-binding fallback, the frame's own `taskId` is not read, and the host run never stands in for a child-leg value that cannot be read). It takes no part in the decision, is not sent on the wire, and the pure projection exits do not carry it. From 0.110.0 an approval card request (and the card facts of the pure projection exits) also carries an optional `inputSource`: `'record'` when the arguments on the card come from the record side (the pending row's input, or the frame's arguments) and `'stream'` when the record side had no usable arguments and the package fell back to the model's original call from the event stream. It is left out when there are no arguments to read (the card shows an empty object or a path recovered from the question), when the stream value is not an object, and on a suspended card the host declared blind; it is read from the card's input reading only, so a same-named key lying on the frame, the card face or the row is never copied. It sits alongside `argsRewritten` and `argsUnavailable` rather than replacing them, takes no part in the decision and is not sent on the wire. |
|
|
611
|
+
| `scripts/run-approval-card-face-projection-test.mjs` | Three card positions added in 0.94.0. `boundGrants` (what approving a Skill card also grants: the skill name or null, the entries that then run without asking, and the entries refused for the rest of the run) is read once from the live approval frame through a strict reader; any malformed part drops the whole position, the copy is frozen, the digest is not put on the card, and the durable pending-row path never carries it; `approvalBoundGrantsDetail` gives one sentence that says how many tool entries run without asking and how many are refused, and (from 0.106.0) names each of them — up to six per list, then "and N more"; every name goes through the display sanitizer and is quoted, entries the engine reads as nothing are neither named nor counted, and the skill name is never put in the sentence. `classifierNoRuling` (why the auto-mode classifier gave no ruling) is read by word shape as an open set from the live frame only; `classifierNoRulingDetail` has a phrase for each of the engine's words and a generic sentence otherwise. `priorCardsForCall` lists, for the same tool call, the earlier cards that asked about something else (a different class set, origin or mandate word), each with its approval id, origin, mandate word and, when answered here, whether it was allowed or denied; replays and parked twins of the same question do not count, both card paths share one bounded ledger, and `approvalPriorCardsDetail` says this is a separate check that needs its own answer. Each line of the card's "why is this asking" block is rendered from the package's own sentence; only the values interpolated into a sentence (a limit name, a rule entry, probe text, an engine message, a cause word or code) go through the display sanitizer, so a fixed sentence such as "only its basic facts are shown" is no longer rewritten by the credential filter, while a credential-shaped value in any interpolated slot is still masked and never shown in the clear. Since 0.110.0, `requestedBy` (which extension registration started the tool call being asked about) is read from the top level of the live approval frame and of the presented-card frame through one strict reader and lands on the card request as a frozen `{ kind: 'extension', name }` copy; malformed or hostile shapes drop the position, while the card body, the generic card, the suspended blind row and the durable pending row never carry it, and it never changes the standing-row or automatic-answer verdicts. `approvalRequestedBySentence` gives the sentence ('Requested by ...' / zh) with the name through the display sanitizer, and `approvalAskReasonsOf` is exported at the package root. From 0.110.0 an approval card request also carries an optional `taskId`: the id of the leg the card belongs to — the wire run id for a main-leg card, the delegated child's session id for a child-leg card — and a card only ever has that one value (it is the same identity value the package uses to recognise the same approval). It is present only when the package can read it from the card's own source (for a main-leg frame, the run binding registered for the frame first, otherwise the run the caller passes in, read as own data slots so getters are never run; for a child-leg frame, the source session position on the frame; the parked-row path uses the task id it was driven with, the same value as `parkRow.taskId`); it is absent when it cannot be read, never an empty string, and the package never guesses (no latest-binding fallback, the frame's own `taskId` is not read, and the host run never stands in for a child-leg value that cannot be read). It takes no part in the decision, is not sent on the wire, and the pure projection exits do not carry it. From 0.110.0 an approval card request (and the card facts of the pure projection exits) also carries an optional `inputSource`: `'record'` when the arguments on the card come from the record side (the pending row's input, or the frame's arguments) and `'stream'` when the record side had no usable arguments and the package fell back to the model's original call from the event stream. It is left out when there are no arguments to read (the card shows an empty object or a path recovered from the question), when the stream value is not an object, and on a suspended card the host declared blind; it is read from the card's input reading only, so a same-named key lying on the frame, the card face or the row is never copied. It sits alongside `argsRewritten` and `argsUnavailable` rather than replacing them, takes no part in the decision and is not sent on the wire. Since 0.113.0 it also covers `approvalCardHeadlineOf`: one frozen `{ source: 'origin', text }` or nothing, per origin word and language, with quiet, unknown, absent and hostile origins handled without throwing, and the three older sentence exits (`approvalCardReasons`, `askOriginNoteOf`, `askOriginDetail`) pinned byte for byte against a 0.112.1 snapshot. |
|
|
612
612
|
| `scripts/run-retry-status-notice-test.mjs` | The retry-line sentence. `retryStatusNotice(status, ctx?)` turns a retry status (or `null` when the row should be cleared, which gives no sentence) into one sentence, a machine-readable way out (`switch_model`, `wait`, `fix_request`, or none) and the phase of the line. The gate pins the full truth table (arm × terminal × provider-condition word × HTTP status × attempt counts) against an independent oracle and a set of literal sentences; the first-token wait reads as a wait and never as an error; absent or invalid positions are read as absent (no zeros, no invented reasons, no quota wording); words outside the engine's closed set read like `unknown` and are never echoed; the way out is given only where it applies; unknown arms read as the default arm; per-key throwing proxies, a single read per key, a frozen result and purity; the sentence table's keys equal the engine's `PROVIDER_CONDITIONS` and no second table exists; sentence hygiene; and an end-to-end run through the real wire projection. `retryStatusParts(status, ctx?)` returns the same facts as separate segments instead of one string: the model name, the condition phrase, the HTTP status, the attempt number and its cap, the wait and limit of a first-token wait, the engine's own words, the retry-at time as a wall-clock millisecond value for the host to count down, a one-sentence note on what the condition was judged from, and the way out. A segment is present only when the engine gave it, and nothing is ever filled in with zero. The one-line sentence is assembled from the same segments, and the gate checks that the line equals an independently written join of the segments across thousands of combinations. |
|
|
613
613
|
| `scripts/run-tool-row-terminal-projection-test.mjs` | A tool call held by a park gets no terminal row from its park-time close frame: the call is not settled yet, so the projection takes its card off the open-card table, records it on the adapter instance (with the parked run and checkpoint id, delivered by an internal terminal-fact arm that the stream emits before the result frame) and emits one `tool_parked` chrome event naming the call and the transcript uuid of its start row. Its terminal row is minted only by the executing leg: a later frame for the same call id claims the original card only with a witness (a continuation the question bridge spliced into the same projection); without one it is a new call with its own start row. A claim also needs the parked start row to still be the latest start row the instance minted for that call id (whichever of the two was recorded first), refuses a frame naming a different tool or bound to a run outside the park, and keeps the original row identity while taking the continuation's input. Collateral calls, unreadable frames and a park the package itself settled (Esc on a parked card) keep their terminal rows; the print lane uses the same judge; the registry travels with `exportLedger` / `importLedger`; a run that ends without parking retires its entries; and results pair with the nearest preceding unpaired start row of the same call id. The host-declared continuation witness no longer exists: only a continuation spliced into the same projection reclaims the parked row, and across projections every continuation is a new call until the engine marks the frame itself. |
|
|
614
614
|
| `scripts/run-standing-rows-test.mjs` | Single judge `standingRowsOf(card, host?)` for the three standing rows on an approval card: allow for this session, don't ask again, and the deployment-list session row. Every row widens what runs without asking, so each one fails closed. An unreadable card, `rememberRefused` from the engine (own or inherited) or a present-but-unreadable value closes all three rows, even when rule offers are present. The session row needs the route fact `allowSessionCarried`, which the live-frame and pending-row paths stamp and the v1 path does not. The deployment-list row needs every ask class to be the deployment-list class, checked against the installed engine's `classifyAsk`. Engine rule offers in either slot give `engine_offer`; an unreadable offer key gives none. A local whole-tool rule is offered only when no engine lane closed the door, the arguments are available, the grant binding reads `none`, a standing allow rule could clear the ask, and the host reports a generic card face and that the rule would be sent; each precondition is flipped on its own. The live-frame, pending-row and v1 paths are exercised end to end, the grant-binding reading (since 0.98.0 the engine's own `boundGrantAbsence` word, not the tool name) is compared form by form on all three paths with the server's three grant-binding functions when a 7.129.0-or-later server fixture is installed, and may only be stricter where the card cannot tell which of them the server applies, malformed, inherited or throwing refusal evidence on the live-frame, pending-row and v1 carriers is recorded on the card as `unreadableEvidence` and closes the rows instead of reading as absent (checked end to end through the real paths); results are frozen, and hostile host facts never throw. Since 0.97.0 the seat request's allow-for-this-session bit `sessionRow` is minted by `toolPermissionRequestAskBits(card, requestId)` from the same premise as the judge's session row and the request id's domain (`live:` and `durable:` carry a session answer; `question:`, `elicit:` and `plan:` do not); an unreadable or refused remember flag never lets the seat show that row, and the crossing never copies the bit from its input — a seat request, or any input carrying a request id, passed back through it gets no bit. The deployment-list question class names come only from the generated ask-class table. The session row and the deployment-list session row are withheld when the card carries unreadable evidence or an unreadable card payload. Since 0.109.0 the persistent row reads the verdict's question-level reading (whether a whole-tool rule could clear this kind of question), so `standingRowsOf` and `rememberOfferable` are byte-identical to the previous release; the guard fails when the previous release is not supplied. Since 0.109.0 a presence check that a host proxy answers falsely (the has trap says the key is not there while an own slot is) no longer reads as absence: presence and value come from one observation, anything that cannot prove absence closes all three rows like a throwing has trap does, and a truly absent key reads exactly as before. |
|
|
615
615
|
| `scripts/run-steer-tier-test.mjs` | Steer priority goes through one gate. The capability reader for `fleet.steerPriority` keeps three states, takes the word as an open set and treats `fleet: false` as not reported; its judge answers `engine` only for the exact word. The steer body has one constructor, `steerBodyOf`, used by the router and by clients that build their own request: `next` and no priority never enter the body (byte-identical to the previous releases on six call shapes), `now` and `later` enter it only when the reading is `engine`, and otherwise the constructor throws a refusal with a stable code and no request is sent. Against the 7.116.0 server package the guard runs the capabilities route, the steer route with the engine's own steer verb and an in-memory checkpoint store: the words the client admits are accepted, the words it refuses are rejected with the same field error, same-key replays stay idempotent, a real tier change is refused, and a `now` steer's cut notice carries the receipt's message id. It also pins the one sentence for a same-key replay (it does not tell the user to resend under a new key unless the text changed), the rewritten refusal sentences, the single receipt reader, the cut-notice verdict and sentence through both projection layers, the first-beat order (bounded capability wait, then reading, then send) and the engine-switch chain where an invalidated reading sends nothing |
|
|
616
616
|
| `scripts/run-session-policy-grammar-test.mjs` | Reading and rewriting a session’s own rule record never changes which rule grammar its entries are read in. The record carries a grammar stamp: the read exposes it (absent means a record stored before the stamp, which the engine reads in its earlier grammar — never folded into a number), and both rewriting paths — tightening and removing refused entries — send the same stamp back. When a record has no stamp and a rewrite would carry back one of its allow entries written in a tool’s own rule form (such as Bash(git:*)), which the earlier grammar reads as allowing nothing and the current grammar reads as a working allow rule, the rewrite sends nothing and returns a machine-readable refusal naming those entries — for every caller identity alike, since an identity allowed to widen rules would otherwise have the write accepted. An engine refusal of the same kind maps to the same outcome, and a write receipt that comes back without the stamp it was sent with, or with an older one, is never reported as written. The guard runs each case for both caller identities against a modelled engine and, when the matching server build is installed, against the real engine write judge and the server’s error mapping, recording the pre-fix widening as its failing baseline; over a generated corpus it checks that every entry the engine reads differently in the earlier grammar is one the package refuses to carry back, and that every extra entry it refuses is one the engine would refuse to write anyway. Malformed stamps, accessors, per-key throwing proxies and a polluted prototype are refused rather than read as absent. |
|
|
617
|
-
| `scripts/run-hooks-merged-disable-projection-test.mjs` | The fourth governance leg of the hooks projection: `disableAllHooks` set in a non-managed settings source. The value that counts is the **merged** one, computed by the package from the per-layer raw documents handed over through `HostSettingsSource.readLayer` (the former optional `SettingsPort.mergedDisableAllHooks()` host answer retired in 0.101.0), because a per-source approximation ("any source says true") reads user `true` with local `false` backwards — the merged value there is `false` and every source's hooks ship. When the merged value is `true`, only managed-settings hooks are sent to the engine: non-managed settings can switch off their own hooks, never the managed ones, and a managed `disableAllHooks` is still judged first and sends nothing at all. A full matrix over the four sources, each true, false or absent, is merged with the reference rule (later sources override earlier ones, managed settings last) and every cell's projection is asserted. The reader is the only authority: per-source values never second-guess it, and only a strict `true` counts. A host that does not implement it keeps the previous behaviour and gets exactly one warning per installed settings port, never one per request, and none on paths where the reader would not have been consulted; a reader that throws is treated as `true`, so managed hooks still ship. The session goal's Stop hook and the final-verification yield rule, which reads the projected hooks, follow the same verdict, and the trust gate and the three managed gates are evaluated before the reader is ever called. The last leg pins the member's declared shape in the built declarations: optional, no parameters, returning a boolean or `undefined`. The flag settings source (a settings file or inline settings given at startup) is projected after the local source, and settings hooks stay concatenated managed first (managed → user → project → local → flag): the engine runs hooks in request order, and three of its budgets go to whoever comes first — the per-event wall-clock budget, the per-event cap on model-backed entries, and the total cap on added context — so a managed hook placed after the others could be crowded out and silently not run. The flag source moves with user, project and local under every managed or merged gate; its exec-form entries are dropped and warned about like any other settings source, and the not-run notice names it `Flag settings`. An optional leg feeds the request body to an installed server's hook runner and requires the managed deny, block and context to take effect while non-managed hooks exhaust each of those budgets, and pins the known cost of that order: a non-managed `PreToolUse` hook placed after the managed one can still rewrite the input after the managed check allowed it. Without an installed server the leg reports that it did not run. Safe and bare mode: the host reports its startup mode through the optional `SettingsPort.hooksStartupMode()`, which both the plain and the plan path read and which answers `'safe'` or `'bare'`; the flag already carried by the plugin hooks reading, which only the plan path reads and which cannot tell the two modes apart, counts as safe mode unless the reader named a mode. In either mode only managed-settings hooks are sent from the settings sources: bare mode is handled like safe mode, so managed hooks are still sent, also under the managed-only gates. In both modes plugin hooks are excluded as before and the session goal's Stop hook is still sent. Only the two exact words count; a host that does not implement the reader, returns `undefined`, returns any other value or throws keeps the previous request byte for byte, checked for every such reply, both flag states, both paths and with or without a session goal; any other value and a throw each leave one debug line, and so does a reader written as a property instead of a method, which narrows nothing. The reader is called at most once per request and not at all when hooks are already switched off entirely. The optional server leg requires the left-out sources' hooks not to run while the managed ones still run and still deny, in either mode. A hook written in more than one settings place is sent once: entries on the same event, in groups that are identical apart from their hooks (same matcher),
|
|
617
|
+
| `scripts/run-hooks-merged-disable-projection-test.mjs` | The fourth governance leg of the hooks projection: `disableAllHooks` set in a non-managed settings source. The value that counts is the **merged** one, computed by the package from the per-layer raw documents handed over through `HostSettingsSource.readLayer` (the former optional `SettingsPort.mergedDisableAllHooks()` host answer retired in 0.101.0), because a per-source approximation ("any source says true") reads user `true` with local `false` backwards — the merged value there is `false` and every source's hooks ship. When the merged value is `true`, only managed-settings hooks are sent to the engine: non-managed settings can switch off their own hooks, never the managed ones, and a managed `disableAllHooks` is still judged first and sends nothing at all. A full matrix over the four sources, each true, false or absent, is merged with the reference rule (later sources override earlier ones, managed settings last) and every cell's projection is asserted. The reader is the only authority: per-source values never second-guess it, and only a strict `true` counts. A host that does not implement it keeps the previous behaviour and gets exactly one warning per installed settings port, never one per request, and none on paths where the reader would not have been consulted; a reader that throws is treated as `true`, so managed hooks still ship. The session goal's Stop hook and the final-verification yield rule, which reads the projected hooks, follow the same verdict, and the trust gate and the three managed gates are evaluated before the reader is ever called. The last leg pins the member's declared shape in the built declarations: optional, no parameters, returning a boolean or `undefined`. The flag settings source (a settings file or inline settings given at startup) is projected after the local source, and settings hooks stay concatenated managed first (managed → user → project → local → flag): the engine runs hooks in request order, and three of its budgets go to whoever comes first — the per-event wall-clock budget, the per-event cap on model-backed entries, and the total cap on added context — so a managed hook placed after the others could be crowded out and silently not run. The flag source moves with user, project and local under every managed or merged gate; its exec-form entries are dropped and warned about like any other settings source, and the not-run notice names it `Flag settings`. An optional leg feeds the request body to an installed server's hook runner and requires the managed deny, block and context to take effect while non-managed hooks exhaust each of those budgets, and pins the known cost of that order: a non-managed `PreToolUse` hook placed after the managed one can still rewrite the input after the managed check allowed it. Without an installed server the leg reports that it did not run. Safe and bare mode: the host reports its startup mode through the optional `SettingsPort.hooksStartupMode()`, which both the plain and the plan path read and which answers `'safe'` or `'bare'`; the flag already carried by the plugin hooks reading, which only the plan path reads and which cannot tell the two modes apart, counts as safe mode unless the reader named a mode. In either mode only managed-settings hooks are sent from the settings sources: bare mode is handled like safe mode, so managed hooks are still sent, also under the managed-only gates. In both modes plugin hooks are excluded as before and the session goal's Stop hook is still sent. Only the two exact words count; a host that does not implement the reader, returns `undefined`, returns any other value or throws keeps the previous request byte for byte, checked for every such reply, both flag states, both paths and with or without a session goal; any other value and a throw each leave one debug line, and so does a reader written as a property instead of a method, which narrows nothing. The reader is called at most once per request and not at all when hooks are already switched off entirely. The optional server leg requires the left-out sources' hooks not to run while the managed ones still run and still deny, in either mode. A hook written in more than one settings place is sent once: entries on the same event, in groups that are identical apart from their hooks (same matcher), whose whole content is the same (every key and value, key order ignored, a key whose value is undefined read as absent) are sent once, at the position of their first occurrence in the concatenation order; the other entries keep their order, groups without duplicates are sent as they were, and a group left empty is not sent. Entries that differ anywhere are both sent: a blocking hook and an asynchronous copy of the same command, a different timeout, run-once flag, failure policy or model, different HTTP headers or allowed environment variables, even a different status message — merging those would leave only one of them, and a blocking guard merged into its asynchronous copy would silently stop blocking. A merged copy is the same hook and is not reported as dropped. 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 either; 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 one-second-timeout copy next to a ten-second-timeout copy of a two-second command to be sent as two entries, the first timing out and the second completing. When the managed settings turn all hooks off, the not-run notice list gains exactly one line saying so; in every other case that line is absent. |
|
|
618
618
|
| `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. Since 0.100.0 it also pins the sibling word for credential values (a fact that can be false; absent on older servers) read the same way by its own reader. Since 0.101.0 the judgement is two-armed: the credential-values word is an input of the decision (the request asks for the whole environment only when both words are confirmed), a missing or unreadable value-word reading omits the field instead of silently opening it, an identity that is not verified is reported before the value word is, only properties the input and the readings own count (an inherited or polluted property is read as missing), and a second sentence says that the engine keeps its own credential variables out of shells but has not confirmed that it also keeps out copies under other names; the truth table, the loosening-direction negative controls for the second word, both sentences in English and Chinese, and the end-to-end path through the capabilities cache are pinned. |
|
|
619
619
|
| `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. |
|
|
620
620
|
| `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. |
|
|
@@ -648,7 +648,7 @@ guard still cross-checks the table by name).
|
|
|
648
648
|
| `scripts/run-tool-history-accepted-test.mjs` | The accepted-history bit on a result frame. After a tool-history mismatch parks a session, the next ending that the engine actually accepts is flagged once with `_sema_tool_history_accepted`, so a way-out picker does not keep pre-selecting the broken turn. The suite checks the key and its reader, the ledger semantics (reported once; a rate-limit, failure or blocked ending in between does not lose the bit; a replay is not an acceptance), an independent oracle over seeded random sequences, the real stream pipeline end to end, and that the key is minted from exactly one place and registered in the type-superset table. |
|
|
649
649
|
| `scripts/run-attachments-credential-word-test.mjs` | What happens when the attachments setting holds something that looks like a credential. A single unrecognised word used to fall back silently to the defaults, while naming a retired key was refused loudly. Now a single word, or any list entry, that has the shape of a known credential is refused: the thrown error and the on-screen sentence both leave the value out (the token field carries the redaction marker, never the text) and say that a credential does not belong in this setting. Ordinary unrecognised single words such as a typo keep their historical fallback to the defaults, and every recognised spelling answers byte for byte as before. |
|
|
650
650
|
| `scripts/run-engine-health-facts-test.mjs` | The self-reported facts an engine publishes on its unauthenticated health body, read in one place: which centre it is attached to, which identity sources it accepts, whether an operator exists, and whether it says it is draining. Each of the four is read on its own, from own data properties only, and a malformed one is absent rather than guessed; the others are unaffected. Absence never means the negative: a missing centre is not no centre, a missing drain object is not not draining. Draining is read only from the engine signal object, never from the deprecated flat boolean, and the guard compares it against the settings contract's own signal reader on a pool of well-formed signals. The same reader also returns, each on its own, the engine version as written, the in-flight count (zero is a reading, a missing count is not zero), the data root exactly as the engine reports it, and a marker that is present only when the engine says its in-flight count already includes background shells left running in sessions; that marker is never false. |
|
|
651
|
-
| `scripts/run-delegated-prompt-absence-test.mjs` | Why a delegated subagent's prompt could not be shown
|
|
651
|
+
| `scripts/run-delegated-prompt-absence-test.mjs` | Why a delegated subagent's prompt could not be shown, and whether that still holds. The fetch carries a closed reason with a ready sentence in the language the host asks for: no location for the conversation has been reported to this client, the session store has no conversation for it, access was refused, the conversation was read but holds no delegated prompt, or the reason is unknown. A synchronous reader answers whether the last outcome still stands for a task. Every outcome is remembered together with the conversation location it was reached on, so a different location never inherits it and a read that finishes after the location changed is dropped; an already-running fetch carries no reason because it is not an absence. |
|
|
652
652
|
| `scripts/run-error-supplement-roles-test.mjs` | Who decides whether a one-sentence explanation can stand in for the raw error line. When a request fails, the engine's machine-readable error line may be followed by a sentence this package wrote; whether that sentence fully replaces the line or only adds one thing to it is declared by the package for each sentence reader, and the host reads the declaration instead of guessing from sentence length. A reader is declared a replacement only when its sentence says on its own what happened, what was left untouched and what to do, never points at a neighbouring line, and never leaves the key fact to the engine's own message; a reader whose sentence depends on the raw line is declared a supplement, and a reader nobody has declared is treated as a supplement as well, so an unknown or newer reader can never hide the raw line. The lookup goes by the reader function itself rather than its name, so renaming or wrapping a reader cannot silently inherit a role. The suite samples every declared sentence in both languages and checks the evidence for its declared role, and it fails when a new reader ending in `Content` or `Supplement` appears on the root entry without either a declaration or a written reason for staying undeclared. |
|
|
653
653
|
| `scripts/run-argv-values-equivalence-test.mjs` | The headless `-p` lane no longer scans the command line itself. Its resolvers for permission mode, scenario, limits, final verification, interactive tools, sandbox and the live tool face now take the values the terminal's own option parser already settled — a small object per flag group — and the old shared last-flag-wins scanner plus the six argv readers built on it are gone from the public surface, so exactly one layer reads a command line and hosts without a command line (web, desktop) pass the same values. The behaviour of the `-p` lane is unchanged: the suite replays more than a thousand argument vectors — hand-written edge cases for every flag (missing value, flag-shaped value, `=` form, repeats, tokens after `--`, both permission flags, empty strings) plus a seeded random mix — under three environments through a stand-in for the terminal parser, and requires the full outcome of all seven resolvers to match, byte for byte as a digest, what the last release that still read argv produced for the same vectors. It also pins the input discipline of the new value shapes (hostile objects count as absent flags and never throw; wrongly typed fields count as absent; the empty string is a value), the exact meaning of a flag that is present but has no value for each resolver, the three-state reader of the deployment knob (absent / a word / invalid with its notice, no one-shot latch, frozen results) and the independently exported downgrade notice for the `dontAsk` mode, and that none of the eight modules mentions an argument vector in code. |
|
|
654
654
|
| `scripts/run-slash-command-gate-test.mjs` | The slash-command gate and the command roster, shared by every host. Whether a slash command is usable on this deployment used to be decided three times — once in the terminal client against its own roster file, and again, from capability readers, in each of the web and desktop command surfaces. The package now owns both halves: a roster of command names in three buckets (local interface state, mappable to the SDK, not applicable to a deployment) pinned to the Claude Code version it was taken from, with per-command overrides, and one judge that takes a command reference and a capabilities snapshot and answers allowed, degraded (visible, with a gap to render), hidden (loaded and callable by exact name but left out of help and completion) or denied, with the reason from a closed set of seven words that hosts can show as the disabled reason. The judge reads capability bits as an open set and fails closed: a missing snapshot, a missing bit or a false bit never counts as present, though a degradable command is kept visible; the host's own two gates are applied first and a missing answer from them means they were not asked, not that they failed. Everything read from the host is read as plain own data (accessors are not run, prototype-only keys do not count, a hostile object counts as absent), command names are matched exactly and through a map so that names like `__proto__` never reach a table entry, and verdicts are frozen and freshly minted. The suite compares the roster and overrides with the terminal's own file when it is available, checks a hand-written verdict matrix, and replays a line-by-line transcription of the terminal's own gate steps over every name, fourteen snapshot shapes, three sources and four host-gate combinations. |
|
|
@@ -673,14 +673,14 @@ guard still cross-checks the table by name).
|
|
|
673
673
|
| `scripts/run-session-namespace-key-test.mjs` | Engine namespace key of the session map. The key under which a connected engine's continuity record (engine session id, last task id, sync watermark) is stored is its lowercased origin plus its normalized path: trailing slashes are dropped, path case and percent-escapes are kept as written, and the query, fragment and userinfo are not part of it, so two engines behind one relay origin no longer share a record. A target with an empty path keeps the earlier origin-only key and reads its older entry unchanged; an entry stored under the bare origin is never read for a target that has a path (one reading, no fallback). The self-spawned engine stays `local`; a malformed url, and a url whose origin is opaque, falls back to its raw lowercased string. Cells cover the key shape and spelling variants, role and origin separation, the same-engine comparison an auto-sync no-op check builds on, reading of older records, boundaries (IPv6 hosts, default ports, dot segments, percent-escapes, doubled slashes), the single exported reading, and a never-throws sweep. |
|
|
674
674
|
| `scripts/run-core-7590-pick-projection-test.mjs` | Readings that follow the engine's 7.59 changes. An MCP server that answers 401 or 403 is now reported with the failure kind `unauthorized` and an `authorization` record (`reason`, optional trusted `authorizeUrl`, optional untrusted `resourceMetadataUrl`); `readMcpAuthorization` reads it in three ways (absent, present but unreadable, a record) from own data properties only, and the wiring-receipt row, the per-server connection reading and the reconnect result carry the record as given or a `authorizationUnreadable: true` flag, never folding unreadable into absent. The `mcp.authorization_required` notice has a fact reader (the `context.image_removed` reader left when the engine renamed that notice to `context.media_removed`; the renamed notice's reader, `readContextMediaRemoved`, still reads the old name for servers that send it); the status frame's `sleptMs` and `waitSegment` reach the retry status; a server name starting with lowercase `remote-` is refused on both config paths while other letter cases are accepted; the ask class `write_protection_on_disk` reads with the same sentence as the `write_protection` mandate; the extension kernel contract is 5; a hook denial carries the denying extension's registered name when known; an MCP server's `startupTimeoutMs` reaches the request on both config paths as given. Names the engine added that this package does not read are pinned to zero code reads. An erase receipt the engine marks partial carries its cause (entries not erased, only leftover stored records that refer to erased entries, or unknown), and the session erase outcome no longer says entries were not erased when only leftover records remain; the decision-conflict reader takes one snapshot of the error's `extra` bag at entry. |
|
|
675
675
|
| `scripts/run-server-7137-pick-projection-test.mjs` | Readings that follow the server's 7.137 additions. A `delegation.ask_unresolvable` notice's `remedies` list is read into the existing fact view (kinds from the engine's closed set, an unknown kind or a malformed member dropped on its own, an empty list never minted), with one sentence per way out from `askUnresolvableRemedyContent`; `invalid_session` (a stored session log whose parent chain does not resolve) is treated like `corrupt_session` on every carrier: no retry offer, the same damaged-session sentence, the original code kept; a `resume.constraint_rejected` refusal carries `acceptableForm` as given whenever the refusal includes it; edited rule text is trimmed at its outer edges (and between the tool name and the parenthesis) by one function, `trimEditedRuleText`, before both the live card arm and the precheck port; a plan-review decision carries `checkpointId` whenever the approval card names its gate and does not read the engine's `planReviewCheckpointId` bit (an approval needs its card and is refused locally, with nothing sent, when it has none; a rejection without a card goes out without the key; a gate the engine refuses to recognise is not carried); `observedSandboxGitCredentialPlane` reads `credentialPlane.sandboxGit` in five states and never reads an absent bit as off; the device lane reading carries `relayUrl` when present; an `inheritEnv` refusal of a request that carried the field, with any value, carries the engine's `reason` word when the field is named, and its sentence states which values the engine accepts instead of claiming the request asked for the whole environment. Session-sync retries stay with the client's retrier, which stops on a terminal permanence. The same readings are also checked end to end through errors the real client library mints: the three session-sync pull legs, `invalid_session` on a read and on a submit, and `acceptableForm` on all five decision legs. |
|
|
676
|
-
| `scripts/run-pkg-policy-registration-test.mjs` | Package policy registration. The package's own `package.json` is checked
|
|
676
|
+
| `scripts/run-pkg-policy-registration-test.mjs` | Package policy registration. The package's own `package.json` is checked against its row in the cross-package policy table. Three items must be equal: the package-name sets of the four dependency kinds (version ranges are not compared; one name more or fewer is red and the difference is printed), the `exports` key set, and the first sentence of `description`. The fourth item, the pinned version, is the version a consumer currently has installed, so it is not compared for equality: it only has to be a valid version string that is not ahead of the highest released CHANGELOG section (equal or older is fine, however many versions older; the `version` field of `package.json` takes no part, so a bump that is not yet released is never a reason for red). The table is taken from `SEMA_PKG_POLICY_PATH` when that is set (no fallback), otherwise from a sibling checkout found by walking up from the package root; when it cannot be reached the whole gate reports a skipped section and exits 0, which is not a pass. Two negative controls: one adds an unlisted dev dependency and expects the dependency check to go red, the other leaves only a very old released section in the CHANGELOG and expects the version check to go red |
|
|
677
677
|
| `scripts/run-core-7600-pick-projection-test.mjs` | Readings that follow the engine's 7.60 changes. The MCP failure kinds include `host_refused` (a host-supplied pipe did not send the request; safe to retry, no liveness reading), and the wiring-receipt row and the per-server connection reading carry it like any other failure word. A `status` frame's `seat` (which seat's model call the frame is about; absent means the main loop's own call) reaches the retry status through both projection layers, open set, malformed values dropped on their own, never minted when absent; a frame about another seat is not labelled with the session's model, offers no switch-model recourse, and in `-p` stream-json takes the `_sema_model_status` frame with `seat` instead of the `api_retry` frame. The single-image size limit used by the human-turn encoder is generated from the engine's image admission table, with the same value as before. The cost breakdown type no longer names the line the engine removed, while older engines' line still passes through. The extension kernel identity reading reports contract 6 for the 7.60 kernel; the kernel the `./mods` subpath passes through and the engine's own kernel carry the same contract number and event table, with the four new stations defined and `tool.call` failing as `refuse`. Engine names this package does not read are pinned to zero code reads. |
|
|
678
678
|
| `scripts/run-core-7610-pick-projection-test.mjs` | Readings that follow the engine's 7.61 changes. The notice `context.image_removed` is renamed `context.media_removed` (images and documents read the same way): `readContextMediaRemoved` reads its facts (`reason` as given, the image and document counts, the session and run ids) or nothing, never throwing, each field read once; the old name is no longer in the engine's own catalog, but servers up to 7.142 still send it, so this package keeps recognizing it as a user notice and reads it with the same reader (cause `rejected_by_provider`, no documents). Four new notice codes are in the catalog with the engine's audiences, and `classifier.seat_rate_limited` is now a user notice. The extension kernel contract is 7 (the four `session.*` rows gain stations). The engine no longer ships the TaskOutput tool; a `task-output` card from an older engine still reads as a rich result. Names the engine added, renamed or removed that this package does not read are pinned to zero code reads. |
|
|
679
679
|
| `scripts/run-session-row-tag-projection-test.mjs` | Conditional writes on a session row. A row's `rowTag` is read only when it is a non-empty own string (absent means the row cannot be written conditionally; never an empty string); the seat record carries it and the seat patch request carries `expectedRowTag` copied from the same row, with both seat gates checking the shapes. After a refusal the next step reads the same way through the real client error: already applied (the row already holds the values written), decide again against the current row and its new tag (never resend as is), a read-only mirror (fork first), or an older replica that does not know the tag (keep the pending change, never drop the tag and resend). A list row whose stored history cannot be read carries `unreadable` with the engine's error code; `sessionRowUnreadableOf()` is its only reading (present only for a plain object, the code passed on as sent), and the seat record carries it as `unreadable` with absence meaning the list did not report the session as unreadable. |
|
|
680
680
|
| `scripts/run-server-7139-pick-projection-test.mjs` | Readings that follow the server's 7.139 changes. The repin is checked against the installed type package itself (the execution-placement and children-retire calls exist, the old device-binding read does not, the row tag, request tag and background-row session id are optional keys). Each new refusal code from this release reaches the generic failure classifier as an HTTP verdict carrying the server's own code and words without being taken for a dedicated arm, and the resume fold names it. A background-agent row carries its child session id only when the server sent a non-empty own string. Names the server or type package removed have no code-position reads left in the package sources. |
|
|
681
681
|
| `scripts/run-display-decision-body-profile-test.mjs` | The reading profile for body text a person reads before choosing or before bytes are written — the plan on a plan-review card, a diff, the body of a new file — exported as one frozen options object so that every shell reads the same one instead of minting its own. Only two things are left alone: the full-width ideographic space U+3000 (two cells wide, so it does not pass for an ordinary space, and Chinese, Japanese and Korean text uses it as ordinary typography) and the two-character CRLF line break that Windows editors save. Everything else that can pass for a space without being one is marked — the no-break space and the other one-cell-wide space characters, the Hangul filler characters, the blank Braille cell — as are zero-width and bidirectional controls, the other control characters and a lone carriage return. The blank-character switch takes `blanks: 'except-ideographic'` for this (the earlier `'fillers'` value, which let the whole space class through, is withdrawn and now reads as off), and every other old value behaves exactly as before. The guard runs a matrix of what stays unmarked and what stays marked, in the plain form and in the injective backslash-paired form, which must carry the same set of marks; walks the whole Basic Multilingual Plane to prove the profile's blank set is the full set minus U+3000, checked against the Unicode space-separator category rather than against the package's own table, and that the source holds each filler code point in one place only; pins that every old option combination (648 of them, over 104 texts that include credential-bearing strings) still yields byte-identical output and marks to an earlier release; fuzzes the credential scanner to prove the profile reads exactly like the full switch wherever neither U+3000 nor its escape form appears; and carries three negative controls inside the guard — U+3000 marked again, CRLF marked again, every blank let through — each of which the matrix must turn red on, with the red reading printed, so an all-green run cannot mean the matrix measured nothing |
|
|
682
682
|
| `scripts/run-run-follow-projection-test.mjs` | One shape for "this screen is no longer following run X". The fact is minted only where the package's re-attach judges give up (count-based policy, wall-clock window, headless continuation), carries the run and the last break cause when known, and renders as one catalog sentence (English / Chinese) that never says failed, finished or resend and never mentions approval cards; the failure reading stops offering retry for it, the seat reader rejects a contradictory retry pair, and the headless idle line becomes one combined sentence (byte-identical when the fact is absent). An in-leg draining ladder that runs out is handed up exactly once through the ask-bridge `onBreak` dependency (same port on the root entry and the composer, carrying the run the bridge follows after a hand-off), and a lane that never re-attaches uses a zero budget so no leg is left without an upper layer. Also pins the `isStreamEndedWithoutTerminalError` predicate. |
|
|
683
|
-
| `scripts/run-plan-review-card-facts-projection-test.mjs` | Plan-review card facts. A plan-review card now has exactly one set of facts: which gate it asks about (task id and checkpoint id) and which plan text it would approve, or why that text cannot be shown. The package computes them when the stream stops at a plan gate, from the structured result of the main-lane plan tool call, and freezes them. The chrome event, the question frame the package publishes, the options on the card and the answer reader's domain are all derived from those facts, and only a plan that can be shown as written offers approval; otherwise the card offers reject and ask-for-the-plan-again. Each gate gets its own card instance with its own question id. The gate drives the real stream and the real arm, answer and reopen paths through the package root: clean, marked, oversized, missing and ambiguous plans, sub-agent plan calls, replays and reconnects, frames without a gate identity, malformed frames, the plan-file form, other review gates, an answer from a retired card, an approval label on a withheld card, ask-again with a note, a frame round-tripped through JSON or corrupted, a new gate on the same run with no plan call in between, a conflict on one gate followed by a replayed terminal frame, a run record pushed out of the bounded registry, a temporary reading after a stored one, and that the chrome facts and the frame facts are the same frozen object. Cards the package mints when it reopens or resumes a review take their facts by the same rule: the facts already stored for that gate when they are current, otherwise the run's ledger is read back (at most about three seconds; the run must be parked at the same gate before and after the read, two passes must agree, and the gate identity must match the one the caller asked for), and a reopened card offers approval only when that reading shows the plan. The gate also drives the self-heal reopen with the gate identity from a busy reply (no ledger read), concurrent reopens of one gate (one ledger read, one card), a stream that moves to a new gate while an older ledger read is in flight (the older reading is dropped and the new card stays), a retry after an unreadable reading, a conflicting reading that swaps the card instance, and the stored high-water mark. |
|
|
683
|
+
| `scripts/run-plan-review-card-facts-projection-test.mjs` | Plan-review card facts. A plan-review card now has exactly one set of facts: which gate it asks about (task id and checkpoint id) and which plan text it would approve, or why that text cannot be shown. The package computes them when the stream stops at a plan gate, from the structured result of the main-lane plan tool call, and freezes them. The chrome event, the question frame the package publishes, the options on the card and the answer reader's domain are all derived from those facts, and only a plan that can be shown as written offers approval; otherwise the card offers reject and ask-for-the-plan-again. Each gate gets its own card instance with its own question id. The gate drives the real stream and the real arm, answer and reopen paths through the package root: clean, marked, oversized, missing and ambiguous plans, sub-agent plan calls, replays and reconnects, frames without a gate identity, malformed frames, the plan-file form, other review gates, an answer from a retired card, an approval label on a withheld card, ask-again with a note, a frame round-tripped through JSON or corrupted, a new gate on the same run with no plan call in between, a conflict on one gate followed by a replayed terminal frame, a run record pushed out of the bounded registry, a temporary reading after a stored one, and that the chrome facts and the frame facts are the same frozen object. Cards the package mints when it reopens or resumes a review take their facts by the same rule: the facts already stored for that gate when they are current, otherwise the run's ledger is read back (at most about three seconds; the run must be parked at the same gate before and after the read, two passes must agree, and the gate identity must match the one the caller asked for), and a reopened card offers approval only when that reading shows the plan. The gate also drives the self-heal reopen with the gate identity from a busy reply (no ledger read), concurrent reopens of one gate (one ledger read, one card), a stream that moves to a new gate while an older ledger read is in flight (the older reading is dropped and the new card stays), a retry after an unreadable reading, a conflicting reading that swaps the card instance, and the stored high-water mark. It also pins the published-card presence check at the publish step, the card header, the replaces-mark written only after hand-over, the registry-wide monotonic generation, and the four fact functions at the package root. |
|
|
684
684
|
| `scripts/run-server-7141-pick-projection-test.mjs` | Readings that follow the server's 7.140 and 7.141 changes. The repin is checked against the installed type package (rule entries may be a string or a `{ rule, layer }` object, the memory backend word is `mysql`, the new keys are optional). The memory sync reading appears only when the server sent a well-formed `memorySync` and reports a parked capture as parked, and the sentence for a paused sync names the layers only when every name is printable and short, and names the operator's way out only when every layer reports it; an erasure refused because the personal store could not be judged reads as not erased; the engine lease on the health reading needs both numbers; a workflow completion carries its completion id through the stream, the adapter and the notification facts, while the engine's diagnostics text is offered for display only and never enters the model-facing notification body or the queued item; a subagent tool call the engine closed without running is shown as not executed rather than as a failure; the permission-mode write and MCP transport capability bits are read with absence kept apart from false, and an MCP server whose transport the engine says it does not support is held back with a sentence naming the transport. When the engine reports layered rule entries, deny and ask rules from settings files go out as `{ rule, layer }` (deduplicated within a layer, not across layers), request-level rules and allow rules stay plain strings, and with the bit absent the body is byte-identical to the previous release. Names the server or type package removed or that the package does not consume have no code-position reads in the package sources, and tasks are submitted only through the streaming call, so the reply body of a non-streaming submit is never parsed. An MCP entry's `sessionMetaKey` goes onto the request as written on both the local and the center config paths, and is left out when not set. |
|
|
685
685
|
| `scripts/run-notification-completion-dedup-test.mjs` | One completion reaching the client over several channels is shown once. A background completion can arrive as the live notification frame, as a parked-inbox entry replayed later, as a frame replayed after a reconnect, and as a fleet notification; the service now stamps the same completion id on all of them. The render de-duplication of task notifications reads that id: a notification is identified by its locally computed key, which is always present, plus the completion id when the notification is terminal, not external and carries one, and every key a notification carries is recorded as soon as it is seen. A notification whose local key was already seen is a replay of the same frame and is skipped entirely, as before; one whose only match is the completion id is another copy of the same completion and is not rendered again, while its other duties (withdrawing a still-queued card of the same run, closing the panel row, the terminal hint) still run exactly as they did before. So two copies of the same completion whose local keys disagree (for example one carries a sequence number and the other does not) render once, three mixed copies render once, and two completions that differ in both the id and the local key still render twice. The local key stays because three things never carry the id: external notifications, frames from servers that predate the id on inbox entries and replays, and records restored from a saved transcript or seeded by a host; a frame that carries the id is still matched against those records, and the exported ledger keeps both keys so a host reading it by the local key sees no change. The cross-channel checks that stop a card already shown from a status probe, a fleet notification or a poll result from being fed or shown a second time stay as they were, because some of those sources never carry the id; the gate pins each of them, and the queue withdrawal that removes a still-queued card when the engine's echo arrives. Ordering, concurrency and failure are each exercised: all 24 arrival orders of four channels show the completion exactly once, two interleaved streams on one adapter render it once, and an unreadable, empty or non-string id falls back to the local key without throwing |
|
|
686
686
|
| `scripts/run-background-task-table-projection-test.mjs` | Background-task table rows after a compaction, read the way the engine mints them: a row is the `- [<id>] Task "<description>" ` prefix, a status phrase, and at most one optional tail that starts with ` — ` and runs to the end of the line. The tail is projected verbatim onto the optional `read` field of the task-status attachment (absent means the engine sent none — never an empty string), rows without a tail stay byte-identical to the previous release (a frozen copy of the old reader and a seeded random pool of 4000 batches prove it), a `parked` row is recognised with or without a tail, a second ` — ` inside the tail stays part of it, and unknown phrases and the `(+N more …)` footer line are still not recognised. The status phrase is always read after the last `" ` (quote plus space) on the line, so a description that itself contains a phrase never turns an unknown status into a known one; a table whose row description breaks across lines (an opening quote never closed on a line that is not the last non-empty one) is not read as rows at all and falls back to the generic row. The engine's own table renderer is driven for real, status by status and read form by read form. |
|
|
@@ -701,11 +701,11 @@ guard still cross-checks the table by name).
|
|
|
701
701
|
| `scripts/run-seat-computer-use-switch-test.mjs` | Host switch for computer use. Whether the host currently allows computer use, where that value came from, whether an environment variable locks it, and when a change takes effect are answered once, by the host that carries the feature. The seat capability table gains `computerUseSwitch`, reported `true` only by a host that really carries computer use; a browser lane (no seat store) and the terminal never report it, and an absent capability means the switch is not rendered at all, not that it is off. `getComputerUse()` returns `{ enabled, source, lockedByEnv, appliesOn }`; `setComputerUse()` takes `{ enabled: false }` or `{ enabled: true, confirmed: true }` and returns `{ ok: true, changed, state }` or `{ ok: false, reason, state }`, a refusal being a receipt rather than an exception. The words are three frozen tuples: sources `env` / `settings` / `default`, refusal reasons `locked-by-env` / `confirmation-required` / `invalid-request` / `write-failed`, and the effect point `next-engine-start`. The request is untrusted input: `checkSetComputerUseRequest` accepts exactly the two shapes, and `planSetComputerUse(request, lockedByEnv)` decides, in the order malformed shape, then lock, then missing confirmation, whether the host may write; a lock flag that is not strictly `false` counts as locked, and the function never throws. Both outbound gates are closed shapes that also refuse two contradictions: a state locked by the environment must name `env` as its source, and a `locked-by-env` refusal must carry a locked state; the converse, and the locked flag on a successful receipt, are left to the host. Each key of the request is read once and the value judged is the value used. The suite checks registration, both outbound gates key by key, the inbound judge, the planner against an independent reference over a generated pool, the capability reading (including the browser lane and the bare name `computerUse`, which is not the capability), and an end-to-end host double that passes every receipt through the same gate. |
|
|
702
702
|
| `scripts/run-server-7144-pick-projection-test.mjs` | What the client reads from the server's newest session, approval-queue and live-text facts. Each retired, renamed or added server fact has exactly one reading: an older notice or error code that servers in the supported range still send keeps its own reader until the support floor moves past it, and the new code beside it is never folded into the old view or the other way round. Two new error codes are recognised and passed through unchanged. A cross-session message held for review shows an expiry time only when the server sends one; it is never computed from the hold time. A resume refusal tells the engine's row judge tripping over a row (an engine fault) apart from a row that does not read the way it says it should. Text that arrives in several blocks lands as one transcript row per block, and a credential split across two blocks stays out of the transcript on servers that mask it. A block the engine delivers only as its end frame shows up in the live transcript between its neighbours, with the same text a reloaded session shows on servers that include it in their stored rows (older servers leave it out of the stored rows). The user-facing sentence for a refused fork or log replacement on a session archive that still carries an earlier version's owner stamp is read by code only (the generic conflict of earlier servers does not match), is worded in English and Chinese word for word from the server's fixed sentence (compared against the server fixture when present), and carries no machine code. |
|
|
703
703
|
| `scripts/run-frame-arm-completion-projection-test.mjs` | Two frames the server sends in normal operation no longer count as dropped frames. The single-card close frame of the in-stream approval family (`approval_complete`) goes through the same envelope reading as the open and revoke frames and reaches the host as an `approval_complete` chrome event carrying the frame as-is, whose stream context the existing approval frame context reader answers. The session title notice (`session_titled`) becomes a `session_titled` chrome event with the session id and title, read as own non-empty string fields; the session row stays the only source of the title. Approval-card frames (open, revoke and close) never count as proof that a submission created a run: the delivery ledger and the event leg's refusal detection read one shared check, so a busy refusal that follows a replayed card still reaches `onRefused`. Malformed copies are still reported as `malformed`, frames the projector does not know are still reported, and both events stay on the host side of a seat. |
|
|
704
|
-
| `scripts/run-plan-review-decide-card-projection-test.mjs` | Plan-review decisions answer the card that was shown. The gate a plan-review decision answers has one source: the card facts that come with it. The package's own card and a reopened card hand over their own facts; the public decision call reads the `card` option. That gate identity goes into the decision body and is the gate the result is compared with afterwards. Approving needs a card that offers approval (a plan shown as written, with a gate identity); without one nothing is sent and the result says why, in the card's own words when the card withheld approval. If the gate moved on before the decision arrived, the engine refuses it and the result says the plan on the card is no longer the one waiting, without reopening anything. If the engine cannot recognise a gate identity that is still current, the result says so
|
|
704
|
+
| `scripts/run-plan-review-decide-card-projection-test.mjs` | Plan-review decisions answer the card that was shown. The gate a plan-review decision answers has one source: the card facts that come with it. The package's own card and a reopened card hand over their own facts; the public decision call reads the `card` option. That gate identity goes into the decision body and is the gate the result is compared with afterwards. Approving needs a card that offers approval (a plan shown as written, with a gate identity); without one nothing is sent and the result says why, in the card's own words when the card withheld approval. If the gate moved on before the decision arrived, the engine refuses it and the result says the plan on the card is no longer the one waiting, without reopening anything. If the engine cannot recognise a gate identity that is still current, the result says so. An approval always carries the card's gate identity, so on such a gate the engine refuses it again and nothing changes; a rejection or a request for a new plan on that gate is sent without it, and when it was actually sent its result says so, without claiming it took effect. Both the public decision call and the raw bridge verb read the decision word once and send nothing when it is not one of their own words. A default-mode retry carries the same gate identity as the first send, built from a frozen copy of the request body. Why a decision did not take effect is one cause word (`card_missing`, `card_withholds`, `gate_moved`, `gate_untrusted`, `stored_form_unsupported`), set only for the five effect pairs of one table and the same on the queue item and the callback. The raw bridge verb `HitlBridge.reviewPlan` refuses an approval or edit without a gate identity before sending anything. The gate drives the public decision call, the package card, a reopened card and the raw bridge verb against an injected client and an installed target, with a fake engine that models the gate id a run shows separately from the trusted identity of the current gate: missing and malformed cards, a card for another task, withheld cards, a moved gate, the unrecognised-gate path and its alternatives, repair after an unrecognised gate, a client that rewrites the body it was given, the bounded set of unrecognised gates, every effect and cause pair, the public types, and that the cause is set at most once per decision. |
|
|
705
705
|
| `scripts/run-session-engine-lane-projection-test.mjs` | Which engine a session lives on, decided once when the session starts. `sessionEngineBindingFor` turns the engine a host is wired to into a fixed identity key (a versioned JSON tuple for the host's own engine, for a declared engine by center, space and name, or for a direct address) and fails with a reason instead of guessing when a piece is missing; `commitSessionEngine` is the only first writer of the session's engine snapshot (first write, identical replay, a different value refused, an older record restored only on proof; the one other writer is `repointSessionEngine`, the session half of the one-time engine re-point) and raises a notice when the host's own engine is recorded as a Cloud session; `readSessionEngine` only reads the snapshot; `sessionDispatchTarget` picks the target for a session from that snapshot and the host's wiring snapshot and refuses with one of four reasons instead of falling back to the current default; `foldSessionPlacement` keeps a session's execution lane ordered by the device revision. The seat session row carries `engine` and `executionLane`, and the seat capability `engineModeDefaultOnly` says that switching the engine only changes the default for new sessions. The dispatch refusals also include `session-ambiguous` (the same session id sits on more than one engine's ledger); the host judges it and takes the refusal from `sessionDispatchRefusedFor`, and the dispatch judgement itself never answers it. A new session's lane intent (`start({ engine })`) uses exactly the two words a session's engine `kind` carries and the binding commit accepts. |
|
|
706
706
|
| `scripts/run-park-row-card-leg-projection-test.mjs` | Durable pending rows that the engine says it cannot continue: a row whose `rowVerdict` is not `ok` (written by a newer engine, bytes this engine does not resume, an unknown verdict word, or an unreadable verdict) is not shown as an approvable or answerable card and no decision is sent, on both card legs (durable fetch and question-card reopen triage) and in the pending-row count; the outcome carries the verdict and the engine's own sentence, and the fail-soft closing sentence no longer says to decide on the card. Rows with an `ok` verdict and rows from servers that do not send a verdict behave as before. Also pinned: the one way-out sentence shared by the unbound-row refusal and the local `binding_missing` stop, and the synthesized card frame carrying `sourceSessionId`. The same boundary applies when a host hands the decide or answer entry a row it fetched itself, when the verdict position is an accessor (it is not run), and to rows whose gate the engine cannot read (listed by newer engines with a non-`ok` verdict), while rows on a gate this leg does not own are still left to their own leg. Pinned on rows read by the real checkpoint store of two server generations and on rows summarized by the installed engine. When a reopen is refused because this engine cannot continue the parked question row, the self-heal row carries the engine sentence and the cannot-be-decided-here way out, never the resume or decide-on-the-engine way outs. Since 0.108.0 the `settled` verdict of the question-card triage and of the bounded re-check loop also carries `rowTaskId` (the judged row's own run id, present only together with `rowVerdict`; a sole child-run row resolved by fallback yields the child's id, never the requester's; `unborn` / `row` verdicts never carry it), and the root entry exports `checkpointRowBarOf` / `checkpointRowBarSentence` / `CheckpointRowBar` (engine sentence first, package sentence when unreadable). Since 0.108.0 the way-out sentence follows the pending row's own expiry position: with a finite `deadline` it still says to cancel the run or let the approval expire, with `deadline: null` (no expiry timer) it says to cancel the run and that this approval has no expiry timer, and when the position is absent or unreadable it only says to cancel the run; the decide-refusal receipt and the local `binding_missing` stop have no row in hand and always use the cancel-only sentence, the bridge stop reads the row's `deadline`, and the self-heal row reads the optional `_sema_rowDeadline` the host hands next to `_sema_rowVerdict`. Pinned on three states in both languages, a reader table that never runs accessors, and the host verdict in eight shapes. |
|
|
707
707
|
| `scripts/run-steer-input-ids-projection-test.mjs` | Steered input ids reach the host, and inputs the engine reports as unused are matched by id. The only id a steered input has is the `messageId` the server puts on the steer receipt; the client never mints one. Every steer path in the package that holds a receipt hands that id to the host: the control router's `steer()` returns the receipt as received for live (200) and parked (202) deliveries, and the self-heal path puts it on the `running-steered` outcome (absent when the receipt carries none). `readUserInputUndrained()` reads `detail.inputIds` only when the list names exactly the counted inputs, and `undrainedInputsVerdict()` matches it against the host's ids: an id the host sent and has not yet resent under the same id is resent, an id already resent once is handed to the user, and everything else (an unnamed notice, the server's crash report, a sub-agent copy, or ids the host never sent) is reported by count only. If the host's record of resent ids cannot be read, nothing is resent. When the child-session field on an undrained-input notice is present but unreadable, the id list is withheld and nothing is resent; a notice for this session is still resent by id. |
|
|
708
|
-
| `scripts/run-hooks-unread-keys-projection-test.mjs` | Hook settings carry only the keys the server's hook reader reads. A matcher group or hook entry that also holds a key the reader does not read (a failure policy, rewake text, a cloud placement, arguments on a non-command hook, a misspelled group key) is not sent: the server refuses a whole new request for such a key and would otherwise drop it on replay, so the key never silently stops meaning what it says. Which keys the reader reads comes from the same settings-contract classifier the server uses (its projection of each piece) plus the server's own wire keys, with no field list of the package's own. Settings hooks are recorded with the reason `key_not_read` and the coordinates of every unread key, plugin hooks get the same reason; a hook that will then run nowhere is listed in the hooks-not-run notice with coordinates in the same form as the server's `unknownKeys` and the way out (remove the key, or use a server that reads it). The turn is never refused for this, managed settings included. When the build-time copy of the classifier is newer than the connected server's own, a per-generation table of the server's reader also removes the entry keys that generation does not read (a server version that is unreported, unreadable or newer than the table is treated as not reading them), and a dropped failure policy adds a sentence that a hook set to block on failure will not block. Checked against a real server admission path: the projected body is accepted, the body as written is refused, and the package's coordinates equal the server's list. |
|
|
708
|
+
| `scripts/run-hooks-unread-keys-projection-test.mjs` | Hook settings carry only the keys the server's hook reader reads. A matcher group or hook entry that also holds a key the reader does not read (a failure policy, rewake text, a cloud placement, arguments on a non-command hook, a misspelled group key) is not sent: the server refuses a whole new request for such a key and would otherwise drop it on replay, so the key never silently stops meaning what it says. Which keys the reader reads comes from the same settings-contract classifier the server uses (its projection of each piece) plus the server's own wire keys, with no field list of the package's own. Settings hooks are recorded with the reason `key_not_read` and the coordinates of every unread key, plugin hooks get the same reason; a hook that will then run nowhere is listed in the hooks-not-run notice with coordinates in the same form as the server's `unknownKeys` and the way out (remove the key, or use a server that reads it). The turn is never refused for this, managed settings included. When the connected server reports its hook reader's field list for each entry type, entry keys are judged by that list alone; otherwise, when the build-time copy of the classifier is newer than the connected server's own, a per-generation table of the server's reader also removes the entry keys that generation does not read (a server version that is unreported, unreadable or newer than the table is treated as not reading them), and a dropped failure policy adds a sentence that a hook set to block on failure will not block. Checked against a real server admission path: the projected body is accepted, the body as written is refused, and the package's coordinates equal the server's list. |
|
|
709
709
|
| `scripts/run-task-notification-admission-test.mjs` | Wake-up submissions for task notifications are confirmed only on positive evidence. `taskNotificationAdmissionOf` classifies one read-back of the run bound to a submission key as `admitted`, `pending`, `not_found` or `unreadable`, with a reason from a closed set of eight; the verdict follows from the reason. The read-back is passed in one of three explicit forms: the 200 record, a not-found form the caller passes only after recognising the endpoint's own 404, or an error form for everything else. `admitted` needs either an ISO date-time in the record's `admittedAt` (a field the service has not shipped yet; read when present, and `null` is not taken as a no) or an integer `result.stats.turns` above zero, the same reading the service uses to latch acceptance. A running row however long it has run, zero-turn completed, failed, parked or blocked rows, rows with token usage but no turns, and missing or unknown status words or codes are all `pending`; the status word, error code and token count are never read. Errors are never inspected: any error form, a raw error object, a malformed body or an unrecognised input is `unreadable`, never `not_found`. Only own data properties are read, accessors never run, and the function never throws. `batchCompatible` now refuses to batch a task notification whose fact carries a completion id, whether it is the head or the follower, so each such notification goes in its own submission; the host passes the queue item's notification fact through on the batching entry. The suite covers the two pause points before placement and before engine output, the point between a zero-work refusal being written and its key being released, reads counted through instrumented accessors and proxies, the reason and verdict sets in both directions at compile time and at run time, deliberately broken judges that must fail the matching cells, and the record shape against the type package's declarations. |
|
|
710
710
|
| `scripts/run-hooks-authored-source-projection-test.mjs` | Hook group sources on the wire are set only by this package. The server runs each hook matcher group in a phase chosen by the group's `source` (a managed-policy source runs in the final managed phase), so a `source` written in a settings file or in a plugin's hook manifest would let a project, user, local or unmanaged-plugin hook claim a managed phase. Any `source` written there, whatever its value (even one that matches where it came from), keeps that matcher group off the request: settings hooks are recorded with the reason `source_not_authored`, plugin hooks get the same reason, and a hook that will then run nowhere is listed in the hooks-not-run notice with the coordinate `settings.hooks.<event>.<group>.source` and the way out (remove the key). The key is never stripped or replaced with another value. A group whose `source` cannot be read plainly (an accessor, a proxy trap) counts as written. Pieces the settings classifier does not accept are left to it and to the pre-send check, so a malformed guard still refuses the turn. Every request body the suite builds is swept: no matcher group carries a `source`. Checked against a real server's hook reader: the body as written puts the hook in a managed phase, the projected body leaves every group unmanaged. |
|
|
711
711
|
| `scripts/run-seat-actions-test.mjs` | The seat action channel. The host can ask the window it hosts to do one of a closed set of things over one one-way event, `onSeatAction`: open the new-project dialog (`new-project`), or open a fresh new-session page with text placed in the prompt box and, optionally, a folder for the next session (`prefill-composer`), which is never sent on the user's behalf and never replaces a draft the user has already typed. Focusing a session is not an action word: it already travels as the `focusedSessionState` store push, and there is no route-opening word. The window first claims the words it really handles with `claimSeatActions`, and the host offers an entry point (a menu item, a link route) only for claimed words, so a host never shows an entry that does nothing. A claim may only name words from the closed set: one word outside it refuses the whole claim and names that entry, repeated words are harmless and are collapsed in closed-set order, and a malformed claim reads as an empty claim. The outbound gate is a closed shape per action (an unknown action word, an extra key, an over-long or control-character text, a bidirectional-override character or a lone surrogate is refused by name; accessors are never run and the gate never throws), and the window-side reader `readSeatAction` reads an unknown word as `unknown` (dropped, leaving a trace) rather than as malformed, and never throws. One optional capability bit `seatActions` governs the claim verb and the event; a host that does not report it reads as `not_reported` and the window neither claims nor subscribes. The suite checks the registration, both gates and the reader key by key, the capability reading, and an end-to-end host double that sends only claimed words. |
|
|
@@ -718,7 +718,7 @@ guard still cross-checks the table by name).
|
|
|
718
718
|
| `scripts/run-local-engine-anchor-shape-test.mjs` | The shape of the local engine's discovery record, checked against the host library that writes it. The package reads that record by structure, without importing the host library, so this guard compares the two: every key the package reads must exist in the host library's record type with a compatible type and must not be optional there if the package requires it; keys the package already reads ahead of the host library are listed and turn red once the host library adds them; and the package's older, deprecated copy of the record type must not drift from the host library's until it is removed. The list of keys the package reads is taken from its source, so a new read without a matching entry fails. When the terminal source tree is not available the comparison is reported as skipped, not passed. |
|
|
719
719
|
| `scripts/run-server-7147-pick-projection-test.mjs` | What the client reads from the server's two newest releases. `readEngineHealthFacts()` adds three optional readings to its existing pieces: the engine's first-attach grace window under `engineLease`, an `inflight` piece holding the server's total and, key by key, its per-lane breakdown (a server that sends only the total yields `{ total }`; the `sessionResident` key being present is what says the total already counts session-resident shells, since the bare total of older servers meant something narrower), and whether a service credential without a principal acts for the deployment under `auth`; a piece whose new field is present but malformed is dropped whole, like its existing optional fields. `readCcImportRedeemCounts()` reads the engine's refusal code and sentence on refused rows with the same row reader as rows whose outcome is unknown. Three submit refusals get readers and sentences matched by code and machine fields only: credentials in `settings.env` (naming the variables, never their values), a deleted session id that now runs only for its earlier owner, and the per-deployment cap on runs in flight (with the wait in seconds). Exercised against the server's own `/health` route, its rule-import lane redeeming a ticket minted by an older server, its credential judge and its cap refusal, through the real SDK error mapper. For the 7.147.0 release build the gate also pins the fixture to the registry bytes (lock integrity, no repack marker, bundled engine and settings-contract pins, the shipped wire-change files, the module only the release has), and checks that a request member the server's readers cannot read is refused (400 on submit, 409 on resume) with a reader and a sentence that name which part it is (hook settings, environment variables, another setting, an MCP server's connection settings, an A2A peer's headers), never claiming the neighbouring refusals that carry their own readers; that the plan-review prepare step applies a restatement before judging the stored body while this client never sends one; that the `none` execution-lane word is read at each place the server reports a lane and nowhere it does not; and that all 79 additions in the shipped wire-change file have a disposition. |
|
|
720
720
|
| `scripts/run-seat-subagent-alias-projection-test.mjs` | The additive `subagent_alias` chrome event — the cross-process carrier for the "delegating card id → subagent task id" alias. Subagent content frames carry only the delegating card id, so when the adapter runs in a seat host's main process the renderer's own ledger never learns which task a card belongs to and the subagent detail view reads empty. The adapter registers the alias in exactly two places (the async-launch receipt and the per-subagent progress tick); each now emits the event in the same step with the same arguments on every call that the registration port did not refuse (never consulting the ledger, so adapter output stays a pure function of the frame sequence), and the renderer registers the same half into its own ledger through `readSeatSubagentAlias` / `noteSeatSubagentAlias` — no second copy of the receipt/tick judgement. The guard drives the real adapter, then feeds the emitted events into a second, empty ledger (a separately instantiated ledger module) and checks that content published under the card id lands under the task id, that the `content` scope never writes the steer-target half while `full` does, that content arriving before the alias is migrated, and that a malformed event, an unknown scope word, a degenerate pair, a workflow-lane tick or a non-receipt frame registers and emits nothing; feeding the same frames twice yields byte-identical output, and de-duplication happens only on the renderer side (a second registration of the same pair is ignored). The renderer-side reader also checks the lane proof (it must name the subagent lane and the same delegating card as the payload — the seat's outbound gate only checks the proof's general shape), and the progress-tick form carries the frame's own cycle number as order evidence so that, after a renderer reload, a late tick of an older engine id cannot displace the newer card as the steer target. |
|
|
721
|
-
| `scripts/run-engine-notice-user-codes-test.mjs` | Notice sentences for
|
|
721
|
+
| `scripts/run-engine-notice-user-codes-test.mjs` | Notice sentences for sixteen engine notices (fifteen user-facing, one for operators), and one shared sentence for the durable-approval notice. `durableGateUnavailableSentence(facts, lang?)` turns the facts read from the durable-approval-unavailable notice into one sentence for all three clients. The notice records whether a live approver is attached but not how permission requests are handled when there is none (the engine may approve them without judgment or refuse them), so the sentence says that a live approver answers them only when the facts say so, and otherwise says only that the outcome follows the engine's configured posture; it never states a direction it cannot read. Sixteen notices (memory capture and harvest, cross-session messages, the classifier denial limit, late approvals, an unconsumed stop, a blocked parked input, a model whose catalogue entry declares no image input, a turn whose file state could not be saved for rewinding, a stop point ended by a stop handler, and, for operators, an inherited environment entry blocked by a late declaration) each get a reader `read<Code>(notice)` that reads own data fields strictly (a missing or malformed required field gives `undefined`, a malformed optional field is dropped and never folded to zero) and a sentence function in English and Chinese whose untrusted strings pass the display filter. Where the notice does not say which of two opposite outcomes happened, the sentence does not guess. The readers are also rows of the dispatch table. This gate checks every reader on good, absent and malformed frames and on hostile input, every sentence arm, the display filter, the direction rule of the durable-approval sentence across all combinations, and the dispatch table and root exports. Since 0.111.0 it also checks that a source word the package does not know still yields the reading and one cautious sentence in both languages (no echo of the word, no alternative exit, the way out chosen by the cause), and that an empty or non-string source word still reads as absent. |
|
|
722
722
|
| `scripts/run-local-engine-stop-ladder-test.mjs` | The local engine's stop ladder. The three durations live in one constant, and the replace-engine prompt's upper bound for an immediate replacement is computed from the same constant. The step function decides each tick: the first termination signal; in the idle form, a kill only after the engine's own drain deadline plus a grace period; in the immediate form, a second termination signal exactly once and only after evidence that the engine processed the first one (a drain started by that signal after it was sent), and a kill at a bound derived from that evidence. Whenever the process's liveness cannot be determined, no signal is sent in that tick, and the ladder gives up at the end of the segment instead. A ladder that gives up takes the engine state machine to a stop-failed state and releases the start lock. The step function is pure, returns frozen results, and treats malformed input as a reason to send nothing. |
|
|
723
723
|
| `scripts/run-local-engine-step-matrix-test.mjs` | The local engine's lifecycle state machine: one pure reducer the host feeds with observations, lock outcomes, ticks and user answers, and whose effects the host carries out. The guard checks every one of the 384 cells of the published state-by-event matrix, each guarded partition separately, against the row it should take: target state and key effects, an unchanged state with no effect where the matrix says so. It also checks the start-lock cases, that withdrawing the only cause takes the prompt down in the same step, and the invariants: signals are sent only from the two ladder rows and only to the target verified under the lock; an older client never signals or starts an engine; an unknown reading never counts as idle; an immediate replacement only follows a confirmed prompt; each cause is tried automatically at most once per target; every lock-holding state has a deadline exit; the non-interactive lane always ends with a bounded failure instead of waiting for a person; and repeated crashes stop automatic restarts within a fixed bound. Finally it covers the seat projection of each state, the outcome of answering the prompt (a changed target or changed readings make the answer stale), and the outcome of a restart request. |
|
|
724
724
|
| `scripts/run-bash-curtailed-projection-test.mjs` | Bash commands that ran and were then cut off — timed out, killed for producing no output, interrupted, or ended by a failed output-stream callback — now carry the fact on every result surface. The card body leads with a one-line family sentence when there is partial output, the result record's `stderr` is prefixed with the same sentence, and a structured `_sema_curtailed` marker names the kind and, when the engine reported one, the number of seconds. The error flag is never decided here: the card's `isError`, the print lane's `is_error` and the record's `interrupted` stay exactly what the engine said (a cut with partial output is not an error unless the engine says so; a cut with no output is), so a client that wants to know whether a command was cut reads the marker, not the error flag. The reader looks only at own data slots of a Bash-shaped result: an accessor is never run, a key found only on the prototype chain does not count, a family flag counts only as the literal `true`, a seconds value that is not a non-negative safe integer is dropped while the family is kept, and any other result shape leaves everything byte-for-byte as before |
|
|
@@ -728,10 +728,10 @@ guard still cross-checks the table by name).
|
|
|
728
728
|
| `scripts/run-interrupt-refusal-note-test.mjs` | The sentence an Esc interrupt gets when the engine refuses it. The engine's machine code on a refused interrupt maps to one sentence per code — the run is waiting on a decision so no turn is in flight, this server holds no live turn for the run, the run is no longer running — each in English and Chinese, saying what happened, that the run itself was not changed, and the way out. Anything outside those three codes (absent, empty, a near-miss spelling, a code from another family, a non-string, a hostile object) gets one neutral sentence that says the interrupt was not accepted, does not echo the input, does not say it failed and does not claim the run's state. The match is strict equality that never reads a property of its input, no sentence invites a resend or carries a machine word, the language argument falls back to English, the wording lives only in the message catalog, and the three codes are checked against the server's route bytes and real SDK error classes through the real emitter, with the escalation decision left unchanged. |
|
|
729
729
|
| `scripts/run-seat-notification-levels-test.mjs` | Host notification levels. Which level the host uses for each of its three native notifications (permission requests, questions, task complete) is answered once, by the host's own store. The seat capability table gains `notificationLevels`, reported `true` only by a host that serves both verbs and reads that same store at every native send point; a browser lane and the terminal never report it, and an absent capability means the window keeps its own notification switches. When it is reported, the window keeps no copy and no default of its own, reads and writes only through the verbs, and stops raising its own reminders for those three kinds, so each reminder is sent once. `getNotificationLevels()` returns `{ levels, supported }`: the level each kind uses right now (all three always present; a kind never set reports the host default) and the levels this host can deliver, a subsequence of the closed set in order, without repeats, always starting with `off`. `setNotificationLevel(type, level)` returns `{ ok: true, state }` or `{ ok: false, reason, state }`, a refusal being a receipt rather than an exception. The words are three frozen tuples: kinds `permission` / `question` / `idle`, levels `off` / `badge` / `banner`, and refusal reasons `invalid-request` / `unsupported-level` / `write-failed`. `planSetNotificationLevel(type, level, supported)` is the one precondition check before the host writes: a kind or level outside the closed sets is `invalid-request`, a level the host cannot deliver (or a supported list it cannot read) is `unsupported-level`, and it never throws. Both outbound gates are closed shapes that also refuse a state whose level for some kind is missing from its own supported list. The suite checks registration, both gates key by key, the planner against an independent reference over a generated pool, the capability reading, and an end-to-end host double whose send point reads the same store. |
|
|
730
730
|
| `scripts/run-session-mods-seat-test.mjs` | Which mods a conversation's last run used, and how many operator-declared mod packages the engine loaded. `sessionModsOf` reads one wiring report — the streaming report event or the submit receipt's view — into `{ rows }`, an empty list meaning that run registered nothing, or into `unreadable` when the report has no readable mods section; it never turns a missing section into an empty list and never keeps an earlier run's rows. A desktop-style host caches that reading per conversation and serves it through the seat verb `readSessionMods`, gated by the seat capability `modsView`, answering `unarmed` until the conversation has run on the current engine instance; a browser-lane renderer applies the same reader to the report it receives, so both lanes show the same thing. The guard checks the verb's registration and declared signature, the outbound check against every row shape and failure arm (including that it refuses a transport arm and never runs accessors), the reader against the row reader on a generated pool, an end-to-end path from a wire frame through the adapter to the host cache (with a real server projection when the fixture is installed), the capability reading, and `readModsDeploymentPackages`, which answers `present` with a count, `absent` or `unreadable` from the health body without folding absence into zero or false. |
|
|
731
|
-
| `scripts/run-hook-env-seat-projection-test.mjs` | The request's `settings.env` block, which the engine reads when it fills `$NAME` references in the headers of HTTP hooks it runs. The host-supplied settings entry now has a seventh row and a required source `env` (the host environment, or `'none'`). The names sent are exactly the union of `allowedEnvVars` over the `type: 'http'` hooks the request body carries on events the engine fires (already narrowed by the admitted `httpHookAllowedEnvVars` lists; entries on events the engine never fires stay in the body but do not count) that are present in the host environment, with their values; no name, no key. The suite checks that the engine's reading and the host's own reading of the same hook produce the same header bytes through the contract package's interpolation function, that nothing outside that intersection is sent and credential names are not pre-filtered (the server judges them), that values never appear in hook entries, receipts, rows, notices or any log line even with `SEMA_DEBUG` on, the lane seats, the row for a missing, absent or unreadable source, the dispatch-time recheck (only tighter), forged products, and the server's own credential refusal naming only names this package sent. |
|
|
731
|
+
| `scripts/run-hook-env-seat-projection-test.mjs` | The request's `settings.env` block, which the engine reads when it fills `$NAME` references in the headers of HTTP hooks it runs. The host-supplied settings entry now has a seventh row and a required source `env` (the host environment, or `'none'`). The names sent are exactly the union of `allowedEnvVars` over the `type: 'http'` hooks the request body carries on events the engine fires (already narrowed by the admitted `httpHookAllowedEnvVars` lists; entries on events the engine never fires stay in the body but do not count) that are present in the host environment, with their values; no name, no key. The suite checks that the engine's reading and the host's own reading of the same hook produce the same header bytes through the contract package's interpolation function, that nothing outside that intersection is sent and credential names are not pre-filtered (the server judges them), that values never appear in hook entries, receipts, rows, notices or any log line even with `SEMA_DEBUG` on, the lane seats, the row for a missing, absent or unreadable source, the dispatch-time recheck (only tighter), forged products, and the server's own credential refusal naming only names this package sent, and, when an earlier narrowing by an admission list has since been widened, that the recheck keeps the narrowed entry as a subset of the current one and adds its names to `settings.env`. |
|
|
732
732
|
| `scripts/run-approval-dual-frame-retired-test.mjs` | Since server 7.133.0 an approval ask is presented in one frame family per leg, and the supported server floor is above that, so the package no longer correlates a replayed older-family frame with a card-family frame carrying the same approval id. A replayed older-family frame that the pending list shows as crash-converged is reported to the host without pre-recording the process-level settled table (a host claim for the same id is held, not joined, and a repeat of the same frame is judged against the list again); an opening card-family frame is no longer evidence that a ledger frame is still pending (the frame path reads the list once, and its ledger keeps no per-segment set of such ids); the card frame itself still flows downstream. Positive controls keep the cross-route dedup in place: a host claim waits for a live card to settle and then joins, an engine close-out that is not a park-move joins while a park-move does not, and a simulated double-frame arrival still joins. The seat contract no longer says that one ask emits two frames; the supported server floor is pinned at 7.133.0 or higher; the host predicate `replayedApprovalClosesCardFrame` stays as a registered pending retirement. |
|
|
733
733
|
| `scripts/run-self-knowledge-output-style-merge-test.mjs` | Self-knowledge block versus the caller's own output style. When the engine does not accept the first-class `appendSystemPrompt` seat, the built-in product context rides along in `settings.outputStyle`; the live-defaults layer now merges the request's own style text behind it instead of overwriting it (context first, a blank line, then the caller's text — byte-identical to what a terminal host that pre-merges already sends, and idempotent when the result is fed through again). A merged value longer than the 16384-character server limit is never sent: the context alone goes out when it can be separated from the caller's text, otherwise the key is left off, and one turn-plane notice says the style was not applied. A style value that cannot be read as text — an accessor (never run), an inherited key, a non-string — is never merged and gets its own notice. A second entry point, `applyLiveRequestDefaultsWithNotices`, returns the request together with those notices; the original entry point is its `.request`, byte for byte. The first-class seat follows the very same rule: when the engine does accept `appendSystemPrompt`, the request's own appended prompt is kept behind the context instead of being replaced by it, with the same limit, the same idempotence, the same two notices and the key dropped from the request when nothing can be sent within the limit; a generated pool feeds the same pair of texts to both seats and requires identical outcomes, so the two cannot drift apart |
|
|
734
|
-
| `scripts/run-hooks-engine-owned-wire-projection-test.mjs` | The request body carries only engine-owned hook events.
|
|
734
|
+
| `scripts/run-hooks-engine-owned-wire-projection-test.mjs` | The request body carries only engine-owned hook events. Which hook events go to the engine depends on the peer: an event goes on the wire only when the settings contract lists it as engine-owned and the server's reported contract version (`settingsContract.version`) is at or above the version from which that event is engine-owned; when the server reports no readable version, the support floor's contract version is used (nine events: PreToolUse, PostToolUse, PostToolUseFailure, PostToolBatch, PreCompact, PostCompact, Stop, StopFailure, UserPromptSubmit; PermissionDenied joins from 26.0.0). Every other event in the settings contract is run by the client itself and never goes on the wire. Hooks written under those other events, in any of the five settings layers, stay out of the merged hooks and out of the request body while the engine events next to them are sent as before; settings that hold only client-run events send no `hooks` key at all. When this client runs those events itself, the verdict rows (dropped entries, executor verdicts, exclusions, plugin verdicts, the hooks-not-run notice) match the same settings without those events; when it does not, each such entry adds exactly one not-running row (see the suite for client-run events without a runner). Side effects that exist only for entries on the request body (stripping a hand-written `plugin` key, same-entry de-duplication, HTTP allow-list rows) are counted only for entries that really are on it. The set of client-run events is derived from the contract's event table minus the events the plan sends to the engine, never hand-copied, and every plan and request body the suite builds is swept: its event keys are always a subset of the events the plan sends to the engine. |
|
|
735
735
|
| `scripts/run-gate-remote-shape-test.mjs` | Remote-shaped runs of the suites. The suites also run on machines that carry only the tracked files: no fixtures, no neighbouring client trees, and a one-commit repository without release tags. A suite that depends on such material must say so and carry on rather than stop its later sections. When the prerequisite material of a fence is missing it prints a skipped-section marker and keeps going (it is red only in the required-fixtures mode), instead of exiting early; a red on an expiry-type cell is recorded and the rest of the suite still runs before the suite fails; and a repository whose history did not travel with the tree is told apart from a ledger that names a commit nobody has. The guard exercises the three shared helpers behind this on throwaway scripts and throwaway repositories (a deferred red still fails the run, the later cells still run, and the strict failure modes keep their exit codes; a fence whose material is present still exits when its control fails; a one-commit or depth-one repository reports no history while a two-commit one reports history), checks that each suite that used to stop on missing material now goes through them (every wiring is paired with a one-character mutation that must break the check), and runs three previously affected suites in a clean environment with no fixture variables set, requiring them to finish. |
|
|
736
736
|
| `scripts/run-session-policy-loosen-sentence-test.mjs` | The wording for a session-rule removal that the engine refuses as a loosening. Since server 7.135.0 / engine 7.55.0 a non-operator write is judged against the operator part of the stored row, not the whole row: a session's owner can take back the entries the owner added, and cannot take back what an operator wrote (a row stored before that release, or one an operator last wrote, counts as the operator's throughout). The refusal code stays the same while the engine's own sentence changed between those generations, so the package reads the code and never the sentence. The support floor sits above that generation line, so there is exactly one meaning inside the supported range and exactly one sentence; the guard ties that claim to the declared floor and goes red if the floor is ever lowered below the line. The sentence says the record is unchanged, that the engine counted at least one entry as written by an operator, and that an owner can only take back the entries the owner added; a deployment without an operator gets a different closing instead of a pointer to someone who does not exist. Its reason lives once, in the message catalog, and both closings embed it. The guard pins the English and Chinese text byte for byte, shows that three generations of engine wording with the same code produce the same sentence while a refusal without the code does not, and runs the real engine store through the removal verb: rows an operator wrote, rows an operator rewrote after the owner, and rows stored before the operator part existed are refused with the record untouched, while an owner's own entry is removed. The old claim that only an operator can remove entries appears nowhere in the sentences or the catalog. |
|
|
737
737
|
| `scripts/run-tool-permission-settled-projection-test.mjs` | A seat's tool-permission receipt can now say that the request was already decided elsewhere: the receipt carries an optional `settledElsewhere` slot (the first decision, and whether it is the opposite of what the user pressed), minted once where the engine reports a replayed answer and read back through a reader and a sentence helper that share the existing message-catalog sentences. The answer-receipt reader also recognises the parked-redemption arm (first redemption and replay), and every case checks that a non-replay answer is unchanged, malformed bodies read as absent without throwing, and accessors are never executed. The receipt reader observes every slot of a receipt exactly once: each slot's own property descriptor is read once (getters and proxy get traps never run, and a slot that exists only on the prototype chain does not count), nested rule lists and anchors are captured in the same pass, and every arm judges and copies only from that one observation. The guard checks that a getter answering a valid value first and a bad one afterwards never puts the bad value in the view, that an unreadable discriminating slot makes the whole receipt read as absent, that a proxy whose traps throw never makes the reader throw, that the view's rule list is a fresh array rather than the caller's, and that three thousand seeded JSON receipts read byte for byte as before. |
|
|
@@ -751,12 +751,12 @@ guard still cross-checks the table by name).
|
|
|
751
751
|
| `scripts/run-session-hooks-roster-test.mjs` | Two seat verbs. The Quick Entry shortcut: the seat capability `quickEntryShortcut` gates `getQuickEntryShortcut` and `setQuickEntryShortcut`; the state says which shortcut is saved, which one is registered right now (`null` when none, never an empty string) and why a saved shortcut failed to register; a refused change is a result with a closed reason (invalid request, unsupported combination, registration refused by the system, or the preference could not be written) that carries the unchanged state, never an error and never a false success. The session hooks roster: the seat capability `hooksRoster` gates `readSessionHooksRoster`, whose answer is produced only by `sessionHooksRosterOf(plan)`, a row-by-row projection of the same hooks plan that assembles the turn — event, matcher and hook index, entry type, status (engine, shell, not run, not sent) and reason, plus per-layer outcomes and HTTP allowlist drops. Every string in a roster is a closed-set word, so commands, URLs and user-written key names cannot appear. The guard checks both verbs' registration and declared signatures, both outbound checks (including the false-success and closed-leg contradictions), the shortcut pre-check, a host stand-in for each verb, a planted-secret sweep over a pool of plans, and a row-by-row comparison between roster and plan. |
|
|
752
752
|
| `scripts/run-subagent-model-ports-test.mjs` | The subagent model word ports share one reading. One vocabulary of engine words that catalog rows do not list (tier words, provider aliases, `best`, `default`) is read by both the word resolver and the catalog acceptance judge. `subagentModelAdmissionOf({ word, liveCatalog?, local? })` judges whether this engine admits an agent entry carrying the word the reading sends: it asks the same acceptance judge, except that the mention list does not gate agents and an engine word the catalog does not serve is reported as `not_served`; with no catalog from the engine the answer is unknown, never refused. `subagentModelAdmissionLine` renders the sentence for a refusal and `subagentModelCallRefusalReason` the sentence for a call-time refusal, both from closed reasons through the message catalog. The gate compares the admission verdict with the installed engine's tier expansion and the server's exact-key agent check on generated catalogs, and checks the vocabulary, `default`, both host assertion words, absence, hostile inputs and the sentences in both languages. |
|
|
753
753
|
| `scripts/run-thrown-slot-reader-test.mjs` | Two readers for the fields of a thrown value, both of which never throw. `thrownSlotOf(value, name, kind?)` reads one field of whatever was thrown — an error object, a proxy, a revoked proxy, `null` or a primitive. By default it reads along the prototype chain (where a built-in error's `name` lives); with `kind: true` it reads only the value's own keys; with `kind: 'data'` it reads only its own data fields, so an accessor is never run and a field planted on `Object.prototype` is never seen. A field that is absent and a field that cannot be read (a getter or proxy trap that throws, a revoked proxy, `null` or `undefined` as the thrown value) both answer `undefined`, so the result can be defaulted or printed directly; narrow with `typeof` before using it as a string or number. `errnoCode(value)` is the fixed call for the error code of a system error: the own, non-empty string `code`, and `undefined` for everything else. |
|
|
754
|
-
| `scripts/run-engine-unavailable-line-test.mjs` | What a client says when the engine cannot serve the session, and what the way out is. One reader takes the engine's start-up log lines: structured lines are read first and plain echoed text only as a fallback, and a refusal code is recognised only where the engine states it as a code. A data-folder refusal additionally yields its variant, the recorded engine version, the engine's own version and the marker path when the engine names them, and nothing when it does not. One line names the case, carries a machine-readable prefix and the exit details (none when unknown), and gives the way out for that case; the lane only changes how retrying is phrased. Ways out for a refused data folder follow how the client is installed: an update command is offered only to global and managed installs, a client started by the host never suggests overriding the data folder and instead names the configuration-folder variable the start-up table pins, and with no install shape the engine's own words are shown unchanged. Headless runs exit with 69. Whether the engine answered its health probe is told with the probe verdict itself (unanswered, answered but not ok, answered ok), read by the same function that words the verdict, so the line and the verdict sentence never disagree; an engine that was asked to stop and is still running is its own reason. |
|
|
755
|
-
| `scripts/run-self-heal-conversation-left-test.mjs` | Conversation-left handling for the busy-session self-heal. A host can answer one optional question, `stillHere()` (is the conversation that issued this action still the one on screen?), and the package reads it one way only, as three states: a strict `true` means still here, a strict `false` means the conversation was left, and a reader that is supplied but answers anything else or throws means it cannot be confirmed; no reader at all keeps the previous behaviour. When a resend is about to be authorised but the conversation was left or cannot be confirmed, the outcome becomes `resend-withheld-left` or `resend-withheld-unconfirmed` (not delivered either way, so the host never sends the message into a different conversation) with its own sentences in both languages; the deferred-batch wait `waitForInjectedResubmit` asks the same question every round and settles `left` or `unconfirmed`; and ending a run after the conversation was left maps to six result classes with their own sentences. |
|
|
754
|
+
| `scripts/run-engine-unavailable-line-test.mjs` | What a client says when the engine cannot serve the session, and what the way out is. One reader takes the engine's start-up log lines: structured lines are read first and plain echoed text only as a fallback, and a refusal code is recognised only where the engine states it as a code. A data-folder refusal additionally yields its variant, the recorded engine version, the engine's own version and the marker path when the engine names them, and nothing when it does not. One line names the case, carries a machine-readable prefix and the exit details (none when unknown), and gives the way out for that case; the lane only changes how retrying is phrased. Ways out for a refused data folder follow how the client is installed: an update command is offered only to global and managed installs, a client started by the host never suggests overriding the data folder and instead names the configuration-folder variable the start-up table pins, and with no install shape the engine's own words are shown unchanged. Headless runs exit with 69. Whether the engine answered its health probe is told with the probe verdict itself (unanswered, answered but not ok, answered ok), read by the same function that words the verdict, so the line and the verdict sentence never disagree; an engine that was asked to stop and is still running is its own reason. Since 0.113.0 it also covers the four-way-out form of the data-root refusal (servers 7.148.0 and later): each table form is recognised by its exact opening phrase, the skipped-migration finding and the minimum starting version it names are read, the variant order holds for every order of findings, an unknown table form is flagged rather than guessed, and a refusal carrying the library schema-mismatch class name is no longer read as a data-root problem. |
|
|
755
|
+
| `scripts/run-self-heal-conversation-left-test.mjs` | Conversation-left handling for the busy-session self-heal. A host can answer one optional question, `stillHere()` (is the conversation that issued this action still the one on screen?), and the package reads it one way only, as three states: a strict `true` means still here, a strict `false` means the conversation was left, and a reader that is supplied but answers anything else or throws means it cannot be confirmed; no reader at all keeps the previous behaviour. When a resend is about to be authorised but the conversation was left or cannot be confirmed, the outcome becomes `resend-withheld-left` or `resend-withheld-unconfirmed` (not delivered either way, so the host never sends the message into a different conversation) with its own sentences in both languages; the deferred-batch wait `waitForInjectedResubmit` asks the same question every round and settles `left` or `unconfirmed`; and ending a run after the conversation was left maps to six result classes with their own sentences. It also pins two more read points at the publish step with two refusal words and their final sentences, and the self-heal forwarding of the presence reader. |
|
|
756
756
|
| `scripts/run-refusal-noted-projection-test.mjs` | Refusal notes on tool results. When a person refuses a tool call on the approval card and attaches a note, the engine records the note and the turn continues with that direction; a refusal without a note is a bare refusal. Tool result rows (interactive transcript and print frames) carry `_sema_refusal_noted: true` when the same row's denial kind is `user-rejected` and its gate record is a person's refusal whose note is present (empty or non-string notes count as absent), read with `toolResultRowFlagsOf(row).refusalNoted`; the key name is exported as `SEMA_REFUSAL_NOTED_KEY` and listed in `TRANSCRIPT_ROW_SIDECAR_KEYS`. Subagent content events, items and view slots carry the same facts as `denialKind` (the same closed vocabulary as the main transcript) and `refusalNoted`, on the live stream, the background tail and replay alike, and the seat validator accepts only those values. The gate checks every refusal and note shape on both lanes, row self-consistency, key order, replay equivalence, the reader's isolation, the three subagent views, the seat contract, and by syntax tree that the key name and the rule each live in one place. |
|
|
757
757
|
| `scripts/run-decide-ack-outcomes-test.mjs` | Tool-permission receipts on the seat answer one question, whether the user's decision is in effect right now, with exactly three answers: yes (`ok: true`), no (`ok: false`), or not known (`ok: false` with an `unconfirmed` slot naming the cause). The cause words are the package's existing words for a decision that got no verdict (`transport`, `server_error`, `local_error`, `aborted`, `gone_after_retry`) plus `registry_unreadable` for a host that could not read the pending-request list when it settled the receipt. The `unconfirmed` and `settledElsewhere` slots never appear together, `unconfirmed` always comes with `ok: false`, and when `settledElsewhere` is present `ok` must say the opposite of `contrary`; the receipt's key set is closed. A receipt with neither slot is still valid and reads as before. One reader turns a receipt into applied, not applied or unconfirmed and judges with the same check the outbound gate runs, a sentence helper gives the unconfirmed line in both interface languages (the after-retry cause reuses the existing sentence), and a mint helper turns a thrown decision error into the slot using the same classification the decision sender already applies, returning nothing when the engine gave a verdict. The guard covers every arm accepted and every malformed or contradictory form refused with the slot named, accessors never run, and the three counterexamples: a server error folded into a refusal, an unreadable pending-request list folded into success, and both slots in one receipt. |
|
|
758
758
|
| `scripts/run-engine-swap-crossing-test.mjs` | One judgement before any client stops or replaces the local engine (`engineSwapCrossingVerdict`). Whether the running engine is newer than this client is taken only from the local engine judge's verdict, never compared a second time: a newer running engine is never stopped, and no confirmation changes that. When this client is newer, work still waiting on the running engine that may not survive the version change holds the replacement while it can still clear (waiting approval cards counted per session, or a reading that failed this time); a class the engine cannot report at all is listed but does not hold. When the direction cannot be read, the engine is not replaced automatically. A confirmation releases a hold only if it names the fingerprint of the exact verdict the person saw, so one more waiting card or a different engine instance asks again, and the replacement then says what may have been lost. The fallback reading (`parkedToolApprovalsBySession`) counts, from the approvals feed as emitted, only rows on the approval decision path whose engine verdict is absent or ok; anything it cannot read fully is a failed reading, never zero. Sentences come in English and Chinese (`engineSwapCrossingText`). Accessors are never executed and hostile inputs never throw. |
|
|
759
|
-
| `scripts/run-attachment-route-test.mjs` | The attachment-route gate. Whether a file's contents may be read on this machine and handed to the model depends on whether the session's tools work on this machine's files. The package exposes one judge, `attachmentRouteOf`, which takes the kind of attachment (an @-mention, a changed-file note, a paste, an editor selection) and the host's reading of where the tools run (this machine, elsewhere, or unconfirmed) and answers: attach, attach with a note saying the content is this machine's copy, or withhold with one of two reasons. A location that cannot be read is treated as unconfirmed, never as this machine and never as a confirmed elsewhere, and accessors are never executed. The guard pins every cell of the judge including hostile inputs, the two reasons in both languages for the person and the model-facing clause in the attachment envelope (both drawn from the message catalog, so the two faces cannot disagree), the note for this machine's copy, the six attachment kinds a host routes through the judge, and that no other module reads the location or spells the two reasons. |
|
|
759
|
+
| `scripts/run-attachment-route-test.mjs` | The attachment-route gate. Whether a file's contents may be read on this machine and handed to the model depends on whether the session's tools work on this machine's files. The package exposes one judge, `attachmentRouteOf`, which takes the kind of attachment (an @-mention, a changed-file note, a paste, an editor selection) and the host's reading of where the tools run (this machine, elsewhere, or unconfirmed) and answers: attach, attach with a note saying the content is this machine's copy, or withhold with one of two reasons. A location that cannot be read is treated as unconfirmed, never as this machine and never as a confirmed elsewhere, and accessors are never executed. The guard pins every cell of the judge including hostile inputs, the two reasons in both languages for the person and the model-facing clause in the attachment envelope (both drawn from the message catalog, so the two faces cannot disagree), the note for this machine's copy, the six attachment kinds a host routes through the judge, and that no other module reads the location or spells the two reasons. When the host also hands the seat reading, the scenario card and the lane, an @-mention whose tools are not on this machine is handed to the engine as a path only if both the seat and the scenario accept paths; on this machine the answer is unchanged and those inputs are not read. |
|
|
760
760
|
| `scripts/run-resupply-truncated-projection-test.mjs` | Resupplied notifications that arrive in their minimal form. When the server cannot fit a resupplied `task_notification` or `workflow_complete` frame into its size budget it sends only the minimal form and marks it with `payloadTruncated` (the full byte count and the names of the keys it left out). The client turns a well-formed mark into one additive chrome event, `resupply_truncated`, carrying the source frame, the task or run id, the byte count, the dropped key names and one sentence in the interface language: the notice was shortened, the full record is that many bytes and over the notice size limit, so that many fields were left out, and the full content can still be viewed in the run record. The sentence counts the dropped keys and never names them. An absent mark (a full frame, a live frame, an older server) says nothing; a malformed one (a byte count that is not a positive integer, a key list that is empty, too long or holds anything but non-empty names, an accessor, a trap) is dropped as a whole, never filled in with a zero or half kept. The event is sent once per delivered frame, before the transcript row for a task notification and after the enqueue event for a workflow completion (a consumer that stops reading at the event has already been handed the enqueue event, so a completion is never marked as delivered without it), and it passes the seat gate. The gate drives real frames through the adapter and the projection door for both frames and both languages. |
|
|
761
761
|
| `scripts/run-screen-decision-cards-test.mjs` | Facts of a decision card on screen and the engine-generation card plan. Each card has three facts with one reader per carrier: its kind (`SCREEN_CARD_ORIGINS`, read from an approval card with `approvalCardOriginOf` and from a question id with `questionCardOriginOf`), the gate it asks about (`approvalCardGateOf`, `questionFrameGateOf`: a tool gate is the run plus the gated call, a plan gate is the run plus the checkpoint) and the engine generation it came from (`sourceGeneration` on the approval card request, `questionFrameSourceGenerationOf` on a question frame; read before the pending list is read, or when the stream was opened, and absent when it cannot be read). `engineGenerationCardPlan` judges both beats with the same card type: when a generation ends only a live card whose window is certainly open is retracted (the existing convergence judge decides), and once the next generation is ready a card is withdrawn only when it is a withdrawable kind, has a gate, is from an earlier generation, matches a row owned by this session and that row's verdict is not ok; every other card is kept with one of `SCREEN_CARD_KEEP_REASONS`. The suite pins each reader against parked and live cards of both gate kinds and their absence, the generation timing, the golden cases exported with the package, a seeded comparison against an independent oracle, the recheck-due predicate, the engine-decision truth table and the declaration files. |
|
|
762
762
|
| `scripts/run-decision-refusal-carrier-test.mjs` | Cross-process carrier for refused decisions. `decisionRefusalCarrierOf` turns the error a refused approval decision threw into a plain object that is safe to send through structured clone and carries exactly the fields the package's own failure readers read on that error (or hand back unchanged): no field whose name looks like a credential at any depth, and nothing nobody reads. Before returning it, the package checks that every one of its failure readers gives the same answer for the carrier as for the original error; when it cannot prove that (an unreadable field, a reader that depends on the error class, or a credential-named field a reader needs) it returns nothing, and the host keeps its generic failure path. The guard compares every root failure reader, with every closed-set argument, on the original error, the carrier and its structured clone across the decision-refusal families raised by real SDK errors, and checks that credential-named keys and their values never appear, that unread body fields are left out, that every carrier path exists on the original, that hostile shapes give nothing without running getters, that the carrier is deeply frozen, that a seat renderer reading the cloned carrier reaches the same verdict and sentence as the host, and that every root failure reader is on the carrier's reader list. |
|
|
@@ -781,12 +781,25 @@ guard still cross-checks the table by name).
|
|
|
781
781
|
| `scripts/run-local-config-kernel-test.mjs` | The shared local configuration kernel. One document table names every local configuration file once: its logical name, a path template with four slots (resolved by the host's configuration-home resolver, never by this package), how much of its shape the settings contract owns, the name of its lock rank, and whether the product also writes it outside configuration edits; project-level files live only under `.sema`, and the global state file lives inside the configuration home, located by one function that never probes an older location. A file reading has four states: loaded with a revision, absent, unreadable with a cause from the settings contract's closed set, or outside the fence. Every edit is an intent made of single-file steps that each lock their files, re-read them inside the lock, plan exactly once, hand the host one whole next document with the revision it was planned on, and are never replayed after a step was not written; a multi-file intent is a sequence of such commits (a key slot before the catalog entry, deletions in reverse) whose receipt lists any harmless residue. The receipt has four arms (applied with when it takes effect and any disclosure of which layer decides or overrides the value, refused with a closed reason that always means nothing was saved, unavailable with a closed cause, unknown only when a write was handed over and its outcome cannot be told), a preview is a separate shape that carries the same disclosures, and every word has one sentence in both interface languages. One lane check runs first: a Cloud engine, an engine not confirmed to serve one user, or an unsettled configuration home blocks reads and writes alike; the two browser routes are judged separately, a direct route to the local companion service is read-only for now, and writes are offered only when every named precondition holds. The write intent table derives the settings write keys, takes layer admission and value shape from the settings contract's unit table, and the settings write plan is a pure document-in, document-out function. |
|
|
782
782
|
| `scripts/run-local-config-mcp-test.mjs` | MCP server configuration as one shared runtime. Reading: one source precedence table and one override-by-name merge across the user, project and local layers plus the additional sources a host supplies, one project approval rule (merged values when the folder is trusted, per-layer presence otherwise, session approvals in both, nothing approved without a settings source), one tool-list agreement rule (a name whose places disagree on its tool list is held back), and one rule that a configuration file able to add or limit servers being unreadable holds back every server instead of silently dropping that file; names compare byte for byte, and the host's own built-in server names are refused alike on read and write. Deployment rows are listed beside request rows. Writing: one edit entry point (add, update, remove, enable or disable, project approval, import) that hands exactly one file to the shared local configuration kernel and answers with the kernel's four receipt arms; the hot global state file only changes at the edited key path. A dry run writes nothing, and a connection test dials once, only after the dry run succeeds, with the same entry the read side would load. Bridge views carry no secret values. |
|
|
783
783
|
| `scripts/run-local-config-skills-test.mjs` | Skills in the shared local configuration kernel. One root table lists where skills, commands and agents come from: managed, the user's configuration home, each project level from the working directory up to the repository root (nearest first) under `.sema` only, additional directories, and the host's plugin roster; a `.claude` folder is never read. One view lists every skill with its layer, whether it reaches the model and why not (turned off, model invocation turned off by its own file, folder not trusted, trust not confirmed, or shadowed by a same-named skill from a higher layer), the description only when its header states one, its effective state with the layer that decides it, and whether it can be changed here. The effective state follows one chain: managed settings first, then the merged settings (full names before bare names, local over project over user), aliases tightened only by managed settings, plugin skills untouched by ordinary layers, MCP skills by the first enabled layer that names them, built-in skills clamped when built-ins are off. A state change writes the local layer when a workspace is open and the user layer otherwise, writes only the difference from the layers below, migrates bare-name keys, keeps sibling MCP skills' states when a shared label key is removed, refuses with one of four sentences when the state is locked, and reports which layer decides the result after the write; the projection to the engine reads the same effective state, so a skill turned off in one client is not sent by another. |
|
|
784
|
-
| `scripts/run-local-config-models-test.mjs` | Model pool and tiers on the shared local configuration kernel. Every pool edit (add one or many models, update one, remove one, set the default, import environment models, import from another tool's profile, bind a tier, switch, add or remove a tier group, or migrate the old tier settings once) goes through one pure planner that takes the model catalog and the user settings and returns both next documents, then runs as single-file kernel steps: a model's key slot is written before its catalog entry, a removal deletes the entry before its now-unused key slot, and each step re-checks inside its lock what the earlier step decided. A batch is refused as a whole when any entry is malformed, with one reason per entry; an entry whose probe did not pass is still saved and marked unverified; an entry for a local endpoint may carry no key at all, and is accepted only when the engine's own pairing judge says a keyless request is allowed there. The default model has one place, the catalog's default role: only setting the default (or picking one while adding) writes it, no other pool edit recomputes it, and the default reader reports when the settings file's top-level model shadows it for new sessions. Removing a model that is still the default, bound by a role, or listed in an old roster is refused; tier bindings and the @-mention allowlist follow the removal and the consequences are reported. The view lists every entry without any key value, counts entries it could not read instead of claiming a complete catalog, and takes provider sign-up links from the package's own preset table. The seat adds one read capability (the pool view and a separate candidate-model discovery call) and one write capability (one edit verb with a dry-run option); keys are accepted only on the request side. The probe reports first-token latency as a number, and the live catalog reader carries the count of rows it dropped so the acceptance judge answers incomplete instead of refusing. |
|
|
784
|
+
| `scripts/run-local-config-models-test.mjs` | Model pool and tiers on the shared local configuration kernel. Every pool edit (add one or many models, update one, remove one, set the default, import environment models, import from another tool's profile, bind a tier, switch, add or remove a tier group, or migrate the old tier settings once) goes through one pure planner that takes the model catalog and the user settings and returns both next documents, then runs as single-file kernel steps: a model's key slot is written before its catalog entry, a removal deletes the entry before its now-unused key slot, and each step re-checks inside its lock what the earlier step decided. A batch is refused as a whole when any entry is malformed, with one reason per entry; an entry whose probe did not pass is still saved and marked unverified; an entry for a local endpoint may carry no key at all, and is accepted only when the engine's own pairing judge says a keyless request is allowed there. The default model has one place, the catalog's default role: only setting the default (or picking one while adding) writes it, no other pool edit recomputes it, and the default reader reports when the settings file's top-level model shadows it for new sessions. Removing a model that is still the default, bound by a role, or listed in an old roster is refused; tier bindings and the @-mention allowlist follow the removal and the consequences are reported. The view lists every entry without any key value, counts entries it could not read instead of claiming a complete catalog, and takes provider sign-up links from the package's own preset table. The seat adds one read capability (the pool view and a separate candidate-model discovery call) and one write capability (one edit verb with a dry-run option); keys are accepted only on the request side. The probe reports first-token latency as a number, and the live catalog reader carries the count of rows it dropped so the acceptance judge answers incomplete instead of refusing. The probe and discovery default budgets come from one frozen table (the number literals appear nowhere else in the judgement module; the override slots still take effect). The pool view now says whether the engine environment was read (read, absent or unreadable): an unreadable environment is treated as empty and no longer makes the view call throw, the seat's outbound check accepts the word as an optional key and rejects any other word, and a normalizing reader answers not_reported when an older host did not write it. A view carrying the new key is rejected whole by a check from before this key existed, which is why renderers must be upgraded before or together with their host. |
|
|
785
785
|
| `scripts/run-model-config-gap-notice-test.mjs` | Model-configuration gap sentences. `modelConfigGapNotice({ parts, keyChannel?, catalogEntries? }, lang?)` turns the list of missing model-configuration items (model name, gateway base URL, API key — the key tagged with its channel, and the number of enabled catalog entries the gap affects) into one `{ head, detail }` pair in English or Chinese, so every place that has to say why a model cannot be used reads the same sentence and differs only in whether it takes the short head or the longer detail. The gate checks thirty cells (five readers, single, double and all-three gaps, two languages) against an independent reference model, twelve pinned sentences, the optional fields (absent, out-of-set and malformed values read as not reported and only act when the item they belong to is missing), the empty and malformed answers (an empty list says nothing is missing and invents no sentence; an unreadable list is reported as malformed, never as nothing missing), hostile inputs (throwing getters, revoked proxies, prototype pollution: own data properties only) and single sourcing: the five readers' fields are the one function's fields, replacing one catalog key in a separate process changes exactly the readers that take that field, the source holds no English sentence literal, and each head sentence occurs once in the whole source tree. A consumer compile checks the shipped declarations, and planted second sources prove the checkers can fail. |
|
|
786
786
|
| `scripts/run-server-7149-pick-projection-test.mjs` | What the client reads from the newest server release and client library generation. When the engine refuses a parked approval because this engine cannot continue it, the refusal now names the way out: cancel the run, or continue it on a newer engine. The client reads that word once, keeps it on the refusal reading and classification, stops treating such a refusal as worth resending as is, does not stop the run when a newer engine can continue it, and words the way out with the same sentence it already uses for the same verdict on a pending row; the sentence for a refused parked step uses that same wording when the caller hands it the word, and is unchanged otherwise. A model row's context window now says whether it was declared or assumed; the client keeps the number and adds the source, and an older server's row reads as unknown, never as declared. A run result that disclosed that its read-deny rules were not byte-enforced reads as such, and its absence is never read as the rules being kept. The list of error codes for which resending cannot change the answer is the library's own list, snapshotted at build time, so a newly listed code stops being offered as retryable. |
|
|
787
787
|
| `scripts/run-model-pool-migrate-sweep-test.mjs` | Model pool leftovers. Migrating the old tier settings now merges them into the model catalog instead of dropping groups that exist only in the old settings: a group with the same name keeps the catalog's version and is disclosed in the write receipt only when its content really differs. A separate edit sweeps orphaned key slots (a stored key that no model references any more), checking for references again inside the same lock, and the model-pool view always carries a count of such slots. One shared rule decides which model becomes the default when a batch is added without an explicit choice: keep an explicitly configured default; otherwise take the first model in the batch that was verified; if none was verified, change nothing. Pure planner functions and a stand-in-rule check over the real built package. |
|
|
788
788
|
| `scripts/run-engine-attach-ownership-test.mjs` | Local engine credentials and connection reuse. A credential for a local engine is sent only over a connection opened after the moment the engine on that port was proven to be the expected one: either a transport that opens a new connection for every request, or a pool opened after that proof and bound to the same engine generation. `engineCredentialReleaseVerdictOf` takes the host's ownership reading (match, mismatch or unreadable) and the declared transport and answers present, or withhold with `identity: 'unproven'`, a reason from `ENGINE_CREDENTIAL_WITHHOLD_REASONS` and whether a bound pool should be closed. An undeclared transport means the shared default pool and never carries the credential, and nothing is inferred from a missing reading. `makeLocalEngineWireClient` applies the verdict before building the client, so requests still go out without the credential when it is withheld. The gate checks the judge matrix, three attach-time call shapes (diagnostics capability probe, side query, background task probe) across a generation change and before and after the proof, that every shape follows the same judge (a stand-in judge shifts all three), that the default pool never sees the credential, hostile inputs, and that the SDK health probe still has no connection-reuse option. |
|
|
789
789
|
| `scripts/run-console-ca-pin-test.mjs` | Console certificate pinning, decided in one place. `readCenterAddress` reads an invitation link or a typed center address once and answers with the TLS trust origin (`origin`) and the credential partition key (`centerKey`), or with one of sixteen closed refusal reasons: invitation links must be `https://` with no path and may carry only a `#ca=` list of canonical fingerprints, a typed address may use plain `http://` only for this machine, and user info, percent-encoded hosts, trailing dots and non-standard IP spellings are refused. `trustOriginOf` maps the final URL handed to a transport (including `ws://` and `wss://`) to its trust origin. Fingerprints are always the SHA-256 of the whole certificate DER, in the settings contract's `sha256:` spelling; flag input also accepts uppercase and colon-separated forms and folds them. `readCaBootstrapBody` takes the raw bytes of the console's certificate answer and a host-supplied certificate parser, requires every served certificate to be canonical single-block PEM with standard DER lengths, binds the parser's reading to the exact bytes, refuses duplicates inside the served set, and requires every named fingerprint to be served and self-signed. `caBootstrapOpen` / `caBootstrapStep` plan the fetch: one attempt plus three retries on a 120-second monotonic budget, `Retry-After` is a lower bound and its absence means stop, never a clamp. `caPinRefusalSentence` turns any refusal reason into one English or Chinese sentence that states the rule and the limit and never echoes input. All of it is pure: no file system, network, clock, randomness or timers, and nothing throws. The gate checks every reason and success form, the single-source bindings to the settings contract's URL judge and digest spelling, that the shared SHA-256 leaf is the only implementation in the package, that the engine address normaliser and the relay path check behave exactly as before after moving onto the shared readers, and that every refusal sentence exists in both languages without echoing its input. |
|
|
790
|
+
| `scripts/run-local-engine-bind-gate-test.mjs` | How the local engine's listen address is gated when a client starts the engine itself. By default the engine listens on `127.0.0.1` only; asking for any other address (a different loopback address, `localhost`, `0.0.0.0`, `::`, a LAN address or a host name; there is no classification, only the one default spelling passes) requires the host to turn on an explicit switch, which it reads as a boolean and hands to the package. One pure gate takes the requested string and that boolean and answers either pass (with the trimmed string to hand to the engine, derived by the package, and whether to show a risk notice) or refuse with one closed reason; only an own data property equal to `true` counts as the switch being on, so absence, strings such as `'true'`, accessors and prototype values all mean off. The start-up environment table no longer lets an inherited `BIND_HOST` through: the child gets the gate's string or `127.0.0.1`, a refusal also keeps any non-default address out of the environment returned by seeding (including when seeding refuses for another reason), and the switch key is never passed to the child. The refusal reason and the disclosure word are closed sets, the machine-readable code is derived from the word in one place, and each of the two sentences exists in English and Chinese: the refusal sentence echoes the switch name the host supplies exactly once and offers two ways out, the disclosure sentence states that other machines or other programs on this computer may be able to connect and run commands as the user. |
|
|
791
|
+
| `scripts/run-attachment-path-plan-test.mjs` | The path-attachment plan gate. When the engine can read a workspace file itself, the client sends only the path and reads nothing locally. The package's planner places the attachments that go to the engine, in mention order, into the request's attachment seat, up to the seat's `maxEntries`. An attachment whose path or line range is not valid takes no place and is handed back unchanged (the range is never swapped, dropped or clamped), as are those past the limit; each comes back with a reason, and `attachmentPathOverflowText` gives the sentence for it in the interface language. The package never reads the disk or resolves the path. The placed entries are screened in-process by the engine's own attachment check. The public batch entry is `attachmentRoutePlanOf`: for each attachment it asks the route judge, places the ones going to the engine as above, and answers one step per attachment in the order given (send the path, read it on this machine, or withhold it), with the reason a planned path fell back when it did; the whole batch is refused only when it is malformed or two attachments share an id. |
|
|
792
|
+
| `scripts/run-attachment-outcome-test.mjs` | The attachment-outcome gate. After a message with attachments is sent, the engine reports one row per attachment, numbered from one in the order the client declared them. `attachmentOutcomesOf` maps those rows back to the client's own attachment ids by that number alone and never by name: a row numbered beyond the attachments that were sent, or whose kind contradicts how that attachment was supplied (a read-from-disk kind on an attachment the client supplied as content, or the reverse), makes the whole report untrusted and every attachment comes back unknown; a missing row leaves just that attachment unknown; two rows for one attachment are used only when they agree. The engine's three outcome words pass through unchanged and any other word, including the one older engines use for a file that has not changed since it was read, comes back as unknown with the original word kept. When the caller supplies the identity of the submission and of the report frame and the two differ, every attachment comes back unknown. When the engine refused the whole request, every attachment comes back refused, the one the engine named (counting from zero) is marked, and how that attachment was supplied is derived so the refusal sentence can say that a path attachment needs a workspace. `attachmentOutcomeText` turns each row into one English or Chinese sentence and shows an extension's name through the same escaping as other names. The gate pins every mapping, every misalignment form, hostile inputs (accessors are never run, revoked proxies and sparse arrays never throw), and a few thousand seeded random inputs against an independent reference model, end to end from an encoded message through the projected receipt, and that the reader contains no name-based pairing. |
|
|
793
|
+
| `scripts/run-session-occupant-projection-test.mjs` | Occupancy of a held session, read once and worded once. When a submission is refused because another run holds the session, the reject signal now carries one occupancy word (`occupancy`: `running`, `parked`, `unreadable` or `released`), read from the reject body by a single reader — in the same pass that classifies the body, from one snapshot of the body's own data slots, with an accessor never executed. Both places that word the refusal read one five-row sentence table by that word: the stream's reject sentence and the unattended-run sentence, so a `print` or `utility` lane's reject sentence is byte-identical to the zero-argument unattended sentence. Before, the stream sentence told every holder state that nothing releases it on its own while the unattended path split the same signal three ways, and a claim the engine reported as released (`activeTaskId: null`) was called never released. Now a running holder gets a conditional sentence (wait for it, or end it if it never finishes), a parked holder is reported as one that will not release on its own (and, on the unattended path with a decision route on the wire, the sentence names that route without promising that deciding it frees the session), a holder whose state cannot be read is reported as exactly that, and a released claim says so and asks the user to send again. A signal built by a host without the word reads as unreadable rather than being re-derived from other fields. Also covered: the reader of the plan-review `approval_stale` refusal body that returns the engine's echoed current-gate id. The gate pins the occupancy reader over its whole input grid (5 id forms × 7 status forms × 3 stale forms) on the public signal, on the stream sentence and on the reader itself, the sentence table byte for byte in both languages, both faces and three slot forms, and the current-gate reader over seven body forms. |
|
|
794
|
+
| `scripts/run-fleet-idle-projection-test.mjs` | Idle rows for finished background subagents, decided once in the fleet ledger. A terminal row stays listed past the retention window only while it is idle: the end facts of the task's current cycle, merged from the three lanes that report them (the task row, the background notification and the turn notification), say completed and resumable; nothing later withdrew that; no session switch or host signal voided it; and the first end of that cycle is less than `RESUMABLE_IDLE_FALLBACK_MS` ago. Such a row carries `idleUntil: null` (the deadline is unknown, and the fallback window is not a deadline) and `fleetTaskShownStatus` answers `idle`; any other row has no such key. Viewing a row keeps it listed but never extends idleness. The suite refutes the rule on three axes: timing (a viewed row past the window, a session switch through each of the three paths and a return to the original session, the host signal followed by the same cycle's end arriving again through every entry, as a redelivered notification or a replayed event log would bring it, a same-cycle reopen that must keep the first end as the anchor), concurrency (all 27 value sets of the three lanes in all six arrival orders give one answer per set, a newer-cycle notification retiring an older live row is idle by the cycle that ended, turn notifications for tasks this ledger does not hold leave the projection byte-identical) and failure class (absent, non-boolean or false resumability, failed or killed ends, cycle-less or malformed readings never make a row idle; unreadable turn notifications change nothing; `idleUntil` is only ever `null` or absent). A completed notification still cannot overwrite a known non-success end of the same cycle, now also after a same-cycle reopen, a turn notification or a session switch. Every cell runs with the wall clock and the monotonic clock replaced by throwing stubs. |
|
|
795
|
+
| `scripts/run-plan-review-accept-projection-test.mjs` | Plan-review decisions, from acceptance to the sentence a person reads. When the last request of a decision is accepted, the package reads the acceptance body's own data slots once into a frozen snapshot; the acceptance callback (called exactly once, before the post-decision re-read), the debug line and the outcome all read that snapshot, so a host that rewrites the live body inside the callback changes nothing. The callback is isolated: a throw, a rejected promise or a promise that never settles does not reach the decision path. The outcome now says how the host should deliver it (`delivery`: follow the run when the engine says the gate was settled by a rejection or a request to present the plan again, otherwise submit), whether this answer left without the gate id (`gateIdOmitted`, on exactly the outcomes whose text carries that sentence) and, when the engine named a way forward on a refusal, that word (`wayOut`). The `approval_stale` judge reads the refusal body's current gate through the same three-state reader as the public folded reader, and a re-read row that is still at the plan review but has no result is reported as unreadable rather than as a moved gate. `planReviewOutcomeNotice(outcome, cardOnScreen?, lang?)` turns an outcome into one sentence by a single table; the gate checks it against an independent oracle over 4704 combinations in both languages. |
|
|
796
|
+
| `scripts/run-core-7670-pick-projection-test.mjs` | Readings that follow the engine's 7.67 changes. The attachment outcome set has three words and the unchanged-file card type is gone; the new ask origin `integrity_guard` has its own sentence on the approval card; session-rule writes always declare the rule grammar (a first write on a session with no stored rules now lands, and rules stored before the grammar stamp are handed back declaring the grammar they were written in); a failed Agent card's code is read from `code` (cards from earlier engines are still read from `errorCode`); the model-retry status carries the provider's own message and, on the classifier's frames, the tool call being judged; a parked subagent's state line reads `park`; the notice catalog matches the installed engine (135 codes) with the renamed revocation-probe code, two codes moved to the user audience, and the retired instructions-change notice still recognised from servers that send it; the new plaintext-credential pairing status reads as broken. The config-refusal check still judges by the `config.` prefix; the gate pins where that differs from the engine's own list of config refusal codes (eight resume-point codes the engine lists that the prefix does not match, seven codes the prefix matches that the engine does not list). |
|
|
797
|
+
| `scripts/run-subagent-edit-outcomes-test.mjs` | The sub-agent edit-outcome gate. A finished sub-agent's card carries the engine's tool statistics, and newer engines add how its file-edit calls ended next to the count of attempts. `subagentEditOutcomesOf` reads exactly those two fields and returns four numbers (attempted, succeeded, not run, failed); it returns nothing when the engine did not report outcomes (older engines, or no edit was attempted), when any count is malformed, or when the outcomes add up to more than the attempts, so an unknown is never shown as zero. `subagentEditOutcomesSentence` turns the four numbers into one English or Chinese sentence and stays neutral about who declined an edit. The gate pins the reading, every malformed form, both languages word for word, and that the reader counts nothing on its own. |
|
|
798
|
+
| `scripts/run-result-intake-projection-test.mjs` | The result-frame intake position. Newer engines report on every run result whether the call's own input was taken into the conversation: either taken in, with the session and entry it was stored under, or not taken in at all. The result frame carries that fact as `_sema_intake`, minted on success and error frames alike by one shape check, and `resultIntakeOf` reads it back through the same check. A missing or malformed report leaves the position absent, which means unknown rather than not taken in; why an input was not taken in is told by the frame's own outcome, not by this position. The position is pass-through only: the gate checks that adding or removing it changes nothing else on the frame, and checks the reading against results built by the engine itself. |
|
|
799
|
+
| `scripts/run-extension-turn-abort-projection-test.mjs` | The extension turn-cut gate. Newer engines let an extension end the running model turn, and say so with a turn-interrupted notice whose cause is `extension_abort` and which names the extension and the turn. `readTurnInterrupted` reads that as its own lane with those two fields, `turnInterruptedNotice` gives one English or Chinese sentence for it (naming the extension only when its name is readable, shown through the same escaping and length cap as other names), and `steerCutVerdict` answers `extension_abort` for it. Separately, when such a cut ended the run's last turn, the engine's result names the extension, and the result frame carries that name as `_sema_turn_aborted_by`, read back with `resultTurnAbortedByOf`. The two facts are kept apart: neither is inferred from the other. The gate pins both readings, the sentences word for word, the other three lanes unchanged, an end-to-end pass from the wire frame to the sentence, and results built by the engine itself. |
|
|
800
|
+
| `scripts/run-server-7152-pick-projection-test.mjs` | Which door an approval decision goes through. Servers from 7.152.0 take a person's decision on an approval card only at the ask decision door (the old live-card respond door and its read door are gone); older servers keep the respond door. The client picks the door once per send from the server's self-reported version, through one judge, and never switches doors after a failure: allow-for-this-session rides the decision body on the new door, a 425 parking answer is read as parking (never as accepted), a 404 route-not-found gets its own outcome (`route_missing`, in both the send and live-card outcome families) with its own sentence in English and Chinese instead of being read as the question being gone, a parked-question redemption is applied without a receipt view, "don't ask again" rule suggestions on a card can be chosen on the new door too (single suggestions by their exact text, batch suggestions by their position in the full list and only when the batch capability is confirmed, never with a denial), and old-family approval frames replayed by a new server are ignored with a log line. When the client has no way to answer on the door the server needs, nothing is sent, the outcome names the missing door, and the sentence shown states what is known and what to try without claiming a cause it cannot know (the raw error of a missing respond function is no longer shown). A failure after an earlier attempt that may have landed is reported as uncertain, a safety stop is never re-sent, and a decision answer that names another request or another decision is never reported as this answer. Also covers the renamed source-session key on live pending rows (new name first, old name only when the new one is proven absent) and the matching view field, now `sourceSessionId` as well, and keeps the version threshold to a single literal. |
|
|
801
|
+
| `scripts/run-hooks-reader-capability-test.mjs` | Whether a key on a hook entry goes to the server is answered by the connected server alone: when the server reports its hook reader's field list for each entry type, a key is sent exactly when it is in that list for the entry's type, so a failure policy written on a command or HTTP guard reaches a server that reads it; when the server does not report the list, or the capabilities have not been read yet, keys are judged by the server version as before, and a list that is present but unreadable is treated the same way and disclosed once in the hooks-not-run notice. Which hook events are guard events is derived from the same settings-contract classifier the package already carries rather than written out, and every dropped settings hook row says whether its event is a guard event. When a user-editable settings layer cannot be read in full this turn (the settings were too large to read, or a layer is malformed or not loaded) and the plan therefore sends managed-settings hooks only, that layer is listed as withheld with the reason `layer_degraded` and every hook dropped from the readable layers is listed with the same reason, so a guard hook never disappears without a row; explicit narrowing (managed hooks only, all hooks disabled, safe mode) does not use this reason. |
|
|
802
|
+
| `scripts/run-onboard-steps-test.mjs` | First-time setup steps for the model side. The package answers four rows in a fixed order (model, tiers, optional, ready), frozen, and never throws. The model step is required only when new sessions default to the local engine and the local engine is known to have no model at all: the catalog is known to be empty and the engine environment was read and holds none. When the target cannot be read, or the environment was not reported, was absent or was unreadable, the step stays skippable with no reason (never skipped, and never reported as not applicable); a cloud default makes it skippable because the target is not local; an enabled entry, or models supplied only by the environment, make it skippable because something is already configured, which does not mean it works. The tiers step is skipped only when the catalog is known to be complete with no enabled entry; an incomplete, unreadable, contradictory or sparse view never skips it. The mode value is read once and means the same under both meanings of the engine switch. |
|
|
790
803
|
|
|
791
804
|
Each suite carries a floor that only moves up — a refactor that stops executing a group of
|
|
792
805
|
assertions is a failure, not a quieter pass. Guards anchor on the **installed artefact's content**
|