@sema-agent/core 7.8.0 → 7.9.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 (103) hide show
  1. package/CHANGELOG.md +75 -1
  2. package/dist/agents/subagent.d.ts +1 -1
  3. package/dist/core/ask-origin.d.ts +16 -1
  4. package/dist/core/ask-origin.js +5 -1
  5. package/dist/core/checkpoint-store.d.ts +36 -35
  6. package/dist/core/effective-path-target.d.ts +43 -0
  7. package/dist/core/effective-path-target.js +56 -0
  8. package/dist/core/engine-notice.d.ts +8 -0
  9. package/dist/core/fs-write-gate-policy.js +2 -1
  10. package/dist/core/gate-lanes.d.ts +1 -0
  11. package/dist/core/gate-lanes.js +60 -28
  12. package/dist/core/gate-outcome.d.ts +8 -4
  13. package/dist/core/gate-outcome.js +2 -1
  14. package/dist/core/governance-codes.d.ts +1 -1
  15. package/dist/core/governance-codes.js +2 -0
  16. package/dist/core/hooks.d.ts +66 -32
  17. package/dist/core/mcp-injection-drop.d.ts +74 -0
  18. package/dist/core/mcp-injection-drop.js +27 -0
  19. package/dist/core/permission-rule-consent.d.ts +29 -36
  20. package/dist/core/permission-rule-consent.js +110 -60
  21. package/dist/core/permission-rule-model.d.ts +311 -96
  22. package/dist/core/permission-rule-model.js +309 -50
  23. package/dist/core/permission-rule-org.d.ts +19 -12
  24. package/dist/core/permission-rule-org.js +13 -16
  25. package/dist/core/permission-rule-provider.d.ts +10 -6
  26. package/dist/core/permission-rule-provider.js +12 -8
  27. package/dist/core/permission-rule-session.d.ts +7 -6
  28. package/dist/core/permission-rule-session.js +29 -10
  29. package/dist/core/permission-rule-store.d.ts +40 -18
  30. package/dist/core/permission-rule-store.js +68 -42
  31. package/dist/core/permission-rule-sync.d.ts +9 -3
  32. package/dist/core/permission-rule-sync.js +29 -25
  33. package/dist/core/permission-rule-syntax.d.ts +30 -0
  34. package/dist/core/permission-rule-syntax.js +44 -0
  35. package/dist/core/permission-rules.d.ts +56 -33
  36. package/dist/core/permission-rules.js +88 -55
  37. package/dist/core/persisted-rule-arms.d.ts +61 -0
  38. package/dist/core/persisted-rule-arms.js +50 -0
  39. package/dist/core/runner/active-skill-scope.js +2 -1
  40. package/dist/core/runner/contracts.d.ts +21 -2
  41. package/dist/core/runner/permission-rule-lanes.d.ts +42 -27
  42. package/dist/core/runner/permission-rule-lanes.js +25 -21
  43. package/dist/core/runner/prepare-gate-stations.js +1 -1
  44. package/dist/core/runner/prepare-safety-scan.js +8 -2
  45. package/dist/core/runner/prepare-task.js +1 -1
  46. package/dist/core/runner/resume-admission.d.ts +53 -0
  47. package/dist/core/runner/resume-admission.js +83 -0
  48. package/dist/core/runner/resume-apply.d.ts +50 -0
  49. package/dist/core/runner/resume-apply.js +184 -0
  50. package/dist/core/runner/resume-checkpoint-screen.d.ts +18 -0
  51. package/dist/core/runner/resume-checkpoint-screen.js +108 -0
  52. package/dist/core/runner/resume-claim.d.ts +32 -0
  53. package/dist/core/runner/resume-claim.js +27 -0
  54. package/dist/core/runner/resume-internals-and-config.d.ts +33 -0
  55. package/dist/core/runner/resume-internals-and-config.js +50 -0
  56. package/dist/core/runner/resume-policy-outcome.d.ts +31 -0
  57. package/dist/core/runner/resume-policy-outcome.js +127 -0
  58. package/dist/core/runner/resume-preflight.d.ts +40 -0
  59. package/dist/core/runner/resume-preflight.js +122 -0
  60. package/dist/core/runner/resume-review-outcome.d.ts +30 -0
  61. package/dist/core/runner/resume-review-outcome.js +88 -0
  62. package/dist/core/runner/run-harness-handlers.js +1 -1
  63. package/dist/core/runner/runtask.d.ts +20 -0
  64. package/dist/core/runner/runtask.js +85 -734
  65. package/dist/core/runner/session-rule-policy.js +2 -1
  66. package/dist/core/runner/tool-end-body.d.ts +12 -5
  67. package/dist/core/runner/tool-end-body.js +5 -5
  68. package/dist/core/runner/tool-face-overlay.js +22 -3
  69. package/dist/core/runner/tool-output-projection.d.ts +9 -6
  70. package/dist/core/runner/tool-output-projection.js +1 -18
  71. package/dist/core/runner/turn-attachments.d.ts +2 -2
  72. package/dist/core/sensitive-path-policy.js +5 -3
  73. package/dist/core/shell-lexer.d.ts +47 -0
  74. package/dist/core/shell-lexer.js +478 -0
  75. package/dist/core/shell-scan.d.ts +60 -0
  76. package/dist/core/shell-scan.js +183 -0
  77. package/dist/core/shell-wrapper-table.d.ts +297 -0
  78. package/dist/core/shell-wrapper-table.js +58 -0
  79. package/dist/core/store-contracts/permission-rule-sync-contract.js +29 -10
  80. package/dist/core/tool-catalog-entries.js +6 -6
  81. package/dist/core/tool-face.d.ts +80 -4
  82. package/dist/core/tool-face.js +10 -0
  83. package/dist/core/tool-policy.d.ts +1 -6
  84. package/dist/core/tool-registry.d.ts +8 -11
  85. package/dist/core/tool-registry.js +5 -2
  86. package/dist/core/tool-roster.d.ts +20 -2
  87. package/dist/core/tool-roster.js +36 -8
  88. package/dist/index.d.ts +7 -4
  89. package/dist/index.js +6 -3
  90. package/dist/stores/file/checkpoint-store.js +1 -1
  91. package/dist/stores/file/permission-rule-store.d.ts +28 -23
  92. package/dist/stores/file/permission-rule-store.js +64 -16
  93. package/dist/tools/fs/bash-readonly-classifier.d.ts +1 -1
  94. package/dist/tools/fs/fs-pdf.d.ts +1 -1
  95. package/dist/tools/fs/fs-search-tools.d.ts +3 -2
  96. package/dist/tools/fs/fs-search-tools.js +17 -9
  97. package/dist/tools/fs/index.js +1 -1
  98. package/dist/tools/fs/notebook.d.ts +1 -1
  99. package/dist/tools/fs/safety.d.ts +1 -1
  100. package/dist/tools/fs/search.d.ts +0 -8
  101. package/dist/tools/fs/search.js +0 -23
  102. package/package.json +1 -1
  103. package/test/export-surface.snapshot.json +99 -11
