@sema-agent/core 7.4.0 → 7.5.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. package/CHANGELOG.md +69 -0
  2. package/dist/agents/cascade.d.ts +2 -1
  3. package/dist/agents/peer-notices.d.ts +11 -1
  4. package/dist/agents/peer-session-drain.js +2 -0
  5. package/dist/agents/verify.d.ts +2 -1
  6. package/dist/core/ask-origin.d.ts +130 -0
  7. package/dist/core/ask-origin.js +35 -0
  8. package/dist/core/auto-mode-arming.d.ts +40 -1
  9. package/dist/core/auto-mode-arming.js +51 -3
  10. package/dist/core/auto-mode.d.ts +87 -10
  11. package/dist/core/auto-mode.js +34 -6
  12. package/dist/core/checkpoint-store.d.ts +41 -6
  13. package/dist/core/checkpoint-store.js +8 -0
  14. package/dist/core/hooks.d.ts +63 -19
  15. package/dist/core/hooks.js +37 -16
  16. package/dist/core/mcp.d.ts +47 -55
  17. package/dist/core/mcp.js +89 -31
  18. package/dist/core/park-selfcheck.js +3 -0
  19. package/dist/core/permission-rule-consent.d.ts +2 -11
  20. package/dist/core/permission-rule-consent.js +13 -62
  21. package/dist/core/permission-rule-org.d.ts +70 -54
  22. package/dist/core/permission-rule-org.js +47 -61
  23. package/dist/core/permission-rule-provider.d.ts +162 -0
  24. package/dist/core/permission-rule-provider.js +148 -0
  25. package/dist/core/permission-rule-session.d.ts +23 -19
  26. package/dist/core/permission-rule-session.js +5 -5
  27. package/dist/core/permission-rule-store.d.ts +46 -21
  28. package/dist/core/permission-rule-store.js +13 -6
  29. package/dist/core/permission-rule-sync.d.ts +2 -1
  30. package/dist/core/permission-rule-sync.js +11 -0
  31. package/dist/core/runner/assemble-result.d.ts +3 -2
  32. package/dist/core/runner/checkpoint-scope.d.ts +32 -0
  33. package/dist/core/runner/checkpoint-scope.js +4 -0
  34. package/dist/core/runner/contracts.d.ts +1878 -0
  35. package/dist/core/runner/contracts.js +1 -0
  36. package/dist/core/runner/denial-limit-arms.d.ts +57 -31
  37. package/dist/core/runner/denial-limit-arms.js +42 -17
  38. package/dist/core/runner/derived-route-fallback.d.ts +34 -0
  39. package/dist/core/runner/derived-route-fallback.js +16 -0
  40. package/dist/core/runner/prepare-acquire-reconcile.d.ts +1 -1
  41. package/dist/core/runner/prepare-announce-once.d.ts +83 -0
  42. package/dist/core/runner/prepare-announce-once.js +105 -0
  43. package/dist/core/runner/prepare-caps-and-workflow.d.ts +170 -0
  44. package/dist/core/runner/prepare-caps-and-workflow.js +255 -0
  45. package/dist/core/runner/prepare-config-doors.d.ts +2 -10
  46. package/dist/core/runner/prepare-defer-classify.d.ts +86 -0
  47. package/dist/core/runner/prepare-defer-classify.js +107 -0
  48. package/dist/core/runner/prepare-delegation-surface.d.ts +104 -0
  49. package/dist/core/runner/prepare-delegation-surface.js +144 -0
  50. package/dist/core/runner/prepare-execution-env.d.ts +54 -0
  51. package/dist/core/runner/prepare-execution-env.js +86 -0
  52. package/dist/core/runner/prepare-file-history.d.ts +95 -0
  53. package/dist/core/runner/prepare-file-history.js +383 -0
  54. package/dist/core/runner/prepare-hands-readface.d.ts +6 -8
  55. package/dist/core/runner/prepare-hands-readface.js +3 -3
  56. package/dist/core/runner/prepare-inherited-gate.d.ts +268 -0
  57. package/dist/core/runner/prepare-inherited-gate.js +266 -0
  58. package/dist/core/runner/prepare-listings.d.ts +77 -0
  59. package/dist/core/runner/prepare-listings.js +76 -0
  60. package/dist/core/runner/prepare-lsp.d.ts +55 -0
  61. package/dist/core/runner/prepare-lsp.js +27 -0
  62. package/dist/core/runner/prepare-memory.d.ts +1 -1
  63. package/dist/core/runner/prepare-offload-wrappers.d.ts +62 -0
  64. package/dist/core/runner/prepare-offload-wrappers.js +45 -0
  65. package/dist/core/runner/prepare-permission-rules.d.ts +132 -0
  66. package/dist/core/runner/prepare-permission-rules.js +140 -0
  67. package/dist/core/runner/prepare-project-context.d.ts +131 -0
  68. package/dist/core/runner/prepare-project-context.js +150 -0
  69. package/dist/core/runner/prepare-prompt-inputs.d.ts +138 -0
  70. package/dist/core/runner/prepare-prompt-inputs.js +141 -0
  71. package/dist/core/runner/prepare-protocol-tools.d.ts +91 -0
  72. package/dist/core/runner/prepare-protocol-tools.js +182 -0
  73. package/dist/core/runner/prepare-question-face.d.ts +119 -0
  74. package/dist/core/runner/prepare-question-face.js +83 -0
  75. package/dist/core/runner/prepare-run-refs.d.ts +89 -0
  76. package/dist/core/runner/prepare-run-refs.js +39 -0
  77. package/dist/core/runner/prepare-safety-scan.d.ts +3 -2
  78. package/dist/core/runner/prepare-task.d.ts +11 -1815
  79. package/dist/core/runner/prepare-task.js +138 -2542
  80. package/dist/core/runner/prepare-tool-disclosure-mount.d.ts +111 -0
  81. package/dist/core/runner/prepare-tool-disclosure-mount.js +219 -0
  82. package/dist/core/runner/prepare-wiring-manifest.d.ts +184 -0
  83. package/dist/core/runner/prepare-wiring-manifest.js +240 -0
  84. package/dist/core/runner/prepare-workspace-restore.d.ts +1 -27
  85. package/dist/core/runner/prepare-workspace-restore.js +1 -22
  86. package/dist/core/runner/rollback-stack.d.ts +32 -0
  87. package/dist/core/runner/rollback-stack.js +30 -0
  88. package/dist/core/runner/runtask.d.ts +11 -2
  89. package/dist/core/runner/runtask.js +27 -9
  90. package/dist/core/runner/workspace-path.d.ts +33 -0
  91. package/dist/core/runner/workspace-path.js +22 -0
  92. package/dist/core/sensitive-path-policy.d.ts +16 -0
  93. package/dist/core/sensitive-path-policy.js +1 -1
  94. package/dist/core/tool-policy.d.ts +57 -9
  95. package/dist/core/tool-policy.js +11 -0
  96. package/dist/core/types.d.ts +63 -51
  97. package/dist/core/wiring-manifest.d.ts +40 -3
  98. package/dist/core/wiring-manifest.js +4 -3
  99. package/dist/core/write-protect.d.ts +13 -2
  100. package/dist/core/write-protect.js +58 -29
  101. package/dist/engine/harness/types.d.ts +38 -16
  102. package/dist/engine/harness/types.js +25 -1
  103. package/dist/engine/session/session.d.ts +3 -11
  104. package/dist/index.d.ts +10 -6
  105. package/dist/index.js +9 -5
  106. package/dist/internal/harness.d.ts +1 -0
  107. package/dist/stores/file/adoption/adopt.d.ts +1 -1
  108. package/dist/stores/file/adoption/marker.d.ts +1 -1
  109. package/dist/stores/file/permission-rule-adopt.js +4 -3
  110. package/dist/stores/file/permission-rule-store.d.ts +65 -25
  111. package/dist/stores/file/permission-rule-store.js +215 -37
  112. package/dist/stores/file/task-list-store.d.ts +1 -1
  113. package/dist/tools/fs/read-face.d.ts +1 -1
  114. package/package.json +8 -2
  115. package/test/export-surface.snapshot.json +76 -28
