@sema-agent/client-core 0.103.0 → 0.104.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (45) hide show
  1. package/CHANGELOG.md +85 -0
  2. package/README.md +18 -14
  3. package/dist/adapter/downstream/eventToSdkMessage.js +2 -0
  4. package/dist/adapter/downstream/terminalToSdkResult.d.ts +0 -1
  5. package/dist/adapter/downstream/terminalToSdkResult.js +3 -1
  6. package/dist/agentSession/engineAgentRegistryRead.js +3 -0
  7. package/dist/coreValuePorts.d.ts +3 -1
  8. package/dist/coreValuePorts.js +5 -0
  9. package/dist/deviceRebind.d.ts +17 -4
  10. package/dist/deviceRebind.js +43 -14
  11. package/dist/engineAgentAbsence.d.ts +1 -0
  12. package/dist/executionPlacementWords.d.ts +3 -0
  13. package/dist/executionPlacementWords.js +2 -0
  14. package/dist/generated/engineFactTables.d.ts +3 -2
  15. package/dist/generated/engineFactTables.js +17 -1
  16. package/dist/generated/engineNoticeTables.js +12 -0
  17. package/dist/hitl/automaticAnswerVerdict.js +47 -11
  18. package/dist/hitl/livePendingAsk.d.ts +2 -1
  19. package/dist/hitl/livePendingAsk.js +7 -1
  20. package/dist/hitl/sessionPolicyDeliverable.js +31 -5
  21. package/dist/hitl/toolApprovalWire.d.ts +5 -4
  22. package/dist/hitl/toolApprovalWire.js +215 -47
  23. package/dist/host.d.ts +0 -3
  24. package/dist/hostTurnSettings.js +3 -1
  25. package/dist/humanTurn.js +2 -2
  26. package/dist/index.d.ts +5 -2
  27. package/dist/index.js +1 -0
  28. package/dist/jsonFaithful.js +5 -3
  29. package/dist/mcpLiveness.d.ts +1 -1
  30. package/dist/mcpWireCaps.js +1 -1
  31. package/dist/printModelStatus.d.ts +1 -0
  32. package/dist/printModelStatus.js +6 -2
  33. package/dist/retryStatus.d.ts +8 -1
  34. package/dist/retryStatus.js +13 -4
  35. package/dist/ruleCompileVerdict.d.ts +1 -0
  36. package/dist/ruleCompileVerdict.js +3 -1
  37. package/dist/seatContract.d.ts +7 -7
  38. package/dist/seatContract.js +39 -10
  39. package/dist/sessionRowTag.d.ts +24 -0
  40. package/dist/sessionRowTag.js +77 -0
  41. package/dist/steering.d.ts +1 -0
  42. package/dist/steering.js +34 -26
  43. package/dist/systemReminderTag.js +14 -4
  44. package/docs/INTEGRATION-CLIENTS.md +346 -11
  45. 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.103.0
38
+ **Version:** 0.104.1
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
@@ -339,10 +339,10 @@ 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. |
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. |
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 |
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. |
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. |
346
346
  | `scripts/run-read-face-posture-projection-test.mjs` | The operator-face `readFace: ReadFacePosture` reader (server >=7.65.0). Three ways of "can't say" are pinned to three different, literal sentences, and none of them may read as "nothing is pinned" — that statement belongs to exactly one case, `face: null`, which is a positive fact reported by the engine, not an absence: not having read an operator response yet, having read one from an engine too old to report the key, and the engine actually saying nothing is pinned are three different next steps for an operator and must not collapse into each other. `source` is read as an open set (the server's closed four words plus an escape hatch) rather than narrowed to an enum, so a new word added upstream is not silently turned into a bad reading. The free-text `note` is sanitized and length-capped before it is ever rendered. A companion pure function flags disagreement between this face and the tenant-facing `capabilities.readFace` — silent only when the two actually agree, honest-absent when either side cannot be read at all, never asserting agreement as a fact. The gate's last leg reads the installed SDK's own `openapi.yaml` directly rather than restating the schema in prose, so the package's leniency cannot quietly drift from the real contract |
347
347
  | `scripts/run-display-body-test.mjs` | The engine wraps text it hands a model in a fence — an opening marker naming the payload, the payload itself, and a closing marker — so the model reads it as data and not as instructions. That fence is minted and read in one place here, which makes stripping it for a human reader this package's job rather than each shell's: a shell that renders the envelope verbatim is showing a person a defence that was written for a model. The reader answers with a discriminated union — fenced, with the label and the payload, or not fenced, with the text as it came in — and it reaches that answer through the **same** matcher the mint side registers, never a second copy of it; the guard proves that by walking the syntax tree of every source file and requiring exactly one literal carrying the marker text, and by requiring the reader's own body to contain no matcher of its own. Eighteen shapes are run through both entry points and required to agree line for line. Anything the package does not recognise — a near-miss in the wording, a hyphen where the marker has a dash, a different case, an opening marker with no close, a close before an open, a truncated close, or any non-whitespace byte outside the pair — comes back unfenced with the input returned **verbatim**: no guessing, no trimming, no repair, because a half-stripped envelope puts a sentence on screen that nobody wrote. Only the outermost layer is removed, so a nested fence, or one forged inside the payload, survives byte-for-byte in the body — those bytes are part of what the engine said, not part of this protocol. Nothing else is washed: control characters, leading and trailing whitespace and a twenty-thousand-character payload all pass through untouched, and so does the label, because sanitising and length-capping belong to the mint point that puts a string on a screen and a passage of text must not have two launderers. A value that is not text is answered with **nothing at all** rather than with an empty payload: the reader never stringifies it, never calls its `toString`, and never emits `[object Object]`, and it does not hand back a body of zero length either — an empty payload is a real reading (a fence can legitimately wrap nothing, and an empty string is an empty string), so folding "there was no readable text" into it would leave a caller unable to show a degraded line at all. Those three stay apart: no text yields nothing, an empty string yields an unfenced empty payload, and an empty fenced payload yields a fenced one with its label. The `fenced` discriminator is always present on a reading, and the label key exists only on the fenced arm, so a missing label is never rendered as an empty one. One shape needed more than the whole-string match this started with. When the engine reports back from a delegated run, the fence is only **one section** of the report: ahead of it sit a frame header, a handful of optional field lines and a section label, behind it a closing instruction addressed to the model, an optional internal identifier and a usage block. A matcher anchored to both ends of the input answers *not fenced* on that, and the whole scaffold - written for a model - goes on screen. The reader therefore also locates the fence **inside** a recognised report frame, using four anchors that are always present and always byte-for-byte fixed, and hands the located slice back to the same single matcher rather than a second one; the guard assembles its corpus from the engine package actually installed (the fence from that package's own constructor, the frame lines read structurally out of the minting file and then compared byte-for-byte with what this package registers), so a rewording or a reordering upstream turns the guard red the day it lands. Three readings are pinned one cell each: a fenced result section yields the payload the delegate actually wrote; a partial-findings section yields that text and says which of the two it is, with the prefix line kept out of the payload; and a section the engine filled with its own no-text sentinel yields an empty payload with a reason, which stays distinguishable from an input that was simply an empty string. Whatever surrounded the fence is returned alongside rather than dropped - the bytes before it, the slice itself and the bytes after it reassemble into the input exactly - and the payload never contains a line of the frame. The criterion deliberately does **not** enumerate the lines outside the fence: several of those field lines carry interpolated untrusted text and cannot be told apart from prose, so requiring every one of them to be recognised would mean that a single new field line upstream sends every failed report back to being unreadable, and a failed report is exactly when a person most needs to read what the delegate said. Fourteen negative shapes hold the line against the easy widening, strip anything that looks like a fence: a fence sitting in ordinary text, a missing frame header, a different sentence where the closing instruction belongs, a report with no closing instruction at all while the fence sits at the very end, a header and label in the wrong order, mismatched open and close tags, an open with no close, a bare unfenced section, a stray line between the label and the fence or between the fence and the closing instruction, an entirely absent section, a no-text sentinel with another line after it, a partial-findings prefix followed by something that is not a fence, and the whole report in carriage-return line endings all come back unfenced with the input verbatim and no frame at all. Above all, **provenance is not in the text**: locating a fence inside a frame happens only when the caller states where the bytes came from, because four anchors can only recognise a shape and never prove an origin. The default reading is byte-for-byte what it was before, so a passage of ordinary prose that happens to quote a report - with real warnings on either side of the quoted part - is returned untouched and those warnings stay on screen; a caller that does state the origin gets the located reading, and even then every surrounding byte comes back alongside. Twenty-three malformed origin values fall back to the narrower default without throwing: a near-miss in case, a camel-cased spelling, the right word padded with spaces or tabs or a newline or a zero-width character, a string wrapper object, an object whose `toString` or `valueOf` reports the right word, and an object whose converters both throw. The guard compares the caller’s argument for **exact equality** and nothing else — no trimming, no stringifying, no calling the value’s own converters, because that would let a value of unknown provenance choose its own lane — and a source-level cell requires that the argument reach the comparison unreassigned and unnormalised, with its own two-way check that those patterns speak. The two accepted words are read off the published type rather than copied into the guard; adding a third word later is a type-compatibility change for any caller that switches exhaustively on them. Each of the three anchors must also be **unique** in the text, and ambiguity means the reader declines. The reason is not hypothetical: the frame header interpolates the task's own description verbatim, and the engine only requires that description to be a string, so it can carry newlines and a complete set of protocol lines. Any rule that picks one candidate out of several can therefore be made to pick the planted one, hiding the real result among the surrounding bytes - a guard cell reproduces exactly that, with a planted section and a real one, and requires the reader to decline and the real text to stay on screen. Two reports back to back are the same ambiguity and are declined the same way, with both payloads left visible; a delegate that quotes any one of the three anchor lines inside its own answer also falls back to the input verbatim, which is the registered cost of the rule, and a discrimination cell shows the same corpus reads cleanly once the quoted line is gone. The two legs are not the same shape either: the forked one ends at its closing instruction with no trailing bytes at all, and that real shape has its own cell. The witness arm reads each leg only inside its own array of lines, decodes every extracted literal to its **runtime** value rather than trusting the spelling in the source, and fails loudly if it cannot - an escape rewrite upstream leaves the runtime label unchanged while the spelling diverges, and since that same extracted value builds the corpus and serves as the expectation, trusting the spelling would close a self-proving loop. The two payload labels are therefore also pinned in the guard and compared against what was extracted, so an equivalent rewrite stays green while a real rename turns red the day it lands. A delegate that quotes the frame lines inside its own answer does not move the location, and those quoted lines survive in the payload byte-for-byte. For a recognised report frame the reader also hands back the header slots a shell used to assemble by counting lines: the state line (read only when it sits immediately after the frame header and is the only state line in the frame, so a line planted in the task description cannot masquerade as the engine's own), an error line and a usage line (each only when it is the only one of its kind), and every line it did not claim, verbatim and in order; when the state line cannot be read from that position all four slots stay absent and every byte stays in the remainder |
348
348
  | `scripts/run-display-cap-order-test.mjs` | The order in which untrusted text is sanitised and length-capped, across every mint point that puts an engine- or database-supplied string on a screen. The sanitiser rewrites each invisible character as a six-character escape, so capping the **raw** string first and escaping afterwards hands the screen six times the width that was budgeted — a forty-character allowance becomes two hundred and forty. The guard does not hardcode that allowance, because each mint point wraps its field in different fixed prose and the prose moves: it anchors on the deciding quantity instead, feeding one benign and one control-character input of the same length through the same mint and requiring the second not to come out longer. That criterion is immune to wording changes and stays sensitive to the expansion, and it is `<=` rather than `==` on purpose — a correct escape-then-cap backs the cut off a partially-consumed escape token, so the control-character line is legitimately the shorter of the two, and demanding equality would score that avoidance as a regression. Each mint is bracketed by two positive controls (the input really reaches the screen; the cap really engages) and the expansion predicate is shown to turn red against a deliberately cap-then-escape reference, so an all-green run cannot mean the guard simply measured nothing. The shared mint point is checked directly for the two avoidances it owes — never splitting an escape token in half, which would leave something on screen that looks like the beginning of a complete answer, and never splitting a legal surrogate pair, which would manufacture the very lone surrogate the sanitiser exists to catch |
@@ -444,7 +444,7 @@ guard still cross-checks the table by name).
444
444
  | `scripts/run-dist-comments-test.mjs` | **dist ships zero comments.** Since 0.77.2 the build strips comments (`removeComments`); this gate walks every shipped `dist/**/*.js` / `*.d.ts` and counts comments with the TypeScript parser (a comment is whatever sits between two tokens, so string, template and regular-expression literals that merely look like comments, and generator methods, are not comments), failing on the first one (`DIST-COMMENT-FAIL`). Source comments are an internal surface; what still ships is code, string literals and type-level text, which the hygiene gate screens. Negative control: one plain comment appended to `dist/index.js` turns it red. |
