@sema-agent/core 7.38.2 → 7.39.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 (98) hide show
  1. package/CHANGELOG.md +175 -6
  2. package/dist/core/ask-origin.d.ts +16 -7
  3. package/dist/core/ask-origin.js +4 -2
  4. package/dist/core/auto-mode-directive.d.ts +114 -19
  5. package/dist/core/auto-mode-directive.js +67 -19
  6. package/dist/core/checkpoint-store.d.ts +72 -41
  7. package/dist/core/checkpoint-store.js +28 -48
  8. package/dist/core/classifier-unavailable-arm.js +2 -2
  9. package/dist/core/decision-receipt.d.ts +2 -0
  10. package/dist/core/decision-receipt.js +1 -1
  11. package/dist/core/engine-notice.d.ts +41 -17
  12. package/dist/core/engine-notice.js +26 -14
  13. package/dist/core/gate-outcome.d.ts +80 -42
  14. package/dist/core/gate-outcome.js +21 -6
  15. package/dist/core/governance-codes.d.ts +1 -3
  16. package/dist/core/governance-codes.js +0 -3
  17. package/dist/core/hooks.d.ts +1 -1
  18. package/dist/core/hooks.js +2 -2
  19. package/dist/core/human-input-projection.d.ts +12 -0
  20. package/dist/core/human-input-projection.js +12 -0
  21. package/dist/core/memory-engine/engine.js +11 -11
  22. package/dist/core/memory-engine/layout.d.ts +21 -2
  23. package/dist/core/memory-engine/layout.js +17 -5
  24. package/dist/core/mode-policy.d.ts +26 -0
  25. package/dist/core/mode-policy.js +37 -1
  26. package/dist/core/output-style.d.ts +21 -0
  27. package/dist/core/output-style.js +29 -0
  28. package/dist/core/real-approval.d.ts +25 -0
  29. package/dist/core/real-approval.js +27 -0
  30. package/dist/core/reminder-mint.d.ts +0 -9
  31. package/dist/core/reminder-mint.js +0 -11
  32. package/dist/core/retired-keys.d.ts +2 -1
  33. package/dist/core/retired-keys.js +3 -1
  34. package/dist/core/runner/assemble-result.d.ts +2 -0
  35. package/dist/core/runner/assemble-result.js +1 -1
  36. package/dist/core/runner/compaction-restate.d.ts +45 -4
  37. package/dist/core/runner/compaction-restate.js +89 -11
  38. package/dist/core/runner/contracts.d.ts +6 -6
  39. package/dist/core/runner/denial-limit-arms.js +4 -4
  40. package/dist/core/runner/inherited-ask-grants.d.ts +4 -2
  41. package/dist/core/runner/inherited-ask-grants.js +0 -3
  42. package/dist/core/runner/permission-rule-lanes.d.ts +18 -0
  43. package/dist/core/runner/permission-rule-lanes.js +35 -25
  44. package/dist/core/runner/prepare-context-lane.d.ts +1 -0
  45. package/dist/core/runner/prepare-context-lane.js +6 -0
  46. package/dist/core/runner/prepare-inherited-gate.js +2 -9
  47. package/dist/core/runner/prepare-task.js +10 -24
  48. package/dist/core/runner/prepare-wiring-manifest.js +2 -2
  49. package/dist/core/runner/prompt-block.d.ts +9 -0
  50. package/dist/core/runner/prompt-block.js +8 -0
  51. package/dist/core/runner/resume-admission.js +4 -1
  52. package/dist/core/runner/resume-apply.js +28 -18
  53. package/dist/core/runner/resume-policy-outcome.js +10 -4
  54. package/dist/core/runner/run-attachment-seats.js +1 -1
  55. package/dist/core/runner/run-identity-wiring.js +9 -2
  56. package/dist/core/runner/run-leg.js +42 -31
  57. package/dist/core/runner/run-settle-and-teardown.js +4 -3
  58. package/dist/core/runner/run-turn-boundary.js +4 -3
  59. package/dist/core/runner/runtask.d.ts +4 -2
  60. package/dist/core/runner/steer-admission.d.ts +9 -0
  61. package/dist/core/runner/steer-admission.js +8 -0
  62. package/dist/core/runner/stream-steer-verb.js +16 -11
  63. package/dist/core/runner/synthetic-tools.d.ts +4 -0
  64. package/dist/core/runner/tool-turn-repair-seat.js +1 -1
  65. package/dist/core/runner/turn-attachments.d.ts +16 -22
  66. package/dist/core/runner/turn-attachments.js +3 -20
  67. package/dist/core/runner-deps.d.ts +1 -1
  68. package/dist/core/store-contracts/checkpoint-store-contract.d.ts +8 -0
  69. package/dist/core/store-contracts/checkpoint-store-contract.js +23 -0
  70. package/dist/core/task-event.d.ts +7 -2
  71. package/dist/core/task-spec.d.ts +29 -7
  72. package/dist/core/terminal-cause.d.ts +44 -3
  73. package/dist/core/terminal-cause.js +5 -0
  74. package/dist/core/tool-policy.d.ts +55 -29
  75. package/dist/core/tool-policy.js +42 -31
  76. package/dist/core/types.d.ts +1 -1
  77. package/dist/core/types.js +1 -1
  78. package/dist/core/wiring-manifest.d.ts +17 -3
  79. package/dist/core/wiring-manifest.js +8 -3
  80. package/dist/engine/harness/agent-harness.d.ts +10 -0
  81. package/dist/engine/harness/agent-harness.js +12 -1
  82. package/dist/engine/llm/tool-turns.d.ts +15 -2
  83. package/dist/engine/llm/tool-turns.js +2 -2
  84. package/dist/index.d.ts +4 -3
  85. package/dist/index.js +4 -3
  86. package/dist/prompt-assembly/event-registry.js +2 -1
  87. package/dist/stores/file/fs-atomic.d.ts +29 -4
  88. package/dist/stores/file/fs-atomic.js +55 -6
  89. package/dist/tools/fs/index.d.ts +2 -1
  90. package/dist/tools/fs/read-deny-home.d.ts +3 -2
  91. package/dist/tools/fs/read-deny-home.js +6 -4
  92. package/dist/tools/fs/shell-read-check.d.ts +10 -3
  93. package/dist/tools/fs/shell-read-check.js +2 -2
  94. package/package.json +1 -1
  95. package/test/export-surface.snapshot.json +21 -1
  96. package/wire-changes/core@7.38.2.json +20 -2
  97. package/wire-changes/core@7.38.3.json +36 -0
  98. package/wire-changes/core@7.39.0.json +845 -0
