@sema-agent/core 7.17.6 → 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 (164) hide show
  1. package/CHANGELOG.md +165 -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 +375 -530
  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.6.json +81 -0
  162. package/wire-changes/core@7.17.7.json +49 -0
  163. package/dist/tools/fs/read-face.d.ts +0 -68
  164. package/dist/tools/fs/read-face.js +0 -47
@@ -0,0 +1,89 @@
1
+ /** A containment-face state. `"roots"` = the containment judgment runs. `"open"` = the containment
2
+ * step is skipped (canonicalization, the deny/sensitive sets, the UNC out-of-set refusal and the
3
+ * type gates all still run). One type for both seats: the two faces differ in WHICH band consults
4
+ * them, never in what the word means. */
5
+ export type ContainmentFace = "open" | "roots";
6
+ /** Which band a face governs. Selects the seat spellings and the error codes below — never the
7
+ * resolution ORDER, which is one body shared by both. */
8
+ export type ContainmentFaceKind = "read" | "write";
9
+ /** Which layer a seat belongs to. The SPELLING is derived from (kind, layer), never re-typed at a
10
+ * call site: three doors screen the same two seats, and the value gate's message is the only place
11
+ * a deployment reads the seat's name back. */
12
+ export type ContainmentFaceLayer = "spec" | "deps";
13
+ /** The seat spelling for a (kind, layer) pair — single-sourced from {@link FACE_DIALECT}. */
14
+ export declare function containmentFaceSeatName(kind: ContainmentFaceKind, layer: ContainmentFaceLayer): string;
15
+ /** Loud value gate: a containment-face seat carries exactly "open" | "roots" — anything else (case
16
+ * variants, truthy garbage) refuses at wiring time, never folds to a default. Exported
17
+ * module-internally (not on the package surface) so prepare's unconditional config-guard leg can
18
+ * screen the seats on runs that never mount hands. The resolver itself also runs on every hands-less
19
+ * leg; this screen still fires first, so a garbage value refuses before any leg-shape branching. */
20
+ export declare function assertContainmentFaceValue(v: unknown, kind: ContainmentFaceKind, layer: ContainmentFaceLayer): ContainmentFace | undefined;
21
+ /** Inputs to {@link resolveContainmentFace} — all structural/declaration facts, never permission modes
22
+ * (a deliberate axis separation: this shape carries facts, not verdicts). */
23
+ export interface ContainmentFaceInputs {
24
+ /** Which face is being resolved. Selects the seat spellings and the mint codes; the ORDER below is
25
+ * one body, identical for both. */
26
+ kind: ContainmentFaceKind;
27
+ /** TaskSpec.readFace / TaskSpec.writeFace (raw — validated here). The TASK layer: may only tighten
28
+ * under governance. */
29
+ specFace?: unknown;
30
+ /** RunnerDeps / HandsToolkitOptions seat of the same face (raw) — the DEPLOYMENT's own declaration. */
31
+ depsFace?: unknown;
32
+ /** The verifier read-only mount (`handsReadOnly` / toolkit `readOnly`): for the read face its
33
+ * containment is a LOAD-BEARING wall (bash_readonly is genuinely confined by it); for the write
34
+ * face it is the absence of the band entirely. Neither is openable. */
35
+ readOnlyMount: boolean;
36
+ /** Org governance declared (`RunnerDeps.permissionRuleStore.partitions.org === true`): default roots; the task layer
37
+ * may not open (tighten-only under governance; the deps layer, being the deployment's own
38
+ * declaration, still may). */
39
+ orgGoverned: boolean;
40
+ /** Is the FULL shell (`core.bash@1`) structurally reachable on this mount? Computed BEFORE
41
+ * tool-band assembly from spec-time facts (env kind / handsReadOnly / Bash exclusion — every
42
+ * exclusion source is spec-time-frozen, so this is always computable). When false, the roots fence
43
+ * is the ONLY boundary this face has, so no IMPLICIT default may open it; an EXPLICIT "open" on
44
+ * either seat still wins (row 4 of the order is a default, not a clamp). */
45
+ fullShellReachable: boolean;
46
+ /** Observation hook for the ONE silent branch of the order: row 1's deployment-seat clamp
47
+ * (readOnly mount + deps "open" + NO task seat). Fired exactly when the deps declaration was the
48
+ * deciding seat and the mount overrode it — not when a task-level "roots" pinned the same value
49
+ * anyway (the mount changed nothing there, and announcing it would claim a false cause), and
50
+ * never on the throw rows (those are already loud). The resolver stays the single owner of the
51
+ * clamp predicate; stations own delivery/dedup ({@link deploymentFaceClampNotice}). */
52
+ onDeploymentClamp?: () => void;
53
+ }
54
+ /**
55
+ * The ONE resolution order, first hit wins — ONE body for BOTH faces. prepare-task AND
56
+ * createHandsToolkit both call it, once per face, so a library-direct mount gets identical validation
57
+ * and identical defaults on each seat.
58
+ *
59
+ * 1. read-only (verifier) mount → ROOTS; an explicit "open" on the TASK seat is a genuine
60
+ * per-call CONTRADICTION and refuses loudly (never silently pick a side). An "open" on the
61
+ * DEPLOYMENT seat alone is a deployment-wide default, not a per-task assertion — it silently
62
+ * CLAMPS to roots (stricter-wins), the same seat distinction row 2 draws for org governance.
63
+ * 2. org-governed: the task seat's "open" refuses (task layer only tightens); deps "open" wins;
64
+ * otherwise ROOTS.
65
+ * 3. explicit seat: spec ?? deps.
66
+ * 4. bash-less mount: ROOTS (the fence is the only boundary there — "the fence guards nothing bash
67
+ * reaches anyway" does not hold, so no implicit default may open it).
68
+ * 5. default: ROOTS — the engine never opens implicitly.
69
+ *
70
+ * @contract fs.containment_face.seat_independence — the two containment seats never read each other:
71
+ * each band passes its OWN resolved value into the shared containment step, a seat nobody declared
72
+ * resolves to "roots" on its own, and no gate other than that step is touched by either word. A
73
+ * deployment that wants both bands open declares both.
74
+ */
75
+ export declare function resolveContainmentFace(i: ContainmentFaceInputs): ContainmentFace;
76
+ /**
77
+ * The ONE text of a face's deployment-clamp announcement (`config.<face>_face_deployment_clamped`).
78
+ * Both stations deliver THIS object through their own `deliverEngineNotice` call (prepare-task with
79
+ * the `RunnerDeps.onNotice` seat, once per notice sink — the sink, not the deps object, because the
80
+ * Runner rebuilds its deps; `createHandsToolkit` with its band-local `HandsToolkitOptions.onNotice`
81
+ * seat — toolkit creation is itself the one-per-mount boot moment, and an absent seat prints the
82
+ * same line via `console.warn`). Built here, import-free, so the message can never fork between
83
+ * the stations OR between the two faces.
84
+ */
85
+ export declare function deploymentFaceClampNotice(kind: ContainmentFaceKind): {
86
+ code: string;
87
+ message: string;
88
+ detail: Record<string, unknown>;
89
+ };
@@ -0,0 +1,81 @@
1
+ import { describeRejectedValue } from "../../core/tool-search-setting.js";
2
+ const FACE_DIALECT = {
3
+ read: {
4
+ key: "readFace",
5
+ specSeat: "TaskSpec.readFace",
6
+ depsSeat: "readFace (deployment seat)",
7
+ depsOwnKey: "RunnerDeps.readFace",
8
+ noun: "open read face",
9
+ invalidCode: "config.read_face_invalid",
10
+ readOnlyConflictCode: "config.read_face_readonly_conflict",
11
+ orgEscalationCode: "config.read_face_org_task_escalation",
12
+ readOnlyWhy: `that mount's containment is load-bearing (its read-only shell face is confined by it) and is never openable.`,
13
+ clampWhy: `the mount's containment is load-bearing and never openable`,
14
+ },
15
+ write: {
16
+ key: "writeFace",
17
+ specSeat: "TaskSpec.writeFace",
18
+ depsSeat: "writeFace (deployment seat)",
19
+ depsOwnKey: "RunnerDeps.writeFace",
20
+ noun: "open write face",
21
+ invalidCode: "config.write_face_invalid",
22
+ readOnlyConflictCode: "config.write_face_readonly_conflict",
23
+ orgEscalationCode: "config.write_face_org_task_escalation",
24
+ readOnlyWhy: `that mount carries no write faces at all (the hand band's write half does not mount on it), so an open write face declares a posture for tools this call does not have.`,
25
+ clampWhy: `that mount carries no write faces for it to widen`,
26
+ },
27
+ };
28
+ export function containmentFaceSeatName(kind, layer) {
29
+ const d = FACE_DIALECT[kind];
30
+ return layer === "spec" ? d.specSeat : d.depsSeat;
31
+ }
32
+ export function assertContainmentFaceValue(v, kind, layer) {
33
+ if (v === undefined)
34
+ return undefined;
35
+ if (v === "open" || v === "roots")
36
+ return v;
37
+ const d = FACE_DIALECT[kind];
38
+ const e = new Error(`${containmentFaceSeatName(kind, layer)}: invalid ${d.key} value ${describeRejectedValue(v)} — expected "open" or "roots" (bad values refuse loudly; nothing falls back to a default).`);
39
+ e.code = d.invalidCode;
40
+ throw e;
41
+ }
42
+ export function resolveContainmentFace(i) {
43
+ const d = FACE_DIALECT[i.kind];
44
+ const spec = assertContainmentFaceValue(i.specFace, i.kind, "spec");
45
+ const deps = assertContainmentFaceValue(i.depsFace, i.kind, "deps");
46
+ if (i.readOnlyMount) {
47
+ if (spec === "open") {
48
+ const e = new Error(`${d.key}: "open" contradicts the read-only (verifier) mount — ${d.readOnlyWhy} Drop the ${d.key} declaration or the readOnly mount.`);
49
+ e.code = d.readOnlyConflictCode;
50
+ throw e;
51
+ }
52
+ if (deps === "open" && spec === undefined)
53
+ i.onDeploymentClamp?.();
54
+ return "roots";
55
+ }
56
+ if (i.orgGoverned) {
57
+ if (spec === "open") {
58
+ const e = new Error(`${d.specSeat}: "open" is refused under organization governance — the task layer may only tighten (set ${d.depsOwnKey}: "open" if the DEPLOYMENT declares the ${d.noun}).`);
59
+ e.code = d.orgEscalationCode;
60
+ throw e;
61
+ }
62
+ if (spec === "roots")
63
+ return "roots";
64
+ return deps ?? "roots";
65
+ }
66
+ if (spec !== undefined)
67
+ return spec;
68
+ if (deps !== undefined)
69
+ return deps;
70
+ if (!i.fullShellReachable)
71
+ return "roots";
72
+ return "roots";
73
+ }
74
+ export function deploymentFaceClampNotice(kind) {
75
+ const d = FACE_DIALECT[kind];
76
+ const message = `${d.depsSeat}: the deployment-wide "open" declaration is not in force on this read-only (verifier) mount — clamped to "roots" (stricter-wins: ${d.clampWhy}). Other mounts still honor "open".`;
77
+ const detail = { seat: d.depsSeat, declared: "open", inForce: "roots", cause: "read_only_mount" };
78
+ return kind === "read"
79
+ ? { code: "config.read_face_deployment_clamped", message, detail }
80
+ : { code: "config.write_face_deployment_clamped", message, detail };
81
+ }
@@ -115,7 +115,10 @@ export declare function bashReversibilityProbe(allow?: readonly string[], bounda
115
115
  * on the filesystem, and a target that leaves the roots only after a symlink is followed is REFUSED —
116
116
  * refused rather than asked, because by then the approval step is behind the call (see `createBashTool`'s
117
117
  * `readBoundary` option). The leg runs where an environment can resolve a link locally; a remote adapter
118
- * keeps the textual answer alone.
118
+ * keeps the textual answer alone. A program FILE named in the slot of a verb this floor has no row for
119
+ * (`awk -f prog.awk`, `jq -f filter.jq`, `python3 script.py`) is judged as the in-root operand it is, and the
120
+ * program inside it is text nobody here parsed — vouched as any in-root operand is, which is the floor's
121
+ * stated reach, not a hole in the sandbox.
119
122
  */
120
123
  export declare function bashReadBoundaryProbe(boundary: BashReadonlyRootBoundary | (() => BashReadonlyRootBoundary | undefined)): (args: unknown) => ReversibilityVerdict;
121
124
  /**
@@ -131,6 +134,16 @@ export { FULL_SHELL_CONTRACT_ID } from "../../core/tool-catalog-entries.js";
131
134
  * (CC `cLp`). Exported for its unit tests. */
132
135
  export declare function bashExitOneInterpretation(command: string): string | undefined;
133
136
  export declare function canAutoBackground(command: string, depth?: number): boolean;
137
+ /** One observation of a running command, handed to {@link runShell}'s caller once per
138
+ * {@link TOOL_PROGRESS_BOUND}.intervalMs. NO clock: how long the call has been running is the RUN's fact,
139
+ * not this leg's — the two producers of a progress frame do not start their clocks at the same instant, so
140
+ * the lane that mints the frame owns the origin. The output trio rides or stays away TOGETHER, and only
141
+ * when the environment has actually streamed something — see the frame's own absence law. */
142
+ export interface RunningShellTick {
143
+ output?: string;
144
+ totalLines?: number;
145
+ totalBytes?: number;
146
+ }
134
147
  /**
135
148
  * `bash` (effect:write) — a full shell. ⚠️ It runs with `rootCanonical` as the initial cwd but is NOT
136
149
  * sandboxed: a command can `cd` out, read/write/delete any path the process can reach, and use the
@@ -169,8 +182,35 @@ export declare function createBashTool(env: ExecutionEnv, rootCanonical: string,
169
182
  * `canAutoBackground`-eligible command it never expected to survive its timeout started surviving
170
183
  * it). A deployment that wants the CC-parity behavior sets this explicitly; sema's own default
171
184
  * Runner wiring currently does NOT set it, so this feature ships INERT until a caller opts in.
185
+ *
186
+ * IT ARMS TWO TRIGGERS, not one — the name is older than the second. Both are the engine deciding
187
+ * to adopt a running command instead of ending it, both take the same adoption chain, and both are
188
+ * gated on the same eligibility read of this command's text (`canAutoBackground`): (1) the command's
189
+ * OWN timeout fires, and (2) a person's input is waiting for the turn boundary while this command
190
+ * still runs (see {@link queuedHumanInput}). A deployment that opted in for one has opted in for
191
+ * both; there is deliberately no second knob, because "may this command be moved to the background
192
+ * by the engine" is one question and answering it twice is how the two triggers would drift apart.
172
193
  */
173
194
  autoBackgroundOnTimeout?: boolean;
195
+ /**
196
+ * The run's tool-progress sink: one call per liveness tick of a running foreground command (about
197
+ * once a second), minted into a `tool_progress` frame by whoever wired this. Runner-wired; a
198
+ * standalone toolkit build leaves it absent, and the execute leg then opens no streaming callbacks
199
+ * at all — its `env.exec` options are byte-identical to what they were before this seat existed.
200
+ */
201
+ onToolProgress?: (body: import("../../core/runner/contracts.js").ToolProgressBody) => void;
202
+ /**
203
+ * Is a PERSON's input waiting for this run's next turn boundary? Read on each liveness tick of a
204
+ * running foreground command: `true` on an {@link autoBackgroundOnTimeout}-armed command moves that
205
+ * command into the background (through the SAME detach verb ctrl+b fires) so the message is
206
+ * delivered this turn instead of after the command. CC 2.1.259 `backgroundedToDeliverMessage`.
207
+ *
208
+ * The predicate is the HARNESS's own accepted-but-unserved account and deliberately counts only what
209
+ * a PERSON sent: an engine-authored reminder is not somebody waiting, and stepping a build aside for
210
+ * one would fire on nearly every long command. Absent ⇒ nothing is ever auto-backgrounded for a
211
+ * message (the pre-seat behavior), which is also what a standalone toolkit build gets.
212
+ */
213
+ queuedHumanInput?: () => boolean;
174
214
  /**
175
215
  * Mirrors {@link import("../../core/types.js").TaskSpec.oneShot}: this run has no later
176
216
  * turn for an async background notification to land in (a headless `-p` process exits once the
@@ -253,6 +293,11 @@ export declare function createBashReadonlyTool(env: ExecutionEnv, rootCanonical:
253
293
  * canonical). A read this deployment already sanctions for Read/Grep/Glob is equally sanctioned
254
294
  * here — the two faces must not disagree about which directories exist for this session. */
255
295
  additionalRoots?: readonly string[];
296
+ /** The run's tool-progress sink — the SAME seat the full shell leg takes, for the same reason its
297
+ * two neighbours above are shared: both foreground legs run through `runShell`, so a long command
298
+ * must breathe on both or a person watching one of them is told nothing. This face has no detach
299
+ * chain, so it ticks and never auto-backgrounds. Absent ⇒ no streaming callbacks, byte-unchanged. */
300
+ onToolProgress?: (body: import("../../core/runner/contracts.js").ToolProgressBody) => void;
256
301
  }): AgentTool;