@@ -0,0 +1,91 @@
1
+ import type { AgentTool } from "../../internal/harness.js";
2
+ import { type MaterializedMcp, type McpDroppedTool } from "../mcp.js";
3
+ import { type MaterializedA2a } from "../a2a.js";
4
+ import type { LockedPreflight } from "../locked-config.js";
5
+ import type { RunnerDeps, TaskSpec, ToolEffect } from "../types.js";
6
+ import type { AnnounceOnceSink, IrreversibilityTier, Prepared, RollbackStack, ToolFaceSnapshot } from "./contracts.js";
7
+ export interface PrepareProtocolToolsInput {
8
+ /** borrowed-readonly — the frozen preflight; only `mcp` (the declared server entries) is read: the
9
+ * materialize input, the RefreshMcpTools mount predicate, and whether a real client was acquired. */
10
+ lockedPreflight: Pick<LockedPreflight, "mcp">;
11
+ /** borrowed-readonly — the REBOUND spec. Read: `principal` (the MCP header / the A2A identity), `tools`
12
+ * (the caller names both reserved-name guards check against), `a2a` (the peer entries). Never mutated. */
13
+ spec: Pick<TaskSpec, "principal" | "tools" | "a2a">;
14
+ /** borrowed-readonly — the deployment seats the materializers and their operator faces read: `onElicit`
15
+ * and `mcpImageResizer` ride into the MCP intake; `onError` takes the A2A warnings (no ledger — a peer is
16
+ * re-read every prepare); `mcpRevocations` + `onNotice` make the revocation wiring. */
17
+ deps: Pick<RunnerDeps, "onElicit" | "mcpImageResizer" | "onError" | "mcpRevocations" | "onNotice">;
18
+ /** borrowed-readonly — the leg's reminder mark (the MCP intake's disclosure bounding reads it). */
19
+ reminderMark: string;
20
+ /** borrowed-mutable — the reminder-disclosure counters: the MCP intake's bounding writes them here, and
21
+ * the run loop's attachment lanes keep writing the same object (`Prepared.reminderDisclosureCounts`). */
22
+ reminderDisclosureCounts: Prepared["reminderDisclosureCounts"];
23
+ /** borrowed-readonly — the run's invocation id (the revocation probe-failure notice names it). */
24
+ runId: string;
25
+ /** borrowed-readonly — the acquired session id (operator-line context). */
26
+ sessionId: string;
27
+ /** borrowed-mutable — the once-per-session ledger: its deduplicated operator sink (`onError`, one line per
28
+ * (code, message) per session) and the announce-lane prune of tools already delivered on this branch.
29
+ * The ledger module owns the record; this phase writes through those two methods and nothing else. */
30
+ onceLedger: AnnounceOnceSink & {
31
+ pruneDroppedTools(dropped: McpDroppedTool[]): void;
32
+ };
33
+ /** borrowed-mutable — the run's shared roster (identity is the contract: `Prepared.tools`, the wire order).
34
+ * Writers here: three pushes (the MCP tools through the wrapper, RefreshMcpTools, the A2A tools through the
35
+ * wrapper) and, at RUN time, the refresh closure's splice within a server's name-prefix domain. Later
36
+ * phases push their own mounts; the roster's full writer table is the delegation-surface phase's. */
37
+ tools: AgentTool[];
38
+ /** borrowed-readonly — the large-result wrapper for protocol tools (per-tool `_meta` bar, 50K default). */
39
+ remoteToolOffload: (tool: AgentTool) => AgentTool;
40
+ /** borrowed-readonly — the frozen tool face; `exclude` is read at REFRESH time so a deployment-excluded
41
+ * tool is never re-mounted by a model-triggered refresh. */
42
+ toolFaceSnapshot: Pick<ToolFaceSnapshot, "exclude">;
43
+ /** borrowed-mutable — the effect table the fold writes under a has-guard (`?? "write"` default) and the
44
+ * refresh closure resets per prefix domain before re-folding; RefreshMcpTools registers itself `read`. */
45
+ toolEffects: Map<string, ToolEffect>;
46
+ /** borrowed-mutable — the explicit-negative axes (a caller's vouched `never` / `egress:false`); written by
47
+ * the fold and reset per prefix domain by the refresh closure. */
48
+ axisExplicitNegatives: Map<string, {
49
+ irreversible?: false;
50
+ egress?: false;
51
+ }>;
52
+ /** borrowed-mutable — the irreversibility tier, tightened to `always` by the fold (monotone). */
53
+ irreversibilityTier: IrreversibilityTier;
54
+ /** borrowed-mutable — the irreversible set, added to by the fold (tighten-only). */
55
+ irreversibleTools: Set<string>;
56
+ /** borrowed-mutable — the egress set, added to by the fold (tighten-only; the effect:"write" invariant). */
57
+ egressTools: Set<string>;
58
+ /** borrowed-readonly — the run's abort controller; its `signal` rides into the A2A materialization. */
59
+ abortController: Pick<AbortController, "signal">;
60
+ /** borrowed-mutable — the driver-owned prepare-failure stack; this phase pushes one entry per REAL handle
61
+ * acquired (the MCP clients, then the A2A peers) and nothing else touches it here. */
62
+ rollback: RollbackStack;
63
+ }
64
+ export interface PrepareProtocolToolsResult {
65
+ /** owned — the materialized MCP handle (`Prepared.mcp`): clients, axes, server instructions, statuses, the
66
+ * refresh and the `dispose` the Runner's finish calls. Its prepare-failure release is the rollback entry
67
+ * this phase pushed (only when a real client was acquired — the inert stub owns nothing). */
68
+ mcp: MaterializedMcp;
69
+ /** owned — the materialized A2A handle: the driver projects it onto `Prepared.a2a` only when it carries
70
+ * tools. Same rollback posture as `mcp`, pushed after it (disposed before it on unwind). */
71
+ a2a: MaterializedA2a;
72
+ /** borrowed-mutable — the RefreshMcpTools rebuild seam (`current` undefined until installed). Writers: the
73
+ * tool-disclosure mount, by exactly one of its two mutually exclusive arms (the deferred arm's
74
+ * rematerialize; the else-arm's bare setTools + fingerprint sync). Read by the refresh closure at run time. */
75
+ rebuildHarnessToolsRef: {
76
+ current?: () => Promise<void>;
77
+ };
78
+ /** borrowed-mutable — the content-origin re-classification seat. Writer: the content-origin wrap section
79
+ * (installs the re-runnable pass ONCE, when an engine-memory session mounted; undefined otherwise). Read by
80
+ * the rebuild closures before a swapped-in tool becomes callable. */
81
+ contentOriginWrapRef: {
82
+ current?: () => void;
83
+ };
84
+ /** borrowed-mutable — whether the tool_call gate handler was REGISTERED at prepare time. Writer: the driver,
85
+ * once, at gate registration. Read by the refresh closure's honesty warning. */
86
+ toolCallGateArmedRef: {
87
+ armed: boolean;
88
+ };
89
+ }
90
+ /** The M5 phase body — prepareTask's protocol-tools stretch, verbatim (see the module header). */
91
+ export declare function prepareProtocolTools(input: PrepareProtocolToolsInput): Promise<PrepareProtocolToolsResult>;
@@ -0,0 +1,182 @@
1
+ import { Type } from "typebox";
2
+ import { materializeMcpTools } from "../mcp.js";
3
+ import { materializeA2aTools } from "../a2a.js";
4
+ import { deliverEngineNotice } from "../types.js";
5
+ export async function prepareProtocolTools(input) {
6
+ const { lockedPreflight, spec, deps, reminderMark, reminderDisclosureCounts, runId, sessionId, onceLedger, tools, remoteToolOffload, toolFaceSnapshot, toolEffects, axisExplicitNegatives, irreversibilityTier, irreversibleTools, egressTools, abortController, rollback } = input;
7
+ const mcp = lockedPreflight.mcp?.length
8
+ ? await materializeMcpTools(lockedPreflight.mcp, spec.principal, deps.onElicit, deps.mcpImageResizer, { reminderMark, counts: reminderDisclosureCounts }, mcpRevocationWiring(deps, runId))
9
+ : { tools: [], toolAxes: [], warnings: [], serverInstructions: [], instructionsDelta: { pendingAdds: [], pendingRemovals: [] }, droppedTools: [], statuses: [], refresh: async () => [], dispose: async () => { } };
10
+ if (lockedPreflight.mcp?.length)
11
+ rollback.push(function mcpDispose() { return mcp.dispose(); });
12
+ for (const w of mcp.warnings)
13
+ onceLedger.onError(w, { phase: "mcp", sessionId });
14
+ for (const d of mcp.droppedTools) {
15
+ onceLedger.onError(new Error(`MCP tool dropped at intake — ${d.tool} (server "${d.server}"): ${d.reason}`), { phase: "mcp", sessionId });
16
+ }
17
+ onceLedger.pruneDroppedTools(mcp.droppedTools);
18
+ {
19
+ const callerNames = new Set((spec.tools ?? []).flatMap((t) => [t.name, ...(t.aliases ?? [])]));
20
+ const clash = mcp.tools.find((t) => [t.name, ...(t.aliases ?? [])].some((name) => callerNames.has(name)));
21
+ if (clash) {
22
+ await mcp.dispose();
23
+ const e = new Error(`Tool name "${clash.name}" is reserved by an injected MCP tool — a caller tool of the same name would silently shadow it.`);
24
+ e.code = "config.reserved_tool_name";
25
+ throw e;
26
+ }
27
+ }
28
+ tools.push(...mcp.tools.map((t) => remoteToolOffload(t)));
29
+ const rebuildHarnessToolsRef = {};
30
+ const contentOriginWrapRef = {};
31
+ const toolCallGateArmedRef = { armed: false };
32
+ if (lockedPreflight.mcp?.length) {
33
+ tools.push({
34
+ name: "RefreshMcpTools",
35
+ label: "RefreshMcpTools",
36
+ description: "Refresh the tool list of connected MCP servers. Never dials or re-dials connections — it only re-reads the tool list over the existing connection. The refreshed tools are available immediately; you can call them on your next step. Use when a server's expected tool is missing, or the tool list looks stale after a connection recovered. Omit `server` to refresh every connected server.",
37
+ parameters: Type.Object({
38
+ server: Type.Optional(Type.String({ description: "Name of a single MCP server to refresh; omit to refresh all connected servers." })),
39
+ }),
40
+ execute: async (_toolCallId, params) => {
41
+ const serverArg = params?.server;
42
+ const results = await mcp.refresh(typeof serverArg === "string" && serverArg.length > 0 ? serverArg : undefined);
43
+ const lines = [];
44
+ let changed = false;
45
+ let anyActiveFailure = false;
46
+ for (const r of results) {
47
+ if (r.status !== "refreshed" || r.tools === undefined) {
48
+ lines.push(`${r.server}: ${r.status}${r.error !== undefined ? ` (${r.error})` : ""}`);
49
+ if (r.status === "failed")
50
+ anyActiveFailure = true;
51
+ continue;
52
+ }
53
+ const excludedSet = new Set(toolFaceSnapshot.exclude ?? []);
54
+ const pushable = r.tools.filter((t) => !excludedSet.has(t.name));
55
+ const excludedNow = r.tools.filter((t) => excludedSet.has(t.name)).map((t) => t.name);
56
+ const domainSnapshot = (m) => new Map([...m].filter(([name]) => name.startsWith(r.prefix)));
57
+ const restoreDomain = (m, snap) => {
58
+ for (const name of [...m.keys()])
59
+ if (name.startsWith(r.prefix))
60
+ m.delete(name);
61
+ for (const [name, v] of snap)
62
+ m.set(name, v);
63
+ };
64
+ const priorDomainEffects = domainSnapshot(toolEffects);
65
+ const priorDomainNegatives = domainSnapshot(axisExplicitNegatives);
66
+ for (const name of priorDomainEffects.keys())
67
+ toolEffects.delete(name);
68
+ for (const name of priorDomainNegatives.keys())
69
+ axisExplicitNegatives.delete(name);
70
+ try {
71
+ foldProtocolAxes((r.axes ?? []).filter((a) => !excludedSet.has(a.name)), "MCP");
72
+ }
73
+ catch (foldErr) {
74
+ restoreDomain(toolEffects, priorDomainEffects);
75
+ restoreDomain(axisExplicitNegatives, priorDomainNegatives);
76
+ lines.push(`${r.server}: failed (${foldErr instanceof Error ? foldErr.message : String(foldErr)})`);
77
+ anyActiveFailure = true;
78
+ continue;
79
+ }
80
+ let domainAnchor = -1;
81
+ for (let i = tools.length - 1; i >= 0; i--) {
82
+ const t = tools[i];
83
+ if (t.name.startsWith(r.prefix)) {
84
+ domainAnchor = i;
85
+ tools.splice(i, 1);
86
+ }
87
+ }
88
+ const refreshedMounts = pushable.map((t) => remoteToolOffload(t));
89
+ if (domainAnchor >= 0)
90
+ tools.splice(domainAnchor, 0, ...refreshedMounts);
91
+ else
92
+ tools.push(...refreshedMounts);
93
+ changed = true;
94
+ const detail = [];
95
+ const shownAdded = r.added.filter((n) => !excludedSet.has(n));
96
+ if (shownAdded.length > 0)
97
+ detail.push(`added: ${shownAdded.join(", ")}`);
98
+ if (r.removed.length > 0)
99
+ detail.push(`removed: ${r.removed.join(", ")}`);
100
+ if (excludedNow.length > 0)
101
+ detail.push(`excluded by deployment config (not mounted): ${excludedNow.join(", ")}`);
102
+ if ((r.dropped?.length ?? 0) > 0)
103
+ detail.push(`dropped: ${r.dropped.map((d) => `${d.tool} (${d.reason.length > 90 ? `${d.reason.slice(0, 90)}…` : d.reason})`).join("; ")}`);
104
+ lines.push(`${r.server}: refreshed — ${pushable.length} tool${pushable.length === 1 ? "" : "s"}${detail.length > 0 ? ` (${detail.join("; ")})` : ""}`);
105
+ }
106
+ if (changed)
107
+ await rebuildHarnessToolsRef.current?.();
108
+ if (changed && !toolCallGateArmedRef.armed && (irreversibleTools.size > 0 || egressTools.size > 0)) {
109
+ lines.push("WARNING: refreshed tools declare irreversible/egress safety hints, but this task started with no approval gate registered (no policy/hooks and no gated tools at start) — these hints cannot arm a gate mid-task (RB-46); the refreshed tools run ungated on this deployment shape.");
110
+ }
111
+ const text = lines.length === 0 ? "No connected MCP servers to refresh." : lines.join("\n");
112
+ return {
113
+ content: [{ type: "text", text }],
114
+ details: { results },
115
+ terminate: false,
116
+ ...(anyActiveFailure && !changed ? { isError: true } : {}),
117
+ };
118
+ },
119
+ });
120
+ toolEffects.set("RefreshMcpTools", "read");
121
+ }
122
+ const foldProtocolAxes = (axes, protocolLabel) => {
123
+ for (const axis of axes) {
124
+ if (axis.irreversibility === "always") {
125
+ irreversibilityTier.set(axis.name, "always");
126
+ irreversibleTools.add(axis.name);
127
+ }
128
+ if (axis.irreversibility === "never") {
129
+ axisExplicitNegatives.set(axis.name, { ...axisExplicitNegatives.get(axis.name), irreversible: false });
130
+ }
131
+ if (axis.egress === false) {
132
+ axisExplicitNegatives.set(axis.name, { ...axisExplicitNegatives.get(axis.name), egress: false });
133
+ }
134
+ if (axis.egress) {
135
+ if (axis.effect !== undefined && axis.effect !== "write") {
136
+ const e = new Error(`${protocolLabel} tool "${axis.name}" resolves to egress:true with effect:"${axis.effect}" — an egress tool (external write) must have effect:"write". Clear egress (toolAxes egress:false) if it is a pure read, or set effect:"write".`);
137
+ e.code = "config.egress_requires_write_effect";
138
+ throw e;
139
+ }
140
+ egressTools.add(axis.name);
141
+ }
142
+ if (!toolEffects.has(axis.name))
143
+ toolEffects.set(axis.name, axis.effect ?? "write");
144
+ }
145
+ };
146
+ foldProtocolAxes(mcp.toolAxes, "MCP");
147
+ const a2aDeclared = spec.a2a?.length ? spec.a2a : undefined;
148
+ const a2a = a2aDeclared
149
+ ? await materializeA2aTools(a2aDeclared, spec.principal, abortController.signal)
150
+ : { tools: [], toolAxes: [], warnings: [], statuses: [], refresh: async () => [], dispose: async () => { } };
151
+ if (a2aDeclared)
152
+ rollback.push(function a2aDispose() { return a2a.dispose(); });
153
+ for (const w of a2a.warnings)
154
+ deps.onError?.(w, { phase: "a2a", sessionId });
155
+ {
156
+ const callerNames = new Set((spec.tools ?? []).flatMap((t) => [t.name, ...(t.aliases ?? [])]));
157
+ const clash = a2a.tools.find((t) => callerNames.has(t.name));
158
+ if (clash) {
159
+ await a2a.dispose();
160
+ await mcp.dispose();
161
+ const e = new Error(`Tool name "${clash.name}" is reserved by an injected A2A tool — a caller tool of the same name would silently shadow it.`);
162
+ e.code = "config.reserved_tool_name";
163
+ throw e;
164
+ }
165
+ }
166
+ tools.push(...a2a.tools.map((t) => remoteToolOffload(t)));
167
+ foldProtocolAxes(a2a.toolAxes, "A2A");
168
+ return { mcp, a2a, rebuildHarnessToolsRef, contentOriginWrapRef, toolCallGateArmedRef };
169
+ }
170
+ function mcpRevocationWiring(deps, runId) {
171
+ if (deps.mcpRevocations === undefined)
172
+ return undefined;
173
+ const ledger = deps.mcpRevocations;
174
+ return {
175
+ isRevoked: (name) => ledger.isRevoked(name),
176
+ onProbeFailure: (e) => deliverEngineNotice(deps.onNotice, {
177
+ code: "mcp.revocation_probe_failed",
178
+ message: `the mcpRevocations.isRevoked probe threw — MCP dispatch fails OPEN (no server treated as revoked) until the probe recovers: ${e instanceof Error ? e.message : String(e)}`,
179
+ detail: { message: e instanceof Error ? e.message : String(e), runId },
180
+ }),
181
+ };
182
+ }
@@ -0,0 +1,119 @@
1
+ /**
2
+ * design/390 §1.2 M7 — prepareTask's QUESTION-FACE phase, verbatim from the driver: the leg's ONE checkpoint-store
3
+ * read, the durable mandate as it binds at the question seat, the content-ask binding table (with the leak
4
+ * bound the routing lane checks against — the constant below), the stranded-answer disclosure and the leg-end
5
+ * settle, the three-way mount decision (live face / durable face / override), and the AskUserQuestion mount
6
+ * with its redeemed-approval, posture, fallback and synthetic-continuation arms. The interface is the
7
+ * dependency list the segment had implicitly (design/238 R-1).
8
+ *
9
+ * SYNCHRONOUS FUNCTION, SYNCHRONOUS CALL: the stretch has no await, so the phase adds no yield between the
10
+ * offload-reader mount before it and the scheduler mount after it (design/390 §1.5 S1: a stretch with no await
11
+ * is extracted as a sync function — an `async` wrapper would open a microtask window). Read-stability of the
12
+ * host handles for the call: {@link RunInternals} (`@contract prepare.deps-read-stable`).
13
+ *
14
+ * One seat is a LIVE GETTER (design/238 D-3): the inherited-unavailable marker set is declared by the driver's
15
+ * gate-fold section AFTER this phase and read per call at RUN time, so it enters as `() => ReadonlySet` — a
16
+ * read before the driver declares it throws (temporal dead zone), which is the fail-loud an uninitialized
17
+ * live read owes.
18
+ */
19
+ import type { AgentTool } from "../../internal/harness.js";
20
+ import { type OnQuestion, type QuestionAnswer } from "../ask-question.js";
21
+ import { type CheckpointStore } from "../checkpoint-store.js";
22
+ import type { RunnerDeps, RuntimeCaps, TaskSpec } from "../types.js";
23
+ import type { PrepareResume } from "./contracts.js";
24
+ export declare const CONTENT_ASK_BINDING_CAP = 32;
25
+ /** One gate-routed content-ask outcome, keyed by the call id it was produced for (see the table's own note). */
26
+ export type ContentAskBinding = ({
27
+ kind: "answered";
28
+ answer: QuestionAnswer;
29
+ } | {
30
+ kind: "failed";
31
+ error: unknown;
32
+ }) & {
33
+ /** Digest of the exact question batch this outcome belongs to. The call id alone is not enough
34
+ * to redeem a binding: it comes from the model/provider and can repeat, so an entry that
35
+ * outlived its call (its call was allowed but never executed) could otherwise be handed to a
36
+ * LATER call that happens to reuse the id. Matching on the batch as well means an outcome can
37
+ * only ever be delivered for the question it was produced for. */
38
+ questionsHash: string;
39
+ /** The engine-minted per-delivery identity (see AskQuestionRequest.deliveryId) — carried so a
40
+ * stranded-answer disclosure names DELIVERIES, not call ids: call ids can repeat, and a
41
+ * disclosure keyed on them would collapse two lost answers into one. */
42
+ deliveryId: string;
43
+ };
44
+ export interface PrepareQuestionFaceInput {
45
+ /** borrowed-readonly — the run's frozen question seat (spec > deps), resolved once at the top of prepare;
46
+ * the mount predicate and the live-face derivation both read this one value. */
47
+ frozenOnQuestion: OnQuestion | undefined;
48
+ /** borrowed-readonly — the REBOUND spec. Read: `checkpointStore` (the per-run off switch), `durableApproval`
49
+ * (the durable face's opt-in), `interactiveTools` (the override), `principal` (the tool's source identity),
50
+ * `interactiveQuestionFallback` (restores the continuation under a resolved posture). Never mutated. */
51
+ spec: Pick<TaskSpec, "checkpointStore" | "durableApproval" | "interactiveTools" | "principal" | "interactiveQuestionFallback">;
52
+ /** borrowed-readonly — the deployment seats: `checkpointStore` (the store the leg snapshot resolves against)
53
+ * and `onError` (the stranded-answer and synthetic-continuation operator lines; swallow-guarded). */
54
+ deps: Pick<RunnerDeps, "checkpointStore" | "onError">;
55
+ /** borrowed-readonly — the principal's resolved runtime caps as of this point (`forceDurableGate` is the
56
+ * mandate that outranks a live face only where a store exists to park the question). */
57
+ runtimeCaps: Pick<RuntimeCaps, "forceDurableGate"> | undefined;
58
+ /** borrowed-readonly — the resolved interaction posture (the door's value), threaded to the tool so both
59
+ * no-answer arms harden under it. */
60
+ resolvedInteractionPosture: "interactive" | "headless" | undefined;
61
+ /** borrowed-readonly — the durable-resume carrier; only the redeemed content-ask call id and its questions
62
+ * hash are read (the one call an operator's approval was spent on). */
63
+ resume: Pick<PrepareResume, "redeemedContentAskCallId" | "redeemedContentAskQuestionsHash"> | undefined;
64
+ /** borrowed-readonly — the acquired session id (the tool's source task id; operator-line context). */
65
+ sessionId: string;
66
+ /** borrowed-mutable — the run's shared roster (identity is the contract). Writer here: ONE push (the question
67
+ * tool) when the mount predicate holds; `contentAskRoutable` reads `includes` per call against the LIVE
68
+ * array (the roster is edited in place after the mount). */
69
+ tools: AgentTool[];
70
+ /** borrowed-readonly — LIVE GETTER over the inherited-unavailable marker set (declared by the driver's
71
+ * gate-fold section after this phase; `has` only, per call at run time). See the module header. */
72
+ inheritedUnavailableAsks: () => ReadonlySet<string>;
73
+ }
74
+ export interface PrepareQuestionFaceResult {
75
+ /** borrowed-readonly — THE leg's store snapshot (`resolveCheckpointStore(spec, deps)`): the mount face, the
76
+ * routability predicate, the manifest and suspendAsk all read this one value. */
77
+ checkpointStore: CheckpointStore | undefined;
78
+ /** borrowed-readonly — the run's live question face through the shared predicate (the reserved resume
79
+ * placeholder is a function and is NOT a live face), or undefined. */
80
+ liveQuestionFace: OnQuestion | undefined;
81
+ /** borrowed-mutable — the one-shot answer binding table for questions the GATE routed (scope = this leg).
82
+ * Writers (R-3): the routing lane (`set`, cap-checked against {@link CONTENT_ASK_BINDING_CAP} before every
83
+ * insertion), the mounted face here (`delete`, one-shot redemption or a mismatched-batch drop), the settle
84
+ * here (`clear`). Identity is the contract (the routing lane and the face share this exact map). */
85
+ contentAskBindings: Map<string, ContentAskBinding>;
86
+ /** owned — THE routability predicate for a content ask (per call, against the live roster and the marker set). */
87
+ contentAskRoutable: (toolCallId: string) => boolean;
88
+ /** borrowed-mutable — late answers noted before the leg settles. Writer: the routing lane's delivery-race
89
+ * watcher (`push`); reader: the settle here (merged into the result disclosure by delivery id). */
90
+ lateStrandedAnswers: Array<{
91
+ deliveryId: string;
92
+ toolCallId: string;
93
+ }>;
94
+ /** owned — THE disclosure lane for an answer a person gave that no call could collect (swallow-guarded). */
95
+ discloseStrandedAnswers: (records: ReadonlyArray<{
96
+ deliveryId: string;
97
+ toolCallId: string;
98
+ }>, why: string) => void;
99
+ /** owned — the leg-end settle (`Prepared.settleContentAskBindings`): discloses surviving answers, clears the
100
+ * table, returns the records the result carries. */
101
+ settleContentAskBindings: () => ReadonlyArray<{
102
+ deliveryId: string;
103
+ toolCallId: string;
104
+ }>;
105
+ /** owned — whether a durable delivery face exists (store wired ∧ (durableApproval ∨ forceDurableGate));
106
+ * the manifest's composition-lie detector reuses this exact value. */
107
+ durableQuestionFace: boolean;
108
+ /** owned — the face the tool was mounted with: the gate-bound answer for THIS call id if one exists (one-shot),
109
+ * else the live face itself; the raw frozen seat when no live face exists. */
110
+ mountedQuestionFace: OnQuestion | undefined;
111
+ /** owned — whether THIS prepare mounted the first-party question tool (the routing lane keys on it, not on
112
+ * the reserved name). */
113
+ questionToolMounted: boolean;
114
+ /** borrowed-readonly — the mounted tool object itself (the exact roster member), so the routing lane can
115
+ * validate a batch against the schema the executing call is re-checked against; undefined when not mounted. */
116
+ mountedQuestionTool: AgentTool | undefined;
117
+ }
118
+ /** The M7 phase body — prepareTask's question-face stretch, verbatim (see the module header). */
119
+ export declare function prepareQuestionFace(input: PrepareQuestionFaceInput): PrepareQuestionFaceResult;
@@ -0,0 +1,83 @@
1
+ import { createAskUserQuestionTool, isLiveQuestionFace } from "../ask-question.js";
2
+ import { boundInputHashOf } from "../canonical-json.js";
3
+ import { resolveCheckpointStore } from "../checkpoint-store.js";
4
+ export const CONTENT_ASK_BINDING_CAP = 32;
5
+ export function prepareQuestionFace(input) {
6
+ const { frozenOnQuestion, spec, deps, runtimeCaps, resolvedInteractionPosture, resume, sessionId, tools, inheritedUnavailableAsks } = input;
7
+ const onQuestion = frozenOnQuestion;
8
+ const liveQuestionFace = isLiveQuestionFace(onQuestion) ? onQuestion : undefined;
9
+ const contentAskBindings = new Map();
10
+ const checkpointStore = resolveCheckpointStore(spec, deps);
11
+ const questionParkStoreWired = checkpointStore !== undefined;
12
+ const questionDurableMandateBinding = runtimeCaps?.forceDurableGate === true && questionParkStoreWired;
13
+ const contentAskRoutable = (toolCallId) => liveQuestionFace !== undefined &&
14
+ mountedQuestionTool !== undefined &&
15
+ tools.includes(mountedQuestionTool) &&
16
+ !questionDurableMandateBinding &&
17
+ !inheritedUnavailableAsks().has(toolCallId);
18
+ const lateStrandedAnswers = [];
19
+ const discloseStrandedAnswers = (records, why) => {
20
+ if (records.length === 0)
21
+ return;
22
+ try {
23
+ deps.onError?.(new Error(`AskUserQuestion: ${records.length} question(s) were answered by a person but the call never ` +
24
+ `executed to collect the answer (${records.map((r) => `${r.toolCallId} [delivery ${r.deliveryId}]`).join(", ")}) — ${why}. The answer(s) were NOT ` +
25
+ `delivered to the model and are gone with this leg; re-ask if the decision is still needed.`), { phase: "degraded", sessionId, classification: "unconsumed-human-answer" });
26
+ }
27
+ catch {
28
+ }
29
+ };
30
+ const settleContentAskBindings = () => {
31
+ const stranded = [];
32
+ for (const [callId, bound] of contentAskBindings) {
33
+ if (bound.kind === "answered")
34
+ stranded.push({ deliveryId: bound.deliveryId, toolCallId: callId });
35
+ }
36
+ contentAskBindings.clear();
37
+ discloseStrandedAnswers(stranded, "the leg ended first (abort, batch teardown, or a loop failure)");
38
+ const byDelivery = new Map();
39
+ for (const r of [...stranded, ...lateStrandedAnswers])
40
+ byDelivery.set(r.deliveryId, r);
41
+ return [...byDelivery.values()];
42
+ };
43
+ const durableQuestionFace = questionParkStoreWired && (spec.durableApproval !== undefined || runtimeCaps?.forceDurableGate === true);
44
+ const mountedQuestionFace = liveQuestionFace !== undefined
45
+ ? async (req, signal) => {
46
+ const bound = contentAskBindings.get(req.toolCallId);
47
+ if (bound !== undefined) {
48
+ if (bound.questionsHash === boundInputHashOf(req.questions)) {
49
+ contentAskBindings.delete(req.toolCallId);
50
+ if (bound.kind === "answered")
51
+ return bound.answer;
52
+ throw bound.error;
53
+ }
54
+ contentAskBindings.delete(req.toolCallId);
55
+ }
56
+ return liveQuestionFace(req, signal);
57
+ }
58
+ : onQuestion;
59
+ const questionToolMounted = spec.interactiveTools === true || (spec.interactiveTools !== false && (onQuestion !== undefined || durableQuestionFace));
60
+ let mountedQuestionTool;
61
+ if (questionToolMounted)
62
+ tools.push((mountedQuestionTool =
63
+ createAskUserQuestionTool(mountedQuestionFace, { principal: spec.principal, sourceTaskId: sessionId }, {
64
+ ...(resume?.redeemedContentAskCallId !== undefined
65
+ ? {
66
+ redeemedApprovalCallId: resume.redeemedContentAskCallId,
67
+ ...(resume.redeemedContentAskQuestionsHash !== undefined ? { redeemedApprovalQuestionsHash: resume.redeemedContentAskQuestionsHash } : {}),
68
+ }
69
+ : {}),
70
+ ...(resolvedInteractionPosture !== undefined ? { posture: resolvedInteractionPosture } : {}),
71
+ ...(spec.interactiveQuestionFallback === true ? { interactiveFallback: true } : {}),
72
+ onSyntheticContinuation: ({ questionId, reason }) => {
73
+ deps.onError?.(new Error(`AskUserQuestion ${questionId}: no human answer was obtainable (` +
74
+ (reason === "seam_absent"
75
+ ? "no onQuestion seam is wired"
76
+ : reason === "declined_unavailable"
77
+ ? "the wired question channel reported nobody was reachable"
78
+ : "the wired question channel failed") +
79
+ `) — the model was instructed to self-answer and the run CONTINUES (warning, not a failure).`), { phase: "degraded", sessionId, classification: "no-human-autoanswered" });
80
+ },
81
+ })));
82
+ return { checkpointStore, liveQuestionFace, contentAskBindings, contentAskRoutable, lateStrandedAnswers, discloseStrandedAnswers, settleContentAskBindings, durableQuestionFace, mountedQuestionFace, questionToolMounted, mountedQuestionTool };
83
+ }
@@ -0,0 +1,89 @@
1
+ /**
2
+ * design/390 §1.2 M3a — prepareTask's RUN-REFS phase, verbatim from the driver: the run's own edited-files
3
+ * ledger, the harness ref, the per-task skill scope, the display-forwarding wrapper, the three run-local refs
4
+ * (review request / stop-after-turn / plan mode) with the ctx primitives that write them, the subagent-retain
5
+ * ledger and the worktree-isolation helper. Nothing here has behavior of its own: the phase MINTS SEATS —
6
+ * mutable cells and the closures bound to them — that later phases, the tool ctx and the run loop read and
7
+ * write. The interface is the dependency list the segment had implicitly (design/238 R-1).
8
+ *
9
+ * SYNCHRONOUS FUNCTION, SYNCHRONOUS CALL: the stretch has no await, so the phase adds no yield between the
10
+ * file-history phase before it and the inherited-gate fold after it (design/390 §1.5 S1: a stretch with no
11
+ * await is extracted as a sync function — an `async` wrapper would open a microtask window).
12
+ *
13
+ * Every cell that leaves here as `borrowed-mutable` carries its writer table on the Result member (design/238
14
+ * R-3): the closures minted here, the driver's harness build, the project-context phase's inherited-skill
15
+ * seed, the skill tool, and the run loop's two writes — nothing else writes them. Read-stability of the host
16
+ * handles for the call: {@link RunInternals} (`@contract prepare.deps-read-stable`).
17
+ */
18
+ import type { AgentHarness, ExecutionEnv } from "../../internal/harness.js";
19
+ import type { SubagentWorktreeIsolation } from "../../agents/subagent.js";
20
+ import { SubagentRetainLedger } from "../../agents/retain-ledger.js";
21
+ import type { TaskEvent, TaskSpec } from "../types.js";
22
+ import { ActiveSkillScope } from "./active-skill-scope.js";
23
+ import type { Prepared, RunInternals } from "./contracts.js";
24
+ import { type EditedFilesLedger } from "./edited-files-ledger.js";
25
+ export interface PrepareRunRefsInput {
26
+ /** borrowed-readonly — the REBOUND spec. Read: `forwardSubagentEvents` (widens the forwarding filter),
27
+ * `retainSubagentSessions` (the ledger opt-in and its config), `handsReadOnly` (a read-only run mints no
28
+ * worktree helper). Never mutated. */
29
+ spec: Pick<TaskSpec, "forwardSubagentEvents" | "retainSubagentSessions" | "handsReadOnly">;
30
+ /** borrowed-readonly — the trusted spawn-side channel; only `onForwardEvent` (the deployment's display
31
+ * sink) is read, once, to decide whether a forwarding wrapper exists at all. */
32
+ internals: Pick<RunInternals, "onForwardEvent"> | undefined;
33
+ /** borrowed-readonly — the run's resolved env; the worktree helper closes over it (lazy: no I/O until a
34
+ * delegation actually mints a worktree). */
35
+ executionEnv: ExecutionEnv;
36
+ /** borrowed-readonly — the SETTLED task root (`taskRootFinal`): the worktree helper's base repo root. */
37
+ taskRootFinal: string;
38
+ }
39
+ export interface PrepareRunRefsResult {
40
+ /** owned — the ledger's writer (`FileEditedHook`): handed to the hands/read-face phase as its post-write
41
+ * seat; nothing else calls it. */
42
+ noteFileEdited: EditedFilesLedger["note"];
43
+ /** owned — the ledger's LIVE read face (`Prepared.editedFilesSnapshot`, read at result assembly). */
44
+ editedFilesSnapshot: EditedFilesLedger["snapshot"];
45
+ /** borrowed-mutable — the harness seat: holds the harness once built, so a tool's execute can read the model
46
+ * the parent is CURRENTLY on (design/38 1B: the `ToolExecuteContext.model` snapshot a delegation tool uses
47
+ * for `inherit`), and breaks the harness↔tool_search build cycle (design/36 rematerialize). Writers (R-3):
48
+ * the driver, ONCE, when the harness is built (`harnessRef.current = harness`) — before any run. Readers:
49
+ * `requestStopAfterTurn` here (mirrors the stop onto the harness's one-way flag), the tool ctx's model
50
+ * snapshot, the rebuild seam and the delegation mounts. Identity is the contract: every closure minted
51
+ * before the harness exists reads this exact cell. */
52
+ harnessRef: {
53
+ current?: AgentHarness;
54
+ };
55
+ /** borrowed-mutable — the per-task LIFO skill scope. Writers: the project-context phase (the inherited-
56
+ * from-parent seed frame, `push`) and the `skill` tool minted over it (`push` on a manifest load; a frame
57
+ * closes by `pop`). Readers: the tool ctx's `activeSkillScope()` snapshot and the skill-scope policy layer. */
58
+ skillScope: ActiveSkillScope;
59
+ /** owned — the FILTERED display-forwarding wrapper, or undefined when the deployment wired no sink (the
60
+ * tool ctx then carries no `forwardEvent` key). */
61
+ forwardEvent: ((e: TaskEvent) => void) | undefined;
62
+ /** borrowed-mutable — the review-request cell (`Prepared.reviewRequestRef`). Writers: `requestReview` here
63
+ * (the first request wins) and the run loop's turn-boundary consumer, which clears `pending` once the
64
+ * `plan_review` checkpoint is minted. */
65
+ reviewRequestRef: Prepared["reviewRequestRef"];
66
+ /** owned — the ctx primitive that arms `reviewRequestRef` (design/80 D-B). */
67
+ requestReview: (opts?: {
68
+ reason?: string;
69
+ }) => void;
70
+ /** borrowed-mutable — the stop-after-turn latch (`Prepared.stopRequestedRef`). Writers: `requestStopAfterTurn`
71
+ * here, and nothing else; the run loop reads it at a turn boundary. */
72
+ stopRequestedRef: Prepared["stopRequestedRef"];
73
+ /** owned — the ctx primitive: flips the latch and mirrors it onto the harness's one-way flag. */
74
+ requestStopAfterTurn: () => void;
75
+ /** borrowed-mutable — the plan-mode flag (`Prepared.planModeRef`). Writers: `enterPlanMode` here, and the run
76
+ * loop's resume ladder, which re-arms it when a REJECTED `plan_review` resumes into a re-plan. Read by the
77
+ * tool-call gate (the write-effect refusal). */
78
+ planModeRef: Prepared["planModeRef"];
79
+ /** owned — the ctx primitive (mounted on the ctx only under `enablePlanMode`). */
80
+ enterPlanMode: () => void;
81
+ /** ownership-ref — the parent-run subagent-retain ledger, or undefined when the spec did not opt in. Its
82
+ * lifecycle protocol belongs to the Runner (disposed in the task's terminal finally, design/122 D4); a
83
+ * prepare that throws has nothing to release here — the ledger is empty until a delegation retains. */
84
+ subagentRetain: SubagentRetainLedger | undefined;
85
+ /** owned — the Agent tool's worktree-isolation helper (lazy; write-capable runs only), or undefined. */
86
+ worktreeIsolation: SubagentWorktreeIsolation | undefined;
87
+ }
88
+ /** The M3a phase body — prepareTask's run-refs stretch, verbatim (see the module header). */
89
+ export declare function prepareRunRefs(input: PrepareRunRefsInput): PrepareRunRefsResult;
@@ -0,0 +1,39 @@
1
+ import { createSubagentWorktreeHelper } from "../../agents/subagent.js";
2
+ import { SubagentRetainLedger } from "../../agents/retain-ledger.js";
3
+ import { ActiveSkillScope } from "./active-skill-scope.js";
4
+ import { createEditedFilesLedger } from "./edited-files-ledger.js";
5
+ export function prepareRunRefs(input) {
6
+ const { spec, internals, executionEnv, taskRootFinal } = input;
7
+ const { note: noteFileEdited, snapshot: editedFilesSnapshot } = createEditedFilesLedger();
8
+ const harnessRef = {};
9
+ const skillScope = new ActiveSkillScope();
10
+ const forwardSink = internals?.onForwardEvent;
11
+ const forwardEvent = forwardSink
12
+ ? (e) => {
13
+ if (e.type === "task_progress")
14
+ forwardSink(e);
15
+ else if (spec.forwardSubagentEvents === true &&
16
+ (e.type === "text_delta" || e.type === "text_end" || e.type === "reasoning_delta" || e.type === "tool_start" || e.type === "tool_end")) {
17
+ forwardSink(e);
18
+ }
19
+ }
20
+ : undefined;
21
+ const reviewRequestRef = {};
22
+ const requestReview = (opts) => {
23
+ if (reviewRequestRef.pending === undefined) {
24
+ reviewRequestRef.pending = opts?.reason !== undefined ? { reason: opts.reason } : {};
25
+ }
26
+ };
27
+ const stopRequestedRef = { current: false };
28
+ const requestStopAfterTurn = () => {
29
+ stopRequestedRef.current = true;
30
+ harnessRef.current?.requestStopAfterTurn();
31
+ };
32
+ const planModeRef = { active: false };
33
+ const enterPlanMode = () => {
34
+ planModeRef.active = true;
35
+ };
36
+ const subagentRetain = spec.retainSubagentSessions ? new SubagentRetainLedger(spec.retainSubagentSessions) : undefined;
37
+ const worktreeIsolation = spec.handsReadOnly !== true ? createSubagentWorktreeHelper(executionEnv, taskRootFinal) : undefined;
38
+ return { noteFileEdited, editedFilesSnapshot, harnessRef, skillScope, forwardEvent, reviewRequestRef, requestReview, stopRequestedRef, requestStopAfterTurn, planModeRef, enterPlanMode, subagentRetain, worktreeIsolation };
39
+ }
@@ -1,4 +1,5 @@
1
1
  import type { RunnerDeps, TaskSpec, ToolEffect } from "../types.js";
