@sema-agent/core 7.25.0 → 7.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (75) hide show
  1. package/CHANGELOG.md +81 -0
  2. package/dist/core/arg-summary.d.ts +0 -7
  3. package/dist/core/arg-summary.js +2 -1
  4. package/dist/core/ask-origin.d.ts +84 -0
  5. package/dist/core/ask-origin.js +31 -3
  6. package/dist/core/auto-compaction.d.ts +5 -0
  7. package/dist/core/auto-compaction.js +7 -0
  8. package/dist/core/auto-mode-directive.d.ts +45 -0
  9. package/dist/core/auto-mode-directive.js +47 -0
  10. package/dist/core/checkpoint-store.d.ts +10 -1
  11. package/dist/core/engine-notice.d.ts +10 -0
  12. package/dist/core/gate-lanes.js +18 -10
  13. package/dist/core/governance-codes.d.ts +1 -1
  14. package/dist/core/governance-codes.js +3 -0
  15. package/dist/core/hooks.d.ts +3 -0
  16. package/dist/core/mcp-probe.d.ts +122 -0
  17. package/dist/core/mcp-probe.js +132 -0
  18. package/dist/core/memory-engine/engine.d.ts +6 -1
  19. package/dist/core/memory-engine/engine.js +30 -2
  20. package/dist/core/memory-engine/index.d.ts +1 -0
  21. package/dist/core/memory-engine/index.js +1 -0
  22. package/dist/core/memory-engine/shadowing.d.ts +58 -0
  23. package/dist/core/memory-engine/shadowing.js +40 -0
  24. package/dist/core/memory-engine/tools.d.ts +33 -1
  25. package/dist/core/memory-engine/tools.js +136 -31
  26. package/dist/core/memory-engine/types.d.ts +10 -0
  27. package/dist/core/memory.d.ts +60 -0
  28. package/dist/core/memory.js +47 -2
  29. package/dist/core/permission-rule-store.d.ts +51 -8
  30. package/dist/core/permission-rule-store.js +112 -65
  31. package/dist/core/reminder-mint.d.ts +41 -0
  32. package/dist/core/reminder-mint.js +55 -0
  33. package/dist/core/runner/compaction-restate.d.ts +9 -0
  34. package/dist/core/runner/compaction-restate.js +20 -0
  35. package/dist/core/runner/contracts.d.ts +18 -1
  36. package/dist/core/runner/denial-limit-arms.js +1 -1
  37. package/dist/core/runner/permission-rule-lanes.d.ts +5 -17
  38. package/dist/core/runner/permission-rule-lanes.js +5 -23
  39. package/dist/core/runner/prepare-ask-lane.js +2 -0
  40. package/dist/core/runner/prepare-context-lane.d.ts +4 -0
  41. package/dist/core/runner/prepare-context-lane.js +7 -2
  42. package/dist/core/runner/prepare-inherited-gate.d.ts +5 -1
  43. package/dist/core/runner/prepare-inherited-gate.js +4 -2
  44. package/dist/core/runner/prepare-memory.js +47 -6
  45. package/dist/core/runner/prepare-park-ask.js +1 -0
  46. package/dist/core/runner/prepare-policy-chain.js +8 -4
  47. package/dist/core/runner/prepare-suspend-saga.d.ts +2 -0
  48. package/dist/core/runner/prepare-suspend-saga.js +3 -1
  49. package/dist/core/runner/prepare-task.js +7 -4
  50. package/dist/core/runner/run-leg.js +9 -1
  51. package/dist/core/runner/run-recovery-lanes.js +2 -2
  52. package/dist/core/runner/run-terminal-adoption.js +2 -2
  53. package/dist/core/runner/run-turn-boundary.js +2 -2
  54. package/dist/core/store-contracts/permission-rule-write-contract.d.ts +20 -0
  55. package/dist/core/store-contracts/permission-rule-write-contract.js +77 -0
  56. package/dist/core/task-result.d.ts +11 -0
  57. package/dist/core/task-spec.d.ts +31 -0
  58. package/dist/core/tool-catalog-entries.js +3 -3
  59. package/dist/core/tool-policy.d.ts +38 -0
  60. package/dist/core/tool-policy.js +35 -24
  61. package/dist/core/types.d.ts +1 -1
  62. package/dist/index.d.ts +7 -2
  63. package/dist/index.js +6 -1
  64. package/dist/stores/file/permission-rule-store.js +48 -29
  65. package/dist/tools/fs/bash-lexer.d.ts +13 -0
  66. package/dist/tools/fs/bash-lexer.js +61 -0
  67. package/dist/tools/fs/bash-program-position.d.ts +32 -3
  68. package/dist/tools/fs/bash-program-position.js +33 -2
  69. package/dist/tools/fs/bash-readonly-classifier.d.ts +6 -4
  70. package/dist/tools/fs/bash-readonly-classifier.js +55 -18
  71. package/dist/tools/fs/shell-read-plan.js +9 -11
  72. package/package.json +1 -1
  73. package/test/export-surface.snapshot.json +29 -1
  74. package/wire-changes/core@7.25.1.json +64 -0
  75. package/wire-changes/core@7.26.0.json +695 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,78 @@
1
1
  # Changelog
2
2
 