257
302
  /**
258
303
  * `TaskOutput` — read a background shell's NEW output
@@ -1,7 +1,9 @@
1
+ import { randomBytes } from "node:crypto";
1
2
  import { Type } from "typebox";
2
3
  import { defineTool, errorResult } from "../../core/tools.js";
3
4
  import { defaultTaskRegistry } from "../../core/task-registry.js";
4
5
  import { accountDroppedBytes, spoolDropNote } from "../../core/task-registry-shared.js";
6
+ import { RollingTailBuffer } from "../../core/exec-output-tail.js";
5
7
  import { TASK_OUTPUT_TOOL_NAME, TASK_STOP_TOOL_NAME, TASK_OUTPUT_MISSING_ID_MESSAGE, TASK_STOP_MISSING_ID_MESSAGE, TASK_STOP_PARAMS, resolveTaskIdArg, ENV_DIRECT_TASK_TOOL_CAPS, composeTaskOutputDescription, composeTaskOutputParams, composeTaskStopDescription, } from "../../core/task-tool-shape.js";
6
8
  import { hasBackgroundShell } from "../../core/background-shell.js";
7
9
  import { delimitUntrusted } from "../../core/untrusted-text.js";
@@ -349,27 +351,142 @@ function msTimeoutToRequestedSec(timeoutMs) {
349
351
  return undefined;
350
352
  return Math.max(1, Math.round(timeoutMs / 1000));
351
353
  }
352
- async function runShell(env, cwd, toolName, command, timeoutSec, caps, signal, cwdRef, detach, readOnly, containmentRoots) {
354
+ const TOOL_PROGRESS_BOUND = Object.freeze({ intervalMs: 1_000, tailLines: 20, tailMaxBytes: 16 * 1024 });
355
+ function lastLines(text, n) {
356
+ const body = text.endsWith("\n") ? text.slice(0, -1) : text;
357
+ const lines = body.split("\n");
358
+ return lines.length <= n ? body : lines.slice(-n).join("\n");
359
+ }
360
+ const EPILOGUE_HOLD_MAX_CHARS = 4096;
361
+ function createEpilogueFilter(marker) {
362
+ let held = "";
363
+ let inside = false;
364
+ return (chunk) => {
365
+ let buf = held + chunk;
366
+ held = "";
367
+ let out = "";
368
+ for (;;) {
369
+ if (inside) {
370
+ const close = buf.indexOf(marker);
371
+ if (close === -1)
372
+ break;
373
+ buf = buf.slice(close + marker.length);
374
+ inside = false;
375
+ continue;
376
+ }
377
+ const open = buf.indexOf(marker);
378
+ if (open === -1)
379
+ break;
380
+ out += buf.slice(0, open);
381
+ buf = buf.slice(open + marker.length);
382
+ inside = true;
383
+ }
384
+ if (inside) {
385
+ if (buf.length > EPILOGUE_HOLD_MAX_CHARS) {
386
+ inside = false;
387
+ return out + marker + buf;
388
+ }
389
+ held = buf;
390
+ return out;
391
+ }
392
+ for (let n = Math.min(marker.length - 1, buf.length); n > 0; n--) {
393
+ if (buf.endsWith(marker.slice(0, n))) {
394
+ held = buf.slice(buf.length - n);
395
+ return out + buf.slice(0, buf.length - n);
396
+ }
397
+ }
398
+ return out + buf;
399
+ };
400
+ }
401
+ function readCwdEpilogue(stderr, marker) {
402
+ let out = "";
403
+ let rest = stderr;
404
+ let cwd;
405
+ for (;;) {
406
+ const open = rest.indexOf(marker);
407
+ if (open === -1)
408
+ break;
409
+ const close = rest.indexOf(marker, open + marker.length);
410
+ if (close === -1)
411
+ break;
412
+ out += rest.slice(0, open);
413
+ cwd = rest.slice(open + marker.length, close);
414
+ rest = rest.slice(close + marker.length);
415
+ }
416
+ return { stderr: out + rest, cwd };
417
+ }
418
+ function describeSafely(sink, body) {
419
+ if (sink === undefined)
420
+ return;
421
+ try {
422
+ const returned = sink(body);
423
+ if (typeof returned?.then === "function")
424
+ void returned.then(undefined, () => { });
425
+ }
426
+ catch {
427
+ }
428
+ }
429
+ function startLivenessTicks(onTick, filterStderr) {
430
+ const tail = new RollingTailBuffer(TOOL_PROGRESS_BOUND.tailMaxBytes);
431
+ let totalBytes = 0;
432
+ let totalLines = 0;
433
+ let cached;
434
+ const feed = (chunk) => {
435
+ if (chunk === "")
436
+ return;
437
+ const bytes = Buffer.from(chunk, "utf8");
438
+ totalBytes += bytes.length;
439
+ totalLines += chunk.split("\n").length - 1;
440
+ tail.push(bytes);
441
+ };
442
+ const describeTo = (t) => describeSafely(onTick, t);
443
+ const tick = () => {
444
+ if (totalBytes === 0) {
445
+ describeTo({});
446
+ return;
447
+ }
448
+ if (cached?.atBytes !== totalBytes)
449
+ cached = { atBytes: totalBytes, text: lastLines(tail.result().text, TOOL_PROGRESS_BOUND.tailLines) };
450
+ describeTo({ output: cached.text, totalLines, totalBytes });
451
+ };
452
+ const timer = setInterval(tick, TOOL_PROGRESS_BOUND.intervalMs);
453
+ timer.unref?.();
454
+ return {
455
+ onStdout: feed,
456
+ onStderr: filterStderr === undefined ? feed : (chunk) => feed(filterStderr(chunk)),
457
+ stop: () => clearInterval(timer),
458
+ };
459
+ }
460
+ async function runShell(env, cwd, toolName, command, timeoutSec, caps, signal, cwdRef, detach, readOnly, containmentRoots, onTick) {
353
461
  const requestedSec = Math.max(1, Math.floor(timeoutSec ?? caps.defaultSec));
354
462
  let timeout = Math.min(caps.maxSec, requestedSec);
355
463
  const cappedByMaxTimeout = requestedSec > caps.maxSec;
356
464
  const effectiveCwd = cwdRef ? cwdRef.current : cwd;
465
+ const cwdMarker = cwdRef ? `${CWD_SENTINEL}${randomBytes(8).toString("hex")}` : CWD_SENTINEL;
357
466
  const toRun = cwdRef
358
- ? `${command}\n__cc_rc=$?; printf '%s%s%s' '${CWD_SENTINEL}' "$(pwd)" '${CWD_SENTINEL}' 1>&2; exit $__cc_rc`
467
+ ? `${command}\n__cc_rc=$?\n{ set +vx; } 2>/dev/null\nprintf '%s%s%s' '${cwdMarker}' "$(pwd)" '${cwdMarker}' 1>&2\nexit $__cc_rc`
359
468
  : command;
360
469
  const execEnv = cwdRef ? { env: { PWD: effectiveCwd } } : undefined;
361
- const res = await env.exec(toRun, {
362
- cwd: effectiveCwd,
363
- timeout,
364
- abortSignal: signal,
365
- ...execEnv,
366
- ...(detach
367
- ? {
368
- detachSignal: detach.signal,
369
- autoBackgroundOnTimeout: detach.autoBackgroundEligible === true,
370
- }
371
- : {}),
372
- });
470
+ const live = onTick === undefined ? undefined : startLivenessTicks(onTick, cwdRef ? createEpilogueFilter(cwdMarker) : undefined);
471
+ let res;
472
+ try {
473
+ res = await env.exec(toRun, {
474
+ cwd: effectiveCwd,
475
+ timeout,
476
+ abortSignal: signal,
477
+ ...execEnv,
478
+ ...(live !== undefined ? { onStdout: live.onStdout, onStderr: live.onStderr } : {}),
479
+ ...(detach
480
+ ? {
481
+ detachSignal: detach.signal,
482
+ autoBackgroundOnTimeout: detach.autoBackgroundEligible === true,
483
+ }
484
+ : {}),
485
+ });
486
+ }
487
+ finally {
488
+ live?.stop();
489
+ }
373
490
  if (!res.ok) {
374
491
  if (res.error.code === "spawn_error" || res.error.code === "shell_unavailable") {
375
492
  const diagnosis = await diagnoseDeadCwd(env, toolName, effectiveCwd, cwd, cwdRef);
@@ -461,10 +578,10 @@ async function runShell(env, cwd, toolName, command, timeoutSec, caps, signal, c
461
578
  let { stderr } = res.value;
462
579
  const { stdout, exitCode } = res.value;
463
580
  if (cwdRef) {
464
- const m = stderr.match(new RegExp(`${CWD_SENTINEL}([\\s\\S]*?)${CWD_SENTINEL}`));
465
- if (m) {
466
- stderr = stderr.replace(m[0], "");
467
- const captured = m[1].trim();
581
+ const epilogue = readCwdEpilogue(stderr, cwdMarker);
582
+ stderr = epilogue.stderr;
583
+ if (epilogue.cwd !== undefined) {
584
+ const captured = epilogue.cwd.trim();
468
585
  const windowsWorld = /^[A-Za-z]:[\\/]/.test(cwdRef.current);
469
586
  const msys = /^\/([A-Za-z])(\/.*)?$/.exec(captured);
470
587
  if (/^[A-Za-z]:[\\/]/.test(captured))
@@ -756,6 +873,22 @@ export function createBashTool(env, rootCanonical, coAuthor = false, cwdRef = {
756
873
  }
757
874
  const detachHub = taskOpts.detachHub;
758
875
  const detachRegistry = taskOpts.taskRegistry;
876
+ const autoBackgroundArmed = taskOpts.autoBackgroundOnTimeout === true && canAutoBackground(command);
877
+ let backgroundedForQueuedMessage = false;
878
+ const wantsTicks = taskOpts.onToolProgress !== undefined || (autoBackgroundArmed && detachHub !== undefined && taskOpts.queuedHumanInput !== undefined);
879
+ const onTick = !wantsTicks
880
+ ? undefined
881
+ : (t) => {
882
+ describeSafely(taskOpts.onToolProgress, { toolCallId: ctx.toolCallId, toolName: "Bash", ...t });
883
+ if (!autoBackgroundArmed || backgroundedForQueuedMessage || detachHub === undefined)
884
+ return;
885
+ if (ctx.signal?.aborted === true)
886
+ return;
887
+ if (taskOpts.queuedHumanInput?.() !== true)
888
+ return;
889
+ backgroundedForQueuedMessage = true;
890
+ detachHub.request(ctx.toolCallId);
891
+ };
759
892
  const detachEnv = hasBackgroundShell(env) ? env : undefined;
760
893
  const detachChain = detachHub !== undefined && detachRegistry !== undefined && detachEnv !== undefined && detachEnv.backgroundCapabilities.supportsDetach === true
761
894
  ? {
@@ -804,7 +937,9 @@ export function createBashTool(env, rootCanonical, coAuthor = false, cwdRef = {
804
937
  return {
805
938
  content: (cause === "timeout"
806
939
  ? `Command exceeded its timeout but is still running — moved to background instead of being killed (task_id=${taskId}). `
807
- : `Command moved to background; task_id=${taskId}. `) +
940
+ : backgroundedForQueuedMessage
941
+ ? `Command is still running and was moved to the background so a queued message could be delivered without waiting for it (task_id=${taskId}). It was NOT stopped and nothing about it failed. `
942
+ : `Command moved to background; task_id=${taskId}. `) +
808
943
  (outputFile !== undefined
809
944
  ? `Output file: ${outputFile} (full output is appended there — Read it any time). `
810
945
  : `Use TaskOutput("${taskId}") to read its output. `) +
@@ -818,7 +953,7 @@ export function createBashTool(env, rootCanonical, coAuthor = false, cwdRef = {
818
953
  adoptBudgetNote +
819
954
  backgroundLifetimeNote(adoptCaps, bgSessionScoped, bgEnvIsolatedOwned) +
820
955
  (tail ? `\n--- output so far (tail) ---\n${tail}` : ""),
821
- details: { type: "bash", detached: true, task_id: taskId, ...(description !== undefined ? { description } : {}), ...(outputFile !== undefined ? { output_file: outputFile } : {}), ...(cause === "timeout" ? { autoBackgrounded: true } : {}) },
956
+ details: { type: "bash", detached: true, task_id: taskId, ...(description !== undefined ? { description } : {}), ...(outputFile !== undefined ? { output_file: outputFile } : {}), ...(cause === "timeout" || backgroundedForQueuedMessage ? { autoBackgrounded: true } : {}) },
822
957
  };
823
958
  },
824
959
  autoBackgroundEligible: taskOpts.autoBackgroundOnTimeout === true && canAutoBackground(command),
@@ -826,7 +961,7 @@ export function createBashTool(env, rootCanonical, coAuthor = false, cwdRef = {
826
961
  : undefined;
827
962
  let res;
828
963
  try {
829
- res = await runShell(env, rootCanonical, "Bash", command, msTimeoutToRequestedSec(timeout), timeoutCapsSecView, ctx.signal, cwdRef, detachChain, false, [rootCanonical, ...(taskOpts.additionalRoots ?? [])]);
964
+ res = await runShell(env, rootCanonical, "Bash", command, msTimeoutToRequestedSec(timeout), timeoutCapsSecView, ctx.signal, cwdRef, detachChain, false, [rootCanonical, ...(taskOpts.additionalRoots ?? [])], onTick);
830
965
  }
831
966
  finally {
832
967
  taskOpts.detachHub?.gc(ctx.toolCallId);
@@ -1210,7 +1345,8 @@ export function createBashReadonlyTool(env, rootCanonical, allow, opts) {
1210
1345
  const escape = await canonicalEscapeRefusal(canonicalBoundary, { literal: [...(boundary.checkedPaths ?? []), ...(boundary.checkedSpellings ?? [])], patterns: [...(boundary.undecidedPaths ?? []), ...(boundary.undecidedSpellings ?? [])] }, readRoots, READONLY_FACE_ESCAPE_PROSE, ctx.signal);
1211
1346
  if (escape !== undefined)
1212
1347
  return escape;
1213
- return await runShell(env, rootCanonical, "Bash", command, msTimeoutToRequestedSec(timeout), timeoutCapsSecView, ctx.signal, undefined, undefined, true);
1348
+ const readOnlyTick = opts?.onToolProgress === undefined ? undefined : (t) => describeSafely(opts.onToolProgress, { toolCallId: ctx.toolCallId, toolName: "Bash", ...t });
1349
+ return await runShell(env, rootCanonical, "Bash", command, msTimeoutToRequestedSec(timeout), timeoutCapsSecView, ctx.signal, undefined, undefined, true, undefined, readOnlyTick);
1214
1350
  },
1215
1351
  });
1216
1352
  }
@@ -6,4 +6,4 @@ import { type ReminderDisclosureCounts } from "../../core/reminder-disclosure.js
6
6
  export declare function createReadFileTool(env: ExecutionEnv, state: ReadFileState, rootCanonical: string, cwdRef?: CwdRef, additionalRoots?: readonly string[], imageDownsampler?: ReadImageDownsamplerOption, pdfCapabilities?: PdfModelCapabilities, bgOutputReadExemption?: (canonicalKey: string, ctx: {
7
7
  taskId?: string;
8
8
  principal?: string;
9
- }) => boolean, readCyberReminder?: boolean, readDeny?: import("./read-deny.js").ReadDenyMatcher, readFace?: import("./read-face.js").ReadFace, reminderMark?: string, reminderDisclosureCounts?: ReminderDisclosureCounts): AgentTool;
9
+ }) => boolean, readCyberReminder?: boolean, readDeny?: import("./read-deny.js").ReadDenyMatcher, readFace?: import("./containment-face.js").ContainmentFace, reminderMark?: string, reminderDisclosureCounts?: ReminderDisclosureCounts): AgentTool;
@@ -1,6 +1,6 @@
1
1
  import type { AgentTool, ExecutionEnv } from "../../internal/harness-types.js";
2
2
  import type { ReadDenyMatcher } from "./read-deny.js";
3
- import type { ReadFace } from "./read-face.js";
3
+ import type { ContainmentFace } from "./containment-face.js";
4
4
  import type { CwdRef } from "./fs-shared.js";
5
5
  /**
6
6
  * The Grep card's details assembly, PURE over the engine text ({mode, offset} from the request) —
@@ -11,5 +11,5 @@ import type { CwdRef } from "./fs-shared.js";
11
11
  * honesty boundary.
12
12
  */
