@sema-agent/core 7.17.7 → 7.19.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 (201) hide show
  1. package/CHANGELOG.md +233 -13
  2. package/dist/agents/send-message-tool.js +1 -0
  3. package/dist/agents/subagent.d.ts +1 -0
  4. package/dist/agents/subagent.js +5 -0
  5. package/dist/agents/teacher.js +1 -0
  6. package/dist/agents/verify.js +3 -0
  7. package/dist/brain/anthropic.js +6 -3
  8. package/dist/brain/open-responses.js +10 -4
  9. package/dist/brain/openai.js +5 -2
  10. package/dist/brain/reasoning.d.ts +13 -13
  11. package/dist/brain/reasoning.js +2 -2
  12. package/dist/brain/route-adjudicator.d.ts +18 -0
  13. package/dist/brain/route-adjudicator.js +16 -0
  14. package/dist/brain/stream-engine.js +2 -1
  15. package/dist/brain/stream-shared.d.ts +3 -0
  16. package/dist/brain/stream-shared.js +1 -0
  17. package/dist/core/agent-definition.d.ts +10 -0
  18. package/dist/core/artifact-host.d.ts +41 -4
  19. package/dist/core/artifact-host.js +18 -2
  20. package/dist/core/ask-origin.d.ts +16 -0
  21. package/dist/core/ask-origin.js +8 -0
  22. package/dist/core/auto-mode-arming.d.ts +6 -0
  23. package/dist/core/auto-mode-arming.js +5 -5
  24. package/dist/core/auto-mode-defaults.d.ts +43 -15
  25. package/dist/core/auto-mode-defaults.js +4 -1
  26. package/dist/core/auto-mode.d.ts +184 -59
  27. package/dist/core/auto-mode.js +188 -13
  28. package/dist/core/checkpoint-store.d.ts +23 -2
  29. package/dist/core/checkpoint-store.js +2 -2
  30. package/dist/core/engine-notice.d.ts +77 -1
  31. package/dist/core/engine-notice.js +7 -0
  32. package/dist/core/gate-fold.js +5 -1
  33. package/dist/core/gate-lanes.js +9 -8
  34. package/dist/core/gate-outcome.d.ts +42 -2
  35. package/dist/core/gate-outcome.js +23 -0
  36. package/dist/core/governance-codes.d.ts +1 -1
  37. package/dist/core/governance-codes.js +14 -0
  38. package/dist/core/hook-seats.d.ts +66 -0
  39. package/dist/core/hook-seats.js +37 -0
  40. package/dist/core/hooks.d.ts +1 -1
  41. package/dist/core/read-root-candidate.d.ts +12 -0
  42. package/dist/core/read-root-candidate.js +24 -0
  43. package/dist/core/roles.d.ts +9 -1
  44. package/dist/core/roles.js +4 -0
  45. package/dist/core/runner/advertised-writable-dirs.d.ts +8 -3
  46. package/dist/core/runner/assemble-result.d.ts +2 -1
  47. package/dist/core/runner/assemble-result.js +1 -1
  48. package/dist/core/runner/classifier-ladder.d.ts +227 -0
  49. package/dist/core/runner/classifier-ladder.js +214 -0
  50. package/dist/core/runner/contracts.d.ts +71 -13
  51. package/dist/core/runner/decide-continuation.d.ts +1 -3
  52. package/dist/core/runner/decide-continuation.js +1 -7
  53. package/dist/core/runner/deferral-census.d.ts +52 -0
  54. package/dist/core/runner/deferral-census.js +44 -0
  55. package/dist/core/runner/denial-limit-arms.js +2 -2
  56. package/dist/core/runner/derived-route-fallback.d.ts +8 -4
  57. package/dist/core/runner/derived-route-fallback.js +8 -3
  58. package/dist/core/runner/gate-exit.d.ts +12 -0
  59. package/dist/core/runner/gate-exit.js +6 -3
  60. package/dist/core/runner/prepare-artifact.d.ts +10 -2
  61. package/dist/core/runner/prepare-artifact.js +5 -4
  62. package/dist/core/runner/prepare-ask-lane.js +1 -0
  63. package/dist/core/runner/prepare-caps-and-workflow.d.ts +4 -2
  64. package/dist/core/runner/prepare-caps-and-workflow.js +43 -55
  65. package/dist/core/runner/prepare-config-doors.d.ts +7 -1
  66. package/dist/core/runner/prepare-config-doors.js +13 -13
  67. package/dist/core/runner/prepare-defer-classify.d.ts +13 -19
  68. package/dist/core/runner/prepare-defer-classify.js +8 -3
  69. package/dist/core/runner/prepare-gate-stations.d.ts +26 -0
  70. package/dist/core/runner/prepare-gate-stations.js +1 -1
  71. package/dist/core/runner/prepare-hands-readface.d.ts +24 -7
  72. package/dist/core/runner/prepare-hands-readface.js +60 -24
  73. package/dist/core/runner/prepare-inherited-gate.d.ts +17 -4
  74. package/dist/core/runner/prepare-inherited-gate.js +10 -2
  75. package/dist/core/runner/prepare-lsp.d.ts +2 -2
  76. package/dist/core/runner/prepare-lsp.js +1 -1
  77. package/dist/core/runner/prepare-policy-chain.js +2 -0
  78. package/dist/core/runner/prepare-project-context.d.ts +5 -1
  79. package/dist/core/runner/prepare-project-context.js +7 -4
  80. package/dist/core/runner/prepare-prompt-inputs.d.ts +4 -2
  81. package/dist/core/runner/prepare-prompt-inputs.js +4 -1
  82. package/dist/core/runner/prepare-run-refs.d.ts +9 -1
  83. package/dist/core/runner/prepare-run-refs.js +3 -2
  84. package/dist/core/runner/prepare-suspend-saga.d.ts +5 -0
  85. package/dist/core/runner/prepare-suspend-saga.js +5 -2
  86. package/dist/core/runner/prepare-task.js +19 -14
  87. package/dist/core/runner/prepare-tool-disclosure-mount.d.ts +5 -0
  88. package/dist/core/runner/prepare-tool-disclosure-mount.js +10 -2
  89. package/dist/core/runner/prepare-turn-wiring.d.ts +8 -4
  90. package/dist/core/runner/prepare-turn-wiring.js +3 -2
  91. package/dist/core/runner/prepare-wiring-manifest.d.ts +5 -5
  92. package/dist/core/runner/prepare-wiring-manifest.js +5 -2
  93. package/dist/core/runner/resume-apply.js +3 -3
  94. package/dist/core/runner/resume-checkpoint-screen.js +7 -0
  95. package/dist/core/runner/run-harness-handlers.js +4 -1
  96. package/dist/core/runner/run-identity-wiring.js +6 -2
  97. package/dist/core/runner/run-leg.d.ts +2 -0
  98. package/dist/core/runner/run-leg.js +45 -45
  99. package/dist/core/runner/run-notification-lane.js +55 -27
  100. package/dist/core/runner/run-settle-and-teardown.js +1 -0
  101. package/dist/core/runner/run-terminal-adoption.js +16 -13
  102. package/dist/core/runner/run-turn-boundary.js +15 -5
  103. package/dist/core/runner/runtask.d.ts +6 -5
  104. package/dist/core/runner/runtask.js +26 -14
  105. package/dist/core/runner/skill-base-dir.d.ts +54 -0
  106. package/dist/core/runner/skill-base-dir.js +42 -0
  107. package/dist/core/runner/synthetic-tools.d.ts +83 -48
  108. package/dist/core/runner/synthetic-tools.js +69 -32
  109. package/dist/core/runner/tool-defer-gate.d.ts +22 -9
  110. package/dist/core/runner/tool-defer-gate.js +15 -5
  111. package/dist/core/runner/tool-disclosure.d.ts +5 -0
  112. package/dist/core/runner/tool-disclosure.js +2 -0
  113. package/dist/core/runner/tool-progress-lane.d.ts +42 -0
  114. package/dist/core/runner/tool-progress-lane.js +61 -0
  115. package/dist/core/runner/turn-attachments.d.ts +43 -12
  116. package/dist/core/runner/turn-attachments.js +58 -14
  117. package/dist/core/runner-deps.d.ts +48 -7
  118. package/dist/core/sensitive-path-policy.d.ts +58 -17
  119. package/dist/core/sensitive-path-policy.js +33 -5
  120. package/dist/core/skill-listing-budget.d.ts +112 -0
  121. package/dist/core/skill-listing-budget.js +58 -0
  122. package/dist/core/swappable-deps.d.ts +9 -7
  123. package/dist/core/swappable-deps.js +7 -3
  124. package/dist/core/task-event.d.ts +96 -29
  125. package/dist/core/task-notification.d.ts +142 -16
  126. package/dist/core/task-notification.js +106 -0
  127. package/dist/core/task-result.d.ts +9 -1
  128. package/dist/core/task-spec.d.ts +46 -3
  129. package/dist/core/tighten-task-spec.js +7 -5
  130. package/dist/core/tool-model-gate.d.ts +1 -1
  131. package/dist/core/tool-policy.d.ts +19 -4
  132. package/dist/core/tool-policy.js +6 -7
  133. package/dist/core/tool-search-setting.d.ts +78 -0
  134. package/dist/core/tool-search-setting.js +90 -0
  135. package/dist/core/tool-spec.d.ts +45 -4
  136. package/dist/core/types.d.ts +2 -2
  137. package/dist/core/types.js +1 -1
  138. package/dist/core/wiring-manifest.d.ts +46 -0
  139. package/dist/core/wiring-manifest.js +9 -1
  140. package/dist/core/write-protect.d.ts +8 -3
  141. package/dist/core/write-protect.js +8 -5
  142. package/dist/engine/execution-env/node-execution-env.js +10 -4
  143. package/dist/engine/harness/agent-harness.d.ts +42 -0
  144. package/dist/engine/harness/agent-harness.js +61 -29
  145. package/dist/engine/loop/agent-loop.d.ts +68 -3
  146. package/dist/engine/loop/agent-loop.js +109 -16
  147. package/dist/index.d.ts +13 -8
  148. package/dist/index.js +11 -7
  149. package/dist/internal/harness.d.ts +1 -1
  150. package/dist/orchestration/run-workflow-tool.d.ts +3 -0
  151. package/dist/orchestration/run-workflow-tool.js +6 -1
  152. package/dist/orchestration/workflow-governance.d.ts +7 -1
  153. package/dist/orchestration/workflow-governance.js +9 -5
  154. package/dist/orchestration/workflow-primitives.d.ts +3 -0
  155. package/dist/orchestration/workflow-primitives.js +6 -1
  156. package/dist/orchestration/workflow-types.d.ts +48 -13
  157. package/dist/orchestration/workflow-types.js +1 -1
  158. package/dist/orchestration/workflow.js +45 -17
  159. package/dist/prompt-assembly/epoch.js +2 -0
  160. package/dist/prompt-assembly/packs/sema-default.js +2 -2
  161. package/dist/prompt-assembly/section-usage.d.ts +26 -0
  162. package/dist/prompt-assembly/section-usage.js +10 -0
  163. package/dist/prompt-assembly/types.d.ts +8 -6
  164. package/dist/prompts/default.d.ts +13 -0
  165. package/dist/prompts/default.js +16 -8
  166. package/dist/tools/artifact/artifact-text.d.ts +10 -1
  167. package/dist/tools/artifact/artifact-text.js +6 -3
  168. package/dist/tools/artifact/artifact-tool.d.ts +12 -2
  169. package/dist/tools/artifact/artifact-tool.js +203 -70
  170. package/dist/tools/artifact/conformance.d.ts +56 -0
  171. package/dist/tools/artifact/conformance.js +349 -0
  172. package/dist/tools/artifact/local-stub.js +1 -1
  173. package/dist/tools/artifact/reject-breaker.d.ts +28 -0
  174. package/dist/tools/artifact/reject-breaker.js +42 -0
  175. package/dist/tools/fs/bash-lexer.d.ts +132 -0
  176. package/dist/tools/fs/bash-lexer.js +563 -0
  177. package/dist/tools/fs/bash-program-position.d.ts +142 -0
  178. package/dist/tools/fs/bash-program-position.js +125 -0
  179. package/dist/tools/fs/bash-readonly-classifier.d.ts +81 -16
  180. package/dist/tools/fs/bash-readonly-classifier.js +180 -558
  181. package/dist/tools/fs/containment-face.d.ts +89 -0
  182. package/dist/tools/fs/containment-face.js +81 -0
  183. package/dist/tools/fs/fs-bash.d.ts +46 -1
  184. package/dist/tools/fs/fs-bash.js +165 -25
  185. package/dist/tools/fs/fs-read.d.ts +1 -1
  186. package/dist/tools/fs/fs-search-tools.d.ts +3 -3
  187. package/dist/tools/fs/fs-write.d.ts +4 -3
  188. package/dist/tools/fs/fs-write.js +9 -9
  189. package/dist/tools/fs/index.d.ts +24 -5
  190. package/dist/tools/fs/index.js +18 -6
  191. package/dist/tools/fs/read-deny.d.ts +4 -3
  192. package/dist/tools/fs/repo-map.d.ts +2 -2
  193. package/dist/tools/fs/safety.d.ts +18 -3
  194. package/dist/tools/fs/safety.js +11 -4
  195. package/package.json +1 -1
  196. package/test/export-surface.snapshot.json +99 -13
  197. package/wire-changes/core@7.17.7.json +49 -0
  198. package/wire-changes/core@7.18.0.json +1273 -0
  199. package/wire-changes/core@7.19.0.json +457 -0
  200. package/dist/tools/fs/read-face.d.ts +0 -68
  201. package/dist/tools/fs/read-face.js +0 -47