2
+ import type { IrreversibilityTier, ReversibilityProbes } from "./contracts.js";
2
3
  /** The namespaced-name shapes the protocol table currently owns, rendered for the two messages that have
3
4
  * to name them (the caller-name reservation and the policy audit's unprefixed-name arm). Read from the
4
5
  * table rather than written out: a message that hard-codes ONE protocol becomes wrong — while staying
@@ -38,14 +39,14 @@ export interface PrepareSafetyScanResult {
38
39
  /** borrowed-mutable — tools whose effect is irreversible (`always`, or `maybe` incl. by probe). */
39
40
  irreversibleTools: Set<string>;
40
41
  /** borrowed-mutable — static per-tool irreversibility tier (prepare-time resolution). */
41
- irreversibilityTier: Map<string, "never" | "maybe" | "always">;
42
+ irreversibilityTier: IrreversibilityTier;
42
43
  /** borrowed-mutable — explicit NEGATIVE axis judgments (report-face only; `riskAxesOf` reads). */
43
44
  axisExplicitNegatives: Map<string, {
44
45
  irreversible?: false;
45
46
  egress?: false;
46
47
  }>;
47
48
  /** borrowed-mutable — declared reversibility probes by tool name. */
48
- reversibilityProbes: Map<string, NonNullable<TaskSpec["tools"]>[number]["reversibilityProbe"]>;
49
+ reversibilityProbes: ReversibilityProbes;
49
50
  /** borrowed-readonly — the caller roster's own names, snapshotted before the first await. */
50
51
  ownToolNames: ReadonlySet<string>;
51
52
  }