13
13
  export declare function grepDetailFields(text: string, mode: "files_with_matches" | "content" | "count", offset?: number, structuredRows?: readonly import("./search.js").GrepRow[]): Record<string, unknown>;
14
- export declare function createGrepTool(env: ExecutionEnv, rootCanonical: string, additionalRoots?: readonly string[], readDeny?: ReadDenyMatcher, readFace?: ReadFace, cwdRef?: CwdRef): AgentTool;
15
- export declare function createGlobTool(env: ExecutionEnv, rootCanonical: string, additionalRoots?: readonly string[], readDeny?: ReadDenyMatcher, readFace?: ReadFace, cwdRef?: CwdRef): AgentTool;
14
+ export declare function createGrepTool(env: ExecutionEnv, rootCanonical: string, additionalRoots?: readonly string[], readDeny?: ReadDenyMatcher, readFace?: ContainmentFace, cwdRef?: CwdRef): AgentTool;
15
+ export declare function createGlobTool(env: ExecutionEnv, rootCanonical: string, additionalRoots?: readonly string[], readDeny?: ReadDenyMatcher, readFace?: ContainmentFace, cwdRef?: CwdRef): AgentTool;
@@ -1,6 +1,7 @@
1
1
  import type { AgentTool, ExecutionEnv } from "../../internal/harness-types.js";
