@sema-agent/core 7.25.1 → 7.26.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 (83) hide show
  1. package/CHANGELOG.md +103 -0
  2. package/dist/core/ask-origin.d.ts +84 -0
  3. package/dist/core/ask-origin.js +31 -3
  4. package/dist/core/auto-compaction.d.ts +5 -0
  5. package/dist/core/auto-compaction.js +7 -0
  6. package/dist/core/auto-mode-directive.d.ts +45 -0
  7. package/dist/core/auto-mode-directive.js +47 -0
  8. package/dist/core/checkpoint-store.d.ts +10 -1
  9. package/dist/core/engine-notice.d.ts +10 -0
  10. package/dist/core/gate-lanes.js +18 -10
  11. package/dist/core/governance-codes.d.ts +1 -1
  12. package/dist/core/governance-codes.js +3 -0
  13. package/dist/core/hooks.d.ts +3 -0
  14. package/dist/core/mcp-probe.d.ts +122 -0
  15. package/dist/core/mcp-probe.js +132 -0
  16. package/dist/core/memory-engine/engine.d.ts +6 -1
  17. package/dist/core/memory-engine/engine.js +30 -2
  18. package/dist/core/memory-engine/index.d.ts +1 -0
  19. package/dist/core/memory-engine/index.js +1 -0
  20. package/dist/core/memory-engine/shadowing.d.ts +58 -0
  21. package/dist/core/memory-engine/shadowing.js +40 -0
  22. package/dist/core/memory-engine/tools.d.ts +33 -1
  23. package/dist/core/memory-engine/tools.js +136 -31
  24. package/dist/core/memory-engine/types.d.ts +10 -0
  25. package/dist/core/memory.d.ts +60 -0
  26. package/dist/core/memory.js +47 -2
  27. package/dist/core/permission-rule-provider.js +6 -1
  28. package/dist/core/permission-rule-store.d.ts +87 -8
  29. package/dist/core/permission-rule-store.js +223 -76
  30. package/dist/core/permission-rule-sync.js +14 -5
  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 +6 -2
  43. package/dist/core/runner/prepare-inherited-gate.js +6 -3
  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/runner/session-rule-policy.d.ts +1 -1
  55. package/dist/core/runner/session-rule-policy.js +6 -7
  56. package/dist/core/session-policy-store.d.ts +18 -0
  57. package/dist/core/session-policy-store.js +3 -0
  58. package/dist/core/store-contracts/permission-rule-write-contract.d.ts +20 -0
  59. package/dist/core/store-contracts/permission-rule-write-contract.js +90 -0
  60. package/dist/core/store-contracts/read-back-shape-contract.d.ts +56 -0
  61. package/dist/core/store-contracts/read-back-shape-contract.js +78 -0
  62. package/dist/core/task-result.d.ts +11 -0
  63. package/dist/core/task-spec.d.ts +31 -0
  64. package/dist/core/tool-catalog-entries.js +3 -3
  65. package/dist/core/tool-policy.d.ts +38 -0
  66. package/dist/core/tool-policy.js +5 -1
  67. package/dist/core/types.d.ts +1 -1
  68. package/dist/index.d.ts +11 -2
  69. package/dist/index.js +10 -1
  70. package/dist/stores/file/adoption/adopt.js +4 -10
  71. package/dist/stores/file/permission-rule-store.d.ts +5 -1
  72. package/dist/stores/file/permission-rule-store.js +52 -31
  73. package/dist/stores/file/session-policy-store.d.ts +32 -0
  74. package/dist/stores/file/session-policy-store.js +72 -50
  75. package/dist/tools/fs/shell-read-check.d.ts +58 -1
  76. package/dist/tools/fs/shell-read-check.js +47 -33
  77. package/dist/tools/fs/shell-read-establish.js +19 -8
  78. package/dist/tools/fs/shell-read-judge.d.ts +3 -0
  79. package/dist/tools/fs/shell-read-judge.js +3 -1
  80. package/package.json +1 -1
  81. package/test/export-surface.snapshot.json +79 -1
  82. package/wire-changes/core@7.26.0.json +695 -0
  83. package/wire-changes/core@7.26.1.json +605 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,100 @@
