@sema-agent/core 7.38.2 → 7.39.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 (98) hide show
  1. package/CHANGELOG.md +175 -6
  2. package/dist/core/ask-origin.d.ts +16 -7
  3. package/dist/core/ask-origin.js +4 -2
  4. package/dist/core/auto-mode-directive.d.ts +114 -19
  5. package/dist/core/auto-mode-directive.js +67 -19
  6. package/dist/core/checkpoint-store.d.ts +72 -41
  7. package/dist/core/checkpoint-store.js +28 -48
  8. package/dist/core/classifier-unavailable-arm.js +2 -2
  9. package/dist/core/decision-receipt.d.ts +2 -0
  10. package/dist/core/decision-receipt.js +1 -1
  11. package/dist/core/engine-notice.d.ts +41 -17
  12. package/dist/core/engine-notice.js +26 -14
  13. package/dist/core/gate-outcome.d.ts +80 -42
  14. package/dist/core/gate-outcome.js +21 -6
  15. package/dist/core/governance-codes.d.ts +1 -3
  16. package/dist/core/governance-codes.js +0 -3
  17. package/dist/core/hooks.d.ts +1 -1
  18. package/dist/core/hooks.js +2 -2
  19. package/dist/core/human-input-projection.d.ts +12 -0
  20. package/dist/core/human-input-projection.js +12 -0
  21. package/dist/core/memory-engine/engine.js +11 -11
  22. package/dist/core/memory-engine/layout.d.ts +21 -2
  23. package/dist/core/memory-engine/layout.js +17 -5
  24. package/dist/core/mode-policy.d.ts +26 -0
  25. package/dist/core/mode-policy.js +37 -1
  26. package/dist/core/output-style.d.ts +21 -0
  27. package/dist/core/output-style.js +29 -0
  28. package/dist/core/real-approval.d.ts +25 -0
  29. package/dist/core/real-approval.js +27 -0
  30. package/dist/core/reminder-mint.d.ts +0 -9
  31. package/dist/core/reminder-mint.js +0 -11
  32. package/dist/core/retired-keys.d.ts +2 -1
  33. package/dist/core/retired-keys.js +3 -1
  34. package/dist/core/runner/assemble-result.d.ts +2 -0
  35. package/dist/core/runner/assemble-result.js +1 -1
  36. package/dist/core/runner/compaction-restate.d.ts +45 -4
  37. package/dist/core/runner/compaction-restate.js +89 -11
  38. package/dist/core/runner/contracts.d.ts +6 -6
  39. package/dist/core/runner/denial-limit-arms.js +4 -4
  40. package/dist/core/runner/inherited-ask-grants.d.ts +4 -2
  41. package/dist/core/runner/inherited-ask-grants.js +0 -3
  42. package/dist/core/runner/permission-rule-lanes.d.ts +18 -0
  43. package/dist/core/runner/permission-rule-lanes.js +35 -25
  44. package/dist/core/runner/prepare-context-lane.d.ts +1 -0
  45. package/dist/core/runner/prepare-context-lane.js +6 -0
  46. package/dist/core/runner/prepare-inherited-gate.js +2 -9
  47. package/dist/core/runner/prepare-task.js +10 -24
  48. package/dist/core/runner/prepare-wiring-manifest.js +2 -2
  49. package/dist/core/runner/prompt-block.d.ts +9 -0
  50. package/dist/core/runner/prompt-block.js +8 -0
  51. package/dist/core/runner/resume-admission.js +4 -1
  52. package/dist/core/runner/resume-apply.js +28 -18
  53. package/dist/core/runner/resume-policy-outcome.js +10 -4
  54. package/dist/core/runner/run-attachment-seats.js +1 -1
  55. package/dist/core/runner/run-identity-wiring.js +9 -2
  56. package/dist/core/runner/run-leg.js +42 -31
  57. package/dist/core/runner/run-settle-and-teardown.js +4 -3
  58. package/dist/core/runner/run-turn-boundary.js +4 -3
  59. package/dist/core/runner/runtask.d.ts +4 -2
  60. package/dist/core/runner/steer-admission.d.ts +9 -0
  61. package/dist/core/runner/steer-admission.js +8 -0
  62. package/dist/core/runner/stream-steer-verb.js +16 -11
  63. package/dist/core/runner/synthetic-tools.d.ts +4 -0
  64. package/dist/core/runner/tool-turn-repair-seat.js +1 -1
  65. package/dist/core/runner/turn-attachments.d.ts +16 -22
  66. package/dist/core/runner/turn-attachments.js +3 -20
  67. package/dist/core/runner-deps.d.ts +1 -1
  68. package/dist/core/store-contracts/checkpoint-store-contract.d.ts +8 -0
  69. package/dist/core/store-contracts/checkpoint-store-contract.js +23 -0
  70. package/dist/core/task-event.d.ts +7 -2
  71. package/dist/core/task-spec.d.ts +29 -7
  72. package/dist/core/terminal-cause.d.ts +44 -3
  73. package/dist/core/terminal-cause.js +5 -0
  74. package/dist/core/tool-policy.d.ts +55 -29
  75. package/dist/core/tool-policy.js +42 -31
  76. package/dist/core/types.d.ts +1 -1
  77. package/dist/core/types.js +1 -1
  78. package/dist/core/wiring-manifest.d.ts +17 -3
  79. package/dist/core/wiring-manifest.js +8 -3
  80. package/dist/engine/harness/agent-harness.d.ts +10 -0
  81. package/dist/engine/harness/agent-harness.js +12 -1
  82. package/dist/engine/llm/tool-turns.d.ts +15 -2
  83. package/dist/engine/llm/tool-turns.js +2 -2
  84. package/dist/index.d.ts +4 -3
  85. package/dist/index.js +4 -3
  86. package/dist/prompt-assembly/event-registry.js +2 -1
  87. package/dist/stores/file/fs-atomic.d.ts +29 -4
  88. package/dist/stores/file/fs-atomic.js +55 -6
  89. package/dist/tools/fs/index.d.ts +2 -1
  90. package/dist/tools/fs/read-deny-home.d.ts +3 -2
  91. package/dist/tools/fs/read-deny-home.js +6 -4
  92. package/dist/tools/fs/shell-read-check.d.ts +10 -3
  93. package/dist/tools/fs/shell-read-check.js +2 -2
  94. package/package.json +1 -1
  95. package/test/export-surface.snapshot.json +21 -1
  96. package/wire-changes/core@7.38.2.json +20 -2
  97. package/wire-changes/core@7.38.3.json +36 -0
  98. package/wire-changes/core@7.39.0.json +845 -0
@@ -1,35 +1,130 @@
1
1
  import type { AskEffective } from "./wiring-manifest.js";