445
445
  | `scripts/run-task-request-omission-receipt-test.mjs` | Where every key a client hands to the request constructor ends up. The constructor used to answer "not stamped" the same way for four different reasons — value absent, no such row, wrong lane, live gate closed — and a key it had never heard of did not even get that: an unattended run could pass a system prompt, an output schema and a spend cap and receive a body holding the objective and the session id, with nothing anywhere saying what was left out or why. The guard pins the three answers apart. **Seated** keys reach the body verbatim on the unattended lane. Keys the package **knows but did not carry** never throw, never reach the body, and each gets a receipt row with one word from a frozen cause list — every present key is on the body or on the receipt, never both and never neither, checked across both lanes with the live gate open and closed against a key-by-key table written independently of the package's own routing. Keys the package **does not know** are refused loudly and are a separate cell, not a fourth cause: the cause list has no word that could hold them, and the seat reader answers `unknown`, not `none`. The cause list is bitten from both sides (exact, every word producible, nothing produced outside it, the judge table's keys read from source through the TypeScript parser) and no second hand-copied list may exist in `src/`. Upstream claims are read straight off the installed SDK typings: a key seated in this release must be a named request field, a key registered as having no upstream counterpart must not be — the day it appears the guard turns red — and the index signature counts as evidence for nothing. The per-run file-history opt-out word is covered the same way: seated on the two user lanes behind the live gate and absent from the side-channel lane, stamped only for its one legal word, refused loudly for any other value, and read after every existing declaration so that it cannot erase them. The wording shown to end users is pinned alongside the wording for integrators: a second sentence table, keyed by the same frozen cause list and exhaustive over it at compile time (the check sits on a two-column table literal — a lead-in plus a reason clause — from which the full sentences are derived), gives each cause one plain sentence that says the item was not sent and why (the integrator table now carries the same literal-level check — before, a missing sentence failed the build but an extra one did not), with no integrator vocabulary and no claim about whether anything took effect; every sentence differs from the others and from the integrator wording and passes the published-text hygiene list, and an unknown word, a non-string, a throwing object or a prototype-chain name gets one honest generic sentence instead of a throw, an echo or an invented cause. The full sentences and the generic fallback stay byte-identical to the sentences shipped in 0.88.0, every reason clause inside them starts lower-case with no closing full stop, an unreadable cause gets the generic sentence, no full sentence may appear as a literal anywhere in the source, and the separate reason-clause export withdrawn in 0.98.0 is checked to be absent from the root entry. When a consuming client's own copy of the cause table is on disk, the guard also checks that every word in it is one of the package's causes, with no word listed twice (a word only the client's table has fails; a word only the package has is reported, not judged), and reports that check as skipped rather than passed when the table is not there. A request key the engine retired and refuses by name is, from 0.92.0, refused at construction by the same unknown-key rule as any other key the package does not know (it names the key and never echoes the value), on every lane, in every live state and through both doors; the seat reader answers unknown and the wire-side judge names it. |
446
446
  | `scripts/run-session-policy-wire-test.mjs` | The per-session tool-rule face: the capability bit that says whether an engine keeps such rules at all, and the narrow read plus tightening orchestration built on it. The bit is read the same four-state way as its sibling capability readers — an absent key is not reported (this binary predates the position itself, which says nothing about whether the face exists), `true` is present, `false` is a positive absent (this deployment keeps no per-session rules), any non-boolean value is unreadable and drops the cell rather than being folded into "absent", and a capabilities body that is not an object at all (an array included) is unreadable rather than "not reported". Its single-source verdict answers whether to show the tightening entry: only an engine that says yes is `yes`, both a positive no and a binary too old to answer are `no`, and never having observed a capabilities body is `unknown`. Whether to put a request on the wire is deliberately a **different** question with a different answer for that last state, and lives with the orchestration. The read narrows three ways that must not collapse into each other: a record that really is empty (present, version zero — what an engine answers for a session no rules were ever written for), a record that cannot be read, and a call that failed with a typed disposition. A half-bad record — one rule bucket well-formed and another the wrong shape — counts as unreadable in full, because the write verb replaces the whole record: dropping the bad bucket and writing the rest back would empty it, which relaxes the rules while the caller sees a 200. An unreadable version stamp is never filled in with a zero, a bucket that reports an implausible number of entries is unreadable rather than walked or truncated, each array's length and each of its indices are read exactly once, and a throwing accessor is unreadable rather than propagated. Every load-bearing key is read as an own property — the envelope, the version stamp, each of the five buckets and each array index — because a prototype lookup would let a polluted prototype put a bucket into the reading that the wire never carried, and since the write replaces the whole record the union would then write that invented restriction back as a real one; a guard pollutes the object and array prototypes in place and proves all four shapes stay out. Because the write replaces the whole record, adding a restriction means writing "what is already there, plus the new entries": the union only ever adds, de-duplicates verbatim, keeps a bucket that is present but empty (present-and-empty and absent are opposite meanings, and dropping it would relax the rules), mints no bucket neither side had, copies entry bytes as they came (no trimming, sorting or path rewriting — those judgements belong to the engine), and takes its bucket names from the engine's own type surface rather than a hand-copied list, so a new bucket upstream is a compile error instead of a silently dropped one. Which differences count as relaxing is the engine's judgement and is never re-implemented here: a refusal on those grounds is reported verbatim, never swallowed and never retried. The orchestration is guarded on three axes. Timing: when the record moves between the read and the write, it re-reads and re-writes **exactly once** — two reads and two writes, no more — and the second attempt's union carries the other writer's entries, which is the entire point of re-reading; a second collision is reported rather than retried a third time, and an uncontended write makes exactly one round trip. Concurrency: an explicit barrier holds both orchestrations first reads at the same version before either may write, and the criterion is how many times the store actually rejected a stale version rather than how many writes it saw — the latter is equally true of two serial successes, so it would stop detecting contention the day the interleaving changed. Under real contention the store rejects exactly once, both writers land on strictly different versions, both writers entries survive in the final record, and the round trips are exactly three reads and three writes; the same two orchestrations run serially are asserted to reject zero times in two reads and two writes, which is what proves those numbers are discriminating. On a store where every write loses the race both report a collision having written exactly twice each. Failure classification: a relaxation refusal, a refusal to stamp a version the store cannot establish (the same status code as the relaxation refusal but a different machine code, and folding it into that arm would send the caller off to edit entries that are not the problem), a missing session, a deployment without the face, an ownerless session, a collision code, a bare conflict with no machine code, a rejected body and an unauthorized call each land on their own arm — the collision arm is matched on the machine code verbatim rather than on the status, because two different situations share that status and only one of them is worth retrying. The remaining split is not "which code is this" but "did the engine answer at all": an answered client-side refusal is allowed to say nothing was written, because every such refusal on this endpoint is emitted before the record is touched, while a throw with no answer at all — a dropped connection, a timeout, a response body the transport itself could not decode, a server fault — can only say "unknown", since that throw may well have happened after the record was already saved. Two guards prove that is not theoretical: a write whose receipt cannot be read, and a write that throws after the fixture store has committed, both leave the record changed. Neither is success nor failure: the only honest answer is "unknown", it carries no version, and it is never retried. The direction of the change is likewise never claimed. The engine’s tighten-only rule is an identity gate, not a field gate — for a principal the deployment treats as an operator it does not run at all, so a union that adds a name to an existing allowlist is accepted and really does widen it. This package does not mint a second copy of that rule, so what it reports is the fact it can stand behind: the record now holds what it already had plus the entries sent here. The sentence for a saved write is pinned to contain no claim of tightening, narrowing or restriction, and a guard reproduces the operator case to prove the widening is real while the wording stays honest. Every sentence the module mints is checked pairwise distinct, with the receipt-unreadable one required to keep its "may already be in effect" and the record-unreadable one required to say nothing was written. When a read comes back without a usable record, one sentence per failure kind and per unreadable position says so in plain words, never naming internal codes, never saying the record is empty, and falling back to one general sentence for anything it does not recognise. A sixteenth sentence covers a session that has not begun on the engine yet — a state only the host knows, since the server gives the same answer for a session that does not exist and for someone else's session — and it is checked like the others: distinct from the other fifteen and from the general fallback, free of machine words, never claiming the record is empty or that nothing is restricted, never saying the contents are unknown (there is nothing to know yet), read from own data properties only, and compared with the previous release's package so that the other fifteen sentences and the general fallback stay unchanged byte for byte. |
447
- | `scripts/run-session-policy-deliverable-test.mjs` | Which of a batch of user-written permission rules can be written into a session’s own rule record without changing their meaning, and why each of the others cannot. The record holds whole tool names and command names only, so exactly one class maps across losslessly: a deny rule that names one tool with no qualifier. Everything else is withheld with one word from a closed seven-word list — an ask rule (the record has no ask tier), a deny rule with a parenthesised qualifier (recording just the name could block more), a rule that names a server or agent peer without naming one of its tools (for every protocol namespace the engine knows, checked against the engine package's own table) or contains a wildcard (*) anywhere (an engine that compares exact names would block nothing), an entry that is not a tool name, and a name the engine refuses at the start of every run — a retired tool name, or one containing "__" without a protocol prefix, where the prefix check is case-sensitive (once such a name is in the record, every later run of the session fails at startup until that entry is removed; the retired-name list is checked entry by entry against the engine package's own list, and a withheld retired name carries its current name when the engine says it was renamed), and — only when the caller passes the tool roster of a run — a name that roster does not list as a tool name or alias, compared exactly with letter case counted (recorded as it is it would block nothing on that deployment) — and each word has one sentence, which never echoes the rule itself; asking for the sentence never throws, even with a value that throws when turned into a string. A name with leading or trailing whitespace counts as not a tool name: the record compares exact bytes, so it would block nothing. The guard pins the batch semantics: the deliverable part is either the whole batch or empty, never a subset, so a caller cannot send half a change and report it as saved. An end-to-end check runs the engine package itself: every batch this function would deliver — the recorded vectors and a fixed-seed sample of generated names — is written into an in-memory session rule store and the next run must get past its start-up checks and reach the model, while every name withheld as refused — every retired name included — must indeed make that run fail at start-up, and every string literal in the judgement source that it withholds as refused must be one the engine package's own tables refuse. It also checks that malformed input never throws and never delivers anything (non-arrays, non-string entries, holes, a polluted array prototype, a length or index that throws, a changing index read once), that a batch which cannot be read at all is marked `unreadable: true` while an empty batch is not, so the two stay tellable apart, that each word is produced by some vector and nothing outside the list is produced, and — when a checkout of the previous in-client implementation is present — that this function gives the same answer on every recorded vector and on tens of thousands of generated rules and pairs, except for five deliberately stricter classes (whitespace-padded names; rules with a wildcard anywhere, which the previous implementation sent as exact names unless the wildcard was the whole tool part of a server rule; peer-wide rules outside the MCP namespace, which it did not recognise; names the engine refuses at start-up, which it sent as ordinary names; and names the engine's rule compiler does not accept as one whole tool, such as an escaped parenthesis or a backslash in the name), whose disagreements are counted per class and must match an independent count exactly. The optional roster only ever withholds more: with no roster, or an absent or null one, every answer is byte-identical to the last release before the roster existed, checked against that release's published file over tens of thousands of inputs, apart from the rule-syntax class described below; a name withheld without a roster stays withheld with the same word, and the same current name, when the roster lists it as a name or an alias; a name delivered without a roster is withheld as not in the roster exactly when the roster lacks it; the engine's own namespace-covering forms keep their word; and a roster or options value that cannot be read marks the whole batch unreadable instead of falling back to no roster, while a readable empty roster is a roster. An end-to-end check takes the roster from a real run of the engine package that mounts a tool with an alias: every name delivered with that roster passes the engine's start-up name audit without being reported, and every name withheld as not in the roster is one that audit reports as matching no mounted tool. When the previous in-client implementation carries its own roster check, the two are compared under a given roster and may differ only for an alias (delivered here, withheld there) and for a refused name that the roster lists (withheld here, delivered there). A deny rule is delivered only when the installed engine's rule compiler, asked about that one string the same way the request-body permission assembly asks about whole tool names, accepts it as a rule for one whole tool: a name with an escaped parenthesis, a backslash, a lone closing parenthesis or more characters than a rule may have is withheld as not a tool name, because the record would compare those bytes and match no tool. When the compiler is not installed or cannot answer, a batch that needs it is marked unreadable rather than delivered, while batches that never reach that step answer as before. The guard checks that every delivered name is one the compiler accepts, that nothing delivered now was withheld by the earlier release and the extra withholding is exactly that class, that a roster never makes such a name deliverable, that the compiler of every engine generation in the server fixtures gives the same answers, and that the request-body assembly reaches the same verdict on the same names. Since 0.99.0 the cells that depend on the engine's tool-name grammar take their expectation from the installed engine generation, which refuses blank, invisible and wildcard-only tool names. |
447
+ | `scripts/run-session-policy-deliverable-test.mjs` | Which of a batch of user-written permission rules can be written into a session’s own rule record without changing their meaning, and why each of the others cannot. The record holds whole tool names and command names only, so exactly one class maps across losslessly: a deny rule that names one tool with no qualifier. Everything else is withheld with one word from a closed seven-word list — an ask rule (the record has no ask tier), a deny rule with a parenthesised qualifier (recording just the name could block more), a rule that names a server or agent peer without naming one of its tools (for every protocol namespace the engine knows, checked against the engine package's own table) or contains a wildcard (*) anywhere (an engine that compares exact names would block nothing), an entry that is not a tool name, and a name the engine refuses at the start of every run — a retired tool name, or one containing "__" without a protocol prefix, where the prefix check is case-sensitive (once such a name is in the record, every later run of the session fails at startup until that entry is removed; the retired-name list is checked entry by entry against the engine package's own list, and a withheld retired name carries its current name when the engine says it was renamed), and — only when the caller passes the tool roster of a run — a rule for which the engine's own matching finds no tool in that roster: no tool name or alias equal to it, compared exactly with letter case counted, and, for a name the engine also reads as covering every tool of a server, no listed tool of that server (recorded as it is it would block nothing on that deployment) — and each word has one sentence, which never echoes the rule itself; asking for the sentence never throws, even with a value that throws when turned into a string. A name with leading or trailing whitespace counts as not a tool name: the record compares exact bytes, so it would block nothing. The guard pins the batch semantics: the deliverable part is either the whole batch or empty, never a subset, so a caller cannot send half a change and report it as saved. An end-to-end check runs the engine package itself: every batch this function would deliver — the recorded vectors and a fixed-seed sample of generated names — is written into an in-memory session rule store and the next run must get past its start-up checks and reach the model, while every name withheld as refused — every retired name included — must indeed make that run fail at start-up, and every string literal in the judgement source that it withholds as refused must be one the engine package's own tables refuse. It also checks that malformed input never throws and never delivers anything (non-arrays, non-string entries, holes, a polluted array prototype, a length or index that throws, a changing index read once), that a batch which cannot be read at all is marked `unreadable: true` while an empty batch is not, so the two stay tellable apart, that each word is produced by some vector and nothing outside the list is produced, and — when a checkout of the previous in-client implementation is present — that this function gives the same answer on every recorded vector and on tens of thousands of generated rules and pairs, except for five deliberately stricter classes (whitespace-padded names; rules with a wildcard anywhere, which the previous implementation sent as exact names unless the wildcard was the whole tool part of a server rule; peer-wide rules outside the MCP namespace, which it did not recognise; names the engine refuses at start-up, which it sent as ordinary names; and names the engine's rule compiler does not accept as one whole tool, such as an escaped parenthesis or a backslash in the name), whose disagreements are counted per class and must match an independent count exactly. The optional roster only ever withholds more: with no roster, or an absent or null one, every answer is byte-identical to the last release before the roster existed, checked against that release's published file over tens of thousands of inputs, apart from the rule-syntax class described below; a name withheld without a roster stays withheld with the same word, and the same current name, when the roster lists it as a name or an alias; a name delivered without a roster is withheld as not in the roster exactly when the engine's start-up name audit would find it matches no tool in that roster — an exact name or alias, or, for an MCP name the engine reads two ways, a tool its covering reading reaches (that covering check asks the installed engine's own covering matcher; when it is not installed or cannot answer, a batch that needs it is marked unreadable instead of falling back to exact names); the engine's own namespace-covering forms that are withheld without a roster keep their word; and a roster or options value that cannot be read marks the whole batch unreadable instead of falling back to no roster, while a readable empty roster is a roster. An end-to-end check takes the roster from a real run of the engine package that mounts a tool with an alias: every name delivered with that roster passes the engine's start-up name audit without being reported, and every name withheld as not in the roster is one that audit reports as matching no mounted tool. When the previous in-client implementation carries its own roster check, the two are compared under a given roster and may differ only for an alias (delivered here, withheld there) and for a refused name that the roster lists (withheld here, delivered there). A deny rule is delivered only when the installed engine's rule compiler, asked about that one string the same way the request-body permission assembly asks about whole tool names, accepts it as a rule for one whole tool: a name with an escaped parenthesis, a backslash, a lone closing parenthesis or more characters than a rule may have is withheld as not a tool name, because the record would compare those bytes and match no tool. When the compiler is not installed or cannot answer, a batch that needs it is marked unreadable rather than delivered, while batches that never reach that step answer as before. The guard checks that every delivered name is one the compiler accepts, that nothing delivered now was withheld by the earlier release and the extra withholding is exactly that class, that a roster never makes such a name deliverable, that the compiler of every engine generation in the server fixtures gives the same answers, and that the request-body assembly reaches the same verdict on the same names. Since 0.99.0 the cells that depend on the engine's tool-name grammar take their expectation from the installed engine generation, which refuses blank, invisible and wildcard-only tool names. |
448
448
  | `scripts/run-session-policy-refused-removal-test.mjs` | The way out for a session that already carries a tool name the engine refuses. The engine refuses to start a run when any rule applied to it names a tool its own tool-name check does not call current — a retired tool, a name containing "__" without a protocol prefix, or (engines from 7.48.0) a protocol name whose server or peer segment it never mints — so a session whose own rule record holds such a name fails at startup on every run. The guard pins three pieces. First, the refusal test itself: since 0.97.0 it is the engine's own tool-name check read through `ruleToolNameVerdictOf`, compared name by name, in both directions, with that check called directly over a corpus that includes padded, wildcard and parenthesised forms, and it is shown to be wider than the verdict used before a write — a padded or wildcard form is withheld there for another reason yet still refuses the run, so a removal driven by that verdict would leave the session broken. Every corpus name is then written alone into the deny list and into the allow list of a real engine run: the test says "refused" exactly when the run fails at startup with the refusal code and the model is never called, while the same shapes in the command lists are not audited at all. Second, the removal verb: it takes only the refused entries out of those two lists and leaves every other entry, list and ordering byte-for-byte, keeping an emptied allow list as a present empty list; after it runs, the same session's next real engine run gets past startup, for every refused name in the corpus and in both lists. Against the installed engine as an ordinary caller, taking a deny entry out is refused as a loosening — reported as such, written exactly once, the record unchanged and the next run still refused, never a false success — while an allow-list-only removal, which narrows, succeeds; against a model of the newer contract the engine has announced, the removal succeeds, and the same model still refuses a removal of anything else. A concurrent writer causes exactly one re-read, judged again on the other writer's record, and a second collision is reported rather than retried; read failures, coded write refusals and unconfirmed writes (no verdict, a receipt that cannot be read, or a receipt that does not account for the removal, including one that still holds a removed entry) each land in their own arm, and running the verb again after an unconfirmed write reports nothing left to remove. Third, the wording: one sentence per outcome; the loosening refusal names operators and says the record is unchanged, the unconfirmed one says the change may already be in effect, and none carries a rule string or engine text. The startup-failure sentence answers to that one code only, lists every place the entry can be set without naming who may remove it, and a real run with the name in the caller's own policy rather than in the session record yields the same code — which is why the sentence may not claim the entry is in the session record. The verb also requires the tool roster reported by a run on the deployment: a name listed there, as a tool name or an alias and compared exactly as the engine compares it, is left in place even when it is a retired name, because the engine accepts that rule — with a real engine run that mounts a deployment tool under a retired name (or with that name as an alias), the removal keeps the deny rule and the next run still starts. Without a roster, or with one that cannot be read, nothing is read or written and the outcome says why; a readable empty roster is still a roster. The removal sentence speaks separately about names taken out of the deny list and out of the allow list. |
449
449
  | `scripts/run-rules-legacy-tool-name-projection-test.mjs` | The retired session-rule write refusal for tool names, pinned as retired. Servers before 7.129.0 refused a session rule write naming a retired tool or a name with "__" but no protocol prefix with a dedicated code; from 7.129.0 that code is gone and the same names are refused with the inert-entry code, which this package reads as its own named outcome. With the support floor at 7.129.1 the old code is no longer read: its constant is gone from the root entry, and when an older server still sends it the failure classifier answers the generic answered-refusal arm (no list, name, verdict or current name is carried) for any 4xx status, while other statuses follow the usual source rules; the family prefix test still recognises the code. Against a model of the old refusing write gate, the tightening flow answers unknown with a refused write, writes exactly once and leaves the stored record untouched, and the removal verb answers a refused write; the before-write withholding of such names is unchanged. The run-startup refusal sentence is still given for the startup code only, the old write sentence is no longer given for either the old code or a hand-made old outcome, and the cross-copy body-slot read is pinned on the live inert-entry arm instead. |
450
450
  | `scripts/run-persisted-rule-write-test.mjs` | The **single-step tightening write** for persisted permission rules — the dual of the revoke surface, and the half where a hopeful reading is expensive. The two behaviours this entry accepts are **derived** from the three-state vocabulary by subtracting the widening one, never hand-copied: the guard bites in both directions (every word in the derived table is really accepted; every constructed outsider — casing variants, trailing whitespace, the widening word itself — is refused before a single round trip), keeps a word-count canary against the parent table, and pins that the source file contains **exactly one** array literal carrying two or more behaviour words, so a second hand-written table shows up as a boundary failure rather than as drift nobody reads. A standing approval is minted by answering a permission question or by importing settings; this entry is not a third route, and the widening word is unspellable in the type. The outcome is a discriminated union whose two failure arms are **not** interchangeable: ten refusal causes each promise the same single thing — not one byte reached the store — while three separate words say the opposite, that the outcome could not be read at all. The service's own "I cannot tell" (a write that could not be confirmed as standing: store wobble, a redemption leg with no decidable ending, or a write that landed and was revoked concurrently before the read-back) stays in the second group, because announcing "nothing was written" invites a clean retry that is not clean, and announcing success misreports a tightening that may already be gone. Anything the shared failure classifier does not recognise defaults to the same place — this is a non-idempotent verb, so "unclassified" must mean "unknown", never "no write": a 500 can happen after the store commits. A 2xx whose body cannot be read is pinned in the same direction and from both sides: it reads as unknown, and the unknown arm carries **neither** the revision nor the written row, so a consumer cannot even spell the shape that would let "unreadable" pass for "written". `persisted` is guarded against the reading everyone reaches for first: it says *this call wrote*, not *a new rule now exists* — an equivalent rule already in the store still mints a fresh causal point, so the lane honestly reports `persisted`, and the material for judging whether the **logical** rule is new (the approval ledger on the returned row) is handed to the caller rather than folded into the discriminant, since the package does not have the one fact that judgement needs. The returned row goes through the **same single narrower** the listing surface uses — proven by running one row corpus through both legs and asserting the two verdicts agree entry for entry (an adversarial pass that forks the listing leg back into an inline copy reds here immediately), plus a source pin that the predicate is defined once and called from exactly the two legs. That sharing is what keeps a row whose behaviour cell is unreadable **visible in both places** rather than hidden by one of them — and the shared narrower is deliberately followed by a second, *different* question only the write leg can ask: is the row that came back **the rule that was just sent**? A rule's identity is a triple, so a receipt missing its behaviour cell, carrying the sibling state, carrying the widening one, or naming another text or another scope is not evidence that the requested tightening is standing; it reads as unknown with its own word, kept distinct from "unreadable" so the two stay tellable apart, while display and derived cells may vary freely. An adversarial pass found both of the gaps this pins: the receipt check that only looked at whether the row was renderable, and a subtler one — pulling the verb off the injected port and calling it bare drops the receiver, so a host that hands over a real resource object (a class instance whose verbs reach the transport through `this`) would see every write throw and be reported as "could not tell", retry after retry, while the package's three other ports call their verbs as methods and work fine. Both are pinned from the failing side: a shorthand-method facade and a class-instance facade must reach the transport and return a real outcome, with the bare-call throw proven to be a real failure mode first. A 405 is split in two, because only the engine's own bare code is evidence about **the engine**: with it, the path exists and this verb does not, so this worker predates the verb; without it — an absent, empty, or foreign code, which is what a proxy or gateway blocking the method typically returns as HTML or an empty body — what was seen is that the verb was refused, while **who** refused it and **at which hop** is unknown, so it lands in the unreadable-outcome arm with its own word rather than sending someone to upgrade a worker that is fine, hiding an entry that is live, or — the part a second adversarial pass insisted on — promising that nothing was written. That promise is what the refusal group means, and a middlebox is free to forward the request and only then answer 405 on its own policy, so a caller who skipped reconciliation on that word would leave behind a standing refusal the user believes never took effect; the guard pins exactly that shape, with a double that writes the rule and *then* answers 405, and with the engine's own bare code still landing in the refusal group beside it. (The same passes caught the naive status-only reading and the asymmetry where an empty-string code fell through to a different bucket.) The documented recovery for an unreadable outcome — retry, then reconcile — is pinned to be **ledger-safe** rather than merely asserted: against a double that models the engine's own "is this identity already standing?" question, re-sending the same identity comes back as a no-op with the approval ledger and the bucket revision both unmoved, however many times it is repeated, while two concurrent writers each landing a causal point are both honestly reported as having written. Finally the four local gates are pinned to be free: a missing write verb on the injected port, an unwritable direction, an unreadable identity pair, and a principal key that is present but cannot name anyone all refuse **without sending anything** — the last of those because silently degrading a blank target into absence would land a tightening aimed at someone else in the caller's own bucket and return a 200; an empty operator list (501 `capability.operator_principals_required`) is reported as a refusal (`operators_unconfigured`) rather than an unknown outcome |