@@ -703,7 +703,7 @@ export type TaskEvent = ({
703
703
  * is unchanged. `preview` is the injected text clipped to a display bound.
704
704
  *
705
705
  * design/133 (LOW-11/F11): the turn-boundary attachment producers ride the same echo — sources
706
- * `todo_reminder` / `task_reminder` / `changed_files` / `plan_mode` and the G1/续批 members
706
+ * `todo_reminder` / `task_reminder` / `changed_files` and the G1/续批 members
707
707
  * `background_tasks` / `tools_delta` / `agent_listing` / `skills_listing` / `mcp_instructions`
708
708
  * (one event per attachment; a multi-attachment boundary still coalesces into ONE steer message
709
709
  * on the model lane). [c209-C]: the listing family's FIRST-FRAME deliveries (initial roster /
@@ -714,9 +714,14 @@ export type TaskEvent = ({
714
714
  * (first-frame / resume-continuation / compaction re-assert deliveries alike). Its `preview`
715
715
  * is a CONSTANT wording on purpose (never frame bytes): branch names and status text are
716
716
  * repo-controlled and must not enter the event telemetry plane through this echo.
717
+ *
718
+ * `output_style` (additive member): the frame stating the deployment-declared `TaskSpec.outputStyle`, echoed
719
+ * at every delivery (a leg's first turn, a resumed leg's continuation, each continuing boundary).
720
+ * The permission-posture frames (planning / bypass / auto, and their exits) are model-lane only and have no
721
+ * echo; the retired `plan_mode` member was the echo of the opt-in planning reminder they replaced.
717
722
  */
718
723
  type: "steering_injected";
719
- source: "limit_approach" | "todo_reminder" | "task_reminder" | "tool_search_usage_reminder" | "changed_files" | "plan_mode" | "date_change" | "instructions_change" | "workflow_size_guideline_change" | "budget_usd" | "total_tokens_reminder" | "background_tasks" | "tools_delta" | "agent_listing" | "skills_listing" | "mcp_instructions" | "mcp_dropped_tools" | "final_verification" | "git_status";
724
+ source: "limit_approach" | "todo_reminder" | "task_reminder" | "tool_search_usage_reminder" | "changed_files" | "output_style" | "date_change" | "instructions_change" | "workflow_size_guideline_change" | "budget_usd" | "total_tokens_reminder" | "background_tasks" | "tools_delta" | "agent_listing" | "skills_listing" | "mcp_instructions" | "mcp_dropped_tools" | "final_verification" | "git_status";
720
725
  preview: string;
721
726
  } & TaskEventIdentity) | ({
722
727
  /**
@@ -252,6 +252,28 @@ export interface TaskSpec {
252
252
  * so it never breaks prefix-cache friendliness. Composes WITH `systemPrompt` (appended after it).
253
253
  */
254
254
  appendSystemPrompt?: string;
255
+ /**
256
+ * The OUTPUT STYLE the deployment's shell has active, declared so the engine can state it to the model in its
257
+ * own marked reminder: `"<name> output style is active. <turnReminder>"` (the reference implementation's
258
+ * per-turn reminder sentence; absent `turnReminder` ⇒ its default "Remember to follow the specific guidelines
259
+ * for this style."). The style's guidelines themselves are not carried here — they go in `systemPrompt` /
260
+ * `appendSystemPrompt`; this seat is the per-turn statement that the style is in force, which a reminder the
261
+ * shell splices into the objective cannot be (unmarked reminder-shaped text is data). Declare it only for a
262
+ * non-default style; there is no client-trusted reminder seat behind it — the engine renders the sentence.
263
+ *
264
+ * Cadence: the first turn of every leg (a resumed leg's continuation included) and every continuing turn
265
+ * boundary — the reference attaches it at every attachment cycle, with no window. Echoed as
266
+ * `steering_injected` with source `output_style`.
267
+ *
268
+ * Refused at prepare (`config.output_style_invalid`), never ignored: a value that is not an object of exactly
269
+ * these two members; a `name` that is not a string, is empty, or is longer than 256 characters
270
+ * (`OUTPUT_STYLE_NAME_MAX`, the reference implementation's own bound — it drops the reminder past it); a
271
+ * `turnReminder` that is present but not a non-empty string.
272
+ */
273
+ outputStyle?: {
274
+ name: string;
275
+ turnReminder?: string;
276
+ };
255
277
  /**
256
278
  * Per-task ROLE-LAYER prompt provider (overrides `RunnerDeps.promptProvider`). Lets one Runner serve
257
279
  * different scenarios with different role prompts. 提示词主权批: `stableSystem` supplies the role base
@@ -889,7 +911,7 @@ export interface TaskSpec {
889
911
  enableFork?: boolean;
890
912
  /**
891
913
  * The OPERATOR's shell floor: `"always"` ⇒ every `bash` / `Monitor` command is a confirmed ask (an
892
- * `operator_forced` ask — kept under `bypassPermissions`, not settled by `dontAsk`; a standing allow rule never
914
+ * `operator_forced` ask — kept under `bypassPermissions`, refused by `dontAsk` like every ask; a standing allow rule never
893
915
  * clears it). Absent ⇒ no floor: the shape tier the engine installs on every full shell (a provably read-only
894
916
  * single command runs, everything else asks) is arbitrated by the task's {@link permissionMode}. The retired
895
917
  * values `"off"` / `"classify"` are refused at the door (`config.shell_gate_value_retired`): the tier is no longer a
@@ -912,7 +934,7 @@ export interface TaskSpec {
912
934
  * `bypassPermissions` releases every ask but the immune classes (an explicit ask rule, a hook's ask, the
913
935
  * question tool, an unverifiable recursive delete, an org rule, an operator's forced confirmation, unreadable
914
936
  * governance, the classifier's denial-limit fallback) and discloses each release; `dontAsk` refuses every ask
915
- * but a hook's or an operator authority's; `auto` arms the classifier (`RunnerDeps.autoMode` present and
937
+ * but a hook's own (no rule or safety check also asked); `auto` arms the classifier (`RunnerDeps.autoMode` present and
916
938
  * `RuntimeCaps.autoMode` not `false`, else folded to `default` and announced); `plan` reads only and mounts the
917
939
  * plan pair; `acceptEdits` writes inside the root without asking; `default` asks. The mode also installs its own
918
940
  * seats ({@link import("./mode-policy.js").modePolicyOf}).
@@ -1240,9 +1262,6 @@ export interface TaskSpec {
1240
1262
  * whose mtime moved past the recorded read time (+2s epsilon) is listed (path-only v1, "do not
1241
1263
  * blindly revert — re-read"). OFF ⇒ zero stat calls. Self-edits are immune (write-back
1242
1264
  * refreshes the read stamp); mtime skew on exotic fs remains a documented false-positive source.
1243
- * - `planModeReminder`: while `enter_plan_mode` is active, re-surface the read-only constraint
1244
- * every 5 assistant turns (full copy every 5th, sparse between) so a long plan phase doesn't
1245
- * drift into write-deny thrash. Exiting plan mode stops it.
1246
1265
  * - `backgroundTasks` (G1 通告层 — CC /compact parity): after a compaction LANDS, the
1247
1266
  * next boundary announces this run's still pending/running background tasks (task registry
1248
1267
  * snapshot: id/description/status, CC three-state copy). ONE-SHOT per compaction — never
@@ -1274,8 +1293,12 @@ export interface TaskSpec {
1274
1293
  * the add lane for a future dynamic-connect face. Added instruction text is UNTRUSTED server
1275
1294
  * text → fenced with `delimitUntrusted` (deliberate CC deviation: CC injects it raw).
1276
1295
  *
1296
+ * The read-only planning posture is not a member: the engine states it whenever the leg's mode is `plan`
1297
+ * (with its restatement cadence and its exit frame), as it states the bypass and auto postures. A spec still
1298
+ * carrying the retired `planModeReminder` key is refused at prepare (`config.spec_retired_key`).
1299
+ *
1277
1300
  * Telemetry: `steering_injected` (source `todo_reminder`/`task_reminder`/`changed_files`/
1278
- * `plan_mode`/`background_tasks`/`tools_delta`/`agent_listing`/`skills_listing`/
1301
+ * `output_style`/`background_tasks`/`tools_delta`/`agent_listing`/`skills_listing`/
1279
1302
  * `mcp_instructions`) + `stats.mechanisms.attachmentsInjected` (first-frame listing deliveries
1280
1303
  * emit the same echo frames and count too — source-accurate observability for the shell).
1281
1304
  */
@@ -1308,7 +1331,6 @@ export interface TaskSpec {
1308
1331
  changedFiles?: true | {
1309
1332
  maxFiles?: number;
1310
1333
  };
1311
- planModeReminder?: true;
1312
1334
  /**
1313
1335
  * SR-3 (CC 198 `budget_usd` lane, producer QFm pretty.js:480424-480428 + renderer :600225): the
1314
1336
  * progressive $-budget readout `USD budget: $used/$total; $remaining remaining` (CC-verbatim
@@ -20,7 +20,9 @@ import type { AssertAllKeysHandled } from "./ask-origin.js";
20
20
  * the one cause→plane derivation, for the faces that still speak the word.
21
21
  * - `completed` — the run finished (a person's clean halt included — see `TaskResult.haltedByUser`).
22
22
  * - `failed` — a limit, a provider failure, an abort, an invalid output…
23
- * - `blocked` — the AGENT reported, through `report_blocked`, that it cannot finish, and why. A TERMINAL
23
+ * - `blocked` — the run ended without doing the work, and `detail` says by whom: ABSENT = the AGENT reported,
24
+ * through `ReportBlocked`, that it cannot finish, and why; `detail.kind === "prompt_blocked_by_hook"` = a
25
+ * `userPromptSubmit` hook kept the prompt from being submitted (see {@link TerminalBlockedDetail}). A TERMINAL
24
26
  * cause: the leg is over and it waits for nobody (nothing to resume, no token). The waiting-on-a-person
25
27
  * states are the `paused` cause's two status words — `suspended` (a pre-action approval of a tool
26
28
  * call) and `needs_review` (a review pause); the task-registry face spells both as `parked`.
@@ -87,9 +89,23 @@ export type TerminalCause = {
87
89
  nestedPause?: PausedCause;
88
90
  } | {
89
91
  kind: "blocked";
90
- /** Why the agent could not finish — the agent's own words from its `report_blocked` call. Terminal: the
91
- * run is over and nobody is being waited on (contrast `paused`, whose token is the resume capability). */
92
+ /**
93
+ * Why the run ended without doing the work — one of two sources, told apart by {@link detail}. With no
94
+ * `detail`: the agent's own words from its `ReportBlocked` call. With `detail.kind ===
95
+ * "prompt_blocked_by_hook"`: the engine's sentence about the deployment's `userPromptSubmit` hook, framed
96
+ * as a reminder for the model-facing lane (the hook's own words, when it gave some, sit inside the
97
+ * frame). Terminal: the run is over and nobody is being waited on (contrast `paused`, whose token is the
98
+ * resume capability).
99
+ */
92
100
  reason: string;
101
+ /**
102
+ * WHO stopped the run, for the one shape a consumer offers its own way out of: `prompt_blocked_by_hook` —
103
+ * the prompt was NOT submitted (the objective never became a message and the loop never started), so
104
+ * changing it and sending again is meaningful; `cause` says which of the hook's four endings it was.
105
+ * PRESENCE: only on that terminal. ABSENT means the agent reported `ReportBlocked` ("I cannot go on"), or an
106
+ * engine from before this key — branch on `detail?.kind`, never on the prose in `reason`.
107
+ */
108
+ detail?: TerminalBlockedDetail;
93
109
  } | {
94
110
  kind: "paused";
95
111
  /**
@@ -136,6 +152,31 @@ export interface TerminalFailureDetail {
136
152
  /** The provider's own word for the refusal (its error envelope's `error.code`, else `error.type`), when stated. */
137
153
  code?: string;
138
154
  }
155
+ /**
156
+ * The endings of a `userPromptSubmit` hook that keep a prompt from being submitted — the closed set of
157
+ * {@link TerminalBlockedDetail.cause} words:
158
+ * - `"block"` — the hook answered and refused the prompt (its words ride `hookReason`).
159
+ * - `"crashed"` — the hook threw or rejected; the seat is fail-closed, a screen that failed has not cleared the prompt.
160
+ * - `"timeout"` — the hook did not answer within its `timeoutMs` bound; fail-closed the same way.
161
+ * - `"cancelled"` — the task was cancelled while the hook was still screening.
162
+ */
163
+ export declare const PROMPT_BLOCK_CAUSES: readonly ["block", "crashed", "timeout", "cancelled"];
164
+ export type PromptBlockCause = (typeof PROMPT_BLOCK_CAUSES)[number];
165
+ /** Membership test for {@link PromptBlockCause}. */
166
+ export declare function isPromptBlockCause(v: unknown): v is PromptBlockCause;
167
+ /** The machine-readable account a `blocked` cause may carry ({@link TerminalCause}). `kind` is closed; one member. */
168
+ export interface TerminalBlockedDetail {
169
+ kind: "prompt_blocked_by_hook";
170
+ /** Which of the hook seat's endings kept the prompt back ({@link PROMPT_BLOCK_CAUSES}). */
171
+ cause: PromptBlockCause;
172
+ /**
173
+ * The hook's own words for refusing the prompt, folded to one line, bounded, with the engine's envelope tags
174
+ * defused — still the hook's text, so a consumer renders it as untrusted. PRESENT only on cause `"block"` and
175
+ * only when the hook gave words; never carries the engine's reminder frame or its session mark (the framed
176
+ * sentence is `reason`).
177
+ */
178
+ hookReason?: string;
179
+ }
139
180
  /** The paused arm alone — the shape the run's committed-pause holder carries. */
140
181
  /**
141
182
  * The one disposition table over `TerminalCause["kind"]`: which causes a resume journal may REPLAY
@@ -1,3 +1,8 @@
1
+ export const PROMPT_BLOCK_CAUSES = Object.freeze(["block", "crashed", "timeout", "cancelled"]);
2
+ const PROMPT_BLOCK_CAUSE_SET = new Set(PROMPT_BLOCK_CAUSES);
3
+ export function isPromptBlockCause(v) {
4
+ return PROMPT_BLOCK_CAUSE_SET.has(v);
5
+ }
1
6
  export const TERMINAL_CAUSE_IS_REPLAYABLE = {
2
7
  completed: true,
3
8
  failed: false,
@@ -1,6 +1,7 @@
1
1
  import { type AskOrigin, type AssertAllKeysHandled } from "./ask-origin.js";
2
2
  import { type ReadRootGrantScreen } from "./read-root-grant-screen.js";
3
3
  import { type Settlement } from "./gate-outcome.js";
4
+ export { policyCannotAnswer, rowDemandsAPerson } from "./real-approval.js";
4
5
  import { type DecisionReason } from "./decision-receipt.js";
5
6
  export { isEngineRefusal } from "./decision-receipt.js";
6
7
  /** A tool call presented to a policy before it executes. */
@@ -160,8 +161,8 @@ export declare function advertisedWritableRootsSeat(seat: {
160
161
  export type { DecisionReason };
161
162
  /** Carry every engine attestation from a decision onto a reconstruction of it — the
162
163
  * seam for the engine's OWN clone sites outside this module (the inherited chain's final-form re-check carries the
163
- * post-rewrite args on a spread of an ancestor's deny). Not re-exported from `src/index.ts`. */
164
- export declare function carryEngineAttestations(from: object, to: object): void;
164
+ * post-rewrite args on a spread of an ancestor's deny). Returns the reconstruction. Not re-exported from `src/index.ts`. */
165
+ export declare function carryEngineAttestations<T extends object>(from: object, to: T): T;
165
166
  /** Carry a PRECEDING decision's settlement onto a later deny that has none of its own — the veto record (a person's
166
167
  * yes on the settlement, the refusing layer on the disposition; invariant I3) for the engine's own clone sites,
167
168
  * where the deny is minted over a decision that already carried an attested approval. A deny that carries its own
@@ -255,7 +256,9 @@ export declare function screenApproverAttribution(v: unknown): {
255
256
  * configuration is "no approver looked at this at all", indistinguishable from headless, for a rule
256
257
  * whose whole point is that a blanket policy setting must not be the thing that clears it (see
257
258
  * {@link resolveAsk}). A live auto-mode classifier verdict (tried earlier in the gate, if configured)
258
- * or a genuine approver CALLBACK (actual judgment, even if it always says yes) still can.
259
+ * or a genuine approver CALLBACK (actual judgment, even if it always says yes) still can — unless the
260
+ * callback itself reports its allow as the deployment's POLICY (`settledBy: "policy"`): that is the same
261
+ * "nobody judged this" a blanket seat is, and it is refused the same way.
259
262
  * - `deny` blocks the call.
260
263
  * Human-readable text is `message`; read it via {@link decisionText}.
261
264
  *
@@ -402,14 +405,15 @@ export type PermissionResult = {
402
405
  ruleUnreadable?: "content_deny" | "content_ask" | "persisted_call";
403
406
  /** The ask was authored by the DEPLOYMENT's own governance (an operator's must-ask list, a managed command
404
407
  * policy's default ask) — a fact a host's policy states on its own ask so the class derivation reads it
405
- * as `operator_forced`: a permission mode releases nothing an operator mandated, and `dontAsk` does not
406
- * settle it. Public: a host composing such a policy sets it; {@link combinePolicies} carries it onto the
408
+ * as `operator_forced`: a permission mode releases nothing an operator mandated (`dontAsk` refuses it like
409
+ * every ask). Public: a host composing such a policy sets it; {@link combinePolicies} carries it onto the
407
410
  * surviving ask (tighten-only). Not on the approval request — the card shows `origin` / `mandate`. */
408
411
  authoredBy?: "deployment";
409
412
  /** The classes of the OTHER asks a policy fold collapsed into this one
410
413
  * ({@link import("./ask-origin.js").decisionAskClassesOf} per contributing ask, unioned), so a member the
411
414
  * fold does not copy — a guard mark, an unreadable-rule fact, a bare real-approval bit — is not lost behind
412
- * the ask that won the slot. ENGINE-WRITTEN by {@link combinePolicies}; fold-internal, never on the request. */
415
+ * the ask that won the slot. ENGINE-WRITTEN by {@link combinePolicies}, which attests the object it returns; the receipt
416
+ * refuses the member on any other object (a policy's own, a copy, an edited list). Fold-internal, never on the request. */
413
417
  foldedAskClasses?: readonly import("./ask-origin.js").AskClass[];
414
418
  } | {
415
419
  action: "deny";
@@ -757,7 +761,7 @@ export declare const DECISION_MEMBERS: readonly DecisionMember[];
757
761
  * consumers with two values, and which consumer read which value would decide the call. So the caller's object is read
758
762
  * ONCE, here, and every station after the receipt (the fold's ranking, the origin stamp, the classifier's eligibility, the
759
763
  * mode arbitration, the approval request, the rewrite that executes) reads the copy; a copy handed to the screen again
760
- * comes back as it is, unread. In order: (1) the CARRIER's type — a decision that is not an object (a string, a function
764
+ * comes back as it is, unread (so does the engine's own refusal, {@link isEngineRefusal}). In order: (1) the CARRIER's type — a decision that is not an object (a string, a function
761
765
  * holding the members) has no `action`; (2) the ONE READ — ONE function ({@link receiveContract}) for the decision and for the
762
766
  * records it nests (`receiveNestedRecords`, decision-receipt.ts), on an ORDINARY object (a Proxy, as the carrier or anywhere on its prototype
763
767
  * chain, is refused before a trap of its could run): each contract member absent or an own enumerable DATA property, its value
@@ -793,6 +797,20 @@ export declare const DECISION_MEMBERS: readonly DecisionMember[];
793
797
  export declare function refuseOutOfContractDecision(d: PermissionResult, opts?: {
794
798
  reasonIsNonInput?: boolean;
795
799
  }): PermissionResult;
800
+ /**
801
+ * The PreToolUse hook seat's receipt under its public name — what a host that composes several hooks into the one the
802
+ * engine takes runs on EACH slot's result before folding it, so it reads the result the way the engine will. It is
803
+ * {@link refuseOutOfContractDecision} at the hook seam (`reasonIsNonInput`), the same function the engine's seat calls (hooks.ts
804
+ * `screenPreToolUseResult`): no second statement of the contract.
805
+ *
806
+ * What comes back, for an object: the engine's own refusal (a `deny` for which {@link isEngineRefusal} is true — return it as the
807
+ * composed hook's answer and the engine takes it as its own fail-closed deny), or the engine's frozen, detached copy of the
808
+ * result (fold the copy, never the original). `undefined`, `null` and `false` are no opinion and come back as themselves. The
809
+ * caller's object is not changed and none of its code runs; an answer handed back to this function returns as itself.
810
+ */
811
+ export declare function receiveHookResult(result: unknown): (PermissionResult & {
812
+ additionalContext?: string;
813
+ }) | null | undefined | false;
796
814
  /** The raw tool-name lists a name-keyed policy was built from (audit feed, see block note above). */
797
815
  export interface ToolPolicyNameSets {
798
816
  readonly allow?: readonly string[];
@@ -861,9 +879,10 @@ export type ApprovalSeatOutcome = boolean | (Extract<AskOutcome, {
861
879
  * WHAT `approve` MAY RETURN: a boolean, as always, or the `{allow, …}` OUTCOME OBJECT the `onAsk` seat
862
880
  * takes, read by the SAME screen ({@link readApproverOutcomeObject}) and narrowed to what this seat has
863
881
  * a lane for ({@link ApprovalSeatOutcome}) — so a deployment here can report WHO ended the wait
864
- * (`settledBy:"policy"` / `"timeout"`), attach the decider's `reason` and name the `approver`. A bare
865
- * `false` is unchanged: a person's refusal, with this factory's own terse sentence. Before this it was
866
- * the only refusal spelling there was, so a machine's decision filed a PERSON's record.
882
+ * (`settledBy:"policy"` for its own policy's yes or no, `"timeout"` for its own elapsed window), attach the
883
+ * decider's `reason` and name the `approver`. A bare `true` / `false` is unchanged: a person's approval or
884
+ * refusal, the refusal with this factory's own terse sentence. Before the object arm a bare boolean was the
885
+ * only spelling there was, so a machine's decision filed a PERSON's record.
867
886
  */
868
887
  export declare function createApprovalPolicy(opts: {
869
888
  /** Tools that need an approval decision. */
@@ -1302,9 +1321,10 @@ export interface AskRequest {
1302
1321
  * drives which deny text the model is told (stop-and-wait vs adapt-or-report), forks included. */
1303
1322
  readonly isDelegatedChild?: true;
1304
1323
  /** Carried from the originating {@link PermissionResult}'s ask
1305
- * variant of the same name: `true` ⇒ {@link resolveAsk} must not let a blanket `onAsk: "allow"`
1306
- * clear this ask (see that field's doc). `readonly`, filled by the gate from the decision, never a
1307
- * caller/worker-settable field. */
1324
+ * variant of the same name: `true` ⇒ {@link resolveAsk} must not let an allow NO PERSON JUDGED clear
1325
+ * this ask — a blanket `onAsk: "allow"`, or an approver's allow it reports as the deployment's policy
1326
+ * (`settledBy: "policy"`) (see that field's doc). `readonly`, filled by the gate from the decision, never
1327
+ * a caller/worker-settable field. */
1308
1328
  readonly requiresRealApproval?: boolean;
1309
1329
  /**
1310
1330
  * PRESENCE ONLY, and a FACT ABOUT THIS ASK: no persisted allow rule and no STANDING ANSWER — a release
@@ -1499,15 +1519,19 @@ export type OnAsk = "deny" | "allow" | ((req: AskRequest, signal?: AbortSignal)
1499
1519
  * refusing. The three words the host may self-report:
1500
1520
  * - `"human"` — a person answered (identical to omitting the field);
1501
1521
  * - `"timeout"` — the host's own approval window elapsed with no answer;
1502
- * - `"policy"` — the deployment's OWN policy answered: a machine refused, not a person (a headless lane
1503
- * whose approver seat exists but can show nobody a prompt). Spelled `false`, such a deny told the model
1504
- * the USER did not want to proceed and to wait for their direction — a sentence about a person, in a run
1505
- * where none is coming; with the word it reads {@link policyRefusalMessage} instead, and the note travels
1506
- * as the policy's words. OMITTING the field still reads as `"human"`, so an unrevised host is unchanged.
1507
- * The two non-human words are legal ONLY with `allow: false` — neither an elapsed window nor a refusing
1508
- * policy can be what APPROVED an action, and neither has an allow-side settlement to become — so
1509
- * `{allow: true, settledBy: "timeout" | "policy"}` is refused fail-closed (the same narrowing the
1510
- * `PermissionResult` allow arm already spells in its type).
1522
+ * - `"policy"` — the deployment's OWN policy answered, either way: a machine decided, not a person (an
1523
+ * unattended lane answering by class, a headless lane whose approver seat exists but can show nobody a
1524
+ * prompt). Its allow settles `policy_allowed`, its refusal `policy_refused`. Spelled `true` / `false`, such
1525
+ * an answer filed a PERSON's record, and its refusal told the model the USER did not want to proceed and to
1526
+ * wait for their direction — a sentence about a person, in a run where none is coming; with the word the
1527
+ * refusal reads {@link policyRefusalMessage} instead, and the note travels as the policy's words. A
1528
+ * policy's allow is the same allow a person's is — the edit, the `readRootGrant` and every lane after the
1529
+ * gate treat it alike — and only the record names a different party, with ONE exception that is about the
1530
+ * party itself: an ask carrying {@link AskRequest.requiresRealApproval} demands a person, and a policy's
1531
+ * allow is refused there exactly as the blanket `"allow"` seat is (`blanket_allow_refused`). OMITTING the
1532
+ * field still reads as `"human"`, so an unrevised host is unchanged.
1533
+ * `"timeout"` is legal ONLY with `allow: false` — an elapsed window is a wait nobody answered, and it cannot be
1534
+ * what APPROVED an action — so `{allow: true, settledBy: "timeout"}` is refused fail-closed.
1511
1535
  * `"aborted"` is deliberately NOT accepted here — that word names the engine's OWN fail-closed ends
1512
1536
  * (abort, throw, unavailable, out-of-contract value), each already stamped at its own arm, and a
1513
1537
  * self-reported one would let a host relabel its refusal as an engine failure.
@@ -1542,14 +1566,15 @@ export type OnAsk = "deny" | "allow" | ((req: AskRequest, signal?: AbortSignal)
1542
1566
  * the party that ended this wait. Core does not authenticate it, compare it, or read it back to decide
1543
1567
  * anything; it transcribes it onto the settlement observation next to `settledBy`, so an audit that can
1544
1568
  * already say "a person ended this wait" can also say which one, without core growing an identity
1545
- * surface it deliberately does not have. Read the two together: `settledBy:"timeout"` with an
1546
- * `approver` names the queue whose window elapsed, NOT someone who refused.
1569
+ * surface it deliberately does not have. Read the two together: `settledBy:"policy"` with an `approver` names
1570
+ * WHICH of the deployment's answering seats decided (a closed word the host owns, never a person).
1547
1571
  * Screened, not trusted (see {@link screenApproverAttribution}): a non-string, an over-long value or one
1548
1572
  * carrying control characters is a loud fail-closed refusal at {@link resolveAsk}, the same posture this
1549
1573
  * seam takes for every other out-of-contract value — and the same reason, since a defective attribution
1550
1574
  * that were quietly dropped would leave a settlement looking unattributed rather than misreported. It is
1551
- * carried on the arms where the CALLER settled something (its allow, its human/timeout deny) and never
1552
- * on an end the engine produced (an abort, a throw, an unclonable edit): nobody approved those.
1575
+ * carried on the DECIDER arms (a person's or a policy's allow or refusal) and never on an end the engine
1576
+ * produced (an abort, a throw, an unclonable edit): nobody approved those. A host's own elapsed window has
1577
+ * no attribution seat on its record (`approval_window_expired` names the host as a party, not a seat).
1553
1578
  *
1554
1579
  * `readRootGrant` — "yes, and add this directory": the card's {@link AskRequest.readRootCandidate} `dir`, sent back
1555
1580
  * byte for byte (`shell.read_boundary.grant_candidate_is_the_grant`). Beside `allow: true` on an ask carrying that
@@ -1708,8 +1733,8 @@ export declare function policyRefusalMessage(toolName: string, askMessage: strin
1708
1733
  export type ResolvedAsk = (Extract<PermissionResult, {
1709
1734
  action: "allow";
1710
1735
  }> & {
1711
- /** Present exactly when a PERSON approved (`human_allowed`, with the channel's attribution when it
1712
- * reported one); absent on the posture allows (a blanket `"allow"`, a reused grant) — nobody was
1736
+ /** Present exactly when a DECIDER approved (`human_allowed` / `policy_allowed`, with the channel's
1737
+ * attribution when it reported one); absent on the posture allows (a blanket `"allow"`) — nobody was
1713
1738
  * asked, so nothing was settled. */
1714
1739
  settlement?: Settlement;
1715
1740
  /** The EXACT data snapshot the approver was shown (the `structuredClone` of the presented args). On
@@ -1766,7 +1791,8 @@ export declare function carriesBidiControls(value: unknown, limits?: {
1766
1791
  * arm: the approver's own verdicts settle as a person's unless the outcome names another word on
1767
1792
  * {@link AskOutcome.settledBy} (its own window, its own policy), and the fail-closed ends around them
1768
1793
  * (abort, a throw, an "unavailable" report, an unclonable presentation, an out-of-contract value) settle
1769
- * as the engine's own nobody-ended family. The three POSTURE arms at the top — blanket `"allow"`, its
1794
+ * as the engine's own nobody-ended family; a policy's allow on a `requiresRealApproval` ask is refused as the
1795
+ * blanket seat's is ({@link unjudgedAllowRefused}). The three POSTURE arms at the top — blanket `"allow"`, its
1770
1796
  * `requiresRealApproval` refusal, and the headless auto-deny — name no person and no window on purpose:
1771
1797
  * nobody was asked, so there is no wait for anyone to have ended, and `decisionReason: "mode"` is already
1772
1798
  * the honest word for what produced them.
@@ -3,7 +3,9 @@ import { homedir } from "node:os";
3
3
  import { brandPolicyAskClass } from "./ask-class.js";
4
4
  import { decisionAskClassesOf, headlessDenialMessage } from "./ask-origin.js";
5
5
  import { ownReadRootCandidate, readRootGrantSeat } from "./read-root-grant-screen.js";
6
- import { decidersNote, settledByPerson } from "./gate-outcome.js";
6
+ import { decidersNote, settledByDecider } from "./gate-outcome.js";
7
+ import { unjudgedAllowRefused } from "./real-approval.js";
8
+ export { policyCannotAnswer, rowDemandsAPerson } from "./real-approval.js";
7
9
  import { sanitizeAutoModeArmingRecipe } from "./auto-mode-arming.js";
8
10
  import { join, normalize as normalizePath, sep } from "node:path";
9
11
  import { BASH_READONLY_DEFAULT_ALLOW, parseLeadingCommandName } from "../tools/fs/index.js";
@@ -13,7 +15,7 @@ import { boundInputHashOf } from "./canonical-json.js";
13
15
  import { delimitUntrusted, inlineUntrusted, REVIEWER_NOTE_MAX_BODY } from "./untrusted-text.js";
14
16
  import { describeRejectedValue } from "./tool-search-setting.js";
15
17
  import { thrownText } from "./thrown-value.js";
16
- import { nestedRecordsOf, receiveContract, receiveNestedRecords, refusedOutOfContract, screenContractValues } from "./decision-receipt.js";
18
+ import { isEngineRefusal, nestedRecordsOf, receiveContract, receiveNestedRecords, refusedOutOfContract, screenContractValues } from "./decision-receipt.js";
17
19
  export { isEngineRefusal } from "./decision-receipt.js";
18
20
  import { isNamespacedCoveringRuleName, namespacedRuleNameCovers, parsePermissionRule } from "./permission-rules.js";
19
21
  import { protocolOf } from "./protocol-table.js";
@@ -46,6 +48,7 @@ function hasEngineAttestation(o) {
46
48
  }
47
49
  export function carryEngineAttestations(from, to) {
48
50
  transferEngineAttestations(from, to);
51
+ return to;
49
52
  }
50
53
  export function carryPrecedingSettlement(preceding, to) {
51
54
  if (ENGINE_SETTLEMENTS.get(to) !== undefined)
@@ -93,7 +96,12 @@ export function askUnresolvableCauseOf(d, call) {
93
96
  return undefined;
94
97
  return v.cause;
95
98
  }
96
- const ENGINE_ATTESTATION_SEATS = [ENGINE_SETTLEMENTS, CLASSIFIER_DENY_CAUSES_SEAT, ASK_UNRESOLVABLE_CAUSES_SEAT, CLASSIFIER_ROUNDS_SEAT];
99
+ const FOLDED_CLASSES_SEAT = new WeakMap();
100
+ function foldedAskClassesAttested(d, held) {
101
+ const signed = FOLDED_CLASSES_SEAT.get(d);
102
+ return signed !== undefined && Array.isArray(held) && signed.length === held.length && signed.every((c, i) => c === held[i]);
103
+ }
104
+ const ENGINE_ATTESTATION_SEATS = [ENGINE_SETTLEMENTS, CLASSIFIER_DENY_CAUSES_SEAT, ASK_UNRESOLVABLE_CAUSES_SEAT, CLASSIFIER_ROUNDS_SEAT, FOLDED_CLASSES_SEAT];
97
105
  export function engineSettlementOf(d, call) {
98
106
  if (typeof d !== "object" || d === null)
99
107
  return undefined;
@@ -265,7 +273,7 @@ export function refuseOutOfContractDecision(d, opts) {
265
273
  if (typeof d !== "object" || d === null) {
266
274
  return refusedOutOfContract(`a permission decision's "action" is absent (the decision is ${d === null || d === undefined ? String(d) : `a ${typeof d}`}, not an object) — the closed set is allow/deny/ask; denied fail-closed rather than reading a word outside it as an allow`);
267
275
  }
268
- if ((hookSeam ? HOOK_RECEIPTS : ENGINE_RECEIPTS).has(d))
276
+ if ((hookSeam ? HOOK_RECEIPTS : ENGINE_RECEIPTS).has(d) || isEngineRefusal(d))
269
277
  return d;
270
278
  const decision = receiveContract(d, hookSeam ? HOOK_RESULT_MEMBERS : DECISION_MEMBERS, "");
271
279
  if (decision.kind === "refused")
@@ -276,6 +284,11 @@ export function refuseOutOfContractDecision(d, opts) {
276
284
  const refused = receiveNestedRecords(contract) ?? screenContractValues(contract, hookSeam ? { skip: HOOK_SEAM_UNREAD, optional: HOOK_SEAM_OPTIONAL } : undefined);
277
285
  if (refused !== undefined)
278
286
  return refused;
287
+ if (contract.foldedAskClasses !== undefined && !foldedAskClassesAttested(d, contract.foldedAskClasses)) {
288
+ return refusedOutOfContract(`a permission decision carries "foldedAskClasses" that no policy fold of the engine wrote on this object — the member is ` +
289
+ `engine-written (combinePolicies records the classes of the asks it collapsed); denied fail-closed rather than letting a ` +
290
+ `class no station judged reach the class derivation`);
291
+ }
279
292
  if (hookSeam && contract.additionalContext !== undefined && typeof contract.additionalContext !== "string") {
280
293
  return refusedOutOfContract(`a PreToolUse hook result's "additionalContext" is not a string — the member is the text appended for the model; denied fail-closed rather than rendering a value this layer cannot classify`);
281
294
  }
@@ -298,6 +311,9 @@ export function refuseOutOfContractDecision(d, opts) {
298
311
  (hookSeam ? HOOK_RECEIPTS : ENGINE_RECEIPTS).add(receipt);
299
312
  return receipt;
300
313
  }
314
+ export function receiveHookResult(result) {
315
+ return refuseOutOfContractDecision(result, { reasonIsNonInput: true });
316
+ }
301
317
  const DEADLINE_ELAPSED = Symbol("approval.deadline_elapsed");
302
318
  function withTimeout(p, ms, onTimeout) {
303
319
  if (ms === undefined) {
@@ -474,7 +490,7 @@ export function createApprovalPolicy(opts) {
474
490
  }
475
491
  const okRaw = ok;
476
492
  if (okRaw === true)
477
- return attest({ action: "allow" }, settledByPerson("human_allowed", undefined));
493
+ return attest({ action: "allow" }, settledByDecider("person", "allowed", undefined));
478
494
  if (okRaw !== false) {
479
495
  return attest({
480
496
  action: "deny",
@@ -482,7 +498,7 @@ export function createApprovalPolicy(opts) {
482
498
  `(this policy's \`approve\` returns true, false, or the {allow, settledBy?, reason?, approver?} outcome object)`,
483
499
  }, settledByNobody("approver_contract"));
484
500
  }
485
- return attest({ action: "deny", message: `approval denied for "${req.toolName}"` }, settledByPerson("human_refused", undefined));
501
+ return attest({ action: "deny", message: `approval denied for "${req.toolName}"` }, settledByDecider("person", "refused", undefined));
486
502
  }
487
503
  if (opts.denyByDefault && !nameListHolds(auto, req) && !namespacedCoveringHit(autoCovering, req)) {
488
504
  return { action: "deny", message: `tool "${req.toolName}" requires explicit allow` };
@@ -584,7 +600,10 @@ export function combinePolicies(...policies) {
584
600
  for (const k of decisionAskClassesOf(c))
585
601
  folded.add(k);
586
602
  const withClasses = folded.size > 0 ? { ...withAuthor, foldedAskClasses: [...folded] } : withAuthor;
587
- return merged !== undefined ? { ...withClasses, updatedInput: merged } : withClasses;
603
+ const out = merged !== undefined ? { ...withClasses, updatedInput: merged } : withClasses;
604
+ if (folded.size > 0)
605
+ FOLDED_CLASSES_SEAT.set(out, Object.freeze([...folded]));
606
+ return out;
588
607
  }
589
608
  const allowed = rewrite ?? ALLOW;
590
609
  if (!anyRecord)
@@ -1251,11 +1270,7 @@ function readApproverOutcomeObject(ok, toolName, editLane) {
1251
1270
  if (editLane === "none" && grant !== undefined) {
1252
1271
  return refused(`the approver for "${toolName}" attached a readRootGrant on a seat that offers no directory — denied fail-closed (a grant answers an approval card's readRootCandidate)`, settledByNobody("approver_contract"));
1253
1272
  }
1254
- if (settledWord !== "human" && allowed === true) {
1255
- return refused(`the approver for "${toolName}" returned an allow settled by "${settledWord}" — only a person approves, and that word ` +
1256
- `names a wait that ended with nobody judging: it cannot be what approved a call; denied fail-closed ` +
1257
- `(report "${settledWord}" with allow:false, or allow with settledBy "human"/omitted)`, settledByNobody("approver_contract"));
1258
- }
1273
+ const decider = settledWord === "policy" ? "policy" : settledWord === "human" ? "person" : undefined;
1259
1274
  if (allowed !== true) {
1260
1275
  let reasonText;
1261
1276
  try {
@@ -1275,15 +1290,16 @@ function readApproverOutcomeObject(ok, toolName, editLane) {
1275
1290
  word: settledWord,
1276
1291
  note,
1277
1292
  grant,
1278
- settlement: settledWord === "timeout"
1279
- ? { kind: "approval_window_expired", who: { party: "host" }, when: Date.now() }
1280
- : settledWord === "policy"
1281
- ? settledByNobody("policy_refused")
1282
- : settledByPerson("human_refused", attribution.approver, note),
1293
+ settlement: decider === undefined ? { kind: "approval_window_expired", who: { party: "host" }, when: Date.now() } : settledByDecider(decider, "refused", attribution.approver, note),
1283
1294
  };
1284
1295
  }
1296
+ if (decider === undefined) {
1297
+ return refused(`the approver for "${toolName}" returned an allow settled by "${settledWord}" — that word names a wait that ended ` +
1298
+ `with nobody answering: it cannot be what approved a call; denied fail-closed ` +
1299
+ `(report "${settledWord}" with allow:false; a person's allow is settledBy "human"/omitted, the deployment policy's is "policy")`, settledByNobody("approver_contract"));
1300
+ }
1285
1301
  if (suppliedEdit === undefined)
1286
- return { kind: "allow", edit: undefined, settlement: settledByPerson("human_allowed", attribution.approver), grant };
1302
+ return { kind: "allow", edit: undefined, settlement: settledByDecider(decider, "allowed", attribution.approver), grant };
1287
1303
  if (editLane === "none") {
1288
1304
  return refused(`the approver for "${toolName}" attached an edit (updatedInput) on a seat that has no lane to adjudicate one — ` +
1289
1305
  `this seat's answer is the deployment POLICY's verdict, and an edit arriving through it would execute without the ` +
@@ -1294,20 +1310,12 @@ function readApproverOutcomeObject(ok, toolName, editLane) {
1294
1310
  if (!edit.ok) {
1295
1311
  return refused(`the approved edit for "${toolName}" is not safely clonable (${edit.reason}) — denied fail-closed`, settledByNobody("presentation_failed"));
1296
1312
  }
1297
- return { kind: "allow", edit: { value: edit.value }, settlement: settledByPerson("human_allowed", attribution.approver), grant };
1313
+ return { kind: "allow", edit: { value: edit.value }, settlement: settledByDecider(decider, "allowed", attribution.approver), grant };
1298
1314
  }
1299
1315
  async function resolveAskArms(req, onAsk, signal, onLateSettlement) {
1300
1316
  if (onAsk === "allow") {
1301
- if (req.requiresRealApproval === true) {
1302
- return {
1303
- action: "deny",
1304
- message: `approval required for "${req.toolName}" but the wired approver is a blanket "allow" (no ` +
1305
- `judgment was applied) — this decision requires an actual auto-mode classifier verdict or a ` +
1306
- `real approval callback, neither of which a blanket bypass can provide: ${req.message}`,
1307
- decisionReason: "mode",
1308
- settlement: settledByNobody("blanket_allow_refused"),
1309
- };
1310
- }
1317
+ if (req.requiresRealApproval === true)
1318
+ return unjudgedAllowRefused(req, "blanket");
1311
1319
  return { action: "allow", decisionReason: "mode" };
1312
1320
  }
1313
1321
  if (onAsk === undefined || onAsk === "deny") {
@@ -1396,6 +1404,9 @@ async function resolveAskArms(req, onAsk, signal, onLateSettlement) {
1396
1404
  const reading = readApproverOutcomeObject(ok, req.toolName, "readjudicated");
1397
1405
  if (reading.kind === "refused")
1398
1406
  return { action: "deny", message: reading.message, decisionReason: "mode", settlement: reading.settlement };
1407
+ if (reading.kind === "allow" && reading.settlement.who.party === "policy" && req.requiresRealApproval === true) {
1408
+ return { ...unjudgedAllowRefused(req, "policy"), ...readRootGrantSeat(reading.grant, offered, false) };
1409
+ }
1399
1410
  const grantSeat = readRootGrantSeat(reading.grant, offered, reading.kind === "allow");
1400
1411
  if (reading.kind === "deny") {
1401
1412
  return {
@@ -1416,7 +1427,7 @@ async function resolveAskArms(req, onAsk, signal, onLateSettlement) {
1416
1427
  }
1417
1428
  const okRaw = ok;
1418
1429
  if (okRaw === true)
1419
- return { action: "allow", decisionReason: "mode", presentedInput: presented.value, settlement: settledByPerson("human_allowed", undefined) };
1430
+ return { action: "allow", decisionReason: "mode", presentedInput: presented.value, settlement: settledByDecider("person", "allowed", undefined) };
1420
1431
  if (okRaw !== false) {
1421
1432
  return {
1422
1433
  action: "deny",
@@ -1425,5 +1436,5 @@ async function resolveAskArms(req, onAsk, signal, onLateSettlement) {
1425
1436
  settlement: settledByNobody("approver_contract"),
1426
1437
  };
1427
1438
  }
1428
- return { action: "deny", message: humanRefusalMessage(req), decisionReason: "mode", settlement: settledByPerson("human_refused", undefined) };
1439
+ return { action: "deny", message: humanRefusalMessage(req), decisionReason: "mode", settlement: settledByDecider("person", "refused", undefined) };
1429
1440
  }
@@ -30,5 +30,5 @@ export type { BackgroundChildEvent, DelegationLifecycleEvent } from "./delegatio
30
30
  export { __resetMalformedDelegationSeatAnnouncement, deliverDelegationLifecycle } from "./delegation-frames.js";
31
31
  export type { ProjectMemoryLoad, ResumePreflightInfo, ResumePreflightVerdict, RunnerDeps } from "./runner-deps.js";
32
32
  export type { EngineNotice, OwedUserInputAccount, UserSteerUndrainedCause } from "./engine-notice.js";
33
- export { __resetMalformedNoticeSeatAnnouncement, deliverEngineNotice, owedUserInputCounts, settleOwedUserInput, undrainedUserInputNotices, USER_STEER_UNDRAINED_CAUSES } from "./engine-notice.js";
33
+ export { __resetMalformedNoticeSeatAnnouncement, deliverEngineNotice, owedUserInputCounts, parentLegIds, settleOwedUserInput, undrainedUserInputNotices, USER_STEER_UNDRAINED_CAUSES } from "./engine-notice.js";
34
34
  export type { AgentTool, Model, StreamFn, ThinkingLevel, ToolInputValidationContext, ToolInputVerdict };
@@ -1,4 +1,4 @@
1
1
  export { MODEL_ROLES } from "./model-seat.js";
2
2
  export { AUTO_MODE_DENY_SOURCES, isAutoModeDenySource } from "./runtime-caps.js";
3
3
  export { __resetMalformedDelegationSeatAnnouncement, deliverDelegationLifecycle } from "./delegation-frames.js";
4
- export { __resetMalformedNoticeSeatAnnouncement, deliverEngineNotice, owedUserInputCounts, settleOwedUserInput, undrainedUserInputNotices, USER_STEER_UNDRAINED_CAUSES } from "./engine-notice.js";
4
+ export { __resetMalformedNoticeSeatAnnouncement, deliverEngineNotice, owedUserInputCounts, parentLegIds, settleOwedUserInput, undrainedUserInputNotices, USER_STEER_UNDRAINED_CAUSES } from "./engine-notice.js";
@@ -20,6 +20,7 @@
20
20
  * copies of the derivation would drift, so there is exactly one. A startup snapshot and a leg's
21
21
  * effective snapshot are two DIFFERENT facts; neither may impersonate the other.
22
22
  */
23
+ import { type PermissionMode } from "./ask-origin.js";
23
24
  import type { RunnerDeps, TaskSpec } from "./types.js";
24
25
  import { type StoreDurability } from "./checkpoint-store.js";
25
26
  import type { WiringManifestHookEntry } from "./hook-seats.js";
@@ -600,8 +601,13 @@ export declare function resolveSubagentTranscriptTier(agentStoreWired: boolean,
600
601
  } | undefined): SubagentTranscriptTier;
601
602
  /**
602
603
  * The ONE `ask.effective` derivation (design/173) — shared by the effective manifest AND
603
- * the posture door (`config.interaction_posture`), so "what does the door require" and "what does
604
- * the manifest report" can never disagree:
604
+ * the posture door (`config.interaction_posture`): the door passes no `mode` and judges the SEAT alone, the
605
+ * manifest passes its leg's effective mode AT THE LEG'S START (the manifest is minted once; a runtime reader pairs
606
+ * {@link askSeatEffective} with the live mode instead), so the two read one table and differ only by the one fact the
607
+ * door does not have:
608
+ * - MODE BEFORE SEAT: under `dontAsk` the mode refuses every ask a policy raises (`arbitrateByMode`; only a
609
+ * hook's own ask beside no rule or safety ask is handed on, and that is the hook lane's decision, not a policy
610
+ * `ask`), so no seat answers one — whatever the seat and the park lane say, the word is `auto_deny`;
605
611
  * - a live approver callback ⇒ `human_reachable` (regardless of the park lane);
606
612
  * - blanket `"allow"`/`"deny"` ⇒ the park lane decides first: a string seat is NOT a live approver,
607
613
  * so on an armed park lane the ask parks and the honest report is `park_only` — reporting the
@@ -610,7 +616,15 @@ export declare function resolveSubagentTranscriptTier(agentStoreWired: boolean,
610
616
  * - no seat at all ⇒ the park lane decides: armed ⇒ `park_only`, unresolved ⇒ `unresolved`,
611
617
  * else the headless degrade `auto_deny`.
612
618
  */
613
- export declare function deriveAskEffective(form: AskSeamForm, parkEffective: boolean | "unresolved"): AskEffective;
619
+ export declare function deriveAskEffective(form: AskSeamForm, parkEffective: boolean | "unresolved", mode?: PermissionMode): AskEffective;
620
+ /**
621
+ * The SEAT's own `ask.effective` of an effective manifest — {@link deriveAskEffective} with no mode, from the manifest's own
622
+ * form and park facts. The manifest's `ask.effective` is the word AS OF THE LEG'S START under its start mode, and a steer moves
623
+ * the mode afterwards; a runtime consumer that reads the word per call (the classifier's approval seat, the posture row, the peer
624
+ * lane's mode fold) therefore reads THIS word and the leg's LIVE mode beside it — never the start-of-leg fold, which would carry a
625
+ * `dontAsk` start's refusal into the leg's later modes.
626
+ */
627
+ export declare function askSeatEffective(manifest: Pick<WiringManifest, "ask" | "parkLane">): AskEffective;
614
628
  /**
615
629
  * Does an ask this leg hands on carrying `requiresRealApproval` reach a PERSON — never an automatic yes? The leg's
616
630
  * APPROVAL SEAT, read off the ONE ask derivation above, never re-derived: `human_reachable` (a live approver) and