@sema-agent/core 7.5.2 → 7.6.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 (85) hide show
  1. package/CHANGELOG.md +41 -0
  2. package/dist/agents/cascade.d.ts +2 -2
  3. package/dist/agents/cascade.js +12 -10
  4. package/dist/agents/repair-loop.d.ts +5 -3
  5. package/dist/agents/repair-loop.js +13 -15
  6. package/dist/agents/subagent.d.ts +24 -42
  7. package/dist/agents/subagent.js +119 -105
  8. package/dist/agents/suspend-guard.d.ts +31 -19
  9. package/dist/agents/suspend-guard.js +14 -8
  10. package/dist/agents/teacher.js +9 -9
  11. package/dist/agents/team.d.ts +4 -3
  12. package/dist/agents/team.js +10 -8
  13. package/dist/agents/verify.d.ts +3 -3
  14. package/dist/agents/verify.js +17 -17
  15. package/dist/core/a2a.js +2 -1
  16. package/dist/core/ask-origin.d.ts +60 -7
  17. package/dist/core/ask-origin.js +26 -1
  18. package/dist/core/checkpoint-store.d.ts +78 -76
  19. package/dist/core/checkpoint-store.js +17 -1
  20. package/dist/core/gate-outcome.d.ts +189 -0
  21. package/dist/core/gate-outcome.js +70 -0
  22. package/dist/core/hooks.d.ts +18 -92
  23. package/dist/core/hooks.js +88 -85
  24. package/dist/core/mcp-failure.d.ts +104 -0
  25. package/dist/core/mcp-failure.js +128 -0
  26. package/dist/core/mcp.d.ts +21 -77
  27. package/dist/core/mcp.js +76 -150
  28. package/dist/core/pause-registry.d.ts +131 -0
  29. package/dist/core/pause-registry.js +27 -0
  30. package/dist/core/runner/assemble-result.d.ts +32 -41
  31. package/dist/core/runner/assemble-result.js +55 -74
  32. package/dist/core/runner/contracts.d.ts +46 -64
  33. package/dist/core/runner/denial-limit-arms.d.ts +1 -1
  34. package/dist/core/runner/denial-limit-arms.js +3 -3
  35. package/dist/core/runner/gate-exit.d.ts +74 -0
  36. package/dist/core/runner/gate-exit.js +55 -0
  37. package/dist/core/runner/park-commit.d.ts +17 -23
  38. package/dist/core/runner/park-commit.js +14 -15
  39. package/dist/core/runner/prepare-ask-lane.d.ts +0 -3
  40. package/dist/core/runner/prepare-ask-lane.js +3 -5
  41. package/dist/core/runner/prepare-boundary-parks.d.ts +3 -6
  42. package/dist/core/runner/prepare-boundary-parks.js +3 -3
  43. package/dist/core/runner/prepare-caps-and-workflow.js +1 -1
  44. package/dist/core/runner/prepare-gate-stations.d.ts +4 -7
  45. package/dist/core/runner/prepare-gate-stations.js +29 -54
  46. package/dist/core/runner/prepare-inherited-gate.js +1 -1
  47. package/dist/core/runner/prepare-memory.d.ts +44 -26
  48. package/dist/core/runner/prepare-park-ask.d.ts +2 -4
  49. package/dist/core/runner/prepare-park-ask.js +5 -5
  50. package/dist/core/runner/prepare-task.js +8 -9
  51. package/dist/core/runner/prepare-wiring-manifest.d.ts +7 -15
  52. package/dist/core/runner/prepare-wiring-manifest.js +9 -10
  53. package/dist/core/runner/runtask.d.ts +16 -31
  54. package/dist/core/runner/runtask.js +109 -120
  55. package/dist/core/runner/terminal-projection.d.ts +22 -0
  56. package/dist/core/runner/terminal-projection.js +28 -0
  57. package/dist/core/store-contracts/checkpoint-store-contract.d.ts +4 -1
  58. package/dist/core/store-contracts/checkpoint-store-contract.js +8 -2
  59. package/dist/core/terminal-cause.d.ts +137 -0
  60. package/dist/core/terminal-cause.js +9 -0
  61. package/dist/core/tool-policy.d.ts +43 -139
  62. package/dist/core/tool-policy.js +79 -112
  63. package/dist/core/types.d.ts +67 -164
  64. package/dist/core/wiring-manifest.d.ts +6 -3
  65. package/dist/core/workflow-journal-store.js +3 -4
  66. package/dist/engine/harness/agent-harness.d.ts +1 -1
  67. package/dist/index.d.ts +10 -7
  68. package/dist/index.js +8 -5
  69. package/dist/orchestration/builtin-workflows.d.ts +2 -2
  70. package/dist/orchestration/builtin-workflows.js +1 -1
  71. package/dist/orchestration/goal.js +8 -7
  72. package/dist/orchestration/run-spec.js +5 -3
  73. package/dist/orchestration/run-workflow-tool.d.ts +1 -1
  74. package/dist/orchestration/run-workflow-tool.js +4 -4
  75. package/dist/orchestration/workflow-governance.d.ts +4 -4
  76. package/dist/orchestration/workflow-governance.js +4 -2
  77. package/dist/orchestration/workflow-primitives.d.ts +1 -1
  78. package/dist/orchestration/workflow-primitives.js +1 -1
  79. package/dist/orchestration/workflow.d.ts +11 -0
  80. package/dist/orchestration/workflow.js +64 -39
  81. package/dist/prompts/supervisor.d.ts +1 -1
  82. package/dist/prompts/supervisor.js +3 -3
  83. package/dist/scenarios/scenario-registry.js +1 -1
  84. package/package.json +3 -1
  85. package/test/export-surface.snapshot.json +74 -22
@@ -1,5 +1,6 @@
1
1
  import { isDegenerateCutMessage } from "../../brain/terminal-cause.js";
2
2
  import { extractErrorCode, stripErrorCodePrefix } from "../../brain/errors.js";