2
- /** The permission-mode words the composition reads (the engine's closed set, as the wire spells it). */
3
2
  import type { PermissionMode } from "./ask-origin.js";
4
- /** The auto-mode directive body (title + the two paragraphs). Rendered when the run's auto-mode
5
- * INTENT is resolved true — the seat, the chain it inherited, or the bit a suspend recorded. */
3
+ import { type EngineRegionMessage } from "./reminder-mint.js";
4
+ /** The auto-mode frame (title + the two paragraphs). */
6
5
  export declare const AUTO_MODE_DIRECTIVE: string;
7
6
  /** The shell-first paragraph. The reference implementation serves it under three different headings;
8
7
  * the one heading this engine has a fact for is the bypass posture below. */
9
8
  export declare const BASH_FIRST_DIRECTIVE: string;
10
- /** The bypass-posture directive body: the reference implementation's heading over the shell-first
11
- * paragraph, the exact pair its bypass arm renders. */
9
+ /** The bypass-posture frame: the reference implementation's heading over the shell-first paragraph, the
10
+ * exact pair its bypass arm renders. */
12
11
  export declare const BYPASS_POSTURE_DIRECTIVE: string;
12
+ /** The auto-mode frame's exit: the reference exit renderer's long arm (the retired frame was not steer-only)
13
+ * with no shell-first sentence (the retired frame carried none). */
14
+ export declare const AUTO_MODE_EXIT_DIRECTIVE: string;
15
+ /** The bypass frame's exit: the same renderer's short arm (the retired frame was steer-only) with its
16
+ * shell-first sentence (the retired frame carried the shell-first paragraph). */
17
+ export declare const BYPASS_POSTURE_EXIT_DIRECTIVE: string;
18
+ /** The planning frame — the reference full reminder's opening sentence verbatim (its own "supercedes"
19
+ * spelling included); the tail is adapted to the engine's exit (the plan-exit tool, a human approves). */
20
+ export declare const PLAN_MODE_DIRECTIVE: string;
21
+ /** The planning frame's sparse restatement — the reference sparse shape minus the plan-file clause. */
22
+ export declare const PLAN_MODE_SPARSE_DIRECTIVE: string;
23
+ /** The planning frame for a leg with NO plan-exit tool on its roster: the same opening, and the plan is the leg's
24
+ * final answer (no tool is named, no approval request is promised). */
25
+ export declare const PLAN_MODE_NO_EXIT_DIRECTIVE: string;
26
+ /** The no-tool planning frame's sparse restatement. */
27
+ export declare const PLAN_MODE_NO_EXIT_SPARSE_DIRECTIVE: string;
28
+ /** The planning frame of a DELEGATED child — the reference's sub-agent variant: the same opening, no exit tool named,
29
+ * the child answers the request it was given (its final answer goes back to the agent that delegated to it). The
30
+ * reference has no sparse arm for a sub-agent, so a restatement of this row is this body. */
31
+ export declare const PLAN_MODE_CHILD_DIRECTIVE: string;
32
+ /** The planning frame's exit — the reference exit renderer's text without its plan-file clause. Names no tool, so
33
+ * one exit retires every planning wording. */
34
+ export declare const PLAN_MODE_EXIT_DIRECTIVE: string;
13
35
  /** The mode facts of one leg, both the ENGINE's own resolution — never a requested value, never spec shape. */
14
36
  export interface ApprovalPostureFacts {
15
37
  /** The leg's EFFECTIVE permission mode (a requested mode the deployment refused has already folded back). */
16
38
  mode: PermissionMode;
17
- /** The wiring manifest's ask derivation — `auto_allow` means no person answers an ask on this leg. */
39
+ /** The seat's own ask derivation (`askSeatEffective`, no mode) — `auto_allow` means no person answers an ask on this leg.
40
+ * The LIVE `mode` is read before it: the manifest's reported word is the leg's start under its start mode, and a steer moves
41
+ * the mode after it was minted. */
18
42
  askEffective: AskEffective;
19
43
  }
20
44
  /**
21
- * The ONE composition, and the ONE invariant: **the segment the leg's mode calls for is stated exactly
22
- * once in the active model context**. `statedBodies` is the transcript's ENGINE REGIONS (read by metadata
23
- * — a caller's own bytes are never in one, so a spliced copy cannot suppress the engine's); a segment
24
- * already standing there is not restated, and a segment that has left the context — compacted away, or
25
- * never delivered — is. Returns `undefined` when there is nothing left to say, which is both "no segment
26
- * for this mode" (zero bytes) and "already said".
27
- *
28
- * Segment-wise rather than body-wise on purpose: a leg whose mode CHANGES — it now runs under bypass, so the
29
- * segment it owes changes — must state the new segment without restating one already standing, and must be
30
- * able to read a body an earlier leg minted under a composition that joined two. The caller wraps the result
31
- * in the engine's marked reminder shell; this module never wraps, so the mark stays owned by its own mint home.
45
+ * The shape facts of one leg the planning rows word themselves by — read off what the leg IS (its minted tool roster, its
46
+ * minted delegation identity), never off the requested spec.
32
47
  */