2
2
  import type { BeforeWriteHook, FileEditedHook, TrackFileEditHook } from "../../core/types.js";
3
3
  import { type ReadFileState } from "./safety.js";
4
+ import type { ContainmentFace } from "./containment-face.js";
4
5
  import { type CwdRef } from "./fs-shared.js";
5
6
  /**
6
7
  * The `beforeWrite` CONTENT hook, mounted here rather than in ToolPolicy because only this band sees
@@ -12,8 +13,8 @@ import { type CwdRef } from "./fs-shared.js";
12
13
  * Absent hook ⇒ byte-identical behavior.
13
14
  */
14
15
  export type { BeforeWriteRequest, BeforeWriteResult, BeforeWriteHook, TrackEditRequest, TrackEditResult, TrackFileEditHook } from "../../core/types.js";
15
- export declare function createEditFileTool(env: ExecutionEnv, state: ReadFileState, rootCanonical: string, cwdRef?: CwdRef, additionalRoots?: readonly string[], beforeWrite?: BeforeWriteHook, trackEdit?: TrackFileEditHook, onEdited?: FileEditedHook): AgentTool;
16
- export declare function createWriteFileTool(env: ExecutionEnv, state: ReadFileState, rootCanonical: string, cwdRef?: CwdRef, additionalRoots?: readonly string[], beforeWrite?: BeforeWriteHook, trackEdit?: TrackFileEditHook, onEdited?: FileEditedHook): AgentTool;
16
+ export declare function createEditFileTool(env: ExecutionEnv, state: ReadFileState, rootCanonical: string, cwdRef?: CwdRef, additionalRoots?: readonly string[], beforeWrite?: BeforeWriteHook, trackEdit?: TrackFileEditHook, onEdited?: FileEditedHook, writeFace?: ContainmentFace): AgentTool;
17
+ export declare function createWriteFileTool(env: ExecutionEnv, state: ReadFileState, rootCanonical: string, cwdRef?: CwdRef, additionalRoots?: readonly string[], beforeWrite?: BeforeWriteHook, trackEdit?: TrackFileEditHook, onEdited?: FileEditedHook, writeFace?: ContainmentFace): AgentTool;
17
18
  /**
18
19
  * design v1.163 — NotebookEdit: replace/insert/delete a single cell in a .ipynb. CC-parity tool over the SAME hand-band
19
20
  * safety skeleton as Edit/Write (resolveKey containment → requireRead read-before-edit → checkStale content-hash
@@ -21,4 +22,4 @@ export declare function createWriteFileTool(env: ExecutionEnv, state: ReadFileSt
21
22
  * .ipynb as the cell projection but records read state (hash/totalLines) in the RAW notebook-text
22
23
  * coordinate — that raw-coordinate read record is what this tool's freshness check depends on.
23
24
  */