@@ -459,7 +459,7 @@ guard still cross-checks the table by name).
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`. |
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`. |
462
- | `scripts/run-automatic-answer-verdict-test.mjs` | The single verdict `automaticAnswerVerdict(card, lane, facts?)` that decides whether an automatic lane (the standing permission mode, or a standing allow rule) may answer an approval card on the user's behalf. The only `may_answer` exit is a well-formed class set whose classes are all known to this build, whose folded row does not block on that lane, with no must-ask evidence on the card and, for the mode lane, a host reading that its own ask rules did not match; every other card is `must_ask` (with the road that decided it) or `unknown` (with why). The guard derives every single-class card and every ordered pair of classes on both lanes from the engine's own arbitration table, walks every row of the decision table on both sides (including positive evidence when the class set is absent or malformed, class sets that change while being read, and class names this build does not know), compares the package's fold against the engine's fold function over all singles, pairs, triples and a random sample, checks the mode lane against the engine's bypass-mode arbitration (including the release disclosure), confirms on a real engine run that a matched ask rule always puts `ask_rule` in the class set, checks that the two older predicates never answer more loosely than this verdict nor than the previous release, and, when a terminal reference tree is available, that the terminal's own decision chain auto-approves nothing new when it is built against this package. Since 0.94.0 it also covers the class-aware form of the mandated-question sentence: the clause naming an automatic check appears only when the card's question classes let the classifier or the sandbox answer, checked class by class against the engine's own table, and any unreadable card falls back to the generic sentence. Since 0.94.0 the mode lane reads the bypass column on the tool face of the card (the face comes only from the generated shell-face name list, matched exactly, and releasable classes are read per face from the generated table): a write whose protection could not be resolved is answerable on the shell face and still asked on file tools, while lookalike tool names (case, whitespace, zero-width or homoglyph characters, protocol prefixes, rule syntax, inherited or accessor names, non-strings, absence) always read as the other face; when the installed engine exposes faces, the fold and the bypass arbitration are compared per face against the engine and every catalog name is checked against its face. A third lane, `permission_request_hook`, reads the same column as the standing-rule lane (whether a stored rule may clear the class) with the same must-ask evidence first, ignores allow-rule shadow evidence and host facts, and `permissionRequestHookScope` turns its verdict into allow-or-deny, deny-only or skip; a real engine run confirms that an automatic answerer's allow is refused exactly on asks that carry the real-approval bit, and that the lane never reads more loosely than the engine does. The terminal-chain comparison accepts exactly the shell-face release as the one intended widening. |
462
+ | `scripts/run-automatic-answer-verdict-test.mjs` | The single verdict `automaticAnswerVerdict(card, lane, facts?)` that decides whether an automatic lane (the standing permission mode, or a standing allow rule) may answer an approval card on the user's behalf. The only `may_answer` exit is a well-formed class set whose classes are all known to this build, whose folded row does not block on that lane, with no must-ask evidence on the card and, for the mode lane, a host reading that its own ask rules did not match; every other card is `must_ask` (with the road that decided it) or `unknown` (with why). The guard derives every single-class card and every ordered pair of classes on both lanes from the engine's own arbitration table, walks every row of the decision table on both sides (including positive evidence when the class set is absent or malformed, class sets that change while being read, and class names this build does not know), compares the package's fold against the engine's fold function over all singles, pairs, triples and a random sample, checks the mode lane against the engine's bypass-mode arbitration (including the release disclosure), confirms on a real engine run that a matched ask rule always puts `ask_rule` in the class set, checks that the two older predicates never answer more loosely than this verdict nor than the previous release, and, when a terminal reference tree is available, that the terminal's own decision chain auto-approves nothing new when it is built against this package. Since 0.94.0 it also covers the class-aware form of the mandated-question sentence: the clause naming an automatic check appears only when the card's question classes let the classifier or the sandbox answer, checked class by class against the engine's own table, and any unreadable card falls back to the generic sentence. Since 0.94.0 the mode lane reads the bypass column on the tool face of the card (the face comes only from the generated shell-face name list, matched exactly, and releasable classes are read per face from the generated table): a write whose protection could not be resolved is answerable on the shell face and still asked on file tools, while lookalike tool names (case, whitespace, zero-width or homoglyph characters, protocol prefixes, rule syntax, inherited or accessor names, non-strings, absence) always read as the other face; when the installed engine exposes faces, the fold and the bypass arbitration are compared per face against the engine and every catalog name is checked against its face. A third lane, `permission_request_hook`, reads the same column as the standing-rule lane (whether a stored rule may clear the class) with the same must-ask evidence first, ignores allow-rule shadow evidence and host facts, and `permissionRequestHookScope` turns its verdict into allow-or-deny, deny-only or skip; a real engine run confirms that an automatic answerer's allow is refused exactly on asks that carry the real-approval bit, and that the lane never reads more loosely than the engine does. The terminal-chain comparison accepts exactly the shell-face release as the one intended widening. The hook lane's class column is the set of ask classes the engine stamps the real-approval bit on, derived from the installed engine: a card raised by an ask rule lets a hook answer, while every class in that set, and every card carrying the bit, still skips the hook; an engine run per class confirms that the engine refuses an automatic answerer's allow exactly where the lane skips. |
463
463
  | `scripts/run-allow-rule-shadow-projection-test.mjs` | The reader for the fact that a standing allow layer matched a call but could not clear its ask (`readApprovalAllowRuleShadow`, `approvalAllowRuleShadowed`, and the structured `allowRuleShadow` disclosure stamped on the approval card). Servers send a list of members with their matched entries, and that list is the only thing read. Since 0.98.0 the single text that older servers and older parked rows sent is no longer read as a disclosure: a frame or row that carries only that older text has no disclosure and no sentence, while its presence still counts against automatic answers — the card paths record it as unreadable evidence and the automatic-answer check answers "not known" for it, also when a bare frame is handed straight to the check — so retiring the reading never lets a remembered rule answer an ask it could not answer before. The guard covers every member, several members in order, empty entry lists, the count of omitted entries, member names this build does not know, four kinds of malformed values (all of which still count as evidence), absence, the older text on both card paths and the parked-ask path through the real card port (including a row whose risk descriptor is inherited or comes from a getter), a check against the previous release that every older-shaped frame and row it treated as evidence is still not answered automatically, real frames and durable rows minted by the server fixtures when available, and the effect on every automatic lane. The one sentence a card shows for that fact is minted in one place (`allowRuleShadowSentence`): an absent disclosure renders nothing (it never says there is no rule), an unreadable one says so, and entries and member words arrive display-sanitised. |
464
464
  | `scripts/run-engine-agent-absence-projection-test.mjs` | Absent background agents: when the engine stops reporting a background agent and no final state has arrived, the row is marked absent and this package owns every decision about it, so all clients agree. One predicate says whether a row is absent (the mark, not the status, decides). An absent row keeps its last known status, never counts as running, and is never counted as completed, failed or stopped; its elapsed time stops at the last moment it was seen, and its sentence says it may still be running. The end-of-turn sweep never settles an absent row (or a resident one). A row that comes back, or a real final state for the current cycle, clears the mark; a late final state from an earlier cycle does not. Absent rows are never removed at the short grace window. After the hard limit (30 minutes from the last time they were seen) the host is asked for the background-agent registry reading of each row: only a reading that the agent has ended or is not listed lets the row go, and each removal is returned as a fact the host must act on and announce; a reading of running, unknown, missing or unrecognised keeps the row and schedules nothing, so no standing poll is created. Until a registry reading is available every absent row stays. A row someone is viewing is held and reported separately only once the registry confirms it is gone. The row sentence, the removal sentence and the late-result sentence come from one place, never state an outcome or that the agent finished, and escape control characters in names, in the engine's removal word and in the late-result status. An end-to-end cell drives the real fleet projection and the real absence channel through every decision. From 0.84.0 the package also reads the registry itself: a reader lists the session's background registry only when the server advertises the listing, classifies its failures as unavailable (a failed read keeps the HTTP status and error code when the error carries them, and never invents either), and refuses a partly readable listing as a whole; a classifier turns the listing into the per-row reading, treating a registry status it cannot place as unknown and reading "not listed" only for keys shaped like registry handles and only when the host states the engine is a single process and names the row's session, because a key from another identity space proves nothing by being absent and, on a multi-replica deployment, a listing answered by another replica does not contain this session's agents at all (without that statement a missing row reads unknown and the row stays); and a predicate returns the agents the registry still counts as live that the host has no row for. A listing at the server's 500-row cap cannot show that an agent is gone either, so a missing row there reads unknown; the cap is checked against the engine's own list clamp and, when a server build is supplied, against the server's route. Each listing carries its session and a per-client sequence number, and a listing superseded by a later delivered read, or read for another session than the one the host names, counts for nothing. Only the exact listing object the reader returned can authorize removing a row or adding one: a spread copy, a structured clone, a filtered or a hand-built listing reads unknown and adds nothing, the listing, its row array and every row are frozen, the cap check uses the row count recorded when the page was read, and rewriting a listing's sequence number fools nothing. The fill predicate holds back agents first seen within the absence settle window — timed on one monotonic local clock from when this client's reader first saw the id, so a skew between the server's clock and this one, or reconnecting to a long-running agent, cannot skip the window — agents without a readable registration time and agents the host saw end since the read went out; the registry key of a row, progress or absence event is whichever of its two ids is shaped like a registry handle; and none of the reader, the classifier or the predicate throws. |