3
+ ## 7.26.0 — 2026-09-21
4
+
5
+ ### Six root exports the neighbours were hand-copying (#995; additive; +6 exports; @server @cli @client-core)
6
+ - `TASK_OUTPUT_REPLAY_KINDS` / `TaskOutputReplay` (the `background_bash` poll body's `replay` vocabulary — the server projected `cursorSemantics` from a hand-written two-word list), `REMOVAL_LIVENESS_WORDS` / `RemovalLiveness` (the removal read-back's three words — the server derived them with `Extract`), `normalizeReadRootCandidate` / `READ_ROOT_CANDIDATE_DIR_MAX` (the one validator a card renderer needs to accept a `ReadRootGrantCandidate`, with the directory-length bound — the server kept a weaker copy). Nothing moves; a consumer that pinned its own copy can delete it.
7
+
8
+ ### A no-run MCP probe: connect, list, close, with the run's own rows (additive; +2 exports; @server @client-core @cli)
9
+ - **What lands.** `probeMcpServers(specs, opts?)` (`src/core/mcp-probe.ts`, exported from the package index with its result type `McpProbeResult`): dial every declared MCP server, read its tool list, close it again — no task, no registry entry, no session, no store, no model. It answers `{ entries, servers }`, where `entries` are the rows a prepared leg publishes on `wiring_manifest.mcp[]`, minted by the SAME projection from the SAME materialization (one mint point — nothing here assembles a row), and `servers` carries the probe-only reading per server: `connectMs` (measured wall time of that dial), `tools` (the namespaced names that would mount) and `errorCode`.
10
+ - **Contract — index alignment.** `entries` and `servers` always have the length of `specs` and are ordered by it: `entries[i]` and `servers[i]` describe `specs[i]`, with failed and timed-out servers keeping their slot. A consumer pairs its own declaration list by index, never by name matching.
11
+ - **Contract — residue bound, stated part by part.** The call does not settle while a handle it opened is undisposed, including a dial the clock abandoned (disposed when it lands, awaited before the call resolves or rejects). A dial has two request-BUDGET phases — the handshake (one request) and the initial `tools/list` (a paginated walk whose whole budget is that same number) — each carrying the startup-phase bound (`MCP_TIMEOUT`, else the MCP SDK's 60 000 ms default); the stdio close ladder then adds ≤ 4 000 ms (SIGTERM, then SIGKILL). So, **per dial**, against a peer that either answers or fails its requests: that dial's residue is gone within `max(timeoutMs, the handshake budget + the listing budget) + 4 000 ms`, and a server that exits when its input closes is gone at return. The CALL is the walk over the declarations, so its own duration is those per-dial times summed over `ceil(n / concurrency)` rounds — not a whole-call guarantee. **One part is bounded by nothing**: a transport SEND (the post-handshake `notifications/initialized`, an HTTP body read) carries no request deadline and the dial owns no cancellation, so a peer that answers the handshake and then stalls that send holds the call open. The bound belongs to the dial, not to this composition — recorded in `docs/KNOWN-LIMITS.md`.
12
+ - **`timeoutMs` bounds the verdict, not the transport.** A dial the clock abandoned keeps running while the walk moves on, so `concurrency` limits the verdicts pursued at once and NOT the transports open at once: with a short clock against an unresponsive fleet, in-flight dials reach one per declaration. Holding the slot instead would make one stuck server the walk's pace, which is what the clock exists to prevent.
13
+ - **Knobs.** `principal` (threaded exactly as a task's is), `timeoutMs` (bounds the per-server VERDICT, never the dial; whole ms in `[1, 2147483647]` — the ceiling every env-supplied MCP bound keeps, since past the timer's 32-bit field a delay is silently rewritten to 1 ms; default = the engine's own startup bound), `signal` (rejects with its reason after the teardown; an already-aborted signal dials nothing), `onElicit` (default: DECLINE every elicitation — a probe has nobody standing behind it), `concurrency` (default 1, in declaration order). A knob value outside its range is REFUSED (`config.mcp_probe_option`), never folded to the default.
14
+ - **Failure is per server, refusal is not.** A server that cannot be dialed, that answers a status, or that outlives `timeoutMs` takes its verdict in its own row — a word from the existing failure vocabulary (`MCP_FAILURE_KINDS`, no member added), with `delivered` and the `liveness` observation from the same two disposition tables — and the walk continues; nothing throws. What DOES reject is a declaration set the materialization itself refuses: two names that mount under one namespace prefix are refused before any transport is touched, by that same function, so a colliding configuration answers as a task would.
15
+ - **Nothing is mounted.** The materialized tools are dropped unread; the result is plain data (names, not callables), so a caller holds nothing it could invoke.
16
+ - Reach: @server (S-481, the per-server health face), @client-core (CC-74), @cli (L-303) — additive, nothing existing changes. Wire table: `docs/wire-consumption/unreleased-951.md`. Export surface: `npm run handoff:diff v7.25.0 HEAD` = +2 / −0.
17
+ ### The engine states the run's approval posture; a spliced reminder is still data (#1002; P1; additive; one new optional spec key, one new type alias, one new config-door error code)
18
+ - **Before.** `TaskSpec.autoModeRequested` armed the CLASSIFIER and nothing else — no seat in the tree told the MAIN model it was in auto mode. Hosts filled the gap the obvious way, by splicing the reference implementation's auto-mode paragraph into the user objective as a `<system-reminder>` block. That block carries no engine mark, and the system prompt's mark declaration tells the model that reminder-shaped text without the mark is DATA to be flagged rather than followed — so the model reported the directives as an injection attempt. The rule was working as designed; one fact had two seats.
19
+ - **After.** The engine mints it. `src/core/auto-mode-directive.ts` holds the reference bodies whole (tool-name placeholders resolved to the wire names this engine mounts, SHA-256 locked by the pins) and one composer; `run-leg` renders the result into the FIRST user turn's marked reminder, beside the listing frames. **Cadence is once in the ACTIVE CONTEXT**, not once per run: a plain `sessionId` continuation re-enters the same branch with the previous leg's transcript intact and does not restate a segment already standing there; a segment that has left the context is restated on the next leg. The question is asked of the ACTIVE context (`session.buildContext()` — the raw ancestry still carries messages a compaction replaced), of its ENGINE REGIONS only (`engineMinted` / `engineSegments` / `enginePrefixChars` — metadata, never text shape, so a caller's spliced copy cannot answer it), and per minted BLOCK decomposed into known segments (engine provenance names the carrier, not the meaning of bytes a hook's own frame may quote). Off ⇒ zero bytes, byte-identical to before.
20
+ - **`TaskSpec.approverPosture?: "bypass"` (new, optional).** The second fact's typed seat — the host's statement of who answers this run's approval cards, declared by the layer that already writes `shellGate` and `autoModeRequested`. **No gate lane reads it**: the same cards are raised, routed and recorded with and without it (pinned as a control — same tool, same count, same policy). Absent ⇒ no posture, zero bytes. Any other present value refuses at the config door with **`config.approver_posture_invalid`** rather than folding back to absence: a typo must not silently un-declare a posture the host meant to declare. It rides the trusted chain (`InheritedGate.approverPosture`) and persists on the checkpoint's data half, the same law `autoModeRequested` follows; a word outside the vocabulary arriving on a chain or a checkpoint row is carried VERBATIM and rendered by nothing — the screens are the caller's door and the renderer, and a leg that cannot read a word must not destroy it (a checkpoint written by a newer version would otherwise lose its posture permanently on the first older-leg re-suspend).
21
+ - **Who is hurt.** ① A host that keeps splicing now sends two copies — its own (unmarked, data, unchanged) and the engine's (marked, authoritative). Nothing breaks; the splice can be removed. ② `UserMessage.enginePrefixChars` grows by the frame's length on an auto-mode or bypass-posture task's first message. Every derived view that cuts by that metadata (classifier window, compaction serializer, prompt-suggestion projection) cuts correctly with no change, but a consumer that hard-codes an expected first-message body for such a task sees the new head. ③ A host bucketing config-door `terminal.code` words on an exhaustive list gains one member. Compensation: none is owed for ①/③; ② is the metadata contract working as documented.
22
+ - **Deliberately NOT done, and why.** The reference implementation reads ONE permission mode, so its bypass arm REPLACES the auto-mode body; here the two are independent seats and a run may hold both, so each renders its own segment and both are emitted when both hold — silencing the auto-mode paragraph while the classifier is armed would withhold guidance the run is operating under. Registered divergence, pinned by a mutation. The reference's consent-flow paragraph (its batch-the-asks block) is not shipped: its upstream gate has no fact seat here — `docs/KNOWN-LIMITS.md` KL-1002a. A within-turn-loop compaction still takes the frame out of a context the run continues in (measured; restored on the next leg, and the reference implementation has the same shape) — KL-1002b.
23
+ - **headless.** Zero change to any headless lane: no gate decision, no card, no notice code, no wire frame key moves. A headless deployment that declares neither fact renders byte-identically to 7.25.1.
24
+ - `test/backlog1002-auto-mode-directive.test.ts` (15 pins, 7 mutations recorded); `docs/wire-consumption/unreleased-1002.md`; `docs/INTEGRATION-CORE.md` §4.7.
25
+ ### A failed rule write says WHICH failure it is, and a no-op says what it saw (#996; P2; additive type face; no wire key, +0 root exports)
26
+ - **Before.** `AddResult.failed` / `RemoveResult.failed` carried one prose sentence for two facts a caller must branch on differently: the store ANSWERED and nothing landed (no write face, a target the type face refused, a read that failed before any write, a refused preflight, an epoch seal, a compare-and-set that lost every attempt), versus the apply THREW — which proves nothing either way, because a backend can commit and lose its acknowledgement. `RuleAddLanding.committed` had the word already; both entry points dropped it, so a consumer either parsed the sentence or answered "retry" to every failure.
27
+ - **After.** Both `failed` arms carry `committed: "no" | "unknown"` — one vocabulary, registered (`RULE_WRITE_COMMITTED_WORDS` / `RuleWriteCommitted`, `docs/CLOSED-SETS.md`), spelled once and read by all three sites. The add entry passes the landing's own word through rather than re-deriving it; the removal entry reads it off the flag its retry loop already set (a thrown apply, never the message text). `error` is unchanged and stays a sentence for a person.
28
+ - **The `no-op` arm carries `stillLive`.** `RemoveResult`'s nothing-to-remove arm reports `RemovalLiveness` through the SAME read-back the `removed` arm takes (`readRuleLiveness`, one function, both arms): a no-op is decided on a snapshot, and an approval landing in that window leaves the rule STANDING while the caller is told there was nothing to remove. A consumer that minted its own liveness word to cover that window now reads core's — one question, one answer. A read-back that could not be taken is the third word, never `no`.
29
+ - **Two pre-apply failures reach the union at all (adversarial round).** `removePersistedRule` awaited `writer.nextDot()` outside its error handling, so a backend that mints by WRITING (the file backend persists its counter) answered an ordinary disk failure with a REJECTED promise — the one exit that carries no word; the add entry had caught the same failure since it was written. Acquiring the write face is likewise an operation on a backend that guards its bytes with a process lock, and both entries read it outside a catch. Both are now `{ status: "failed", committed: "no" }` through one resolution (`resolveRuleWriter`), which is what "the failed arm says which failure it is" means for a failure that could not previously reach the arm. Who is hurt: a caller with a `try`/`catch` around `removePersistedRule` that relied on the rejection — the declared type was always `Promise<RemoveResult>`, and the add entry's shape is the one both now have.
30
+ - **Who is hurt.** Nobody who READS these results: both changes are additive members on existing arms. A consumer that CONSTRUCTS an `AddResult`/`RemoveResult` (a test double, a fake lane) adds the key. One sentence changed: a removal whose compare-and-set lost every attempt AND whose read-back then failed used to be reported "indeterminate"; it is `committed: "no"` and says the store answered every attempt — the add entry never hedged that case either (`committed: "no"` is terminal there, read-back or no read-back).
31
+ - Reach: **@server** — the revoke/write routes map `committed: "no"` to a terminal answer instead of 503+retry, and the no-op arm's own `stillLive` read-back (`rules.ts`, the second mint point) retires in favour of core's word; **@cli** — a revoke result's "it is gone" branch stays `stillLive === "no"`, now on both 200-shaped arms; **@client-core** — type rows only. Headless: unchanged behaviour, one more field on two arms. `test/backlog996-failed-arm-committed.test.ts`.
32
+
33
+ ### An unnamed write-delta kind is refused by both backends, identically (#1001; P2; loud refusal where the file backend silently deleted)
34
+ - **Before.** `RuleWriteDelta`'s four arms were dispatched by an if/else chain whose LAST arm swallowed everything else: the in-memory backend sent an unnamed kind into `applySyncJoin` (which threw for want of an inbound state), while the file backend folded it as a **tighten-delete** — a delta carrying a well-formed tombstone under an unnamed kind was written to disk as a removal. Two backends, opposite answers, for an input that is DATA (a foreign caller, a replayed record, a deserialized join), not a typed call the compiler has vetted.
35
+ - **After.** Both backends dispatch an exhaustive `switch` with a `never` arm — the compile-time half, which reds when a fifth kind joins the union — over ONE shared refusal (`unknownRuleWriteDeltaError`), so the two cannot drift in what they say. The file backend's "unknown kind with a tombstone is a delete" arm is gone; a refused delta leaves the bucket byte-identical and the revision unmoved.
36
+ - **The contract kit says so out loud.** `permissionRuleWriteContract` (new, exported) is the write-face acceptance suite an out-of-repo twin runs: the named arms land, a stale revision is ANSWERED (`conflict`) rather than thrown, and a kind outside the union is refused instead of folded as the arm it resembles. The unnamed-kind clause ships WITH the positive control in the same run, because a backend that throws at everything would otherwise pass it.
37
+ - **What the kit MEASURES (two adversarial rounds).** Three clauses were added after wrappers passed the suite while being broken: a landed write must ADVANCE the revision and spend the token the caller held (a constant revision is a number, not a fence — such a store accepts every write against any snapshot); a declined write must move nothing in the RAW state, not merely in the live rows (a landed delete is invisible to a live-rows check); and the raw face must RETAIN the tombstoned add and the whole tombstone (that is the evidence both write entries reconcile a lost acknowledgement against, and a dropped tombstone also lets a collected add reappear through a sync join). Each clause ships with the broken backend that motivated it, in `test/backlog996-write-delta-kinds.test.ts`.
38
+ - **Who is hurt.** A caller that relied on the file backend folding an unnamed kind into a delete — none in-tree, and the behaviour was unreachable through a typed call. Exports +2 (`permissionRuleWriteContract`, `PermissionRuleWriteContractHooks`), removals 0.
39
+ - Reach: **@server** — the store twin's acceptance floor gains one suite to run (the sync kit is unchanged); **@cli** / **@client-core** — zero. Headless: a malformed delta from a foreign backend caller now throws where the file backend previously wrote a tombstone. `test/backlog996-write-delta-kinds.test.ts`.
40
+ ### A mandate is a fact about the ask, not about a lane — `AskRequest.mandated` (#997; P2; approval face; additive, no export change)
41
+ - **Before.** The only way an approval surface could learn that a card was MANDATED — that no persisted allow rule and no standing posture could retire it — was `AskRequest.ruleOffersAbsence === "mandated"`, a seat that reports what the RULE-OFFER LANE has to give. That lane speaks for one tool, only where a rule store is wired, and only on a task that can hold a rule; its own contract says absence is not a claim. Measured on the base of this entry, real Runner, `cat <path outside the session roots>`, both `shellGate` doctrines: `Bash` with a store and a principal carried `"mandated"`; the SAME command on `Monitor`, on `Bash` with no store, and on an anonymous task carried nothing. One approval card in every row; the mandate — the read station's own structural demotion — present in every row.
42
+ - **After.** `AskRequest.mandated?: true` (and the `PermissionResult` ask-arm member it is copied from) states it on the ask. Minted ONCE, at the gate's single origin stamp, from the one derivation the card's rule-offer word is also read from (`ruleClearanceOf`): the door SET decides the bit, the incumbent display ladder decides the word. Which door a card shows is a display choice; whether the ask is mandated is not — so a card worded `shadowed` over a probe mandate is mandated all the same, which the two-enumeration shape got wrong.
43
+ - **The doors,** in one place: the tool seat's `egress` / `irreversibility` marks, an operator `shellGate: "always"`, the shell read station's structural demotion or a probe that timed out or threw, a write the protection judge matched or could not resolve, `requiresRealApproval`, a hook-raised ask, and an ask decided at an ANCESTOR's frozen approver. **Not** a door: a person's own explicit `ask` permission rule — the lane words that `shadowed`, because a rule they wrote is already speaking.
44
+ - **What the bit says, and what it does not.** It says a persisted allow RULE may not clear the card. It does NOT say an approver may not answer it: a person, a hook and a deployment's blanket `onAsk: "allow"` seat all resolve a mandated ask exactly as before, and `requiresRealApproval` remains the separate, stronger "a person, not a configuration" demand. A shell whose POSTURE seat declines to auto-answer mandated cards reads this member to know which they are — @cli L-459, @client-core CC-110 (frame mirror), @server in transit. Absent on an older engine is not the opposite claim; tell the two apart by engine version, not by this seat.
45
+ - **Who is hurt.** For an approval surface, nobody: the member is additive, every allow/deny/ask verdict a deployment sees is unchanged, and a surface that ignores it renders the card it rendered before. Inside the gate one thing does move, from the same predicate: the persisted-allow-rule arm and the read-only-command arm now read the shared derivation instead of enumerating the boundary a second time, so a `mandated` bit an upstream layer set (an ancestor's gate, a composing policy) is no longer cleared by a matching allow rule or by the read-only tables — measured before this entry: a policy ask carrying the bit, with a covering `Bash(npm run build)` allow rule, was allowed with nobody asked, and `ls` was cleared by the read-only arm. Tightening only, and exactly on the mandate set: the arms read the ENFORCEMENT half of the shared derivation (`ruleMayClear` — the mandate doors plus a person's explicit `ask` rule), never its display word, so `persistedRuleShadowed` — an account of a refusal that already happened, display-only by contract — does not refuse a second time. A deployment that never carries a mandate onto an ask is byte-identical. Where the new arms do refuse a carried mandate this task's own marks cannot name, the card no longer claims an ask-me-each-time rule matched (it says the call arrived already marked), which the old catch-all sentence would have.
46
+ - Tightening-only in both directions that matter: the derivation has no clearing arm, and the policy fold carries a folded-away ask's bit onto the survivor exactly as it already carries `probeMandated` — so the inherited (delegation) lane, which replays an ancestor's stamped ask as one layer of that fold, cannot be where a mandate goes missing. Public export surface unchanged (`handoff:diff` 0/0); `ruleOffersAbsence` and `probeMandated` are untouched.
47
+
48
+ ### Merge-tree adversarial round: two fixes (R1 #997 × durable park; R2 #1002 × compaction × resume)
49
+ - **R1.** A mandated ask that PARKS (durable approval, no live approver) now carries the stamped mandate through the ask carry (`AskCarry.mandated`, minted by `carryOf` at every park site) into the row's rule-offer facts: the parked row offers NO rule and says `ruleOffersAbsence: "mandated"` — what the live card says for the same ask. Before the fix the park sites dropped the bit and the row of a policy-mandated `Bash npm run build` offered `Bash(npm run build)` + `Bash(npm run *)`, a standing grant solicited for an ask no rule can clear. The row itself carries no `mandated` twin (KL-997a): a durable consumer reads the mandate as the closed reason word.
50
+ - **R2/R3.** The posture directive now stands in EVERY request's context, not only the first turn's: (i) the moment a compaction lands — at all three compaction sites (turn boundary, forced/guard pass, end-of-task) — the runner asks the first-turn question of the compacted context (`statedReminderBodiesOf`, one reader) and appends the missing segments as one engine-minted frame, the git frame's post-compaction carrier; (ii) a request clipped to the window keeps or reinserts the newest posture carrier through the same protection slot the git frame rides. Before the fix only the first-turn branch asked: a compaction before a durable suspend, a compaction inside the resumed leg (the approved call's large result), and a per-request clip each left the model without the directives while the checkpoint still carried the posture. The reference implementation re-mints its attachment whenever the message array no longer holds one; a compaction or clip is where that happens here, so the restatement rides the same events (cadence stated in the ship post as a divergence candidate). A context that still holds the frame mints nothing (unchanged; never doubled).
51
+ - Pins: `test/backlog7260-review-fixes.test.ts` (R1 + control; R2 compaction + clip before a suspend; R3 compaction inside the resumed leg + control), each red on the merged tree before its fix; `backlog1002` P7 restated (restate at once, never doubled). Headless: R1 changes a parked row's offers only where a mandate is declared; R2/R3 change only what a run with a declared posture sends the model.
52
+
53
+
54
+ ## 7.25.1 — 2026-09-20
55
+
56
+ ### A bare `NAME+=value` word is the assignment bash reads, not a program name (#993; P2; read boundary; narrowing on deny rows, widening on the rest; no wire key, no export)
57
+ - **Before.** `A=.env` and `A+=.env` are one act to bash, and the program-position reader read only the plain spelling. The append word therefore arrived AS the program — an unrecognised one — so the segment took the verb-blind reading and its VALUE met no judge at all. Measured on the base of this entry, four faces, deny row `**/.env`: `A=.env; cat "$A"` was refused at zero cards with `read_path_denied` on all four, while `A+=.env; cat "$A"` produced ONE approval card naming no deny row on both Bash seats and a YES returned the file's bytes.
58
+ - **After, as one rule.** A leading assignment is the plain spelling, or the append spelling whose **prefix** — the NAME and the OPERATOR — is written bare in the raw token (`isLeadingAssignmentWord`, `src/tools/fs/bash-program-position.ts`). That is the question bash itself asks, before it removes any quoting and about those characters only: quoting any of `NAME+=` makes the whole word a program name on PATH (`'A+=x'`, `"A+=x"`, `A\+=x` — their argument stays judged), while quoting the VALUE does not (`A+=".env"` and `A+='.env'` assign `.env` exactly as `A+=.env` does — verified against the shell). A whole-word bareness test conflates the two and leaves the quoted-value spelling one quote character away from the escape this entry closes; the adversarial round found it and it is pinned as a mutation. The value channel is the same one rule: an assignment word carries TWO independent facts — what the text could not settle, and what path the text DOES name — and the append form has both, so both are handed over (`assignmentWordPayload`). A tilde-bearing or empty value still names nothing. The seat that answers "which program, and where its argv begins" (`argvProgramOf`) no longer strips the leading assignments a second time; the one reader answers.
59
+ - **NARROWING — who is hurt.** A deny row spelled in a bare append word is now REFUSED at zero cards on every face (`deniedBy: "read_boundary"`, `structured.read_path_denied`) where it was an approvable card: `A+=.env; cat "$A"`, `A+=".env"; cat "$A"`, `A+=.env cat x`, `A+=.env && …`, `A+=.env | …`, `B+=.env` behind another assignment, and the same word behind a launcher that owns assignment operands (`env A+=.env dd if=f`). Anyone whose command spells a deny-listed path in an append word loses the card. Compensation: none is owed — the plain spelling has had exactly this verdict since 7.25.0, and the card it replaces was one no approver could read the deny row from.
60
+ - **WIDENING — who is hurt.** The same alignment runs the other way for values that are merely outside the roots: `A+=/etc/passwd <unlisted>` and `A+=~/secret <unlisted>` were a MANDATED card (the verb-blind reading of a word mistaken for a program) and now vouch, because a leading environment word's value is the deny judge's alone and no containment candidate (KL-945a, the standing rule for `A=…`). A deployment that relied on that card as an out-of-root alarm on the append spelling loses it; the deny list is the channel that still fires, on either spelling.
61
+ - **Unchanged, pinned:** a quoted or escaped append word is a program name and its path argument is judged; `env A+=x dd if=<root>/f` reads word for word as before (the launcher owns the word, `dd` is the program); behind a launcher with no assignment grammar (`nice A+=x true`) the word is still the program it execs; `A+="$(cat <deny row>)"` is refused through the nested plan as before.
62
+ - **ONE question for both operators (E5; P2, narrowing; closes KL-945h / the #987 program-position格).** The predicate asks what bash asks — is the matched `NAME=` / `NAME+=` prefix written bare in the raw token — for BOTH operators. Reading the plain spelling on the FOLDED word alone conflated the two the other way: `'A=x' /outside/report.txt` is a command NAMED `A=x` to bash, and the reader took it for an assignment, stepped over the real program and read the file as a program PATH (executed, not disclosed, so no containment question at all). It is that program's operand now, and it is judged — a mandated card with a `readRootCandidate`, where there was none. Who is hurt: a command whose first word is a quoted or escaped assignment shape (`'A=x' file`, `"A+=x" file`) loses its free pass. Compensation: the card carries the way out; the bare spelling is unchanged, and so is a quoted VALUE.
63
+ - **A subscripted name is an assignment too (E6; P2, narrowing; closes KL-993a).** `A[0]=.env` writes one element of `A` and `${A[0]}` reads it back — bash's own reading, verified against the shell — and the grammar knew only unsubscripted names, so the whole word became a program name and its VALUE met no judge. `ASSIGNMENT_PREFIX` reads a subscript carrying no whitespace; the subscript itself is not parsed and the value is judged like any other. A quoted `'A[0]=x'` stays a program name. One element does not settle the whole variable, so the delete walk treats it as it treats an append: verifiable only where the prior content was. Who is hurt: a command spelling a deny-listed path in a subscripted assignment loses its card.
64
+ - **A declaration's operand is read after quote removal (G2).** `export` / `declare` and their family are BUILTINS: the word reaches them with the quotes gone, so `export 'D=/tmp/safe'` assigns what the bare spelling assigns. Requiring the word to be written bare there — the question the LEADING position asks, where the shell itself decides whether a word is an assignment at all — refused to read a word the builtin reads and left the name unregistered, which is a finding on a command that settles its target. The quoted spelling now answers exactly as its bare twin does in both directions: `export "D+=$UNKNOWN"` and `export 'D+=x'` (no prior value) revoke, `D=/tmp/safe; export 'D+=x'` does not.
65
+ - **The declared spelling is the same state transition (E7; P2, narrowing).** `export` / `declare` / `typeset` / `local` / `readonly` carry the same operand, and the second write site of the in-window-assignment rule kept its own copy of the grammar — `NAME=` only, `export` only. So `export D+=$UNKNOWN` left `D` verifiable while the bare `D+=$UNKNOWN` revoked it, and `rm -rf "$D"` behind the declared spelling drew no finding. Both sites read the one grammar now, and both answer "does this settle the whole name?" the same way. Who is hurt: a command that used a declaration head to keep a name verifiable through an append or a subscript now draws the finding the bare spelling always drew.
66
+ - **The activity summary skips every leading assignment (E8; privacy).** `primaryActivityArg` (`src/core/arg-summary.ts`) steps over a command's leading assignments to reach the command NAME, and its own copy of the grammar knew `NAME=` alone — so `PAYLOAD+=<secret> cat f` was read as a command called `PAYLOAD+=<secret>` and the VALUE was written out as the activity's name. That summary is persisted and SSE-forwarded. It reads the one grammar now; a lone assignment and a quoted one still withhold.
67
+ - **Same grammar, second walk (leg B; P2, narrowing).** The unverifiable-recursive-delete walk (`findUnverifiableRecursiveDelete`, `src/core/tool-policy.ts`) skips a statement's leading assignments to reach the command, and recognised them with a private copy of the grammar that knew only the plain operator — so `A+=x rm -rf $UNSET` stopped that walk on its first token and the delete behind it was never examined, while `A=x rm -rf $UNSET` reported its unverifiable target. Both walks now read ONE spelling (`ASSIGNMENT_PREFIX`, exported beside the reader). With it comes the append form's own asymmetry: `NAME+=value` hands the program what NAME already held plus this value, so a readable value settles only the tail — the name is verifiable only where its prior content was (`A+=/tmp/j rm -rf $A` reports; `A=/tmp/j; A+=/k rm -rf $A` does not), and the root mark an append carries is only ever added, never cleared. Who is hurt: a command that used one `+` in front of a delete and drew no finding now draws one — the fail-closed side, and the plain spelling's answer. A quoted prefix (`'A+=x' rm …`, a program name to bash) is unchanged on both spellings. `test/backlog993-tool-policy-leading-assignment.test.ts`.
68
+ - **Which `$` opens a quote form is ONE question (H; regression, fail-open).** The word scan decided it with a one-character peek behind the `$`, and the expansion scan did not look behind at all — so `cat x\\$'…'` (a literal backslash followed by a REAL ANSI-C quote: bash reads `x\\$'y'` as `x\y`) was read as a literal `$` by one and as a quote by the other. The word folded to a path INSIDE the roots while nothing reported an expansion, and the read ran at zero cards on the seat with no shape tier. Both walkers now ask one function, and it counts the backslash RUN — odd is escaped (`\$'y'` is a literal `$`), even is bare — which a peek at one character cannot do.
69
+ - **The same class, third walk (leg C; P2, narrowing).** `$'…'` (ANSI-C quoting) and `$"…"` are QUOTE FORMS, and the lexer folded their `$` into the word — so a deny row written `$'.env'` reached every judge as `$.env`, a spelling no filesystem has, and the read escaped into an approvable card a yes then executed (measured: one card naming no deny row on both Bash seats, sentinel returned, for `cat $'.env'`, `A=$'.env'; cat "$A"` and `A+=$'.env'; cat "$A"`). The forms are read as quotes now: a body with no backslash is its own text exactly as a single-quoted one is, `$"…"` is an ordinary double-quoted string, an unterminated `$'` is a command bash refuses to parse and is unreadable here too, and the quote walks know that `\'` does not close the region. `raw` keeps the written spelling, so every reader that asks "was this written bare" is unaffected. A `$` that OPENS a quote no longer counts as an unquoted expansion either — calling it one made the word unresolvable, which names no path for a judge to meet. **The ESCAPE SEQUENCES are deliberately not folded**: a body carrying any backslash is unreadable (the card this release started from, fail-closed). Three adversarial rounds each found a different transcription error in a decoder for them — a code point read where bash builds a byte, a UTF-16 unit encoded where bash writes a character, a control-letter formula off by one table — and each named a DIFFERENT FILE than the shell would open, so the table belongs with the single-lexer work (#994) where a differential test against `/bin/bash printf` bytes can carry it. `test/backlog993-ansi-c-quoting.test.ts`; KL-993g records the limit, KL-993d the read-only shell's own `$` grammar.
70
+ - **The tilde question is asked of the PREFIX, not the word (E3).** A slot expands only where its tilde-PREFIX — up to the terminating `/` or `:` — carries no quoting, which is bash's own rule. Asking it of the whole word answered it for `A=~/'.env'` and `A+=x:~/'.env'`, whose prefixes are bare and whose values are not: bash expands those (`A=~/".env"` gives `<home>/.env`, verified against the shell) and calling the value literal named a file no shell would open. Both now read unresolved and ask on both faces; `A='~'/x` — a QUOTED prefix — is the ordinary relative path it is.
71
+ - **The same class, fourth walk (leg D; P2, narrowing).** A tilde in an assignment value is read by POSITION, as bash reads it: expanded at the start of the value and right after each `:`, LITERAL everywhere else, and nowhere at all where the word is not written bare. Reading "contains a tilde" as "cannot be settled" threw away every value whose tilde was literal, and a value nobody names is a value no judge meets — so `A=.env~` / `A+=.env~` (the editor-backup spelling) reached a deny row spelled `**/.env*` as nothing at all, on both spellings, and asked where `cat .env~` refused. The value is now named (`.env~`), the row bites, and a quoted leading tilde is an ordinary path component spelled `./~…`. A tilde WHERE BASH EXPANDS ONE is still reported unresolved and still asks on both faces, unchanged. Who is hurt: these values lose their card where they name a deny row. KL-993b now records only what is left — such a tilde is not placed against the declared home.
72
+ - **Three corners of the same three legs (F1–F3).** (F1, regression) An ANSI-C body's LITERAL run was encoded by UTF-16 unit, so a character outside the basic plane became two replacement characters — bytes that decode cleanly and name a different file: `cat $'🔒/credentials.txt'` under a deny row for that directory read it at zero cards on the seat with no shape tier. Literal runs are encoded by code point now, and the decoder keeps a BOM instead of stripping bytes bash keeps. (F2) The bareness question covers the NAME and the OPERATOR and nothing else: a quoted SUBSCRIPT still names the element (`A['0']=v` assigns it), and comparing the whole prefix took that for a program name — a card where a refusal was owed. (F3) A declaration carries its own OPTIONS and as many operands as it likes; reading one word after the head word left every later name unregistered and every option-carrying declaration unread, so `export -n D=/tmp/safe; rm -rf "$D"` drew a finding on a command that settles its target. Every operand takes the same state transition now, an unreadable one revokes rather than vouches, and the transition is written once for both sites.
73
+ - **KNOWN-LIMITS:** KL-945h and KL-993a are CLOSED by E5 / E6 above (both operators and the subscripted name now take the one bare-prefix question). Left open: KL-993b (a tilde in an expansion slot reports "unresolved" rather than resolving against the declared home), KL-993c (a value assembled across two appends — `A+=.en; A+=v` — is not one text, so the read it feeds is the ordinary unresolvable-expansion card), KL-993d (the read-only shell's own grammar still refuses every `$` spelling, `$'…'` included), KL-993e (a value equal to the substitution placeholder text is dropped as a substitution), KL-993f (a subscript carrying whitespace is not read as one word).
74
+ - Reach: @server / @cli / @client-core — zero: no wire key, no notice code, no export added or removed, no card SHAPE changed. Headless: a deny row in an append word refuses (`read_boundary`) where it auto-denied an unanswerable card; an out-of-root value in one vouches where it refused. `test/backlog993-append-assignment-word.test.ts` pins P1–P5 with a positive control; `backlog860` G2 and `backlog862b` ③/⑧ carry the retired dialect's tripwire.
75
+
3
76
  ## 7.25.0 — 2026-09-20
4
77
 
5
78
  ### One station judges every shell read, before approval (#945 S1; BREAKING ×3; one narrowing, three widenings; #976 closed structurally)
@@ -129,6 +202,14 @@
129
202
  - **Who is hurt.** `BackgroundAgentRecord.teamName?` and `RegisterBackgroundAgentInput.teamName?` are removed — a dead column: two copy sites forwarded it and no site in `src/` ever minted a value. Readers of the core row: server 0 (`record_json` passthrough), cli 0 (its own `teamName` is its CC fork's, not this row's), client-core 0, sdk 0.
130
203
  - **Compensation.** None needed — the column never held a value; a hand-written row holding the key still reads back unchanged (`backgroundAgentStoreContract` gains the case). The team key is `rootSessionId`, already on every roster row, registry row, fleet spawn frame and durable record: one implicit team per session (CC 2.1.259 marks `team_name` deprecated / ignored).
131
204
  - **Contract sentence.** `task_list.partition_is_root_session`: a deployment that mounts a `TaskListStore` per session keys it by the delegation tree's ROOT session — every child of the tree (spawned live, or revived by a host verb) reads and writes the same list. In-engine children inherit the leader's tool objects and share by construction; a host's revive leg that resolves a store per call is the divergence this sentence names.
205
+ ### The session's write home wins by name, and a deployment can state its project identity (#863 片2; L3; NARROWING of the default memory read face + 1 new export + 1 new notice code; @server @cli @client-core)
206
+ - **NARROWING.** A read-only memory layer's entry whose NAME the session's write home already carries leaves the DEFAULT read face: no mechanical row in the injected index (a model-worded link row pointing at it drops with a warning; linkless prose stays), no `memory_search` hit, no `memory_index` row, and a bare slug whose candidates include the write home resolves to the write home instead of refusing `ambiguous_slug`. **Nothing is written, moved or deleted**: `memory_get id=…` delivers the entry with a head note naming the shadowing one, `includeShadowed` (new, on `memory_search` and `memory_index`) retrieves it tagged `[shadowed]`, the hidden count is always disclosed, and the file's bytes are untouched.
207
+ - **Who is affected.** Any deployment mounting more than one layer with a write home among them — including an **org** layer, which is deliberately NOT exempt: a tenant that writes its own `deploy-steps` retires the org layer's `deploy-steps` from that tenant's default face. The disclosure is `TaskResult.effectiveMemoryScopes.scopes[i].shadowed` (new, present-iff non-zero, never on the write home's own row) and `memory_index`'s count sentence. A single-layer session, and any session whose tool faces are given no write home, are byte-identical.
208
+ - **Two laws authorize a shadow.** The NAME SOURCE is a committed header face — an in-session `name:` edit retires nothing before the harvest boundary admits it. The CARRIER must itself stand on the seat's own default face: withheld, superseded and undeliverable are not three conditions but three ways of not standing there. The second law is what keeps the pair honest — a write-home entry replaced by a consolidation product is committed, unchallenged and on disk, and letting it shadow took BOTH same-named answers off every enumeration at once (pinned as a three-entry world).
209
+ - **New export (+1):** `deriveShadowedSet` — the one predicate behind all four faces, beside `deriveSupersededSet`.
210
+ - **New declaration.** `TaskSpec.memory.projectKey: { scope } | { absent: "not_git" | "bare" | "unreadable" }` is what an assembly point STATES about this task's project identity. It configures nothing — the layering is still `scopes` + `writeScope`. It closes the one silent direction those cannot express: an omitted `writeScope` defaults to the LAST scope, so an assembler that derived a project key and forgot to pin the write face writes every project's entries into the shared layer, silently. A `{ scope }` arm disagreeing with `writeScope` now refuses the task (`config.memory_write_scope_mismatch`; a read-only layering is exempt); an unreadable spelling refuses (`config.memory_project_key_spelling`) rather than folding to "no key stated"; an `{ absent }` arm mints one operator notice **`memory.project_scope_absent`** (new `ENGINE_NOTICE_CODES` member, audience `operator`) at the DECLARATION rather than at the mount. Absence of the key is byte-identical.
211
+ - `TaskResult.effectiveMemoryScopes` (mounted) gains `projectKey?` echoed verbatim — absence means the deployment said nothing, which is not `{ absent }`, which means it looked and found none.
212
+ - Tool contract revisions: `memory_search` 6 → 7, `memory_get` 6 → 7, `memory_index` 1 → 2. Wire table: `docs/wire-consumption/unreleased-863p2.md`.
132
213
 
133
214
  ## 7.24.2 — 2026-09-20
134
215
 
@@ -1,10 +1,3 @@
1
- /**
2
- * design/99 — a SHORT, SECRET-SCRUBBED one-line summary of a tool call's arguments, for an OPERATOR-FACING
3
- * display (the MF-W workflow monitor's `Read(path)` / `Bash(grep …)` activity line; the MF-24 permission-denial
4
- * ledger's denied-tool input). Shared so both the activity sink and the denial record scrub identically — a
5
- * single audited boundary. NOT `redactHostLeaks` (that is for untrusted-LLM egress and over-redacts the very
6
- * path the trusted owner-scoped monitor wants to show). Reviewed: 3 blocker + 5 major findings closed.
7
- */
8
1
  /** Known credential SHAPES + `secretKey=value` — redacted from the FINAL abbreviated string, the single scrub
9
2
  * point before any store/SSE fan-out. Conservative (real credential formats). */
10
3
  /** every prefix is LEFT-ANCHORED with \`(?<![A-Za-z0-9])\` — without it, ordinary words whose
@@ -1,3 +1,4 @@
1
+ import { writtenAssignmentPrefix } from "../tools/fs/bash-program-position.js";
1
2
  const ACTIVITY_ARG_MAX = 80;
2
3
  const SENSITIVE_KEY = /token|secret|key|password|passwd|credential|auth/i;
3
4
  export function summarizeRedactions(findings) {
@@ -234,7 +235,7 @@ export function primaryActivityArg(args) {
234
235
  if (command !== undefined) {
235
236
  const tokens = command.trim().split(/\s+/);
236
237
  let i = 0;
237
- while (i < tokens.length && /^[A-Za-z_][A-Za-z0-9_]*=/.test(tokens[i])) {
238
+ while (i < tokens.length && writtenAssignmentPrefix(tokens[i]) !== undefined) {
238
239
  if (/["'\\]/.test(tokens[i]))
239
240
  return undefined;
240
241
  i++;
@@ -121,6 +121,13 @@ export interface AskOriginFacts {
121
121
  * record of its own layer (a tighten replaces an `allow` wholesale, so at most one fires per pass),
122
122
  * never a read-back of `decisionReason: "safety"` off a decision a policy could have composed. */
123
123
  readonly tightened: "shell_gate" | "safety" | undefined;
124
+ /** The MANDATE mark inputs of this call — the tool SEAT's marks plus the gate's own per-call records
125
+ * ({@link MandateMarks}), read from the same two places the lane's own derivation reads them. The
126
+ * per-call PROBE member is deliberately not here: it is engine-stamped on the DECISION, and
127
+ * {@link ruleClearanceOf} reads it there, so one fact cannot arrive twice with two values. A station
128
+ * holding no tool seat (the inherited lane) passes `{}` — an honest empty, since the decision's own
129
+ * members still answer. */
130
+ readonly mandateMarks: Omit<MandateMarks, "probeMandated">;
124
131
  }
125
132
  /**
126
133
  * The ONE derivation of an ask's origin, from the surviving decision plus the gate's own facts. The
@@ -149,6 +156,83 @@ export declare function stampAskOrigin<T extends Extract<PermissionResult, {
149
156
  }>>(decision: T, facts: AskOriginFacts): T & {
150
157
  origin: AskOrigin;
151
158
  };
159
+ /**
160
+ * CAN a persisted allow rule clear this ask — and, if not, which door closed? ONE derivation with TWO
161
+ * readers, which is the whole point of the shape: the approval card's `ruleOffersAbsence` word and the
162
+ * ask's own {@link import("./tool-policy.js").AskRequest.mandated} bit. They used to be two
163
+ * enumerations of one boundary, and the drift was live — the card's lane speaks for one tool, only
164
+ * where a rule store is wired and only on a task that can hold a rule, so the same mandated read asked
165
+ * as a mandated question on the shell tool and as an apparently ordinary one on the monitor tool.
166
+ *
167
+ * "Allow rules silence the classifier's questions, never a mandated one." The doors, as a SET:
168
+ * · the MARKS family — {@link persistedRuleMandateOf} over the tool seat's marks and the gate's own
169
+ * per-call records (the read station's structural demotion, an unanswered probe, an operator's
170
+ * per-call shell doctrine, the tool's own egress / irreversibility marks, a write the protection
171
+ * judge matched or could not resolve);
172
+ * · GOVERNANCE (`requiresRealApproval`) — an org verdict, a safety policy's own demand, the
173
+ * classifier's denial-limit fallback: only judgment clears it;
174
+ * · a HOOK-raised ask — the deployment's own per-call screening authority, which the lane's clearing
175
+ * conjuncts refuse;
176
+ * · ANCESTOR authority (`inheritedUnresolved` / `ancestorResolved`) — the ask belongs to an ancestor's
177
+ * approver and this task's rule lane never adjudicates it;
178
+ * · the bit already stamped on the ask, which no layer below the mint may clear (tightening-only).
179
+ *
180
+ * And the SHADOW doors, which are not mandates: a matching rule the person already wrote is speaking
181
+ * and is outranked (`persistedRuleShadowed`), or their explicit "ask me each time" rule matched
182
+ * (`matchedAskRule`). A rule minted from such a card silences nothing, so the lane offers none — but
183
+ * the reason is that a RULE is already speaking, not that the question is beyond rules, and a surface
184
+ * that auto-answers by posture is not crossing a mandate when it answers one.
185
+ *
186
+ * THREE named readings of the one pass, because three callers need three different things and taking
187
+ * the wrong one is how this boundary drifts:
188
+ * · `word` — the DISCLOSED reason, first door wins, in the display order the card has always used
189
+ * (governance, the shadow, the hook, the ask rule, the two ancestor doors, then the marks). Display
190
+ * only. A structural door the offer factory owns (no lane wired, a tool it does not speak for, a
191
+ * task that cannot hold a rule) is decided BEFORE this and is deliberately silent;
192
+ * · `mandated` — the FACT the ask carries as its own member. Read off the whole mandate set, so which
193
+ * door is DISPLAYED cannot change it: an ask that is both shadowed and mandated is mandated;
194
+ * · `ruleMayClear` — the ENFORCEMENT reading the gate's clearing arms take. It is `mandated` plus the
195
+ * person's explicit `ask` rule, and deliberately NOT `word !== undefined`: `persistedRuleShadowed`
196
+ * is an ACCOUNT of a refusal that already happened, display-only by contract, and reading it as a
197
+ * blocker would let a policy forwarding that metadata refuse a call nothing is blocking. A
198
+ * display word is never an authority.
199
+ */
200
+ export declare function ruleClearanceOf(ask: RuleClearanceFacts | undefined, marks: Omit<MandateMarks, "probeMandated">): {
201
+ word?: "mandated" | "shadowed";
202
+ mandated: boolean;
203
+ ruleMayClear: boolean;
204
+ };
205
+ /** The facts one ask carries about whether a rule could clear it — the members {@link ruleClearanceOf}
206
+ * reads beside the tool seat's marks. Every one is either engine-stamped or, where a caller can
207
+ * self-declare it, tightening-only: a claim here can opt an ask OUT of automatic answering and never
208
+ * into it. */
209
+ export interface RuleClearanceFacts {
210
+ /** The bit a gate already stamped (see {@link import("./tool-policy.js").AskRequest.mandated}). */
211
+ mandated?: true;
212
+ requiresRealApproval?: boolean;
213
+ /** A matching persisted allow rule was found and did not clear the ask — carries its text. */
214
+ persistedRuleShadowed?: string;
215
+ decisionReason?: import("./tool-policy.js").DecisionReason;
216
+ /** An EXPLICIT `ask` permission rule matched this call (DSL, stamped at the rule policy's three
217
+ * matched-ask exits — bare, covering and param). */
218
+ matchedAskRule?: string;
219
+ /** The engine-stamped probe mandate (the reversibility probe declared this call's demotion
220
+ * structural — for the built-in shell probe, a read outside the session's allowed directories). */
221
+ probeMandated?: boolean;
222
+ /** An ancestor's approver answered "unavailable" for this call, or a durable mandate was floated here. */
223
+ inheritedUnresolved?: boolean;
224
+ /** The ask is being resolved at an ANCESTOR's frozen approver (the three inherited-lane mint sites
225
+ * pass it literally) — this task's rule lane never adjudicates it. */
226
+ ancestorResolved?: boolean;
227
+ }
228
+ /** The ask FACE's mandate member — what an approval request carries, spread at the four mint stations.
229
+ * The stations hand it the SAME facts object they hand the rule-offer factory, so the card's word and
230
+ * this bit are two readings of one derivation and cannot drift. The tool SEAT is not re-judged here:
231
+ * its marks were folded into the stamped bit at the gate, and a station minting a request from an
232
+ * ANCESTOR's decision holds no seat of this task's to judge. */
233
+ export declare function askMandateMember(ask: RuleClearanceFacts | undefined): {
234
+ mandated?: true;
235
+ };
152
236
  /**
153
237
  * Which origins IMPLY the `requiresRealApproval` bit, and WHERE that implication is verified — the
154
238
  * consistency table {@link askOriginOf} reads. An ask of a must-carry origin exists so that no automatic
@@ -18,11 +18,39 @@ export function askOriginOf(decision, facts) {
18
18
  const TIGHTEN_ONLY_ORIGINS = new Set(["shell_gate_tighten", "safety_tighten"]);
19
19
  export function stampAskOrigin(decision, facts) {
20
20
  const origin = askOriginOf(decision, facts);
21
- if (decision.probeReadRootCandidate === undefined || TIGHTEN_ONLY_ORIGINS.has(origin))
22
- return { ...decision, origin };
23
- const { probeReadRootCandidate: _withdrawn, ...kept } = decision;
21
+ const mandated = ruleClearanceOf(decision, facts.mandateMarks).mandated && decision.mandated !== true ? { ...decision, mandated: true } : decision;
22
+ if (mandated.probeReadRootCandidate === undefined || TIGHTEN_ONLY_ORIGINS.has(origin))
23
+ return { ...mandated, origin };
24
+ const { probeReadRootCandidate: _withdrawn, ...kept } = mandated;
24
25
  return { ...kept, origin };
25
26
  }
27
+ export function ruleClearanceOf(ask, marks) {
28
+ const mandated = ask?.mandated === true ||
29
+ ask?.requiresRealApproval === true ||
30
+ ask?.decisionReason === "hook" ||
31
+ ask?.inheritedUnresolved === true ||
32
+ ask?.ancestorResolved === true ||
33
+ persistedRuleMandateOf({ ...marks, probeMandated: ask?.probeMandated === true }) !== undefined;
34
+ const word = (() => {
35
+ if (ask?.requiresRealApproval === true)
36
+ return "mandated";
37
+ if (ask?.persistedRuleShadowed !== undefined)
38
+ return "shadowed";
39
+ if (ask?.decisionReason === "hook")
40
+ return "mandated";
41
+ if (ask?.matchedAskRule !== undefined)
42
+ return "shadowed";
43
+ if (ask?.inheritedUnresolved === true)
44
+ return "mandated";
45
+ if (ask?.ancestorResolved === true)
46
+ return "mandated";
47
+ return mandated ? "mandated" : undefined;
48
+ })();
49
+ return { ...(word !== undefined ? { word } : {}), mandated, ruleMayClear: !mandated && ask?.matchedAskRule === undefined };
50
+ }
51
+ export function askMandateMember(ask) {
52
+ return ruleClearanceOf(ask, {}).mandated ? { mandated: true } : {};
53
+ }
26
54
  function originLadder(decision, facts) {
27
55
  if (facts.contentQuestion)
28
56
  return "content_question";
@@ -375,6 +375,11 @@ export interface MaybeCompactOptions {
375
375
  };
376
376
  land: () => Promise<void>;
377
377
  };
378
+ /** The approval-posture directive's post-compaction restatement (the runner's `compactionRestateOptions`):
379
+ * runs right after `gitRestate.land`, under the same belt and for the same reason — the compacted
380
+ * context may have lost an engine frame the run still needs, and every reader of the new baseline
381
+ * (the next model request first) must see it restated. */
382
+ postureRestate?: () => Promise<void>;
378
383
  /**
379
384
  * Fixed per-request prompt overhead (system prompt + tool schemas, in tokens), applied ONLY in the
380
385
  * anchor-less regime (design/64 §26.7, search [91] fourth candidate): when no assistant in the
@@ -466,6 +466,13 @@ export async function maybeCompact(opts) {
466
466
  catch {
467
467
  }
468
468
  }
469
+ if (opts.postureRestate !== undefined) {
470
+ try {
471
+ await opts.postureRestate();
472
+ }
473
+ catch {
474
+ }
475
+ }
469
476
  try {
470
477
  opts.onApplied?.(attachedComplete, excludedReadStatePreserveKeys);
471
478
  }
@@ -0,0 +1,45 @@
1
+ import type { ApproverPosture } from "./task-spec.js";
2
+ /** The auto-mode directive body (title + the two paragraphs). Rendered when the run's auto-mode
3
+ * INTENT is resolved true — the seat, the chain it inherited, or the bit a suspend recorded. */
4
+ export declare const AUTO_MODE_DIRECTIVE: string;
5
+ /** The shell-first paragraph. The reference implementation serves it under three different headings;
6
+ * the one heading this engine has a fact for is the bypass posture below. */
7
+ export declare const BASH_FIRST_DIRECTIVE: string;
8
+ /** The bypass-posture directive body: the reference implementation's heading over the shell-first
9
+ * paragraph, the exact pair its bypass arm renders. */
10
+ export declare const BYPASS_POSTURE_DIRECTIVE: string;
11
+ /** The posture words this engine accepts, as literals (the door refuses anything else loudly). The
12
+ * TYPE is declared beside the seat it belongs to ({@link ApproverPosture}, task-spec.ts); this array
13
+ * is its runtime twin, and the assignment below is the compile-time tie between them. */
14
+ export declare const APPROVER_POSTURES: readonly ApproverPosture[];
15
+ /** Value-shape check for the word — used by the spec door below and by the chain/checkpoint readers. */
16
+ export declare function isApproverPosture(value: unknown): value is ApproverPosture;
17
+ /** The SEAT's door screen, declared beside the vocabulary it screens against. The renderer keys on the
18
+ * exact word, so any other present value would render zero bytes and be indistinguishable from "no
19
+ * posture declared"; it is refused instead. The value that passed is the value RETURNED — the render
20
+ * reads that snapshot, never the caller's spec object again. */
21
+ export declare function refuseInvalidApproverPosture(spec: {
22
+ approverPosture?: ApproverPosture;
23
+ }): ApproverPosture | undefined;
24
+ /** The resolved posture facts of one leg: both already folded across seat, chain and checkpoint. */
25
+ export interface ApprovalPostureFacts {
26
+ /** The run's resolved auto-mode intent (seat ∨ inherited chain ∨ checkpoint seed). */
27
+ autoMode: boolean;
28
+ /** The declared approver posture, absent when the host declared none. */
29
+ approverPosture?: ApproverPosture;
30
+ }
31
+ /**
32
+ * The ONE composition, and the ONE invariant: **each segment the run's posture calls for is stated
33
+ * exactly once in the active model context**. `alreadyStated` is the transcript's ENGINE REGIONS (read
34
+ * by metadata — a caller's own bytes are never in one, so a spliced copy cannot suppress the engine's);
35
+ * a segment already standing there is not restated, and a segment that has left the context — compacted
36
+ * away, or never delivered — is. Returns `undefined` when there is nothing left to say, which is both
37
+ * "no posture declared" (zero bytes, byte-identical to the pre-seat shape) and "already said".
38
+ *
39
+ * Segment-wise rather than body-wise on purpose: a leg that gains the second fact must state the second
40
+ * paragraph without restating the first. The caller wraps the result in the engine's marked reminder
41
+ * shell; this module never wraps, so the mark stays owned by its own mint home.
42
+ */
43
+ export declare function composeApprovalPostureDirective(facts: ApprovalPostureFacts, statedBodies?: readonly string[]): string | undefined;
44
+ /** SHA-256 of a directive body — the pins' lock port, so the lock is computed the one way. */
45
+ export declare function directiveDigest(body: string): string;
@@ -0,0 +1,47 @@
1
+ import { createHash } from "node:crypto";
2
+ export const AUTO_MODE_DIRECTIVE = "## Auto Mode Active\n\nBias toward working without stopping for clarifying questions — when you'd normally pause to check, make the reasonable call and keep going; they'll redirect you if needed. If the user, a skill, or the shape of the task suggests they want you to ask (with AskUserQuestion or otherwise), do so. And even absent that signal, it's still fine to stop when you're genuinely blocked — unclear direction, missing input, a decision only they can make.\n\nBefore any command that could discard uncommitted work — `git checkout`/`restore`/`reset`/`clean`, `rm -rf` in the repo, restoring from a snapshot — run `git status` first and stash (with `-u` for untracked) or commit anything that's there. When staging or committing, review what's included (`git status` after a broad `git add`), and if you see anything suspicious that might reveal secrets — even if the filename looks innocuous — double-check the file's contents before pushing.";
3
+ export const BASH_FIRST_DIRECTIVE = "Do your work through the Bash tool wherever it can accomplish the job: read files with cat, head, or sed -n, search with grep and find, and make file changes with sed, heredocs, or short scripts, rather than using the dedicated Read, Edit, or Write tools. Fall back to a dedicated tool only when Bash genuinely cannot do the job.";
4
+ export const BYPASS_POSTURE_DIRECTIVE = `While bypass permissions mode is active:\n\n${BASH_FIRST_DIRECTIVE}`;
5
+ export const APPROVER_POSTURES = ["bypass"];
6
+ export function isApproverPosture(value) {
7
+ return typeof value === "string" && APPROVER_POSTURES.includes(value);
8
+ }
9
+ export function refuseInvalidApproverPosture(spec) {
10
+ if (spec.approverPosture !== undefined && !isApproverPosture(spec.approverPosture)) {
11
+ const e = new Error(`approverPosture must be one of ${APPROVER_POSTURES.map((w) => JSON.stringify(w)).join(", ")} when present (got ${JSON.stringify(spec.approverPosture)}) — omit it to declare no posture; an unrecognized value is refused rather than read as absence.`);
12
+ e.code = "config.approver_posture_invalid";
13
+ throw e;
14
+ }
15
+ return spec.approverPosture;
16
+ }
17
+ export function composeApprovalPostureDirective(facts, statedBodies = []) {
18
+ const parts = [];
19
+ if (facts.autoMode)
20
+ parts.push(AUTO_MODE_DIRECTIVE);
21
+ if (facts.approverPosture === "bypass")
22
+ parts.push(BYPASS_POSTURE_DIRECTIVE);
23
+ if (parts.length === 0)
24
+ return undefined;
25
+ const stated = new Set();
26
+ for (const body of statedBodies)
27
+ for (const seg of postureSegmentsOf(body) ?? [])
28
+ stated.add(seg);
29
+ const owed = parts.filter((seg) => !stated.has(seg));
30
+ return owed.length === 0 ? undefined : owed.join("\n\n");
31
+ }
32
+ const POSTURE_SEGMENTS = [AUTO_MODE_DIRECTIVE, BYPASS_POSTURE_DIRECTIVE];
33
+ function postureSegmentsOf(body) {
34
+ const out = [];
35
+ let rest = body;
36
+ while (rest.length > 0) {
37
+ const seg = POSTURE_SEGMENTS.find((s) => rest === s || rest.startsWith(`${s}\n\n`));
38
+ if (seg === undefined)
39
+ return undefined;
40
+ out.push(seg);
41
+ rest = rest === seg ? "" : rest.slice(seg.length + 2);
42
+ }
43
+ return out.length > 0 ? out : undefined;
44
+ }
45
+ export function directiveDigest(body) {
46
+ return createHash("sha256").update(body, "utf8").digest("hex");
47
+ }
@@ -7,7 +7,7 @@ import type { ReadEntry } from "../tools/fs/safety.js";
7
7
  import type { RepairBundle } from "../agents/repair-loop.js";
8
8
  import type { WorkspaceHandle } from "./remote-env.js";
9
9
  import type { SessionPermissionRules } from "./session-policy-store.js";
10
- import type { NestedUsageAccum, ShellGateDoctrine, TaskResult } from "./types.js";
10
+ import type { ApproverPosture, NestedUsageAccum, ShellGateDoctrine, TaskResult } from "./types.js";
11
11
  import type { ActiveWorktreeSession } from "../internal/harness-types.js";
12
12
  /**
13
13
  * design/45 — the **durable-checkpoint** primitive: cross-process / resumable `suspend`/`resume`.
@@ -1111,6 +1111,15 @@ export interface CheckpointState {
1111
1111
  * (byte-identical to the pre-bit row); an older worker that ignores it resumes un-armed, the
1112
1112
  * narrower direction. */
1113
1113
  autoModeRequested?: true;
1114
+ /** The chain's declared APPROVER POSTURE at suspend (data half, same law as `shellGate`): the word
1115
+ * the suspended leg was told to render — its own seat, the one its live chain carried, or the one an
1116
+ * earlier suspend of the same chain recorded. A resume leg reads it beside the re-supplied seat and
1117
+ * the waking chain, so a redemption in another process states the same posture the suspend leg did.
1118
+ * It authorizes nothing (the gate never reads it) and it renders nothing on a resume leg either —
1119
+ * the first-turn frame that carries it is already in the restored transcript; what it feeds is the
1120
+ * chain the resumed leg hands its OWN children. Absent on older checkpoints and on tasks that
1121
+ * declared no posture (byte-identical row). */
1122
+ approverPosture?: ApproverPosture;
1114
1123
  /** Org-memory admission freeze (ruled 2026-08-05): the chain's admitted org-scope set at
1115
1124
  * suspend (data half, plain strings). The resume leg folds it seed ∩ live (tighten-only) and
1116
1125
  * re-runs admission under it — a resume must never widen the delegation freeze. Absent on
@@ -112,6 +112,16 @@ export interface EngineNotice {
112
112
  * Containment is unaffected and the committed copy is still in the store, so reconciling the two is
113
113
  * the host's act (audience `operator`). `detail: { paths: [{ path, entryId?, rev? }], sessionId? }`,
114
114
  * `rev` = the declined generation. Dedup unit: once per containment PASS, never per path.
115
+ * - `"memory.project_scope_absent"` — the assembly point DECLARED that it could derive no project
116
+ * identity for this session's working root (`TaskSpec.memory.projectKey: { absent }`), so the
117
+ * layering it sent is used exactly as written: entries this session writes go to whatever the
118
+ * spec named, typically a layer shared with every other project. Audience `operator` — the
119
+ * reading is about how the deployment resolves working roots, and only an operator can change
120
+ * it; nothing retries and nothing is refused. `detail: { reason, taskRoot, sessionId,
121
+ * writeScope? }`, `reason` the deployment's own word (not_git | bare | unreadable),
122
+ * `writeScope` absent ⇔ a read-only layering. Minted at the DECLARATION rather than at the
123
+ * mount (the declaration is already the fact, and a mount that later fails must not swallow
124
+ * it). Dedup unit: once per prepared leg.
115
125
  * - `"mcp.revocation_probe_failed"` (design/338) — the deployment's `mcpRevocations.isRevoked`
116
126
  * probe threw; MCP dispatch FAILS OPEN (revocation is a tightening face) and this announces
117
127
  * once per materialization (a resume re-materializes and may announce again).
@@ -1,5 +1,5 @@
1
1
  import { attestClassifierDenyCause, classifierDenyCauseOf, decisionText, describeThrown } from "./tool-policy.js";
2
- import { classifierMayAnswer, classifierMayAnswerMandate, mandateDisclosureOf, persistedRuleMandateOf, safetyAxisOf, sandboxMayAdmitMandate, stampAskOrigin } from "./ask-origin.js";
2
+ import { classifierMayAnswer, classifierMayAnswerMandate, mandateDisclosureOf, persistedRuleMandateOf, ruleClearanceOf, safetyAxisOf, sandboxMayAdmitMandate, stampAskOrigin } from "./ask-origin.js";
3
3
  import { classifierParseFailureDenyMessage, classifierUnavailableDenyMessage, denialLimitFallbackMessage, unarmedWindow } from "./auto-mode.js";
4
4
  import { inlineUntrusted } from "./untrusted-text.js";
5
5
  import { isRuleBehavior } from "./permission-rule-model.js";
@@ -156,12 +156,21 @@ export async function runGateLanes(pass) {
156
156
  let orgRealApprovalRequired = false;
157
157
  let markedThisRound;
158
158
  const markedUnresolvable = () => (markedThisRound ??= input.isMarkedUnresolvable?.(input.event.toolCallId) === true);
159
+ const mandateMarksNow = () => ({
160
+ egress: input.egress,
161
+ shellGated: input.shellGated,
162
+ irreversibility: input.irreversibility,
163
+ probeUnanswered: pass.probeUnanswered === true,
164
+ writeProtected: pass.writeProtected === true,
165
+ writeProtectionUnresolved: pass.writeProtectionUnresolved === true,
166
+ });
159
167
  const askOriginFacts = (org, ruleStore) => ({
160
168
  contentQuestion: toolName === ASK_USER_QUESTION_TOOL_NAME,
161
169
  markedUnresolvable: markedUnresolvable(),
162
170
  org,
163
171
  ...(ruleStore !== undefined ? { ruleStore } : {}),
164
172
  tightened: pass.tightenedBy,
173
+ mandateMarks: mandateMarksNow(),
165
174
  });
166
175
  let orgTightenCount = 0;
167
176
  let orgEvidence = (() => {
@@ -275,14 +284,10 @@ export async function runGateLanes(pass) {
275
284
  pass.req.args = pass.policyRewrite;
276
285
  }
277
286
  const persistedRuleMandate = persistedRuleMandateOf({
278
- egress: input.egress,
279
- shellGated: input.shellGated,
280
- irreversibility: input.irreversibility,
287
+ ...mandateMarksNow(),
281
288
  probeMandated: pass.decision.action === "ask" && pass.decision.probeMandated === true,
282
- probeUnanswered: pass.probeUnanswered === true,
283
- writeProtected: pass.writeProtected === true,
284
- writeProtectionUnresolved: pass.writeProtectionUnresolved === true,
285
289
  });
290
+ const ruleMayClear = () => ruleClearanceOf(pass.decision.action === "ask" ? pass.decision : undefined, mandateMarksNow()).ruleMayClear;
286
291
  let personalEvidence = { dotsAbsent: input.persistedRules === undefined ? "not_wired" : "not_adjudicated" };
287
292
  if (laneAnswer?.unreadable === true)
288
293
  personalEvidence = { dotsAbsent: "unavailable" };
@@ -316,7 +321,7 @@ export async function runGateLanes(pass) {
316
321
  };
317
322
  if (answer.hit?.behavior === "ask")
318
323
  personalEvidence = unionDotsOf(answer.hit.rules);
319
- if (hitRules !== undefined && persistedRuleMandate === undefined && pass.decision.matchedAskRule === undefined) {
324
+ if (hitRules !== undefined && ruleMayClear()) {
320
325
  pass.decision = {
321
326
  action: "allow",
322
327
  message: hitRules.length === 1
@@ -330,7 +335,9 @@ export async function runGateLanes(pass) {
330
335
  else if (hitRules !== undefined) {
331
336
  personalEvidence = unionDotsOf(hitRules);
332
337
  const shownRule = disclosedRuleSet(hitRules);
333
- const mandateNoun = mandateDisclosureOf(persistedRuleMandate);
338
+ const mandateNoun = persistedRuleMandate === undefined && pass.decision.matchedAskRule === undefined
339
+ ? "this call arrived already marked as requiring confirmation (an earlier layer of this run, or the task that delegated it, judged it so) — it is cleared by confirming this call, never by a standing allow rule"
340
+ : mandateDisclosureOf(persistedRuleMandate);
334
341
  pass.decision = {
335
342
  ...pass.decision,
336
343
  persistedRuleShadowed: shownRule,
@@ -346,7 +353,7 @@ export async function runGateLanes(pass) {
346
353
  const stamped = { ...unstamped, ruleEvidence: mintRuleEvidence(personalEvidence), ...(laneCoverage !== undefined ? { segmentCoverage: laneCoverage } : {}), ...(originFacts.ruleStore !== undefined ? { ruleStoreUnreadable: originFacts.ruleStore } : {}) };
347
354
  pass.decision = stampAskOrigin(stamped, originFacts);
348
355
  }
349
- if (allowLayerMayClear() && pass.decision.action === "ask" && persistedRuleMandate === undefined && pass.probeBoundaryDeclined !== true && pass.decision.matchedAskRule === undefined && pass.req.toolName === COMMAND_RULE_TOOL) {
356
+ if (allowLayerMayClear() && pass.decision.action === "ask" && ruleMayClear() && pass.probeBoundaryDeclined !== true && pass.req.toolName === COMMAND_RULE_TOOL) {
350
357
  const commandParam = catalogRuleFaceOf(COMMAND_RULE_TOOL)?.primaryParams[0] ?? "command";
351
358
  const args = pass.req.args;
352
359
  const command = typeof args === "object" && args !== null && !Array.isArray(args) ? args[commandParam] : undefined;
@@ -459,6 +466,7 @@ export async function runGateLanes(pass) {
459
466
  ...(d.action === "ask" && d.denialLimitFallback !== undefined ? { denialLimitFallback: unarmedWindow(d.denialLimitFallback) } : {}),
460
467
  ...(d.action === "ask" && d.origin !== undefined ? { origin: d.origin } : {}),
461
468
  ...(d.action === "ask" && d.ruleStoreUnreadable !== undefined ? { ruleStoreUnreadable: d.ruleStoreUnreadable } : {}),
469
+ ...(d.action === "ask" && d.mandated === true ? { mandated: true } : {}),
462
470
  });
463
471
  if (suspendAsk && pass.decision.action === "ask") {
464
472
  const parkArgs = [pass.req, pass.currentInput, safety, undefined, realApprovalOf(pass.decision), pass.decision.action === "ask" ? pass.decision.persistedRuleShadowed : undefined, pass.decision.action === "ask" ? pass.decision.decisionReason : undefined, pass.decision.action === "ask" ? pass.decision.probeReason : undefined, pass.decision.action === "ask" ? pass.decision.probeCause : undefined, pass.decision.action === "ask" ? pass.decision.segmentCoverage : undefined, pass.decision.action === "ask" ? pass.decision.matchedAskRule : undefined, pass.decision.action === "ask" ? pass.decision.probeMandated : undefined];
@@ -119,7 +119,7 @@ export type NoticeAudience = "user" | "operator";
119
119
  * the union's catalog ({@link EngineNotice.code}). The groups below say only which family a row joins,
120
120
  * so the catalog is not written a second time here and the two cannot drift.
121
121
  */
122
- export declare const ENGINE_NOTICE_CODES: readonly ["config.autocompact_window_clamped", "config.env_timeout_discarded", "config.materialize_env_discarded", "config.models_swapped", "config.read_face_swapped", "config.read_face_deployment_clamped", "config.write_face_swapped", "config.write_face_deployment_clamped", "config.tool_model_gate_removed", "config.tool_model_gate_unknown_class", "config.tool_model_gate_env_invalid", "config.artifact_host_invalid", "config.task_root_not_canonical", "config.write_protection_target_view_absent", "config.write_protection_unresolved", "config.execution_env_capability_invalid", "config.tool_card_undeclared", "config.tool_face_undeclared", "config.tool_face_invalid", "config.forward_sink_failed", "config.durable_gate_unavailable", "config.peer_admission_out_of_range", "config.peer_lane_unmounted", "peer.inbound_disposition", "peer.held_settled", "peer.idle_subscription", "classifier.denial_limit", "classifier.fallback", "classifier.role_defaulted", "classifier.contract_broken", "checkpoint.execution_outcome_unrecorded", "delegation.transcript_integrity", "delegation.ask_unresolvable", "mcp.injection_dropped", "mcp.revocation_probe_failed", "mcp.server_redialed", "workflow.governance_key_stripped", "workflow.agent_option_ignored", "memory.session_polluted", "memory.harvest_quarantined", "memory.delegation_static_mark_waived", "memory.content_class_declared", "memory.hold_opened", "memory.hold_released", "memory.hold_disposed", "memory.consolidation_recommended", "memory.consolidation_committed", "memory.consolidation_conflict", "memory.consolidation_incomplete", "memory.consolidation_refused", "memory.consolidation_withheld", "memory.restore_withheld", "route.fallback_to_primary", "route.classifier_unroutable", "route.base_url_changed_key_unchanged", "task.user_steer_undrained", "task.user_followup_undrained", "steering.parked_input_blocked", "task.turn_interrupted", "task.halt_unconsumed", "task.late_approval", "task.interrupt_unconsumed", "memory.capture_opted_out", "memory.capture_optout_unpersisted", "skills.listing_truncated", "agents.listing_truncated", "tool_result.offload_put_failed"];
122
+ export declare const ENGINE_NOTICE_CODES: readonly ["config.autocompact_window_clamped", "config.env_timeout_discarded", "config.materialize_env_discarded", "config.models_swapped", "config.read_face_swapped", "config.read_face_deployment_clamped", "config.write_face_swapped", "config.write_face_deployment_clamped", "config.tool_model_gate_removed", "config.tool_model_gate_unknown_class", "config.tool_model_gate_env_invalid", "config.artifact_host_invalid", "config.task_root_not_canonical", "config.write_protection_target_view_absent", "config.write_protection_unresolved", "config.execution_env_capability_invalid", "config.tool_card_undeclared", "config.tool_face_undeclared", "config.tool_face_invalid", "config.forward_sink_failed", "config.durable_gate_unavailable", "config.peer_admission_out_of_range", "config.peer_lane_unmounted", "peer.inbound_disposition", "peer.held_settled", "peer.idle_subscription", "classifier.denial_limit", "classifier.fallback", "classifier.role_defaulted", "classifier.contract_broken", "checkpoint.execution_outcome_unrecorded", "delegation.transcript_integrity", "delegation.ask_unresolvable", "mcp.injection_dropped", "mcp.revocation_probe_failed", "mcp.server_redialed", "workflow.governance_key_stripped", "workflow.agent_option_ignored", "memory.session_polluted", "memory.harvest_quarantined", "memory.delegation_static_mark_waived", "memory.content_class_declared", "memory.hold_opened", "memory.hold_released", "memory.hold_disposed", "memory.consolidation_recommended", "memory.consolidation_committed", "memory.consolidation_conflict", "memory.consolidation_incomplete", "memory.consolidation_refused", "memory.consolidation_withheld", "memory.restore_withheld", "memory.project_scope_absent", "route.fallback_to_primary", "route.classifier_unroutable", "route.base_url_changed_key_unchanged", "task.user_steer_undrained", "task.user_followup_undrained", "steering.parked_input_blocked", "task.turn_interrupted", "task.halt_unconsumed", "task.late_approval", "task.interrupt_unconsumed", "memory.capture_opted_out", "memory.capture_optout_unpersisted", "skills.listing_truncated", "agents.listing_truncated", "tool_result.offload_put_failed"];
123
123
  /** A code this engine mints (see {@link ENGINE_NOTICE_CODES}). NOT the type of
124
124
  * `EngineNotice.code`, which stays `string` — a host forwarding its own notices through the same
125
125
  * sink is a supported shape, and narrowing that field would break it. */