@@ -0,0 +1,40 @@
1
+ /**
2
+ * design/393 S3 — the resume ladder's deployment PREFLIGHT rung (E9), verbatim from `Runner.resumeStream`:
3
+ * the LAST rung before the CAS. The deployment's `resumePreflight` screens the resume off the persisted row's
4
+ * facts under its own validated deadline; every non-answer arm refuses retry-later (the row stays pending);
5
+ * only an explicit, fully readable `terminal` verdict settles the row through the single-shot expire CAS
6
+ * (with the reap duty that win carries), and the window verdict is re-asserted after the hook's await.
7
+ *
8
+ * Async (the seat, the expire, the loss re-read, the bounded destroy). It ends by handing off to the rung
9
+ * after it (`next`) in its LAST continuation — the claim opens on the tick the preflight cleared — and
10
+ * returns what that rung returns.
11
+ */
12
+ import { type Checkpoint, type CheckpointStore, type CheckpointToken } from "../checkpoint-store.js";
13
+ import type { InheritedGate, RunnerDepsSeat, SuspendReap } from "./contracts.js";
14
+ export interface ResumePreflightInput<R> {
15
+ /** borrowed-readonly — the checkpoint token (handed to the deployment and to the settling CAS). */
16
+ token: CheckpointToken;
17
+ /** borrowed-readonly — the pending row; the deployment reads its persisted facts, never the caller's bag. */
18
+ cp: Checkpoint;
19
+ /** borrowed-readonly — the ONE signal read from admission; the seat's wait is bounded by it. */
20
+ resumeSignal: AbortSignal | undefined;
21
+ /** borrowed-readonly — the window verdict to re-assert after the seat's await (undefined when unarmed). */
22
+ recheckGovernanceWindow: (() => Promise<void>) | undefined;
23
+ /** borrowed-readonly — the store the row lives in (the terminal arm's expire and its loss re-read). */
24
+ store: CheckpointStore;
25
+ /** borrowed-readonly — the Runner's deployment deps, read LIVE through the Runner (reads `resumePreflight`, `resumePreflightTimeoutMs`, `onError` (after the seat's await too) and is the seat's owner); the one home of why it is a
26
+ * seat and not a captured object is {@link RunnerDepsSeat}. */
27
+ runner: RunnerDepsSeat;
28
+ /** borrowed-mutable — the Runner's same-Runner claim marks: opened with the expire attempt, released at the duty decision. */
29
+ locallyClaimedTokens: Set<CheckpointToken>;
30
+ /** borrowed-mutable — the Runner's parked-constraint registry: the token's entry is evicted when the terminal arm wins. */
31
+ parentConstraintRegistry: Map<CheckpointToken, NonNullable<InheritedGate["parentConstraints"]>>;
32
+ /** borrowed-mutable — the Runner's suspended-env reap handles: the winning terminal arm takes and destroys this token's. */
33
+ suspendedEnvReaps: Map<CheckpointToken, SuspendReap>;
34
+ /** borrowed-readonly — the rung after the preflight, entered in this rung's last continuation. */
35
+ next: (screened: ResumePreflightResult) => Promise<R>;
36
+ }
37
+ /** The preflight's completion is its whole result: every refusal throws, so reaching `next` IS the fact. */
38
+ export interface ResumePreflightResult {
39
+ }
40
+ export declare function resumePreflight<R>(input: ResumePreflightInput<R>): Promise<R>;
@@ -0,0 +1,122 @@
1
+ import { CheckpointError } from "../checkpoint-store.js";
2
+ import { MAX_HOOK_TIMEOUT_MS, runHookSeat } from "../hooks.js";
3
+ import { hasDestroy } from "../remote-env.js";
4
+ import { inlineUntrusted } from "../untrusted-text.js";
5
+ import { describeSuppliedValue } from "./decide-continuation.js";
6
+ import { settleTeardownLeg } from "./teardown-bounded.js";
7
+ const RESUME_PREFLIGHT_DEFAULT_TIMEOUT_MS = 10_000;
8
+ export async function resumePreflight(input) {
9
+ const { token, cp, resumeSignal, recheckGovernanceWindow, store, runner, locallyClaimedTokens, parentConstraintRegistry, suspendedEnvReaps } = input;
10
+ if (runner.deps.resumePreflight !== undefined) {
11
+ const preflight = runner.deps.resumePreflight;
12
+ const rawDeadline = runner.deps.resumePreflightTimeoutMs;
13
+ let preflightTimeoutMs = RESUME_PREFLIGHT_DEFAULT_TIMEOUT_MS;
14
+ if (rawDeadline !== undefined) {
15
+ if (typeof rawDeadline === "number" && Number.isFinite(rawDeadline) && rawDeadline > 0 && rawDeadline <= MAX_HOOK_TIMEOUT_MS) {
16
+ preflightTimeoutMs = rawDeadline;
17
+ }
18
+ else {
19
+ try {
20
+ runner.deps.onError?.(new Error(`RunnerDeps.resumePreflightTimeoutMs is not a positive finite number within the seat ceiling of ${MAX_HOOK_TIMEOUT_MS}ms (got ${describeSuppliedValue(rawDeadline)}) — the resume preflight falls back to its own ${RESUME_PREFLIGHT_DEFAULT_TIMEOUT_MS}ms default`), { phase: "config", sessionId: cp.sessionId });
21
+ }
22
+ catch {
23
+ }
24
+ }
25
+ }
26
+ const info = {
27
+ token,
28
+ sessionId: cp.sessionId,
29
+ ...(cp.state.placementRootSessionId !== undefined ? { placementRootSessionId: cp.state.placementRootSessionId } : {}),
30
+ ...(cp.principal ? { principal: cp.principal } : {}),
31
+ ...(cp.state.workspaceHandle !== undefined ? { workspaceHandle: structuredClone(cp.state.workspaceHandle) } : {}),
32
+ gateKind: cp.gate.kind,
33
+ };
34
+ const refusalTail = "; the checkpoint stays pending and the same token is redeemable once the obstacle clears";
35
+ let verdict;
36
+ try {
37
+ const seat = await runHookSeat("resumePreflight", {
38
+ timeoutMs: preflightTimeoutMs,
39
+ ...(resumeSignal !== undefined ? { signal: resumeSignal } : {}),
40
+ abortEnds: true,
41
+ onBadTimeout: (badErr) => {
42
+ try {
43
+ runner.deps.onError?.(badErr instanceof Error ? badErr : new Error(String(badErr)), { phase: "hook", sessionId: cp.sessionId });
44
+ }
45
+ catch {
46
+ }
47
+ },
48
+ owner: runner.deps,
49
+ }, (sig) => preflight(info, sig));
50
+ if (seat.expired) {
51
+ throw new CheckpointError("resume.preflight_rejected", seat.cause === "timeout"
52
+ ? `the deployment's resumePreflight did not answer within its ${preflightTimeoutMs}ms deadline while screening this resume — refused fail-closed (retry-later arm)${refusalTail}`
53
+ : `the resume was cancelled while the deployment's resumePreflight was still screening it — refused (retry-later arm)${refusalTail}`);
54
+ }
55
+ verdict = seat.value;
56
+ }
57
+ catch (preflightErr) {
58
+ if (preflightErr instanceof CheckpointError)
59
+ throw preflightErr;
60
+ const err = preflightErr instanceof Error ? preflightErr : new Error(String(preflightErr));
61
+ try {
62
+ runner.deps.onError?.(err, { phase: "hook", sessionId: cp.sessionId });
63
+ }
64
+ catch {
65
+ }
66
+ throw new CheckpointError("resume.preflight_rejected", `the deployment's resumePreflight crashed while screening this resume (${inlineUntrusted(err.message)}) — refused fail-closed (retry-later arm)${refusalTail}`);
67
+ }
68
+ const verdictOk = verdict !== null && typeof verdict === "object" ? verdict.ok : undefined;
69
+ if (verdictOk !== true) {
70
+ const bag = verdict !== null && typeof verdict === "object" ? verdict : undefined;
71
+ const disposition = bag?.disposition;
72
+ const suppliedMessage = bag?.message;
73
+ const retryAfterMs = bag?.retryAfterMs;
74
+ const readableMessage = typeof suppliedMessage === "string" && suppliedMessage !== "" ? suppliedMessage : undefined;
75
+ const said = readableMessage !== undefined ? `: ${inlineUntrusted(readableMessage)}` : " (no readable verdict — fail-closed)";
76
+ if (disposition === "terminal" && readableMessage !== undefined) {
77
+ locallyClaimedTokens.add(token);
78
+ let settled;
79
+ try {
80
+ settled = await store.expire(token, cp.scope);
81
+ }
82
+ catch (expireErr) {
83
+ locallyClaimedTokens.delete(token);
84
+ throw expireErr;
85
+ }
86
+ if (!settled) {
87
+ locallyClaimedTokens.delete(token);
88
+ const live = await store.get(token);
89
+ if (live?.status === "pending") {
90
+ throw new CheckpointError("resume.preflight_rejected", `the deployment's resumePreflight ruled this resume PERMANENTLY blocked${said}, but the row moved under this refusal (a concurrent resolve/reopen cycle) — nothing was settled here; the row is pending again, re-resume against the current state (the preflight screens every attempt)`);
91
+ }
92
+ throw new CheckpointError("resume.preflight_rejected", `the deployment's resumePreflight ruled this resume PERMANENTLY blocked${said} — a concurrent actor had already settled the row (${describeSuppliedValue(live?.status ?? "consumed")}); nothing was settled by this refusal`);
93
+ }
94
+ parentConstraintRegistry.delete(token);
95
+ const terminalReap = suspendedEnvReaps.get(token);
96
+ suspendedEnvReaps.delete(token);
97
+ locallyClaimedTokens.delete(token);
98
+ if (terminalReap?.env !== undefined && hasDestroy(terminalReap.env)) {
99
+ const terminalEnv = terminalReap.env;
100
+ await settleTeardownLeg(() => terminalEnv.destroy(), "terminal-preflight env destroy", (envErr) => {
101
+ try {
102
+ runner.deps.onError?.(envErr, { phase: "config", sessionId: terminalReap.sessionId });
103
+ }
104
+ catch {
105
+ }
106
+ });
107
+ }
108
+ try {
109
+ runner.deps.onError?.(new Error(`resumePreflight ruled this row PERMANENTLY blocked${said} — the row was settled terminally (expired) by this refusal's single-shot CAS (token no longer redeemable)`), { phase: "hook", sessionId: cp.sessionId });
110
+ }
111
+ catch {
112
+ }
113
+ throw new CheckpointError("resume.preflight_rejected", `the deployment's resumePreflight ruled this resume PERMANENTLY blocked${said} — the row was settled terminally (expired by this refusal's single-shot CAS); the token is not redeemable`);
114
+ }
115
+ const waitHint = typeof retryAfterMs === "number" && Number.isFinite(retryAfterMs) && retryAfterMs >= 0 ? retryAfterMs : undefined;
116
+ throw new CheckpointError("resume.preflight_rejected", `the deployment's resumePreflight refused this resume${said}${refusalTail}${waitHint !== undefined ? ` (deployment wait hint: ${String(waitHint)}ms)` : ""}`, waitHint !== undefined ? { retryAfterMs: waitHint } : undefined);
117
+ }
118
+ if (recheckGovernanceWindow !== undefined)
119
+ await recheckGovernanceWindow();
120
+ }
121
+ return await input.next({});
122
+ }
@@ -0,0 +1,30 @@
1
+ /**
2
+ * design/393 S3 — the resume ladder's gate-match rung and the review / park TWINS (E3), verbatim from
3
+ * `Runner.resumeStream`: the outcome arm must match the persisted gate kind; a review-gate outcome
4
+ * (plan_review / dry_run_review) is captured into a plain twin with its domain and text-payload checks, the
5
+ * break-out-tag hygiene and the env_failed replay guard; the two tool-less park lanes (wake / resource_limit)
6
+ * get their twins too, so no lane hands the caller's object downstream. Synchronous; every refusal throws
7
+ * pre-CAS.
8
+ */
9
+ import { type Checkpoint, type PendingSteerEntry, type ResumeOutcome } from "../checkpoint-store.js";
10
+ export interface ResumeReviewOutcomeInput {
11
+ /** borrowed-readonly — the pending row (the lane is keyed on the STORED gate kind). */
12
+ cp: Checkpoint;
13
+ /** borrowed-readonly — the caller's outcome; each review field is read exactly once, into the twin. */
14
+ outcome: ResumeOutcome;
15
+ /** borrowed-readonly — the ONE read of the outcome discriminant, taken at admission. */
16
+ outcomeGate: ResumeOutcome["gate"];
17
+ /** borrowed-readonly — the validated wake message, carried into the wake twin. */
18
+ wakeMessage: Omit<PendingSteerEntry, "seq"> | undefined;
19
+ }
20
+ export interface ResumeReviewOutcomeResult {
21
+ /** The review lane's plain twin (a plan_review / needs_review row), else undefined. */
22
+ plainReviewOutcome: Extract<ResumeOutcome, {
23
+ gate: "plan_review" | "dry_run_review";
24
+ }> | undefined;
25
+ /** The tool-less park lanes' plain twin (a wake or a resource_limit continue), else undefined. */
26
+ plainParkOutcome: Extract<ResumeOutcome, {
27
+ gate: "wake" | "resource_limit";
28
+ }> | undefined;
29
+ }
30
+ export declare function resumeReviewOutcome(input: ResumeReviewOutcomeInput): ResumeReviewOutcomeResult;
@@ -0,0 +1,88 @@
1
+ import { CheckpointError } from "../checkpoint-store.js";
2
+ import { isGateKind, PAUSE_REGISTRY, resumeGateMatches } from "../pause-registry.js";
3
+ import { sanitizeUntrustedText } from "../untrusted-text.js";
4
+ import { assertOutcomeText, describeSuppliedValue } from "./decide-continuation.js";
5
+ export function resumeReviewOutcome(input) {
6
+ const { cp, outcome, outcomeGate, wakeMessage } = input;
7
+ const gateMatch = isGateKind(cp.gate.kind) && resumeGateMatches(cp.gate.kind, outcomeGate);
8
+ if (!gateMatch) {
9
+ throw new CheckpointError("checkpoint.gate_mismatch", `resume outcome (gate "${describeSuppliedValue(outcomeGate)}") does not match checkpoint gate "${cp.gate.kind}" — resume serves ${Object.entries(PAUSE_REGISTRY)
10
+ .filter(([, row]) => !row.terminal)
11
+ .map(([k, row]) => `${k}/${row.resume}`)
12
+ .join(", ")}`);
13
+ }
14
+ let plainReviewOutcome;
15
+ if (cp.gate.kind === "plan_review") {
16
+ const decide = outcome;
17
+ const decision = decide.decision;
18
+ const editedPlan = decide.editedPlan;
19
+ const reason = decide.reason;
20
+ if (decision !== "approve" && decision !== "edit" && decision !== "reject") {
21
+ throw new CheckpointError("checkpoint.invalid_outcome", `resume decision "${describeSuppliedValue(decision)}" is outside the plan_review domain — a plan review is exactly "approve", "edit" or "reject"; refusing pre-CAS, the checkpoint stays pending`);
22
+ }
23
+ assertOutcomeText(editedPlan, "editedPlan");
24
+ assertOutcomeText(reason, "reason");
25
+ plainReviewOutcome = {
26
+ gate: "plan_review",
27
+ decision,
28
+ ...(editedPlan !== undefined ? { editedPlan } : {}),
29
+ ...(reason !== undefined ? { reason } : {}),
30
+ };
31
+ }
32
+ else if (cp.gate.kind === "needs_review") {
33
+ const decide = outcome;
34
+ const decision = decide.decision;
35
+ const reason = decide.reason;
36
+ if (decision !== "approve" && decision !== "reject") {
37
+ throw new CheckpointError("checkpoint.invalid_outcome", `resume decision "${describeSuppliedValue(decision)}" is outside the dry_run_review domain — a dry-run review is exactly "approve" or "reject"; refusing pre-CAS, the checkpoint stays pending`);
38
+ }
39
+ assertOutcomeText(reason, "reason");
40
+ plainReviewOutcome = {
41
+ gate: "dry_run_review",
42
+ decision,
43
+ ...(reason !== undefined ? { reason } : {}),
44
+ };
45
+ }
46
+ if (plainReviewOutcome !== undefined) {
47
+ const reason = plainReviewOutcome.reason;
48
+ if (plainReviewOutcome.decision === "reject" && reason && sanitizeUntrustedText(reason) !== reason) {
49
+ throw new CheckpointError("checkpoint.invalid_outcome", "resume deny/reject reason must not contain a </system-reminder> tag");
50
+ }
51
+ const capturedPlan = plainReviewOutcome.gate === "plan_review" ? plainReviewOutcome.editedPlan : undefined;
52
+ if (plainReviewOutcome.decision === "edit" &&
53
+ capturedPlan &&
54
+ sanitizeUntrustedText(capturedPlan) !== capturedPlan) {
55
+ throw new CheckpointError("checkpoint.invalid_outcome", "resume editedPlan must not contain a </system-reminder> tag");
56
+ }
57
+ if (cp.reopenReason === "env_failed") {
58
+ const winner = cp.resolvedOutcome;
59
+ if (winner === undefined) {
60
+ throw new CheckpointError("checkpoint.reopen_revote", "review checkpoint was reopened after an env-restore failure (env_failed) but carries no persisted winner to replay — refusing to resume (inconsistent row, fail-closed)");
61
+ }
62
+ if (winner.decision !== plainReviewOutcome.decision) {
63
+ throw new CheckpointError("checkpoint.reopen_revote", `an env_failed reopen replays the ALREADY-RECORDED review decision ("${winner.decision}") — refusing a re-vote with a different decision ("${plainReviewOutcome.decision}")`);
64
+ }
65
+ if (winner.updatedInput !== capturedPlan) {
66
+ throw new CheckpointError("checkpoint.reopen_revote", "an env_failed reopen replays the ALREADY-RECORDED review decision — refusing a re-vote whose edited plan differs from the recorded one");
67
+ }
68
+ if (winner.reason !== undefined && winner.reason !== plainReviewOutcome.reason) {
69
+ throw new CheckpointError("checkpoint.reopen_revote", "an env_failed reopen replays the ALREADY-RECORDED review decision — refusing a re-vote whose reviewer note differs from the recorded one");
70
+ }
71
+ }
72
+ }
73
+ let plainParkOutcome;
74
+ if (outcomeGate === "wake") {
75
+ plainParkOutcome = {
76
+ gate: "wake",
77
+ ...(wakeMessage !== undefined ? { message: wakeMessage } : {}),
78
+ };
79
+ }
80
+ else if (outcomeGate === "resource_limit") {
81
+ const decision = outcome.decision;
82
+ if (decision !== "continue") {
83
+ throw new CheckpointError("checkpoint.invalid_outcome", `resume decision "${describeSuppliedValue(decision)}" is outside the resource_limit domain — a slice resume is exactly "continue"; refusing pre-CAS, the checkpoint stays pending`);
84
+ }
85
+ plainParkOutcome = { gate: "resource_limit", decision };
86
+ }
87
+ return { plainReviewOutcome, plainParkOutcome };
88
+ }
@@ -261,7 +261,7 @@ export function createHarnessHandlers(input) {
261
261
  toolName: event.toolName,
262
262
  ...(toolLabels.get(event.toolName) !== undefined ? { label: toolLabels.get(event.toolName) } : {}),
263
263
  isError: event.isError,
264
- ...toolEndBodyFrom(event.result, event.isError, gate, prepared.abortController.signal.aborted ? gatedCallIdOf(prepared) : undefined),
264
+ ...toolEndBodyFrom(event.result, event.isError, prepared.structuredProjector, gate, prepared.abortController.signal.aborted ? gatedCallIdOf(prepared) : undefined),
265
265
  ...ident(),
266
266
  });