465
465
  | `scripts/run-plan-review-dismissal-test.mjs` | An automatic reopen does not put back a plan-review card the user closed (the first-presentation path neither checks nor clears that record, so a replayed park frame still presents its card). A plan-review card the user dismissed (Esc, abort, or any answer that is not approve or reject) is recorded per session and run at the moment of dismissal, synchronously, before anything queued behind the card can be released; a reopen marked `trigger: 'automatic'` then refuses with `{ reopened: false, dismissedByUser: true }` instead of minting a new card the user's next keystroke would land on, while the user's own next action (`trigger: 'user'`) reopens it and clears the record. The record is keyed by gate instance when the host supplies an instance reader: a new plan gate on the same run is still surfaced, and anything that cannot prove the gate is new (no reader, a failed, empty, thrown or timed-out read) refuses on the conservative side. The asynchronous form re-checks after its reads and before minting — a decision handed over meanwhile (seen by the package, or reported by the host's optional hand-over predicate) answers as "your answer is on its way"; a record that changed meanwhile makes the stale evaluation mint nothing and answer from the current record: another close refuses as the user's close and keeps the newer record, a card already back on screen (the user's own action or a concurrent automatic reopen, waited for within the receipt window and re-read once the wait is over) answers `reopened: true`, and a record that is gone (session change, ledger overflow) answers a plain refusal without `dismissedByUser`; a hand-over predicate that throws refuses with a plain `{ reopened: false }`. Each run has at most one reopen on its way: a reopen that arrives while an earlier one's card is published but not yet settled joins it instead of minting a second card and retiring the first card's answer path. Instance readers are snapshotted when they resolve, so a host that hands over its own live set still gets a new gate recognised; and a decisive-looking host answer note does not clear the record for the very card the package's own responder already judged non-decisive (the label was not on that card). A successful reopen replaces only the record taken before the card was minted (with an on-screen marker, not a deletion), a decisive answer clears it, the per-session ledger is bounded, and a session change clears its bucket. Without `trigger` the reopen answers exactly as before, apart from joining a reopen already on its way. A reopen for a session with no view subscribed to question frames (`onQuestionFrameFor(sessionKey, handler)`; `onQuestionFrame` for the default session) refuses with `{ reopened: false, _sema_noPresentationSurface: true }` — in the synchronous and the receipt form alike, per session key, and also when the view goes away while the asynchronous check is reading — and that answer comes before the user-closed check, at the entry and after the asynchronous read alike, whether or not the read proved a newer gate (after the read, a decision handed over while the read was in progress, and a dismissal record that was replaced or cleared meanwhile, still take precedence over the missing view); a host log or probe port that throws changes none of the reopen answers (the suite runs the same arrangement with a quiet and with a throwing log port and requires the same answer and the same card frames); every other refusal (receipt window lapsed, decision in flight, user-closed card, non-default slot without a delivery path, record gone) keeps its own keys without the flag. The first-presentation call does not check for a view: without one it still answers `true` and the card frame goes nowhere, which the suite records as today's behaviour, not as a guarantee. A free-text answer on the plan card (no option picked, a non-blank note) counts as a decisive answer: it clears the record and delivers exactly one rejection; a blank note, or a contradictory multi-select that carries a note, does not. |
@@ -470,7 +470,7 @@ guard still cross-checks the table by name).
470
470
  | `scripts/run-detach-durable-off-verdict-test.mjs` | The verdict behind the durable-off hint, now a public entry point without the process-level gate. `isDetachDurableOff400(err)` answers whether an error is the engine's refusal of the detach-on-disconnect header on a deployment with no durable run ledger: status 400 and the engine's own refusal sentence in the message, and nothing else — in particular not the error code that refusal carries, because the engine uses the same generic precondition code for unrelated refusals on the same submit path, and treating those as this one would silently resubmit a turn without the header. Browser and desktop hosts, which never arm the terminal's detach ledger, can now ask the same question instead of keeping their own copy. The guard pins the positive case on the real sentence (with or without the code, with surrounding text, and on the error the SDK actually throws when a stubbed engine answers 400), the negative case on the engine's other refusals that share the code (verbatim, and again through the SDK), non-400 statuses, and eighteen malformed inputs that must answer `false` without throwing. It shows the verdict does not read the process ledger and that the hint still returns nothing before the header has been sent. The verdict never throws: an error object whose `status` or `message` getter throws, a proxy whose trap throws and a revoked proxy all answer `false`, each property is read at most once (and `message` not at all unless the status is 400), and the hint therefore answers nothing for those objects instead of throwing as it did before. Against a frozen copy of the previous hint, every other input gives the same answer byte for byte, armed or not. When the engine's source tree is available, it also checks that exactly one refusal site with that code carries the sentence and that every other one is answered `false` |
471
471
  | `scripts/run-fixture-flat-done-ratchet-test.mjs` | Test fixtures that feed the engine's terminal `done` frame are kept in the shape the live engine actually sends. Since the terminal record became a tagged cause (`result.terminal`), the older flat shape (`result.status` plus loose keys) reaches a client only in two ways: a stored row replayed verbatim from before the upgrade, and the server's own refusal envelope — so a fixture written in the flat shape tests the replay path while claiming to test the live one. The suite finds every `{ type: 'done', result }` literal under `scripts/`, follows `result` to the object literal it really comes from (in place, through a variable, through a helper's parameter at each of its call sites, through a spread, or through the rows of an iterated array) and counts the flat ones that carry no one-line note saying they model a replayed row or a refusal envelope. That count may only go down: it is a ceiling kept in the ratchet registry, and a count under the ceiling prints a step-down line instead of passing in silence. A planted corpus with a known number of flat, noted and cause-shaped frames in each of those forms must be counted exactly, and a note that sits inside a string or gives no reason does not count |
472
472
  | `scripts/run-authority-envelope-mirror-test.mjs` | The authority-envelope tags that a peer's message body is defused against before it is written into a transcript line. Tags the engine uses to speak with its own authority (reminders, completion notices and the like) must never survive inside a body a peer wrote, or a forged completion notice could be read back on resume as a real one and poison the dedup ledger. The package's list is generated at build time from the engine package's public entry point, and the guard checks that the source binds that generated list rather than a hand-written copy and that it matches the installed engine package in both directions: a tag the engine treats as authority and the package lacks is red, and so is a tag the package defuses that the engine does not, since that rewrites ordinary text in a peer's message. It also confirms the published list is the engine module's own value and is still derived from the engine's envelope registry, which it reads from the engine's module by path because the registry itself is not published. At the rendered output, every engine authority tag placed in a body (opening, closing, with attributes, upper case) comes out defused on all three peer lanes, and the engine's non-authority envelope tags come out untouched |
473
- | `scripts/run-upstream-tables-test.mjs` | Engine facts the package may not import at run time (the notice code register and its audience table, the reasons an injected MCP server is dropped, the protocol namespace prefixes and the retired tool names with their current names) are generated at build time from the installed engine package's own public entry point into plain literal modules, which are committed. The generated files must be byte-identical to what the installed engine produces today, no other file may sit beside them, each names the generator and the engine version it came from, none imports anything, and the package's public names are the generated objects themselves. The generator refuses rather than guesses: a table that is missing or empty, a notice code without an audience row (or the reverse), a third audience value, or a retirement note in a shape it does not know all stop generation instead of producing an empty or partial table, and it reads only the engine package installed in this package's own dependency folder. Two word lists the SDK already exports as values (who asked a question, and which word a mandated approval stands on) are now the SDK's own arrays rather than copies; because those arrays are not frozen, the package's own decisions are shown not to change when they are modified in place. Inside the package, the thinking-level and permission-mode vocabularies, the tier words, the rejection sentence and the cancelled code each exist exactly once in the source. A second batch covers engine fact tables that newer engine packages publish from their entry point (the notice cause and source lists, the run-ending reasons, the reserved control-verb names, the workflow size caps, the authority envelope tags, the denial layers, the reasons a requested permission mode did not take effect, and the structured tool-card types): each is generated the same way and bound where it is used, no hand-written copy of any generated table may remain in the source, and an engine package too old to publish them makes the generator stop and name every missing export rather than fall back to reading private files. A third batch covers the closed set of codes the engine refuses a declared caller tool with: it is generated the same way and folded into the package's configuration-refusal recognition set, which must contain every one of those codes; the set only recognises them (no code path branches on them), an unexpected code shape stops generation, and an engine package that does not yet publish the set makes the generator stop and name it. A fourth batch (from 0.88.0) covers the closed set of reasons a permission mode's release of a question may carry: it is generated the same way, the package's public word list is that generated object, no hand-written copy of its words remains in the source, and an engine package that does not yet publish it makes the generator stop and name it. A sixth batch (from 0.90.0) covers the table of tool-name aliases that hook matchers resolve (the alias spelling of a built-in tool to its canonical name): the engine publishes no ready-made table, so the generator expands the aliases declared on each row of the engine's static tool catalog, and the package reads the result through two public functions (the table itself is not exported). Those functions must agree name by name with the engine's own name resolution, leave every name outside the table untouched (empty strings and inherited object keys included), never fold case or whitespace, and never throw. The generator stops rather than writing an empty table when the catalog declares no alias at all, when a catalog row has an unexpected shape, when an alias collides with another row's canonical name, or when an alias is an integer-like string (object keys of that shape would be reordered), and a run of it against a catalog without aliases exits non-zero and leaves every generated file byte-identical; the generated table is built from key-value pairs and is executed in the check against the engine's own resolver, so an alias spelled like an inherited object key still lands as an own entry The generator also produces five closed sets that the engine exports from its package root — the reasons a rule's evidence can be absent in an approval question, the lanes of the system injection queue, the full list of built-in read-deny tiers, the tiers active by default, and the per-class arbitration switches — as frozen literal tables that the package's own root entry hands out as the very same objects. The check compares them item by item and in order with the engine (the arbitration rows bit by bit, with the key set compared to the engine's own list of ask classes, and the tier lists with the engine's own tier resolver), refuses to generate when a name is missing, a list is empty or has duplicates or non-strings, a default tier is not in the full list, or an arbitration row has a missing, extra or non-boolean switch, and a run of the generator against an engine without one of them exits non-zero and leaves every generated file byte-identical. The reader that answers whether a standing answer may settle an ask reads only the generated arbitration table: its answers follow a table with one switch flipped, a class added or a class removed, the source holds no class name literal, and it treats a throwing getter, a prototype-only field or an unknown class as unknown rather than as permission. The engine code string for a self-contradictory request body appears as a string literal exactly once in the source; the three refusal readers that use it keep their own criteria. From 0.92.0 two more batches come from the engine's package root — four closed sets that stay inside the build, and nine facts the package used to copy by hand (settlement words, classifier refusal causes, the auto-mode unavailable causes, tier order and the familiar tier aliases, the background agent handle shape, the sender permission classes, MCP liveness words and the three rule behaviors) — and a third source is read: the settings contract package, from which the engine-owned hook events, the hook events whose matcher reads a tool name, the known-secret literal shape, the tier group name shape, the budget keys grouped by value type and the model entry interface words are generated into their own file. Each former copy gets a check that the public name is the generated object itself, or, for module-private constants, that the initialiser is the imported generated identifier; regex and alias-table copies are scanned for alongside array copies; and malformed upstream shapes, extra members and missing names each make the generator refuse. Two numeric limits are generated as well (the largest inline image in base64 characters, and the longest follow-up text): the same value as the engine's export, never zero, absent or malformed, not on the root entry, read by the human-submission assembly through the generated constants with no hand-written copy left there. The closed set of causes by which a deployment hook can keep a prompt from being submitted is generated as well: the same words in the same order as the engine's export, frozen, a literal tuple type, exported at the root as the generated object itself, agreed with by the engine's own membership test, and with no copy of its words in the reader that uses it; an extra member changes the generated output, while a malformed shape or a missing name makes the generator refuse. Since 0.94.0 the generator also derives the request-side permission-mode words and the user-input lane table (lane to notice code and count key); the package-side fold of the arbitration table takes an optional question face, reads the releasable classes per face from the generated ask-face tables only, and is diffed against the engine's own fold on every face. From 0.94.0 the settings-contract batch also generates the private-key header definition, which must be unanchored and judges the same samples as the contract's own check; a missing, anchored or stateful definition makes the generator refuse, and the display module's header constant is the generated object itself. Since 0.94.0 it also generates the ask-face tables (the face words, the tool names on the shell face derived from the engine's own face function over its catalog, and the releasable classes per face), checks them against the engine item by item and against the arbitration table, and refuses to generate on every malformed shape or missing export. From 0.95.0 a settings-contract function batch copies three pure functions from the contract package's entry point (the narrow loopback check used before sending no credentials, the hierarchical URL check, and the @-mention narrowing) by their own source text: the generator refuses when a function reaches for any name outside itself except an allowed global, the copies must answer exactly like the originals on independent samples, and the package's readers use the copies instead of their own versions. The configuration-refusal registry has no read point inside the package (the only reading of "is this a configuration refusal" is the prefix predicate), and its own comment says so. Since 0.96.0 the approval mandate word list is generated from the engine package root too, followed by the one word older supported engines still send. Since 0.96.0 the settings contract's hook-matcher reading (`hookMatcherSelects` / `hookMatcherForm`, with the two private declarations they depend on copied verbatim from the installed module) is generated the same way and must answer exactly as the installed contract on an independent sample set; generation is refused when the module touches those private declarations anywhere else (modified at load time, reassigned, or exported), because then the copied text would not be what runs upstream. Since 0.99.0 it also covers three more generated groups: the request-attachment seat description, its three closed sets and two limits match the installed engine entry for entry, stay frozen and off the root entry, and have no readers outside the generated directory; the interface-language value set is generated and the public UI_LANGUAGES is that same array; and the hooks-configuration classifier is copied with its private closure into its own generated file (the only file in src exempt from type checking), bound to the generated hook-event table and re-exported with the settings contract's own declared signature, answering exactly like the installed original on two independent sample sets. The types in that signature, with everything they depend on, are copied declaration by declaration from the contract's declaration files into a generated type mirror, so no source file and no shipped declaration file references the development-only contract package; a compile check inside the guard proves every mirrored type and the installed one are assignable to each other in both directions, and two planted edits, each breaking one direction only, are shown to fail in exactly that direction. The generated files also cover the contract package's request-plane MCP entry judges (seven exports plus the helpers they depend on, copied as one closure whose two cross-module dependencies are factory parameters bound to the generated word list and prefix guard) and the engine's ordered list of ask classes, which the ask-class pair reader now reads instead of borrowing the arbitration table's key order; independent closure derivation, byte-for-byte declarations, same-answer pools, an empty-context binding witness and refusal controls guard them. The URL-pattern admit function used for the managed http-hook list is copied the same way, with an independently built same-answer pool. So are the layer-admission judge that decides which settings layers' lists count, the trust-ladder and member-admission functions and the contract table it reads (chained through bound factory parameters, the ladder's word list injected from a generated table), and the text-echo helper and line-strip set the request-plane judges depend on (the helper is an unexported leaf upstream, so it is checked against a public reader that echoes through it); a regression to the previous shape, in which the ladder's closure reached a schema enum, is shown to be refused. Since 0.101.0 the generator also writes the engine's closed list of approval card words as a generated table (the member set of the approval card reader) and refuses a missing, empty, duplicated or non-string list; the check pins that the reader takes its members from that table and that no source file outside the generated directory repeats the list. |
473
+ | `scripts/run-upstream-tables-test.mjs` | Engine facts the package may not import at run time (the notice code register and its audience table, the reasons an injected MCP server is dropped, the protocol namespace prefixes and the retired tool names with their current names) are generated at build time from the installed engine package's own public entry point into plain literal modules, which are committed. The generated files must be byte-identical to what the installed engine produces today, no other file may sit beside them, each names the generator and the engine version it came from, none imports anything, and the package's public names are the generated objects themselves. The generator refuses rather than guesses: a table that is missing or empty, a notice code without an audience row (or the reverse), a third audience value, or a retirement note in a shape it does not know all stop generation instead of producing an empty or partial table, and it reads only the engine package installed in this package's own dependency folder. Two word lists the SDK already exports as values (who asked a question, and which word a mandated approval stands on) are now the SDK's own arrays rather than copies; because those arrays are not frozen, the package's own decisions are shown not to change when they are modified in place. Inside the package, the thinking-level and permission-mode vocabularies, the tier words, the rejection sentence and the cancelled code each exist exactly once in the source. A second batch covers engine fact tables that newer engine packages publish from their entry point (the notice cause and source lists, the run-ending reasons, the reserved control-verb names, the workflow size caps, the authority envelope tags, the denial layers, the reasons a requested permission mode did not take effect, and the structured tool-card types): each is generated the same way and bound where it is used, no hand-written copy of any generated table may remain in the source, and an engine package too old to publish them makes the generator stop and name every missing export rather than fall back to reading private files. A third batch covers the closed set of codes the engine refuses a declared caller tool with: it is generated the same way and folded into the package's configuration-refusal recognition set, which must contain every one of those codes; the set only recognises them (no code path branches on them), an unexpected code shape stops generation, and an engine package that does not yet publish the set makes the generator stop and name it. A fourth batch (from 0.88.0) covers the closed set of reasons a permission mode's release of a question may carry: it is generated the same way, the package's public word list is that generated object, no hand-written copy of its words remains in the source, and an engine package that does not yet publish it makes the generator stop and name it. A sixth batch (from 0.90.0) covers the table of tool-name aliases that hook matchers resolve (the alias spelling of a built-in tool to its canonical name): the engine publishes no ready-made table, so the generator expands the aliases declared on each row of the engine's static tool catalog, and the package reads the result through two public functions (the table itself is not exported). Those functions must agree name by name with the engine's own name resolution, leave every name outside the table untouched (empty strings and inherited object keys included), never fold case or whitespace, and never throw. The generator stops rather than writing an empty table when the catalog declares no alias at all, when a catalog row has an unexpected shape, when an alias collides with another row's canonical name, or when an alias is an integer-like string (object keys of that shape would be reordered), and a run of it against a catalog without aliases exits non-zero and leaves every generated file byte-identical; the generated table is built from key-value pairs and is executed in the check against the engine's own resolver, so an alias spelled like an inherited object key still lands as an own entry The generator also produces five closed sets that the engine exports from its package root — the reasons a rule's evidence can be absent in an approval question, the lanes of the system injection queue, the full list of built-in read-deny tiers, the tiers active by default, and the per-class arbitration switches — as frozen literal tables that the package's own root entry hands out as the very same objects. The check compares them item by item and in order with the engine (the arbitration rows bit by bit, with the key set compared to the engine's own list of ask classes, and the tier lists with the engine's own tier resolver), refuses to generate when a name is missing, a list is empty or has duplicates or non-strings, a default tier is not in the full list, or an arbitration row has a missing, extra or non-boolean switch, and a run of the generator against an engine without one of them exits non-zero and leaves every generated file byte-identical. The reader that answers whether a standing answer may settle an ask reads only the generated arbitration table: its answers follow a table with one switch flipped, a class added or a class removed, the source holds no class name literal, and it treats a throwing getter, a prototype-only field or an unknown class as unknown rather than as permission. The engine code string for a self-contradictory request body appears as a string literal exactly once in the source; the three refusal readers that use it keep their own criteria. From 0.92.0 two more batches come from the engine's package root — four closed sets that stay inside the build, and nine facts the package used to copy by hand (settlement words, classifier refusal causes, the auto-mode unavailable causes, tier order and the familiar tier aliases, the background agent handle shape, the sender permission classes, MCP liveness words and the three rule behaviors) — and a third source is read: the settings contract package, from which the engine-owned hook events, the hook events whose matcher reads a tool name, the known-secret literal shape, the tier group name shape, the budget keys grouped by value type and the model entry interface words are generated into their own file. Each former copy gets a check that the public name is the generated object itself, or, for module-private constants, that the initialiser is the imported generated identifier; regex and alias-table copies are scanned for alongside array copies; and malformed upstream shapes, extra members and missing names each make the generator refuse. Two numeric limits are generated as well (the largest inline image in base64 characters, and the longest follow-up text): the same value as the engine's export, never zero, absent or malformed, not on the root entry, read by the human-submission assembly through the generated constants with no hand-written copy left there. The closed set of causes by which a deployment hook can keep a prompt from being submitted is generated as well: the same words in the same order as the engine's export, frozen, a literal tuple type, exported at the root as the generated object itself, agreed with by the engine's own membership test, and with no copy of its words in the reader that uses it; an extra member changes the generated output, while a malformed shape or a missing name makes the generator refuse. Since 0.94.0 the generator also derives the request-side permission-mode words and the user-input lane table (lane to notice code and count key); the package-side fold of the arbitration table takes an optional question face, reads the releasable classes per face from the generated ask-face tables only, and is diffed against the engine's own fold on every face. From 0.94.0 the settings-contract batch also generates the private-key header definition, which must be unanchored and judges the same samples as the contract's own check; a missing, anchored or stateful definition makes the generator refuse, and the display module's header constant is the generated object itself. Since 0.94.0 it also generates the ask-face tables (the face words, the tool names on the shell face derived from the engine's own face function over its catalog, and the releasable classes per face), checks them against the engine item by item and against the arbitration table, and refuses to generate on every malformed shape or missing export. From 0.95.0 a settings-contract function batch copies three pure functions from the contract package's entry point (the narrow loopback check used before sending no credentials, the hierarchical URL check, and the @-mention narrowing) by their own source text: the generator refuses when a function reaches for any name outside itself except an allowed global, the copies must answer exactly like the originals on independent samples, and the package's readers use the copies instead of their own versions. The configuration-refusal registry has no read point inside the package (the only reading of "is this a configuration refusal" is the prefix predicate), and its own comment says so. Since 0.96.0 the approval mandate word list is generated from the engine package root too, followed by the one word older supported engines still send. Since 0.96.0 the settings contract's hook-matcher reading (`hookMatcherSelects` / `hookMatcherForm`, with the two private declarations they depend on copied verbatim from the installed module) is generated the same way and must answer exactly as the installed contract on an independent sample set; generation is refused when the module touches those private declarations anywhere else (modified at load time, reassigned, or exported), because then the copied text would not be what runs upstream. Since 0.99.0 it also covers three more generated groups: the request-attachment seat description, its three closed sets and two limits match the installed engine entry for entry, stay frozen and off the root entry, and have no readers outside the generated directory; the interface-language value set is generated and the public UI_LANGUAGES is that same array; and the hooks-configuration classifier is copied with its private closure into its own generated file (the only file in src exempt from type checking), bound to the generated hook-event table and re-exported with the settings contract's own declared signature, answering exactly like the installed original on two independent sample sets. The types in that signature, with everything they depend on, are copied declaration by declaration from the contract's declaration files into a generated type mirror, so no source file and no shipped declaration file references the development-only contract package; a compile check inside the guard proves every mirrored type and the installed one are assignable to each other in both directions, and two planted edits, each breaking one direction only, are shown to fail in exactly that direction. The generated files also cover the contract package's request-plane MCP entry judges (seven exports plus the helpers they depend on, copied as one closure whose two cross-module dependencies are factory parameters bound to the generated word list and prefix guard) and the engine's ordered list of ask classes, which the ask-class pair reader now reads instead of borrowing the arbitration table's key order; independent closure derivation, byte-for-byte declarations, same-answer pools, an empty-context binding witness and refusal controls guard them. The URL-pattern admit function used for the managed http-hook list is copied the same way, with an independently built same-answer pool. So are the layer-admission judge that decides which settings layers' lists count, the trust-ladder and member-admission functions and the contract table it reads (chained through bound factory parameters, the ladder's word list injected from a generated table), and the text-echo helper and line-strip set the request-plane judges depend on (the helper is an unexported leaf upstream, so it is checked against a public reader that echoes through it); a regression to the previous shape, in which the ladder's closure reached a schema enum, is shown to be refused. Since 0.101.0 the generator also writes the engine's closed list of approval card words as a generated table (the member set of the approval card reader) and refuses a missing, empty, duplicated or non-string list; the check pins that the reader takes its members from that table and that no source file outside the generated directory repeats the list. It also derives the ask classes the engine stamps the real-approval bit on (the engine's explaining classes plus the class its own classifier gives a bare bit), the hook lane's class column, and refuses a malformed or missing source rather than writing a table by hand. |
474
474
  | `scripts/run-retirement-ledger-test.mjs` | Transitional code whose retirement condition used to live only in a comment now has a machine-readable trigger. `scripts/retirement-ledger.json` lists each piece (a compatibility read, a mirrored table, a normalizer inside the differential check) with an anchor into its own code, the retirement condition quoted from the source comment, and one of three trigger kinds: this package's version reaching a retire-by version; the installed upstream package reaching a version, exporting a name from its entry, or declaring a member on a given type (read with the TypeScript checker); this package's declared support floor (`scripts/support-floor.json`) for the server, the engine or the SDK reaching a given version (an unreadable or malformed declaration fails the check instead of being skipped); or the reference client tree used by the differential check having moved to a given version of this package. The check fails when a trigger has fired and the transitional code is still there, when a row's anchor can no longer be found (the code is gone but the row stayed), when the quoted condition no longer matches the source comment, when a row has no machine trigger at all, when a retire-by version sits more than three minor lines ahead, and when the anchor of an already retired piece reappears. A reading that cannot be taken is reported as skipped when the absence is legitimate (no reference tree on disk, an upstream declared only as a peer and not installed) and is a harness fault otherwise. Before giving any verdict the judge proves itself on in-memory fixtures and on one known-present and one known-absent name in the installed upstream types. Rows already moved to the retired list may also carry the names, keys and function arities that the retirement removed; for those rows the check confirms the names are absent from the package root entry (as exports and as runtime keys), from the built output, and from every check script's code positions (comments excluded), that each retired function has its reduced parameter count (taken from every type signature as well as the runtime length, so an added optional, default or rest parameter is seen), and that a compile-time pin on the root entry's keys holds in both directions — no retired name present, every known live name still present, with a planted live name that must fail. Rows may carry a structured blockedOn (upstream ticket + landing criterion + a ruling id cited in the ruling text): a due row with it prints as BLOCKED-DUE instead of failing, capped at 3 such rows. A blockedOn may name a calendar date instead of a ticket (for a condition that is itself a date): a due row then prints as BLOCKED-DUE only while today is before that date, and fails once the date has passed or today cannot be read. |
475
475
  | `scripts/run-plan-review-decision-ledger-test.mjs` | The plan-review reopen path refuses on one decision ledger shared by every delivery path, not only the package's own POST window: a decision the host reports as handed over (before the card's answer reaches the package, or on the host's own delivery path) counts as in flight until it settles; a pending-row read that started before a decision settled is a stale snapshot of the gate just answered, so a reopen carrying that read mark refuses; and a decision proven not to have taken effect (nothing sent, a refusal that by contract changed nothing, or the engine proven to still hold the very same gate instance — a status alone does not prove it, since a run that moved on to a new plan gate reports the same status) neither counts as in flight nor makes such a snapshot stale, so a failed decision does not consume a legitimate reopen. The ledger's capacity bound never evicts the package's own in-flight decisions, so a duplicate approval stays latched however many decisions are in flight. The package's own in-flight window now runs from the POST to the moment the outcome is emitted (settlement happens before the outcome is queued, so a read the host starts from the queue port is not stale), while the duplicate-delivery latch keeps its POST-only window. A handed-over decision the package's delivery path then sends is the same ledger entry (the host's early settle does not release it); two decisions on one run each settle on their own; positive evidence that the run left the gate settles what is open without releasing the latch; bad inputs neither throw nor record. The ledger is checked against the terminal's current implementation vector by vector when that tree is on disk. Positive evidence that the run left the gate can arrive after the host has handed a decision over and before the package's delivery path picks it up (while the host is still waiting on its gate snapshot, or in the microtasks between that wait ending and the delivery call); the delivery path then still picks up that same entry instead of minting a new one, so the latest decision mark the host keyed its snapshot and its not-applied notice on does not move, the duplicate-delivery latch and the capacity rule hold for it, and the three failure outcomes read byte for byte as they would without the hand-over. Only an entry settled by such evidence is picked up again: one the host settled itself, one it revoked, and one that already produced an outcome are not, and a new decision gets a new mark |
476
476
  | `scripts/run-session-background-stop-test.mjs` | The session-level stop for background work (`stopEngineSessionBackground`): one call asks the engine to stop the background tasks of a session, with `includeRetained` passed through as the required true / false choice. It resolves its client through the same single construction point as the other engine verbs (a host-supplied client, or the installed engine target for the chosen session slot) and sends nothing unless the engine has declared the stop face (`background.exitFaces` strictly `true`) under the capability key that applies: evidence that is missing or unread, an older engine without the face, a malformed flag, a flag found only on the prototype chain, or evidence recorded for a different deployment all end in `unavailable` with zero requests. The request body on the wire is exactly `{"includeRetained": <boolean>}` whatever object the caller passes. A 200 answer returns the receipts verbatim (identifier and outcome word; a row whose word it does not recognise is kept with that word as it is, so the host can still name it and the companion below sorts it as may still be running), counts rows whose identifier or word it cannot read instead of inventing them, and leaves the receipt list absent rather than empty when the body cannot be read. Refusals are sorted by machine code before HTTP status (missing service credential, no ownership face, unknown session versus missing route on the same 404, request shape, not permitted, rate limited with the server's wait hint, anything else), a server failure is an `error`, and each call sends exactly one request even through a client configured to retry. A request that may have reached the engine but got no answer — a time limit, a connection that dropped after the request went out, or a cancellation by the caller after the request went out — carries `unconfirmed: true`, while a real answer, a connection that never opened, or a cancellation already in place before the call (nothing is sent) does not. Malformed arguments, options, connections, clients, capability records, answer bodies and error objects never throw and never leave an unhandled rejection, and a planted token never reaches a log line or a result. A pure companion (`engineSessionBackgroundReceiptReadingOf`, with the frozen state list `ENGINE_SESSION_BACKGROUND_RECEIPT_STATES` derived from its wording table) sorts each receipt word into stopped, still running (kept on purpose) or may still be running, with one fixed sentence per state so every client says the same thing: each of the four words lands in its state, while any word it does not recognise — a future word, a different letter case, stray whitespace, a near spelling, a prototype-chain name — lands in may still be running with the word carried back verbatim, and non-string input (boxed strings and objects with their own `toString` included) lands there too without being coerced and without throwing; it never reports stopped for anything it cannot confirm. The three sentences differ, the two that are not stopped never use the word stop, and the state list and the states actually produced match in both directions. |
@@ -525,7 +525,7 @@ guard still cross-checks the table by name).
525
525
  | `scripts/run-park-expiry-judgements-test.mjs` | The judgements the approval flow needs once a streamed approval's window has closed and the engine has moved the call into its durable queue instead of refusing it. `classifyApprovalCompleteFrame` reads a closing frame as one of three outcomes (moved to the queue, no queue evidence so retract the card, decided by a person) or none for anything it does not recognise; only a literal `true` counts as queue evidence, and a new outcome word is never mistaken for an expiry. `parkRowDeadlineState` reads a durable row's `deadline` into none, pending or past, treating a missing, null or absurd value as no deadline rather than clamping it. `lateDecideRoute` picks the endpoint for a late decision (the durable decide endpoint only when the window had closed and the row was actually found). `runReapedByDeadline` reads the run record and answers true only for the reaped-by-deadline error code; anything unreadable is false, so an unreadable record is never reported as a step that was not run. The guard carries the terminal client's offline cases over one by one and runs a frozen copy of that client's original implementations side by side over more than fifteen hundred inputs; the registered differences, both on the conservative side, are that a host client whose property read throws is answered with false instead of a rejection, and that a park bit sitting on the prototype chain or behind an accessor is not read as moved-to-the-queue (it is read through the same single reader the desktop seat uses, which only accepts own data properties). |