33
- export declare function composeApprovalPostureDirective(facts: ApprovalPostureFacts, statedBodies?: readonly string[]): string | undefined;
34
- /** SHA-256 of a directive body — the pins' lock port, so the lock is computed the one way. */
48
+ export interface PostureLegFacts {
49
+ /** The plan-exit tool ({@link PRESENT_PLAN_TOOL_NAME}) is on this leg's roster — its wire name or an alias a call resolves. */
50
+ exitToolMounted: boolean;
51
+ /** This leg runs as a delegated child (every core spawn lane, a fork included). */
52
+ isChildLeg: boolean;
53
+ }
54
+ /** Every fact a row's `owedBy` reads: the mode facts and the leg's shape. */
55
+ export type PostureFacts = ApprovalPostureFacts & PostureLegFacts;
56
+ /** A posture family: one family stands in a context at a time per family, and its exit retires whichever of its rows stands. */
57
+ export type PostureFamily = "plan" | "auto";
58
+ /** One row of {@link POSTURE_TABLE}. */
59
+ export interface PostureRow {
60
+ readonly family: PostureFamily;
61
+ /** Whether the leg's mode facts call for this row. */
62
+ readonly owedBy: (facts: PostureFacts) => boolean;
63
+ /** The frame that states the posture. */
64
+ readonly frame: string;
65
+ /** The frame that retires it. */
66
+ readonly exit: string;
67
+ /**
68
+ * A restatement while the row stays owed and standing: due once `everyTurns` assistant turns have passed since
69
+ * the family's last frame; the restatement is the full frame when it is the family's `1 + k·fullEvery`-th frame
70
+ * since its last exit, else `sparse`. Absent ⇒ stated once and never restated while it stands.
71
+ */
72
+ readonly cadence?: {
73
+ readonly everyTurns: number;
74
+ readonly fullEvery: number;
75
+ readonly sparse: string;
76
+ };
77
+ }
78
+ /**
79
+ * Every posture the engine states, in the order the owed row is chosen. `plan` first: it is the mode the leg
80
+ * was put in, and the bypass frame's shell-first instruction would contradict it. Within it the leg's shape picks the
81
+ * wording — a delegated child (the reference's sub-agent variant), else a leg with the plan-exit tool on its roster
82
+ * (the planning body that names it), else the body that names no tool. Then the reference
83
+ * implementation's own arm order for its one posture attachment: bypass (effective `bypassPermissions`, or — outside
84
+ * `dontAsk`, whose mode refuses every ask before any seat could answer — no person answers an ask), then auto (the
85
+ * classifier is armed — a requested auto mode the engine folded back is not stated, because the run is not operating
86
+ * under it).
87
+ */
88
+ export declare const POSTURE_TABLE: readonly PostureRow[];
89
+ /** The row the leg's facts call for, or undefined (no posture is owed). */
90
+ export declare function owedPostureRow(facts: PostureFacts): PostureRow | undefined;
91
+ /** What one family's standing frame is, as the fold over the active context left it. */
92
+ export interface StandingPosture {
93
+ readonly row: PostureRow;
94
+ /** The family's frames since its last exit (the cadence's full/sparse count). */
95
+ readonly statements: number;
96
+ /** Assistant turns since the family's last frame (the cadence's due clock). */
97
+ readonly turnsSince: number;
98
+ }
99
+ /** The standing families of a context, keyed by family, in the order they first stood. */
100
+ export type PostureStanding = ReadonlyMap<PostureFamily, StandingPosture>;
101
+ /**
102
+ * The ordered fold over the ACTIVE context's messages (the compaction boundary applied — never the raw ancestry,
103
+ * which still carries what a compaction replaced): the engine's marked reminder bodies, read off ENGINE REGIONS
104
+ * only (metadata, never text shape, so a caller's spliced copy — marked or not — cannot answer), per minted block;
105
+ * an assistant message advances every standing family's turn clock.
106
+ */
107
+ export declare function postureStandingOf(messages: readonly EngineRegionMessage[], reminderMark: string): PostureStanding;
108
+ /**
109
+ * The ONE composition: the frames that make the active context state exactly the posture the leg's facts call for
110
+ * — the exit of every standing family the owed row does not belong to, then the owed row's frame (or its cadence's
111
+ * restatement) when it is owed. Empty when the context already says it.
112
+ */
113
+ export declare function composePostureFrames(facts: PostureFacts, standing: PostureStanding): string[];
114
+ /**
115
+ * The run-local half of the posture: the newest posture carrier's text (the context lane's protection slot —
116
+ * a clipped request keeps or reinserts the message carrying it; the carrier of an exit takes the slot, so a
117
+ * retired frame is no longer protected); the owed row the last composition answered for (a boundary whose facts
118
+ * still call for that row, with no cadence to keep, asks nothing of the context — absent means the last
119
+ * composition read nothing or its carrier was not written, so the next boundary asks again); the frames a boundary
120
+ * could not write (stated on each request's view, never persisted, until a boundary writes them); and where a
121
+ * write failure is disclosed.
122
+ */
123
+ export interface PostureFrameRef {
124
+ protectedText?: string;
125
+ composedFor?: PostureRow | null;
126
+ unwritten?: string;
127
+ report?: (err: Error) => void;
128
+ }
129
+ /** SHA-256 of a frame body — the pins' lock port, so the lock is computed the one way. */
35
130
  export declare function directiveDigest(body: string): string;
@@ -1,34 +1,82 @@
1
1
  import { createHash } from "node:crypto";
2
+ import { PRESENT_PLAN_TOOL_NAME } from "./present-plan-tool.js";
3
+ import { engineRegionTextsOf, markedReminderBodiesOf } from "./reminder-mint.js";
2
4
  export const AUTO_MODE_DIRECTIVE = "## Auto Mode Active\n\nBias toward working without stopping for clarifying questions — when you'd normally pause to check, make the reasonable call and keep going; they'll redirect you if needed. If the user, a skill, or the shape of the task suggests they want you to ask (with AskUserQuestion or otherwise), do so. And even absent that signal, it's still fine to stop when you're genuinely blocked — unclear direction, missing input, a decision only they can make.\n\nBefore any command that could discard uncommitted work — `git checkout`/`restore`/`reset`/`clean`, `rm -rf` in the repo, restoring from a snapshot — run `git status` first and stash (with `-u` for untracked) or commit anything that's there. When staging or committing, review what's included (`git status` after a broad `git add`), and if you see anything suspicious that might reveal secrets — even if the filename looks innocuous — double-check the file's contents before pushing.";
3
5
  export const BASH_FIRST_DIRECTIVE = "Do your work through the Bash tool wherever it can accomplish the job: read files with cat, head, or sed -n, search with grep and find, and make file changes with sed, heredocs, or short scripts, rather than using the dedicated Read, Edit, or Write tools. Fall back to a dedicated tool only when Bash genuinely cannot do the job.";
4
6
  export const BYPASS_POSTURE_DIRECTIVE = `While bypass permissions mode is active:\n\n${BASH_FIRST_DIRECTIVE}`;
