@sema-agent/core 7.17.7 → 7.18.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 (163) hide show
  1. package/CHANGELOG.md +140 -13
  2. package/dist/agents/send-message-tool.js +1 -0
  3. package/dist/agents/subagent.d.ts +1 -0
  4. package/dist/agents/subagent.js +5 -0
  5. package/dist/agents/teacher.js +1 -0
  6. package/dist/agents/verify.js +3 -0
  7. package/dist/brain/route-adjudicator.d.ts +18 -0
  8. package/dist/brain/route-adjudicator.js +16 -0
  9. package/dist/core/agent-definition.d.ts +10 -0
  10. package/dist/core/artifact-host.d.ts +41 -4
  11. package/dist/core/artifact-host.js +18 -2
  12. package/dist/core/auto-mode.d.ts +141 -45
  13. package/dist/core/auto-mode.js +109 -3
  14. package/dist/core/checkpoint-store.d.ts +16 -2
  15. package/dist/core/engine-notice.d.ts +47 -1
  16. package/dist/core/engine-notice.js +7 -0
  17. package/dist/core/gate-lanes.js +2 -1
  18. package/dist/core/gate-outcome.d.ts +17 -2
  19. package/dist/core/gate-outcome.js +16 -0
  20. package/dist/core/governance-codes.d.ts +1 -1
  21. package/dist/core/governance-codes.js +8 -0
  22. package/dist/core/hook-seats.d.ts +66 -0
  23. package/dist/core/hook-seats.js +37 -0
  24. package/dist/core/runner/advertised-writable-dirs.d.ts +8 -3
  25. package/dist/core/runner/assemble-result.d.ts +2 -1
  26. package/dist/core/runner/assemble-result.js +1 -1
  27. package/dist/core/runner/classifier-ladder.d.ts +89 -0
  28. package/dist/core/runner/classifier-ladder.js +65 -0
  29. package/dist/core/runner/contracts.d.ts +69 -13
  30. package/dist/core/runner/decide-continuation.d.ts +1 -3
  31. package/dist/core/runner/decide-continuation.js +1 -7
  32. package/dist/core/runner/deferral-census.d.ts +52 -0
  33. package/dist/core/runner/deferral-census.js +44 -0
  34. package/dist/core/runner/derived-route-fallback.d.ts +8 -4
  35. package/dist/core/runner/derived-route-fallback.js +8 -3
  36. package/dist/core/runner/gate-exit.d.ts +12 -0
  37. package/dist/core/runner/gate-exit.js +6 -3
  38. package/dist/core/runner/prepare-artifact.d.ts +10 -2
  39. package/dist/core/runner/prepare-artifact.js +5 -4
  40. package/dist/core/runner/prepare-caps-and-workflow.d.ts +4 -2
  41. package/dist/core/runner/prepare-caps-and-workflow.js +30 -31
  42. package/dist/core/runner/prepare-config-doors.d.ts +1 -1
  43. package/dist/core/runner/prepare-config-doors.js +5 -3
  44. package/dist/core/runner/prepare-defer-classify.d.ts +13 -19
  45. package/dist/core/runner/prepare-defer-classify.js +8 -3
  46. package/dist/core/runner/prepare-gate-stations.d.ts +26 -0
  47. package/dist/core/runner/prepare-gate-stations.js +1 -1
  48. package/dist/core/runner/prepare-hands-readface.d.ts +24 -7
  49. package/dist/core/runner/prepare-hands-readface.js +60 -24
  50. package/dist/core/runner/prepare-inherited-gate.d.ts +17 -4
  51. package/dist/core/runner/prepare-inherited-gate.js +10 -2
  52. package/dist/core/runner/prepare-lsp.d.ts +2 -2
  53. package/dist/core/runner/prepare-lsp.js +1 -1
  54. package/dist/core/runner/prepare-policy-chain.js +2 -0
  55. package/dist/core/runner/prepare-project-context.js +4 -2
  56. package/dist/core/runner/prepare-prompt-inputs.d.ts +4 -2
  57. package/dist/core/runner/prepare-prompt-inputs.js +4 -1
  58. package/dist/core/runner/prepare-run-refs.d.ts +9 -1
  59. package/dist/core/runner/prepare-run-refs.js +3 -2
  60. package/dist/core/runner/prepare-suspend-saga.d.ts +5 -0
  61. package/dist/core/runner/prepare-suspend-saga.js +5 -2
  62. package/dist/core/runner/prepare-task.js +18 -13
  63. package/dist/core/runner/prepare-tool-disclosure-mount.d.ts +5 -0
  64. package/dist/core/runner/prepare-tool-disclosure-mount.js +10 -2
  65. package/dist/core/runner/prepare-turn-wiring.d.ts +8 -4
  66. package/dist/core/runner/prepare-turn-wiring.js +3 -2
  67. package/dist/core/runner/prepare-wiring-manifest.d.ts +5 -5
  68. package/dist/core/runner/prepare-wiring-manifest.js +5 -2
  69. package/dist/core/runner/resume-checkpoint-screen.js +7 -0
  70. package/dist/core/runner/run-harness-handlers.js +4 -1
  71. package/dist/core/runner/run-identity-wiring.js +6 -2
  72. package/dist/core/runner/run-leg.d.ts +2 -0
  73. package/dist/core/runner/run-leg.js +35 -41
  74. package/dist/core/runner/run-notification-lane.js +55 -27
  75. package/dist/core/runner/run-settle-and-teardown.js +1 -0
  76. package/dist/core/runner/run-terminal-adoption.js +16 -13
  77. package/dist/core/runner/run-turn-boundary.js +3 -1
  78. package/dist/core/runner/runtask.d.ts +6 -5
  79. package/dist/core/runner/runtask.js +26 -14
  80. package/dist/core/runner/skill-base-dir.d.ts +54 -0
  81. package/dist/core/runner/skill-base-dir.js +42 -0
  82. package/dist/core/runner/synthetic-tools.d.ts +16 -19
  83. package/dist/core/runner/synthetic-tools.js +18 -10
  84. package/dist/core/runner/tool-defer-gate.d.ts +22 -9
  85. package/dist/core/runner/tool-defer-gate.js +15 -5
  86. package/dist/core/runner/tool-disclosure.d.ts +5 -0
  87. package/dist/core/runner/tool-disclosure.js +2 -0
  88. package/dist/core/runner/tool-progress-lane.d.ts +42 -0
  89. package/dist/core/runner/tool-progress-lane.js +61 -0
  90. package/dist/core/runner-deps.d.ts +48 -7
  91. package/dist/core/swappable-deps.d.ts +9 -7
  92. package/dist/core/swappable-deps.js +7 -3
  93. package/dist/core/task-event.d.ts +96 -29
  94. package/dist/core/task-notification.d.ts +142 -16
  95. package/dist/core/task-notification.js +106 -0
  96. package/dist/core/task-result.d.ts +9 -1
  97. package/dist/core/task-spec.d.ts +20 -3
  98. package/dist/core/tighten-task-spec.js +7 -5
  99. package/dist/core/tool-model-gate.d.ts +1 -1
  100. package/dist/core/tool-search-setting.d.ts +78 -0
  101. package/dist/core/tool-search-setting.js +90 -0
  102. package/dist/core/tool-spec.d.ts +8 -0
  103. package/dist/core/types.d.ts +1 -1
  104. package/dist/core/types.js +1 -1
  105. package/dist/core/wiring-manifest.d.ts +46 -0
  106. package/dist/core/wiring-manifest.js +9 -1
  107. package/dist/engine/execution-env/node-execution-env.js +10 -4
  108. package/dist/engine/harness/agent-harness.d.ts +42 -0
  109. package/dist/engine/harness/agent-harness.js +61 -29
  110. package/dist/engine/loop/agent-loop.d.ts +68 -3
  111. package/dist/engine/loop/agent-loop.js +109 -16
  112. package/dist/index.d.ts +10 -5
  113. package/dist/index.js +9 -5
  114. package/dist/internal/harness.d.ts +1 -1
  115. package/dist/orchestration/run-workflow-tool.d.ts +3 -0
  116. package/dist/orchestration/run-workflow-tool.js +6 -1
  117. package/dist/orchestration/workflow-governance.d.ts +7 -1
  118. package/dist/orchestration/workflow-governance.js +9 -5
  119. package/dist/orchestration/workflow-primitives.d.ts +3 -0
  120. package/dist/orchestration/workflow-primitives.js +6 -1
  121. package/dist/orchestration/workflow-types.d.ts +48 -13
  122. package/dist/orchestration/workflow-types.js +1 -1
  123. package/dist/orchestration/workflow.js +45 -17
  124. package/dist/prompt-assembly/epoch.js +2 -0
  125. package/dist/prompt-assembly/packs/sema-default.js +2 -2
  126. package/dist/prompt-assembly/section-usage.d.ts +26 -0
  127. package/dist/prompt-assembly/section-usage.js +10 -0
  128. package/dist/prompt-assembly/types.d.ts +8 -6
  129. package/dist/prompts/default.d.ts +13 -0
  130. package/dist/prompts/default.js +16 -8
  131. package/dist/tools/artifact/artifact-text.d.ts +10 -1
  132. package/dist/tools/artifact/artifact-text.js +6 -3
  133. package/dist/tools/artifact/artifact-tool.d.ts +12 -2
  134. package/dist/tools/artifact/artifact-tool.js +203 -70
  135. package/dist/tools/artifact/conformance.d.ts +56 -0
  136. package/dist/tools/artifact/conformance.js +349 -0
  137. package/dist/tools/artifact/local-stub.js +1 -1
  138. package/dist/tools/artifact/reject-breaker.d.ts +28 -0
  139. package/dist/tools/artifact/reject-breaker.js +42 -0
  140. package/dist/tools/fs/bash-lexer.d.ts +132 -0
  141. package/dist/tools/fs/bash-lexer.js +563 -0
  142. package/dist/tools/fs/bash-program-position.d.ts +142 -0
  143. package/dist/tools/fs/bash-program-position.js +125 -0
  144. package/dist/tools/fs/bash-readonly-classifier.d.ts +54 -16
  145. package/dist/tools/fs/bash-readonly-classifier.js +121 -552
  146. package/dist/tools/fs/containment-face.d.ts +89 -0
  147. package/dist/tools/fs/containment-face.js +81 -0
  148. package/dist/tools/fs/fs-bash.d.ts +46 -1
  149. package/dist/tools/fs/fs-bash.js +158 -22
  150. package/dist/tools/fs/fs-read.d.ts +1 -1
  151. package/dist/tools/fs/fs-search-tools.d.ts +3 -3
  152. package/dist/tools/fs/fs-write.d.ts +4 -3
  153. package/dist/tools/fs/fs-write.js +9 -9
  154. package/dist/tools/fs/index.d.ts +24 -5
  155. package/dist/tools/fs/index.js +18 -6
  156. package/dist/tools/fs/repo-map.d.ts +2 -2
  157. package/dist/tools/fs/safety.d.ts +3 -3
  158. package/dist/tools/fs/safety.js +4 -4
  159. package/package.json +1 -1
  160. package/test/export-surface.snapshot.json +85 -9
  161. package/wire-changes/core@7.17.7.json +49 -0
  162. package/dist/tools/fs/read-face.d.ts +0 -68
  163. package/dist/tools/fs/read-face.js +0 -47