526
526
  | `scripts/run-usage-analytics-projection-test.mjs` | Reads of the engine's usage analytics (`GET /v1/usage/summary`, `/series` and `/breakdown`) and the card view built from them. Each reader never throws and answers ok, unavailable (the engine says it has no such face: no run store, no such route, or the feature is switched off) or failed (any other read failure, which is never reported as the backend lacking the feature). A missing number is simply absent from the view, never zero, and a total counts as exact only when the engine says `estimated: false`: otherwise it is an estimate and the view says so — a lower bound for money, an approximation for tokens, each with a shared prefix for the host to render. The card view has three states: available, estimated, and unavailable with one sentence for a backend that has no analytics and a different one for a read that failed; an unavailable card carries no numbers at all. The heat map and the active-day count exist only when the day series was both read and read completely, so a failed read, a series with unreadable points or invalid dates, or one the engine reports as truncated is never drawn as a grid of zero-activity days; the peak hour and the favourite model follow the same completeness rule, and a failed per-model breakdown is different from a window with no model usage. A point's timestamp is read once for both the day and the hour series: the same date-and-time grammar, a real calendar date, an explicit time zone for date-time forms (offsets are converted to UTC first), and a point that cannot be read is dropped and counted as unreadable in either series. The guard also runs a real SDK client against a stubbed fetch and checks the four requests it makes. |
527
527
  | `scripts/run-interactive-interrupt-emitter-test.mjs` | The interactive interrupt sender (`fireInteractiveInterrupt`): one bare `POST /v1/runs/:id/interrupt` through the client's runs resource, with the method called on its receiver, the session parameter sent only when non-empty and no idempotency key, so exactly one request goes out. Every outcome is a value, never a throw: a receipt yields `halted` with `turnCut` read only from an own boolean data property; a thrown error is classified by the same at-most-once judge the steer and cancel paths use, so a definite refusal becomes `refused` with its status and code, while a 5xx, a network failure or an abort becomes `transport` (no verdict). A missing resource yields `unarmed` with zero requests. The suite cross-checks every thrown sample against the shared judge, drives a real SDK client with a stubbed fetch and feeds the outcomes to `planInteractiveHalt`. |
528
- | `scripts/run-device-rebind-judgement-test.mjs` | The device rebind flow (`listDevices`, `readSessionBinding`, `rebindSession`, `reconcileRebindOutcome`, `rebindProvesRequestNotApplied`): each verb goes through the client's devices resource and returns a discriminated outcome, never a throw. Refusal codes are matched as status-and-code pairs from one closed table (device lane absent, run store absent, not found, active run, revision conflict, service token required); any other failure of the rebind write is classified by the shared at-most-once judge into `refused` or `unconfirmed`. A rebind success is accepted only when the echoed session, the target device and the revision step all hold, and the workspace carryover disclosure is carried only for the literal `none`. Response fields are read as own data properties. Includes a real SDK client with a stubbed fetch and a full grid of the read-back reconciliation. The eleven device row types from the SDK (`DeviceSummary`, `DeviceBinding`, and the rest) are re-exported as types from the package root, and the guard checks each one is identical to the SDK's. |
528
+ | `scripts/run-device-rebind-judgement-test.mjs` | The device rebind flow (`listDevices`, `readSessionBinding`, `rebindSession`, `reconcileRebindOutcome`, `rebindProvesRequestNotApplied`): each verb goes through the client's devices resource (the binding read goes through the sessions resource: the execution placement read, a writer on the device lane with a readable device is the only prior state, an unknown device on that lane is unreadable rather than unbound, and an older server without the read is unsupported) and returns a discriminated outcome, never a throw. Refusal codes are matched as status-and-code pairs from one closed table (device lane absent, run store absent, not found, active run, revision conflict, service token required); any other failure of the rebind write is classified by the shared at-most-once judge into `refused` or `unconfirmed`. A rebind success is accepted only when the echoed session, the target device and the revision step all hold, and the workspace carryover disclosure is carried only for the literal `none`. Response fields are read as own data properties. Includes a real SDK client with a stubbed fetch and a full grid of the read-back reconciliation. The nine device row types from the SDK (`DeviceSummary`, `DeviceRebindResult`, and the rest) are re-exported as types from the package root, and the guard checks each one is identical to the SDK's; the two binding-read types the SDK removed are no longer exported. |
529
529
  | `scripts/run-engine-env-sourcing-test.mjs` | The local-engine environment group sourcing judge (`resolveEngineEnvSourcing`, `engineEnvView`): when a settings source wrote half of an all-or-nothing group and that half is ignored, the group is either skipped (its own switches are dropped) or, for safety groups and groups declared not skippable, left for the engine to refuse with an explanation. Group declarations, the ignore rule and the alias table come from the host; the module holds no retired-name table and reads no platform state. The suite runs a seeded differential test against a frozen transcription of the previous host-side loop, fixed cases for alias precedence, source precedence and case folding, and scans the built module for every retired environment name in the settings contract. |
530
530
  | `scripts/run-engine-caps-single-source-test.mjs` | Engine capability reads have one store: the shared capabilities snapshot. The subagent tail, subagent report and manual-compact paths read it (probing once through it when nothing has landed, with a bounded wait) instead of keeping private per-address tables, so a capability follows `invalidateEngineCaps`; a connection without an identity key is probed once and nothing is stored. The suite checks the internal read path, the three migrated entry points against a host-kicked snapshot and across invalidation, an AST scan that every capabilities call in the source is a probe handed to the shared cache (no exceptions: the custom-agent capability verdict reads the same snapshot, follows invalidation at the same address and re-prepares on a new capability generation), single readers for the task-handle capability and the agent-registry list face, a center-wiring reading from the cache that never carries the per-caller lease, and the retirement of the store-based two-generation readings and of the second argument of `toolsRunHereFromExecutionLane`. |
531
531
  | `scripts/run-mcp-engine-leg-connection-test.mjs` | The engine-hosted MCP connection reader (`mcpEngineLegConnectionOf`): whether a server is connected on the engine's leg is read only from that leg's `wiring_manifest.mcp[]` status, never from the tool roster or a client's own connection. The suite checks the three states and their causes (connected with an optional tool count, not connected because it failed, was skipped or is not listed, and unknown because the section was not received, the name matches several rows, the status is unreadable or the word is not recognised), that the row lookup is shared with the liveness reader, that the liveness record is not read, and the same answers after the package's own section projector, including rows the projector drops because their status is unreadable (counted by name on the projected array, invisible to JSON and key enumeration). |
@@ -535,7 +535,7 @@ guard still cross-checks the table by name).
535
535
  | `scripts/run-rewind-engine-action-test.mjs` | The two pure judgements the terminal client used to keep for its rewind command. The dispatch gate says whether, right now, the engine has explicitly said that a given rewind mode cannot be done: each of the three modes (rewind with code restore, rewind the conversation only, restore code only) reads the same availability judges the package already ships for the menu, only an explicit off refuses, and a missing, unreadable or unobserved reading never refuses. The action judgement takes the chosen option, the target position the host has already resolved (session root, after the root, or undecidable, with the engine entry handle if there is one) and the capability reading, and answers whether to arm the one-shot rewind keys, to reopen the engine session (the two modes that carry a resume point, chosen at the session root, which the server will not rewind past) or to do nothing, with a reason; an undecidable position never acts in either direction, and the code-only mode looks only at the handle. The preview used by the confirmation screen is the same answer's kind. The guard checks both judgements against hand-written truth tables over every bit combination and every target shape, that the dispatch gate and the menu can never disagree, that the default reading follows the current engine target, that hostile inputs never throw and never act, and that the module keeps no mode list or resume-point list of its own. A live comparison against the terminal client's own copies runs in the layering guard. |
536
536
  | `scripts/run-tool-result-content-projection-test.mjs` | How a tool's result body becomes the content of the tool result in the transcript. A result that carries images keeps them: each image whose data is a string and whose type is supported (JPEG, PNG, GIF or WebP) appears as an image block with exactly the bytes the engine sent (the data is not re-validated, matching the reference client), in order, in both the interactive and the print output, while the structured result next to it stays the same text as before. A block that cannot be shown (an image without string data or with an unsupported type, or a block of an unknown kind) is left out and the content ends with one notice saying that unsupported content was omitted, so nothing disappears silently. Plain text and text-only results are unchanged strings, a body the engine already shortened stays a string with the truncation note, and an error result's content is always text. The text readings of a result body (the flattening helpers and the subagent viewer's result slot) come from the same projection and agree with each other. That projection is also the one text reading behind the public `toolEndOutputText` (an empty block list reads as an empty string, an image as `[image]`, an object that is not a string or a block list as absent) and behind the classifier-denial and aborted-call checks, so a frame that carries an image or an unreadable block is never mistaken for one of those engine-minted bodies, and a classifier denial is recognised only when it arrives as a bare string or a single block (a signature split across blocks is not a denial). Whether a result is an error is decided once for both the interactive and the print output: a structured result reporting a failing exit code is an error in both, whatever the tool's name and even when the result carries no body. The task-output tool's content is the model-facing text in both outputs, with the structured task record kept beside it. A result block that reports a media item as not delivered (a descriptor with the media type, an optional MIME type and the decoded byte count, or a summary block with the number of folded blocks and their total bytes) is shown as one sentence at its own position instead of being swallowed by the single unsupported-content notice, and a block whose fields are malformed stays an unreadable block. The same judgement is public: `toolOutputMediaAbsenceOf` reads such a block and `toolOutputMediaAbsenceSentence` gives its sentence in English or Chinese. |
537
537
  | `scripts/run-refused-park-family-test.mjs` | The refused-resume family (`refusedParkFromError`, `refusedParkContent`, `refusedParkBlockedContent`): three 409 codes whose refusal comes back every time the same decision is sent again (the stored park is not supported by this engine version, its resource ledger carries a retired wall-clock allowance, or the engine's gate refuses its stored task configuration or permission-mode seed). The suite checks the reader on SDK error objects and on flattened failure facts (status policy, field token, still-pending reading, getters that throw), compares every sentence byte for byte against a frozen copy of the terminal's previous implementation over each combination of code, field, pending state, lane and run id, pins the deliberate differences one by one, and checks that the sentences are correct per lane, never print an unknown code, pass the package hygiene list and do not trip the already-resolved word scan. |
538
- | `scripts/run-prompt-blocked-sentence-projection-test.mjs` | The sentence for a run whose first prompt was held back by a deployment's `userPromptSubmit` hook. The engine frames its own explanation for the model in an envelope that carries the session's reminder mark, and the terminal row used to print that envelope verbatim (`Run blocked: <system-reminder mark="…">…`) while the machine-readable cause beside it went unread. The package now has one sentence-maker for the `blocked` terminal, shared by the terminal row and the result frame's first error: a hook that refused the prompt reads `UserPromptSubmit operation blocked by hook:` followed by the hook's own words (treated as untrusted text — control and bidirectional characters shown as visible escapes, credentials washed, folded to one line, envelope tags removed even when spelled with zero-width characters); a hook that crashed, timed out, or was cancelled mid-check gets one fixed sentence each, saying the prompt was not submitted; an unrecognised cause word prints only the lead and never echoes the word. When the machine-readable cause is missing or malformed, the old `Run blocked: <reason>` line stays, with a whole engine envelope unwrapped so no tag or mark reaches the screen, and an agent's own free-text reason is byte-for-byte what it was. The new line counts as a governance stop (it is not an `API Error:`) and is recognised by the same row predicate as `Run stopped` / `Run blocked`. The suite also pins the single mint site in source, covers the interactive lane, and checks the tag remover is linear on near-miss input. |
538
+ | `scripts/run-prompt-blocked-sentence-projection-test.mjs` | The sentence for a run whose first prompt was held back by a deployment's `userPromptSubmit` hook. The engine frames its own explanation for the model in an envelope that carries the session's reminder mark, and the terminal row used to print that envelope verbatim (`Run blocked: <system-reminder mark="…">…`) while the machine-readable cause beside it went unread. The package now has one sentence-maker for the `blocked` terminal, shared by the terminal row and the result frame's first error: a hook that refused the prompt reads `UserPromptSubmit operation blocked by hook:` followed by the hook's own words (treated as untrusted text — control and bidirectional characters shown as visible escapes, credentials washed, folded to one line, envelope tags removed even when spelled with zero-width characters — and since 0.104.0 also when the engine hands them over in its defused entity spelling `&lt;system-reminder>`, when they are `task-notification` tags, or when the engine's length cut left half a tag glued to its truncation mark `…` (half a tag name without that mark is ordinary text and stays, so a trailing comparison such as `elapsed < t` is kept); only the display copy is decoded, and only the two entities the installed engine actually emits, which the suite pins by running the engine's own defusal functions); a hook that crashed, timed out, or was cancelled mid-check gets one fixed sentence each, saying the prompt was not submitted; an unrecognised cause word prints only the lead and never echoes the word. When the machine-readable cause is missing or malformed, the old `Run blocked: <reason>` line stays, with a whole engine envelope unwrapped so no tag or mark reaches the screen, and an agent's own free-text reason is byte-for-byte what it was. The new line counts as a governance stop (it is not an `API Error:`) and is recognised by the same row predicate as `Run stopped` / `Run blocked`. The suite also pins the single mint site in source, covers the interactive lane, and checks the tag remover is linear on near-miss input. |
539
539
  | `scripts/run-active-run-busy-read-projection-test.mjs` | How the package reads a rejected submission (the session already has a run in flight) in its terminal row, in its result frame and in the public discriminator `activeRunBusySignal`: one discriminator body and one carrier rule, shared by all three. The server's rejected and conflict terminal frames carry `status: 'failed'` since 7.125.0; the older idempotent-replay form — a cached copy of the original 409 conflict body wrapped as `done{ result, replay: true }`, with neither `status` nor a terminal word — is no longer recognised, since the package's support floor moved past the servers that sent it. The suite pins what such a body reads as today (the discriminator answers null, the row is the single `Outcome unknown` line, the result frame carries the unknown-outcome flag, the public projector agrees, with or without the replay flag, code only, id only), checks over a pool of sixteen frames that the discriminator and the row agree in both directions (including a cause-form frame that carries the busy code), pins which objects count as the carrier, pins that the retired folding function and its flag are gone from the source and the build, and pins in source that the discriminator and the carrier rule are each defined once. |
540
540
  | `scripts/run-fleet-terminal-retain-test.mjs` | The fleet ledger's rule for terminal rows has one home and one function. A terminal row (completed, failed, killed) lives only in the retention pool and never in the active set, whichever way it arrives — a task frame or a reconnect snapshot — and both arms go through the same admission function. A row brought back by a reconnect snapshot therefore expires with the retention window exactly like one that arrived as a frame; before, the snapshot arm left it in the active set for good, and a lost removal frame did the same to a frame-delivered row, and clearing the pool could not clear it. The suite refutes the change on three axes: timing (snapshot before or after the terminal frame, interleaved with removal, repeated delivery, resurrection, a later snapshot that does not carry the row, a row being viewed, pool clearing, and every terminal word through both paths with identical answers), concurrency (twin rows of one task in either order, a notification-retired entry meeting a snapshot row, notifications against a snapshot-carried row, two sessions' ledgers), and failure class (a status outside the terminal vocabulary, absent, or null is never turned into a terminal row; rows without a usable id, with a non-string id, or null entries are dropped and counted, never thrown on). It also pins that the removal hook still reports a known row for a settled terminal row, because downstream bookkeeping credits a removal only when neither the stale mark nor the unknown-row mark is present. Finally it pins the terminal evidence the ledger keeps past the window (status word plus cycle, per task tail, bounded): after a row has aged out or the pool was cleared, a same-cycle completed notification cannot turn a known failure or kill into a success, while a newer cycle, a revival, and the upgrade direction still pass; the evidence itself is cycle-monotonic, so an older generation's terminal row, or one arriving late after a revival, neither overwrites nor re-plants it. Since 0.97.0 it also pins the ledger's single per-task rule, applied in the one admission function to task frames and snapshot rows alike: a reading from an older cycle than the task has already shown is ignored (it neither revives the row, nor withdraws the terminal evidence, nor lets a snapshot drop the task's newer in-flight row); a newer cycle retires older in-flight aliases of the same task; within one cycle, or when the order cannot be told, an in-flight alias wins over a terminal row; otherwise the terminal row settles the task and twin rows collapse into one. Every arrival order of a set of readings, through either arm, gives the same projection and the same in-flight reading. A notification from an older cycle than the task has shown leaves the rows alone even when the running row's latest frame carried no cycle, and the bounded per-task cycle record never evicts a task that still has rows. |
541
541
  | `scripts/run-bg-subagent-row-test.mjs` | The background-subagent identity predicate every end shares. The fleet read surface lists every non-terminal task row — and since server 7.125.0 a session-scoped stream also brings back the session's own run row and its foreground delegation rows — so "is this row a background subagent" needs exactly one answer. `isBackgroundSubagentRow` applies four rules in order: a row with no id, a composite id, or a `run-leg` lane mark is never a subagent (the last two are foreground delegation rows); a non-agent handle (background shell or monitor) never is; a single-segment row with a readable parent is; a row without a readable parent is only when its id is an agent-family handle. `isBackgroundWorkRow` adds the non-agent handles for the "N background tasks" counts, and `isOwnBackgroundSubagentRow` adds ownership — a session-scoped stream replaces the ownership half only, never the row-kind half. The handle shapes are not retyped here: the durable agent-handle pattern is the installed engine's own, and the non-agent prefix letters are read from the engine's task registry when the tables are generated. The read-surface row now carries `sourceLane` (only when the wire row has a non-empty string), and task frames and reconnect snapshots reach the predicate through the same row shape. The suite checks each rule with a positive and a negative example, the absence cases (no id, unreadable parent, unusable lane mark), the 7.125.0 stream shape against the older shape, and that the lane word and the prefix table each have exactly one home in the source. |
@@ -547,7 +547,7 @@ guard still cross-checks the table by name).
547
547
  | `scripts/run-seat-stream-history-test.mjs` | The seat's streaming-preview chunk and the engine session history projection each have one definition now. Live preview text used to be a shape the desktop host built by hand and the web client unpicked by hand; the package now mints it (`seatStreamDeltaMessage`) and reads it (`readSeatStreamDelta`, which tells a chunk, an unknown inner event, a malformed chunk and an ordinary message apart, so a chunk is never mistaken for a committed message). Rebuilding a transcript from the engine's stored session rows used to be written once per client with diverging rules; `projectEngineSessionMessages` is the single projection: it cuts out the engine's own prepended content using the provenance marks the engine stamps on each user row (never by guessing from text shape, so a user's own text that merely looks like a reminder tag is kept), keeps tool results as tool-result blocks, maps the engine's block vocabulary to the chat-message vocabulary, never invents a timestamp the row does not carry, copies tool arguments and unknown blocks as plain JSON so what it returns can always be cloned and sent, and reports every row or block it could not read instead of dropping it silently. The guard checks the minted bytes against the previous hand-built shape, runs a real adapter stream through mint, gate, structured clone and read, pins ten stored rows to an exact expected transcript, checks the provenance rules case by case (segments across blocks, malformed ranges, absent marks), and feeds hostile rows (getters, throwing proxies, sparse arrays, functions and cycles inside tool arguments) to show one bad row never takes the others down. A replayed tool-result row that the engine archived as parked at an approval gate now carries a machine-readable parked flag on its tool-result block, so a replay does not render it as a failure. |
548
548
  | `scripts/run-seat-recents-test.mjs` | The seat's recent folders. The recent-folders list is read in exactly one way: the folders of the session records (derived, never registered by the host) joined with the folders the host registered (the ones the user picked, whether or not a session ever opened there), de-duplicated by exact path, most recent first, and a path present on both sides counts once at its later time. `recentFoldersOf` is the pure reader; `readRecentFolders` is the orchestrating reader that calls `listRecentFolders` only when the seat capability table says `recents` is available, and that keeps the session-record half when the host half is not offered or cannot be read. The seat gets two optional verbs, `listRecentFolders` and `noteRecentFolder`, and one capability bit `recents` that governs both (an absent bit means the host did not say, never "none"). The guard checks the registration, the outbound gates row by row (plain objects only, so what passes the gate is what survives structured cloning), the capability reading and its three distinct sentences, the reader's absence and ordering rules (a reply whose rows are all unreadable is reported as unreadable, never as an empty list), and an end-to-end chain from the host's capability publication through the renderer's reading to the host's verb double. |
