@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,4 +1,5 @@
1
1
  import { isAskOrigin } from "./ask-origin.js";
2
+ import { isAutoModeFallbackCause } from "./auto-mode.js";
2
3
  export const SETTLEMENT_KINDS = [
3
4
  "human_allowed",
4
5
  "human_refused",
@@ -17,6 +18,13 @@ const SETTLEMENT_KIND_SET = new Set(SETTLEMENT_KINDS);
17
18
  export function isSettlementKind(v) {
18
19
  return SETTLEMENT_KIND_SET.has(v);
19
20
  }
21
+ export function decidersNote(text) {
22
+ return text === "" ? undefined : text;
23
+ }
24
+ export function settledByPerson(kind, approver, note) {
25
+ const given = decidersNote(note);
26
+ return { kind, who: { party: "person", ...(approver !== undefined ? { approver } : {}) }, when: Date.now(), ...(given !== undefined ? { note: given } : {}) };
27
+ }
20
28
  export const SETTLEMENT_IS_REFUSAL = {
21
29
  human_allowed: false,
22
30
  human_refused: true,
@@ -89,5 +97,20 @@ export function screenGateOutcome(g) {
89
97
  else if (isDeniedBy(d.deniedBy) && !DENIED_BY_MAY_CARRY_CAUSE[d.deniedBy])
90
98
  defects.push(`I5: a classifier cause cannot stand beside deniedBy "${d.deniedBy}"`);
91
99
  }
100
+ const attribution = d.classifier;
101
+ if (attribution !== undefined) {
102
+ const named = (v) => typeof v === "string" && v !== "";
103
+ if (!named(attribution.modelRequested) || !named(attribution.modelUsed))
104
+ defects.push("I6: disposition.classifier must name both the model the round was for and the model that answered");
105
+ const fb = attribution.fallback;
106
+ if (fb !== undefined) {
107
+ if (!named(fb.from) || !named(fb.to))
108
+ defects.push("I6: disposition.classifier.fallback must name the rung it left and the rung it arrived at");
109
+ else if (fb.to !== attribution.modelUsed)
110
+ defects.push(`I6: the fallback landed on "${fb.to}" but the record names "${attribution.modelUsed}" as the model that answered`);
111
+ if (!isAutoModeFallbackCause(fb.cause))
112
+ defects.push(`I6: fallback cause "${String(fb.cause)}" is outside the closed set`);
113
+ }
114
+ }
92
115
  return defects;
93
116
  }
@@ -104,7 +104,7 @@ export type NoticeAudience = "user" | "operator";
104
104
  * src/ for notice mint shapes and names any code that is minted but unregistered, or registered but
105
105
  * no longer minted.
106
106
  */
107
- export declare const ENGINE_NOTICE_CODES: readonly ["config.autocompact_window_clamped", "config.env_timeout_discarded", "config.materialize_env_discarded", "config.models_swapped", "config.read_face_swapped", "config.read_face_deployment_clamped", "config.tool_model_gate_removed", "config.tool_model_gate_unknown_class", "config.tool_model_gate_env_invalid", "config.artifact_host_invalid", "config.tool_card_undeclared", "config.tool_face_undeclared", "config.tool_face_invalid", "config.durable_gate_unavailable", "config.peer_admission_out_of_range", "config.peer_lane_unmounted", "peer.inbound_disposition", "peer.held_settled", "peer.idle_subscription", "classifier.denial_limit", "checkpoint.execution_outcome_unrecorded", "delegation.transcript_integrity", "delegation.ask_unresolvable", "mcp.injection_dropped", "mcp.revocation_probe_failed", "workflow.governance_key_stripped", "workflow.agent_option_ignored", "memory.session_polluted", "memory.harvest_quarantined", "memory.delegation_static_mark_waived", "memory.content_class_declared", "memory.hold_opened", "memory.hold_released", "memory.hold_disposed", "memory.consolidation_recommended", "memory.consolidation_committed", "memory.consolidation_conflict", "memory.consolidation_incomplete", "memory.consolidation_refused", "memory.consolidation_withheld", "route.fallback_to_primary", "route.base_url_changed_key_unchanged", "task.user_steer_undrained", "task.user_followup_undrained", "steering.parked_input_blocked", "task.turn_interrupted", "task.halt_unconsumed", "task.late_approval", "task.interrupt_unconsumed", "memory.capture_opted_out", "memory.capture_optout_unpersisted", "tool_result.offload_put_failed"];
107
+ export declare const ENGINE_NOTICE_CODES: readonly ["config.autocompact_window_clamped", "config.env_timeout_discarded", "config.materialize_env_discarded", "config.models_swapped", "config.read_face_swapped", "config.read_face_deployment_clamped", "config.write_face_swapped", "config.write_face_deployment_clamped", "config.tool_model_gate_removed", "config.tool_model_gate_unknown_class", "config.tool_model_gate_env_invalid", "config.artifact_host_invalid", "config.tool_card_undeclared", "config.tool_face_undeclared", "config.tool_face_invalid", "config.durable_gate_unavailable", "config.peer_admission_out_of_range", "config.peer_lane_unmounted", "peer.inbound_disposition", "peer.held_settled", "peer.idle_subscription", "classifier.denial_limit", "classifier.fallback", "classifier.role_defaulted", "classifier.contract_broken", "checkpoint.execution_outcome_unrecorded", "delegation.transcript_integrity", "delegation.ask_unresolvable", "mcp.injection_dropped", "mcp.revocation_probe_failed", "workflow.governance_key_stripped", "workflow.agent_option_ignored", "memory.session_polluted", "memory.harvest_quarantined", "memory.delegation_static_mark_waived", "memory.content_class_declared", "memory.hold_opened", "memory.hold_released", "memory.hold_disposed", "memory.consolidation_recommended", "memory.consolidation_committed", "memory.consolidation_conflict", "memory.consolidation_incomplete", "memory.consolidation_refused", "memory.consolidation_withheld", "route.fallback_to_primary", "route.classifier_unroutable", "route.base_url_changed_key_unchanged", "task.user_steer_undrained", "task.user_followup_undrained", "steering.parked_input_blocked", "task.turn_interrupted", "task.halt_unconsumed", "task.late_approval", "task.interrupt_unconsumed", "memory.capture_opted_out", "memory.capture_optout_unpersisted", "skills.listing_truncated", "tool_result.offload_put_failed"];
108
108
  /** A code this engine mints (see {@link ENGINE_NOTICE_CODES}). NOT the type of
109
109
  * `EngineNotice.code`, which stays `string` — a host forwarding its own notices through the same
110
110
  * sink is a supported shape, and narrowing that field would break it. */
@@ -102,6 +102,8 @@ export const ENGINE_NOTICE_CODES = [
102
102
  "config.models_swapped",
103
103
  "config.read_face_swapped",
104
104
  "config.read_face_deployment_clamped",
105
+ "config.write_face_swapped",
106
+ "config.write_face_deployment_clamped",
105
107
  "config.tool_model_gate_removed",
106
108
  "config.tool_model_gate_unknown_class",
107
109
  "config.tool_model_gate_env_invalid",
@@ -116,6 +118,9 @@ export const ENGINE_NOTICE_CODES = [
116
118
  "peer.held_settled",
117
119
  "peer.idle_subscription",
118
120
  "classifier.denial_limit",
121
+ "classifier.fallback",
122
+ "classifier.role_defaulted",
123
+ "classifier.contract_broken",
119
124
  "checkpoint.execution_outcome_unrecorded",
120
125
  "delegation.transcript_integrity",
121
126
  "delegation.ask_unresolvable",
@@ -137,6 +142,7 @@ export const ENGINE_NOTICE_CODES = [
137
142
  "memory.consolidation_refused",
138
143
  "memory.consolidation_withheld",
139
144
  "route.fallback_to_primary",
145
+ "route.classifier_unroutable",
140
146
  "route.base_url_changed_key_unchanged",
141
147
  "task.user_steer_undrained",
142
148
  "task.user_followup_undrained",
@@ -147,6 +153,7 @@ export const ENGINE_NOTICE_CODES = [
147
153
  "task.interrupt_unconsumed",
148
154
  "memory.capture_opted_out",
149
155
  "memory.capture_optout_unpersisted",
156
+ "skills.listing_truncated",
150
157
  "tool_result.offload_put_failed",
151
158
  ];
152
159
  const NOTICE_AUDIENCE_TABLE = {
@@ -159,6 +166,9 @@ const NOTICE_AUDIENCE_TABLE = {
159
166
  "task.user_steer_undrained": "user",
160
167
  "task.user_followup_undrained": "user",
161
168
  "classifier.denial_limit": "user",
169
+ "classifier.fallback": "operator",
170
+ "classifier.role_defaulted": "operator",
171
+ "classifier.contract_broken": "operator",
162
172
  "task.turn_interrupted": "user",
163
173
  "steering.parked_input_blocked": "user",
164
174
  "task.halt_unconsumed": "user",
@@ -175,6 +185,8 @@ const NOTICE_AUDIENCE_TABLE = {
175
185
  "config.models_swapped": "operator",
176
186
  "config.read_face_swapped": "operator",
177
187
  "config.read_face_deployment_clamped": "operator",
188
+ "config.write_face_swapped": "operator",
189
+ "config.write_face_deployment_clamped": "operator",
178
190
  "config.tool_model_gate_removed": "operator",
179
191
  "config.tool_model_gate_unknown_class": "operator",
180
192
  "config.tool_model_gate_env_invalid": "operator",
@@ -200,7 +212,9 @@ const NOTICE_AUDIENCE_TABLE = {
200
212
  "memory.consolidation_incomplete": "operator",
201
213
  "memory.consolidation_refused": "operator",
202
214
  "route.fallback_to_primary": "operator",
215
+ "route.classifier_unroutable": "operator",
203
216
  "route.base_url_changed_key_unchanged": "operator",
217
+ "skills.listing_truncated": "operator",
204
218
  "tool_result.offload_put_failed": "operator",
205
219
  };
206
220
  export const NOTICE_AUDIENCE = NOTICE_AUDIENCE_TABLE;
@@ -0,0 +1,66 @@
1
+ /**
2
+ * The hook seam's SEAT discipline and its disclosure vocabulary: which callback seats exist, what a
3
+ * deployment is allowed to put in one, and how a wiring disclosure names the ones that are wired.
4
+ *
5
+ * Kept beside the interface rather than inside it: `hooks.ts` owns the seam's BEHAVIOUR, and this module owns
6
+ * the seat as a CONFIGURATION VALUE — the same split `wiring-manifest.ts` keeps for the ask/question seats.
7
+ */
8
+ import type { Hooks } from "./hooks.js";
9
+ import type { AssertAllKeysHandled } from "./ask-origin.js";
10
+ import type { SeamProvenance } from "./wiring-manifest.js";
11
+ /**
12
+ * The CALLBACK SEATS of {@link Hooks} as a word list — the vocabulary a wiring disclosure names a wired seam
13
+ * by (`WiringManifest.hooks[].family`), in declaration order. `preToolUseObservational` is deliberately NOT a
14
+ * member (it is a DECLARATION ABOUT `preToolUse`); `timeoutMs` is not a seat either.
15
+ */
16
+ export declare const HOOK_FAMILIES: readonly ["preToolUse", "postToolUse", "userPromptSubmit", "stop", "postToolUseFailure", "postToolBatch", "preCompact", "postCompact", "stopFailure", "permissionDenied"];
17
+ /**
18
+ * One hook seat's name.
19
+ *
20
+ * @contract hooks.family_vocabulary — {@link HOOK_FAMILIES} is the only spelling of a hook seat's name on any
21
+ * disclosure face, and {@link HookFamiliesCoverEverySeat} makes a new callback member of {@link Hooks} red at
22
+ * `tsc` until it has a word there — a disclosure that omits a seat under-states what runs on the critical path.
23
+ */
24
+ export type HookFamily = (typeof HOOK_FAMILIES)[number];
25
+ /** Every CALLBACK member of {@link Hooks} — derived from the interface, never a hand-kept second list. */
26
+ type HookCallbackSeat = {
27
+ [K in keyof Hooks]-?: NonNullable<Hooks[K]> extends (...args: never[]) => unknown ? K : never;
28
+ }[keyof Hooks];
29
+ /** The closed-set fence: `never` while every callback seat has a word, the MISSING SEAT'S NAME when one does
30
+ * not (the constraint form names it; a conditional type says only "something changed"). */
31
+ export type HookFamiliesCoverEverySeat = AssertAllKeysHandled<Exclude<HookCallbackSeat, HookFamily>>;
32
+ /** The typed refusal a hook seat holding a present NON-CALLBACK value raises at prepare. */
33
+ export declare const HOOKS_INVALID_CODE = "config.hooks_invalid";
34
+ /**
35
+ * Screen the resolved hook record's seats (`spec.hooks ?? deps.hooks`).
36
+ *
37
+ * @contract hooks.seat_value — a hook seat is a FUNCTION or it is absent. A PRESENT non-function
38
+ * (`preToolUse: null`, a string, a bare object) is a configuration error and refuses the whole prepare with
39
+ * {@link HOOKS_INVALID_CODE}, naming the family and the type received. It is not folded to "unwired": the tool
40
+ * gate arms on PRESENCE (`hooks?.preToolUse !== undefined`) and would invoke the non-function and fail closed
41
+ * on every call, while a callability-based disclosure reported no hooks at all. The sibling seams already
42
+ * refuse the same way (`resolveQuestionSeam` / `resolveElicitSeam`). `undefined` stays exactly what it was:
43
+ * an unwired seat, silent.
44
+ */
45
+ export declare function assertHookSeatsCallable(hooks: Hooks | undefined, provenance: SeamProvenance): void;
46
+ /**
47
+ * One WIRED HOOK SEAT of this leg. `family` is the seat's name; `owner` is WHICH SEAT supplied the record, the
48
+ * `spec > deps` provenance the ask/question seams report. Deliberately no third owner word: core mounts NO
49
+ * engine-owned hooks, and a delegated child's inherited `preToolUse` rides the gate's constraint fold.
50
+ */
51
+ export interface WiringManifestHookEntry {
52
+ family: HookFamily;
53
+ owner: SeamProvenance;
54
+ /** Only on the `preToolUse` entry, and only when the deployment DECLARED the face observational
55
+ * (`Hooks.preToolUseObservational`) — it still runs on every call but is not folded into a delegated
56
+ * child's inherited constraints. Absence means "not declared", never "declared false". */
57
+ observational?: true;
58
+ }
59
+ /**
60
+ * The wiring manifest's hook read face ({@link WiringManifestHookEntry}): one entry per CALLBACK seat the
61
+ * resolved record wires, in the vocabulary's declaration order. The VOCABULARY is the judge — families are
62
+ * walked, never the record's own keys. `owner` is a property of the RECORD, so every entry agrees. Callability
63
+ * and presence cannot diverge here: {@link assertHookSeatsCallable} has already refused the leg where they would.
64
+ */
65
+ export declare function wiredHookEntries(hooks: Hooks | undefined, owner: SeamProvenance, preToolUseObservational: boolean): WiringManifestHookEntry[];
66
+ export {};
@@ -0,0 +1,37 @@
1
+ export const HOOK_FAMILIES = [
2
+ "preToolUse",
3
+ "postToolUse",
4
+ "userPromptSubmit",
5
+ "stop",
6
+ "postToolUseFailure",
7
+ "postToolBatch",
8
+ "preCompact",
9
+ "postCompact",
10
+ "stopFailure",
11
+ "permissionDenied",
12
+ ];
13
+ export const HOOKS_INVALID_CODE = "config.hooks_invalid";
14
+ export function assertHookSeatsCallable(hooks, provenance) {
15
+ if (hooks === undefined)
16
+ return;
17
+ for (const family of HOOK_FAMILIES) {
18
+ const seat = hooks[family];
19
+ if (seat === undefined || typeof seat === "function")
20
+ continue;
21
+ const e = new Error(`the resolved hooks record (${provenance}) holds ${seat === null ? "null" : typeof seat} at \`${family}\` — a hook seat is a callback or it is absent. ` +
22
+ `A present non-callback is not an unwired seat: the tool gate arms on presence and would refuse every call through it. Omit the key, or wire a function.`);
23
+ e.code = HOOKS_INVALID_CODE;
24
+ throw e;
25
+ }
26
+ }
27
+ export function wiredHookEntries(hooks, owner, preToolUseObservational) {
28
+ if (hooks === undefined)
29
+ return [];
30
+ const entries = [];
31
+ for (const family of HOOK_FAMILIES) {
32
+ if (typeof hooks[family] !== "function")
33
+ continue;
34
+ entries.push({ family, owner, ...(family === "preToolUse" && preToolUseObservational ? { observational: true } : {}) });
35
+ }
36
+ return entries;
37
+ }
@@ -1354,7 +1354,7 @@ export interface ToolGateInput {
1354
1354
  * (the write-protection tighten below). Absent ⇒ the deployment replaced the table with `[]` (or
1355
1355
  * the caller runs the gate without one) and the decision path is byte-identical.
1356
1356
  */
1357
- writeProtectionCheck?: (toolName: string, args: unknown, face?: import("./tool-registry.js").ToolCallLike["face"]) => import("./write-protect.js").WriteProtectedHit | null;
1357
+ writeProtectionCheck?: (toolName: string, args: unknown, face?: import("./tool-registry.js").ToolCallLike["face"], cwd?: string) => import("./write-protect.js").WriteProtectedHit | null;
1358
1358
  /**
1359
1359
  * design/143 批2 (A, CC 2.1.207 auto mode): when present, a surviving `ask` is routed to the
1360
1360
  * small-model policy CLASSIFIER before any human/durable resolution:
@@ -0,0 +1,12 @@
1
+ /** The display bound on a grant candidate's directory: generous next to a real `PATH_MAX`, tight enough
2
+ * that the seat cannot become an unbounded prose channel onto an approval card. */
3
+ export declare const READ_ROOT_CANDIDATE_DIR_MAX = 1024;
4
+ /**
5
+ * The validator for a probe's {@link import("./tool-spec.js").ReversibilityVerdict.readRootCandidate}: the
6
+ * errand `normalizeProbeCause` does for the cause, fail-closed on the disclosure alone — a malformed value
7
+ * yields `undefined`, the ask stands, the card carries no offer. It REFUSES where the cause NEUTRALIZES,
8
+ * per `shell.read_boundary.grant_candidate_is_the_grant`: the cause is prose, so folding a separator out of
9
+ * it keeps it true, while this string IS the grant, and a card showing one spelling while the deployment
10
+ * applies another is the failure the seat prevents.
11
+ */
12
+ export declare function normalizeReadRootCandidate(value: unknown): import("./tool-spec.js").ReadRootGrantCandidate | undefined;
@@ -0,0 +1,24 @@
1
+ import { isPlainRecord, safeDataValue } from "./checkpoint-store.js";
2
+ import { hasWin32AliasedComponent, isAbsolutePathForm, normalizeAbsPathLexically, pathFamilyOf, win32NamespaceScreen } from "../tools/fs/safety.js";
3
+ export const READ_ROOT_CANDIDATE_DIR_MAX = 1024;
4
+ export function normalizeReadRootCandidate(value) {
5
+ if (!isPlainRecord(value))
6
+ return undefined;
7
+ if (safeDataValue(value, "clearsThisAsk") !== true)
8
+ return undefined;
9
+ const dir = safeDataValue(value, "dir");
10
+ if (typeof dir !== "string" || dir.length === 0 || dir.length > READ_ROOT_CANDIDATE_DIR_MAX)
11
+ return undefined;
12
+ if (/[\u0000-\u001f\u007f\u2028\u2029]/.test(dir))
13
+ return undefined;
14
+ if (!isAbsolutePathForm(dir))
15
+ return undefined;
16
+ const folded = normalizeAbsPathLexically(dir);
17
+ if (folded !== dir || normalizeAbsPathLexically(`${dir}/..`) === dir)
18
+ return undefined;
19
+ if (hasWin32AliasedComponent(dir))
20
+ return undefined;
21
+ if (!win32NamespaceScreen(dir, pathFamilyOf({ root: dir })).ok)
22
+ return undefined;
23
+ return { dir, clearsThisAsk: true };
24
+ }
@@ -1,6 +1,6 @@
1
1
  import type { ThinkingLevel } from "../internal/harness-types.js";
2
2
  import type { Model } from "../internal/llm.js";
3
- import type { ModelRef, ModelRole, ModelRoles } from "./types.js";
3
+ import type { ModelRef, ModelRole, ModelRoles, RoleSpec } from "./types.js";
4
4
  export type { ModelRole, RoleSpec, ModelRoles } from "./types.js";
5
5
  /** Resolve a string ModelRef against the catalog (a Model object passes through). */
6
6
  export declare function resolveModel(ref: ModelRef, models?: Record<string, Model>): Model;
@@ -87,6 +87,14 @@ export declare function parseModelMention(text: string, allowedNames: Iterable<s
87
87
  * (即使 catalog 恰有裸模型名 `pro`/`flash` 也不入档位路由——INERT 靠配置成立,不靠键名运气)。
88
88
  */
89
89
  export declare const ROLE_TIER_DEFAULTS: Readonly<Partial<Record<ModelRole, string>>>;
90
+ /**
91
+ * Whether a RoleSpec NAMES a model — a string ref, a Model object, or a config object carrying `model` or
92
+ * `select` — as opposed to a preset-only object (`{ thinking }`, `{ systemPrompt }`) that shapes the role's
93
+ * request but leaves its model to the defaults. The classifier seat's own default (the tier below the run's
94
+ * main model, `classifier-ladder.ts`) applies exactly when this is false: a `{select}` keeps its selection, a
95
+ * preset-only role does not disable the relative pick. One reader, the same parts {@link specParts} reads.
96
+ */
97
+ export declare function roleSpecNamesModel(spec: RoleSpec | undefined): boolean;
90
98
  export interface ResolvedRole {
91
99
  model: Model;
92
100
  /** The effective thinking level: explicit `spec.thinking` wins, else the role's default. */
@@ -130,6 +130,10 @@ function specParts(s) {
130
130
  const cfg = s;
131
131
  return { model: cfg.model, select: cfg.select, thinking: cfg.thinking, systemPrompt: cfg.systemPrompt };
132
132
  }
133
+ export function roleSpecNamesModel(spec) {
134
+ const parts = specParts(spec);
135
+ return parts.model !== undefined || parts.select !== undefined;
136
+ }
133
137
  function toConfig(s) {
134
138
  if (s == null) {
135
139
  return undefined;
@@ -21,9 +21,14 @@
21
21
  * no marker and an absent directory narrows.
22
22
  *
23
23
  * Reverse check (the seat's own invariant): nothing enters `roots` except through `admit`, and every
24
- * caller of `admit` is an advertiser deciding its advertisement off the returned verdict — so there is
25
- * no directory the fence opens that the prompt does not advertise, and none the prompt advertises
26
- * that the fence refuses.
24
+ * caller of `admit` is an advertiser deciding its advertisement off the returned verdict — so no
25
+ * directory reaches the ROOTS FENCE through this seat without the prompt advertising it, and none
26
+ * the prompt advertises is refused by that fence.
27
+ *
28
+ * The invariant is about this seat, not about the whole write surface: under an open write face
29
+ * ({@link import("../../tools/fs/containment-face.js").ContainmentFace}) the roots fence judges
30
+ * nothing, so what the prompt advertises is a floor rather than the whole list — the `# Environment`
31
+ * block says so in its own line, and the advertisement stays true (these directories ARE writable).
27
32
  */
28
33
  import type { ExecutionEnv } from "../../internal/harness.js";
29
34
  export interface AdvertisedWritableDir {
@@ -153,11 +153,12 @@ export interface ResultFlags {
153
153
  * distinct codes from arriving as one anonymous `limits.max_turns_exceeded`. Empty/absent ⇒ the field is omitted. */
154
154
  remoteEnvFailures?: TaskResult["remoteEnvFailures"];
155
155
  /** The run's effective read posture (resolved face + normalized deny
156
- * additions), echoed on `TaskResult.effectiveReadFace` / `.effectiveReadDenyPatterns`. Pure
156
+ * additions), echoed on `TaskResult.effectiveReadFace` / `.effectiveWriteFace` / `.effectiveReadDenyPatterns`. Pure
157
157
  * pass-through (assembly neither adds nor filters) and INDEPENDENT of the status/errorCode chain:
158
158
  * the posture is a fact about the leg that ran, whatever terminal it reached — a post-completion
159
159
  * spawner folds it stricter-wins into follow-on legs; every other consumer may ignore it. */
160
160
  effectiveReadFace?: TaskResult["effectiveReadFace"];
161
+ effectiveWriteFace?: TaskResult["effectiveWriteFace"];
161
162
  effectiveReadDenyPatterns?: TaskResult["effectiveReadDenyPatterns"];
162
163
  /** The memory-visibility observation, echoed on
163
164
  * `TaskResult.effectiveMemoryScopes`. Pure pass-through on every terminal, same law as the
@@ -166,5 +166,5 @@ export function assembleResult(spec, sessionId, final, stats, flags) {
166
166
  if (flags.unpricedSpend)
167
167
  delete publicStats.costMicroUsd;
168
168
  const stampHaltedByUser = haltConsumed(flags.haltConsumption);
169
- return { taskId, ...(flags.runId !== undefined ? { runId: flags.runId } : {}), sessionId, terminal, ...(flags.model !== undefined ? { model: flags.model } : {}), result: result.trim(), salvagedOutput, ...(apiFailure !== undefined ? { apiFailure } : {}), ...(retryAfterMs !== undefined ? { retryAfterMs } : {}), ...(flags.rewindNotes !== undefined && flags.rewindNotes.length > 0 ? { rewindNotes: flags.rewindNotes } : {}), ...(flags.editedFiles !== undefined && flags.editedFiles.length > 0 ? { editedFiles: flags.editedFiles } : {}), ...(flags.haltedOnUserRejection === true ? { haltedOnUserRejection: true } : {}), ...(stampHaltedByUser ? { haltedByUser: true } : {}), ...(flags.remoteEnvFailures !== undefined && flags.remoteEnvFailures.length > 0 ? { remoteEnvFailures: [...flags.remoteEnvFailures] } : {}), ...(flags.strandedHumanAnswers !== undefined && flags.strandedHumanAnswers.length > 0 ? { strandedHumanAnswers: flags.strandedHumanAnswers } : {}), ...(flags.effectiveReadFace !== undefined ? { effectiveReadFace: flags.effectiveReadFace } : {}), ...(flags.effectiveReadDenyPatterns !== undefined && flags.effectiveReadDenyPatterns.length > 0 ? { effectiveReadDenyPatterns: flags.effectiveReadDenyPatterns } : {}), ...(flags.effectiveMemoryScopes !== undefined ? { effectiveMemoryScopes: flags.effectiveMemoryScopes } : {}), ...(flags.effectiveReasoning !== undefined ? { effectiveReasoning: flags.effectiveReasoning } : {}), stats: publicStats };
169
+ return { taskId, ...(flags.runId !== undefined ? { runId: flags.runId } : {}), sessionId, terminal, ...(flags.model !== undefined ? { model: flags.model } : {}), result: result.trim(), salvagedOutput, ...(apiFailure !== undefined ? { apiFailure } : {}), ...(retryAfterMs !== undefined ? { retryAfterMs } : {}), ...(flags.rewindNotes !== undefined && flags.rewindNotes.length > 0 ? { rewindNotes: flags.rewindNotes } : {}), ...(flags.editedFiles !== undefined && flags.editedFiles.length > 0 ? { editedFiles: flags.editedFiles } : {}), ...(flags.haltedOnUserRejection === true ? { haltedOnUserRejection: true } : {}), ...(stampHaltedByUser ? { haltedByUser: true } : {}), ...(flags.remoteEnvFailures !== undefined && flags.remoteEnvFailures.length > 0 ? { remoteEnvFailures: [...flags.remoteEnvFailures] } : {}), ...(flags.strandedHumanAnswers !== undefined && flags.strandedHumanAnswers.length > 0 ? { strandedHumanAnswers: flags.strandedHumanAnswers } : {}), ...(flags.effectiveReadFace !== undefined ? { effectiveReadFace: flags.effectiveReadFace } : {}), ...(flags.effectiveWriteFace !== undefined ? { effectiveWriteFace: flags.effectiveWriteFace } : {}), ...(flags.effectiveReadDenyPatterns !== undefined && flags.effectiveReadDenyPatterns.length > 0 ? { effectiveReadDenyPatterns: flags.effectiveReadDenyPatterns } : {}), ...(flags.effectiveMemoryScopes !== undefined ? { effectiveMemoryScopes: flags.effectiveMemoryScopes } : {}), ...(flags.effectiveReasoning !== undefined ? { effectiveReasoning: flags.effectiveReasoning } : {}), stats: publicStats };
170
170
  }
@@ -0,0 +1,227 @@
1
+ /**
2
+ * The auto-mode classifier's CANDIDATE LADDER, as one module: which models a classification may be asked on
3
+ * (and which model stands in when no `classifier` role names one), its per-rung reasoning seat and the
4
+ * per-attempt user-message tail, how a rung's reply is read for a re-ask or a failure, and the once-per-run
5
+ * disclosures its first move and its broken contract mint.
6
+ *
7
+ * Beside `derived-route-fallback.ts` and in the same family — both are seats the caps phase's classifier
8
+ * arming reaches DOWN for, and both answer "this leg's model did not work out" one layer apart. Which is why
9
+ * the rung comparison here borrows the route adjudicator's own identity law instead of inventing one.
10
+ *
11
+ * THE TWO LAYERS, since one sentence ("the classifier seat did not work out ⇒ use the run's main model") is
12
+ * spelled in two places. They are ordered, and each has its own trigger and its own record face:
13
+ *
14
+ * ① PRE-FLIGHT — `derivedRouteFallsBack`, BEFORE any call. Trigger: the classifier model's route differs
15
+ * from the primary's AND the brain's own route judge finds the credential/URL pairing broken. Effect:
16
+ * the arming SWAPS the seat, so the ladder below is built from the main model and rung 0 IS the main
17
+ * model. Record: the once-per-run operator notice `route.fallback_to_primary` (`detail.{seat, from, to,
18
+ * cause}`) — and only that. `GateDisposition.classifier.modelRequested` reads the MAIN model's id,
19
+ * because the ladder it describes never carried the configured seat; the gate record answers "which
20
+ * model was asked", and "what did the deployment configure, and was it swapped" is the notice's
21
+ * question. `classifierFallback` governs THIS layer too: under `"none"` the pre-flight does not swap —
22
+ * it keeps the declared seat and announces `route.classifier_unroutable`, so the deployment that said
23
+ * "deny rather than fall back" gets the denial (the seat's calls are refused at the request gate)
24
+ * instead of a silent fallback the knob had just declined.
25
+ * ② THE LADDER — here, AFTER a call. Trigger: the rung produced no RULING. Effect: the next rung is asked.
26
+ * Record: `GateDisposition.classifier.fallback` on every classification this run's own gate decided (an
27
+ * inherited-ancestor chain carries the form and no attribution) + the throttled `classifier.fallback`.
28
+ *
29
+ * They FOLD rather than stack: after a ① SWAP the seat and the main model are the same route, so
30
+ * {@link classifierLadderRungs} returns ONE rung and ② cannot fire in the same run (and under `"none"`, where
31
+ * ① does not swap, the ladder is one rung by the knob). The split's cost is the
32
+ * `modelRequested` reading above; the unification (the pre-flight as rung 0 under its own cause word, one
33
+ * record face for both) changes the notice set and is a ticket, not a repair.
34
+ */
35
+ import type { AssistantMessage, Model } from "../../internal/llm.js";
36
+ import { type AutoModeClassified } from "../auto-mode.js";
37
+ import type { RunnerDeps } from "../types.js";
38
+ /**
39
+ * The rungs a classification walks, in order: the classifier seat, and — under `classifierFallback: "main"` (the
40
+ * default) — the run's main model behind it. The knob is screened by `autoModeClassifierFallbackOf`, here and
41
+ * (idempotently) at the caps phase that reads the same word for the pre-flight's posture, so a value outside
42
+ * the closed set refuses the prepare loudly (`config.auto_mode_classifier_fallback_invalid`) instead of
43
+ * reading as the default.
44
+ *
45
+ * ONE RUNG when the two resolve to the SAME ROUTE. Route identity, not `Model.id`: two catalog entries may
46
+ * legitimately share a wire id while pointing at different endpoints (the route adjudicator's own law, and
47
+ * the split its pre-flight exists for) — an id-only comparison would collapse a healthy main endpoint and a
48
+ * broken classifier endpoint into one rung, turning the ladder off for the deployment that needs it most.
49
+ * The collapse where it does apply is deliberate: re-asking the SAME model is a retry, which is what CC's
50
+ * `unavailableOuterRetries` does and what this ladder is not. A catalog value the identity law cannot
51
+ * normalize (a non-string `baseUrl` — it throws) reads as TWO rungs: being wrong that way costs one extra
52
+ * call on a broken catalog, the other way turns the feature off invisibly, and the bad value still earns its
53
+ * loud refusal at the brain's request gate.
54
+ */
55
+ export declare function classifierLadderRungs(knob: unknown, classifier: Model, main: Model): readonly Model[];
56
+ /**
57
+ * WHY the classifier seat is the model it is when no `classifier` role names one — the closed set behind the
58
+ * `classifier.role_defaulted` notice's `reason`. `tier_below_main` = the nearest directly-bound tier below the
59
+ * run's main model; `tier_table` = the static `flash` row (a main bound to no tier); `role_fallback` = the
60
+ * `summarize → default` chain (no tiers at all); `main_only` = the main model is the only model there is (a
61
+ * single-model catalog, or nothing bound below the main's tier); `role_unresolvable` = a `classifier` role
62
+ * NAMED a model the catalog cannot resolve (a stale or misspelled reference), so the main model stands in —
63
+ * the pre-#805 leg fell back the same way, silently.
64
+ */
65
+ export declare const CLASSIFIER_ROLE_DEFAULT_REASONS: readonly ["tier_below_main", "tier_table", "role_fallback", "main_only", "role_unresolvable"];
66
+ export type ClassifierRoleDefaultReason = (typeof CLASSIFIER_ROLE_DEFAULT_REASONS)[number];
67
+ /** What {@link classifierSeatModel} returns: the model, and — when no role NAMED it — why it is that one. */
68
+ export type ClassifierSeat = {
69
+ readonly model: Model;
70
+ readonly defaulted?: ClassifierRoleDefaultReason;
71
+ };
72
+ /**
73
+ * The model the classifier SEAT resolves to for this run — ONE resolver for "which model classifies":
74
+ * · a `classifier` role that NAMES a model (`roleSpecNamesModel`: a string ref, a Model object, `{model}`,
75
+ * `{select}`) ⇒ the role resolver's answer, exactly as before (a preset-only role — `{thinking}` alone —
76
+ * does not name one and takes the default below);
77
+ * · else, with tier bindings declared (`deps.tiers`) and a main model on a tier ⇒ the nearest tier strictly
78
+ * BELOW the main's whose route differs from the main's — `tier_below_main`; the main's tier is the LOWEST
79
+ * tier its route sits on (a route bound as both `max` and `flash` is a `flash` main: deterministic, and
80
+ * the cheaper reading); nothing below ⇒ the main itself, `main_only`. Read off the expanded catalog's
81
+ * tier keys, the Runner's own resolution of a tier word: a key synthesised for an unbound tier falls DOWN
82
+ * to the next bound tier, which is the model this walk reaches anyway, and a catalog key spelled like a
83
+ * tier shadows the binding here as it does everywhere. No thinking preference: whether a model's
84
+ * thinking can be turned off is the SEAT's business ({@link classifierRungSeat}), not routing's;
85
+ * · else the role resolver's own fallback (the static tier row `classifier → flash` when tiers are active,
86
+ * `tier_table`; the `summarize → default` chain otherwise, `role_fallback`); a catalog with no roles at
87
+ * all ⇒ the main model, `main_only`.
88
+ * The relative pick follows the run's MAIN model on purpose: a side channel that resolved without it is the
89
+ * misfit the ticket names (a `flash` classifier above a `lite` main; two tiers below a `max` main).
90
+ */
91
+ export declare function classifierSeatModel(args: {
92
+ deps: Pick<RunnerDeps, "models" | "roles" | "tiers">;
93
+ main: Model;
94
+ }): ClassifierSeat;
95
+ /**
96
+ * The `classifier.role_defaulted` line — minted ONCE per run by the arming, AFTER the route pre-flight, so it
97
+ * names the seat actually asked (a pre-flight swap to the main model is its own line,
98
+ * `route.fallback_to_primary`, same run). Silent when a role named the model: nothing was defaulted.
99
+ */
100
+ export declare function announceClassifierRoleDefaulted(args: {
101
+ onNotice: RunnerDeps["onNotice"];
102
+ seat: ClassifierSeat;
103
+ sessionId?: string;
104
+ runId?: string;
105
+ }): void;
106
+ /**
107
+ * The classifier's REASONING SEAT, read off the model a classification is actually asked on — ONE predicate:
108
+ * does the wire SPELL off ({@link thinkingOffSpelled})? Yes ⇒ `off` + the verdict-sized cap (the request pays
109
+ * for the answer alone); no ⇒ `low` + the verdict cap + the low allowance (a model whose thinking the wire
110
+ * cannot turn off is asked for the least it can grade, in the room that costs — CC 2.1.259 `MU`: spell off,
111
+ * or leave thinking to the provider and pay 2048). A model that declares no reasoning at all is on the `low`
112
+ * seat too: its wire carries nothing either way, so the request is the gateway's default, and a default-on
113
+ * gateway thinks — the arm that used to read it as "never thinks" is the one the field report was about.
114
+ * PER RUNG, not once per arming: the second rung is a different model with its own wire.
115
+ *
116
+ * The THINK seat is the RE-ASK seat ({@link classifierReplyAsksAgain}): a first attempt that produced no
117
+ * verdict is asked once more with thinking allowed the room of CC's stage-2 classifier (8192) — an unknown
118
+ * model that answered NOTHING at its cap has just told us it thinks past it, and is re-asked as one that does.
119
+ */
120
+ export declare const CLASSIFIER_OFF_SEAT: {
121
+ reasoning: "off";
122
+ maxTokens: number;
123
+ };
124
+ export declare const CLASSIFIER_LOW_SEAT: {
125
+ reasoning: "low";
126
+ maxTokens: number;
127
+ };
128
+ export declare const CLASSIFIER_THINK_SEAT: {
129
+ reasoning: "low";
130
+ maxTokens: number;
131
+ };
132
+ export type ClassifierSeatRequest = typeof CLASSIFIER_OFF_SEAT | typeof CLASSIFIER_LOW_SEAT | typeof CLASSIFIER_THINK_SEAT;
133
+ /**
134
+ * The seat for one rung's FIRST attempt — see the seat table above.
135
+ *
136
+ * @contract classifier.budget_is_room — every classifier output cap is ROOM the request asks for, never a
137
+ * bound the engine can vouch for: a gateway may exclude reasoning tokens from `max_tokens` (a vLLM-fronted
138
+ * Qwen gateway answered a cap of 128 with 938 output tokens and `stop`), so an over-long think is cut by the
139
+ * round-trip timeout (`unavailable` / `timeout`), not by the cap (`parse_error` on an empty reply), and the
140
+ * two are attested as the two different facts they are.
141
+ */
142
+ export declare function classifierRungSeat(model: Model): typeof CLASSIFIER_OFF_SEAT | typeof CLASSIFIER_LOW_SEAT;
143
+ /**
144
+ * The user-message TAIL of each attempt — CC 2.1.259's classifier suffixes, verbatim: the FIRST attempt
145
+ * carries the fast-mode suffix (`$ns`: the answer must begin with `<block>`, no preamble), the RE-ASK carries
146
+ * the thinking-stage suffix (`Uns`: think in `<thinking>` first, then answer). The base prompt (2.1.250 `KRg`)
147
+ * already says "no preamble" once; upstream repeats it at the END of the user message, and it is the repeat
148
+ * that lands — live 2026-09-15, a Qwen 3.8 with thinking off went from 70% tagged replies (1.2–3.9k chars of
149
+ * "Looking at this action…" prose) to 90% at 8 output tokens with the first suffix, and the re-ask suffix
150
+ * had it write its reasoning inside `<thinking>` (stripped by the reader) before the tag.
151
+ */
152
+ export declare const CLASSIFIER_ATTEMPT_SUFFIX: {
153
+ readonly first: "\nErr on the side of blocking. Your ENTIRE response MUST begin with <block>. Do NOT output any analysis, reasoning, or commentary before <block>. No \"Looking at...\" or similar preamble.";
154
+ readonly reask: "\nReview the classification process and follow it carefully, making sure you deny actions that should be blocked. As a reminder, explicit (not suggestive or implicit) user confirmation is required to override blocks. Use <thinking> before responding with <block>. Think longer on ambiguous or borderline actions; keep reasoning brief for clear-cut ones.";
155
+ };
156
+ /** The classifier reply's answer text — every text block joined; thinking blocks are not the verdict. */
157
+ export declare function classifierReplyText(reply: AssistantMessage): string;
158
+ /**
159
+ * CC 2.1.259 `$Tt`: an EMPTY reply that stopped at the output cap is "unparseable" — a reply to ask for again,
160
+ * not a refusal and not a verdict. The engine's brains report that cut two ways: `stopReason:"length"` (an
161
+ * engine-shrunk cap) or `stopReason:"error"` with `errorKind:"length_empty"` (a static cap — the classifier's
162
+ * own). An empty reply with any OTHER stop is CC's `policy_refusal` arm: not re-asked.
163
+ */
164
+ export declare function classifierReplyEmptyAtCap(reply: AssistantMessage): boolean;
165
+ /**
166
+ * A reply that may answer in the TAG form only: its text is not a complete answer of its own. Output cut by the cap
167
+ * (`stopReason: "length"`), a dropped connection or a stalled stream (`partialFinalized`, with a synthesised
168
+ * `stop`), and a reply that CALLED A TOOL (`stopReason: "toolUse"`, or a tool-call block in the content — the
169
+ * classifier is given no tools, and the openai brain reports a length cut with a surviving call as `toolUse`):
170
+ * whatever text arrived is a prefix or a fragment — a `No` on its way to `No, this must be blocked` — so it may
171
+ * carry a verdict only through a `<block>` tag, which reads a `<block>no` prefix as CC does; the bare-answer arm
172
+ * never sees it.
173
+ */
174
+ export declare function classifierReplyTagOnly(reply: AssistantMessage): boolean;
175
+ /**
176
+ * The text the DECIDER reads for its verdict: the reply's answer text, except that a tag-only reply
177
+ * ({@link classifierReplyTagOnly}) whose text carries NO tagged answer reads as EMPTY — a cut prefix must not
178
+ * reach the bare-answer arm as if it were the whole reply (adversarial passes 2–3: a connection cut after `No`,
179
+ * and a scratch `<block>` mention before a cut `No`, executed the gated tool). Judged by the reader itself with
180
+ * its bare arm closed, so "no tagged verdict" and "no verdict" cannot drift. Only the MISSING form is emptied:
181
+ * a tag-only reply that carries a CONFLICT keeps its text, so the decider records the conflict and the leg
182
+ * does not re-ask it (adversarial pass 4: emptying every parse failure had turned a cut conflict into a
183
+ * missing reply, and the re-ask into an allow). The decider then records `parse_error` / `missing` for the
184
+ * emptied text, the same word an empty reply gets.
185
+ */
186
+ export declare function classifierReplyVerdictText(reply: AssistantMessage): string;
187
+ /**
188
+ * Whether a first attempt is asked AGAIN on the THINK seat — ONE rule: the reply is MISSING a verdict, either
189
+ * empty at the cap ({@link classifierReplyEmptyAtCap}), cut or fragmentary without a tagged verdict
190
+ * ({@link classifierReplyTagOnly}), or text in which the reader finds no answer at all (`parse_error` of form
191
+ * `missing`: prose, a thinking-only reply). Not a CONFLICT (two `<block>` answers — it carries a `yes`, and a second attempt that answered `no`
192
+ * would erase it: the fail-closed `parse_error` it is stands) and not an empty reply with a non-cap stop
193
+ * (CC's refusal arm). CC 2.1.259 `Vrr` re-asks an unparseable reply up to `maxRetries` times; sema re-asks
194
+ * ONCE, on the stage-2 room, so a rung costs at most two calls. A failed reply ({@link classifierReplyFailure})
195
+ * is judged before this and never reaches it.
196
+ */
197
+ export declare function classifierReplyAsksAgain(reply: AssistantMessage): boolean;
198
+ /**
199
+ * A reply that is a LEG FAILURE rather than an answer — the brains RETURN a provider error as a message
200
+ * (`stopReason: "error"` with the provider's text on `errorMessage`: an HTTP 4xx/5xx, a quota refusal, an
201
+ * overloaded gateway) and a cancelled request as `"aborted"`; neither is a reply the reader may judge. Read as
202
+ * text they were `parse_error` — a card saying "the classifier's reply carried no verdict" about a model that
203
+ * never answered, a ladder record blaming `parse_error` instead of `error`. The leg THROWS what this returns
204
+ * and the decider maps the throw to `unavailable` (`error`), as it maps every leg failure; the sibling
205
+ * side-channel legs (compaction, consolidation) already read an error stop this way. The cap-cut form
206
+ * (`errorKind: "length_empty"`) is not a failure: it is the empty-at-cap reply the re-ask owns.
207
+ */
208
+ export declare function classifierReplyFailure(reply: AssistantMessage): Error | undefined;
209
+ /**
210
+ * The per-decision READ of a classification, as one seat: the `auto_mode.classified` trace frame (wall time,
211
+ * verdict, cause), the ladder's throttled `classifier.fallback` disclosure and the per-model
212
+ * `classifier.contract_broken` streak. All are observers — a throwing sink never reaches the gate (the decider
213
+ * swallows it), and none changes a verdict.
214
+ *
215
+ * The frame's `model` is the model the classification RAN ON, which is what the field has always meant (its
216
+ * declaration reads "the resolved classifier role, or the main model it fell back to"): the route
217
+ * pre-flight's swap and the ladder's walk are two ways to reach that same sentence. `seatModelId` is the
218
+ * fallback for a classification that carries no attribution at all.
219
+ */
220
+ export declare function classifierRoundObserver(args: {
221
+ tracer: RunnerDeps["tracer"];
222
+ onNotice: RunnerDeps["onNotice"];
223
+ taskId: string;
224
+ seatModelId: string;
225
+ sessionId?: string;
226
+ runId?: string;
227
+ }): (info: AutoModeClassified) => void;