3
+ import { terminalProjection } from "./terminal-projection.js";
3
4
  const SALVAGE_ELIGIBLE_TERMINALS = new Set([
4
5
  "output.degenerate",
5
6
  "limits.max_tokens_exceeded",
@@ -40,30 +41,9 @@ function assistantText(msg) {
40
41
  .join("");
41
42
  return stripThinkTags(text);
42
43
  }
43
- export function assembleResult(spec, sessionId, final, stats, flags) {
44
- const taskId = spec.taskId ?? sessionId;
45
- const text = final ? assistantText(final) : "";
46
- if (!flags.unpricedSpend) {
47
- const compactionMicroUsd = stats.compactionMicroUsd ?? 0;
48
- const nestedSubagentMicroUsd = stats.nested?.costMicroUsd ?? 0;
49
- stats.costBreakdown = {
50
- llmRootMicroUsd: Math.max(0, stats.costMicroUsd - compactionMicroUsd),
51
- nestedSubagentMicroUsd,
52
- memoryConsolidationMicroUsd: 0,
53
- compactionMicroUsd,
54
- };
55
- }
56
- let status;
57
- const result = text;
58
- let errorMessage;
59
- let errorCode;
44
+ export function terminalCauseOf(final, stats, flags) {
45
+ let terminal;
60
46
  let apiFailure;
61
- let blockedReason;
62
- let salvagedOutput;
63
- let checkpointToken;
64
- let checkpointId;
65
- let checkpointGate;
66
- let workspaceRestoreMode;
67
47
  const haltOwnsAbortedFinal = flags.userHalted === true &&
68
48
  final?.stopReason === "aborted" &&
69
49
  !flags.abortedLive &&
@@ -71,107 +51,108 @@ export function assembleResult(spec, sessionId, final, stats, flags) {
71
51
  flags.abortedForTurns !== true;
72
52
  const isDegenerate = final?.stopReason === "error" && isDegenerateCutMessage(final);
73
53
  if (flags.outputInvalid) {
74
- status = "failed";
75
- errorCode = "output.invalid";
76
- errorMessage = "model did not produce a valid structured output within the retry limit";
54
+ terminal = { kind: "failed", code: "output.invalid", message: "model did not produce a valid structured output within the retry limit" };
77
55
  }
78
56
  else if (isDegenerate) {
79
- status = "failed";
80
- errorCode = "output.degenerate";
81
- errorMessage = final?.errorMessage;
57
+ terminal = { kind: "failed", code: "output.degenerate", ...(final?.errorMessage !== undefined ? { message: final.errorMessage } : {}) };
82
58
  }
83
59
  else if (flags.budgetHit) {
84
- status = "failed";
85
- errorCode = flags.budgetAxis === "tokens" ? "limits.max_tokens_exceeded" : "limits.max_cost_exceeded";
86
60
  const axisName = flags.budgetAxis === "tokens" ? "limits.maxTokens" : "limits.maxCostUsd";
87
- errorMessage =
88
- flags.budgetHit === "precall"
61
+ terminal = {
62
+ kind: "failed",
63
+ code: flags.budgetAxis === "tokens" ? "limits.max_tokens_exceeded" : "limits.max_cost_exceeded",
64
+ message: flags.budgetHit === "precall"
89
65
  ? `the estimated cost of the first call already exceeds ${axisName}; the task was not started`
90
- : `cumulative usage exceeded ${axisName}`;
66
+ : `cumulative usage exceeded ${axisName}`,
67
+ };
91
68
  }
92
69
  else if (flags.suspendLoop) {
93
- status = "failed";
94
- errorCode = "suspend.loop";
95
- errorMessage = "task suspended too many times (resume/restart loop) — exceeded the suspend limit";
70
+ let message = "task suspended too many times (resume/restart loop) — exceeded the suspend limit";
96
71
  const gates = stats.humanReview?.gates ?? [];
97
72
  if (gates.length > 0 && gates.every((g) => g.decision === "allow")) {
98
- errorMessage +=
73
+ message +=
99
74
  `. Diagnosis: all ${gates.length} recorded gate decision(s) on this run were ALLOW — a gate that keeps ` +
100
75
  `asking combined with an approver that keeps approving consumes the suspend allowance on legitimate ` +
101
76
  `work. Remedies: answer asks at a LIVE onAsk (a synchronous allow parks nothing and consumes no ` +
102
77
  `suspend), keep provably-benign commands inside the read boundary so the classifier auto-allows them, ` +
103
78
  `or raise maxSuspends for genuinely approval-heavy tasks.`;
104
79
  }
80
+ terminal = { kind: "failed", code: "suspend.loop", message };
105
81
  }
106
82
  else if (flags.threw) {
107
- status = "failed";
108
- errorMessage = flags.threw instanceof Error ? flags.threw.message : String(flags.threw);
109
- errorCode = flags.abortedForTimeout
83
+ const message = flags.threw instanceof Error ? flags.threw.message : String(flags.threw);
84
+ const code = flags.abortedForTimeout
110
85
  ? "limits.max_walltime_exceeded"
111
86
  : flags.abortedForTurns
112
87
  ? "limits.max_turns_exceeded"
113
88
  : flags.conflict
114
89
  ? "conflict"
115
90
  : errorCodeOf(flags.threw);
91
+ terminal = { kind: "failed", ...(code !== undefined ? { code } : {}), message };
116
92
  }
117
93
  else if (flags.blockedReason) {
118
- status = "blocked";
119
- blockedReason = flags.blockedReason;
120
- }
121
- else if (flags.suspendRef) {
122
- status = "suspended";
123
- checkpointToken = flags.suspendRef.token;
124
- checkpointId = flags.suspendRef.checkpointId;
125
- checkpointGate = flags.suspendRef.gate;
126
- workspaceRestoreMode = flags.suspendRef.restoreMode;
127
- }
128
- else if (flags.reviewRef) {
129
- status = "needs_review";
130
- errorCode = "review.pending";
131
- checkpointToken = flags.reviewRef.token;
132
- checkpointId = flags.reviewRef.checkpointId;
133
- checkpointGate = flags.reviewRef.gate;
134
- workspaceRestoreMode = flags.reviewRef.restoreMode;
94
+ terminal = { kind: "blocked", reason: flags.blockedReason };
95
+ }
96
+ else if (flags.paused) {
97
+ terminal = flags.paused;
135
98
  }
136
99
  else if ((flags.abortedLive || final?.stopReason === "aborted") && !haltOwnsAbortedFinal) {
137
- status = "failed";
138
- errorCode = flags.abortedForTimeout ? "limits.max_walltime_exceeded" : flags.abortedForTurns ? "limits.max_turns_exceeded" : undefined;
139
- errorMessage = final?.errorMessage ?? (flags.abortedForTurns ? "max turns exceeded" : "run aborted");
100
+ const code = flags.abortedForTimeout ? "limits.max_walltime_exceeded" : flags.abortedForTurns ? "limits.max_turns_exceeded" : undefined;
101
+ terminal = { kind: "failed", ...(code !== undefined ? { code } : {}), message: final?.errorMessage ?? (flags.abortedForTurns ? "max turns exceeded" : "run aborted") };
140
102
  }
141
103
  else if (flags.userHalted === true && (!final || haltOwnsAbortedFinal)) {
142
- status = "completed";
104
+ terminal = { kind: "completed" };
143
105
  }
144
106
  else if (!final) {
145
- status = "failed";
146
- errorMessage = "no assistant message produced";
107
+ terminal = { kind: "failed", message: "no assistant message produced" };
147
108
  }
148
109
  else if (final.stopReason === "error") {
149
- status = "failed";
150
- errorMessage = final.errorMessage ?? "model error";
110
+ let message = final.errorMessage ?? "model error";
111
+ let code;
151
112
  if (final.isApiErrorMessage === true) {
152
113
  apiFailure = {
153
114
  ...(final.apiErrorStatus !== undefined ? { status: final.apiErrorStatus } : {}),
154
115
  ...(final.requestId !== undefined ? { requestId: final.requestId } : {}),
155
116
  };
156
117
  }
157
- const lifted = extractErrorCode(errorMessage);
118
+ const lifted = extractErrorCode(message);
158
119
  if (lifted) {
159
- errorCode = lifted;
160
- errorMessage = stripErrorCodePrefix(errorMessage);
120
+ code = lifted;
121
+ message = stripErrorCodePrefix(message);
161
122
  }
162
123
  else if (flags.gitCoreOverBudget) {
163
- errorCode = "irreducible_core_over_budget";
124
+ code = "irreducible_core_over_budget";
164
125
  }
165
126
  else if (flags.conflict) {
166
- errorCode = "conflict";
127
+ code = "conflict";
167
128
  }
168
129
  else {
169
- errorCode = "provider.error";
130
+ code = "provider.error";
170
131
  }
132
+ terminal = { kind: "failed", code, message };
171
133
  }
172
134
  else {
173
- status = "completed";
135
+ terminal = { kind: "completed" };
174
136
  }
137
+ return { terminal, apiFailure };
138
+ }
139
+ export function assembleResult(spec, sessionId, final, stats, flags) {
140
+ const taskId = spec.taskId ?? sessionId;
141
+ const text = final ? assistantText(final) : "";
142
+ if (!flags.unpricedSpend) {
143
+ const compactionMicroUsd = stats.compactionMicroUsd ?? 0;
144
+ const nestedSubagentMicroUsd = stats.nested?.costMicroUsd ?? 0;
145
+ stats.costBreakdown = {
146
+ llmRootMicroUsd: Math.max(0, stats.costMicroUsd - compactionMicroUsd),
147
+ nestedSubagentMicroUsd,
148
+ memoryConsolidationMicroUsd: 0,
149
+ compactionMicroUsd,
150
+ };
151
+ }
152
+ const result = text;
153
+ let salvagedOutput;
154
+ const { terminal, apiFailure } = terminalCauseOf(final, stats, flags);
155
+ const { status, errorCode } = terminalProjection(terminal);
175
156
  if (errorCode !== undefined && SALVAGE_ELIGIBLE_TERMINALS.has(errorCode)) {
176
157
  salvagedOutput = text.trim() || undefined;
177
158
  }
@@ -181,5 +162,5 @@ export function assembleResult(spec, sessionId, final, stats, flags) {
181
162
  if (flags.unpricedSpend)
182
163
  delete publicStats.costMicroUsd;
183
164
  const stampHaltedByUser = flags.userHalted === true && status !== "suspended" && status !== "needs_review";
184
- return { taskId, ...(flags.runId !== undefined ? { runId: flags.runId } : {}), sessionId, status, ...(flags.model !== undefined ? { model: flags.model } : {}), result: result.trim(), salvagedOutput, blockedReason, errorMessage, errorCode, ...(apiFailure !== undefined ? { apiFailure } : {}), ...(retryAfterMs !== undefined ? { retryAfterMs } : {}), checkpointToken, ...(checkpointId !== undefined ? { checkpointId } : {}), checkpointGate, ...(workspaceRestoreMode !== undefined ? { workspaceRestoreMode } : {}), ...(flags.rewindNotes !== undefined && flags.rewindNotes.length > 0 ? { rewindNotes: flags.rewindNotes } : {}), ...(flags.editedFiles !== undefined && flags.editedFiles.length > 0 ? { editedFiles: flags.editedFiles } : {}), ...(flags.haltedOnUserRejection === true ? { haltedOnUserRejection: true } : {}), ...(stampHaltedByUser ? { haltedByUser: true } : {}), ...(flags.remoteEnvFailures !== undefined && flags.remoteEnvFailures.length > 0 ? { remoteEnvFailures: [...flags.remoteEnvFailures] } : {}), ...(flags.strandedHumanAnswers !== undefined && flags.strandedHumanAnswers.length > 0 ? { strandedHumanAnswers: flags.strandedHumanAnswers } : {}), ...(flags.effectiveReadFace !== undefined ? { effectiveReadFace: flags.effectiveReadFace } : {}), ...(flags.effectiveReadDenyPatterns !== undefined && flags.effectiveReadDenyPatterns.length > 0 ? { effectiveReadDenyPatterns: flags.effectiveReadDenyPatterns } : {}), ...(flags.effectiveMemoryScopes !== undefined ? { effectiveMemoryScopes: flags.effectiveMemoryScopes } : {}), ...(flags.effectiveReasoning !== undefined ? { effectiveReasoning: flags.effectiveReasoning } : {}), stats: publicStats };
165
+ return { taskId, ...(flags.runId !== undefined ? { runId: flags.runId } : {}), sessionId, terminal, ...(flags.model !== undefined ? { model: flags.model } : {}), result: result.trim(), salvagedOutput, ...(apiFailure !== undefined ? { apiFailure } : {}), ...(retryAfterMs !== undefined ? { retryAfterMs } : {}), ...(flags.rewindNotes !== undefined && flags.rewindNotes.length > 0 ? { rewindNotes: flags.rewindNotes } : {}), ...(flags.editedFiles !== undefined && flags.editedFiles.length > 0 ? { editedFiles: flags.editedFiles } : {}), ...(flags.haltedOnUserRejection === true ? { haltedOnUserRejection: true } : {}), ...(stampHaltedByUser ? { haltedByUser: true } : {}), ...(flags.remoteEnvFailures !== undefined && flags.remoteEnvFailures.length > 0 ? { remoteEnvFailures: [...flags.remoteEnvFailures] } : {}), ...(flags.strandedHumanAnswers !== undefined && flags.strandedHumanAnswers.length > 0 ? { strandedHumanAnswers: flags.strandedHumanAnswers } : {}), ...(flags.effectiveReadFace !== undefined ? { effectiveReadFace: flags.effectiveReadFace } : {}), ...(flags.effectiveReadDenyPatterns !== undefined && flags.effectiveReadDenyPatterns.length > 0 ? { effectiveReadDenyPatterns: flags.effectiveReadDenyPatterns } : {}), ...(flags.effectiveMemoryScopes !== undefined ? { effectiveMemoryScopes: flags.effectiveMemoryScopes } : {}), ...(flags.effectiveReasoning !== undefined ? { effectiveReasoning: flags.effectiveReasoning } : {}), stats: publicStats };
185
166
  }
@@ -26,7 +26,7 @@ import type { CompactionForkContext } from "../auto-compaction.js";
26
26
  import type { AutoModeArmingRecipe } from "../auto-mode-arming.js";
27
27
  import type { AutoModeDecider, AutoModeDenialTracker } from "../auto-mode.js";
28
28
  import type { CacheBreakDetector, ToolFingerprintInput } from "../cache-break-detector.js";
29
- import type { Checkpoint, CheckpointGate, CheckpointState, CheckpointToken, PlatformLimitReason, ResourceLedger, ResourceLimitReason, SerializedCheckpointState } from "../checkpoint-store.js";
29
+ import type { Checkpoint, CheckpointState, CheckpointToken, PlatformLimitReason, ResourceLedger, ResourceLimitReason, SerializedCheckpointState } from "../checkpoint-store.js";
30
30
  import type { ClearedProjectionLedger, ContextEditMachine, OccurrenceIndex } from "../context-edit.js";
31
31
  import type { HookInvocationIdentity, Hooks, OrgGateVerdict, PermissionDeniedPayload, ToolGateInput } from "../hooks.js";
32
32
  import type { MaterializedMcp } from "../mcp.js";
@@ -144,6 +144,18 @@ export interface FileHistoryBoundarySeat {
144
144
  begin(entryId: string): void;
145
145
  settle(): Promise<void>;
146
146
  }
147
+ /**
148
+ * The commit-side facts of the run's ONE durable pause: the terminal cause the result assembles
149
+ * ({@link import("../types.js").PausedCause} — gate, token, checkpoint id, restore mode), the COMMITTED scope
150
+ * (read verbatim by the reap/cancel path — never re-derived from the spec, which the cross-scope re-scope
151
+ * may no longer match), and the id of the tool call the park is holding (only a `tool_approval` pending
152
+ * action parks ON a call; absent, never guessed, for the kinds that hold none).
153
+ */
154
+ export interface CommittedPause {
155
+ cause: import("../types.js").PausedCause;
156
+ scope: string;
157
+ gatedCallId?: string;
158
+ }
147
159
  export interface Prepared {
148
160
  harness: AgentHarness;
149
161
  /** The CONCRETE built-in session (engine-internal: prepare constructs/acquires `StoredSession` itself,
@@ -221,17 +233,12 @@ export interface Prepared {
221
233
  *
222
234
  * It is a sideband and not a field on the tool RESULT because a result is not a trustworthy carrier
223
235
  * for this: `details` is arbitrary tool-authored data that post-tool hooks may also replace, so a
224
- * failing tool could stamp `settledBy:"human"` on itself and tell a consumer's audit view that a
225
- * person approved something nobody was ever shown. Provenance has to travel on a channel only the
236
+ * failing tool could stamp a settlement on itself and tell a consumer's audit view that a person
237
+ * approved something nobody was ever shown. Provenance has to travel on a channel only the
226
238
  * adjudicating layer can write. Same reason the entries are keyed by CALL id: the gate adjudicated
227
239
  * that exact call, and the frame that reads it is that call's own.
228
240
  */
229
- approvalSettlement: Map<string, {
230
- settledBy?: import("../tool-policy.js").ApprovalSettledBy;
231
- approver?: string;
232
- resolution?: import("../tool-policy.js").AskDenyResolution;
233
- autoDenied?: true;
234
- }>;
241
+ gateOutcomes: Map<string, import("../gate-outcome.js").GateOutcome>;
235
242
  /**
236
243
  * The parent-thread human-rejection halt fact (see `maybeHumanRejectionHalt`): present from the
237
244
  * moment a bare human rejection halts the turn's batch until the run ends or the NEXT provider
@@ -554,24 +561,15 @@ export interface Prepared {
554
561
  * and the Runner must tear down on task end. Undefined when the env came from a (caller-owned) static
555
562
  * `deps.executionEnv` or the stub — those outlive the task and must NOT be destroyed here. */
556
563
  ownedEnv?: ExecutionEnv;
557
- /** design/45: a mutable holder the durable-suspend gate writes when a policy `ask` was checkpointed
558
- * (capture + abort). The run loop reads it to assemble `status:"suspended"`. Empty unless a suspension
559
- * fired this run.
560
- *
561
- * `gatedCallId` is the id of the tool call the committed gate is holding read straight off the
562
- * committed checkpoint's `tool_approval` pendingAction, so the id a contaminated sibling frame names
563
- * and the id the checkpoint parks on are the SAME value by construction, not by convention. ABSENT
564
- * (never guessed) for a park that binds no tool call at all — a `resource_limit` slice and a
565
- * `plan_review` pause both have pendingActions with no tool call, so there is no causal id to name.
566
- * Written unconditionally by the commit-side publisher precisely so a later park cannot inherit an
567
- * earlier one's id. */
568
- suspendRef: {
569
- token?: CheckpointToken;
570
- checkpointId?: string;
571
- gate?: CheckpointGate;
572
- scope?: string;
573
- restoreMode?: "snapshot" | "park_only";
574
- gatedCallId?: string;
564
+ /** The run's ONE committed durable pause ({@link CommittedPause}): a mutable holder the commit-side
565
+ * publisher (`publishCommittedSuspend`) writes exactly once per run when a park commits an approval
566
+ * or content ask, a resource slice, a platform pause, a plan or dry-run review — and the run loop reads
567
+ * to assemble the result's `terminal: { kind: "paused", … }`. WHICH pause it is is the cause's own
568
+ * `gate.kind` (a registry row), not the holder it landed in: the two same-shaped holders that used to
569
+ * split the approval family from the review family, and the commit-side routing between them, are
570
+ * gone. `current` is absent until a park commits. */
571
+ pausedRef: {
572
+ current?: CommittedPause;
575
573
  };
576
574
  /** Ruled 2026-08-05 (matrix ruling arm A): set true by the resume engine when this leg EXECUTES the
577
575
  * approved pending call — the restart-loop cap then counts from a fresh base (consecutive
@@ -579,22 +577,6 @@ export interface Prepared {
579
577
  suspendProgressRef: {
580
578
  executedApproved: boolean;
581
579
  };
582
- /** design/76 §2.5 (dry-run / shadow) + design/80 D-B (plan-gate): the DUAL of {@link suspendRef} for the
583
- * REVIEW-PAUSE family — a `{kind:"needs_review"}` pause (a profile's dry-run interception committed a
584
- * checkpoint whose predicted state-diff a human/judge must REVIEW) OR a `{kind:"plan_review"}` pause (a
585
- * profile's plan-gate committed a checkpoint whose proposed PLAN a human must approve/edit/reject). The
586
- * commit-side discriminant (`publishCommittedSuspend`) writes HERE for a `needs_review` OR `plan_review`
587
- * gate and into {@link suspendRef} for every other gate kind — **never both** (else assemble-result's slot
588
- * 8.6 `needs_review` branch is dead code, v4 MAJOR-A). The run loop reads it to assemble
589
- * `status:"needs_review"`. Empty unless a review pause fired this run. */
590
- reviewRef: {
591
- token?: CheckpointToken;
592
- checkpointId?: string;
593
- gate?: CheckpointGate;
594
- scope?: string;
595
- restoreMode?: "snapshot" | "park_only";
596
- gatedCallId?: string;
597
- };
598
580
  /** RB-439-a: the remote-workspace lifecycle failures this run hit, appended in call order and echoed
599
581
  * verbatim on `TaskResult.remoteEnvFailures`. A shared array (not a per-call return) because a suspend
600
582
  * refusal is reported through the deployment's `onError` side channel and the run then continues or
@@ -614,7 +596,7 @@ export interface Prepared {
614
596
  * results, and a remote — or static caller-owned, never per-task-stub — env). The run loop calls it at a
615
597
  * CLEAN turn boundary when a resource limit (turns/budget/walltime) was hit: it mints a `resource_limit`
616
598
  * checkpoint + pauses the workspace + stops the loop cleanly (NOT abort). Returns true iff it committed a
617
- * resumable checkpoint (sets `suspendRef`); false ⇒ caller falls through to normal limit handling.
599
+ * resumable checkpoint (publishes the committed pause); false ⇒ caller falls through to normal limit handling.
618
600
  * `sliceSpend` (Slice 4) is THIS slice's cost/tokens/turns, debited onto the cross-slice ledger. */
619
601
  suspendForResource?: (reason: ResourceLimitReason, sliceSpend: {
620
602
  costMicroUsd: number;
@@ -685,7 +667,7 @@ export interface Prepared {
685
667
  };
686
668
  /** design/80 D-B: present (≠ undefined) ONLY when a `checkpointStore` is wired (the deployment can pause). The
687
669
  * run loop calls it at a CLEAN turn boundary when `reviewRequestRef.pending` is set: it mints a `plan_review`
688
- * checkpoint (`status:"needs_review"`, routes to `reviewRef`) + pauses the workspace + aborts the loop, reusing
670
+ * checkpoint (`status:"needs_review"` by its registry row) + pauses the workspace + aborts the loop, reusing
689
671
  * the SAME commit saga as the human/resource suspends. Returns true iff it committed a resumable checkpoint;
690
672
  * false ⇒ the request could not be honored (caller drops it and continues). */
691
673
  suspendForReview?: (reason?: string) => Promise<boolean>;
@@ -1298,6 +1280,25 @@ export interface InheritedGate {
1298
1280
  hookEnv?: unknown;
1299
1281
  }>;
1300
1282
  }
1283
+ /** The runtime feature flags the stable prompt's harness-context block gates its sections on
1284
+ * (design/64 §6.3: claim only what the task has). Spread into the assembly's StablePromptContext
1285
+ * by the prompt-assembly phase, which re-derives the two orchestration flags against the post-exclusion roster. */
1286
+ export interface PromptFeatureFlags {
1287
+ policyEnabled: boolean;
1288
+ hooksEnabled: boolean;
1289
+ isolationEnabled: boolean;
1290
+ reminderMark: string;
1291
+ readFaceOpen: boolean;
1292
+ orchestrationEnabled: boolean;
1293
+ orchestrationDeferred: boolean;
1294
+ promptProfile: "simple" | "classic";
1295
+ fableMitigations: boolean;
1296
+ goalEnabled: boolean;
1297
+ awarenessEnabled: boolean;
1298
+ worktreeIsolated: boolean;
1299
+ withinTaskCompactionEnabled: boolean;
1300
+ isSubagent: boolean;
1301
+ }
1301
1302
  /**
1302
1303
  * design/78 Slice-1 (MAJOR-3 wiring): a TRUSTED, run-scoped internal channel into {@link prepareTask}, set
1303
1304
  * ONLY by a trusted CORE caller (`runRepairLoop` via the Runner's internal `runTaskStream` arg) — NEVER from
@@ -1322,25 +1323,6 @@ export interface InheritedGate {
1322
1323
  * them; the engine does not defend against it. The untrusted surface is {@link TaskSpec}, whose pre-first-
1323
1324
  * await latch is a separate, pinned guarantee (prepare-task-phase-pins T1).
1324
1325
  */
1325
- /** The runtime feature flags the stable prompt's harness-context block gates its sections on
1326
- * (design/64 §6.3: claim only what the task has). Spread into the assembly's StablePromptContext
1327
- * by the prompt-assembly phase, which re-derives the two orchestration flags against the post-exclusion roster. */
1328
- export interface PromptFeatureFlags {
1329
- policyEnabled: boolean;
1330
- hooksEnabled: boolean;
1331
- isolationEnabled: boolean;
1332
- reminderMark: string;
1333
- readFaceOpen: boolean;
1334
- orchestrationEnabled: boolean;
1335
- orchestrationDeferred: boolean;
1336
- promptProfile: "simple" | "classic";
1337
- fableMitigations: boolean;
1338
- goalEnabled: boolean;
1339
- awarenessEnabled: boolean;
1340
- worktreeIsolated: boolean;
1341
- withinTaskCompactionEnabled: boolean;
1342
- isSubagent: boolean;
1343
- }
1344
1326
  export interface RunInternals {
1345
1327
  /** The live repair bundle from a `runRepairLoop` attempt in flight (attemptCount>0). Serialized onto a
1346
1328
  * checkpoint minted MID-attempt so a resume re-seeds it; undefined for any non-repair run. */
@@ -148,7 +148,7 @@ export declare function judgeInheritedClassifier<A extends InheritedAsk>(opts: {
148
148
  subject: "this call" | "the approved edit";
149
149
  }): Promise<InheritedClassifierJudgment<A>>;
150
150
  /** The resolver's answer as the settlement reads it. */
151
- export type FallbackResolution = Pick<ResolvedAsk, "action" | "resolution" | "approverUnavailable">;
151
+ export type FallbackResolution = Pick<ResolvedAsk, "action" | "settlement" | "approverUnavailable">;
152
152
  /** A deny nobody made, at a wrapper arm: no approver wired, or a blanket `onAsk:"allow"` refused as
153
153
  * no approver. `approver_unavailable` is NOT headless here — it floats to the main gate, which owns
154
154
  * that arm (a park, or the fail-closed refusal). */
@@ -53,7 +53,7 @@ export function createDenialLimitStop(opts) {
53
53
  };
54
54
  return { gateStopRef, stopForDenialLimit };
55
55
  }
56
- const INHERITED_STATION_FACTS = { contentQuestion: false, markedUnresolvable: false, org: undefined };
56
+ const INHERITED_STATION_FACTS = { contentQuestion: false, markedUnresolvable: false, org: undefined, tightened: undefined };
57
57
  function fallbackCarry(fallback, liveApprover, tracker) {
58
58
  if (fallback === undefined)
59
59
  return {};
@@ -97,10 +97,10 @@ export async function judgeInheritedClassifier(opts) {
97
97
  return { kind: "resolve", ask: minted, fallback, mintedHere: true, origin: askOriginOf(minted, INHERITED_STATION_FACTS) };
98
98
  }
99
99
  export function headlessDenyAtFold(r) {
100
- return r.resolution === "no_approver" || r.resolution === "blanket_allow_refused";
100
+ return r.settlement?.kind === "no_approver" || r.settlement?.kind === "blanket_allow_refused";
101
101
  }
102
102
  export function headlessDenyAtRecheck(r) {
103
- return headlessDenyAtFold(r) || r.resolution === "approver_unavailable" || r.approverUnavailable === true;
103
+ return headlessDenyAtFold(r) || r.settlement?.kind === "approver_unavailable" || r.approverUnavailable === true;
104
104
  }
105
105
  export function settleDenialLimitFallback(opts) {
106
106
  const { fallback, resolved } = opts;
@@ -0,0 +1,74 @@
1
+ /**
2
+ * The ONE mint of a {@link GateOutcome} — the record of what a tool-gate pass decided, what ask it settled
3
+ * and who asked. The gate accumulates two facts on its way to an exit (WHO refused, written once at the
4
+ * refusing site; WHAT was settled, written at the resolution site) and hands them here; this module
5
+ * assembles the record, runs the invariant screen, and refuses a record the engine composed wrong. Every
6
+ * projection of the outcome (the deny observer's payload, the call's `tool_end` frame, the engine's
7
+ * per-call sideband) is the object minted here, never a re-derivation.
8
+ *
9
+ * A machinery module on the prepare path's first stratum: it reaches down for the vocabulary only.
10
+ */
11
+ import { type DeniedBy, type GateOutcome, type Settlement } from "../gate-outcome.js";
12
+ import type { AskOrigin } from "../ask-origin.js";
13
+ import { type PermissionResult, type ResolvedAsk } from "../tool-policy.js";
14
+ /** The ask a gate pass settled, as the gate captured it at its resolution site: the settlement the
15
+ * resolver composed and the origin word the ask carried. Both or neither (invariant I1). */
16
+ export interface SettledAsk {
17
+ settlement: Settlement;
18
+ origin: AskOrigin;
19
+ }
20
+ /** The facts a gate pass hands the mint. `deniedBy` is present exactly when the pass refused; `settled`
21
+ * exactly when the pass consumed an ask's settlement (a person's yes that was later vetoed included). */
22
+ export interface GateExitFacts {
23
+ deniedBy?: DeniedBy;
24
+ settled?: SettledAsk;
25
+ }
26
+ /**
27
+ * Mint the outcome. A defect here is an ENGINE defect (a refusing site that did not say who refused, a
28
+ * settlement without its origin, a person's yes beside a non-veto layer's deny) and throws: the gate's
29
+ * own composition is not a caller input, so there is no fail-closed "deny" to fall back to that would be
30
+ * more honest than the fault.
31
+ */
32
+ export declare function mintGateOutcome(facts: GateExitFacts): GateOutcome;
33
+ /** A direct refusal by one layer, with no ask settled — the five short-circuit exits (a hook that threw or
34
+ * never answered, the hook's own deny, the compliance lock, the plan-mode block). */
35
+ export declare function directDeny(deniedBy: Exclude<DeniedBy, "ask_resolution">): GateOutcome;
36
+ /**
37
+ * The gate's settlement LEDGER for one pass — the one place "which ask did this pass consume" is written,
38
+ * with ONE rule for every attestation read (the main adjudication, the approval-edit re-check, the gate's
39
+ * own ask resolutions), so the three sites cannot disagree about what an attested decision means:
40
+ * - an engine attestation on a decision object (the approval factory's wait ended inside a policy
41
+ * `check()`; an inherited lane resolved an ancestor's ask) is a settled ask of THIS call — read off the
42
+ * identity-keyed seat, never off a caller-composable member;
43
+ * - a deny carrying a REFUSAL attestation is the ask's own resolution (`ask_resolution`); a deny carrying a
44
+ * person's YES is a later layer's VETO (the yes stays on the settlement, the layer is the disposition —
45
+ * invariant I3);
46
+ * - the LATEST resolution wins (a re-check refusal replaces the first approval; a re-approval renews it);
47
+ * - a SURVIVING ask supersedes any settlement read off an earlier decision (a tighten minted a new
48
+ * question; whoever resolves it settles it afresh).
49
+ * For a resolver's object the attestation outranks the exposed member; the member is the fallback for a
50
+ * resolver seat that composes without attesting (a hand-built resolver; the ask lane's own intercept
51
+ * deny). TRUST BOUNDARY, stated: `ToolGateInput.resolveAsk` is wired by prepare from the engine's own
52
+ * resolver — a wrapper that spreads a resolved object (dropping the identity-keyed attestation) and
53
+ * REPLACES the member is inside the engine's trust line, not a deployment's; making the member
54
+ * attestation-only is the follow-up that migrates the ask lane's composition and the test resolvers
55
+ * onto an engine-owned mint.
56
+ */
57
+ export interface SettlementLedger {
58
+ /** The ask this pass consumed, or undefined when no ask was settled (a direct deny, a plain allow). */
59
+ readonly settled: SettledAsk | undefined;
60
+ /** An engine-composed settlement the gate itself minted (the unshown content ask's refusal). */
61
+ set(settled: SettledAsk): void;
62
+ /** Record an ask's resolution: the resolver's settlement beside the ask's own origin word. */
63
+ fromResolved(resolved: ResolvedAsk, asked: PermissionResult, toolName: string): void;
64
+ /** Read an adjudication's attestation. For a deny, returns the refusing layer word (`ask_resolution`
65
+ * for an attested refusal, `policy` otherwise — a veto when a yes rides the settlement); undefined
66
+ * for an allow or an ask. */
67
+ fromDecision(decision: PermissionResult): DeniedBy | undefined;
68
+ /** A surviving ask supersedes whatever an earlier decision settled. */
69
+ supersede(): void;
70
+ }
71
+ export declare function createSettlementLedger(call: {
72
+ toolCallId: string;
73
+ toolName: string;
74
+ }): SettlementLedger;
@@ -0,0 +1,55 @@
1
+ import { screenGateOutcome, SETTLEMENT_IS_REFUSAL } from "../gate-outcome.js";
2
+ import { engineSettlementOf } from "../tool-policy.js";
3
+ export function mintGateOutcome(facts) {
4
+ const settlement = facts.settled !== undefined ? Object.freeze({ ...facts.settled.settlement, who: Object.freeze({ ...facts.settled.settlement.who }) }) : undefined;
5
+ const outcome = Object.freeze({
6
+ disposition: Object.freeze(facts.deniedBy !== undefined ? { kind: "denied", deniedBy: facts.deniedBy } : { kind: "allowed" }),
7
+ ...(facts.settled !== undefined ? { settlement: settlement, origin: facts.settled.origin } : {}),
8
+ });
9
+ const defects = screenGateOutcome(outcome);
10
+ if (defects.length > 0)
11
+ throw new Error(`the tool gate composed an outcome that violates its own record: ${defects.join("; ")}`);
12
+ return outcome;
13
+ }
14
+ export function directDeny(deniedBy) {
15
+ return mintGateOutcome({ deniedBy });
16
+ }
17
+ export function createSettlementLedger(call) {
18
+ let settled;
19
+ return {
20
+ get settled() {
21
+ return settled;
22
+ },
23
+ set(next) {
24
+ settled = next;
25
+ },
26
+ fromResolved(resolved, asked, toolName) {
27
+ if (asked.action !== "ask" || asked.origin === undefined) {
28
+ if (resolved.settlement === undefined)
29
+ return;
30
+ throw new Error(`an ask for "${toolName}" was resolved without an origin word — the origin stamp precedes every resolution`);
31
+ }
32
+ const attested = engineSettlementOf(resolved, call);
33
+ if (attested !== undefined) {
34
+ settled = { settlement: attested.settlement, origin: asked.origin };
35
+ return;
36
+ }
37
+ if (resolved.settlement === undefined)
38
+ return;
39
+ settled = { settlement: resolved.settlement, origin: asked.origin };
40
+ },
41
+ fromDecision(decision) {
42
+ if (decision.action === "ask")
43
+ return undefined;
44
+ const attested = engineSettlementOf(decision, call);
45
+ if (attested !== undefined)
46
+ settled = { settlement: attested.settlement, origin: attested.origin };
47
+ if (decision.action !== "deny")
48
+ return undefined;
49
+ return attested !== undefined && SETTLEMENT_IS_REFUSAL[attested.settlement.kind] ? "ask_resolution" : "policy";
50
+ },
51
+ supersede() {
52
+ settled = undefined;
53
+ },
54
+ };
55
+ }
@@ -55,23 +55,23 @@ export declare function sanitizedTtlMs(ttlMs: number | undefined): number | unde
55
55
  * SINGLE derivation on purpose. Two consumers need this answer: the abort-classification seam, which
56
56
  * stamps it into the `details` of the results the loop mints for the contaminated siblings, and the
57
57
  * `tool_end` projection, which puts it on the wire. Reading it from one function keeps the two from
58
- * disagreeing about WHICH HOLDER WINS or about what a holder with no call means. They are not otherwise
58
+ * disagreeing about what a pause with no call means. They are not otherwise
59
59
  * interchangeable: the frame face applies strictly narrower conditions on top of this answer (see
60
60
  * `tool_end.gatedCallId`), so a frame may omit an id this function returns — never the reverse.
61
+ * "Which holder wins" is no longer a question: there is one holder, and a pause that holds no call
62
+ * publishes absence.
61
63
  *
62
64
  * The frame side must not read the id back out of a tool RESULT even though the marker is there: a
63
65
  * result's `details` is written by the tool (and replaceable by post-tool hooks), so lifting a
64
66
  * cross-call attribution from it would let any failing tool name an arbitrary call and put a phantom
65
- * approval wait on someone else's frame. Same rule, same reason, as `settledBy`.
67
+ * approval wait on someone else's frame. Same rule, same reason, as `tool_end.gate`.
66
68
  *
67
- * The two holders are never both set (the commit-side discriminant writes exactly one); they are read
68
- * here in assemble-result's slot order so the winner is the same one the terminal status is built from.
69
69
  */
70
- export declare function gatedCallIdOf(p: Pick<Prepared, "suspendRef" | "reviewRef">): string | undefined;
70
+ export declare function gatedCallIdOf(p: Pick<Prepared, "pausedRef">): string | undefined;
71
71
  /**
72
72
  * The abort-classification marker for the loop's short-circuit "Operation aborted" family: when a gate
73
- * committed a durable park this run (`suspendRef` for human/irreversible/content asks, `reviewRef` for
74
- * the dry-run/plan-review family), the contaminated sibling frames carry something a consumer can key
73
+ * committed a durable park this run (`pausedRef.current`, whatever the pause's kind), the contaminated
74
+ * sibling frames carry something a consumer can key
75
75
  * on instead of parsing the (contract-stable) result text (ruled 2026-08-04). The commit is published
76
76
  * BEFORE the abort fires, so this read is race-free at mint time.
77
77
  *
@@ -83,26 +83,20 @@ export declare function gatedCallIdOf(p: Pick<Prepared, "suspendRef" | "reviewRe
83
83
  * Module-level rather than a closure in `prepareTask` (design/238 D-7: extract, don't accrete) — the
84
84
  * marker's shape and its rules belong beside {@link gatedCallIdOf}, the derivation it reads.
85
85
  */
86
- export declare function parkContaminationMarker(refs: Pick<Prepared, "suspendRef" | "reviewRef">): Record<string, unknown> | undefined;
86
+ export declare function parkContaminationMarker(refs: Pick<Prepared, "pausedRef">): Record<string, unknown> | undefined;
87
87
  /**
88
- * design/76 §2.5 — THE COMMIT-SIDE DISCRIMINANT (v4 MAJOR-A, the load-bearing dry-run fix). Called
89
- * AFTER `commitSuspendSaga` returns true (the checkpoint is durably committed) to publish which holder
90
- * the run loop reads for the terminal status. Branch on the COMMITTED `gate.kind`: a REVIEW-PAUSE gate
91
- * writes `reviewRef` (→ assemble-result slot 8.6 `status:"needs_review"` / `review.pending`); every
92
- * approval/resource gate (human / irreversible_ask / resource_limit / task_done) writes `suspendRef` as
93
- * before ( slot 8.5 `status:"suspended"`). NEVER both a review pause that also set `suspendRef`
94
- * would assemble as `"suspended"` and make slot 8.6 dead code. The review-pause family is BOTH the
95
- * dry-run `{kind:"needs_review"}` gate (design/76 §2.5) AND the design/80 D-B `{kind:"plan_review"}`
96
- * gate — both are human-review pauses that share the `needs_review` terminal (a `plan_review` resumes
97
- * with a `plan_review` outcome, a `needs_review` with `dry_run_review`; the distinct gate.kind keeps the
98
- * resume discriminant unambiguous). (The review checkpoints themselves are minted by a PROFILE, out of
99
- * core scope; core only owns this routing seam + the gate/status/resume discriminants.)
88
+ * THE COMMIT-SIDE PUBLICATION. Called AFTER `commitSuspendSaga` returns true (the checkpoint is durably
89
+ * committed) to publish the run's one committed pause for the run loop's terminal assembly. There is no
90
+ * routing: the cause carries the committed gate whole, and WHICH terminal status it reads
91
+ * (`suspended` / `needs_review`) is the gate kind's registry row the two holders the review family and
92
+ * the approval family used to be split across, and the branch that chose between them, are gone.
93
+ * (Review checkpoints themselves are minted by a PROFILE, out of core scope; core owns this seam and the
94
+ * gate/status/resume discriminants.)
100
95
  *
101
- * Module-level, taking the holders as a parameter (design/238 D-7: extract, don't accrete) — the
102
- * closure form captured nothing else, so hoisting it costs nothing and puts the discriminant beside the
96
+ * Module-level, taking the holder as a parameter (design/238 D-7: extract, don't accrete) — beside the
103
97
  * two readers of what it publishes ({@link gatedCallIdOf}, {@link parkContaminationMarker}).
104
98
  */
105
- export declare function publishCommittedSuspend(refs: Pick<Prepared, "suspendRef" | "reviewRef">, token: CheckpointToken, gate: CheckpointGate, scope: string, remoteHandle: WorkspaceHandle | undefined, checkpointId?: string,
99
+ export declare function publishCommittedSuspend(refs: Pick<Prepared, "pausedRef">, token: CheckpointToken, gate: CheckpointGate, scope: string, remoteHandle: WorkspaceHandle | undefined, checkpointId?: string,
106
100
  /** The COMMITTED row's pendingAction — passed whole, not as a pre-extracted id, so no call site can
107
101
  * hand this seam an id the committed row does not actually park on. */
108
102
  pendingAction?: PendingAction): void;