549
549
  | `scripts/run-approval-card-v1-projection-test.mjs` | Projection of the v1 approval card (the `card` of an `approval_request` frame) into approval-card request facts through `approvalCardFactsOfV1`. For every key the v1 card shares with the live approval frame, the same value, well-formed or malformed, fed to both paths yields the same card request key and value; rule offers land in the read-only slot because the ask-decision route carries no rule; the real-approval bit is read through the single real-approval reader (top level or `risk`); every key in the SDK card key list has a recorded disposition, so a new SDK card key fails the gate; keys outside the card contract are not read, host positions are not minted and the session-answer route fact is not stamped; the grant-binding state matches the live path; refusal evidence that is present on the card but unreadable (malformed, inherited or throwing) is recorded as `unreadableEvidence` identically on both paths; non-objects yield undefined, one throwing key drops only that key and records it, and the result is frozen; the v1 card, live-frame and generic-card projections answer every shared key identically. |
550
- | `scripts/run-approval-card-reading-projection-test.mjs` | Single reading of approval-card facts. The live-frame projection `approvalCardFactsOfFrame` yields, key for key, the facts the live approval path hands to the card port (well-formed and malformed frames alike), including the route, grant-binding and unreadable-evidence facts `standingRowsOf` needs, and a live card whose must-ask evidence could not be read is never answered automatically; the generic-card projection `approvalCardFactsOfGeneric` reads the version-independent keys with the same readers as the v1 projection, reads nothing else, marks the payload unreadable and leaves every automatic-answer lane at unknown; the approval-window reader `approvalWindowKeysOf` accepts only non-negative finite values, needs a current frame, stamps the arrival anchor only when a window is present, requires a coherent envelope for unknown frame shapes, and matches the window on the live card; `probeCauseRowsOf` renders the probe-cause roots and further rows or nothing when the shape does not match, with untrusted text neutralised; `approvalCardReasons` orders the single-sentence reasons and the engine's own message, each line equal to its source sentence, deduplicated, within a line budget that reports what it hides; `readRequiresRealApproval` reads the top level or `risk` and every package reading point agrees with it; `countOwnedPendingRows` counts owned rows this client can present and throws instead of answering zero when the list cannot be read. |
550
+ | `scripts/run-approval-card-reading-projection-test.mjs` | Single reading of approval-card facts. The live-frame projection `approvalCardFactsOfFrame` yields, key for key, the facts the live approval path hands to the card port (well-formed and malformed frames alike), including the route, grant-binding and unreadable-evidence facts `standingRowsOf` needs, and a live card whose must-ask evidence could not be read is never answered automatically; the generic-card projection `approvalCardFactsOfGeneric` reads the version-independent keys with the same readers as the v1 projection, reads nothing else, marks the payload unreadable and leaves every automatic-answer lane at unknown; the approval-window reader `approvalWindowKeysOf` accepts only non-negative finite values, needs a current frame, stamps the arrival anchor only when a window is present, requires a coherent envelope for unknown frame shapes, and matches the window on the live card; `probeCauseRowsOf` renders the probe-cause roots and further rows or nothing when the shape does not match, with untrusted text neutralised; `approvalCardReasons` orders the single-sentence reasons and the engine's own message, each line equal to its source sentence, deduplicated, within a line budget that reports what it hides; `readRequiresRealApproval` reads the top level or `risk` and every package reading point agrees with it; `countOwnedPendingRows` counts owned rows this client can present and throws instead of answering zero when the list cannot be read. On the durable parked-approval path the card's tool input is the pending row's `input` — the bytes that will run — and the event stream's `tool_start.args` is used only when the row carries no real input; when both are present and differ as JSON values (compared node by node, stopping at the first difference, within a node and depth budget) the card carries `argsRewritten: true` with an explanatory note, a comparison that cannot be decided adds nothing, and when they match the card is byte-identical to the one built without the stream copy. The live approval card reads its tool input through the same single reader: the frame's own `args` (the bytes that will run) are used whenever they are a plain object, the stream copy is only a comparison partner then, a difference adds the same `argsRewritten` flag and note, and the stream copy is shown only when the frame carries no real input, with a note saying it is the model's original call; the suite also checks from the source syntax tree that both paths call that one reader and that no other function repeats its order or comparison. |
551
551
  | `scripts/run-fail-sentence-test.mjs` | The wording of failure sentences that reach the user, in two parts. (1) A scenario refusal now has one sentence owned by the package: `scenarioNotAllowedContent` renders the subject, the allowed list (names escaped but never truncated, empty list omitted) and, for a fresh request only, the way out of omitting the scenario; the resumed-work form leaves that way out off, since a decision has no scenario to omit. The four decision exits (approve, deny, question card, plan review) append the sentence after the resume-policy sentence when the engine folded the refusal into `resume_blocked_by_policy`, and the failed outcomes of the two approval legs carry the folded original code as `blockedBy` next to the outer `errorCode` (present only when the outer code is the fold code and the body names a non-empty original). (2) The remaining user-facing failure sentences read a thrown value the same single way as the earlier seven: the four write and read exits (persisted rules, session policy, memory governance, MCP probe) no longer fall back to the class name or `[object Object]`; the subagent steer exit and the plan-review outcome sentence use the same reading and send the original thrown value's log form to the host log; the parked-row wait and the local rule parse take an optional `onFailure(log)` so their pure modules can hand the original to the host log; and the retry row's stalled `detail` goes through the same display exit as the four error arms. |
552
552
  | `scripts/run-engine-account-keys-projection-test.mjs` | The projection of five engine keys that older packages dropped on the way to the screen: a tool call's `recovered` start frame and `notExecuted` end frame (literal-true marks, read from own data properties only — never from a getter — and never turned into a claim about whether the call ran: absence means nothing), the completed run's machine-readable detail (the provider ended the last turn with a stop reason the engine does not read), the provider's account-condition word on a refused run, and the two notices that report an unknown stop reason and a stream that kept talking after its first ending. The suite drives the built package through the projection, the transcript adapter and the print-mode frame, and checks that absent, falsy, malformed and hostile values produce no claim and never throw, that the terminal reader keeps the run's kind whatever its detail looks like and isolates each key, and that the single reader for the marks is the only way the sources read them. |
553
553
  | `scripts/run-seat-devices-test.mjs` | The seat's device face. Whether the device face is usable, and what it can do, is read in exactly one way: `devicesFaceOf` takes whether the host wired the device verbs (the seat capability `devices`, or on a direct host whether it holds the SDK slice), the engine's device-lane reading when there is one, and the outcome of the last device list, and answers one of unavailable (with a closed reason), probe pending, probe failed, verbs off, or manage. On the seat lane there is no capability reading, so the first device list is the lane probe: until it answers, only that one read-only request is allowed and every write stays closed (`devicesWritesAllowed`). An unarmed verb and a host that reports `false` are the same face with the same sentence. The five seat verbs return the package emitters' outcome shapes, and the outbound gates check every arm as a closed shape (a missing required key, a bad value, an extra key or an unknown arm is refused, naming the path); the one-time enrollment token passes only at the top of the successful enrollment-token outcome. The guard also compares the direct-lane answers with the browser client's previous judgement cell by cell, walks a 140-cell seat matrix against an independent specification, and replays the cold-start sequence. |
@@ -558,7 +558,7 @@ guard still cross-checks the table by name).
558
558
  | `scripts/run-plan-review-outcome-verdict-test.mjs` | The plan-review decision outcome has one reading rule: each outcome word is given only on positive evidence, otherwise `unconfirmed`. A decision reply that says the review is still pending (`retriable: true`) reads `still_parked` whatever the follow-up status read says; only `completed` reads `took_effect`; `suspended` reads `advanced` only when the follow-up read shows the new pause and that pause has a decision card (an approval or a plan review); `needs_review` reads `still_parked` when the review's instance identity before and after the decision can both be read and match, `advanced` when they differ and the new pause has a decision card, and `unconfirmed` otherwise; a malformed status on the follow-up read falls back to the decision reply like a failed read; every other status (`failed`, `blocked`, `timeout`, `running`, unknown words) reads `unconfirmed` and the sentence states the reading instead of claiming completion. `decidePlanReview` takes an optional `parkedResult` (the paused result the card answers) to supply the before-identity. Pinned with a structural injected client: failed approvals and rejections, the still-pending reply with and without a follow-up read, changed / same / missing / malformed identity, completed with and without the follow-up read, the other status words, the callback and the queue item carrying the same outcome word, and the new option on the shipped declaration. |
559
559
  | `scripts/run-engine-caps-landing-projection-test.mjs` | How a client reads the whole capabilities answer of an engine, and how it swaps the probe used for later re-reads. The whole-answer reader has three states: landed (carrying the frozen snapshot that every narrow capability reader and every per-key reader also read), not landed (no probe yet, still in flight, failed, or invalidated), and unreadable (the probe returned a body whose root cannot be read). A key the engine did not report is unknown and never counts as on: the per-key readers are pinned to a written rule applied to the whole answer, and every narrow reader gives its own not-observed reading on an unreadable root. The same JSON parsed in another realm reads exactly like one parsed here: landing and the snapshot share one plain-record rule, and class instances stay non-records in both realms. A rotation entry for the same engine with a new probe (for example a client rebuilt after a credential refresh) starts that probe when nothing is landed or in flight, and otherwise only replaces the probe used by later single-flight re-reads without invalidating anything; the guard pins that the next re-read calls the new probe and never the old one, together with the in-flight, re-entrant and invalidation orderings. It also pins the root-level nested-value reader for group capabilities (a group is either off or an object of flags), the change-subscription entry (one beat exactly when the whole-answer reader starts returning a different record, per base address, callbacks isolated per subscriber, no replay, idempotent unsubscribe, a seeded random operation sequence proving no extra and no missing beats), and the invalidate-and-reprobe entry that reuses the registered probe. |
560
560
  | `scripts/run-durable-decide-leg-test.mjs` | Which durable decision leg a pending row belongs to has one table and one reading. The approval-card gate kinds are `human` and `irreversible_ask` (the only kinds the approval decide route accepts), the plan-review gate kind is `plan_review`, and the review wording set is `plan_review` and `needs_review`; each table is pinned at compile time in both directions against the engine's pause registry and checked again at run time against the installed engine. `durableDecideLegOf(row)` answers one of five words: `approval`, `plan_review`, `other_gate` (a known gate neither decide route accepts), `unreadable` (a gate word outside the registry) and `absent` (no readable gate word on the row: missing, null, empty, not a string, or a throwing read); it never throws. `askParkForeignGateKind` derives from it. Pinned with the twenty-shape verdict matrix, the derived foreign-kind answer, the row classifier, the pending-row lookup (including refusing to fall back to another row when the row a park names is not an approval), the busy-session recovery on non-gate words, the five-word union on the shipped declaration, and no engine-package reference in the shipped output. Since 0.97.0 the same table also decides which approval arm a parked frame goes to: `isToolApprovalGate` agrees cell by cell with `durableDecideLegOf` (an approval gate kind takes any tool that is not the question tool, an unreadable kind falls back to the file-write and shell name families, any other kind takes neither arm); a real-wire park on an approval gate kind for a tool outside those families raises the approval card and sends the decision end to end, while the non-gate word `tool_approval`, a plan-review kind with a write tool, and a non-gate word with the question tool raise nothing. |
561
- | `scripts/run-rule-tool-name-judge-test.mjs` | One reading for "does the engine accept this rule tool name": the engine's own tool-name check, installed as a value port and read through `ruleToolNameVerdictOf` (current / retired with its current name / unprefixed namespace / unmintable segment / unknown). The session-rule deliverability check asks it once for every name that survives the plain-text steps (an ordinary name the installed engine has newly retired is withheld as `legacy_tool_name`; a check that cannot answer makes the batch unreadable), the refused-entry removal removes every entry it does not call current and answers `judge_failed` without writing when it cannot answer, the sub-agent tool-list pre-check reports retired, unprefixed and unmintable names from it, reports an entry it cannot check as unjudged (sent as written with a warning, like any other deny-list problem) and judges case twins only against a supplied roster, and the request-body assembly rewrites a retired name to the current name it gives only in the deny and ask buckets (allow keeps it). No build-time retired-name table remains in the source. A real engine run that mounts no tool of that name, reading a stored session record, refuses to start exactly for the names this reading does not call current. |
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. |
562
562
  | `scripts/run-tool-card-outcome-test.mjs` | One reading, one sentence, for what a tool result card in the transcript actually says. Three marks decide it: the placeholder mark on the record (the engine sent no body, so the content is an empty placeholder), the engine's positive evidence that the call never ran, and the block's error flag. `readToolCardOutcome(record)` reads them as `normal` (a real result; success or failure still comes from the error flag and is never rewritten), `degraded` with a cause and one sentence (`not-executed` — takes precedence and is independent of the error flag, so a card that looks successful but never ran is caught; `error-without-detail`; `no-result`, where the error flag false is a placeholder and not a success; `unrecognised`, a placeholder value from another version) or `unreadable` (not a tool result record, or the placeholder position cannot be read — never read as normal). The placeholder has two minting points that cannot be told apart on the record (a call closed at the end of the turn with no end frame, and a real end frame without a body), so the no-result sentence states only what both share. Pinned on records produced by the real pipeline in both lanes (the transcript row and the print-lane frame answer the same), a read-failure matrix that never throws and never reads as normal (accessors are not run, prototype-chain keys do not count), four distinct sentences with no internal names or tool-family names, and the source (the sentence table is typed on the cause union, the evidence mark goes through the existing strict-true reader, the placeholder constant is imported not re-written, and each sentence has exactly one minting site in the shipped output). |
563
563
  | `scripts/run-sentence-tables-projection-test.mjs` | The sentences a person reads for the same fact now have one source, shaped as closed key tables checked against the upstream value sets. The checks cover: persistent-rule origin, scope, match form and revoke outcome (the status by liveness grid, with an unknown-status third outcome that never says whether anything was removed, and liveness words other than the three read as unknown); the card-line readers (the origin line with its quiet table, the reason a card has no remember option, the reason a rule offer still asks); crashed-run leftovers (per-row clauses sharing one table with the counted sentences, whose output is frozen byte for byte, and a not-started row never described as partly landed); the one-line ending of an approval the engine closed without an answer (fourteen causes, never described as a refusal), the accepted-decision line that only claims delivery, and the parked-card note shared by the replacement and reopened cards. A parked frame is never described as an ending; a crashed-run row sentence takes the projection and the row and trusts only the projection's bucket. Every table is a literal that carries its own `satisfies Record<…>` clause; a word outside a table, an absent word or a non-string yields a neutral sentence or `undefined` and never echoes the word; readers do not run accessors or throw on proxies. It also pins that no local copy of the server's rule-count cap is minted and that the permission-mode default half of the `policy` origin sentence is present. |
564
564
  | `scripts/run-engine-url-transport-test.mjs` | Engine address handling in one place. `normalizeEngineBaseUrl` trims outer whitespace, strips every trailing slash and keeps the user's spelling; it refuses, with one of five closed reasons (`empty`, `scheme`, `unparseable`, `userinfo`, `query_or_fragment`), anything that is not a plain http(s) base address. A query or fragment is refused even when empty, because request paths are appended to the base as plain text and would end up inside the query. `engineUrlTransportVerdict` lets `https:` through and `http:` only for a host the settings contract's narrow local-host check accepts, the same check that decides whether outbound credentials are dropped, so the credential fence and the plaintext gate cannot disagree about one host; anything else is refused as plaintext to a remote host or as not an http(s) address, and an address it cannot read is refused. Neither function returns message text or echoes the input. The gate checks every refusal reason and success form, the two addresses the terminal and the desktop shell used to answer in opposite ways, that the local-host reading has a single call site over the settings contract's own function and never touches the wider routing check, that the verdict matches the installed settings contract on a host table, that hostile input never throws and a very long run of slashes stays linear, and it compares the package with copies of both shells' former logic on a generated corpus: the package accepts exactly what both former implementations accepted, with the only disagreements between them falling into two registered classes. The cloud configuration's session-mirror address is read through the same normaliser: well-formed addresses come out byte-identical to the former trailing-slash trim, an address the normaliser refuses is passed on as read with its `required` flag intact rather than dropping the mirror, and a long run of slashes stays linear. |
@@ -573,7 +573,7 @@ guard still cross-checks the table by name).
573
573
  | `scripts/run-device-registration-test.mjs` | Device registration verbs and the identity-mismatch refusal. `issueEnrollToken`, `enrollDevice` and `revokeDevice` go through the client's devices slice with the same closed refusal table and at-most-once judge as the rebind calls, and every outcome is a value, never a throw. The one-time enrolment token appears only in the issue call's ok outcome — never in another outcome, in a sentence, or in a log call (a refusal sentence that echoes the token is dropped from the outcome). The enrol body is exactly the six closed keys in fixed order and nothing else the caller passed; a missing token, public key, platform or workspace root is refused locally with no request; the returned device id is accepted only if the host's own shape judge says so. A revoke is ok only if the answer echoes the same device id and the revoked status. The identity-mismatch refusal has one reader for its two forms (the machine code, and the bracketed prefix in a tool error text), and the bound device id is read only from structured fields, never from prose. Pinned on the real client with stubbed fetches (writes are sent exactly once even with retries enabled), on a plain-object pool of thrown values, on a syntax-tree scan, and on the fixture server's minting points. |
574
574
  | `scripts/run-durable-seq-reading-test.mjs` | One reading of a frame's resume position. `durableSeqOf(frame)` returns the cursor value of an event: the stream id comes first (a string or a number; a malformed id hides a valid sequence number, so the cursor does not move), and only when there is no id does a numeric sequence number count — except on progress, notification and task-start events, whose sequence number is a different axis (a sub-agent generation or a stop-cycle batch counter) and never a cursor; a frame with neither stays at the previous cursor. The host app's own copy of this reading answers differently on nine shapes; the guard freezes that copy and pins the exact disagreement set, extracts the real function from the host app tree when it is present, and pins that the gate ledger's cursor is fed by this one function only. |
575
575
  | `scripts/run-task-notification-xml-read-test.mjs` | The single-notification read for transcript task-notification XML. The package already had a batch reader that turns a saved transcript into de-duplication keys; a host that needed the task id and status of one queued notification wrote its own scan. Both now read through one block parser: a block is a `<task-notification …>…</task-notification>` pair, the id and status are the first `<task-id>` / `<status>` pair inside it, un-escaped, and a field that is missing, empty or unterminated is simply absent. The parser is linear: unterminated openers, openers with whitespace and no closing bracket, and look-alike tags no longer cost time proportional to the square of the input. The gate pins the single read against the render function over a generated pool (with escapes, whitespace and newlines), compares the batch reader with a frozen copy of the previous regular-expression implementation over thirty thousand generated texts in three message shapes, runs four adversarial inputs of hundreds of thousands of candidates in a child process with a timeout, and checks by syntax tree that neither entry point contains a regular expression of its own. |
576
- | `scripts/run-task-notification-parity-test.mjs` | Background-shell notifications match the engine's own rendering. The notification text the package builds for a finished background command or agent is now byte-identical to what the engine feeds the model (it carries the task type and the same tag order); facts the engine does not put in the text — stopped-by, partial result, exit code and output lines — travel as a structured record on the queued item, the transcript row and the workflow arm instead, and a reader returns that record. The background-command registration carries the tool call id and the output file when known, and the output read face carries the exit code. Dropping queued notifications reads the structured task id first. A result frame says when its first error line already contains the engine code. |
576
+ | `scripts/run-task-notification-parity-test.mjs` | Background-shell notifications match the engine's own rendering. The notification text the package builds for a finished background command or agent is now byte-identical to what the engine feeds the model (it carries the task type and the same tag order); facts the engine does not put in the text — stopped-by, partial result, exit code and output lines — travel as a structured record on the queued item, the transcript row and the workflow arm instead, and a reader returns that record. The background-command registration carries the tool call id and the output file when known, and the output read face carries the exit code. Dropping queued notifications reads the structured task id first. An error-shaped result frame says when its first error line already contains the engine code; a success-shaped frame has no error lines and never carries that mark. |
577
577
  | `scripts/run-narrow-readers-export-test.mjs` | Four narrow readers for foreign objects, exported from the package root. Hosts used to write their own copies: a three-field reader for thrown errors (status, error code, message) existed three times with identical text, and the one-line checks for a non-empty string or a finite number appeared three to eight times per host. The package already had one reading rule for this, reading only the object's own data properties without running accessors, but it was internal. The four exports are the own-data string reader, the own-data finite-number reader (zero is a value, NaN and infinities are absent), a guard that turns a throwing read into an absent one, and a three-field reader for thrown values whose message is never empty and is produced by the same function as the user-facing failure sentence. The gate drives each reader with every absence shape (missing, inherited, accessor, proxy trap, wrong type), compares the three-field reader with a frozen copy of the host-side code over six thousand generated JSON-shaped errors (the only deliberate difference is the sentence used when no message exists), checks by syntax tree that the three-field reader is a projection of the existing snapshot reader, and compiles a small consumer under the strict settings to prove that an `unknown` from a catch clause can be passed straight in. |