24
- export declare function createNotebookEditTool(env: ExecutionEnv, state: ReadFileState, rootCanonical: string, cwdRef?: CwdRef, additionalRoots?: readonly string[], beforeWrite?: BeforeWriteHook, trackEdit?: TrackFileEditHook, onEdited?: FileEditedHook): AgentTool;
25
+ export declare function createNotebookEditTool(env: ExecutionEnv, state: ReadFileState, rootCanonical: string, cwdRef?: CwdRef, additionalRoots?: readonly string[], beforeWrite?: BeforeWriteHook, trackEdit?: TrackFileEditHook, onEdited?: FileEditedHook, writeFace?: ContainmentFace): AgentTool;
@@ -86,7 +86,7 @@ async function gateToolWrite(hook, tool, path, key, content) {
86
86
  return `Error (${tool}): write rejected by the write gate (${res.code}): ${clipHookText(res.reason)}`;
87
87
  return undefined;
88
88
  }
89
- export function createEditFileTool(env, state, rootCanonical, cwdRef, additionalRoots, beforeWrite, trackEdit, onEdited) {
89
+ export function createEditFileTool(env, state, rootCanonical, cwdRef, additionalRoots, beforeWrite, trackEdit, onEdited, writeFace) {
90
90
  return defineTool({
91
91
  name: "Edit",
92
92
  ...toolFace("edit"),
@@ -111,7 +111,7 @@ export function createEditFileTool(env, state, rootCanonical, cwdRef, additional
111
111
  const path = fileArgPath(args);
112
112
  if (path === undefined)
113
113
  return { ok: true };
114
- const r = await resolveKey(env, rootCanonical, path, ctx.signal, cwdRef?.current, additionalRoots);
114
+ const r = await resolveKey(env, rootCanonical, path, ctx.signal, cwdRef?.current, additionalRoots, undefined, undefined, writeFace);
115
115
  if (!r.ok)
116
116
  return { ok: true };
117
117
  if (a.old_string === a.new_string)
@@ -149,7 +149,7 @@ export function createEditFileTool(env, state, rootCanonical, cwdRef, additional
149
149
  const path = fileArgPath(args);
150
150
  if (path === undefined)
151
151
  return errorResult(`Error (Edit): file_path is required.`);
152
- const r = await resolveKey(env, rootCanonical, path, ctx.signal, cwdRef?.current, additionalRoots);
152
+ const r = await resolveKey(env, rootCanonical, path, ctx.signal, cwdRef?.current, additionalRoots, undefined, undefined, writeFace);
153
153
  if (!r.ok)
154
154
  return errorResult(violationText("Edit", r.violation), violationDetails(r.violation));
155
155
  if (!batch && a.old_string === a.new_string) {
@@ -324,7 +324,7 @@ export function createEditFileTool(env, state, rootCanonical, cwdRef, additional
324
324
  },
325
325
  });
326
326
  }
327
- export function createWriteFileTool(env, state, rootCanonical, cwdRef, additionalRoots, beforeWrite, trackEdit, onEdited) {
327
+ export function createWriteFileTool(env, state, rootCanonical, cwdRef, additionalRoots, beforeWrite, trackEdit, onEdited, writeFace) {
328
328
  return defineTool({
329
329
  name: "Write",
330
330
  ...toolFace("write"),
@@ -343,7 +343,7 @@ export function createWriteFileTool(env, state, rootCanonical, cwdRef, additiona
343
343
  return { ok: true };
344
344
  if (ipynbRedirect("Write", path))
345
345
  return { ok: true };
346
- const r = await resolveKey(env, rootCanonical, path, ctx.signal, cwdRef?.current, additionalRoots);
346
+ const r = await resolveKey(env, rootCanonical, path, ctx.signal, cwdRef?.current, additionalRoots, undefined, undefined, writeFace);
347
347
  if (!r.ok)
348
348
  return { ok: true };
349
349
  const exists = await env.exists(r.key, ctx.signal);
@@ -362,7 +362,7 @@ export function createWriteFileTool(env, state, rootCanonical, cwdRef, additiona
362
362
  const ipynb = ipynbRedirect("Write", path);
363
363
  if (ipynb)
364
364
  return errorResult(ipynb);
365
- const r = await resolveKey(env, rootCanonical, path, ctx.signal, cwdRef?.current, additionalRoots);
365
+ const r = await resolveKey(env, rootCanonical, path, ctx.signal, cwdRef?.current, additionalRoots, undefined, undefined, writeFace);
366
366
  if (!r.ok)
367
367
  return errorResult(violationText("Write", r.violation), violationDetails(r.violation));
368
368
  const exists = await env.exists(r.key, ctx.signal);
@@ -423,7 +423,7 @@ export function createWriteFileTool(env, state, rootCanonical, cwdRef, additiona
423
423
  const notebookNotIpynbMessage = (notebookPath) => `Error (NotebookEdit): "${notebookPath}" is not a .ipynb file; use Edit for other file types.`;
424
424
  const NOTEBOOK_CELL_TYPE_REQUIRED_MESSAGE = `Error (NotebookEdit): Cell type is required when using edit_mode=insert.`;
425
425
  const NOTEBOOK_CELL_ID_REQUIRED_MESSAGE = `Error (NotebookEdit): cell_id is required for replace/delete.`;
426
- export function createNotebookEditTool(env, state, rootCanonical, cwdRef, additionalRoots, beforeWrite, trackEdit, onEdited) {
426
+ export function createNotebookEditTool(env, state, rootCanonical, cwdRef, additionalRoots, beforeWrite, trackEdit, onEdited, writeFace) {
427
427
  return defineTool({
428
428
  name: "NotebookEdit",
429
429
  ...toolFace("notebook-edit"),
@@ -457,7 +457,7 @@ export function createNotebookEditTool(env, state, rootCanonical, cwdRef, additi
457
457
  return { ok: false, code: "invalid", message: NOTEBOOK_CELL_TYPE_REQUIRED_MESSAGE };
458
458
  if (mode !== "insert" && !a.cell_id)
459
459
  return { ok: false, code: "invalid", message: NOTEBOOK_CELL_ID_REQUIRED_MESSAGE };
460
- const r = await resolveKey(env, rootCanonical, a.notebook_path, ctx.signal, cwdRef?.current, additionalRoots);
460
+ const r = await resolveKey(env, rootCanonical, a.notebook_path, ctx.signal, cwdRef?.current, additionalRoots, undefined, undefined, writeFace);
461
461
  if (!r.ok)
462
462
  return { ok: true };
463
463
  const notRead = requireRead(state, r.key);
@@ -477,7 +477,7 @@ export function createNotebookEditTool(env, state, rootCanonical, cwdRef, additi
477
477
  return errorResult(NOTEBOOK_CELL_TYPE_REQUIRED_MESSAGE);
478
478
  if (mode !== "insert" && !cell_id)
479
479
  return errorResult(NOTEBOOK_CELL_ID_REQUIRED_MESSAGE);
480
- const r = await resolveKey(env, rootCanonical, notebook_path, ctx.signal, cwdRef?.current, additionalRoots);
480
+ const r = await resolveKey(env, rootCanonical, notebook_path, ctx.signal, cwdRef?.current, additionalRoots, undefined, undefined, writeFace);
481
481
  if (!r.ok)
482
482
  return errorResult(violationText("NotebookEdit", r.violation), violationDetails(r.violation));
483
483
  const notRead = requireRead(state, r.key);