1
1
  # Changelog
2
2
 
3
+ ## 7.26.1 — 2026-09-21
4
+
5
+ ### One store, one answer about one file; one lineage judgment at every station (#1003 + #1000; narrowing; +0 exports; @server)
6
+
7
+ - **#1003 — the enumeration face judged less than the per-key read.** `FileSessionPolicyStore.getRules` validated a row's SHAPE (a `rev` of at least 1, a whole `gen`, string-array rule fields) and answered ABSENT with a disclosure when it failed; `listBySession` checked "a JSON object carrying our `__sid`" and cast. So one store answered two different things about one file: a row carrying `toolDeny: "rm"` or `rev: 0` was absent to the keyed read and served VERBATIM through the enumeration face that the cross-backend policy export and a deployment's session listing read.
8
+ - **What changes.** The shape judgment is one function now, and both rules-serving faces run it, in the same order, with the same disclosure words. **Narrowing:** a malformed row is absent from `listBySession` and disclosed once through `onCorruptRead`, where it used to be returned. The tolerance is unchanged — one bad row does not void an enumeration. The enumeration attributes before it judges shape, so another session's malformed row is that session's fact rather than noise repeated in every other session's listing.
9
+ - **Who is hurt.** A deployment that read `listBySession` expecting whatever is on disk (a characterization test, a backup differ) sees the row vanish and one extra disclosure. That is the point: nothing could ENFORCE that row, so nothing should have exported it either. The erase sweep and the ownership rebind are unaffected — they read a row's attribution, never its rules, so a malformed-rules row is still erasable and still travels with its key.
10
+ - **#1000 — an absent `gen` meant three things.** `StoredSessionRules.gen` names the lineage a row's `rev` counts within, and the type says an absent one is generation 0. Three stations read it otherwise: the file store's stamp took a READ row carrying no `gen` for "no lineage" and opened a new generation over it; the live reader skipped the comparison unless BOTH rows carried the field; the inherited-gate merge read either side's absence as "same lineage, go ahead". A row of generation 1 could therefore pass for a continuation of a row that spelled its generation nowhere.
11
+ - **What changes.** `continuesLineage(a, b)` is that judgment, in one place, absent = 0; all three stations read it, including the per-call handoff claim, which compared revs without asking at all. **Narrowing:** a live row of another generation no longer replaces the one a leg enforces or the one a checkpoint recorded. The file store's stamp continues a row it could READ and keeps the new generation for the case it was written for — bytes that exist and cannot be read as a row. That half is load-bearing for the other: without it, an upgraded deployment's first tightening over a pre-lineage row would open a generation the leg enforcing that very row would then refuse to follow.
12
+ - **Who is hurt (@server).** A durable twin that does not persist `gen` while core's checkpoint recorded one has its live ancestor rows refused at the merge — fail-closed, the checkpoint's row stands. The repair is to carry the column; both bundled backends do. Absence still carries one other fact, and it is a different question: a backend that stamps no lineage at ALL offers its reader no recycle evidence, so the live reader keeps the older, weaker guard there (it watched the row vanish). What that guard cannot do at the MERGE, which has no observation channel, is recorded as KL-1003a.
13
+ - **Three more from the adversarial round, same class.** (1) **An erase now ends the lineage of EVERY row it removes**, including a torn row belonging to another session. The sweep deletes every unparseable row it meets — that ruling stands — but it used to end the lineage only when the mark named the session being erased, so a foreign key lost its row and kept its generation: the key's next write found no file, continued a generation a leg still enforced, and was taken for that row at a higher rev (measured: the leg answered `allow`, now `deny`). One removal, one lineage transition; the "whose session is this" special case is gone. (2) **The no-lineage fallback reads the LIVE row.** Whether a backend stamps lineages at all is a fact about what it writes NOW; reading the ENFORCED row's absence the same way refused a legitimate generation-0 tightening forever after one transient gap — precisely the row the new file-store stamp produces on an upgraded deployment. A genuinely lineage-less recreation after an observed gap is still refused (pinned as the control). (3) **`listBySession` no longer casts a principal it cannot spell**: a `__principal` that is neither a string nor null is disclosed and skipped instead of shipped through a `string | undefined` seat. That is the enumeration's own attribution question — the keyed read never asks it, because its key comes from the caller.
14
+ - Wire table: `docs/wire-consumption/unreleased-1003.md`. Export surface: `npm run handoff:diff v7.26.0 HEAD` = +0 / −0.
15
+ ### A rule-write outcome is READ, and every revision it carries is judged (#1007; behaviour NARROWING; +0 exports; @server @cli @client-core)
16
+ - **What changed.** `apply`'s answer is DATA — a foreign backend, a deserialized reply, a twin compiled against another generation — and both write entries used to discriminate it by asking whether it carried `conflict` or `refused`, calling everything else a landing. `{}` satisfies that test, so a write nobody could confirm was reported as committed at revision `undefined` through a field the type says is a `number`; `null` threw the `in` operator out of an entry whose declared result union has no throwing arm. One reader (`readRuleWriteOutcome`, module-internal) now tells the arms apart once, for all three call sites (`addPersistedRule`, `removePersistedRule`, the sync round).
17
+ - **What a revision is.** A non-negative safe integer. **Zero is a landing**: a session-scope add lands in a partition that has no revision of its own and the landing reports the durable revision unchanged, which is `0` for a bucket nothing has written yet. A string, a negative, a fraction, `NaN`, `null` or an absent `rev` is an answer nothing can read.
18
+ - **Who is hurt.** A host whose rule backend answered a landing without a readable revision was being told `added` / `removed`; it is now told `failed` with `committed: "unknown"` and reconciles with `list()`. That is the narrowing: a false success becomes an honest indeterminate. **Compensation**: the add entry's indeterminate arm takes an evidence read-back, so a backend that really landed the write still reports `added` — only a backend that landed nothing, or cannot be read, reaches the failure. **Consumer matrix**: in-tree backends (file, in-memory, the session partition) are unaffected — each already answered a readable revision, pinned by the positive controls.
19
+ - **Tags are judged by VALUE, not presence.** `{refused: null}` is not a refusal and `{conflict: false}` is not a decline; an answer carrying both tags is an answer about neither. Reading a tag by presence turned a malformed answer into a DEFINITE `committed: "no"`, which is the same over-claim one arm over.
20
+ - **The landing report rides the same judgement.** A `sync` report that is not an object, or whose `droppedInbound` / `quarantined` are not lists of drop records, no longer reaches the sync round's walk (which threw a type error at a caller holding a committed write). The revision still proves the landing, so the landing stands and the lost report is DISCLOSED in the round's warnings.
21
+ - **The session partition's answer is judged before translation.** `SessionRuleApplyResult` has two named arms (`{landed: true}` / `{refused: "session_ended"}`); the routing writer read it by absence of a refusal, so a host partition answering `{}` produced a well-formed landing at the caller's own revision. Exactly one arm now; anything else is a write face that failed, which the entries report as indeterminate.
22
+ - **A readable revision is not evidence of a row.** The judgement above is about the ANSWER; a backend that reports a landing at a perfectly readable revision and writes nothing answers just as well as one that wrote. Both add doors (the record-less entry and the consent protocol's redemption) now WITNESS a reported landing before reporting it — the same read-back the indeterminate arm already took, asking the evidence face for THIS call's own dot, which is minted once and so identifies this call's work and nobody else's. Not seen ⇒ `committed: "unknown"`, never `"no"` (an eventually-consistent backend may converge later); the read-back itself failing ⇒ `unknown` as well. A backend that really wrote still lands, including when it under-reports its revision.
23
+ - **Two smaller over-claims, same ticket.** The bundled file backend's read-back failure said "the change was not committed" about bytes it had not observed — it now says only that the store did not read back as the version just written, and that whether the change reached disk is unobservable from there. And a write-face accessor that THREW was reported as "nothing was written": the backend's own code had run on the seat whose purpose is to touch bytes, so that arm is now `unknown` (an ABSENT face keeps `no` — nothing ran).
24
+ - **Backend authors**: `permissionRuleWriteContract` now asserts that every arm carrying a revision carries a readable one, and that a decline is tagged `conflict: true`. A backend answering `{}`, or tagging a decline with anything else, fails the kit — it was being certified as usable by an engine that reads its answers as unreadable.
25
+
26
+ ### `store.read_back_shape_judged` — one read-back rule across every store family (#1009; additive; +6 exports; @server @test)
27
+ - **The sentence.** `@contract store.read_back_shape_judged` (`src/core/store-contracts/read-back-shape-contract.ts`, quoted for integrators in `docs/INTEGRATION-CORE.md` §5.0): every read a store answers — a single get and an enumeration alike — judges the SHAPE of what it read; a body that is not the shape the store writes reaches the caller as a refusal or as an absence carrying a disclosure that names the loss, never as an empty set, an empty object or a default. Which read-back failures a store TOLERATES is registered per store in §5.0, never inherited from another store's.
28
+ - **The suite.** `storeReadBackShapeContract` (exported, vitest-free like its siblings) plants four corrupt-body forms — `not-an-object` / `missing-required` / `wrong-type` / `torn-json` (`CORRUPT_BODY_FORMS`) — and asks one mechanical question per form: does the corrupt read answer something OTHER than the empty read, and does it keep the planted bytes out of its answer. Clause 1 is the control: the empty and seeded reads must SUCCEED, answer differently, and the empty answer must be REPRODUCIBLE — which is what stops a face that throws at everything, a face that answers everything alike, and a face carrying a per-read id or timestamp from passing for free.
29
+ - **Its ceiling is written down**: clause 2 proves the corrupt case is distinguishable, not that the distinguishing answer is correct. A backend with no bytes a foreign writer could damage declares that ONCE, at the subject, with a reason — never per attempt, where a planter that had simply gone blind could exempt a backend that still had bodies.
30
+ - **The census** (`test/backlog1009-store-read-back-shape.test.ts`) carries a row for every kit in `src/core/store-contracts/`: bound, pending a named ticket, unbound with what is known, or not a store. A kit with no row reds — "nobody wrote one" and "there is nothing to write" may not look the same. Bound today: the durable permission-rule bucket's `list` / `readRaw` / `quarantined`, the unified `effective`, and `SessionPolicyStore.getRules`.
31
+ - **One store took the fix the sentence asks for**: the durable rule bucket's `quarantined()` answered "the quarantine is empty" about a bucket it could not read — exactly the rows a person is being sent to look at. It now refuses the way `list()` does; an ABSENT bucket is still an empty quarantine.
32
+ - **@server**: this is the sentence S-550 asked for. The SQL twin mounts the same suite against its own read faces; `docs/INTEGRATION-CORE.md` §5.0 is the clause-by-clause table.
33
+ ### The shell read station has a consumer face on the package root (#1010; additive; +20 exports; @cli)
34
+ - **Before.** Everything the read boundary knows lived inside `src/tools/fs/`, so a downstream fence over the 24-grid decision table had to deep-import `dist/tools/fs/*.js` — a path the package's `exports` map does not promise and a directory re-arrangement breaks. A prior statement that this face was already on the root was wrong: the package root had zero of these names.
35
+ - **After.** Twenty names on the root, and nothing else: the vocabulary (`SHELL_READ_FINDINGS`, `SHELL_READ_SEATS`, `SHELL_READ_FINDING_RANK`, `SHELL_READ_DECISION` — the closed sets already registered in `docs/CLOSED-SETS.md`), thirteen shape types (`ShellReadVerdict`, `ShellReadBoundary`, `ShellReadPlan`, `EstablishedRead`, `ReadEstablishment`, …), and ONE call: `judgeShellRead({ command, env, boundary, seat })` → `{ plan, establishment, verdict, offer? }`.
36
+ - **The one call is not a second judgment.** The station's pipeline is now written once and has two entries: the gate input's judge leg, and this one. They differ only in bookkeeping — the gate remembers an admission, the consumer face brings its own establisher — so the verdict a consumer reads is the verdict the gate applies, `offer` included (the verdict's grant candidate through the gate's own `normalizeReadRootCandidate`). Pinned against the real lane (`runToolGate` + the station over a `NodeExecutionEnv` and a real directory) on ten command shapes × three seats.
37
+ - **The steps between stay private** (`planShellReads`, the establisher, `judgeEstablishedPlan`, `foldShellReadFindings`, `decideShellRead`, `readRootGrantCandidateOf`, `judgeShellReadLexically`): they are the station's working, not its answer, and they are free to move. A fence that needs one should name the grid and why the one call is not enough.
38
+ - **Which `ExecutionEnv` an offline judgment may use** — `docs/INTEGRATION-CORE.md` §9. `NodeExecutionEnv` over a temporary directory is the supported face: the same adapter production runs on. `StubExecutionEnv` reads LEXICALLY (measured: a symlink inside the roots pointing out reads `none`/`vouch` under it where the real env reads `outside_roots`/`mandated`; a pattern it cannot expand reads `unestablishable` where the real env names the targets), and `judgeShellReadLexically` is a third reading again — on a pattern that expands onto such a symlink it vouches. Core ships no in-memory filesystem fixture: a third derivation of `realpath`/glob semantics is a third place for them to be wrong.
39
+ - **What the one call does NOT carry: a deadline.** Bounding an environment-facing wait and substituting a fail-closed verdict is the gate FOLD's policy (`withGateTimeout` + the station's `unanswered` arm), not the station's — the station has never held one on either entry. So a stalling filesystem adapter, or a `boundary.alsoInside` promise that never settles, stalls this call where a run would refuse; `signal` reaches the environment's own `canonicalPath` / `listDir` and is advisory, not a deadline (measured: an already-aborted signal still answers). A consumer wanting the gate's posture races its own deadline and reads the row the fold reads, `SHELL_READ_DECISION.unestablishable[seat]`. Recorded as `KL-1010a`; a second deadline inside the library would be a second place deciding it, and would turn a slow-but-correct filesystem's true answer into a fail-closed one with nobody having asked.
40
+ - **Who is hurt / compensation / consumers.** Nobody: 0 exports removed, 0 changed kind, no gate decision moves, no wire key changes. Deep `dist/tools/fs/*.js` imports keep working. @cli L-467 moves its fence to the root face at its own pace. Wire table: `docs/wire-consumption/unreleased-1010.md`. Export surface: `npm run handoff:diff v7.26.0 HEAD` = +20 / −0.
41
+
42
+ ### Merge-tree adversarial round: two fixes (R1 #1000 × erase; R2 #1014 the traversal grant)
43
+ - **R1.** The erase sweep ends a torn row's lineage for EVERY torn row it removes, and the row always goes (a crash artifact must not outlive a delete that reported success): a mark that names no session is bumped as found; a torn row with NO mark leaves a lost-lineage tombstone (`{__hwm: 0, __gen: 0, __lineage: "lost"}`) in the mark's place — without evidence a minted generation could repeat one a leg still enforces (round 2: an erased-once key's enforced row is already generation 1) — and a torn row whose mark is corrupt leaves that mark as it is. A write to a key carrying the tombstone, or an unreadable row and no mark, is refused (`SessionPolicyError("corrupt")`) rather than guessed; deleting the mark is the operator's restart, as the mark's own contract says. Before the fix a session-less mark was skipped (the recreation continued the enforced generation) and a missing one was minted.
44
+ - **R2 (#1014).** A traversal's root-grant offer names what the environment said the target IS, not what its provenance was: `EstablishedRead.node` (`"directory" | "file"`, set only by the walk — the root by its own listing, a member by whether the walk listed it) and `readRootGrantCandidateOf` grants a file through its containing directory. Before the fix `grep -R needle /abs/file.txt` and a recursive read descending a link onto an outside file offered the FILE as a directory grant (`{ dir: …/report.txt }`); a link onto a directory is still granted as that directory (control). The same derivation serves the gate and `judgeShellRead`, so both faces change together.
45
+
46
+
47
+ ## 7.26.0 — 2026-09-21
48
+
49
+ ### Six root exports the neighbours were hand-copying (#995; additive; +6 exports; @server @cli @client-core)
50
+ - `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.
51
+
52
+ ### A no-run MCP probe: connect, list, close, with the run's own rows (additive; +2 exports; @server @client-core @cli)
53
+ - **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`.
54
+ - **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.
55
+ - **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`.
56
+ - **`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.
57
+ - **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.
58
+ - **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.
59
+ - **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.
60
+ - 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.
61
+ ### 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)
62
+ - **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.
63
+ - **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.
64
+ - **`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).
65
+ - **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.
66
+ - **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.
67
+ - **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.
68
+ - `test/backlog1002-auto-mode-directive.test.ts` (15 pins, 7 mutations recorded); `docs/wire-consumption/unreleased-1002.md`; `docs/INTEGRATION-CORE.md` §4.7.
69
+ ### 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)
70
+ - **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.
71
+ - **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.
72
+ - **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`.
73
+ - **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.
74
+ - **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).
75
+ - 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`.
76
+
77
+ ### An unnamed write-delta kind is refused by both backends, identically (#1001; P2; loud refusal where the file backend silently deleted)
78
+ - **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.
79
+ - **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.
80
+ - **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.
81
+ - **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`.
82
+ - **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.
83
+ - 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`.
84
+ ### A mandate is a fact about the ask, not about a lane — `AskRequest.mandated` (#997; P2; approval face; additive, no export change)
85
+ - **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.
86
+ - **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.
87
+ - **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.
88
+ - **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.
89
+ - **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.
90
+ - 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.
91
+
92
+ ### Merge-tree adversarial round: two fixes (R1 #997 × durable park; R2 #1002 × compaction × resume)
93
+ - **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.
94
+ - **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).
95
+ - 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.
96
+
97
+
3
98
  ## 7.25.1 — 2026-09-20
4
99
 
5
100
  ### 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)
@@ -151,6 +246,14 @@
151
246
  - **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.
152
247
  - **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).
153
248
  - **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.
249
+ ### 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)
250
+ - **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.
251
+ - **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.
252
+ - **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).
253
+ - **New export (+1):** `deriveShadowedSet` — the one predicate behind all four faces, beside `deriveSupersededSet`.
254
+ - **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.
255
+ - `TaskResult.effectiveMemoryScopes` (mounted) gains `projectKey?` echoed verbatim — absence means the deployment said nothing, which is not `{ absent }`, which means it looked and found none.
256
+ - Tool contract revisions: `memory_search` 6 → 7, `memory_get` 6 → 7, `memory_index` 1 → 2. Wire table: `docs/wire-consumption/unreleased-863p2.md`.
154
257
 
155
258
  ## 7.24.2 — 2026-09-20
156
259
 
@@ -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. */