@@ -1,29 +1,151 @@
1
1
  import { AUTO_MODE_DEFAULT_TIMEOUT_MS, AUTO_MODE_DENIAL_AUTO_DENY_DEFAULT_MS, AUTO_MODE_DENIAL_LIMIT_DEFAULTS } from "./auto-mode-defaults.js";
2
+ import { describeRejectedValue } from "./tool-search-setting.js";
2
3
  export const AUTO_MODE_UNAVAILABLE_CAUSES = ["error", "timeout"];
3
4
  const AUTO_MODE_UNAVAILABLE_CAUSE_SET = new Set(AUTO_MODE_UNAVAILABLE_CAUSES);
4
5
  export function isAutoModeUnavailableCause(v) {
5
6
  return AUTO_MODE_UNAVAILABLE_CAUSE_SET.has(v);
6
7
  }
7
- export function classifierUnavailableDenyMessage(toolName, cause) {
8
+ export const AUTO_MODE_PARSE_FAILURES = ["missing", "conflict"];
9
+ const AUTO_MODE_PARSE_FAILURE_SET = new Set(AUTO_MODE_PARSE_FAILURES);
10
+ export function isAutoModeParseFailure(v) {
11
+ return AUTO_MODE_PARSE_FAILURE_SET.has(v);
12
+ }
13
+ export const AUTO_MODE_FALLBACK_CAUSES = ["error", "timeout", "parse_error"];
14
+ const AUTO_MODE_FALLBACK_CAUSE_SET = new Set(AUTO_MODE_FALLBACK_CAUSES);
15
+ export function isAutoModeFallbackCause(v) {
16
+ return AUTO_MODE_FALLBACK_CAUSE_SET.has(v);
17
+ }
18
+ export const AUTO_MODE_CLASSIFIER_FALLBACKS = ["main", "none"];
19
+ const AUTO_MODE_CLASSIFIER_FALLBACK_SET = new Set(AUTO_MODE_CLASSIFIER_FALLBACKS);
20
+ export function isAutoModeClassifierFallback(v) {
21
+ return AUTO_MODE_CLASSIFIER_FALLBACK_SET.has(v);
22
+ }
23
+ export function autoModeClassifierFallbackOf(v) {
24
+ if (v === undefined)
25
+ return "main";
26
+ if (isAutoModeClassifierFallback(v))
27
+ return v;
28
+ const e = new Error(`RunnerDeps.autoMode.classifierFallback must be one of ${AUTO_MODE_CLASSIFIER_FALLBACKS.map((x) => `"${x}"`).join(" | ")} ` +
29
+ `(got ${describeRejectedValue(v)}) — omit it for the default "main"`);
30
+ e.code = "config.auto_mode_classifier_fallback_invalid";
31
+ throw e;
32
+ }
33
+ export const AUTO_MODE_ROUND_NOT_ASKED_CODE = "auto_mode.round_not_asked";
34
+ export function autoModeRoundNotAsked(reason) {
35
+ let detail;
36
+ try {
37
+ const m = reason.message;
38
+ if (typeof m === "string")
39
+ detail = m.slice(0, 200);
40
+ }
41
+ catch {
42
+ }
43
+ const e = new Error(`the auto-mode classification could not be assembled: ${detail ?? describeRejectedValue(reason)}`, { cause: reason });
44
+ e.code = AUTO_MODE_ROUND_NOT_ASKED_CODE;
45
+ return e;
46
+ }
47
+ function isAutoModeRoundNotAsked(e) {
48
+ try {
49
+ return typeof e === "object" && e !== null && e.code === AUTO_MODE_ROUND_NOT_ASKED_CODE;
50
+ }
51
+ catch {
52
+ return false;
53
+ }
54
+ }
55
+ export function rungDidNotRule(v) {
56
+ if (v.kind === "unavailable")
57
+ return v.cause;
58
+ if (v.kind === "parse_error")
59
+ return "parse_error";
60
+ return undefined;
61
+ }
62
+ export function classifierUnavailableDenyMessage(toolName, cause, attribution) {
8
63
  const parenthetical = cause === "timeout" ? " (timed out)" : "";
64
+ const fb = attribution?.fallback;
65
+ const rootCause = fb === undefined ? "" : `The classifier model "${fb.from}" did not answer (${fb.cause}) and neither did the model it fell back to, "${fb.to}" (${cause}). `;
9
66
  return (`The auto-mode classifier is temporarily unavailable${parenthetical}, so auto mode cannot determine the safety of ${toolName} right now. ` +
67
+ rootCause +
10
68
  "Wait a moment and then try this action again. " +
11
69
  "If it keeps failing, continue with other tasks that don't require this action and come back to it later. " +
12
70
  "Note: reading files, searching code, and other read-only operations do not require the classifier and can still be used.");
13
71
  }
14
- export const CLASSIFIER_PARSE_FAILURE_DENY_MESSAGE = "Auto mode could not evaluate this action and is blocking it for safety — the classifier's reply carried no verdict.";
15
- export function parseAutoModeResponse(text) {
72
+ export function classifierParseFailureDenyMessage(attribution) {
73
+ const stem = "Auto mode could not evaluate this action and is blocking it for safety";
74
+ return attribution === undefined ? `${stem} — the classifier's reply carried no verdict.` : `${stem} — the classifier model "${attribution.modelUsed}" replied without a verdict.`;
75
+ }
76
+ function stripThinking(text) {
77
+ return text.replace(/<(thinking|think)>[\s\S]*?<\/\1>/gi, "").replace(/<(thinking|think)>[\s\S]*$/i, "");
78
+ }
79
+ const BARE_ANSWER_WRAPPERS = [
80
+ ["**", "**"],
81
+ ["__", "__"],
82
+ ["*", "*"],
83
+ ["_", "_"],
84
+ ["`", "`"],
85
+ ['"', '"'],
86
+ ["'", "'"],
87
+ ["«", "»"],
88
+ ];
89
+ function unwrapBareAnswer(text) {
90
+ let s = text;
91
+ for (;;) {
92
+ const before = s;
93
+ s = s.trim().replace(/[.!]+$/, "").trim();
94
+ for (const [open, close] of BARE_ANSWER_WRAPPERS) {
95
+ if (s.length > open.length + close.length && s.startsWith(open) && s.endsWith(close)) {
96
+ s = s.slice(open.length, s.length - close.length);
97
+ break;
98
+ }
99
+ }
100
+ if (s === before)
101
+ return s;
102
+ }
103
+ }
104
+ function bareAnswerOf(text) {
105
+ const visible = text.replace(/[\u200b-\u200d\ufeff]/g, "").trim();
106
+ if (visible.includes("```") || visible.includes("\n"))
107
+ return undefined;
108
+ let s = unwrapBareAnswer(visible);
109
+ const labelled = labelledAnswer(s);
110
+ if (labelled !== undefined)
111
+ s = unwrapBareAnswer(labelled);
112
+ const word = /^(yes|no)$/i.exec(s)?.[1]?.toLowerCase();
113
+ return word === "yes" || word === "no" ? word : undefined;
114
+ }
115
+ function labelledAnswer(s) {
116
+ const label = "(?:final[ \\t]+)?(?:block|answer|verdict|decision)";
117
+ for (const [open, close] of [["", ""], ...BARE_ANSWER_WRAPPERS]) {
118
+ const re = new RegExp(`^${escapeRe(open)}${label}${escapeRe(close)}[ \\t]*[:=][ \\t]*(.*)$`, "i");
119
+ const m = re.exec(s);
120
+ if (m !== null)
121
+ return m[1];
122
+ }
123
+ return undefined;
124
+ }
125
+ function escapeRe(s) {
126
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
127
+ }
128
+ export function parseAutoModeResponse(text, options) {
16
129
  const rawAnswers = new Set([...text.matchAll(/<block>(yes|no)\b/gi)].map((m) => m[1].toLowerCase()));
17
130
  if (rawAnswers.size > 1)
18
- return { kind: "parse_error", raw: text };
19
- const stripped = text.replace(/<thinking>[\s\S]*?<\/thinking>/g, "").replace(/<thinking>[\s\S]*$/, "");
131
+ return { kind: "parse_error", raw: text, form: "conflict" };
132
+ const stripped = stripThinking(text);
20
133
  const hits = [...stripped.matchAll(/<block>(yes|no)\b(<\/block>)?/gi)];
21
- if (hits.length === 0)
22
- return { kind: "parse_error", raw: text };
23
- const answers = new Set(hits.map((m) => m[1].toLowerCase()));
24
- if (answers.size !== 1)
25
- return { kind: "parse_error", raw: text };
26
- if (!answers.has("yes"))
134
+ let answer;
135
+ if (hits.length > 0) {
136
+ const answers = new Set(hits.map((m) => m[1].toLowerCase()));
137
+ if (answers.size !== 1)
138
+ return { kind: "parse_error", raw: text, form: "conflict" };
139
+ answer = answers.has("yes") ? "yes" : "no";
140
+ }
141
+ else {
142
+ answer = options?.bareAnswer === false ? undefined : bareAnswerOf(stripped);
143
+ if (answer === undefined)
144
+ return { kind: "parse_error", raw: text, form: "missing" };
145
+ if (rawAnswers.size > 0 && !rawAnswers.has(answer))
146
+ return { kind: "parse_error", raw: text, form: "conflict" };
147
+ }
148
+ if (answer === "no")
27
149
  return { kind: "allow" };
28
150
  const category = /<category>([\s\S]*?)<\/category>/i.exec(stripped)?.[1]?.trim() ?? "";
29
151
  const reason = /<reason>([\s\S]*?)<\/reason>/.exec(stripped)?.[1]?.trim() ?? "";
@@ -31,6 +153,15 @@ export function parseAutoModeResponse(text) {
31
153
  }
32
154
  export function createAutoModeDecider(opts) {
33
155
  const timeoutMs = opts.timeoutMs ?? AUTO_MODE_DEFAULT_TIMEOUT_MS;
156
+ if (opts.candidates !== undefined) {
157
+ if (opts.candidates.length === 0) {
158
+ throw new Error("AutoModeDeciderOptions.candidates must name at least one model — omit the member for the leg's own model, never an empty ladder");
159
+ }
160
+ const bad = opts.candidates.findIndex((m) => typeof m !== "string" || m.trim() === "");
161
+ if (bad >= 0)
162
+ throw new Error(`AutoModeDeciderOptions.candidates[${bad}] must be a non-empty model id (got ${JSON.stringify(opts.candidates[bad])})`);
163
+ }
164
+ const ladder = opts.candidates ?? [undefined];
34
165
  const decide = async (input, signal) => {
35
166
  const startedAt = performance.now();
36
167
  const verdict = await decideUnreported(input, signal);
@@ -41,6 +172,7 @@ export function createAutoModeDecider(opts) {
41
172
  ms: Math.max(0, Math.round(performance.now() - startedAt)),
42
173
  verdict: verdict.kind,
43
174
  ...(verdict.kind === "unavailable" ? { cause: verdict.cause } : {}),
175
+ ...(verdict.classifier !== undefined ? { classifier: verdict.classifier } : {}),
44
176
  });
45
177
  }
46
178
  catch {
@@ -50,6 +182,37 @@ export function createAutoModeDecider(opts) {
50
182
  };
51
183
  return { decide };
52
184
  async function decideUnreported(input, signal) {
185
+ const causes = [];
186
+ const round = { notAsked: false };
187
+ let verdict = { kind: "unavailable", cause: "error" };
188
+ let i = 0;
189
+ for (; i < ladder.length; i++) {
190
+ const name = ladder[i];
191
+ verdict = await askRung(input, signal, name !== undefined ? { index: i, model: name } : undefined, round);
192
+ const cause = rungDidNotRule(verdict);
193
+ if (cause === undefined)
194
+ break;
195
+ causes.push(cause);
196
+ if (round.notAsked || signal?.aborted)
197
+ break;
198
+ }
199
+ const landed = round.notAsked ? 0 : Math.min(i, ladder.length - 1);
200
+ const requested = ladder[0];
201
+ const used = ladder[landed];
202
+ if (requested === undefined || used === undefined)
203
+ return verdict;
204
+ const from = landed > 0 ? ladder[landed - 1] : undefined;
205
+ const movedBy = landed > 0 ? causes[landed - 1] : undefined;
206
+ return {
207
+ ...verdict,
208
+ classifier: Object.freeze({
209
+ modelRequested: requested,
210
+ modelUsed: used,
211
+ ...(from !== undefined && movedBy !== undefined ? { fallback: Object.freeze({ from, to: used, cause: movedBy }) } : {}),
212
+ }),
213
+ };
214
+ }
215
+ async function askRung(input, signal, rung, round) {
53
216
  let timer;
54
217
  const inner = new AbortController();
55
218
  const onOuterAbort = () => inner.abort();
@@ -69,7 +232,7 @@ export function createAutoModeDecider(opts) {
69
232
  reject(new AutoModeTimeout());
70
233
  }
71
234
  }, timeoutMs);
72
- opts.classify(input, inner.signal).then((v) => {
235
+ opts.classify(input, inner.signal, rung).then((v) => {
73
236
  if (!settled) {
74
237
  settled = true;
75
238
  resolve(v);
@@ -84,9 +247,13 @@ export function createAutoModeDecider(opts) {
84
247
  return parseAutoModeResponse(raw);
85
248
  }
86
249
  catch (e) {
250
+ if (isAutoModeRoundNotAsked(e)) {
251
+ round.notAsked = true;
252
+ return { kind: "unavailable", cause: "error" };
253
+ }
87
254
  if (signal?.aborted)
88
255
  return { kind: "unavailable", cause: "error" };
89
- return { kind: "unavailable", cause: e instanceof AutoModeTimeout ? "timeout" : "error" };
256
+ return { kind: "unavailable", cause: isAutoModeTimeout(e) ? "timeout" : "error" };
90
257
  }
91
258
  finally {
92
259
  if (timer !== undefined)
@@ -101,6 +268,14 @@ class AutoModeTimeout extends Error {
101
268
  super("auto-mode classify timeout");
102
269
  }
103
270
  }
271
+ function isAutoModeTimeout(e) {
272
+ try {
273
+ return e instanceof AutoModeTimeout;
274
+ }
275
+ catch {
276
+ return false;
277
+ }
278
+ }
104
279
  export function readDenialLimitFallback(v) {
105
280
  if (typeof v !== "object" || v === null)
106
281
  return undefined;
@@ -369,6 +369,13 @@ export interface PendingSteerInput {
369
369
  inputId?: string;
370
370
  priority?: SystemInjectionPriority;
371
371
  }
372
+ /** True ONLY for a PLAIN data record (not null / array / class-instance / proxy-with-odd-prototype) — the only
373
+ * shape we safely digest. Adversarial args (a proxy, a getter that throws) must never crash the mint, so this +
374
+ * {@link safeDataValue}/{@link safeKeys} make {@link buildRiskDescriptor} fully TOTAL. */
375
+ export declare function isPlainRecord(x: unknown): x is Record<string, unknown>;
376
+ /** Read `obj[key]` as a DATA property only — NEVER invokes a getter/accessor (so a throwing getter can't escape
377
+ * AND a non-deterministic getter can't vary the digest); a trapped descriptor read ⇒ undefined. */
378
+ export declare function safeDataValue(obj: Record<string, unknown>, key: string): unknown;
372
379
  /**
373
380
  * design/80 §D-E: build the DETERMINISTIC, REDACTED {@link RiskDescriptor} for an escalation checkpoint at
374
381
  * MINT. Pure — a function ONLY of (`toolName`, `args`, the D-2 `safety` axis, the `shellGated` flag); it reads
@@ -969,6 +976,16 @@ export interface CheckpointState {
969
976
  * scope for any in-row invariant). Absent = the row was minted ungoverned. */
970
977
  realApproval?: true;
971
978
  };
979
+ /**
980
+ * The WRITE-face containment state at suspend: the RESOLVED write face, written ONLY when it is
981
+ * non-default ("open"). An absent section IS "roots" for every reader — which is why it carries NO
982
+ * version stamp: losing it can only make a resume STRICTER. Resume folds STRICTER-WINS, the read seat's
983
+ * law: roots unless the section says open AND the live resolution says open. No deny half and no
984
+ * governance marker.
985
+ */
986
+ writeFace?: {
987
+ face: "open" | "roots";
988
+ };
972
989
  /**
973
990
  * design/319 (A ticket) — the session's reminder provenance mark
974
991
  * ({@link import("./reminder-mint.js").mintReminderMark}), persisted so a durable resume keeps
@@ -1961,10 +1978,14 @@ export declare function summarizeCheckpoint(cp: Checkpoint): CheckpointSummary;
1961
1978
  * parks a steer in the retired pre-queue single seat — terminal for the row's bytes; no worker version
1962
1979
  * reads that seat, the host decides the parked instruction itself).
1963
1980
  * · `invalid_outcome` pre-CAS row-integrity arms: `"real_approval_damaged"`, `"real_approval_forged"`,
1964
- * `"read_face_damaged"`, `"read_face_forged"`, `"constraint_chain_missing"` (all terminal for the row's
1981
+ * `"read_face_damaged"`, `"read_face_forged"`, `"write_face_damaged"`, `"constraint_chain_missing"` (all terminal for the row's
1965
1982
  * current bytes — no worker version redeems a damaged row).
1966
1983
  */
1967
- export type CheckpointErrorReason = "version_newer" | "env_factory_missing" | "governed_unwired" | "origin_missing" | "ledger_event_unknown" | "seat_retired" | "real_approval_damaged" | "real_approval_forged" | "read_face_damaged" | "read_face_forged" | "constraint_chain_missing";
1984
+ export type CheckpointErrorReason = "version_newer" | "env_factory_missing" | "governed_unwired" | "origin_missing" | "ledger_event_unknown" | "seat_retired" | "real_approval_damaged" | "real_approval_forged" | "read_face_damaged" | "read_face_forged"
1985
+ /** `state.writeFace` is present but not a well-formed section. No `_forged` twin: the section is
1986
+ * legal-and-absent at every version, so there is no version to judge a fabrication against — and a
1987
+ * fabricated "open" still needs the LIVE resolution to say open too. */
1988
+ | "write_face_damaged" | "constraint_chain_missing";
1968
1989
  /** A typed checkpoint-store error so callers branch on `code` (mirrors `SessionError`). */
1969
1990
  export declare class CheckpointError extends Error {
1970
1991
  readonly code: "checkpoint.already_exists" | "checkpoint.already_resolved" | "checkpoint.not_found"
@@ -73,7 +73,7 @@ export const MAX_PENDING_STEER_CHARS = 16_000;
73
73
  export const PENDING_STEER_QUEUE_BYTE_BUDGET_BYTES = 48_000;
74
74
  export const MAX_PENDING_STEER_ENTRIES = Math.floor(PENDING_STEER_QUEUE_BYTE_BUDGET_BYTES / MAX_PENDING_STEER_CHARS);
75
75
  export const PENDING_STEER_FROZEN_FIELDS = ["text", "trusted", "actor", "seq", "inputId", "priority"];
76
- function isPlainRecord(x) {
76
+ export function isPlainRecord(x) {
77
77
  try {
78
78
  if (x === null || typeof x !== "object" || Array.isArray(x))
79
79
  return false;
@@ -84,7 +84,7 @@ function isPlainRecord(x) {
84
84
  return false;
85
85
  }
86
86
  }
87
- function safeDataValue(obj, key) {
87
+ export function safeDataValue(obj, key) {
88
88
  try {
89
89
  const d = Object.getOwnPropertyDescriptor(obj, key);
90
90
  return d && "value" in d ? d.value : undefined;
@@ -23,6 +23,22 @@ export interface EngineNotice {
23
23
  * - `"config.tool_face_undeclared"` / `"config.tool_face_invalid"` (design/388 §2.7) — an MCP server's
24
24
  * `toolFaces` overlay names a tool the listing does not carry / does not fit the tool's materialized
25
25
  * schema (the tool mounts bare); once per leg per tool; `detail: { server, tool, problem?, sessionId, runId }`.
26
+ * - `"skills.listing_truncated"` — the `<skills>` disclosure block did not fit its byte budget, so
27
+ * at least one skill is listed without its description (`nameOnly`) or, in the pathological case,
28
+ * not listed at all (`dropped`); the `Skill` tool still serves every one of them. Audience
29
+ * `operator` — raising `TaskSpec.skillListingBudgetFraction`, lowering `skillListingMaxDescChars`
30
+ * or declaring fewer skills are all deployment acts. `detail: { budgetBytes,
31
+ * requestedBudgetBytes, descMaxChars, total, kept, nameOnly: [name], dropped: [name], sessionId }`
32
+ * — `requestedBudgetBytes` is what the declared fraction asked for BEFORE the delivery lane's
33
+ * structural clamp, and differs from `budgetBytes` only when that clamp bound, so a listing
34
+ * shortened by the lane is distinguishable from one shortened by the deployment's own fraction.
35
+ * Announced by the lane that DELIVERS a listing frame — the leg's first-frame producer, or the
36
+ * turn boundary that a drift (or a resume leg's initial listing) rides — once per delivered frame
37
+ * the budget actually degraded, and never for one that fit. Every count is read off that frame, so
38
+ * `total` is the whole roster on an initial listing and the newly announced set on a drift frame.
39
+ * A run whose deployment turned the listing off (`attachments.skillsListing: false`) emits nothing,
40
+ * since nothing is rendered. There is NO dedup key: each notice is one delivered frame`s cost, and
41
+ * a frame is delivered (and committed) once.
26
42
  * - `"tool_result.offload_put_failed"` (#167) — a clear-with-offload persist's fire-and-forget put
27
43
  * failed; THIS attempt stored nothing (the failure arm reports, it never re-inserts under the
28
44
  * ref) — an earlier attempt of the same idempotent re-put may already have stored the row, so
@@ -85,9 +101,12 @@ export interface EngineNotice {
85
101
  * The SEAT is announced, not a leg's resolved face: resolution (task seat, governance, the
86
102
  * read-only mount) still runs per prepare. In-flight legs keep the face they prepared under;
87
103
  * every later prepare resolves against the new declaration. A candidate outside the value gate
88
- * (`assertReadFaceValue`) throws atomically and mints nothing. Own `occurrenceId` for the same
104
+ * (`assertContainmentFaceValue`) throws atomically and mints nothing. Own `occurrenceId` for the same
89
105
  * reason as the catalog line (no run, no session, a two-word payload). Dedup key:
90
106
  * (code, occurrenceId).
107
+ * - `"config.write_face_swapped"` — the same line for the WRITE seat (`RunnerDeps.writeFace`),
108
+ * same payload, same laws. A SEPARATE code rather than a face field on the line above: the audience
109
+ * table and every consumer table are keyed by code.
91
110
  * - `"route.fallback_to_primary"` (key↔URL pairing, `src/brain/route-adjudicator.ts`) — a
92
111
  * DERIVED-leg model (role/tier/system-default resolution, never a caller-explicit one) failed
93
112
  * the pairing pre-flight and the seat fell back to the primary model instead of sinking the
@@ -100,6 +119,35 @@ export interface EngineNotice {
100
119
  * session falling back on the same seat used to mint byte-identical lines; `runId` is what
101
120
  * tells one task falling back twice apart from two tasks falling back once each.
102
121
  * Explicitly-named models never mint this: they refuse at the brain's request gate instead.
122
+ * - `"route.classifier_unroutable"` (same pre-flight, the other POSTURE) — the derived classifier seat
123
+ * failed the same pairing pre-flight while the deployment had declared NO fallback
124
+ * (`RunnerDeps.autoMode.classifierFallback: "none"`), so the seat was KEPT and its calls are refused at
125
+ * the request gate rather than served by the primary model. The line is why "no swap" is still not "no
126
+ * word". `detail: { seat, from, cause, fixHint, action: "refused", sessionId?, runId? }` — the twin
127
+ * above's keys MINUS `to` (nothing moved, and a `to` naming a model that was not used is the one lie a
128
+ * consumer cannot detect); `action` is the discriminator, so a reader never infers the posture from that
129
+ * absence. Audience `operator`, same entitlement reading as its twin. Dedup key: (code, seat) per run.
130
+ * - `"classifier.fallback"` (the classifier's candidate ladder, `src/core/runner/classifier-ladder.ts`)
131
+ * — a gated call's classification did NOT rule on the classifier seat and was re-asked on the model
132
+ * behind it. Sibling of `route.fallback_to_primary` one layer later: that one fires when the derived
133
+ * model is not ROUTABLE at all, this one when it is routable and still does not answer.
134
+ * `detail: { from, to, cause, sessionId?, runId? }` — `cause` is the `AutoModeFallbackCause` word, the
135
+ * two ids are the rungs. Dedup key: (code, from, cause) PER RUN; a second CAUSE on the same rung is
136
+ * its own line.
137
+ * - `"classifier.role_defaulted"` (`src/core/runner/classifier-ladder.ts`, the seat resolver) — no
138
+ * `classifier` role names a model, so another model STANDS IN for the seat. `detail: { model, reason,
139
+ * thinkingOffSpelled, sessionId?, runId? }` — `reason` ∈ `tier_below_main` (the nearest directly-bound
140
+ * tier below the run's main model) / `tier_table` (the static `flash` row, a main bound to no tier) /
141
+ * `role_fallback` (the `summarize → default` chain, no tiers) / `main_only` (the main model is the only
142
+ * model there is) / `role_unresolvable` (the role named a model the catalog cannot resolve; the main
143
+ * stands in); `thinkingOffSpelled` = whether that model's wire can carry a disable key. Minted
144
+ * AFTER the route pre-flight, so it names the seat actually used. Once per run.
145
+ * - `"classifier.contract_broken"` (same file, the round observer) — ONE model answered
146
+ * `AUTO_MODE_CLASSIFIER_CONTRACT_BROKEN_AFTER` CONSECUTIVE classifications without a verdict (each already
147
+ * re-asked once by the leg). `detail: { model, rung, consecutive, sessionId?, runId? }` — `rung` is the
148
+ * ladder position (0 the seat, 1 the model behind it), since two rungs may share an id. A ruling from
149
+ * that rung resets its count; an `unavailable` round neither counts nor resets. Once per run per rung,
150
+ * and by construction before the denial limit's third block hands the user its card.
103
151
  * - `"route.base_url_changed_key_unchanged"` (key↔URL pairing) — `Runner.swapDeps` moved a
104
152
  * same-name entry's `baseUrl` while its Model-visible credential half (auth-bearing headers)
105
153
  * did not change: legal (a provider changing domains), but worth one loud line — if the
@@ -120,6 +168,9 @@ export interface EngineNotice {
120
168
  * a library-direct `createHandsToolkit` mount announces at toolkit creation (one per mount,
121
169
  * through the band-local `HandsToolkitOptions.onNotice` seat, absent ⇒ `console.warn`);
122
170
  * `detail: { seat, declared, inForce, cause }`.
171
+ * - `"config.write_face_deployment_clamped"` — the write seat's twin of the line above, with the
172
+ * same stations, the same once-per-sink ledger (its OWN ledger row) and the same detail shape. The
173
+ * cause differs in prose only: that mount carries no write faces at all.
123
174
  * - `"config.tool_model_gate_removed"` (design/277) — the model gate trimmed default-mounted
124
175
  * scaffold entries from a task's roster. One notice per (model, class), de-duplicated once per
125
176
  * `onNotice` SINK on the (modelId, class, canonical sorted removed-name set) line (same unit
@@ -629,6 +680,31 @@ export declare function deliverEngineNotice(onNotice: ((notice: EngineNotice) =>
629
680
  * one repeated disclosure about one run. The account is per-RUN by its own contract sentence ("at most
630
681
  * once per family, per run"), so it must carry the run's own id. Same omit-never-fabricate rule.
631
682
  */
683
+ /**
684
+ * What this leg still OWES a person, one count per family — the input to {@link undrainedUserInputNotices}.
685
+ * TWO lanes accept caller input and either can be holding some when a run ends: the harness's live queue
686
+ * (`held`, the at-agent_end snapshot, minus what a durable park migrated onto the new checkpoint) and a resumed
687
+ * leg's undelivered continuation frames (`owedContinuation` — the checkpoint's parked steers and the caller's
688
+ * wake message, minus what the same migration carried). They are ONE book: a frame is owed for the same reason
689
+ * on either lane — accepted, and never in the transcript — and counting the live queue alone is what let a leg
690
+ * that committed no pause drop a person's instruction in silence (the original row is already spent by then).
691
+ */
692
+ export declare function owedUserInputCounts(held: {
693
+ steer: number;
694
+ followUp: number;
695
+ } | undefined, migrated: {
696
+ steer: number;
697
+ followUp: number;
698
+ }, continuation: {
699
+ frames: ReadonlyArray<{
700
+ seq?: number;
701
+ }>;
702
+ carried: number;
703
+ rowMayReturn: boolean;
704
+ }): {
705
+ steer: number;
706
+ followUp: number;
707
+ };
632
708
  export declare function undrainedUserInputNotices(counts: {
633
709
  steer: number;
634
710
  followUp: number;
@@ -31,6 +31,13 @@ export function deliverEngineNotice(onNotice, notice) {
31
31
  }
32
32
  console.warn(notice.message);
33
33
  }
34
+ export function owedUserInputCounts(held, migrated, continuation) {
35
+ const owedContinuation = (continuation.rowMayReturn ? continuation.frames.filter((f) => f.seq === undefined).length : continuation.frames.length) - continuation.carried;
36
+ return {
37
+ steer: Math.max(0, (held?.steer ?? 0) - migrated.steer) + Math.max(0, owedContinuation),
38
+ followUp: Math.max(0, (held?.followUp ?? 0) - migrated.followUp),
39
+ };
40
+ }
34
41
  export function undrainedUserInputNotices(counts, taskId, sessionId, runId) {
35
42
  const tid = taskId !== undefined ? { taskId } : {};
36
43
  const sid = sessionId !== undefined ? { sessionId } : {};
@@ -1,5 +1,6 @@
1
1
  import { inlineUntrusted } from "./untrusted-text.js";
2
2
  import { PROBE_REASON_MAX, normalizeProbeCause } from "./checkpoint-store.js";
3
+ import { normalizeReadRootCandidate } from "./read-root-candidate.js";
3
4
  import { traceHookCrash } from "./runner/gate-exit.js";
4
5
  function withProbeTimeout(p, ms, signal) {
5
6
  if (ms === undefined && signal === undefined)
@@ -80,6 +81,7 @@ export async function runGateFold(pass) {
80
81
  let probeReason;
81
82
  let probeCause;
82
83
  let probeMandated;
84
+ let probeReadRootCandidate;
83
85
  if (input.irreversibility === "maybe" && input.reversibilityProbe) {
84
86
  let reversible = false;
85
87
  const probeArgs = pass.policyRewrite !== undefined ? pass.policyRewrite : pass.currentInput;
@@ -105,6 +107,7 @@ export async function runGateFold(pass) {
105
107
  probeCause = normalizeProbeCause(verdict?.cause);
106
108
  if (verdict?.mandated === true)
107
109
  probeMandated = true;
110
+ probeReadRootCandidate = normalizeReadRootCandidate(verdict?.readRootCandidate);
108
111
  }
109
112
  }
110
113
  catch (err) {
@@ -123,6 +126,7 @@ export async function runGateFold(pass) {
123
126
  ...(probeReason !== undefined ? { probeReason } : {}),
124
127
  ...(probeCause !== undefined ? { probeCause } : {}),
125
128
  ...(probeMandated === true ? { probeMandated: true } : {}),
129
+ ...(probeReadRootCandidate !== undefined ? { probeReadRootCandidate } : {}),
126
130
  };
127
131
  pass.tightenedBy = input.shellGated === true ? "shell_gate" : "safety";
128
132
  }
@@ -139,7 +143,7 @@ export async function runGateFold(pass) {
139
143
  pass.tightenedBy = "safety";
140
144
  }
141
145
  if (pass.decision.action === "allow" && input.writeProtectionCheck !== undefined) {
142
- const writeProtectedHit = input.writeProtectionCheck(toolName, pass.policyRewrite !== undefined ? pass.policyRewrite : pass.currentInput, pass.req.face);
146
+ const writeProtectedHit = input.writeProtectionCheck(toolName, pass.policyRewrite !== undefined ? pass.policyRewrite : pass.currentInput, pass.req.face, input.trackedCwd?.());
143
147
  if (writeProtectedHit !== null) {
144
148
  pass.decision = {
145
149
  action: "ask",
@@ -1,6 +1,6 @@
1
1
  import { attestClassifierDenyCause, classifierDenyCauseOf, decisionText, describeThrown } from "./tool-policy.js";
2
- import { askOriginOf, classifierMayAnswer, persistedRuleMandateOf, probeBoundaryMandate, safetyAxisOf } from "./ask-origin.js";
3
- import { CLASSIFIER_PARSE_FAILURE_DENY_MESSAGE, classifierUnavailableDenyMessage, denialLimitFallbackMessage, unarmedWindow } from "./auto-mode.js";
2
+ import { classifierMayAnswer, persistedRuleMandateOf, probeBoundaryMandate, safetyAxisOf, stampAskOrigin } from "./ask-origin.js";
3
+ import { classifierParseFailureDenyMessage, classifierUnavailableDenyMessage, denialLimitFallbackMessage, unarmedWindow } from "./auto-mode.js";
4
4
  import { inlineUntrusted } from "./untrusted-text.js";
5
5
  import { isRuleBehavior } from "./permission-rule-model.js";
6
6
  import { ASK_USER_QUESTION_TOOL_NAME } from "./ask-question.js";
@@ -346,7 +346,7 @@ export async function runGateLanes(pass) {
346
346
  if (pass.decision.action === "ask") {
347
347
  const { ruleStoreUnreadable: _selfDeclared, ...unstamped } = pass.decision;
348
348
  const stamped = { ...unstamped, ruleEvidence: mintRuleEvidence(personalEvidence), ...(laneCoverage !== undefined ? { segmentCoverage: laneCoverage } : {}), ...(originFacts.ruleStore !== undefined ? { ruleStoreUnreadable: originFacts.ruleStore } : {}) };
349
- pass.decision = { ...stamped, origin: askOriginOf(stamped, originFacts) };
349
+ pass.decision = stampAskOrigin(stamped, originFacts);
350
350
  }
351
351
  if (allowLayerMayClear() && pass.decision.action === "ask" && persistedRuleMandate === undefined && pass.decision.matchedAskRule === undefined && pass.req.toolName === COMMAND_RULE_TOOL) {
352
352
  const commandParam = catalogRuleFaceOf(COMMAND_RULE_TOOL)?.primaryParams[0] ?? "command";
@@ -369,6 +369,7 @@ export async function runGateLanes(pass) {
369
369
  const verdict = await input.autoMode.decider
370
370
  .decide({ req: pass.req, askMessage: decisionText(pass.decision) }, input.abortSignal)
371
371
  .catch(() => ({ kind: "unavailable", cause: "error" }));
372
+ pass.classifierRound = verdict.classifier;
372
373
  if (verdict.kind === "allow") {
373
374
  input.autoMode.denialTracking?.recordAllow();
374
375
  pass.decision = {
@@ -381,12 +382,12 @@ export async function runGateLanes(pass) {
381
382
  else if (verdict.kind === "unavailable") {
382
383
  pass.decision = attestClassifierDenyCause({
383
384
  action: "deny",
384
- message: classifierUnavailableDenyMessage(toolName, verdict.cause),
385
+ message: classifierUnavailableDenyMessage(toolName, verdict.cause, verdict.classifier),
385
386
  decisionReason: "classifier",
386
387
  }, "unavailable", pass.req);
387
388
  }
388
389
  else {
389
- const reason = verdict.kind === "block" ? (verdict.reason ? inlineUntrusted(verdict.reason) : "") : CLASSIFIER_PARSE_FAILURE_DENY_MESSAGE;
390
+ const reason = verdict.kind === "block" ? (verdict.reason ? inlineUntrusted(verdict.reason) : "") : classifierParseFailureDenyMessage(verdict.classifier);
390
391
  const category = verdict.kind === "block" && verdict.category ? inlineUntrusted(verdict.category) : "";
391
392
  const tracked = input.autoMode.denialTracking?.recordBlock();
392
393
  if (tracked?.limitReached === true) {
@@ -397,7 +398,7 @@ export async function runGateLanes(pass) {
397
398
  requiresRealApproval: true,
398
399
  denialLimitFallback: tracked.fallback,
399
400
  };
400
- pass.decision = { ...fallbackAsk, origin: askOriginOf(fallbackAsk, originFacts) };
401
+ pass.decision = stampAskOrigin(fallbackAsk, originFacts);
401
402
  }
402
403
  else {
403
404
  const denied = {
@@ -657,7 +658,7 @@ export async function runGateLanes(pass) {
657
658
  const { ruleStoreUnreadable: _selfDeclaredOnEdit, ...recheckUnstamped } = recheck;
658
659
  const editAskSnapshot = { ...recheckUnstamped, ruleEvidence: mintRuleEvidence({ dotsAbsent: "not_adjudicated" }) };
659
660
  markedThisRound = undefined;
660
- const editAskDecision = { ...editAskSnapshot, ...(editRuleStore !== undefined ? { ruleStoreUnreadable: editRuleStore } : {}), origin: askOriginOf(editAskSnapshot, askOriginFacts(editOrg.org, editRuleStore)) };
661
+ const editAskDecision = stampAskOrigin({ ...editAskSnapshot, ...(editRuleStore !== undefined ? { ruleStoreUnreadable: editRuleStore } : {}) }, askOriginFacts(editOrg.org, editRuleStore));
661
662
  const editAskReq = { toolName, args: editArgs, toolCallId, ...(pass.req.face !== undefined ? { face: pass.req.face } : {}) };
662
663
  const rr = await (callSignal !== undefined ? resolveAsk(editAskDecision, editAskReq, callSignal) : resolveAsk(editAskDecision, editAskReq));
663
664
  ledger.fromResolved(rr, editAskDecision, toolName);
@@ -676,7 +677,7 @@ export async function runGateLanes(pass) {
676
677
  editRewrittenSinceHuman = false;
677
678
  }
678
679
  if (editDenied === undefined && editRewrittenSinceHuman && input.writeProtectionCheck !== undefined) {
679
- const editHit = input.writeProtectionCheck(toolName, editArgs, pass.req.face);
680
+ const editHit = input.writeProtectionCheck(toolName, editArgs, pass.req.face, input.trackedCwd?.());
680
681
  if (editHit !== null) {
681
682
  editDenied = {
682
683
  action: "deny",
@@ -8,6 +8,7 @@
8
8
  * verdict ended.
9
9
  */
10
10
  import { type AskOrigin, type AssertAllKeysHandled } from "./ask-origin.js";
11
+ import { type AutoModeClassifierRound } from "./auto-mode.js";
11
12
  /**
12
13
  * How the wait an ask was in ENDED — the closed set. One word per way a wait can end; the `who` on the
13
14
  * record says which party ended it and is fixed BY the word (see {@link Settlement}).
@@ -92,6 +93,31 @@ export type Settlement = {
92
93
  };
93
94
  when: number;
94
95
  };
96
+ /** The person arm of {@link Settlement} — the shape the two human words inhabit. Named so a site that
97
+ * both composes one and quotes its note can read the seat off the record instead of re-deriving it. */
98
+ export type PersonSettlement = Extract<Settlement, {
99
+ kind: "human_allowed" | "human_refused";
100
+ }>;
101
+ /**
102
+ * A decider's NOTE, normalised — the one rule for "did the decider say anything": empty text is a
103
+ * decider who attached nothing and reads as absent wherever the note travels. Whitespace-only text is
104
+ * not empty by this rule and stays a note: that half is PINNED CURRENT BEHAVIOUR, not a promise —
105
+ * nothing here requires a note to carry content, and narrowing it is a separate decision with its own
106
+ * consumers to warn.
107
+ */
108
+ export declare function decidersNote(text: string | undefined): string | undefined;
109
+ /**
110
+ * THE mint for a person's settlement: the synchronous resolver's human arms and the durable decide
111
+ * lane's person arm compose their record here, so one refusal plus one reason text cannot end as two
112
+ * different records depending on which path carried it.
113
+ *
114
+ * @contract settlement.note — the `note` seat is filled exactly when the decider attached a reason, and
115
+ * its absence is how the record says the refusal was bare (the runner reads that absence, and nothing
116
+ * else, as the issuing batch's control-flow boundary). Empty text folds to absence HERE, once, for
117
+ * every path that fills the seat ({@link decidersNote}); a site that hand-builds the record instead is
118
+ * a second answer to the same question, which is how one path came to halt a batch the other let run.
119
+ */
120
+ export declare function settledByPerson(kind: PersonSettlement["kind"], approver: string | undefined, note?: string): PersonSettlement;
95
121
  /** Which settlement kinds are REFUSALS (the ask ended as a deny). Total over the closed set — the fence
96
122
  * below refuses a member without a row — so invariant I2 is a table lookup, not a list in a condition. */
97
123
  export declare const SETTLEMENT_IS_REFUSAL: {
@@ -189,13 +215,23 @@ export type CauseTableCoversEveryDeniedBy = AssertAllKeysHandled<Exclude<DeniedB
189
215
  /** The final disposition of a gate pass. `cause` is present exactly when the deny came out of an auto-mode
190
216
  * classifier round that did not RULE ({@link ClassifierDenyCause}); a ruling block and every non-classifier
191
217
  * deny carry none. Invariant I5 ({@link screenGateOutcome}): a cause stands only beside a layer word in
192
- * {@link DENIED_BY_MAY_CARRY_CAUSE}. */
218
+ * {@link DENIED_BY_MAY_CARRY_CAUSE}.
219
+ *
220
+ * `classifier` — the classification's model attribution, on BOTH arms (a classification ends either way,
221
+ * allow or deny): which model it was for, which one answered, and — only when the ladder moved — the rung it
222
+ * left and why. ABSENT reads UNKNOWN, never "the seat answered" (that is `fallback` absent WITH the member
223
+ * present), and a consumer cannot tell its three causes apart from the record alone: no classification
224
+ * happened; one ran on a decider given no candidate ladder, which can name no model; or one ran on an
225
+ * ANCESTOR's inherited chain, which carries the FORM (`cause`) but has no seat for the attribution yet.
226
+ * Additive. Invariant I6 ({@link screenGateOutcome}) screens its shape. */
193
227
  export type GateDisposition = {
194
228
  kind: "allowed";
229
+ classifier?: AutoModeClassifierRound;
195
230
  } | {
196
231
  kind: "denied";
197
232
  deniedBy: DeniedBy;
198
233
  cause?: ClassifierDenyCause;
234
+ classifier?: AutoModeClassifierRound;
199
235
  };
200
236
  /**
201
237
  * The WHOLE record of one tool-gate pass: the final disposition, plus — when this pass settled an ask —
@@ -212,7 +248,11 @@ export type GateDisposition = {
212
248
  * on the settlement (it is not the approver's refusal);
213
249
  * I4 `disposition.kind === "allowed"` ⇒ `settlement` absent or `human_allowed`;
214
250
  * I5 `disposition.cause` present ⇒ a member of {@link CLASSIFIER_DENY_CAUSES} beside a layer word
215
- * {@link DENIED_BY_MAY_CARRY_CAUSE} admits (the classifier station, or the inherited chain's `policy`).
251
+ * {@link DENIED_BY_MAY_CARRY_CAUSE} admits (the classifier station, or the inherited chain's `policy`);
252
+ * I6 `disposition.classifier` present ⇒ both model ids are non-empty strings, and a `fallback` on it
253
+ * names a `to` that IS `modelUsed` (the ladder landed where the record says) with a cause in
254
+ * {@link import("./auto-mode.js").AUTO_MODE_FALLBACK_CAUSES} — a record whose `to` and `modelUsed`
255
+ * disagree would let a reader answer "which model decided this call?" two ways.
216
256
  * Nothing here is read out of an ABSENCE: an ordinary allow with no ask is `{ disposition: { kind: "allowed" } }`,
217
257
  * a direct policy deny is `{ disposition: { kind: "denied", deniedBy: "policy" } }`, and neither carries a
218
258
  * settlement because neither settled one.