@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
@@ -999,6 +999,7 @@ export function createSendMessageTool(opts) {
999
999
  currentClamps: {
1000
1000
  ...(ctx.handsReadOnly === true ? { handsReadOnly: true } : {}),
1001
1001
  ...(ctx.readFace === "roots" ? { readFaceRoots: true } : {}),
1002
+ ...(ctx.writeFace === "roots" ? { writeFaceRoots: true } : {}),
1002
1003
  ...(ctx.readDenyPatterns !== undefined ? { readDenyPatterns: ctx.readDenyPatterns } : {}),
1003
1004
  ...(ctx.interactiveTools === false ? { interactiveTools: false } : {}),
1004
1005
  ...(ctx.oneShot === true ? { oneShot: true } : {}),
@@ -468,6 +468,7 @@ export declare function createSubagentResume(deps: {
468
468
  oneShot?: true;
469
469
  memoryPersistenceCapable?: false;
470
470
  readFaceRoots?: true;
471
+ writeFaceRoots?: true;
471
472
  readDenyPatterns?: readonly import("../tools/fs/read-deny.js").ReadDenyEntry[];
472
473
  };
473
474
  /** The RESUMING caller's own handback-review seat (its trusted `ToolExecuteContext.autoModeReview`),
@@ -558,6 +558,7 @@ export function createSubagentResume(deps) {
558
558
  ...(deps.currentOnQuestion !== undefined ? { onQuestion: deps.currentOnQuestion } : {}),
559
559
  ...(entry.specSnapshot.handsReadOnly === true || deps.currentClamps?.handsReadOnly === true ? { handsReadOnly: true } : {}),
560
560
  ...(entry.specSnapshot.readFace === "roots" || deps.currentClamps?.readFaceRoots === true ? { readFace: "roots" } : {}),
561
+ ...(entry.specSnapshot.writeFace === "roots" || deps.currentClamps?.writeFaceRoots === true ? { writeFace: "roots" } : {}),
561
562
  ...(entry.specSnapshot.readDenyPatterns !== undefined || deps.currentClamps?.readDenyPatterns !== undefined
562
563
  ? { readDenyPatterns: [...(entry.specSnapshot.readDenyPatterns ?? []), ...(deps.currentClamps?.readDenyPatterns ?? [])] }
563
564
  : {}),
@@ -1903,6 +1904,9 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
1903
1904
  ...(ctx.alwaysLoadTools !== undefined ? { alwaysLoadTools: [...ctx.alwaysLoadTools] } : {}),
1904
1905
  ...(ctx.restoreGatedTools !== undefined ? { restoreGatedTools: ctx.restoreGatedTools === true ? true : [...ctx.restoreGatedTools] } : {}),
1905
1906
  ...(ctx.promptProfile !== undefined ? { promptProfile: ctx.promptProfile } : {}),
1907
+ ...(ctx.readFace === "roots" ? { readFace: "roots" } : {}),
1908
+ ...(ctx.writeFace === "roots" ? { writeFace: "roots" } : {}),
1909
+ ...(ctx.readDenyPatterns !== undefined ? { readDenyPatterns: [...ctx.readDenyPatterns] } : {}),
1906
1910
  enableBlockedReport: false,
1907
1911
  limits: {
1908
1912
  maxTurns: typeof observerDef.maxTurns === "number" && Number.isFinite(observerDef.maxTurns) && observerDef.maxTurns > 0 ? observerDef.maxTurns : 8,
@@ -2159,6 +2163,7 @@ function createSubagentToolNode(opts, depth, excluded, extraToolsBudget) {
2159
2163
  ...(ctx.interactiveTools === false ? { interactiveTools: false } : {}),
2160
2164
  ...(ctx.handsReadOnly === true ? { handsReadOnly: true } : {}),
2161
2165
  ...(ctx.readFace === "roots" ? { readFace: "roots" } : {}),
2166
+ ...(ctx.writeFace === "roots" ? { writeFace: "roots" } : {}),
2162
2167
  ...(ctx.readDenyPatterns !== undefined ? { readDenyPatterns: [...ctx.readDenyPatterns] } : {}),
2163
2168
  ...(ctx.excludeTools !== undefined ? { excludeTools: [...ctx.excludeTools] } : {}),
2164
2169
  ...(ctx.deferTools !== undefined ? { deferTools: [...ctx.deferTools] } : {}),
@@ -257,6 +257,7 @@ async function runTeacherCore(runner, studentSpec, teacher) {
257
257
  ...(studentSpec.promptProfile !== undefined ? { promptProfile: studentSpec.promptProfile } : {}),
258
258
  ...(studentSpec.handsReadOnly === true ? { handsReadOnly: true } : {}),
259
259
  ...(studentSpec.readFace === "roots" ? { readFace: "roots" } : {}),
260
+ ...(studentSpec.writeFace === "roots" ? { writeFace: "roots" } : {}),
260
261
  ...(studentSpec.readDenyPatterns !== undefined ? { readDenyPatterns: [...studentSpec.readDenyPatterns] } : {}),
261
262
  ...(studentSpec.interactiveTools === false ? { interactiveTools: false } : {}),
262
263
  ...(studentSpec.excludeTools !== undefined
@@ -109,6 +109,7 @@ export async function verifyCompleted(runner, result, specBase, objective, confi
109
109
  const notifier = createSafeNotifier(config.onNotifyError !== undefined ? { onError: config.onNotifyError } : undefined);
110
110
  const verifierTools = config.verifierTools ?? (specBase.tools ?? []).filter((t) => t.effect === "read");
111
111
  const foldedRootsFace = specBase.readFace === "roots" || result.effectiveReadFace === "roots";
112
+ const foldedRootsWriteFace = specBase.writeFace === "roots" || result.effectiveWriteFace === "roots";
112
113
  const foldedReadDeny = [...(specBase.readDenyPatterns ?? []), ...(result.effectiveReadDenyPatterns ?? [])];
113
114
  const carryFrozenPosture = (r) => {
114
115
  const face = result.effectiveReadFace === "roots" ? "roots" : r.effectiveReadFace;
@@ -143,6 +144,7 @@ export async function verifyCompleted(runner, result, specBase, objective, confi
143
144
  tools: verifierTools,
144
145
  handsReadOnly: config.verifierHandsReadOnly ?? true,
145
146
  ...(foldedRootsFace ? { readFace: "roots" } : {}),
147
+ ...(foldedRootsWriteFace ? { writeFace: "roots" } : {}),
146
148
  ...(foldedReadDeny.length > 0 ? { readDenyPatterns: [...foldedReadDeny] } : {}),
147
149
  interactiveTools: false,
148
150
  outputSchema: VerdictSchema,
@@ -244,6 +246,7 @@ export async function verifyCompleted(runner, result, specBase, objective, confi
244
246
  sessionId: current.sessionId,
245
247
  objective: fixObjective(outcome.findings),
246
248
  ...(foldedRootsFace ? { readFace: "roots" } : {}),
249
+ ...(foldedRootsWriteFace ? { writeFace: "roots" } : {}),
247
250
  ...(foldedReadDeny.length > 0 ? { readDenyPatterns: [...foldedReadDeny] } : {}),
248
251
  }, legInternals());
249
252
  spend += (current.stats.costMicroUsd ?? 0) + (current.stats.nested?.costMicroUsd ?? 0);
@@ -2,7 +2,7 @@ import {} from "../internal/llm.js";
2
2
  import { BrainError } from "./errors.js";
3
3
  import { startsWithInputTooLongAnchor } from "./input-too-long.js";
4
4
  import { DEGENERATE_MESSAGE, trimDegenerateTail } from "./repetition.js";
5
- import { createRepetitionPoll, parseStreamedToolArgs } from "./stream-shared.js";
5
+ import { STREAM_TORN_WARNING, createRepetitionPoll, parseStreamedToolArgs } from "./stream-shared.js";
6
6
  import { mintFallbackToolCallId } from "./tool-call-id.js";
7
7
  import { emitBrainTelemetry, reportReasoningWireFacts } from "./status-sink.js";
8
8
  import { errorResultMediaNote, IMAGE_OMITTED_NO_VISION, modelSupportsVision, sendableImages } from "./media-degrade.js";
@@ -560,7 +560,7 @@ export function createAnthropicBrain(config = {}) {
560
560
  }
561
561
  handleEvent(evt);
562
562
  },
563
- finalize() {
563
+ finalize(opts) {
564
564
  const finalContent = [];
565
565
  const toolCalls = [];
566
566
  const malformed = [];
@@ -666,6 +666,8 @@ export function createAnthropicBrain(config = {}) {
666
666
  emptyNoFinish ||
667
667
  (toolError !== undefined && noUsableContent) ||
668
668
  (truncatedEmpty && !dynamicCut && !staticReasoningCut);
669
+ const tornWithContent = !errored && opts?.truncated !== true && stopReason == null && !noUsableContent && !degenerate;
670
+ const errorMessageOut = tornWithContent ? (errorMessage ? `${errorMessage}\n${STREAM_TORN_WARNING}` : STREAM_TORN_WARNING) : errorMessage;
669
671
  const errorKind = degenerate
670
672
  ? "degenerate"
671
673
  : !safetyCut && contextWindowExceeded
@@ -680,8 +682,9 @@ export function createAnthropicBrain(config = {}) {
680
682
  usage: computeUsage(model, usageRaw),
681
683
  ...(Object.keys(usageRaw).length === 0 ? { usageMissing: true } : {}),
682
684
  timestamp: Date.now(),
683
- ...(errorMessage ? { errorMessage } : {}),
685
+ ...(errorMessageOut ? { errorMessage: errorMessageOut } : {}),
684
686
  ...(errorKind ? { errorKind } : {}),
687
+ ...(tornWithContent ? { partialFinalized: true } : {}),
685
688
  ...(rep.cut !== undefined || rep.spared.length > 0
686
689
  ? { repetition: { ...(rep.cut !== undefined ? { cut: rep.cut } : {}), ...(rep.spared.length > 0 ? { spared: rep.spared } : {}) } }
687
690
  : {}),
@@ -1,7 +1,7 @@
1
1
  import {} from "../internal/llm.js";
2
2
  import { BrainError } from "./errors.js";
3
3
  import { DEGENERATE_MESSAGE, trimDegenerateTail } from "./repetition.js";
4
- import { createRepetitionPoll, parseStreamedToolArgs } from "./stream-shared.js";
4
+ import { STREAM_TORN_WARNING, createRepetitionPoll, parseStreamedToolArgs } from "./stream-shared.js";
5
5
  import { mintFallbackToolCallId } from "./tool-call-id.js";
6
6
  import { emitBrainTelemetry } from "./status-sink.js";
7
7
  import { errorResultMediaNote, IMAGE_OMITTED_NO_VISION, imagesOmittedNoVisionNote, modelSupportsVision, sendableImages } from "./media-degrade.js";
@@ -195,6 +195,7 @@ function responsesErrorCode(code) {
195
195
  }
196
196
  const INCOMPLETE_MAX_OUTPUT_TOKENS = "max_output_tokens";
197
197
  const INCOMPLETE_CONTENT_FILTER = "content_filter";
198
+ const RESPONSE_INCOMPLETE_WARNING = 'The response above may be incomplete: the endpoint reported status "incomplete" with no reason this client recognises.';
198
199
  function closeFunctionCall(acc) {
199
200
  if (acc.closedTc)
200
201
  return acc.closedTc;
@@ -688,7 +689,8 @@ export function createOpenResponsesBrain(config = {}) {
688
689
  const staticReasoningCut = truncatedEmpty && !dynamicCut && reasoningSeen && options?.staticReasoningCutDowngrade === true;
689
690
  const safetyCut = incompleteReason === INCOMPLETE_CONTENT_FILTER;
690
691
  const refused = accumRefusal.trim().length > 0;
691
- const emptyNoFinish = noUsableContent && terminalStatus === undefined && !degenerate;
692
+ const incompleteUnmapped = terminalStatus === "incomplete" && incompleteReason !== INCOMPLETE_MAX_OUTPUT_TOKENS && incompleteReason !== INCOMPLETE_CONTENT_FILTER;
693
+ const emptyNoFinish = noUsableContent && (terminalStatus === undefined || incompleteUnmapped) && !degenerate;
692
694
  const maxTokensNote = sentMaxTokens !== undefined
693
695
  ? `effective max_output_tokens=${sentMaxTokens} (from ${sentMaxTokensLane === "options"
694
696
  ? "options.maxTokens"
@@ -708,7 +710,7 @@ export function createOpenResponsesBrain(config = {}) {
708
710
  ? `[length_empty] response truncated at max_output_tokens (${maxTokensNote}) with no answer text — the model spent the whole budget on reasoning; raise the cap, or lower the reasoning effort for this task.`
709
711
  : `[length_empty] response truncated at max_output_tokens (${maxTokensNote}) with no answer text — raise the cap`
710
712
  : emptyNoFinish
711
- ? `[stream_torn] model stream ended with no content and no terminal response event — the response was lost (a torn stream or an in-band provider error frame)${malformedFrames > 0 ? ` — ${malformedFrames} unparseable data frame(s) were dropped on this stream (first sample: ${JSON.stringify(malformedSample)})` : ""}`
713
+ ? `[stream_torn] model stream ended with no content and ${terminalStatus === undefined ? "no terminal response event" : `a terminal status "${terminalStatus}" with no reason this client recognises (incomplete_details.reason=${incompleteReason ?? "absent"})`} — the response was lost (a torn stream or an in-band provider error frame)${malformedFrames > 0 ? ` — ${malformedFrames} unparseable data frame(s) were dropped on this stream (first sample: ${JSON.stringify(malformedSample)})` : ""}`
712
714
  : undefined));
713
715
  const errored = degenerate ||
714
716
  safetyCut ||
@@ -716,6 +718,9 @@ export function createOpenResponsesBrain(config = {}) {
716
718
  emptyNoFinish ||
717
719
  (toolError !== undefined && noUsableContent) ||
718
720
  (truncatedEmpty && !dynamicCut && !staticReasoningCut);
721
+ const tornWithContent = !errored && opts?.truncated !== true && (terminalStatus === undefined || incompleteUnmapped) && !noUsableContent && !degenerate;
722
+ const partialWarning = terminalStatus === undefined ? STREAM_TORN_WARNING : RESPONSE_INCOMPLETE_WARNING;
723
+ const errorMessageOut = tornWithContent ? (errorMessage ? `${errorMessage}\n${partialWarning}` : partialWarning) : errorMessage;
719
724
  const errorKind = degenerate
720
725
  ? "degenerate"
721
726
  : !safetyCut && !refused && toolError === undefined && truncatedEmpty && !dynamicCut && !staticReasoningCut
@@ -732,8 +737,9 @@ export function createOpenResponsesBrain(config = {}) {
732
737
  ...(usageRaw === undefined ? { usageMissing: true } : {}),
733
738
  timestamp: Date.now(),
734
739
  ...(responseId !== undefined ? { responseId } : {}),
735
- ...(errorMessage ? { errorMessage } : {}),
740
+ ...(errorMessageOut ? { errorMessage: errorMessageOut } : {}),
736
741
  ...(errorKind ? { errorKind } : {}),
742
+ ...(tornWithContent ? { partialFinalized: true } : {}),
737
743
  ...(rep.cut !== undefined || rep.spared.length > 0
738
744
  ? { repetition: { ...(rep.cut !== undefined ? { cut: rep.cut } : {}), ...(rep.spared.length > 0 ? { spared: rep.spared } : {}) } }
739
745
  : {}),
@@ -1,7 +1,7 @@
1
1
  import {} from "../internal/llm.js";
2
2
  import { repairTextToolCalls } from "./tool-call-repair.js";
3
3
  import { DEGENERATE_MESSAGE, trimDegenerateTail } from "./repetition.js";
4
- import { createRepetitionPoll, parseStreamedToolArgs } from "./stream-shared.js";
4
+ import { STREAM_TORN_WARNING, createRepetitionPoll, parseStreamedToolArgs } from "./stream-shared.js";
5
5
  import { mintFallbackToolCallId } from "./tool-call-id.js";
6
6
  import { emitBrainTelemetry } from "./status-sink.js";
7
7
  import { errorResultMediaNote, IMAGE_OMITTED_NO_VISION, imagesOmittedNoVisionNote, modelSupportsVision, sendableImages } from "./media-degrade.js";
@@ -619,6 +619,8 @@ export function createOpenAIBrain(config = {}) {
619
619
  emptyNoFinish ||
620
620
  (toolError !== undefined && noUsableContent) ||
621
621
  (truncatedEmpty && !dynamicCut);
622
+ const tornWithContent = !errored && opts?.truncated !== true && finishReason == null && !noUsableContent && !degenerate;
623
+ const errorMessageOut = tornWithContent ? (errorMessage ? `${errorMessage}\n${STREAM_TORN_WARNING}` : STREAM_TORN_WARNING) : errorMessage;
622
624
  const errorKind = degenerate
623
625
  ? "degenerate"
624
626
  : !safetyCut && !refused && toolError === undefined && truncatedEmpty && !dynamicCut
@@ -631,8 +633,9 @@ export function createOpenAIBrain(config = {}) {
631
633
  usage: computeUsage(model, usageRaw),
632
634
  ...(usageRaw === undefined ? { usageMissing: true } : {}),
633
635
  timestamp: Date.now(),
634
- ...(errorMessage ? { errorMessage } : {}),
636
+ ...(errorMessageOut ? { errorMessage: errorMessageOut } : {}),
635
637
  ...(errorKind ? { errorKind } : {}),
638
+ ...(tornWithContent ? { partialFinalized: true } : {}),
636
639
  ...(rep.cut !== undefined || rep.spared.length > 0
637
640
  ? { repetition: { ...(rep.cut !== undefined ? { cut: rep.cut } : {}), ...(rep.spared.length > 0 ? { spared: rep.spared } : {}) } }
638
641
  : {}),
@@ -117,25 +117,25 @@ export interface OffCapabilityModel {
117
117
  * "none" tier), on an endpoint that takes an effort value at all (`supportsReasoningEffort` not
118
118
  * false) — else `undefined`: there is nothing to send. The effort formats have no disable key, and an
119
119
  * effort VALUE is never off (it enables reasoning at that tier), so off on those wires is spelled only
120
- * when the deployment says how. The two appliers and {@link thinkingOffExpressible} read this ONE
120
+ * when the deployment says how. The two appliers and {@link thinkingOffSpelled} read this ONE
121
121
  * function, so what the wire carries and what the capability table claims cannot disagree.
122
122
  */
123
123
  export declare function declaredOffSpelling(model: OffCapabilityModel): string | undefined;
124
124
  /**
125
- * Whether this model's wire can SAY "off". A model that does not reason at all (`reasoning` falsy)
126
- * never thinks, so off is trivially expressible. Otherwise, per wire: the anthropic budget form spells
127
- * `{type:"disabled"}` (the ADAPTIVE form rejects it — those models think on every request); deepseek
128
- * `{type:"disabled"}`, the binary enable keys at `false`, openrouter/together `reasoning:{enabled:false}`
129
- * all say it; the effort-only wires (stock openai completions, the Responses lane) say it only through
130
- * a deployment-declared off spelling ({@link declaredOffSpelling}).
125
+ * Whether the wire will CARRY a disable key for this model — off is SPELLED, never assumed. Per wire: the
126
+ * anthropic budget form spells `{type:"disabled"}` (the ADAPTIVE form rejects it — those models think on
127
+ * every request); deepseek `{type:"disabled"}`, the binary enable keys at `false`, openrouter/together
128
+ * `reasoning:{enabled:false}` all spell it; the effort-only wires (stock openai completions, the Responses
129
+ * lane) spell it only through a deployment-declared off spelling ({@link declaredOffSpelling}).
131
130
  *
132
- * A caller that sizes an output cap for an ANSWER alone (the auto-mode classifier's block-verdict
133
- * contract) applies the cap only when this is true: a model that will think regardless cannot have its
134
- * cap derived from the answer's contract, and keeps its own budget. What this table cannot see is a
135
- * model that IGNORES its wire's disable key (a thinking-only build served through a format that has
136
- * one) — that is a per-model fact the compat declaration does not yet carry.
131
+ * A model whose `reasoning` is FALSY spells NOTHING: every applier returns before writing any key for it, so
132
+ * the request is the gateway's own default — and a default-on gateway thinks regardless (three of four live
133
+ * endpoints, 2026-09-15). This arm used to answer `true` ("a non-reasoning model never thinks"): the engine
134
+ * may size a request by what it SENT, never by what it hopes a gateway does when told nothing. What this
135
+ * table cannot see is a gateway that IGNORES the key it was sent — the classifier's re-ask on a no-verdict
136
+ * reply is the seat that learns that from the reply.
137
137
  */
138
- export declare function thinkingOffExpressible(model: OffCapabilityModel): boolean;
138
+ export declare function thinkingOffSpelled(model: OffCapabilityModel): boolean;
139
139
  /**
140
140
  * OPTIONAL per-request facts for {@link resolveReasoning} — what the wire's budget arm knows at
141
141
  * request build that a per-leg eager resolution cannot: the resolved output cap and whether it is a
@@ -30,9 +30,9 @@ export function declaredOffSpelling(model) {
30
30
  const mapped = model.thinkingLevelMap?.off;
31
31
  return typeof mapped === "string" && mapped !== "" ? mapped : undefined;
32
32
  }
33
- export function thinkingOffExpressible(model) {
33
+ export function thinkingOffSpelled(model) {
34
34
  if (!model.reasoning)
35
- return true;
35
+ return false;
36
36
  const compat = (model.compat ?? {});
37
37
  if (model.api === "anthropic-messages")
38
38
  return compat.thinkingMode !== "adaptive";
@@ -197,6 +197,24 @@ export declare function fallbackToPrimaryNotice(args: {
197
197
  sessionId?: string;
198
198
  runId?: string;
199
199
  }): EngineNotice;
200
+ /**
201
+ * The TWIN of {@link fallbackToPrimaryNotice}, for a seat whose deployment declared NO fallback
202
+ * (`RunnerDeps.autoMode.classifierFallback: "none"`): the pairing is broken, nothing was swapped, and the
203
+ * seat's own calls are refused at the request gate — which is what that declaration asked for. Same `detail`
204
+ * keys as its twin MINUS `to`, because nothing moved and a `to` naming a model that was not used is the one
205
+ * lie a consumer cannot detect; `action` is the discriminator, so the posture is read rather than inferred
206
+ * from an absence. `primary` is named in the SENTENCE (what was declined) and not in the detail.
207
+ */
208
+ export declare function unroutableSeatNotice(args: {
209
+ seat: string;
210
+ from: string;
211
+ primary: string;
212
+ verdict: Extract<RouteAdjudication, {
213
+ ok: false;
214
+ }>;
215
+ sessionId?: string;
216
+ runId?: string;
217
+ }): EngineNotice;
200
218
  /** The auth carriers of a header bag, canonicalized (case-fold name → value, sorted) — the
201
219
  * Model-visible credential fingerprint used by catalog-change disclosure (a same-name entry whose
202
220
  * URL moved while this fingerprint did not is the "address changed, credential reference did not"
@@ -184,6 +184,22 @@ export function fallbackToPrimaryNotice(args) {
184
184
  },
185
185
  };
186
186
  }
187
+ export function unroutableSeatNotice(args) {
188
+ return {
189
+ code: "route.classifier_unroutable",
190
+ message: `derived model "${args.from}" (${args.seat}) is not routable (${args.verdict.code}) and this deployment declared no fallback — ` +
191
+ `the seat is kept and its calls are refused rather than served by "${args.primary}". ${args.verdict.detail.fixHint}`,
192
+ detail: {
193
+ seat: args.seat,
194
+ from: args.from,
195
+ cause: args.verdict.code,
196
+ fixHint: args.verdict.detail.fixHint,
197
+ action: "refused",
198
+ ...(args.sessionId !== undefined ? { sessionId: args.sessionId } : {}),
199
+ ...(args.runId !== undefined ? { runId: args.runId } : {}),
200
+ },
201
+ };
202
+ }
187
203
  export function authCarrierFingerprint(headers) {
188
204
  if (!headers)
189
205
  return "";
@@ -1,4 +1,5 @@
1
1
  import { createAssistantMessageEventStream, } from "../internal/llm.js";
2
+ import { STREAM_TORN_WARNING } from "./stream-shared.js";
2
3
  import { FLOOR_OUTPUT_TOKENS, parseContextOverflow, planOutputCapAdjustment } from "./context-overflow.js";
3
4
  import { BrainError, classifyConnectFailure, classifyHttp, describeNetworkError, namesTheFailure, readProviderRequestId } from "./errors.js";
4
5
  import { classifyInputTooLong } from "./input-too-long.js";
@@ -650,7 +651,7 @@ export function runStreamingBrain(args) {
650
651
  midstreamWarning =
651
652
  failure.kind === "stalled"
652
653
  ? "Response stalled mid-stream. The response above may be incomplete."
653
- : "Connection closed mid-response. The response above may be incomplete.";
654
+ : STREAM_TORN_WARNING;
654
655
  parser.finalize({ truncated: true });
655
656
  return;
656
657
  }
@@ -24,6 +24,9 @@ import { type RepetitionEvent } from "./repetition.js";
24
24
  * means "do not emit a `toolcall_end` for this call"; callers must NOT cache that outcome — finalize retries
25
25
  * the parse against the complete accumulation.
26
26
  */
27
+ /** The CC-verbatim warning stamped beside `partialFinalized` when a stream ended before its close — by the
28
+ * mid-stream promotion (the body threw) and by the parsers' own finalize (content arrived, no finish frame). */
29
+ export declare const STREAM_TORN_WARNING = "Connection closed mid-response. The response above may be incomplete.";
27
30
  export declare function parseStreamedToolArgs(json: string): Record<string, unknown> | undefined;
28
31
  /** Repetition telemetry for ONE stream — the cut event plus the allowance-spared hits. */
29
32
  export interface RepetitionPoll {
@@ -1,4 +1,5 @@
1
1
  import { inspectDegenerate } from "./repetition.js";
2
+ export const STREAM_TORN_WARNING = "Connection closed mid-response. The response above may be incomplete.";
2
3
  export function parseStreamedToolArgs(json) {
3
4
  if (!json)
4
5
  return {};
@@ -169,4 +169,14 @@ export interface SkillSpec {
169
169
  path: string;
170
170
  content: string;
171
171
  }>;
172
+ /**
173
+ * The skill's own DIRECTORY on the machine the tools run on, so the model can reach its bundled material
174
+ * by path instead of searching the workspace. Core does not resolve it, read it, or check that it exists
175
+ * (the brain-outside posture {@link files} states). Core does exactly two textual things with it: the
176
+ * result opens with `Base directory for this skill: <baseDir>`, and both root placeholder spellings
177
+ * (`${SKILL_ROOT}`, `${CLAUDE_PLUGIN_ROOT}`) are substituted for it in the body and in every attachment's
178
+ * content, with `files[].path` then read RELATIVE to it. Absent ⇒ no line, no substitution, a
179
+ * byte-identical result; a placeholder with no directory stays verbatim.
180
+ */
181
+ baseDir?: string;
172
182
  }
@@ -1,3 +1,11 @@
1
+ /**
2
+ * The Artifacts hosting seam: the deployment-injected `ArtifactHost` (the `RunnerDeps.artifactHost` seat),
3
+ * its capability declaration, the call context the engine mints for every host call, the request /
4
+ * outcome shapes, and the closed word sets the tool face is assembled from. A host declares WHAT it
5
+ * supports (`capabilities()`); the engine assembles the `Artifact` tool from that declaration alone and
6
+ * never inspects the implementation. `localArtifactStub` (src/tools/artifact/local-stub.ts) is the first
7
+ * implementation of this interface; a server's hosting plane is the second.
8
+ */
1
9
  import type { AssertAllKeysHandled } from "./ask-origin.js";
2
10
  /** The model-facing action words. `publish` is the default action and the one every host must declare. */
3
11
  export declare const ARTIFACT_ACTIONS: readonly ["publish", "list", "read", "list_files", "read_file"];
@@ -6,6 +14,17 @@ export type ArtifactAction = (typeof ARTIFACT_ACTIONS)[number];
6
14
  * `"hosted"` = a hosting plane that mints its own URLs. Declared by the host, never detected. */
7
15
  export declare const ARTIFACT_PLANES: readonly ["hosted", "local-stub"];
8
16
  export type ArtifactPlane = (typeof ARTIFACT_PLANES)[number];
17
+ /** How a throw out of `publish` relates to the host's commit point. `"throw_means_uncommitted"`: every fallible
18
+ * step precedes the commit, so a throw proves nothing was committed; `"uncertain"`: a throw may follow it. */
19
+ export declare const ARTIFACT_COMMIT_CERTAINTIES: readonly ["throw_means_uncommitted", "uncertain"];
20
+ export type ArtifactCommitCertainty = (typeof ARTIFACT_COMMIT_CERTAINTIES)[number];
21
+ /** The `artifact.host_error` outcome word the tool reports when the host THREW, by declaration. An aborted or
22
+ * malformed answer is `"unknown"` under every declaration: the throw guarantee says nothing about those arms. */
23
+ export declare const ARTIFACT_COMMIT_THROW_OUTCOME: {
24
+ readonly throw_means_uncommitted: "not_executed";
25
+ readonly uncertain: "unknown";
26
+ };
27
+ export type ThrowOutcomeTableCoversEveryCommitCertainty = AssertAllKeysHandled<Exclude<ArtifactCommitCertainty, keyof typeof ARTIFACT_COMMIT_THROW_OUTCOME>>;
9
28
  /** The `publishContext` word a publish carries (default `"plain"`). */
10
29
  export declare const ARTIFACT_PUBLISH_CONTEXTS: readonly ["pr_review", "workshop", "whiteboard", "prototype", "plain"];
11
30
  export type ArtifactPublishContext = (typeof ARTIFACT_PUBLISH_CONTEXTS)[number];
@@ -31,6 +50,16 @@ export declare const ARTIFACT_ACTION_HOST_METHOD: {
31
50
  readonly read_file: "readFile";
32
51
  };
33
52
  export type HostMethodTableCoversEveryAction = AssertAllKeysHandled<Exclude<ArtifactAction, keyof typeof ARTIFACT_ACTION_HOST_METHOD>>;
53
+ /** Whether a call of the action may run inside a parallel batch: the read family is (host reads, no host write); a
54
+ * publish is not (it writes the host and the tool's path-binding state). Read by the tool's `isConcurrencySafe`. */
55
+ export declare const ARTIFACT_ACTION_CONCURRENCY_SAFE: {
56
+ readonly publish: false;
57
+ readonly list: true;
58
+ readonly read: true;
59
+ readonly list_files: true;
60
+ readonly read_file: true;
61
+ };
62
+ export type ConcurrencyTableCoversEveryAction = AssertAllKeysHandled<Exclude<ArtifactAction, keyof typeof ARTIFACT_ACTION_CONCURRENCY_SAFE>>;
34
63
  /** The typed failure words a host outcome may carry (`ok: false`). */
35
64
  export declare const ARTIFACT_HOST_FAILURE_CODES: readonly ["conflict", "url_foreign", "forbidden", "payload_too_large", "not_found", "force_refused", "refused"];
36
65
  export type ArtifactHostFailureCode = (typeof ARTIFACT_HOST_FAILURE_CODES)[number];
@@ -56,11 +85,16 @@ export declare const ARTIFACT_CARD = "artifact";
56
85
  * containing `"publish"`; `plane: "local-stub"` ⇒ `actions` is exactly `["publish"]`;
57
86
  * `defaultVisibility` is `"private"`; both byte limits are positive finite integers; `multiFile` is a
58
87
  * boolean; every declared action's host method exists; `plane: "local-stub"` ⇒ `storageDir` is a non-empty
59
- * absolute path.
88
+ * fully qualified path under its own spelling family (`C:\\…` / UNC / `/…` — read from the spelling, never from
89
+ * `process.platform`); `commit` is a word of {@link ARTIFACT_COMMIT_CERTAINTIES} (no default — an undeclared
90
+ * commit protocol is an invalid declaration, never read as either word).
60
91
  */
61
92
  export interface ArtifactHostCapabilities {
62
93
  plane: ArtifactPlane;
63
94
  actions: ReadonlyArray<ArtifactAction>;
95
+ /** What a throw out of `publish` proves about the commit (see {@link ARTIFACT_COMMIT_CERTAINTIES}); the tool words
96
+ * its host-fault result from this and from nothing else about the host. */
97
+ commit: ArtifactCommitCertainty;
64
98
  /** The on-disk store of a `"local-stub"` plane (required there; ignored on `"hosted"`). Contract: the store
65
99
  * must lie OUTSIDE every root the run may write or read (the task root, `additionalDirectories`,
66
100
  * `additionalReadDirectories`) — a store the model can reach through the fs tools is not a trust boundary;
@@ -77,9 +111,12 @@ export interface ArtifactHostCapabilities {
77
111
  * Engine-minted call attribution and idempotency keys — closure-supplied, never a tool parameter.
78
112
  * `taskId` / `sessionId` / `agentName` are audit attribution, never an authorization subject: a host
79
113
  * instance is bound to one principal at construction. `runId` and `attemptId` are engine-minted trusted
80
- * keys: `runId` is the same value `onMount` received; `attemptId` is one opaque value per tool call,
81
- * stable across any host retry of that call, so a host MUST replay the first outcome for a repeated
82
- * `attemptId` (a fresh publish never mints a second page, an update never advances twice).
114
+ * keys: `runId` is the same value `onMount` received; `attemptId` is one opaque value per HOST CALL — the
115
+ * engine mints a fresh one for every call it makes, so two calls of a single tool invocation (the probing
116
+ * `read` that establishes ownership and the `list_files` it gates, say) never arrive under one key — and it
117
+ * is stable across any retry the HOST makes of that call, so a host MUST replay the first outcome for a
118
+ * repeated `attemptId` (a fresh publish never mints a second page, an update never advances twice). The rule
119
+ * is keyed on the value alone: a host may hold one attempt ledger for all its methods.
83
120
  */
84
121
  export interface ArtifactCallContext {
85
122
  taskId: string;
@@ -1,6 +1,11 @@
1
- import { isAbsolute } from "node:path";
1
+ import { isAbsoluteForFamily, pathFamilyOf } from "../tools/fs/safety.js";
2
2
  export const ARTIFACT_ACTIONS = ["publish", "list", "read", "list_files", "read_file"];
3
3
  export const ARTIFACT_PLANES = ["hosted", "local-stub"];
4
+ export const ARTIFACT_COMMIT_CERTAINTIES = ["throw_means_uncommitted", "uncertain"];
5
+ export const ARTIFACT_COMMIT_THROW_OUTCOME = {
6
+ throw_means_uncommitted: "not_executed",
7
+ uncertain: "unknown",
8
+ };
4
9
  export const ARTIFACT_PUBLISH_CONTEXTS = ["pr_review", "workshop", "whiteboard", "prototype", "plain"];
5
10
  export const ARTIFACT_LIST_SCOPES = ["mine", "shared", "all"];
6
11
  export const ARTIFACT_PLANE_CONTENT_ORIGIN = {
@@ -14,6 +19,13 @@ export const ARTIFACT_ACTION_HOST_METHOD = {
14
19
  list_files: "listFiles",
15
20
  read_file: "readFile",
16
21
  };
22
+ export const ARTIFACT_ACTION_CONCURRENCY_SAFE = {
23
+ publish: false,
24
+ list: true,
25
+ read: true,
26
+ list_files: true,
27
+ read_file: true,
28
+ };
17
29
  export const ARTIFACT_HOST_FAILURE_CODES = ["conflict", "url_foreign", "forbidden", "payload_too_large", "not_found", "force_refused", "refused"];
18
30
  export const ARTIFACT_FAILURE_RESULT_CODE = {
19
31
  conflict: "artifact.publish_conflict",
@@ -59,6 +71,9 @@ export function readArtifactHostCapabilities(host) {
59
71
  return { ok: false, problem: 'capabilities.actions must include "publish"' };
60
72
  if (plane === "local-stub" && (actions.length !== 1 || actions[0] !== "publish"))
61
73
  return { ok: false, problem: 'a "local-stub" plane must declare exactly ["publish"]' };
74
+ const commit = c["commit"];
75
+ if (!wordOf(ARTIFACT_COMMIT_CERTAINTIES, commit))
76
+ return { ok: false, problem: `capabilities.commit must be one of ${ARTIFACT_COMMIT_CERTAINTIES.join(" | ")}` };
62
77
  if (c["defaultVisibility"] !== "private")
63
78
  return { ok: false, problem: 'capabilities.defaultVisibility must be "private"' };
64
79
  if (!isPositiveInt(c["maxFileBytes"]))
@@ -68,7 +83,7 @@ export function readArtifactHostCapabilities(host) {
68
83
  if (typeof c["multiFile"] !== "boolean")
69
84
  return { ok: false, problem: "capabilities.multiFile must be a boolean" };
70
85
  const storageDir = c["storageDir"];
71
- if (plane === "local-stub" && (typeof storageDir !== "string" || storageDir.length === 0 || !isAbsolute(storageDir)))
86
+ if (plane === "local-stub" && (typeof storageDir !== "string" || storageDir.length === 0 || !isAbsoluteForFamily(pathFamilyOf({ root: storageDir }), storageDir)))
72
87
  return { ok: false, problem: 'a "local-stub" plane must declare its absolute storageDir' };
73
88
  if (storageDir !== undefined && typeof storageDir !== "string")
74
89
  return { ok: false, problem: "capabilities.storageDir must be a string when present" };
@@ -82,6 +97,7 @@ export function readArtifactHostCapabilities(host) {
82
97
  capabilities: Object.freeze({
83
98
  plane,
84
99
  actions: Object.freeze([...actions]),
100
+ commit,
85
101
  defaultVisibility: "private",
86
102
  maxFileBytes: c["maxFileBytes"],
87
103
  maxTotalBytes: c["maxTotalBytes"],
@@ -133,6 +133,22 @@ export interface AskOriginFacts {
133
133
  export declare function askOriginOf(decision: Extract<PermissionResult, {
134
134
  action: "ask";
135
135
  }>, facts: AskOriginFacts): AskOrigin;
136
+ /**
137
+ * Stamp the ask's origin word — and withdraw the disclosures that speak for the TIGHTEN alone when some
138
+ * layer above it is the reason this call is being asked about. Today that is one member: the read
139
+ * boundary's directory offer, whose claim holds only while the tighten is the whole reason. Every higher
140
+ * rung is a layer with a reason of its own — a standing `ask` row, an unreadable rule store, an org
141
+ * verdict, a hook, the denial-limit fallback — which no directory grant answers, so the ask would stand
142
+ * after the grant and the offer would have promised a clearing it cannot deliver (measured: `cat
143
+ * <out-of-root>` under a standing `Bash(cat:*)` ask rule offered the directory and kept asking after it).
144
+ * It lives at the one derivation rather than the three stamp sites: the withdrawal's input IS the origin
145
+ * word, so a site that stamped without it would be a second, quieter answer to one question.
146
+ */
147
+ export declare function stampAskOrigin<T extends Extract<PermissionResult, {
148
+ action: "ask";
149
+ }>>(decision: T, facts: AskOriginFacts): T & {
150
+ origin: AskOrigin;
151
+ };
136
152
  /**
137
153
  * Which origins IMPLY the `requiresRealApproval` bit, and WHERE that implication is verified — the
138
154
  * consistency table {@link askOriginOf} reads. An ask of a must-carry origin exists so that no automatic
@@ -15,6 +15,14 @@ export function askOriginOf(decision, facts) {
15
15
  }
16
16
  return origin;
17
17
  }
18
+ const TIGHTEN_ONLY_ORIGINS = new Set(["shell_gate_tighten", "safety_tighten"]);
19
+ export function stampAskOrigin(decision, facts) {
20
+ const origin = askOriginOf(decision, facts);
21
+ if (decision.probeReadRootCandidate === undefined || TIGHTEN_ONLY_ORIGINS.has(origin))
22
+ return { ...decision, origin };
23
+ const { probeReadRootCandidate: _withdrawn, ...kept } = decision;
24
+ return { ...kept, origin };
25
+ }
18
26
  function originLadder(decision, facts) {
19
27
  if (facts.contentQuestion)
20
28
  return "content_question";
@@ -180,5 +180,11 @@ export type AutoModeArmingFold = {
180
180
  * nevertheless declare the same epoch — that is a deployment telling us its settings did not move
181
181
  * while its own data says otherwise, and picking either side would be a guess about which one is the
182
182
  * policy in force.
183
+ *
184
+ * @contract auto_mode.arming.absent_timeout_reads_recorded_default — a parked arming's absent `timeoutMs` is
185
+ * read as the default in force when it was recorded (`AUTO_MODE_RECORDED_ABSENT_TIMEOUT_MS`, 15_000 ms), not
186
+ * the default in force when it is redeemed: persisted data is read under the contract it was written under,
187
+ * so a default that moves can never widen an ancestor's bound at redemption; every recipe minted since
188
+ * materialises the knob, so absence dates the recording.
183
189
  */
184
190
  export declare function foldAutoModeArming(recorded: unknown, current: unknown): AutoModeArmingFold;
@@ -1,4 +1,4 @@
1
- import { AUTO_MODE_DEFAULT_TIMEOUT_MS, AUTO_MODE_DEFAULT_WINDOW_MAX_CHARS, AUTO_MODE_DEFAULT_WINDOW_MAX_ENTRIES, AUTO_MODE_DEFAULTS_SENTINEL, AUTO_MODE_DENIAL_AUTO_DENY_DEFAULT_MS, AUTO_MODE_DENIAL_LIMIT_DEFAULTS, } from "./auto-mode-defaults.js";
1
+ import { AUTO_MODE_DEFAULT_TIMEOUT_MS, AUTO_MODE_RECORDED_ABSENT_TIMEOUT_MS, AUTO_MODE_DEFAULT_WINDOW_MAX_CHARS, AUTO_MODE_DEFAULT_WINDOW_MAX_ENTRIES, AUTO_MODE_DEFAULTS_SENTINEL, AUTO_MODE_DENIAL_AUTO_DENY_DEFAULT_MS, AUTO_MODE_DENIAL_LIMIT_DEFAULTS, } from "./auto-mode-defaults.js";
2
2
  export const AUTO_MODE_ARMING_RECIPE_VERSION = 2;
3
3
  const isPlainRecord = (v) => typeof v === "object" && v !== null && !Array.isArray(v);
4
4
  function canonicalStrings(v) {
@@ -143,7 +143,7 @@ export function autoModeArmingRecipeOf(face, bind) {
143
143
  ...(face.settingsDenyRules !== undefined ? { settingsDenyRules: face.settingsDenyRules } : {}),
144
144
  ...(face.sessionContext !== undefined ? { sessionContext: face.sessionContext } : {}),
145
145
  ...(face.window !== undefined ? { window: face.window } : {}),
146
- ...(face.timeoutMs !== undefined ? { timeoutMs: face.timeoutMs } : {}),
146
+ timeoutMs: face.timeoutMs ?? AUTO_MODE_DEFAULT_TIMEOUT_MS,
147
147
  ...(face.denialLimit !== undefined ? { denialLimit: face.denialLimit } : {}),
148
148
  ...(face.settingsEpoch !== undefined ? { settingsEpoch: face.settingsEpoch } : {}),
149
149
  ...(face.crossSessionMessagesRule !== undefined ? { crossSessionMessagesRule: face.crossSessionMessagesRule } : {}),
@@ -208,8 +208,8 @@ export function foldAutoModeArming(recorded, current) {
208
208
  "(the strictest decidable answer: the inherited ask flows the original chain to a human)",
209
209
  };
210
210
  }
211
- const minKnob = (a, b, fallback) => Math.min(a ?? fallback, b ?? fallback);
212
- const timeoutMs = minKnob(rec.timeoutMs, cur.timeoutMs, AUTO_MODE_DEFAULT_TIMEOUT_MS);
211
+ const recordedTimeoutMs = rec.timeoutMs ?? AUTO_MODE_RECORDED_ABSENT_TIMEOUT_MS;
212
+ const timeoutMs = Math.min(recordedTimeoutMs, cur.timeoutMs ?? AUTO_MODE_DEFAULT_TIMEOUT_MS);
213
213
  const denialLimit = tightenDenialLimit(rec.denialLimit, cur.denialLimit);
214
214
  const recordedDenial = tightenDenialLimit(rec.denialLimit, rec.denialLimit);
215
215
  return {
@@ -221,7 +221,7 @@ export function foldAutoModeArming(recorded, current) {
221
221
  denialLimit,
222
222
  ...(cur.settingsEpoch !== undefined ? { settingsEpoch: cur.settingsEpoch } : {}),
223
223
  },
224
- tightened: timeoutMs < (rec.timeoutMs ?? AUTO_MODE_DEFAULT_TIMEOUT_MS) ||
224
+ tightened: timeoutMs < recordedTimeoutMs ||
225
225
  denialLimit.maxConsecutive < recordedDenial.maxConsecutive ||
226
226
  denialLimit.maxTotal < recordedDenial.maxTotal ||
227
227
  denialLimit.autoDenyAfterMs !== recordedDenial.autoDenyAfterMs,