267
267
  announceWorkspaceMove();
@@ -56,6 +56,9 @@ export declare class Runner implements RunnerSelfSeat {
56
56
  * never a wrong destroy. */
57
57
  private readonly locallyClaimedTokens;
58
58
  private static readonly PARENT_CONSTRAINT_REGISTRY_CAP;
59
+ /** The lanes' LIVE view of `deps` ({@link RunnerDepsSeat}): a getter, never a captured object, because
60
+ * `swapModels` replaces the object and a rung reading after its own await must see the replacement. */
61
+ private readonly depsSeat;
59
62
  constructor(deps: RunnerDeps);
60
63
  /** design/73 §1 v1 — fire `RunnerDeps.onTaskOutcome` through the single swallow-guarded chokepoint
61
64
  * ({@link emitTaskOutcome}: mechanical-tier only, throwing sink never breaks the caller). Public so
@@ -227,6 +230,23 @@ export declare class Runner implements RunnerSelfSeat {
227
230
  * authority, so constraint substitution here is outside the enforced threat model (the count check
228
231
  * catches shape MISTAKES, not a untrusted trusted caller). */
229
232
  internals?: RunInternals): Promise<TaskStream>;
233
+ /**
234
+ * design/393 S3 — the resume decision ladder after admission (E3–E10), driving the eight `resume-*` lanes in
235
+ * the order the one-function ladder had: the review / park twins, the approval twin and its bindings, the
236
+ * checkpoint screen, the internals-and-config rungs (whose one window await is taken HERE, where the ladder
237
+ * took it), then the three async rungs after it — apply, preflight, claim — each entering the next in its own last
238
+ * continuation, and the post-claim hand-off to the resumed leg. Every rung throws its typed refusal pre-CAS;
239
+ * the first `return` is the leg's stream.
240
+ */
241
+ private resumeLadder;
242
+ /**
243
+ * design/393 S3 — the ladder's POST-CLAIM rung (E11), the driver's own: the post-consume hook, the answering
244
+ * face minted from the decide's answer, the resumed leg's spec (identity continuity, the existing-session
245
+ * requirement, the stripped pre-aborted preempt signal), the reopen compensation and the execution-record
246
+ * verb bound to the row, the two post-CAS abort re-checks, and the hand-off to `runTaskStream` with the plain
247
+ * twin. Entered in the claim rung's last continuation, so its first statement runs on the tick the win was read.
248
+ */
249
+ private resumeHandoff;
230
250
  /**
231
251
  * design/45 resume step: with the harness idle and the branch rewound to the suspension leaf, resolve
232
252
  * the gated tool call and close the rest of the suspended batch, all by appending `toolResult`s to the