5
- function postureSegmentOf(facts) {
6
- if (facts.mode === "bypassPermissions" || facts.askEffective === "auto_allow")
7
- return BYPASS_POSTURE_DIRECTIVE;
8
- return facts.mode === "auto" ? AUTO_MODE_DIRECTIVE : undefined;
7
+ export const AUTO_MODE_EXIT_DIRECTIVE = "## Exited Auto Mode\n\nYou have exited auto mode. The user may now want to interact more directly. You should ask clarifying questions when the approach is ambiguous rather than making assumptions.";
8
+ export const BYPASS_POSTURE_EXIT_DIRECTIVE = "## Exited Auto Mode\n\nYou have exited auto mode. Resume using the dedicated tools for file reads, searches, and edits.";
9
+ export const PLAN_MODE_DIRECTIVE = `Plan mode is active. The user indicated that they do not want you to execute yet -- you MUST NOT make any edits, run any non-readonly tools (including changing configs or making commits), or otherwise make any changes to the system — every such call asks the user first, and is refused where nobody can answer. This supercedes any other instructions you have received. Instead, you should: research and analyze with read-only tools, and when you have formed a concrete plan, present it with the ${PRESENT_PLAN_TOOL_NAME} tool for approval. That tool inherently requests plan approval — never ask for approval via plain text.`;
10
+ export const PLAN_MODE_SPARSE_DIRECTIVE = `Plan mode still active (see full instructions earlier in conversation). Read-only: no edits, commits, or config changes. When your plan is ready, present it with the ${PRESENT_PLAN_TOOL_NAME} tool for approval. Never ask about plan approval via plain text.`;
11
+ export const PLAN_MODE_NO_EXIT_DIRECTIVE = "Plan mode is active. The user indicated that they do not want you to execute yet -- you MUST NOT make any edits, run any non-readonly tools (including changing configs or making commits), or otherwise make any changes to the system — every such call asks the user first, and is refused where nobody can answer. This supercedes any other instructions you have received. Instead, you should: research and analyze with read-only tools, and when you have formed a concrete plan, give it as your final answer and stop.";
12
+ export const PLAN_MODE_NO_EXIT_SPARSE_DIRECTIVE = "Plan mode still active (see full instructions earlier in conversation). Read-only: no edits, commits, or config changes. When your plan is ready, give it as your final answer and stop.";
13
+ export const PLAN_MODE_CHILD_DIRECTIVE = "Plan mode is active. The user indicated that they do not want you to execute yet -- you MUST NOT make any edits, run any non-readonly tools (including changing configs or making commits), or otherwise make any changes to the system — every such call asks the user first, and is refused where nobody can answer. This supercedes any other instructions you have received. Instead, you should: research and analyze with read-only tools, and answer the request comprehensively — your final answer, carrying the plan, goes back to the agent that delegated to you.";
14
+ export const PLAN_MODE_EXIT_DIRECTIVE = "## Exited Plan Mode\n\nYou have exited plan mode. You can now make edits, run tools, and take actions.";
15
+ const PLAN_CADENCE = { everyTurns: 5, fullEvery: 5, sparse: PLAN_MODE_SPARSE_DIRECTIVE };
16
+ export const POSTURE_TABLE = Object.freeze([
17
+ { family: "plan", owedBy: (f) => f.mode === "plan" && f.isChildLeg, frame: PLAN_MODE_CHILD_DIRECTIVE, exit: PLAN_MODE_EXIT_DIRECTIVE, cadence: { ...PLAN_CADENCE, sparse: PLAN_MODE_CHILD_DIRECTIVE } },
18
+ { family: "plan", owedBy: (f) => f.mode === "plan" && f.exitToolMounted, frame: PLAN_MODE_DIRECTIVE, exit: PLAN_MODE_EXIT_DIRECTIVE, cadence: PLAN_CADENCE },
19
+ { family: "plan", owedBy: (f) => f.mode === "plan", frame: PLAN_MODE_NO_EXIT_DIRECTIVE, exit: PLAN_MODE_EXIT_DIRECTIVE, cadence: { ...PLAN_CADENCE, sparse: PLAN_MODE_NO_EXIT_SPARSE_DIRECTIVE } },
20
+ { family: "auto", owedBy: (f) => f.mode === "bypassPermissions" || (f.mode !== "dontAsk" && f.askEffective === "auto_allow"), frame: BYPASS_POSTURE_DIRECTIVE, exit: BYPASS_POSTURE_EXIT_DIRECTIVE },
21
+ { family: "auto", owedBy: (f) => f.mode === "auto", frame: AUTO_MODE_DIRECTIVE, exit: AUTO_MODE_EXIT_DIRECTIVE },
22
+ ]);
23
+ export function owedPostureRow(facts) {
24
+ return POSTURE_TABLE.find((row) => row.owedBy(facts));
9
25
  }
10
- export function composeApprovalPostureDirective(facts, statedBodies = []) {
11
- const owed = postureSegmentOf(facts);
12
- if (owed === undefined)
13
- return undefined;
14
- for (const body of statedBodies)
15
- if (postureSegmentsOf(body)?.includes(owed) === true)
16
- return undefined;
17
- return owed;
18
- }
19
- const POSTURE_SEGMENTS = [AUTO_MODE_DIRECTIVE, BYPASS_POSTURE_DIRECTIVE];
20
- function postureSegmentsOf(body) {
26
+ const POSTURE_VOCABULARY = POSTURE_TABLE.flatMap((row) => [
27
+ [row.frame, { row, exit: false }],
28
+ [row.exit, { row, exit: true }],
29
+ ...(row.cadence !== undefined ? [[row.cadence.sparse, { row, exit: false }]] : []),
30
+ ]);
31
+ function postureItemsOf(body) {
21
32
  const out = [];
22
33
  let rest = body;
23
34
  while (rest.length > 0) {
24
- const seg = POSTURE_SEGMENTS.find((s) => rest === s || rest.startsWith(`${s}\n\n`));
25
- if (seg === undefined)
35
+ const hit = POSTURE_VOCABULARY.find(([text]) => rest === text || rest.startsWith(`${text}\n\n`));
36
+ if (hit === undefined)
26
37
  return undefined;
27
- out.push(seg);
28
- rest = rest === seg ? "" : rest.slice(seg.length + 2);
38
+ out.push(hit[1]);
39
+ rest = rest === hit[0] ? "" : rest.slice(hit[0].length + 2);
29
40
  }
30
41
  return out.length > 0 ? out : undefined;
31
42
  }
43
+ export function postureStandingOf(messages, reminderMark) {
44
+ const standing = new Map();
45
+ for (const m of messages) {
46
+ if (m.role === "assistant") {
47
+ for (const [family, s] of standing)
48
+ standing.set(family, { ...s, turnsSince: s.turnsSince + 1 });
49
+ continue;
50
+ }
51
+ for (const region of engineRegionTextsOf([m])) {
52
+ for (const body of markedReminderBodiesOf(region, reminderMark)) {
53
+ for (const item of postureItemsOf(body) ?? []) {
54
+ if (item.exit)
55
+ standing.delete(item.row.family);
56
+ else
57
+ standing.set(item.row.family, { row: item.row, statements: (standing.get(item.row.family)?.statements ?? 0) + 1, turnsSince: 0 });
58
+ }
59
+ }
60
+ }
61
+ }
62
+ return standing;
63
+ }
64
+ export function composePostureFrames(facts, standing) {
65
+ const owed = owedPostureRow(facts);
66
+ const frames = [];
67
+ for (const [family, s] of standing)
68
+ if (family !== owed?.family)
69
+ frames.push(s.row.exit);
70
+ if (owed === undefined)
71
+ return frames;
72
+ const s = standing.get(owed.family);
73
+ if (s === undefined || s.row !== owed)
74
+ frames.push(owed.frame);
75
+ else if (owed.cadence !== undefined && s.turnsSince >= owed.cadence.everyTurns) {
76
+ frames.push((s.statements + 1) % owed.cadence.fullEvery === 1 ? owed.frame : owed.cadence.sparse);
77
+ }
78
+ return frames;
79
+ }
32
80
  export function directiveDigest(body) {
33
81
  return createHash("sha256").update(body, "utf8").digest("hex");
34
82
  }
@@ -613,15 +613,16 @@ export type ResumeOutcome = {
613
613
  * The host's DECISION FACTS — who ended this wait: a person (`"person"`, the decide an operator made
614
614
  * through the deployment's approval channel, with the attribution that channel reports), the
615
615
  * host's own SLA sweep at the row's deadline (`"sla_timeout"` — FACET A of the deadline contract; only
616
- * a sweep may say it), or the deployment's OWN POLICY (`"policy"` — a machine refused: no interactive
617
- * approval surface exists for this row, or a standing posture declined it; the durable spelling of the
618
- * synchronous leg's `AskOutcome.settledBy: "policy"`, so ONE refusal reads the same on both faces).
619
- * Only a person can `allow` — the other two words name a wait that ended with nobody judging.
616
+ * a sweep may say it), or the deployment's OWN POLICY (`"policy"` — a machine decided, either way: an
617
+ * unattended seat answering by class, no interactive approval surface for this row, a standing posture;
618
+ * the durable spelling of the synchronous leg's `AskOutcome.settledBy: "policy"`, so ONE decision reads
619
+ * the same on both faces). A person and a policy may each `allow` or `deny`; `"sla_timeout"` names a wait
620
+ * that ended with nobody answering and cannot `allow`.
620
621
  * REQUIRED: a decide that names nobody is not a record of anything that happened, and core does not guess.
621
622
  *
622
623
  * The host supplies FACTS, never the settlement WORD: core mints the record
623
- * ({@link import("./gate-outcome.js").GateOutcome} — `human_allowed` / `human_refused` / `park_sla_expired`
624
- * / `policy_refused`, joined with the row's persisted `origin`) at the resume entry, writes it onto the resolved row
624
+ * ({@link import("./gate-outcome.js").GateOutcome} — `human_allowed` / `human_refused` / `policy_allowed`
625
+ * / `policy_refused` / `park_sla_expired`, joined with the row's persisted `origin`) at the resume entry, writes it onto the resolved row
625
626
  * (`resolvedOutcome.gateOutcome`) and projects it onto the resolved call's `tool_end` frame. A caller
626
627
  * that supplies `gateOutcome` itself is refused pre-CAS.
627
628
  *
@@ -1134,8 +1135,8 @@ export interface CheckpointState {
1134
1135
  * law) and re-judges the deny bits and the classifier face for its own leg: a `bypassPermissions` row does not
1135
1136
  * revive the mode where the redeeming deployment denies it, and an `auto` row arms only where a face is
1136
1137
  * wired. Absent on a task that declared nothing and inherited nothing (`default`). Rows written before this
1137
- * member carried `autoModeRequested` / `approverPosture` / a doctrine `shellGate`; the resume leg migrates
1138
- * them once, announced (`resume.legacy_mode_seed_migrated`), and the next suspend writes this member. The migration is kept through 7.38.x and removed in 7.39.0, which refuses a row still carrying these keys. */
1138
+ * member carried `autoModeRequested` / `approverPosture` / a doctrine `shellGate`; no engine since 7.39.0 reads
1139
+ * them — such a row is refused before the claim ({@link refuseRetiredModeSeed}). */
1139
1140
  permissionMode?: PermissionMode;
1140
1141
  /** Beside a recorded `plan`: the declared word the leg had when a steer or the model's `EnterPlanMode` moved it into
1141
1142
  * plan — the mode an approved or edited `plan_review` resumes under ({@link import("./mode-policy.js").modeRequestAfterResume}). */
@@ -2029,8 +2030,9 @@ export declare function summarizeCheckpoint(cp: Checkpoint): CheckpointSummary;
2029
2030
  * LEDGER carries an event kind this binary does not know, so the replay is refused rather than served
2030
2031
  * incomplete — the one arm that is not row-scoped: NO row in that data directory is readable here, so
2031
2032
  * the host rolls the binary forward instead of routing one token elsewhere), `"seat_retired"` (the row
2032
- * parks a steer in the retired pre-queue single seat — terminal for the row's bytes; no worker version
2033
- * reads that seat, the host decides the parked instruction itself).
2033
+ * carries a seat this engine no longer reads — terminal for the row's bytes: the retired pre-queue single
2034
+ * steer seat, which no worker version reads and the host decides itself, or a retired mode-seed member of
2035
+ * `inheritedGate`, which an engine at 7.38.x or earlier still reads once).
2034
2036
  * · `invalid_outcome` pre-CAS row-integrity arms: `"real_approval_damaged"`, `"real_approval_forged"`,
2035
2037
  * `"read_face_damaged"`, `"read_face_forged"`, `"write_face_damaged"`, `"constraint_chain_missing"` (all terminal for the row's
2036
2038
  * current bytes — no worker version redeems a damaged row).
@@ -2040,6 +2042,17 @@ export type CheckpointErrorReason = "version_newer" | "env_factory_missing" | "g
2040
2042
  * legal-and-absent at every version, so there is no version to judge a fabrication against — and a
2041
2043
  * fabricated "open" still needs the LIVE resolution to say open too. */
2042
2044
  | "write_face_damaged" | "constraint_chain_missing";
2045
+ /**
2046
+ * The codes a RESUME's pre-claim configuration screen carries as `detail.code` of a `checkpoint.config_refused` — the four
2047
+ * words prepare's mode, output-style and recorded-mode doors throw, held unchanged so one refusal reads the same on a fresh
2048
+ * run (`TaskResult.errorCode`) and on a resume (this member). `config.spec_retired_key`: a retired spec key is present;
2049
+ * `config.output_style_invalid`: the output-style seat is malformed; `config.permission_mode_invalid`: a mode word is outside
2050
+ * the closed set (the config's `permissionMode`, or a mode the row recorded); `config.shell_gate_value_retired`: the shell
2051
+ * floor is a retired doctrine word.
2052
+ */
2053
+ export declare const CHECKPOINT_CONFIG_REFUSAL_CODES: readonly ["config.spec_retired_key", "config.output_style_invalid", "config.permission_mode_invalid", "config.shell_gate_value_retired"];
2054
+ export type CheckpointConfigRefusalCode = (typeof CHECKPOINT_CONFIG_REFUSAL_CODES)[number];
2055
+ export declare const isCheckpointConfigRefusalCode: (word: unknown) => word is CheckpointConfigRefusalCode;
2043
2056
  /** A typed checkpoint-store error so callers branch on `code` (mirrors `SessionError`). */
2044
2057
  export declare class CheckpointError extends Error {
2045
2058
  readonly code: "checkpoint.already_exists" | "checkpoint.already_resolved" | "checkpoint.not_found"
@@ -2061,6 +2074,14 @@ export declare class CheckpointError extends Error {
2061
2074
  * Rejected pre-CAS so the checkpoint stays `pending` and a capable worker can still resume it
2062
2075
  * (design/49 §2/§3, code-ready council round-2). */
2063
2076
  | "checkpoint.unsupported_version"
2077
+ /** The resume's re-supplied config, or a mode the checkpoint row recorded, is one prepare would refuse
2078
+ * (a retired spec key, a malformed output style, a mode word outside the closed set, a retired shell-floor
2079
+ * word). Refused PRE-CAS, so the checkpoint stays `pending` and byte-identical. `message` is prepare's own
2080
+ * sentence; `detail.code` is prepare's own code ({@link CheckpointConfigRefusalCode}) and `detail.field`
2081
+ * says which input carries it: `"taskConfig"` (correct the config and resume the SAME token) or
2082
+ * `"inheritedGate"` (the recorded row — no config reaches it; expire the row). A fresh run of the same
2083
+ * bad config reports the same `detail.code` as its `TaskResult.errorCode`. */
2084
+ | "checkpoint.config_refused"
2064
2085
  /** `resume`/`resumeStream` was handed `internals.requestedCwd` for a checkpoint that
2065
2086
  * carries a remote `workspaceHandle`. The restored workspace's mountPath is authoritative for the
2066
2087
  * task root, so the cwd would be ignored at best — and verifying it would touch the env BEFORE the
@@ -2201,14 +2222,19 @@ export declare class CheckpointError extends Error {
2201
2222
  * action: re-review), or `"answer"` (the content-ask answer is missing, or was attached to a
2202
2223
  * decision that cannot carry one: re-decide with a well-formed answer — ruled 2026-08-04), or
2203
2224
  * `"hostDecision"` (the decision facts are missing, name a decider outside `person`/`sla_timeout`/`policy`,
2204
- * pair a non-person decider with an allow, or try to supply the minted word), or — design/252 G-7 — `"approver"` (the attribution is not a
2225
+ * pair the sweep's `sla_timeout` with an allow, or try to supply the minted word), or — design/252 G-7 — `"approver"` (the attribution is not a
2205
2226
  * well-formed identifier: re-decide with a plain, bounded, control-character-free string, or omit
2206
- * it). The throw
2227
+ * it). On a `checkpoint.config_refused` `field` names which INPUT carries the refusal — `"taskConfig"`
2228
+ * (the re-supplied config: correct it and resume the same token) or `"inheritedGate"` (the mode the row
2229
+ * recorded: no config reaches it; expire the row). The throw
2207
2230
  * sites always knew which arm fired; this carries that fact instead of dropping it (ruled
2208
2231
  * 2026-08-03, cross-repo HITL discussion). Additive and optional — `code` remains the only REQUIRED
2209
2232
  * discriminant. */
2210
2233
  readonly detail?: {
2211
- field?: "boundCallId" | "boundInputHash" | "answer" | "hostDecision" | "approver";
2234
+ field?: "boundCallId" | "boundInputHash" | "answer" | "hostDecision" | "approver" | "taskConfig" | "inheritedGate";
2235
+ /** On `checkpoint.config_refused` — prepare's own code for the refusal ({@link CheckpointConfigRefusalCode}), the
2236
+ * word a fresh run of the same bad config reports as its `errorCode`. Absent on every other code. */
2237
+ code?: CheckpointConfigRefusalCode;
2212
2238
  /** #449 G1 — on `resume.usage_window_exhausted`: how long until the binding window frees
2213
2239
  * (the same wait a suspended row carries as `gate.resumeAfterMs`). design/380 O2 — on a
2214
2240
  * retry-later `resume.preflight_rejected`: the deployment preflight's own wait hint, when it
@@ -2240,6 +2266,14 @@ export declare class CheckpointError extends Error {
2240
2266
  * Rejected pre-CAS so the checkpoint stays `pending` and a capable worker can still resume it
2241
2267
  * (design/49 §2/§3, code-ready council round-2). */
2242
2268
  | "checkpoint.unsupported_version"
2269
+ /** The resume's re-supplied config, or a mode the checkpoint row recorded, is one prepare would refuse
2270
+ * (a retired spec key, a malformed output style, a mode word outside the closed set, a retired shell-floor
2271
+ * word). Refused PRE-CAS, so the checkpoint stays `pending` and byte-identical. `message` is prepare's own
2272
+ * sentence; `detail.code` is prepare's own code ({@link CheckpointConfigRefusalCode}) and `detail.field`
2273
+ * says which input carries it: `"taskConfig"` (correct the config and resume the SAME token) or
2274
+ * `"inheritedGate"` (the recorded row — no config reaches it; expire the row). A fresh run of the same
2275
+ * bad config reports the same `detail.code` as its `TaskResult.errorCode`. */
2276
+ | "checkpoint.config_refused"
2243
2277
  /** `resume`/`resumeStream` was handed `internals.requestedCwd` for a checkpoint that
2244
2278
  * carries a remote `workspaceHandle`. The restored workspace's mountPath is authoritative for the
2245
2279
  * task root, so the cwd would be ignored at best — and verifying it would touch the env BEFORE the
@@ -2380,14 +2414,19 @@ export declare class CheckpointError extends Error {
2380
2414
  * action: re-review), or `"answer"` (the content-ask answer is missing, or was attached to a
2381
2415
  * decision that cannot carry one: re-decide with a well-formed answer — ruled 2026-08-04), or
2382
2416
  * `"hostDecision"` (the decision facts are missing, name a decider outside `person`/`sla_timeout`/`policy`,
2383
- * pair a non-person decider with an allow, or try to supply the minted word), or — design/252 G-7 — `"approver"` (the attribution is not a
2417
+ * pair the sweep's `sla_timeout` with an allow, or try to supply the minted word), or — design/252 G-7 — `"approver"` (the attribution is not a
2384
2418
  * well-formed identifier: re-decide with a plain, bounded, control-character-free string, or omit
2385
- * it). The throw
2419
+ * it). On a `checkpoint.config_refused` `field` names which INPUT carries the refusal — `"taskConfig"`
2420
+ * (the re-supplied config: correct it and resume the same token) or `"inheritedGate"` (the mode the row
2421
+ * recorded: no config reaches it; expire the row). The throw
2386
2422
  * sites always knew which arm fired; this carries that fact instead of dropping it (ruled
2387
2423
  * 2026-08-03, cross-repo HITL discussion). Additive and optional — `code` remains the only REQUIRED
2388
2424
  * discriminant. */
2389
2425
  detail?: {
2390
- field?: "boundCallId" | "boundInputHash" | "answer" | "hostDecision" | "approver";
2426
+ field?: "boundCallId" | "boundInputHash" | "answer" | "hostDecision" | "approver" | "taskConfig" | "inheritedGate";
2427
+ /** On `checkpoint.config_refused` — prepare's own code for the refusal ({@link CheckpointConfigRefusalCode}), the
2428
+ * word a fresh run of the same bad config reports as its `errorCode`. Absent on every other code. */
2429
+ code?: CheckpointConfigRefusalCode;
2391
2430
  /** #449 G1 — on `resume.usage_window_exhausted`: how long until the binding window frees
2392
2431
  * (the same wait a suspended row carries as `gate.resumeAfterMs`). design/380 O2 — on a
2393
2432
  * retry-later `resume.preflight_rejected`: the deployment preflight's own wait hint, when it
@@ -3030,31 +3069,23 @@ export declare class InMemoryCheckpointStore implements CheckpointStore {
3030
3069
  /** Test/inspection helper: number of stored checkpoints. */
3031
3070
  get size(): number;
3032
3071
  }
3033
- /** What {@link migrateLegacyModeSeed} read off a checkpoint row's `inheritedGate`: the mode and the floor the resume
3034
- * leg inherits, and — when the row carried a pre-mode key — what was migrated and what was dropped. */
3035
- export interface LegacyModeSeedMigration {
3072
+ /**
3073
+ * The ONE refusal of a retired mode seed, read PRE-CAS on every resume kind (resume-admission): a row whose `inheritedGate`
3074
+ * still carries a retired member is refused as the engine refuses every retired row seat (`readPendingSteerQueue`) —
3075
+ * `checkpoint.unsupported_version` with `reason: "seat_retired"` — terminal for the row's bytes on this engine, and
3076
+ * nothing consumed: the row stays byte-identical and `pending`. No engine reads the retired keys into a mode any more; an
3077
+ * engine at 7.38.x or earlier still does, so the row can be resumed there once (its next suspend writes `permissionMode`), or
3078
+ * expired (`CheckpointStore.expire`).
3079
+ */
3080
+ export declare function refuseRetiredModeSeed(state: Pick<CheckpointState, "inheritedGate">): void;
3081
+ /**
3082
+ * The mode seed a row records (the 7.34.0 shape), read at prepare after {@link refuseRetiredModeSeed} has screened the row:
3083
+ * `permissionMode` must be a member of the closed set and `prePlanMode` a mode a plan can return to (else
3084
+ * `config.permission_mode_invalid`, the row cannot be resumed under a word this engine does not know); the shell floor is
3085
+ * `"always"` or absent. Nothing here migrates or guesses.
3086
+ */
3087
+ export declare function recordedModeSeed(gate: Pick<NonNullable<CheckpointState["inheritedGate"]>, "permissionMode" | "prePlanMode" | "shellGate"> | undefined): {
3036
3088
  permissionMode?: PermissionMode;
3037
3089
  prePlanMode?: PermissionMode;
3038
3090
  shellGate?: ShellGateDoctrine;
3039
- /** Present iff the row carried a retired key (`autoModeRequested` / `approverPosture` / a doctrine `shellGate`). */
3040
- migrated?: {
3041
- permissionMode?: PermissionMode;
3042
- droppedKeys: readonly string[];
3043
- };
3044
- }
3045
- /**
3046
- * Read a row's mode seed ONCE, migrating the pre-7.34.0 keys the row may still carry. The matrix, key by key:
3047
- * - `permissionMode` present ⇒ it must be a member of the closed set (else refused, `config.permission_mode_invalid`,
3048
- * without reading the retired keys); the two retired mode keys beside it are DROPPED unvalidated (the new key is
3049
- * the truth), `shellGate` follows the value rule below;
3050
- * - `autoModeRequested`: absent / `false` ⇒ no mode from it; `true` ⇒ `auto`; anything else ⇒ refused
3051
- * (`resume.legacy_mode_seed`);
3052
- * - `approverPosture`: any value ⇒ dropped (it never authorized anything; the mode comes from the request / chain,
3053
- * and a `"bypass"` posture beside `autoModeRequested: true` reads `auto` — a posture never becomes an executable
3054
- * bypass);
3055
- * - `shellGate`: absent ⇒ none; `"always"` ⇒ kept (a live key, not a retired encoding); `"off"` / `"classify"` ⇒
3056
- * dropped (no floor); anything else ⇒ refused (`resume.legacy_mode_seed`).
3057
- * Idempotent: a row the resume leg writes back carries `permissionMode` and the floor only, so a second read migrates nothing ("once" is per row).
3058
- * Migration precedes any retired-key door, so a window row is read rather than refused. The window: this migration is kept through 7.38.x and removed in 7.39.0, which refuses a row still carrying these keys — resume or abandon a row parked before 7.34.0 before upgrading to 7.39.0.
3059
- */
3060
- export declare function migrateLegacyModeSeed(gate: object | undefined): LegacyModeSeedMigration;
3091
+ };
@@ -313,6 +313,8 @@ export function summarizeCheckpoint(cp) {
313
313
  ...(hasBidi ? { hasBidiControls: true } : {}),
314
314
  };
315
315
  }
316
+ export const CHECKPOINT_CONFIG_REFUSAL_CODES = Object.freeze(["config.spec_retired_key", "config.output_style_invalid", "config.permission_mode_invalid", "config.shell_gate_value_retired"]);
317
+ export const isCheckpointConfigRefusalCode = (word) => CHECKPOINT_CONFIG_REFUSAL_CODES.some((w) => w === word);
316
318
  export class CheckpointError extends Error {
317
319
  code;
318
320
  detail;
@@ -734,57 +736,35 @@ export class InMemoryCheckpointStore {
734
736
  return this.cps.size;
735
737
  }
736
738
  }
737
- export function migrateLegacyModeSeed(gate) {
739
+ const RETIRED_MODE_SEED_MEMBERS = Object.freeze([
740
+ ["autoModeRequested", () => true],
741
+ ["approverPosture", () => true],
742
+ ["shellGate", (v) => v !== "always"],
743
+ ]);
744
+ export function refuseRetiredModeSeed(state) {
745
+ const gate = state.inheritedGate;
746
+ if (gate === undefined)
747
+ return;
748
+ for (const [key, retired] of RETIRED_MODE_SEED_MEMBERS) {
749
+ const value = gate[key];
750
+ if (value === undefined || !retired(value))
751
+ continue;
752
+ const shown = typeof value === "string" ? JSON.stringify(value.slice(0, 40)) : typeof value === "boolean" ? String(value) : typeof value;
753
+ throw new CheckpointError("checkpoint.unsupported_version", `this checkpoint's inheritedGate carries the retired mode-seed member \`${key}\` (${shown}), written before 7.34.0 and read by no engine since 7.39.0 — resume it once on an engine at 7.38.x or earlier (which reads it into \`permissionMode\`), or expire the row (\`CheckpointStore.expire\`); nothing was consumed`, { reason: "seat_retired" });
754
+ }
755
+ }
756
+ export function recordedModeSeed(gate) {
738
757
  if (gate === undefined)
739
758
  return {};
740
- const raw = gate;
741
- const refusal = (code, message) => {
759
+ const refusal = (message) => {
742
760
  const e = new Error(message);
743
- e.code = code;
761
+ e.code = "config.permission_mode_invalid";
744
762
  return e;
745
763
  };
746
- const dropped = [];
747
- let permissionMode;
748
- const declared = raw["permissionMode"];
749
- if (declared !== undefined) {
750
- if (!isPermissionMode(declared))
751
- throw refusal("config.permission_mode_invalid", `the checkpoint row's inheritedGate.permissionMode is ${JSON.stringify(declared)}, not a permission mode — the row cannot be resumed under a mode this engine does not know`);
752
- permissionMode = declared;
753
- if (raw["autoModeRequested"] !== undefined)
754
- dropped.push("autoModeRequested");
755
- if (raw["approverPosture"] !== undefined)
756
- dropped.push("approverPosture");
757
- }
758
- else {
759
- const intent = raw["autoModeRequested"];
760
- if (intent === true) {
761
- permissionMode = "auto";
762
- dropped.push("autoModeRequested");
763
- }
764
- else if (intent !== undefined && intent !== false) {
765
- throw refusal("resume.legacy_mode_seed", `the checkpoint row's inheritedGate.autoModeRequested is ${JSON.stringify(intent)}, which the one-time mode migration cannot read (only true / false / absent are) — the row is refused rather than resumed under a guessed mode`);
766
- }
767
- else if (intent === false) {
768
- dropped.push("autoModeRequested");
769
- }
770
- if (raw["approverPosture"] !== undefined)
771
- dropped.push("approverPosture");
772
- }
773
- const prePlan = raw["prePlanMode"];
774
- if (prePlan !== undefined && (!isPermissionMode(prePlan) || prePlan === "plan"))
775
- throw refusal("config.permission_mode_invalid", `the checkpoint row's inheritedGate.prePlanMode is ${JSON.stringify(prePlan)}, not a permission mode a plan can return to`);
776
- const floor = raw["shellGate"];
777
- let shellGate;
778
- if (floor === "always")
779
- shellGate = "always";
780
- else if (floor === "off" || floor === "classify")
781
- dropped.push("shellGate");
782
- else if (floor !== undefined)
783
- throw refusal("resume.legacy_mode_seed", `the checkpoint row's inheritedGate.shellGate is ${JSON.stringify(floor)}, which is neither the floor word "always" nor a retired doctrine word — the row is refused rather than resumed under a guessed floor`);
784
- return {
785
- ...(permissionMode !== undefined ? { permissionMode } : {}),
786
- ...(prePlan !== undefined ? { prePlanMode: prePlan } : {}),
787
- ...(shellGate !== undefined ? { shellGate } : {}),
788
- ...(dropped.length > 0 ? { migrated: { ...(declared === undefined && permissionMode !== undefined ? { permissionMode } : {}), droppedKeys: dropped } } : {}),
789
- };
764
+ const { permissionMode, prePlanMode, shellGate } = gate;
765
+ if (permissionMode !== undefined && !isPermissionMode(permissionMode))
766
+ throw refusal(`the checkpoint row's inheritedGate.permissionMode is ${JSON.stringify(permissionMode)}, not a permission mode — the row cannot be resumed under a mode this engine does not know`);
767
+ if (prePlanMode !== undefined && (!isPermissionMode(prePlanMode) || prePlanMode === "plan"))
768
+ throw refusal(`the checkpoint row's inheritedGate.prePlanMode is ${JSON.stringify(prePlanMode)}, not a permission mode a plan can return to`);
769
+ return { ...(permissionMode !== undefined ? { permissionMode } : {}), ...(prePlanMode !== undefined ? { prePlanMode } : {}), ...(shellGate !== undefined ? { shellGate } : {}) };
790
770
  }
@@ -1,5 +1,5 @@
1
1
  import { classifierUnavailableAskMessage, classifierUnavailableDenyMessage } from "./auto-mode.js";
2
- import { attestClassifierDenyCause } from "./tool-policy.js";
2
+ import { attestClassifierDenyCause, carryEngineAttestations } from "./tool-policy.js";
3
3
  export function classifierUnavailableArm(args) {
4
4
  const { ask, req, cause } = args;
5
5
  const seated = args.seat?.approvalSeat === true;
@@ -11,7 +11,7 @@ export function classifierUnavailableArm(args) {
11
11
  }
12
12
  }
13
13
  if (seated)
14
- return { kind: "asked", ask: { ...ask, message: classifierUnavailableAskMessage(ask.message ?? `approval required for "${req.toolName}"`, cause), requiresRealApproval: true } };
14
+ return { kind: "asked", ask: carryEngineAttestations(ask, { ...ask, message: classifierUnavailableAskMessage(ask.message ?? `approval required for "${req.toolName}"`, cause), requiresRealApproval: true }) };
15
15
  const deny = { action: "deny", message: classifierUnavailableDenyMessage(req.toolName, cause, args.attribution), decisionReason: "classifier" };
16
16
  return { kind: "denied", deny: attestClassifierDenyCause(deny, "unavailable", req) };
17
17
  }
@@ -1,5 +1,7 @@
1
1
  import type { PermissionResult } from "./tool-policy.js";
2
2
  export declare const refusedOutOfContract: (message: string) => PermissionResult;
3
+ /** Is `d` a refusal the engine itself minted (by object identity — a look-alike `deny` a caller built is not one)? Public: the answer of
4
+ * `receiveHookResult` is one of two things, and this tells them apart. */
3
5
  export declare const isEngineRefusal: (d: unknown) => boolean;
4
6
  /** A decision's `decisionReason` — the closed set; the type is derived FROM this list, so a word added to either side is a
5
7
  * compile error rather than a drift between the type and the screen. Module-private (the package publishes the type). */
@@ -3,7 +3,7 @@ import { ASK_CLASSES, ASK_ORIGINS, isAskClass, isAskOrigin, isPersistedRuleManda
3
3
  import { thrownText } from "./thrown-value.js";
4
4
  const ENGINE_REFUSALS = new WeakSet();
5
5
  export const refusedOutOfContract = (message) => {
6
- const refused = { action: "deny", message, decisionReason: "rule" };
6
+ const refused = Object.freeze({ action: "deny", message, decisionReason: "rule" });
7
7
  ENGINE_REFUSALS.add(refused);
8
8
  return refused;
9
9
  };