578
578
  | `scripts/run-plan-review-offered-mode-test.mjs` | The offered-mode read for plan-review cards, and the retirement of the synchronous reopen overload. A plan-review card is shown either as the old two-choice card or as a three-choice card whose two Yes options each carry a mode to switch to after approval. Reading the user's answer correctly needs the labels that were actually shown, otherwise a three-choice Yes is read as dismissed. The package knew what it had shown but cleared it on the first answer and exposed no reader, so a host kept its own table of what it had armed and re-derived the rules around it (reopened cards are always two-choice, a missing entry means two-choice, an armed card is not rewritten by a replay). One reader now answers `three_choice`, `two_choice` or `unknown` from positive evidence only: the labels on the card or frame when given, then the question id (reopened identities are two-choice by construction, otherwise a bounded record the package keeps when it publishes the card, not cleared by an answer). `unknown` is never read as two-choice by the reader itself. Reopening a card now always returns a promise and requires the presentation-receipt window; a call without it presents nothing and resolves as not reopened. The gate drives the real arm, answer, dismiss and reopen paths, compares the reader with a frozen model of the host-side table over a hundred and sixty random steps across three tasks, and pins by syntax tree and a strict compile that the overload is gone. It also pins the public question-id reader (task id plus a reopened flag, undefined for anything that is not a plan-review card). |
579
579
  | `scripts/run-context-usage-projection-test.mjs` | The one narrow reader for the engine's context-usage payload. The payload (used tokens, window, compaction threshold, and the prompt's section rows) was read in several places: the projection filtered section rows, but the next stage passed any array through, so a hand-built frame fed straight to the adapter put injected keys and invalid rows on the event; the terminal re-implemented the finite-number checks on raw frames to serve both the interactive and the print lane; the web client re-narrowed the rows on the already-projected event. The reader accepts the raw frame, the internal arm or the chrome event, because the four fields have the same names and shapes in all three, and returns each field independently: numbers must be finite (absent is never turned into zero), rows need a non-empty id and a finite token count, rows that fail are dropped one by one, an all-bad list leaves the key absent, and a payload with nothing readable returns nothing. The projection and the chrome arm both call it. The gate checks the table of shapes, accessors and proxies, compares the reader with a frozen copy of the web client's reader over eight thousand generated payloads, runs the same frames through the projection and straight through the adapter to prove the two entries agree, and checks by syntax tree that neither entry narrows rows itself. |
@@ -623,7 +623,7 @@ guard still cross-checks the table by name).
623
623
  | `scripts/run-refusal-machine-reads-test.mjs` | Machine-readable causes on refusal outcomes, shared by every client. A rule the engine's rule compiler refuses is reported in `permissionsDeclarationReport().kept[]` with the compiler's own `code` and `message` for that rule (present only when readable as non-empty text; other reasons never carry them), so a host does not ask the compiler a second time. The refusal note on a live approval outcome (`respondRefusal`) keeps every machine-readable part the shared refusal readers use: the server's `retriable: true`, the not-pending cause word and the refused field from the refusal body (`extra`), and an `unreadable` mark when the thrown error itself cannot be read, so `classifyDecisionRefusal`, `decisionRefusalAskReading` and the close-out check answer the same for the thrown error and for the note. A binding safety stop says where it came from in its code: `binding_missing` when the pending row lacks the binding fields and nothing was sent, `binding_mismatch` only when the engine answered with a binding conflict; the closed set of safety stop codes is frozen, and the shared readers treat the new code as a local stop (not settled, never read as already resolved). 0.98.0: compiler refusal codes are pinned on the allow bucket, the only bucket that still asks the compiler. |
624
624
  | `scripts/run-seat-session-patch-test.mjs` | The seat's rename and pin verb. The seat gains one optional verb, `patchSession`, that writes the two user-writable fields of a session row — `title` (a string renames; `null` clears back to the automatic title, and an empty string is not a way to clear) and `pinned` (a boolean) — and answers with the row as it is after the write; the host forwards it to the engine, and the seat contract fixes only the shape. The inbound judge `checkSessionRowPatchRequest` accepts exactly the three keys, requires a non-empty `sessionId` and at least one of the two fields that is not `undefined`, and refuses anything else naming the key; accessors are never run; a non-enumerable own key is refused naming the key, whatever its value (a structured clone would drop it, so the gate does not let it pass unread); the request must be a plain record (prototype `Object.prototype` or `null`), so `Date`, `Map`, `Set`, class-instance and custom-prototype carriers are refused; a key found only on the prototype chain does not count as carried. It deliberately does not copy the server's title rules (length and surrounding whitespace): the server judges them and reports a typed error, and the guard pins that choice with two cells so that copying them later is a visible decision. The receipt row goes through the existing outbound gate, which now accepts `pinned` only as a boolean (absent means not pinned or unknown, never an error). Whether the verb can be used is answered only by the seat capability `patchSession`: three readings (reported, not reported, unreadable), three distinct disabled sentences that differ from the other capabilities', and no cross-talk between capabilities. The guard also reads the built declaration file with the TypeScript parser for the verb signature, the request members (only `sessionId` required), `LocalSessionRecord.pinned` and `SeatCapabilitiesState.patchSession`, drives a host stand-in end to end through the public entry points (publish the capability, subscribe, judge, call, outbound gate, structured clone, withdraw), and poisons every key of the new coverage-table row. |
625
625
  | `scripts/run-catalog-routing-test.mjs` | A gate for the message-catalog migration. Sentences that move into the catalog must read byte for byte the same in English as before the move, and must exist in Chinese. Each corpus entry (one sentence producer called with one set of inputs) is compared in English, with and without an explicit language argument, against digests taken from the pre-migration build; a sentence the changelog names as reworded is pinned to its new digest instead of being waived. In Chinese every entry must contain Chinese characters, differ from the English text, and contain no run of three English words outside registered protocol terms, quoted foreign values and code spans; no placeholder may be left over and the message ledger must stay empty after the whole corpus has run. A syntax-tree check requires every function that takes a language argument to pass it on to every other language-taking function it calls, and every exported language-taking function must be covered by a corpus (or be listed as pinned by another gate or as a not-yet-migrated placeholder, which the gate verifies). The gate also proves its own detectors on planted cases. Read-only modes print the digests of a build and compare two builds entry by entry. |
626
- | `scripts/run-host-turn-settings-test.mjs` | The single assembly entry for host-supplied request settings (`hostTurnSettings`): hooks, the resolved permission snapshot, the web-search setup and the output-style name reach the request body only through its product, which always carries one row per key (supplied, explicitly absent, or not wired at all, the last one with a session notice and a self-check that names it), a turn verdict, and notices. The request builder takes the product and refuses these keys when they are written straight into the input settings. The suite checks that the request bodies match the previous release byte for byte on inputs whose projection did not change, the missing-source and explicit-none rows, the refusal when managed hooks cannot be loaded (thrown only on a lane that carries the refused key behind an open live gate: the one-shot side-channel lane never carries hooks, so it is sent as before), the final check of the hooks plan against the settings-contract classifier before sending, partial layers, and, against the pinned server fixture, that a configuration the classifier accepts but the server's own limits refuse still fails through the generic invalid-request path. A permission-rule projection failure (for example missing core value ports) now refuses the turn instead of sending without any permission rules, and the thrown refusal carries the underlying assembly refusal as its `cause`. |
626
+ | `scripts/run-host-turn-settings-test.mjs` | The single assembly entry for host-supplied request settings (`hostTurnSettings`): hooks, the resolved permission snapshot, the web-search setup and the output-style name reach the request body only through its product, which always carries one row per key (supplied, explicitly absent, or not wired at all, the last one with a session notice and a self-check that names it), a turn verdict, and notices. The request builder takes the product and refuses these keys when they are written straight into the input settings. The suite checks that the request bodies match the previous release byte for byte on inputs whose projection did not change, the missing-source and explicit-none rows, the refusal when managed hooks cannot be loaded (thrown only on a lane that carries the refused key behind an open live gate: the one-shot side-channel lane never carries hooks, so it is sent as before), the final check of the hooks plan against the settings-contract classifier before sending, partial layers, and, against the pinned server fixture, that a configuration the classifier accepts but the server's own limits refuse still fails through the generic invalid-request path. A permission-rule projection failure (for example missing core value ports) now refuses the turn instead of sending without any permission rules, and the thrown refusal carries the underlying assembly refusal as its `cause`. The host may declare its own memory switch with the same literal as the command-line flag: only exactly `off` turns memory off for the request (checked before the settings section), an absent flag leaves memory on and the request body without the key, and any other value is refused at the entry instead of being read as absent or as a spelling of `off`. |
627
627
  | `scripts/run-settings-credential-key-paths-test.mjs` | The one judge of which keys in a settings document hold credentials. It returns key paths only, never values, as findings that carry an optional `numericByNameOnly` flag for count-style names whose value is a plain integer (the flag is present only when true, so a restrictive file-mode decision can still count them while a user-facing notice leaves them out). Key names are judged on two deliberately wide tables, one for environment-style names and one for structured-config names; values are judged only through the display layer's known-secret shapes, URL userinfo and credential-bearing query strings, and an all-string array is read as a command line (`--password <value>`, `--token=<value>`, `NAME=<value>`, `Authorization: …`). Empty strings, null and false do not count. The golden cases, the residual boundaries (a single-letter short option is not judged, credentials embedded in command strings match only by known shape, an unprefixed random value under an ordinary name does not match) and the never-throws / never-leaks behaviour on cycles, getters, throwing proxies and very deep nesting are pinned, together with a differential against the settings package's own secret-token check and the reference client's name table when they are installed Since 0.101.0 it also pins that percent-encoded YAML block scalars and block sequences inside a string value answer exactly as their plain form does: a known credential shape inside an encoded block is found at every layer of encoding, malformed byte sequences next to it do not hide it, a label with a low-entropy value inside a string value is not found in either form, a block inside a URL query string is found through the query check, a whole address that is itself percent-encoded (credentials in its userinfo or query) is found as the plain address is while an encoded address without credentials is not, and three in-gate negative controls (without the known-shape check, without the query check, reading the address only unencoded) turn the cells red. Since 0.102.0 a string value is judged against the configured-address display outlet's own scan: whatever that outlet would mask as a credential (a scheme-less Slack webhook, a key in an address path, a single-slash address, `access_token=` in a fragment, a labelled value inside prose or a command string) is found, and an address whose only value is already a redaction marker is no longer found; the cells for those shapes, the labelled-value changes to the residual boundary and to the low-entropy block cells, and an in-gate negative control without that scan are pinned. The differential against the settings package's own secret-token check now pins one direction only: every value that check finds is found here too, and the values found only here (shapes the display outlet masks, such as a short `Bearer` value after a prefix) are listed one by one, so a new difference in either direction turns the cell red. |
628
628
  | `scripts/run-mcp-wire-caps-test.mjs` | The local MCP config projection. A server name goes on the wire byte for byte: a name with leading or trailing whitespace, an empty name, or a name that would collide with another entry once the whitespace is stripped is left out (not silently trimmed, which let ` blocked ` reach the wire as `blocked` and slip past a disabled-server list) and is reported by `projectMcpConfigs`, whose `dropped` rows carry a sentence for the user. `elicitation` is judged by one reader for both the local and the cloud-configuration leg (`mcpElicitationVerdict`): a boolean passes through as written (`true` and `false` alike), absence adds no key, and any other value, `null` included, leaves that one server out with a sentence naming the server and the key. A `toolAxes` declaration is judged by the engine's own reader through the core value port (`mcpToolAxesVerdict`): a bad shape, `null` included, leaves that one server out with a sentence naming the server, the key and the fix, the other servers are unaffected, and with no judge installed the declaration passes through unchanged. The suite feeds the products to the live server fixture's request-entry check: the 400 for a bad `toolAxes` is gone, and a non-boolean `elicitation` sent as written would still be refused by the server, naming the key, which is why neither leg sends it. Since 0.100.0 the name, transport and identity-header readers are the settings contract's own judges, copied at build time and read by both the local and the cloud-configuration leg, so the two legs answer every input the same way. A name that is empty, longer than 128 characters or has leading or trailing whitespace leaves its entry out with a sentence built on a bad-name summary that never echoes the whole name; a transport that cannot be used at all (no command, an address that is not an http(s) URL) leaves the entry out and names the key path; and a transport whose optional parts cannot be used (an argument list with a non-string member, an environment or header record that is not plain string data, a member literally named `__proto__`, a non-string principal header) keeps the entry on the wire as the judge's independent copy without those parts and reports what was left out — key paths and member names only, never a value. Setting both identity headers leaves the entry out with a sentence (it used to be skipped silently). The suite feeds these products to the live server fixture's request-entry check on both generations: every product is accepted, while the same declarations sent as written are refused. Nothing is capped at 32 servers any more: forty servers and a skill body above one mebibyte go on the wire as written, servers from 7.132.0 refuse the whole list or body with a field-invalid answer that names the limit, and older servers keep the first 32 on submission as before. A server start timeout passes through as written and a null server list is never produced. A standalone server-name verdict, `mcpServerNameVerdict(name, siblings?)`, answers whether one name can go on the wire byte for byte (usable, or one of four reasons: blank, edge whitespace, collides after trim, too long) with the same judge as the whole-table projection: on one table the two agree name by name. A collision needs the sibling names; without them only the other three reasons are judged. Non-string names count as blank, a non-iterable sibling list throws a type error, and the result is a fresh frozen object. The `toolFaces` declaration of a server entry is judged the same way: its shape is asked of the engine's own reader through the core value port, a malformed declaration keeps that one server off the wire (the other servers go as before) and is reported with the engine's own sentence, a well-formed one goes out as the engine's plain copy, and with no judge installed the value passes through untouched. A pool of three thousand seeded declarations is compared with the engine's reader sentence by sentence, and the published server's request-acceptance door shows which malformed shapes it refuses itself and which only the engine's own door would. |
629
629
  | `scripts/run-panel-reduce-test.mjs` | The panel row reducer: one record per background agent, found through one identity index of engine ids — the fleet row id tail and the sub-run id (the id ticks and transcript anchors carry) are bound together only when a single frame states both, and two rows later proven to be the same agent are merged, the duplicate leaving with a `remove` that names the surviving row (`supersededBy`). A delegating card id never decides which agent a frame belongs to: it is a display field and an "already seen in this process" hint only. One generation rule, one place that constructs terminal facts and one place that emits a `terminal` row delta. Usage is monotonic within one known generation: a tick moves the numbers when its own cycle number matches the row (for an agent without cycle numbers the sub-run id decides), a row frame that binds a new sub-run id to the same tail starts a new generation, a terminal row freezes the phase but not the usage, and finality is reported per counter (`usage_delta.final`), a value-unchanged change of finality included. `reducePanelFrame(state, frame)` is pure — no I/O, no clock read (the projection frame carries its own `nowMs`), no module state, the input state is never mutated, the same input always yields the same deltas and replaying a frame yields none. The delta set is closed (`upsert` / `usage_delta` / `terminal` / `remove`); `subscribeEnginePanelRows` replays a snapshot on subscribe. Twelve end-to-end sequences cover interleaved lanes, the first-published id, revival, reconnect/absence, late subscription, short-lived agents in both orders, historic rows, absent usage never filled with zero, the turn-end sweep scope, the transitional event channel versus the row channel, bounded eviction and snapshot fidelity. The generation criterion now lives in a dependency-free leaf module that both the panel record and the fleet ledger share; each layer keeps its own state (the panel's per-row watermark, the ledger's per-task watermark) but the comparison and the notion of a valid cycle number exist once. A data-flow scan keeps cycle numbers and watermark readings out of every comparison operand in the ledger (with planted violations to prove the scanner speaks, and no false alarm on presence checks or shape tests), and the numeric view is pinned against an independently written integer comparison over a grid that includes malformed values. A background agent terminal notification may carry the child session id; the reducer treats it as binding evidence, so a host that feeds only task events (no fleet rows) gets one row per child with the engine's final numbers, and a revived child no longer keeps the previous generation's tool count beside the new generation's token count. A tick carries its engine cycle number: a background child usage is driven by ticks again when the tick cycle matches the row, a tick from a previous cycle is dropped whole, a tick from a newer cycle neither raises the watermark nor counts, and a terminal tick marks exactly the counters it really carried as final. |
@@ -631,7 +631,7 @@ guard still cross-checks the table by name).
631
631
  | `scripts/run-engine-roster-peers-projection-test.mjs` | The leg's tool roster grouped by peer. For one source word (`mcp` or `a2a`; the word is an open set and is matched exactly), the rows the engine says came from that source are grouped by the peer name each row itself declares — byte for byte, never normalized and never recovered from the tool's name — so two servers whose names differ only in characters that a minted tool name flattens (`a.b` and `a_b`) stay two groups even when their tool names share a prefix. A row of that source that carries no declared peer is reported in a separate bucket instead of being guessed into a group. Each group lists its tools in roster order with their aliases and card identity where the row has them, the set of declaring routes its rows state (a peer declared through more than one route lists all of them), and a count; groups are ordered by peer name in UTF-16 code-unit order, independent of locale. Three kinds of absence stay apart: no roster, an unreadable one, or an unusable source word answers undefined (not known); a readable roster with no row of that source answers an empty result (stated zero, a fresh object each time); and rows without a declared peer land in the bucket. A row that cannot be read drops the whole answer rather than yielding a group that is one tool short, because a short group would report a mounted server as having fewer tools; the roster is read through the package's one roster reader, so a hand-built roster with a bad digest or count, or an array that skips rows while being iterated, answers undefined exactly as the roster projection does, and the result never shares a mutable array with its input. The projection never throws, including on a hand-built roster, throwing getters, proxy traps and peer names such as `__proto__`. One convenience entry point fixes the source word to `a2a`; the `mcp` case goes through the general entry point. The guard drives declared-name pairs that collide once flattened, an end-to-end pass from the raw assembly receipt, a seeded pool of 600 rosters compared against an independently written reference with a conservation identity, and a syntax-tree pin that the grouping function never reads a tool's name, calls a string transform, or uses a locale-dependent comparison. |
632
632
  | `scripts/run-cross-boundary-taint-projection-test.mjs` | The cross-boundary taint reading. When a run executes on a device outside the deployment's trust boundary under a multi-tenant deployment, an approval request carries that fact in exactly two places: the ask-class set (the class naming the cross-boundary obligation) and the mandate word. One reader answers alike for a card, a pending row, a live frame and a seat request: present means a frozen object whose only member is a literal true; anything else — a missing key, a malformed pair, a throwing getter, a prototype-chain key, an unrelated key, a sibling key the wire never carries — is simply absent, never a false and never read as clean. The two sources are read independently from a local snapshot of own data properties (accessors are never run), so one that throws never erases evidence the other can read. The suite drives both card legs, mints real asks with the installed engine and checks the reading, runs a seeded pool against an independently written reference, pins that no code reads the sibling names that have no wire carrier, and checks the one-sentence way out (English and Chinese) against the sentence laws of its neighbours. When a server fixture is installed it also runs the server's own row projection with the installed engine, showing which keys reach a client and that unminted sibling keys do not. |