@@ -20,7 +20,7 @@ import type { ActiveWorktreeSession, AgentHarness, AgentMessage, AgentTool, Anno
20
20
  import type { Model } from "../../internal/llm.js";
21
21
  import type { WorkflowSizeGuideline } from "../../orchestration/workflow-size-guideline.js";
22
22
  import type { ToolManifestRow } from "../../prompt-assembly/tool-catalog.js";
23
- import type { CwdRef, ReadFace } from "../../tools/fs/index.js";
23
+ import type { CwdRef, ContainmentFace } from "../../tools/fs/index.js";
24
24
  import type { MaterializedA2a } from "../a2a.js";
25
25
  import type { CompactionForkContext, CompactionPhaseDurations, MaybeCompactOptions, RapidRefillState } from "../auto-compaction.js";
26
26
  import type { AutoModeArmingRecipe } from "../auto-mode-arming.js";
@@ -43,7 +43,7 @@ import type { SystemInjectionPriority, TaskNotificationPayload } from "../task-n
43
43
  import type { OnAsk, PermissionResult, ResolvedAsk, ToolCallRequest, ToolPolicy } from "../tool-policy.js";
44
44
  import type { RemoteExecutionEnv, SnapshotId, WorkspaceHandle } from "../remote-env.js";
45
45
  import type { ToolDisclosureManifest } from "../trace.js";
46
- import type { AgentDefinition, Brain, CompactOutcome, ModelRoles, NestedUsageAccum, RunnerDeps, ShellGateDoctrine, TaskEvent, TaskResult, TaskSpec, TaskStream, ToolActivity, ToolEffect } from "../types.js";
46
+ import type { AgentDefinition, Brain, CompactOutcome, ModelRoles, NestedUsageAccum, RunnerDeps, ShellGateDoctrine, TaskEvent, TaskEventIdentity, TaskResult, TaskSpec, TaskStream, ToolActivity, ToolEffect } from "../types.js";
47
47
  import type { WiringManifest } from "../wiring-manifest.js";
48
48
  import type { ActiveSkillFrame } from "./active-skill-scope.js";
49
49
  import type { BudgetAxis } from "./assemble-result.js";
@@ -157,6 +157,16 @@ export interface FileHistoryBoundarySeat {
157
157
  * may no longer match), and the id of the tool call the park is holding (only a `tool_approval` pending
158
158
  * action parks ON a call; absent, never guessed, for the kinds that hold none).
159
159
  */
160
+ /** See {@link Prepared.undeliveredContinuationRef} — `parkedSteers` is in ISSUE order, and the wake message rides it with no `seq` (no store minted one). */
161
+ export interface UndeliveredContinuation {
162
+ parkedSteers: Array<Omit<PendingSteerEntry, "seq"> & {
163
+ seq?: number;
164
+ }>;
165
+ orphanTasks: Array<{
166
+ id: string;
167
+ description?: string;
168
+ }>;
169
+ }
160
170
  export interface CommittedPause {
161
171
  cause: import("../types.js").PausedCause;
162
172
  scope: string;
@@ -278,7 +288,9 @@ export interface Prepared {
278
288
  * block's single resolution, or the hands-less legs' resolver run (live spec-time facts, with the
279
289
  * checkpoint seed folded stricter-wins where one exists). Every leg that completes prepare has a
280
290
  * read posture now — this is what its delegation subtree is clamped by even where no faces mount. */
281
- effectiveReadFace?: ReadFace;
291
+ effectiveReadFace?: ContainmentFace;
292
+ /** owned — the same observation on the WRITE seat (`TaskResult.effectiveWriteFace`). */
293
+ effectiveWriteFace?: ContainmentFace;
282
294
  /** The normalized deny ADDITIONS in force (deployment ∪ task ∪ checkpoint seed; built-ins
283
295
  * excluded), echoed on `TaskResult.effectiveReadDenyPatterns`. Present iff non-empty; a defensive
284
296
  * copy (the wide-scope working array stays the engine's own). */
@@ -291,6 +303,13 @@ export interface Prepared {
291
303
  /** The per-task logical cwd ref when a real shell is mounted (else undefined). The Runner
292
304
  * reads `cwdRef.current` after each tool to detect a `cd` move and emit `workspace_changed`. */
293
305
  cwdRef?: CwdRef;
306
+ /** The run's TOOL-PROGRESS lane seat, late-bound for the same reason `harnessRef` is (the tool band is
307
+ * built during prepare; the lane needs `pushContent` and `ident`, which exist only after it returns).
308
+ * The driver binds `current` right after `pushContent` exists. Always present on a completed prepare —
309
+ * the CELL, not the lane: unbound means "no run body has claimed this band", and emissions no-op. */
310
+ toolProgressRef: {
311
+ current?: ToolProgressLane;
312
+ };
294
313
  /** The run's advertised-writable-directory seat (present with hands): `roots` is what the prompt advertised as writable and the fence admitted. Read live wherever a policy is adjudicated off a resumed leg, so the write gate's exemption domain is that same list. */
295
314
  advertisedWritableDirs?: Pick<import("./advertised-writable-dirs.js").AdvertisedWritableDirs, "roots">;
296
315
  /** The ACTIVE EnterWorktree session ref (mounted with the worktree tools; undefined without
@@ -462,6 +481,7 @@ export interface Prepared {
462
481
  * "undiscovered" census the tool_search_usage_reminder lane enumerates. Frozen at prepare time: a
463
482
  * name never leaves the deferred set, it only moves into `activeTools`. */
464
483
  deferredToolNames?: ReadonlySet<string>;
484
+ /** The deferral census seat, present iff {@link deferredToolNames} is. */ toolDisclosure?: import("./deferral-census.js").ToolDisclosureSeat;
465
485
  /** The RESOLVED materialization strategy this run runs under (`true` = "static":
466
486
  * activation never swaps the placeholder; the real schema's only in-context carrier is the
467
487
  * ToolSearch result text). Consumed by the tools_delta boundary renderer, whose "full schemas are
@@ -587,6 +607,13 @@ export interface Prepared {
587
607
  pausedRef: {
588
608
  current?: CommittedPause;
589
609
  };
610
+ /** A resumed leg's continuation content that has NOT reached the transcript yet: EVERY input frame the
611
+ * prompt carries (parked steers, the caller's wake message) and the orphaned background tasks whose obituary
612
+ * rides it. Set before the prompt, emptied at delivery; a re-park carries it forward (the saga folds the
613
+ * orphans in, the adoption re-parks the frames), and what no re-park carries the undrained account announces. */
614
+ undeliveredContinuationRef: {
615
+ current: UndeliveredContinuation;
616
+ };
590
617
  /** Ruled 2026-08-05 (matrix ruling arm A): set true by the resume engine when this leg EXECUTES the
591
618
  * approved pending call — the restart-loop cap then counts from a fresh base (consecutive
592
619
  * no-progress suspends only). See `suspendChainBase`. */
@@ -1345,6 +1372,7 @@ export interface PromptFeatureFlags {
1345
1372
  isolationEnabled: boolean;
1346
1373
  reminderMark: string;
1347
1374
  readFaceOpen: boolean;
1375
+ writeFaceOpen: boolean;
1348
1376
  orchestrationEnabled: boolean;
1349
1377
  orchestrationDeferred: boolean;
1350
1378
  promptProfile: "simple" | "classic";
@@ -1782,10 +1810,6 @@ export interface RunInternals {
1782
1810
  onStatusEvent?: (event: Extract<TaskEvent, {
1783
1811
  type: "status";
1784
1812
  }>) => void;
1785
- /**
1786
- * Trusted run-local system-injection sink. `Runner.runLocked` wires this to the
1787
- * live TaskStream queue plus the current harness follow-up lane; it is not a public TaskSpec field.
1788
- */
1789
1813
  /** The run-local per-tool-call detach hub. runtask creates it and exposes
1790
1814
  * `TaskStream.detach(toolCallId)`; the hands Bash tool threads `signalFor(toolCallId)` into env.exec. */
1791
1815
  detachHub?: import("../tool-detach.js").ToolDetachHub;
@@ -1797,6 +1821,11 @@ export interface RunInternals {
1797
1821
  * channel, same posture as every other field here.
1798
1822
  */
1799
1823
  sessionReadStates?: SessionReadFileStates;
1824
+ /** Trusted system-injection sink (`Runner.runLocked` wires it to the live TaskStream queue plus the
1825
+ * current harness lanes; never a public TaskSpec field), ADDRESSED TO THE SESSION rather than to this
1826
+ * run: a producer wired here (a background shell, a watch, a delegation settle) keeps the closure long
1827
+ * after the run that mounted it ended, and its frame is then routed to whatever run the SESSION has
1828
+ * live — at that run's next turn boundary, the seat a steer lands on; only a session with NO live run parks it. */
1800
1829
  onTaskNotification?: (notification: TaskNotificationPayload,
1801
1830
  /** Injection tier (the ladder is LIVE): "next" = the running turn's next boundary
1802
1831
  * (arrival order, consecutive frames batch); "later" = the run's would-otherwise-stop seat
@@ -1812,9 +1841,9 @@ export interface RunInternals {
1812
1841
  * `injectTaskNotification`), threaded into a CHILD's internals at spawn time so the child's
1813
1842
  * SendMessage("main") uplink lands in the parent's queue at a turn boundary (the CC
1814
1843
  * "delivered automatically" parent half). DELIBERATELY a separate field from
1815
- * {@link RunInternals.onTaskNotification}: that one is "inject into THIS run" (runtask wraps it
1816
- * as upstream-observer + own-queue), and reusing it for the child would tee every internal child
1817
- * notification (grandchild completions, monitor events) into the parent — double delivery.
1844
+ * {@link RunInternals.onTaskNotification}: that one is "inject into the run's own SESSION" (runtask
1845
+ * wraps it as upstream-observer + session route), and reusing it for the child would tee every
1846
+ * internal child notification (grandchild completions, monitor events) into the parent — double delivery.
1818
1847
  */
1819
1848
  parentNotify?: (notification: TaskNotificationPayload, opts?: {
1820
1849
  priority?: import("../task-notification.js").SystemInjectionPriority;
@@ -1831,9 +1860,9 @@ export interface RunInternals {
1831
1860
  * live (runtask calls it with its wrapped `injectTaskNotification`). The spawner stores it on the
1832
1861
  * child's registry handle so a parent/sibling SendMessage can deliver TO the RUNNING child at its
1833
1862
  * next turn boundary (CC's in-memory `pendingMessages` pedestal, anchors/2.1.212
1834
- * messaging-runtime.md §2.5). The injector is RUN-SCOPED but fail-safe after the run: the lane's
1835
- * teardown branch parks late payloads per session (PendingSessionNotifications), which is exactly
1836
- * CC's durable-mailbox posture for an idle teammate (§2.4) — no separate file mailbox needed.
1863
+ * messaging-runtime.md §2.5). The injector outlives its run safely: once this lane is over the frame
1864
+ * is routed to whatever run the SESSION has live, and only a session with no live run parks it
1865
+ * (PendingSessionNotifications) — exactly CC's durable-mailbox posture for an idle teammate (§2.4).
1837
1866
  * TRUSTED chain (core spawner only); never reachable from TaskSpec.
1838
1867
  */
1839
1868
  onNotifyInjectorReady?: (inject: (notification: TaskNotificationPayload, opts?: {
@@ -2360,6 +2389,30 @@ export interface TurnBoundaryDeps {
2360
2389
  /** R5(harness 事件处理器族)的依赖包。全部为 runLocked 内声明顺序早于
2361
2390
  * 工厂调用点的 const 稳定引用 + 三个 runLocked 形参;this 面(deps.onError)经 runnerHooks 打包
2362
2391
  * (B13 #coreView / R9 TurnBoundaryDeps 同款)。*/
2392
+ /** What a PRODUCER of a `tool_progress` frame knows: the FACTS it observed. The frame minus everything only
2393
+ * the run supplies — its discriminator, its {@link TaskEventIdentity} stamp, and the elapsed time, which the
2394
+ * lane computes from its own per-call origin. Derived from the union member, so seat and frame cannot drift. */
2395
+ export type ToolProgressBody = Omit<Extract<TaskEvent, {
2396
+ type: "tool_progress";
2397
+ }>, "type" | "elapsedTimeSeconds" | keyof TaskEventIdentity>;
2398
+ /** The run's TOOL-PROGRESS lane: the ONE mint of `tool_progress` plus the heartbeat clock over the calls
2399
+ * in flight. Declared on the FLOOR because both registry-side readers sit above its implementation (the
2400
+ * driver builds it, the harness-handler lane drives it) and the Prepared seat that carries it to the tool
2401
+ * band is here too. `createToolProgressLane` (tool-progress-lane.ts, layer 1) is its one implementation. */
2402
+ export interface ToolProgressLane {
2403
+ /** Offer ONE frame — from a tool that observes its own progress (the shell's per-second tick) or from
2404
+ * the lane's own heartbeat. The lane stamps the elapsed time, DROPS the offer when it would land inside
2405
+ * the cadence floor, and resets that call's beat clock when it does not. NEVER THROWS: a tool calls this
2406
+ * from inside an environment output callback, where a throw kills the command it is describing. */
2407
+ emit(body: ToolProgressBody): void;
2408
+ /** A tool call entered execution: it joins the heartbeat's in-flight set and its clock starts, stamped
2409
+ * by the LANE (one clock owns one number — see its module header on why that clock is monotonic). */
2410
+ noteStart(toolCallId: string, toolName: string): void;
2411
+ /** A tool call settled: it leaves the in-flight set. */
2412
+ noteEnd(toolCallId: string): void;
2413
+ /** Drop every in-flight call and disarm the timer (turn end / teardown): no beating into the next turn. */
2414
+ clear(): void;
2415
+ }
2363
2416
  export interface HarnessHandlersDeps {
2364
2417
  spec: TaskSpec;
2365
2418
  queue: PushQueue<TaskEvent>;
@@ -2375,6 +2428,9 @@ export interface HarnessHandlersDeps {
2375
2428
  emitCommitted: (entryId: string, role: "user" | "assistant" | "toolResult", toolCallId?: string) => void;
2376
2429
  startedToolCallIds: Set<string>;
2377
2430
  toolStartAt: Map<string, number>;
2431
+ /** The run's tool-progress lane: the handlers tell it when a call enters and leaves execution, and
2432
+ * clear it with the rest of the per-call bookkeeping at turn end. */
2433
+ toolProgress: ToolProgressLane;
2378
2434
  writeFamilyOf: (name: string) => WriteFamily | undefined;
2379
2435
  toolLabels: Map<string, string>;
2380
2436
  postToolBatchHook: ((batch: PostToolBatchCall[], meta?: {
@@ -7,7 +7,7 @@
7
7
  * · which call a CONTENT-ASK checkpoint parks on, and the one-shot answering face a decide's answer is
8
8
  * bound to for the redeemed call;
9
9
  * · the capture-time domain checks on a decide's TEXT payloads, and the label a rejected value gets;
10
- * · the model-facing text for a deferred batch sibling, and the continuation prompt itself.
10
+ * · the continuation prompt itself.
11
11
  *
12
12
  * Pure over its arguments (the answering face is the one closure, and it closes over its own arguments
13
13
  * only). Run-loop machinery (layer 1), named without the `resume-` family prefix: that prefix marks a
@@ -25,8 +25,6 @@ import type { ResumeRun } from "./contracts.js";
25
25
  * reported the same decidePath locked no matter how many times the human decided).
26
26
  */
27
27
  export declare function resumeDecisionWasNegative(resume: ResumeRun): boolean;
28
- /** Model-facing result for a batch sibling of a suspended call that v1 does not blind-run (§4.ter). */
29
- export declare const DEFERRED_REISSUE: string;
30
28
  /**
31
29
  * Structural deep equality of two JSON-serializable values (order-independent over object keys). Used ONLY
32
30
  * by the Runner's `resumeStream` reopen-by-reason guard to confirm an `env_failed` re-resume
@@ -7,8 +7,6 @@ export function resumeDecisionWasNegative(resume) {
7
7
  const o = resume.outcome;
8
8
  return o.decision === "reject" || o.decision === "deny";
9
9
  }
10
- export const DEFERRED_REISSUE = "[DEFERRED] This tool call shared a batch with a call that suspended for durable approval, so it was " +
11
- "NOT executed on resume. If you still need it, issue it again now.";
12
10
  export function deepJsonEqual(a, b) {
13
11
  if (a === b)
14
12
  return true;
@@ -122,11 +120,7 @@ export function resumeContinuation(resume, mark) {
122
120
  const decided = resume.outcome.decision === "allow"
123
121
  ? "was APPROVED and has now been executed — its result is in the tool results above"
124
122
  : `was DENIED${resume.outcome.reason ? `: ${delimitUntrusted("reviewer note", resume.outcome.reason, REVIEWER_NOTE_MAX_BODY)}` : ""}`;
125
- const remaining = pendingAction.batchToolCallIds.filter((id) => id !== pendingAction.toolCallId && !pendingAction.completedCallIds.includes(id)).length;
126
- const batchNote = remaining > 0
127
- ? ` ${remaining} other tool call(s) from that batch were returned as deferred and not run — re-issue any you still need.`
128
- : "";
129
- return formatHookFeedback(`The pending tool call "${pendingAction.toolName}" ${decided}.${batchNote} ` +
123
+ return formatHookFeedback(`The pending tool call "${pendingAction.toolName}" ${decided}. ` +
130
124
  `This is a RESUMED task — every step before this point is already done and its results are in the ` +
131
125
  `conversation above. Do NOT restart the task or re-run any tool you already ran; continue from this ` +
132
126
  `exact point, building on the existing results, and finish the remaining work.`, mark);
@@ -0,0 +1,52 @@
1
+ /**
2
+ * The deferral CENSUS: what a leg discloses about which of its tools ship as name-only placeholders, and the
3
+ * seat that carries it to the stream. Split out of the disclosure mount because it is a different concern —
4
+ * the mount decides the request FACE, this decides what is SAID about it.
5
+ */
6
+ import type { ToolSearchPolicy, ResolvedToolSearch } from "../tool-search-setting.js";
7
+ /**
8
+ * The HOST-facing census of this leg's deferral: which tool names ship as name-only placeholders, which the model has activated, and the policy that decided it.
9
+ * @contract tool_disclosure.snapshot — the `tool_disclosure` frame is minted ONLY here ({@link ToolDisclosureSeat.publish}), exists ONLY on a leg that defers
10
+ * at least one tool, and carries the WHOLE census each time (a consumer replaces its copy; the "what just happened" delta is this snapshot's `activated` minus
11
+ * the previous one's). `deferred` is frozen at prepare (a name never leaves it, it only appears in `activated`); both lists are sorted, so two snapshots
12
+ * compare by value. A leg that defers nothing emits no frame at all — absence is "nothing was deferred", never "the disclosure is off".
13
+ */
14
+ export interface ToolDisclosureSnapshot {
15
+ /** Which arm of `RunnerDeps.toolSearch` decided rule ② on this leg. */
16
+ policy: ToolSearchPolicy;
17
+ /** Present iff `policy === "auto"`: the percent of the window the candidate set had to reach. */
18
+ thresholdPercent?: number;
19
+ /** Every deferred name on this leg, sorted. Non-empty (the frame does not exist otherwise). */
20
+ deferred: readonly string[];
21
+ /** The deferred names ACTIVE on this leg right now, sorted — the engine's state, not a record of commits. Empty on the leg-start census only when nothing
22
+ * seeded it; a RESUMED leg carries whatever the engine reseeded (transcript replay, compaction snapshot, checkpoint) before anything executed. */
23
+ activated: readonly string[];
24
+ }
25
+ /**
26
+ * The run-time seat the frame rides: prepare publishes the leg-start census, every activation that changes it publishes the next, and the run's event wiring
27
+ * subscribes once. Snapshots published before the sink is installed are buffered (bounded) and flushed in order, so the leg-start census can be minted at
28
+ * prepare and still arrive after the manifest frame it belongs beside. Deliberately the same shape as `ToolRosterDeltaSeat` — the two are one problem.
29
+ */
30
+ export declare class ToolDisclosureSeat {
31
+ #private;
32
+ /** Publish a census. A snapshot equal to the last published one is DROPPED: `rematerialize` also runs on an MCP refresh, and a frame that says nothing
33
+ * changed reads as an activation that did not happen. */
34
+ publish(snapshot: ToolDisclosureSnapshot): void;
35
+ /** Install the stream sink; anything published before is flushed in order. */
36
+ subscribe(sink: (snapshot: ToolDisclosureSnapshot) => void): void;
37
+ /** The leg ended: drop the sink so a late publish never reaches a closed stream (it buffers, unread). */
38
+ unsubscribe(): void;
39
+ }
40
+ /**
41
+ * Mint a leg's census producer: the frozen deferred list, the policy it was classified under, and a seat.
42
+ * One place holds both halves, so a census can never name a policy the classification did not run under.
43
+ *
44
+ * `deferred` is frozen because every census shares the one array — a host that mutated the list it was handed
45
+ * would be editing every later frame of the leg. `activated` is filtered to the DEFERRED names: the live
46
+ * activation set is seeded from the transcript and a resume checkpoint, both of which can name an undeferred
47
+ * tool.
48
+ */
49
+ export declare function createDeferralCensus(deferred: ReadonlySet<string>, policy: ResolvedToolSearch): {
50
+ seat: ToolDisclosureSeat;
51
+ publish: (active: ReadonlySet<string>) => void;
52
+ };
@@ -0,0 +1,44 @@
1
+ export class ToolDisclosureSeat {
2
+ #pending = [];
3
+ #sink;
4
+ #last;
5
+ publish(snapshot) {
6
+ if (this.#last !== undefined && sameDisclosure(this.#last, snapshot))
7
+ return;
8
+ this.#last = snapshot;
9
+ if (this.#sink !== undefined)
10
+ this.#sink(snapshot);
11
+ else {
12
+ this.#pending.push(snapshot);
13
+ if (this.#pending.length > 64)
14
+ this.#pending.shift();
15
+ }
16
+ }
17
+ subscribe(sink) {
18
+ this.#sink = sink;
19
+ const pending = this.#pending;
20
+ this.#pending = [];
21
+ for (const s of pending)
22
+ sink(s);
23
+ }
24
+ unsubscribe() {
25
+ this.#sink = undefined;
26
+ }
27
+ }
28
+ function sameDisclosure(a, b) {
29
+ const sameList = (x, y) => x.length === y.length && x.every((n, i) => n === y[i]);
30
+ return a.policy === b.policy && a.thresholdPercent === b.thresholdPercent && sameList(a.deferred, b.deferred) && sameList(a.activated, b.activated);
31
+ }
32
+ export function createDeferralCensus(deferred, policy) {
33
+ const seat = new ToolDisclosureSeat();
34
+ const names = Object.freeze([...deferred].sort());
35
+ return {
36
+ seat,
37
+ publish: (active) => seat.publish({
38
+ policy: policy.policy,
39
+ ...(policy.thresholdPercent !== undefined ? { thresholdPercent: policy.thresholdPercent } : {}),
40
+ deferred: names,
41
+ activated: [...active].filter((n) => deferred.has(n)).sort(),
42
+ }),
43
+ };
44
+ }
@@ -2,15 +2,15 @@
2
2
  * The derived-leg route pre-flight shared by prepareTask's two derived seats: the compaction-summary model (the
3
3
  * driver's own seat, resolved before the offload prelude) and the auto-mode classifier model (the caps-and-workflow
4
4
  * phase's arming). Moved whole out of the orchestrator so the phase reaches DOWN for it instead of the two callers
5
- * spelling it twice. The judgment law itself lives in the brain's route adjudicator; this is the one guarded seat
6
- * around it.
5
+ * spelling it twice. The judgment law and both announcements live in the brain's route adjudicator; this is the one
6
+ * guarded seat around them.
7
7
  */
8
8
  import type { Model } from "../../internal/llm.js";
9
9
  import type { RunnerDeps, TaskSpec } from "../types.js";
10
10
  /**
11
11
  * The derived-leg pairing pre-flight as ONE guarded seat: judge the derived model where its route
12
- * differs from the primary's, announce the fallback on a broken pairing, and ABSTAIN (keep the
13
- * derived model) on ANY throw. The pre-flight is advisory — the brain's request gate re-runs the
12
+ * differs from the primary's, announce what a broken pairing means for THIS seat (`onBrokenPairing` —
13
+ * one judgment, and the POSTURE is the deployment's), and ABSTAIN (keep the derived model) on ANY throw. The pre-flight is advisory — the brain's request gate re-runs the
14
14
  * same law — so neither a throwing judge face nor a garbage catalog value (the route-identity
15
15
  * normalization throws a TypeError on a non-string baseUrl) may widen into "task preparation
16
16
  * failed": at the compaction seat the session is already acquired and the throw-cleanup contract
@@ -31,4 +31,8 @@ export declare function derivedRouteFallsBack(args: {
31
31
  /** The INVOCATION this seat resolution belongs to. The session cannot stand in for it: two
32
32
  * runs of one session that both fall back on the same seat mint otherwise byte-identical lines. */
33
33
  runId?: string;
34
+ /** What this seat's DEPLOYMENT asked for on a broken pairing. Absent / `"fall_back"`: swap and announce `route.fallback_to_primary`
35
+ * (the only posture a seat with no knob of its own has). `"refuse"`: keep the seat and announce `route.classifier_unroutable` — never
36
+ * quietly granting a fallback the deployment declined (`autoMode.classifierFallback: "none"`); announcing is why no swap is not no word. */
37
+ onBrokenPairing?: "fall_back" | "refuse";
34
38
  }): Promise<boolean>;
@@ -1,4 +1,4 @@
1
- import { adjudicateDerivedRoute, fallbackToPrimaryNotice, sameRouteIdentity } from "../../brain/route-adjudicator.js";
1
+ import { adjudicateDerivedRoute, fallbackToPrimaryNotice, sameRouteIdentity, unroutableSeatNotice } from "../../brain/route-adjudicator.js";
2
2
  import { deliverEngineNotice } from "../types.js";
3
3
  export async function derivedRouteFallsBack(args) {
4
4
  try {
@@ -7,8 +7,13 @@ export async function derivedRouteFallsBack(args) {
7
7
  const verdict = await adjudicateDerivedRoute({ brain: args.brain, model: args.derived, getApiKeyAndHeaders: args.getApiKeyAndHeaders });
8
8
  if (verdict === undefined || verdict.ok)
9
9
  return false;
10
- deliverEngineNotice(args.onNotice, fallbackToPrimaryNotice({ seat: args.seat, from: args.derived.id, to: args.primary.id, verdict, ...(args.sessionId !== undefined ? { sessionId: args.sessionId } : {}), ...(args.runId !== undefined ? { runId: args.runId } : {}) }));
11
- return true;
10
+ const shared = { seat: args.seat, from: args.derived.id, verdict, ...(args.sessionId !== undefined ? { sessionId: args.sessionId } : {}), ...(args.runId !== undefined ? { runId: args.runId } : {}) };
11
+ const refuse = args.onBrokenPairing === "refuse";
12
+ if (refuse)
13
+ deliverEngineNotice(args.onNotice, unroutableSeatNotice({ ...shared, primary: args.primary.id }));
14
+ else
15
+ deliverEngineNotice(args.onNotice, fallbackToPrimaryNotice({ ...shared, to: args.primary.id }));
16
+ return !refuse;
12
17
  }
13
18
  catch {
14
19
  return false;
@@ -22,6 +22,7 @@
22
22
  * contract (`ToolGateInput`, `ToolGateResult`, the observer payload), which stays declared there.
23
23
  */
24
24
  import { type ClassifierDenyCause, type DeniedBy, type GateOutcome, type Settlement } from "../gate-outcome.js";
25
+ import type { AutoModeClassifierRound } from "../auto-mode.js";
25
26
  import type { AskOrigin, AskOriginFacts } from "../ask-origin.js";
26
27
  import { type PermissionResult, type ResolvedAsk, type ToolCallRequest } from "../tool-policy.js";
27
28
  import type { SafeNotifier } from "../safe-notify.js";
@@ -40,6 +41,10 @@ export interface GateExitFacts {
40
41
  deniedBy?: DeniedBy;
41
42
  cause?: ClassifierDenyCause;
42
43
  settled?: SettledAsk;
44
+ /** The classifier CLASSIFICATION this pass ran, when it ran one on a decider that can name its model.
45
+ * Orthogonal to `deniedBy`: one that ALLOWED is attributed too (a fallback model's allow is exactly the case
46
+ * an operator needs to see), which is why it rides the disposition's shared member, not the deny arm. */
47
+ classifier?: AutoModeClassifierRound;
43
48
  }
44
49
  /**
45
50
  * Mint the outcome. A defect here is an ENGINE defect (a refusing site that did not say who refused, a
@@ -220,6 +225,13 @@ export interface GatePass {
220
225
  * re-check when an inherited station's attested deny arrives through the chain; undefined for a ruling
221
226
  * block and every non-classifier deny. The exit mints `GateDisposition.cause` from it. */
222
227
  denyCause: ClassifierDenyCause | undefined;
228
+ /** borrowed-mutable — the classifier CLASSIFICATION this pass ran ({@link AutoModeClassifierRound}).
229
+ * Written ONCE by the gate's classifier step from the verdict the decider handed back, on EVERY arm (allow
230
+ * included — a fallback model's allow is the fact the record exists for); absent when no classifier was
231
+ * consulted or the decider could name no model. Both exits mint `GateDisposition.classifier` from it.
232
+ * OPTIONAL rather than `| undefined` (the `probeUnanswered` form): the dispatch that builds the pass is
233
+ * not a station of this lane and has nothing to declare here. */
234
+ classifierRound?: AutoModeClassifierRound;
223
235
  /** borrowed-mutable — the standing verdict. The no-policy default until the adjudication station writes the
224
236
  * policy's answer; then rewritten by every station that changes the verdict (fold, tightens, org, rule
225
237
  * lane, stamp, classifier, sandbox, content lane, resolution, edit chain). */
@@ -4,8 +4,11 @@ import { inlineUntrusted } from "../untrusted-text.js";
4
4
  import { formatHookFeedback } from "../reminder-mint.js";
5
5
  export function mintGateOutcome(facts) {
6
6
  const settlement = facts.settled !== undefined ? Object.freeze({ ...facts.settled.settlement, who: Object.freeze({ ...facts.settled.settlement.who }) }) : undefined;
7
+ const attribution = facts.classifier !== undefined ? Object.freeze({ ...facts.classifier, ...(facts.classifier.fallback !== undefined ? { fallback: Object.freeze({ ...facts.classifier.fallback }) } : {}) }) : undefined;
7
8
  const outcome = Object.freeze({
8
- disposition: Object.freeze(facts.deniedBy !== undefined ? { kind: "denied", deniedBy: facts.deniedBy, ...(facts.cause !== undefined ? { cause: facts.cause } : {}) } : { kind: "allowed" }),
9
+ disposition: Object.freeze(facts.deniedBy !== undefined
10
+ ? { kind: "denied", deniedBy: facts.deniedBy, ...(facts.cause !== undefined ? { cause: facts.cause } : {}), ...(attribution !== undefined ? { classifier: attribution } : {}) }
11
+ : { kind: "allowed", ...(attribution !== undefined ? { classifier: attribution } : {}) }),
9
12
  ...(facts.settled !== undefined ? { settlement: settlement, origin: facts.settled.origin } : {}),
10
13
  });
11
14
  const defects = screenGateOutcome(outcome);
@@ -114,7 +117,7 @@ export async function exitGate(pass) {
114
117
  }
115
118
  if (pass.deniedBy === undefined)
116
119
  throw new Error(`the tool gate refused "${toolName}" without a refusing layer — every deny site attributes itself`);
117
- const gate = mintGateOutcome({ deniedBy: pass.deniedBy, ...(pass.denyCause !== undefined ? { cause: pass.denyCause } : {}), ...(ledger.settled !== undefined ? { settled: ledger.settled } : {}) });
120
+ const gate = mintGateOutcome({ deniedBy: pass.deniedBy, ...(pass.denyCause !== undefined ? { cause: pass.denyCause } : {}), ...(ledger.settled !== undefined ? { settled: ledger.settled } : {}), ...(pass.classifierRound !== undefined ? { classifier: pass.classifierRound } : {}) });
118
121
  const unresolvable = askUnresolvableCauseOf(pass.decision, { toolCallId, toolName }) ?? unresolvableCauseOfSettlement(gate);
119
122
  if (unresolvable !== undefined) {
120
123
  const cause = unresolvable;
@@ -142,7 +145,7 @@ export async function exitGate(pass) {
142
145
  }
143
146
  return {
144
147
  updatedInput: pass.currentInput === event.input ? undefined : pass.currentInput,
145
- gate: mintGateOutcome(ledger.settled !== undefined ? { settled: ledger.settled } : {}),
148
+ gate: mintGateOutcome({ ...(ledger.settled !== undefined ? { settled: ledger.settled } : {}), ...(pass.classifierRound !== undefined ? { classifier: pass.classifierRound } : {}) }),
146
149
  preToolContext: pass.preToolContext,
147
150
  };
148
151
  }
@@ -7,13 +7,18 @@
7
7
  * face) exactly as `Read` does, then reads the bytes once from the execution env. A local stub's store
8
8
  * (`capabilities.storageDir`) must lie OUTSIDE every root the run may write or read — a ledger the model can
9
9
  * edit through the fs tools is not a trust boundary — and a store inside one refuses the mount, naming both.
10
+ * The store is a path on THIS process's filesystem; a root is the execution env's canonical key. The two are
11
+ * compared as identities under one spelling family only when both spell the same family (`within` reads both
12
+ * under the root's family); a root of the other family names a filesystem the store cannot be on, and no
13
+ * containment claim is made against it.
10
14
  * Synchronous: the stretch has no await (the file read is a closure the tool calls later).
11
15
  */
12
16
  import type { ExecutionEnv } from "../../internal/harness.js";
17
+ import type { OnAsk } from "../tool-policy.js";
13
18
  import { type ArtifactPlane } from "../artifact-host.js";
14
19
  import { RosterBuilder } from "../tool-roster.js";
15
20
  import type { AgentTool, RunnerDeps } from "../types.js";
16
- import type { ReadDenyMatcher, ReadFace } from "../../tools/fs/index.js";
21
+ import type { ReadDenyMatcher, ContainmentFace } from "../../tools/fs/index.js";
17
22
  import type { Prepared } from "./contracts.js";
18
23
  export interface PrepareArtifactInput {
19
24
  /** borrowed-readonly — the deployment's hosting seat and the notice sink the refusal arm announces on. */
@@ -26,6 +31,9 @@ export interface PrepareArtifactInput {
26
31
  sessionId: string;
27
32
  /** borrowed-readonly — the spawned name this run carries, when any (call-context attribution only). */
28
33
  agentName: string | undefined;
34
+ /** borrowed-readonly — the run's effective approver (`spec.onAsk ?? deps.onAsk`, the seat the tool gate resolves at):
35
+ * the tool's shared-read consent gate rides it as a mount dep (a core mount's execute ctx is minimal). */
36
+ onAsk: OnAsk | undefined;
29
37
  /** borrowed-readonly — the run's resolved env: the byte source of a published local file. */
30
38
  executionEnv: ExecutionEnv;
31
39
  /** borrowed-readonly — the canonical task root the fs fence contains reads to; undefined ⇒ no local-file publish. */
@@ -35,7 +43,7 @@ export interface PrepareArtifactInput {
35
43
  /** borrowed-readonly — the read-deny matcher in force for the run, when any. */
36
44
  readDenyMatcher: ReadDenyMatcher | undefined;
37
45
  /** borrowed-readonly — the resolved read face (`"open"` lifts root containment for reads). */
38
- resolvedReadFace: ReadFace | undefined;
46
+ resolvedReadFace: ContainmentFace | undefined;
39
47
  /** borrowed-readonly — the tracked-cwd cell; `current` is read per call as the relative-path base. */
40
48
  handsCwdRef: Prepared["cwdRef"];
41
49
  /** borrowed-mutable — the run's roster builder; writer here: ONE mount when the seat is valid. */
@@ -5,10 +5,10 @@ import { RosterBuilder } from "../tool-roster.js";
5
5
  import { defineTool } from "../tools.js";
6
6
  import { inlineUntrusted } from "../untrusted-text.js";
7
7
  import { physicalPathOfExisting } from "../physical-path.js";
8
- import { resolveKey, violationDetails, violationText, withinAnyRoot } from "../../tools/fs/safety.js";
8
+ import { pathFamilyOf, resolveKey, violationDetails, violationText, withinAnyRoot } from "../../tools/fs/safety.js";
9
9
  import { createArtifactTool } from "../../tools/artifact/artifact-tool.js";
10
10
  export function prepareArtifact(input) {
11
- const { deps, runId, taskId, sessionId, agentName, executionEnv, rootCanonical, additionalRootsCanonical, readDenyMatcher, resolvedReadFace, handsCwdRef, roster, firstPartyOffload, envHandToolNames } = input;
11
+ const { deps, runId, taskId, sessionId, agentName, onAsk, executionEnv, rootCanonical, additionalRootsCanonical, readDenyMatcher, resolvedReadFace, handsCwdRef, roster, firstPartyOffload, envHandToolNames } = input;
12
12
  const host = deps.artifactHost;
13
13
  if (host === undefined)
14
14
  return { artifactPlane: undefined };
@@ -26,7 +26,8 @@ export function prepareArtifact(input) {
26
26
  const storageDir = verdict.capabilities.storageDir;
27
27
  if (storageDir !== undefined && rootCanonical !== undefined) {
28
28
  const real = physicalPathOfExisting(storageDir);
29
- const inside = [rootCanonical, ...additionalRootsCanonical].find((root) => withinAnyRoot([root], real));
29
+ const storeFamily = pathFamilyOf({ root: real });
30
+ const inside = [rootCanonical, ...additionalRootsCanonical].find((root) => pathFamilyOf({ root }) === storeFamily && withinAnyRoot([root], real));
30
31
  if (inside !== undefined)
31
32
  return refuse(`the local artifact store ${storageDir} lies inside a root this run may reach through its file tools (${inside}); put the store outside every task root and additional directory`);
32
33
  }
@@ -56,7 +57,7 @@ export function prepareArtifact(input) {
56
57
  return { ok: false, reason: "too_large", size: r.value.byteLength, limit: maxBytes, judgedOn: "bytes" };
57
58
  return { ok: true, key: rk.key, bytes: r.value };
58
59
  };
59
- const artifactSpec = createArtifactTool({ host, capabilities: verdict.capabilities, runId, taskId, sessionId, ...(agentName !== undefined ? { agentName } : {}), readLocalFile });
60
+ const artifactSpec = createArtifactTool({ host, capabilities: verdict.capabilities, runId, taskId, sessionId, ...(agentName !== undefined ? { agentName } : {}), readLocalFile, ...(onAsk !== undefined ? { onAsk } : {}) });
60
61
  roster.mount(firstPartyOffload(defineTool(artifactSpec)), { source: "builtin", mountedBy: "optional" });
61
62
  envHandToolNames.add(ARTIFACT_TOOL_NAME);
62
63
  return { artifactPlane: verdict.capabilities.plane };
@@ -33,7 +33,7 @@ import type { OnAsk } from "../tool-policy.js";
33
33
  import type { RunnerDeps, RuntimeCaps, TaskEvent, TaskSpec, ToolExecuteContext } from "../types.js";
34
34
  import type { AutoModeArmFact } from "../wiring-manifest.js";
35
35
  import type { AssertAllKeysHandled } from "../ask-origin.js";
36
- import type { ReadFace } from "../../tools/fs/index.js";
36
+ import type { ContainmentFace } from "../../tools/fs/index.js";
37
37
  import type { InheritedGate, Prepared, RunInternals, RunnerSelfSeat, ToolFaceSnapshot } from "./contracts.js";
38
38
  import { type BlockedRef } from "./synthetic-tools.js";
39
39
  import { RosterBuilder } from "../tool-roster.js";
@@ -91,7 +91,9 @@ export interface PrepareCapsAndWorkflowInput {
91
91
  frozenOnAsk: OnAsk | undefined;
92
92
  /** borrowed-readonly — the parent SPEC seat the children inherit (minted once by the inherited-gate phase; the Workflow mount's dep, the same value the delegation ctx carries). */ parentSpecSeat: ParentSpecSeat;
93
93
  /** borrowed-readonly — the ONE read-face carrier expression (the Workflow mount's parentReadFace getter calls it). */
94
- carrierReadFace: () => ReadFace | undefined;
94
+ carrierReadFace: () => ContainmentFace | undefined;
95
+ /** borrowed-readonly — the same carrier expression on the WRITE seat. */
96
+ carrierWriteFace: () => ContainmentFace | undefined;
95
97
  /** borrowed-readonly — the filtered display-forwarding wrapper (the Workflow mount's forwardEvent; absent ⇒ no key). */
96
98
  forwardEvent: ((e: TaskEvent) => void) | undefined;
97
99
  /** borrowed-readonly — the child-chain assembly (the Workflow mount's inheritedGateForChildren). */