633
633
  | `scripts/run-release-evidence-test.mjs` | The release-evidence self-check. A release ships a machine-readable record of one full gate run (`release-evidence/<version>.json`, written by the all-gates runner's `--evidence` flag): per gate its class, exit code, discovered-check count (null when the gate's tail line does not state one next to a verdict word — never a made-up 0), skipped-section names and status, plus totals, the runner's own exit code and skip-ratchet violations, and a floor for the next release. This suite judges that record: every gate's exit code must agree with its status, the runner's exit code must agree with the rows, the gate list must equal the registered suites (no missing, no extra, no duplicates), the totals must recompute from the rows, the discovered-check count must not fall below the floor the previous release wrote down, the integrity must be a full sha512 SRI, the tested commit must be an ancestor of the published git head with a clean tree, the tag must be the direct successor of the git head, and the files that differ between them must be declared and limited to the release's own evidence file and the frozen-section table (the table gate may change only inside that table). Forgeries of each kind are built in memory and must each fail with a named problem code; a throwaway git repository exercises the ancestor, successor, tree-diff and replace-object questions; the fill tool is checked to change only the four binding fields and refuse everything else; and the runner's write path is exercised end to end on a synthetic tree of fake gates (pass, partial skip, unparsable tail, skip, fail, timeout, stderr-only skip, ratchet violation), including that credential-like environment variables never reach the file. `--file <path>` judges one record on its own. A record for the current version that does not exist yet is an honest skipped section, not a pass. The negative control rewrites one gate's exit code in the latest released record to 1 while its status stays pass. |
634
- | `scripts/run-print-result-arm-test.mjs` | The arm a print-mode result frame takes, decided in one place. Given the terminal reading of a run and a reading of its last row, `printResultArmOf` returns the subtype, the error flag and the stop reason the result frame should carry. A run that stopped on a limit follows one table that the package's own result frames also read: turns, dollar budget and structured-output retries map to the matching result subtypes, while the wall-clock and token limits, which have no subtype of their own, keep the generic execution-error subtype and are named by a separate `_sema_limit` word on the frame; the last row is not consulted for these. Every other run that stopped without a final answer, whether it stopped on an unrelated failure, waited for an approval, was blocked, or has no readable outcome, follows one rule: if the last row is an API error row or the model's own text (including text salvaged from the write-out window) the frame is a `success` subtype with the error flag set, otherwise it is an execution error; a tool call, a tool result, a non-text last block, empty or blank text, or no row at all never count as the model's text. A completed run is a success without the error flag. Input that cannot be read gives the unknown-outcome arm and never a success. The stop reason is always present, taken from the last row when it carries one and null otherwise, and its type matches the result types of the agent type package. The new `_sema_limit` key is present only for failures that match the limit table, never invented for other failures, and has a reader that rejects unknown words The result text is the last assistant message of the stream, not every message of the turn joined together. |
634
+ | `scripts/run-print-result-arm-test.mjs` | The arm a print-mode result frame takes, decided in one place. Given the terminal reading of a run and a reading of its last row, `printResultArmOf` returns the subtype, the error flag and the stop reason the result frame should carry. A run that stopped on a limit follows one table that the package's own result frames also read: turns, dollar budget and structured-output retries map to the matching result subtypes, while the wall-clock and token limits, which have no subtype of their own, keep the generic execution-error subtype and are named by a separate `_sema_limit` word on the frame; the last row is not consulted for these. A run that stopped to wait for an approval or an answer is always an execution error, whatever its last row is, and the note on how to continue is its first error line, never moved into the result text. Every other run that stopped without a final answer, whether it stopped on an unrelated failure, was blocked, or has no readable outcome, follows one rule: if the last row is an API error row or the model's own text (including text salvaged from the write-out window) the frame is a `success` subtype with the error flag set, otherwise it is an execution error; a tool call, a tool result, a non-text last block, empty or blank text, or no row at all never count as the model's text. A completed run is a success without the error flag. Input that cannot be read gives the unknown-outcome arm and never a success. The stop reason is always present, taken from the last row when it carries one and null otherwise, and its type matches the result types of the agent type package. The new `_sema_limit` key is present only for failures that match the limit table, never invented for other failures, and has a reader that rejects unknown words The result text is the last assistant message of the stream, not every message of the turn joined together. |
635
635
  | `scripts/run-result-way-out-test.mjs` | The way out of a rejected request, as a machine-readable position on the result frame. When a run ends because the model service rejected the request, the error frame may carry `_sema_wayOut` with a closed `word` from the retry-status vocabulary — check the credentials for a 401 or 403, switch model for rate limiting, overload and quota, fix the request when resending will not change the outcome — and, only for the credentials word, the credential source the engine reported for the rejected request. The word comes from the same status code and engine verdict that the interactive lane reads off its synthesized API error row, so both lanes give the same answer for the same ending, including when the status exists only on the engine result. When neither the status nor the verdict names a way out, or the ending is not a service rejection at all, the key is absent: absence means no way out is known, not a failure, and the package never guesses a source it was not told. `resultWayOutOf` reads the key back and rejects unknown words and malformed shapes without throwing, and `wayOutSentence` turns a way out into one sentence in either catalog language, naming the source when there is one and passing it through the display sanitizer. The same word and sentence reach the four faces a print-mode caller shows: the stream-json result, the json result, the text output and the endpoint line on stderr. Carrying the key changes nothing else on the frame |
636
636
  | `scripts/run-workflow-resume-reading-test.mjs` | The **resume reading** on a workflow poll: when the polled run's `resume` object carries how many journaled results were replayed and where the first replay stopped, the projection adds one line right after the agent summary, worded like the engine's own completion notice. A real zero replay (a journal with entries but nothing replayed) is shown as a zero and never folded into absence, and a journal with no entries gets its own sentence. If the object, or both readings, are absent the text is byte-identical to before; each reading stands on its own and a malformed one counts as absent, never as zero. The engine's stop reason is untrusted text and goes through the display sanitizer, the sentences live in the message catalog in both languages, and the note tail is still passed through verbatim |
637
637
  | `scripts/run-park-card-word-arm-test.mjs` | Whether a parked gate is the engine's question or a tool approval, read the same way on both arms. A pending row and a stream-side parked gate now both consult the engine-minted card word first: `question` takes the question arm and any other word takes the tool arm, whatever the tool happens to be called. Only when the word is absent, malformed or outside the closed set do they fall back to the earlier tool-name and gate-kind reading, cell for cell unchanged. The gate-kind judge still runs first, so a gate outside the approval family is never claimed by a card word; the two arms agree cell for cell over a grid of gate kinds, tool names and card words, and the reader is defined exactly once. Since 0.101.0 the reader's closed set is the engine's ten words (a skill card and a workflow card are new), taken from a generated table rather than the SDK's older eight-word list; both new words are read back exactly and still take the tool arm |
@@ -654,7 +654,7 @@ guard still cross-checks the table by name).
654
654
  | `scripts/run-slash-command-gate-test.mjs` | The slash-command gate and the command roster, shared by every host. Whether a slash command is usable on this deployment used to be decided three times — once in the terminal client against its own roster file, and again, from capability readers, in each of the web and desktop command surfaces. The package now owns both halves: a roster of command names in three buckets (local interface state, mappable to the SDK, not applicable to a deployment) pinned to the Claude Code version it was taken from, with per-command overrides, and one judge that takes a command reference and a capabilities snapshot and answers allowed, degraded (visible, with a gap to render), hidden (loaded and callable by exact name but left out of help and completion) or denied, with the reason from a closed set of seven words that hosts can show as the disabled reason. The judge reads capability bits as an open set and fails closed: a missing snapshot, a missing bit or a false bit never counts as present, though a degradable command is kept visible; the host's own two gates are applied first and a missing answer from them means they were not asked, not that they failed. Everything read from the host is read as plain own data (accessors are not run, prototype-only keys do not count, a hostile object counts as absent), command names are matched exactly and through a map so that names like `__proto__` never reach a table entry, and verdicts are frozen and freshly minted. The suite compares the roster and overrides with the terminal's own file when it is available, checks a hand-written verdict matrix, and replays a line-by-line transcription of the terminal's own gate steps over every name, fourteen snapshot shapes, three sources and four host-gate combinations. |
655
655
  | `scripts/run-mcp-status-superset-keys-test.mjs` | Who mints the two extra keys on the headless machine-readable MCP answers. When a headless run answers an MCP status request (one row per server) or a re-dial request on the engine lane, the row carries two keys beyond the Claude Code shape: the connection state of that server on the engine's own leg, and a separate annotation for the host's local connection, which only serves MCP prompts and resources and is neither passed off as the engine nor hidden. The terminal client used to mint both keys itself; the package now owns the key names, the value shapes and the rules for when each is present, so every host that answers these requests produces the same keys. The engine-leg value is a frozen copy of the reading the package already derives from the engine's roster (not shared with the caller's object, copied field by field so a new field in the reading is carried along without a second list of names); the key is absent off the engine lane, which is not a statement that the server is down. The local-connection annotation carries the local state word as given, always says it serves prompts and resources, and carries the failure text only when the state is `failed` and the text is non-empty; when the state word cannot be read the key is simply absent rather than invented. A re-dial answer carries only the local-connection key. The suite replays a transcription of the terminal's existing minting over every engine-leg reading arm, twelve state-word shapes and five failure-text shapes and requires the same keys with the same values, and pins the absence rules, the freezing, and the plain-own-data reading of hostile inputs. |
656
656
  | `scripts/run-task-notification-entry-sampling-test.mjs` | Notification payloads are read from one frozen sample taken at the entry, and the completion identity is carried through. A background-task or workflow-completion notification used to be read field by field at several places (the validity check, normalisation, the rendered text, the de-duplication key, the two ledger verbs), so an object with accessors or proxy traps could show one value to the check and another to the renderer. Each entry — the interactive frame projection, the workflow-completion frame, a host handing frames straight to the adapter, the public helpers, the `-p` output builder, the fleet background notification, the idle watcher's probe answers, the structured body of a TaskOutput result, and transcript replay — now takes exactly one sample, before its first read of the payload (the replay cursor read in the approval path is deliberately outside this and unchanged): own data properties only, accessors never executed (their value is simply absent), proxy traps that throw leave that key absent, prototype-chain keys do not count, cycles and over-deep subtrees are cut, and the result is a deeply frozen copy carrying a package-private brand so a sample passed to another entry is not sampled again. Ordinary JSON round-trips value for value. Notification facts additionally carry the engine-reported completion id and cycle number when the engine reported them (absent means the engine did not say; nothing is defaulted to zero or guessed from the task id), the cycle number the idle watcher keeps for its own bookkeeping is never presented as the engine's, and a finished TaskOutput card (background command, background agent or generic task) carries the completion id under an extra key only when the card is final by its own structured status and the engine supplied it; the cycle number is an integer from 1 up and only when the source reported it. The suite counts reads through instrumented proxies at every entry, compares two independent samples of equal input cell by cell, and proves the counting rule itself turns red on a second sample. |
657
- | `scripts/run-host-settings-source-test.mjs` | The per-turn host settings source (`HostSettingsSource`) that replaced the process-wide settings slot, and the decisions the assembly entry now makes from it: every one of its six members is required and is called exactly once per turn (the hooks plan reuses that single read), a member that throws or answers malformed degrades only its own reading, and an out-of-set workspace trust reading counts as unreadable rather than accepted. The single read is a detached capture: records come back without a prototype, so a value someone later installs on `Object.prototype` cannot change a governance decision; carrier and issue lists are read once by index from own data properties; revoked proxies, shared references and oversized readings are refused as malformed, with bounded work. The suite also pins the six-word managed-settings table cell by cell, the stale-read adoption gate (truncated or rewritten files keep the last trusted read and keep sending its deny and ask rules, deliberate `{}` is adopted, allow rules only follow a clean read, and a changed set of files clears the allow history), the session-state update rule (a state carries its version and the version it was derived from; a host commits only when the stored state is that predecessor, so a concurrent lane or a repeated confirmation cannot overwrite it), asking before an interactive turn proceeds without a settings file that did not load, the output-style decision, and the per-layer permission merge, including the one deliberate loosening: allow rules from the launch-flag settings layer are now admitted. The hooks plan also reports, layer by layer, the repository-controlled settings layers whose hooks were withheld because the host reports no workspace-trust step (`settingsWithheld`, with the sentence listed among the not-run notices); the suite replays one five-layer document under all three trust readings, checks that layers without hooks or narrowed away by managed-only governance are not listed, and pins that the admission judge answers only `trust_withheld` for the hooks category. |
657
+ | `scripts/run-host-settings-source-test.mjs` | The per-turn host settings source (`HostSettingsSource`) that replaced the process-wide settings slot, and the decisions the assembly entry now makes from it: every one of its six members is required and is called exactly once per turn (the hooks plan reuses that single read), a member that throws or answers malformed degrades only its own reading, and an out-of-set workspace trust reading counts as unreadable rather than accepted. The single read is a detached capture: records come back without a prototype, so a value someone later installs on `Object.prototype` cannot change a governance decision; carrier and issue lists are read once by index from own data properties; revoked proxies, shared references and oversized readings are refused as malformed, with bounded work. The suite also pins the six-word managed-settings table cell by cell, the stale-read adoption gate (truncated or rewritten files keep the last trusted read and keep sending its deny and ask rules, deliberate `{}` is adopted, allow rules only follow a clean read, and a changed set of files clears the allow history), the session-state update rule (a state carries its version and the version it was derived from; a host commits only when the stored state is that predecessor, so a concurrent lane or a repeated confirmation cannot overwrite it), asking before an interactive turn proceeds without a settings file that did not load, the output-style decision, and the per-layer permission merge, including the one deliberate loosening: allow rules from the launch-flag settings layer are now admitted. The hooks plan also reports, layer by layer, the repository-controlled settings layers whose hooks were withheld because the host reports no workspace-trust step (`settingsWithheld`, with the sentence listed among the not-run notices); the suite replays one five-layer document under all three trust readings, checks that layers without hooks or narrowed away by managed-only governance are not listed, and pins that the admission judge answers only `trust_withheld` for the hooks category. The per-layer merge also pins the order inside a trust group: a rule written by several sources keeps the strongest source and sits where that source wrote it (a rule repeated within one source keeps its first position, and a weaker source listing it again does not move it), while the order between groups, between the three buckets, and the request body of any input with no rule shared across sources stay byte-identical. A host that hands in a layer document already folded with extra tightening rules (for example a deny or ask list taken from the repository environment) is read the same way as one that hands in the file text: each rule is judged by layer, lands in the trust group of the layer it was folded into, and reaches the request body like a rule written in a file, while a rule folded into the allow direction is admitted no more than a file-written one, and the folded tightening rules are kept when the workspace-trust reading is declined. |
658
658
  | `scripts/run-mcp-server-name-collision-test.mjs` | MCP server names that collide once the engine builds tool names from them. Two declared servers whose names the engine reads as one — `a.b` and `a_b`, or one name written twice — are refused by the engine as a pair, at mount, before any server is started, and the run ends with the engine's refusal code. The client passes that code through unchanged on every carrier a refusal arrives on (a 400 body, a terminal failure, a result frame, the folded resume refusal) and gives it one sentence, in English and Chinese, that says what the engine said and what to change; it does not say which of the two is still used, because neither is. A code the table does not know passes through as it came and gets no sentence. The client never judges this collision itself: the local MCP configuration projection puts both servers on the wire, names byte for byte and in order, with nothing reported as dropped. The suite runs the installed engine on a colliding pair (both spellings, both orders, and one name written twice) and pins that the code is the engine's own, byte for byte, and that no server was started. |
659
659
  | `scripts/run-project-memory-projection-test.mjs` | Request key `projectMemory` and its manifest reader. The request port takes one closed-set word, `disabled` (this run does not read the deployment's project instruction files); an absent value means the request carries no such key at all — the package never mints an "on" word, and a bad value (any other string, `false`, a number, an array) is refused loudly at assembly time on every lane without echoing it. The reader `projectMemoryOf(manifest)` answers one of the manifest's three words — `on`, `disabled`, `absent` — and answers `undefined` for a missing member, an out-of-table word, a non-string value or an unreadable object; it never guesses `absent`, and it reads own data properties only. The gate cross-checks both word sets against the installed engine's declaration files and change record, mints manifests with the engine's own deriver, and runs the same cells against four hand-made broken implementations (mint-on-absence, guess-absent, fold-false-into-absence, read-the-prototype-chain) that must each turn red. A capability reader, `observedProjectMemoryCapability(baseUrl?)`, reads the server's top-level `projectMemory` capability bit from the shared capabilities cache (own data property, strictly `true` to confirm; absent / declined / unreadable / unobserved otherwise), and a fragment helper, `projectMemoryDeclaration(intent, reading)`, hands the request key over only when that reading is confirmed (older servers refuse an unknown top-level request key as a whole): otherwise it returns an empty fragment plus a machine-readable cause (`capability_absent` / `capability_declined` / `capability_unreadable`), and a bilingual sentence says the project instruction files are still sent to the model. There is no version floor anywhere in the helper. A fixture leg and a server-source leg record that the capability bit is minted at the top level by the servers that accept the key and not by older ones. The capability reader's default anchor is read inside the guarded section: a default engine target whose address getter throws yields `unobserved`, as it does for the other readers of the same family. |
660
660
  | `scripts/run-decide-row-unrevivable-projection-test.mjs` | A stored approval card the engine can no longer revive. When a parked approval cannot be brought back — its row has no agent name or no root-session anchor — the engine answers the decision with one refusal code, before it claims anything, and neither approving nor denying the card will ever take effect. The client reads that code with one strict equality test (no prefix, case or whitespace folding) and puts a single optional flag on the refusal reading, present only when the engine said so and never written as false; the flag rides the same code on every carrier a refusal arrives on (a thrown error, a plain response body, and the settled outcome of both decision legs and of the send machine). The flag is distinct from the sibling refusal that asks for a corrected answer, which has the same standing and needs the flag to be told apart, and a body that calls itself retriable does not override it. The client also gives one sentence, in English and Chinese, that says the card cannot be recovered, that nothing chosen on it will apply and that it has to be cleared on the server; it never suggests trying again. The suite runs the installed server's own decision function on rows without a name and without a root anchor, feeds the real bodies through the real SDK error mapping, and swaps in deliberately wrong judges (prefix, substring, case-folding, trimming, any prefixed code) to prove the truth tables turn red. |
@@ -674,6 +674,10 @@ guard still cross-checks the table by name).
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` and `context.image_removed` notices have fact readers; 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` only when the gate identity is known and the engine reports `planReviewCheckpointId: true` (through an injected connection, only under the connection's own `capsBaseUrl`, never borrowing the installed target's evidence); `observedSandboxGitCredentialPlane` reads `credentialPlane.sandboxGit` in five states and never reads an absent bit as off; the device lane reading carries `relayUrl` when present; an `inheritEnv` refusal of a request that carried the field, with any value, carries the engine's `reason` word when the field is named, and its sentence states which values the engine accepts instead of claiming the request asked for the whole environment. Session-sync retries stay with the client's retrier, which stops on a terminal permanence. |
676
676
  | `scripts/run-pkg-policy-registration-test.mjs` | Package policy registration. The package's own `package.json` is checked, item by item and by equality, against its row in the cross-package policy table: the package-name sets of the four dependency kinds (version ranges are not compared; one name more or fewer is red and the difference is printed), the `exports` key set, the pinned version (equal to the `version` field or to the highest released CHANGELOG section, so both sides of a version bump are legal, and a bump the table did not follow is red), and the first sentence of `description`. The table is taken from `SEMA_PKG_POLICY_PATH` when that is set (no fallback), otherwise from a sibling checkout found by walking up from the package root; when it cannot be reached the whole gate reports a skipped section and exits 0, which is not a pass. A negative control adds one unlisted dev dependency and expects the dependency check to go red |
677
+ | `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
+ | `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). |
679
+ | `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. |
680
+ | `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. |
677
681
 
678
682
  Each suite carries a floor that only moves up — a refactor that stops executing a group of
679
683
  assertions is a failure, not a quieter pass. Guards anchor on the **installed artefact's content**