@sema-agent/core 7.19.0 → 7.20.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (114) hide show
  1. package/CHANGELOG.md +199 -2
  2. package/dist/agents/subagent.js +12 -20
  3. package/dist/agents/teacher.js +2 -5
  4. package/dist/agents/verify.js +21 -10
  5. package/dist/brain/anthropic.js +3 -1
  6. package/dist/brain/open-responses.js +4 -2
  7. package/dist/brain/openai.js +3 -1
  8. package/dist/brain/stream-engine.d.ts +4 -0
  9. package/dist/brain/stream-engine.js +4 -0
  10. package/dist/core/auto-mode.d.ts +37 -18
  11. package/dist/core/auto-mode.js +1 -1
  12. package/dist/core/child-clamp-seats.d.ts +79 -0
  13. package/dist/core/child-clamp-seats.js +29 -0
  14. package/dist/core/engine-notice.d.ts +76 -49
  15. package/dist/core/fs-write-gate-policy.d.ts +17 -9
  16. package/dist/core/fs-write-gate-policy.js +51 -4
  17. package/dist/core/gate-bounded-await.d.ts +38 -0
  18. package/dist/core/gate-bounded-await.js +53 -0
  19. package/dist/core/gate-fold.js +16 -48
  20. package/dist/core/gate-lanes.js +11 -3
  21. package/dist/core/gate-outcome.d.ts +9 -6
  22. package/dist/core/governance-codes.d.ts +16 -1
  23. package/dist/core/governance-codes.js +14 -0
  24. package/dist/core/hooks.d.ts +8 -8
  25. package/dist/core/memory-engine/engine.d.ts +14 -9
  26. package/dist/core/memory-engine/engine.js +21 -8
  27. package/dist/core/memory-engine/layout.d.ts +31 -0
  28. package/dist/core/memory-engine/layout.js +68 -3
  29. package/dist/core/memory-engine/tools.js +6 -0
  30. package/dist/core/memory-engine/types.d.ts +24 -0
  31. package/dist/core/memory.d.ts +19 -0
  32. package/dist/core/memory.js +12 -0
  33. package/dist/core/remote-env.d.ts +32 -0
  34. package/dist/core/remote-env.js +4 -1
  35. package/dist/core/runner/advertised-writable-dirs.d.ts +59 -1
  36. package/dist/core/runner/advertised-writable-dirs.js +51 -3
  37. package/dist/core/runner/announce-once-ledger.d.ts +15 -0
  38. package/dist/core/runner/announce-once-ledger.js +8 -0
  39. package/dist/core/runner/classifier-ladder.d.ts +5 -0
  40. package/dist/core/runner/classifier-ladder.js +13 -2
  41. package/dist/core/runner/contracts.d.ts +44 -1
  42. package/dist/core/runner/denial-limit-arms.d.ts +14 -1
  43. package/dist/core/runner/denial-limit-arms.js +5 -4
  44. package/dist/core/runner/env-capability-census.d.ts +30 -0
  45. package/dist/core/runner/env-capability-census.js +38 -0
  46. package/dist/core/runner/forward-sink.d.ts +32 -0
  47. package/dist/core/runner/forward-sink.js +41 -0
  48. package/dist/core/runner/gate-exit.d.ts +4 -0
  49. package/dist/core/runner/gate-exit.js +4 -1
  50. package/dist/core/runner/git-status-frame.d.ts +0 -9
  51. package/dist/core/runner/listing-fold.d.ts +57 -0
  52. package/dist/core/runner/listing-fold.js +75 -0
  53. package/dist/core/runner/prepare-execution-env.d.ts +1 -1
  54. package/dist/core/runner/prepare-execution-env.js +2 -0
  55. package/dist/core/runner/prepare-gate-stations.d.ts +6 -0
  56. package/dist/core/runner/prepare-gate-stations.js +1 -3
  57. package/dist/core/runner/prepare-hands-readface.d.ts +4 -0
  58. package/dist/core/runner/prepare-hands-readface.js +3 -5
  59. package/dist/core/runner/prepare-listings.d.ts +3 -0
  60. package/dist/core/runner/prepare-listings.js +4 -2
  61. package/dist/core/runner/prepare-memory.js +7 -1
  62. package/dist/core/runner/prepare-policy-chain.js +5 -5
  63. package/dist/core/runner/prepare-prompt-inputs.d.ts +2 -2
  64. package/dist/core/runner/prepare-run-refs.d.ts +4 -2
  65. package/dist/core/runner/prepare-run-refs.js +5 -4
  66. package/dist/core/runner/prepare-task.js +8 -6
  67. package/dist/core/runner/prepare-wiring-manifest.d.ts +8 -1
  68. package/dist/core/runner/prepare-wiring-manifest.js +5 -2
  69. package/dist/core/runner/run-clock-and-content.js +3 -7
  70. package/dist/core/runner/run-harness-handlers.js +10 -10
  71. package/dist/core/runner/run-leg.js +28 -19
  72. package/dist/core/runner/run-settle-and-teardown.js +2 -5
  73. package/dist/core/runner/run-turn-boundary.js +44 -33
  74. package/dist/core/runner/runtask.js +2 -1
  75. package/dist/core/runner/synthetic-tools.d.ts +13 -39
  76. package/dist/core/runner/synthetic-tools.js +15 -59
  77. package/dist/core/runner/task-root-canonical.d.ts +45 -0
  78. package/dist/core/runner/task-root-canonical.js +18 -0
  79. package/dist/core/runner/turn-attachments.d.ts +125 -49
  80. package/dist/core/runner/turn-attachments.js +124 -66
  81. package/dist/core/runner/write-protection-station.d.ts +59 -0
  82. package/dist/core/runner/write-protection-station.js +32 -0
  83. package/dist/core/safe-notify.d.ts +5 -0
  84. package/dist/core/safe-notify.js +24 -2
  85. package/dist/core/sensitive-path-policy.d.ts +53 -27
  86. package/dist/core/sensitive-path-policy.js +16 -26
  87. package/dist/core/skill-listing-budget.d.ts +34 -40
  88. package/dist/core/skill-listing-budget.js +25 -26
  89. package/dist/core/skills-directory.js +17 -10
  90. package/dist/core/task-registry.js +2 -1
  91. package/dist/core/task-spec.d.ts +8 -7
  92. package/dist/core/tool-policy.d.ts +12 -1
  93. package/dist/core/tool-policy.js +39 -25
  94. package/dist/core/wiring-manifest.d.ts +31 -0
  95. package/dist/core/wiring-manifest.js +1 -0
  96. package/dist/core/write-protect.d.ts +124 -18
  97. package/dist/core/write-protect.js +91 -34
  98. package/dist/index.d.ts +6 -5
  99. package/dist/index.js +6 -5
  100. package/dist/orchestration/workflow-primitives.js +10 -20
  101. package/dist/stores/file/memory-store.d.ts +5 -2
  102. package/dist/stores/file/memory-store.js +8 -4
  103. package/dist/tools/fs/bash-readonly-classifier.d.ts +13 -2
  104. package/dist/tools/fs/bash-readonly-classifier.js +318 -36
  105. package/dist/tools/fs/fs-bash.d.ts +1 -1
  106. package/dist/tools/fs/fs-bash.js +12 -25
  107. package/dist/tools/fs/fs-shared.d.ts +22 -0
  108. package/dist/tools/fs/fs-shared.js +30 -0
  109. package/dist/tools/fs/index.d.ts +1 -1
  110. package/dist/tools/fs/index.js +1 -1
  111. package/package.json +2 -1
  112. package/test/export-surface.snapshot.json +25 -3
  113. package/wire-changes/core@7.20.0.json +568 -0
  114. package/wire-changes/core@7.20.1.json +183 -0
@@ -1,6 +1,7 @@
1
1
  import { Type } from "typebox";
2
2
  import { CODE_AGENT_PROMPT } from "../prompts/default.js";
3
3
  import { releaseSession } from "./session-util.js";
4
+ import { childClampSeats } from "../core/child-clamp-seats.js";
4
5
  import { mapNestedSuspend, isDurablePause, pauseFamily } from "./suspend-guard.js";
5
6
  import { delimitUntrusted, sanitizeUntrustedText } from "../core/untrusted-text.js";
6
7
  import { createSafeNotifier } from "../core/safe-notify.js";
@@ -136,17 +137,21 @@ export async function verifyCompleted(runner, result, specBase, objective, confi
136
137
  const runVerifier = async (impl, round) => {
137
138
  refuseUnhonorableInternals(internals, "before_leg");
138
139
  const roundEvidence = round === 1 ? config.evidence : undefined;
139
- const v = await runner.runTask({
140
+ const verifierSpec = {
140
141
  objective: verifierObjective(objective, impl, roundEvidence),
141
142
  systemPrompt: verifierPrompt,
142
143
  ...(config.verifierModel ? { model: config.verifierModel } : { modelRole: "verifier" }),
143
144
  roles: specBase.roles,
144
145
  tools: verifierTools,
145
146
  handsReadOnly: config.verifierHandsReadOnly ?? true,
146
- ...(foldedRootsFace ? { readFace: "roots" } : {}),
147
- ...(foldedRootsWriteFace ? { writeFace: "roots" } : {}),
148
- ...(foldedReadDeny.length > 0 ? { readDenyPatterns: [...foldedReadDeny] } : {}),
149
147
  interactiveTools: false,
148
+ ...childClampSeats({
149
+ handsReadOnly: specBase.handsReadOnly,
150
+ interactiveTools: specBase.interactiveTools,
151
+ readFace: foldedRootsFace ? "roots" : undefined,
152
+ writeFace: foldedRootsWriteFace ? "roots" : undefined,
153
+ readDenyPatterns: foldedReadDeny,
154
+ }),
150
155
  outputSchema: VerdictSchema,
151
156
  enableBlockedReport: false,
152
157
  limits: { ...(specBase.limits?.maxWalltimeMs !== undefined ? { maxWalltimeMs: specBase.limits.maxWalltimeMs } : {}) },
@@ -167,7 +172,8 @@ export async function verifyCompleted(runner, result, specBase, objective, confi
167
172
  ...(specBase.clientContext !== undefined ? { clientContext: { ...specBase.clientContext } } : {}),
168
173
  ...(specBase.promptProfile !== undefined ? { promptProfile: specBase.promptProfile } : {}),
169
174
  signal: specBase.signal,
170
- }, legInternals());
175
+ };
176
+ const v = await runner.runTask(verifierSpec, legInternals());
171
177
  try {
172
178
  const cost = (v.stats.costMicroUsd ?? 0) + (v.stats.nested?.costMicroUsd ?? 0);
173
179
  return { verdict: v.structuredOutput, cost, ...(isDurablePause(v.terminal) ? { paused: { ...v, terminal: v.terminal } } : {}) };
@@ -241,14 +247,19 @@ export async function verifyCompleted(runner, result, specBase, objective, confi
241
247
  if (config.costCeilingMicroUsd != null && spend >= config.costCeilingMicroUsd)
242
248
  break;
243
249
  refuseUnhonorableInternals(internals, "before_leg");
244
- current = await runner.runTask({
250
+ const fixLegSpec = {
245
251
  ...specBase,
246
252
  sessionId: current.sessionId,
247
253
  objective: fixObjective(outcome.findings),
248
- ...(foldedRootsFace ? { readFace: "roots" } : {}),
249
- ...(foldedRootsWriteFace ? { writeFace: "roots" } : {}),
250
- ...(foldedReadDeny.length > 0 ? { readDenyPatterns: [...foldedReadDeny] } : {}),
251
- }, legInternals());
254
+ ...childClampSeats({
255
+ handsReadOnly: specBase.handsReadOnly,
256
+ interactiveTools: specBase.interactiveTools,
257
+ readFace: foldedRootsFace ? "roots" : specBase.readFace,
258
+ writeFace: foldedRootsWriteFace ? "roots" : specBase.writeFace,
259
+ readDenyPatterns: foldedReadDeny,
260
+ }),
261
+ };
262
+ current = await runner.runTask(fixLegSpec, legInternals());
252
263
  spend += (current.stats.costMicroUsd ?? 0) + (current.stats.nested?.costMicroUsd ?? 0);
253
264
  implAccount.add(current.stats);
254
265
  if (isDurablePause(current.terminal)) {
@@ -365,6 +365,7 @@ export function createAnthropicBrain(config = {}) {
365
365
  const blocks = new Map();
366
366
  const usageRaw = {};
367
367
  let stopReason = null;
368
+ const terminalFrameSeen = () => stopReason != null;
368
369
  let stopDetails;
369
370
  let nextContentIndex = 0;
370
371
  let malformedFrames = 0;
@@ -532,6 +533,7 @@ export function createAnthropicBrain(config = {}) {
532
533
  hasSubstantiveText,
533
534
  hasCompletedToolCall,
534
535
  hasOnlyThinking: hasThinking && !hasSubstantiveText && !hasCompletedToolCall,
536
+ terminalFrameSeen: terminalFrameSeen(),
535
537
  };
536
538
  },
537
539
  sealForRetry() {
@@ -666,7 +668,7 @@ export function createAnthropicBrain(config = {}) {
666
668
  emptyNoFinish ||
667
669
  (toolError !== undefined && noUsableContent) ||
668
670
  (truncatedEmpty && !dynamicCut && !staticReasoningCut);
669
- const tornWithContent = !errored && opts?.truncated !== true && stopReason == null && !noUsableContent && !degenerate;
671
+ const tornWithContent = !errored && opts?.truncated !== true && !terminalFrameSeen() && !noUsableContent && !degenerate;
670
672
  const errorMessageOut = tornWithContent ? (errorMessage ? `${errorMessage}\n${STREAM_TORN_WARNING}` : STREAM_TORN_WARNING) : errorMessage;
671
673
  const errorKind = degenerate
672
674
  ? "degenerate"
@@ -303,6 +303,7 @@ export function createOpenResponsesBrain(config = {}) {
303
303
  let usageRaw;
304
304
  let responseId;
305
305
  let terminalStatus;
306
+ const terminalFrameSeen = () => terminalStatus !== undefined;
306
307
  let incompleteReason;
307
308
  let accumRefusal = "";
308
309
  const capabilityNotes = [];
@@ -573,6 +574,7 @@ export function createOpenResponsesBrain(config = {}) {
573
574
  hasSubstantiveText,
574
575
  hasCompletedToolCall,
575
576
  hasOnlyThinking: hasReasoning && !hasSubstantiveText && !hasCompletedToolCall,
577
+ terminalFrameSeen: terminalFrameSeen(),
576
578
  };
577
579
  },
578
580
  sealForRetry() {
@@ -690,7 +692,7 @@ export function createOpenResponsesBrain(config = {}) {
690
692
  const safetyCut = incompleteReason === INCOMPLETE_CONTENT_FILTER;
691
693
  const refused = accumRefusal.trim().length > 0;
692
694
  const incompleteUnmapped = terminalStatus === "incomplete" && incompleteReason !== INCOMPLETE_MAX_OUTPUT_TOKENS && incompleteReason !== INCOMPLETE_CONTENT_FILTER;
693
- const emptyNoFinish = noUsableContent && (terminalStatus === undefined || incompleteUnmapped) && !degenerate;
695
+ const emptyNoFinish = noUsableContent && (!terminalFrameSeen() || incompleteUnmapped) && !degenerate;
694
696
  const maxTokensNote = sentMaxTokens !== undefined
695
697
  ? `effective max_output_tokens=${sentMaxTokens} (from ${sentMaxTokensLane === "options"
696
698
  ? "options.maxTokens"
@@ -718,7 +720,7 @@ export function createOpenResponsesBrain(config = {}) {
718
720
  emptyNoFinish ||
719
721
  (toolError !== undefined && noUsableContent) ||
720
722
  (truncatedEmpty && !dynamicCut && !staticReasoningCut);
721
- const tornWithContent = !errored && opts?.truncated !== true && (terminalStatus === undefined || incompleteUnmapped) && !noUsableContent && !degenerate;
723
+ const tornWithContent = !errored && opts?.truncated !== true && (!terminalFrameSeen() || incompleteUnmapped) && !noUsableContent && !degenerate;
722
724
  const partialWarning = terminalStatus === undefined ? STREAM_TORN_WARNING : RESPONSE_INCOMPLETE_WARNING;
723
725
  const errorMessageOut = tornWithContent ? (errorMessage ? `${errorMessage}\n${partialWarning}` : partialWarning) : errorMessage;
724
726
  const errorKind = degenerate
@@ -373,6 +373,7 @@ export function createOpenAIBrain(config = {}) {
373
373
  let textIndex = 0;
374
374
  const toolAccum = new Map();
375
375
  let finishReason = null;
376
+ const terminalFrameSeen = () => finishReason != null;
376
377
  let usageRaw;
377
378
  let malformedFrames = 0;
378
379
  let malformedSample;
@@ -438,6 +439,7 @@ export function createOpenAIBrain(config = {}) {
438
439
  hasSubstantiveText,
439
440
  hasCompletedToolCall,
440
441
  hasOnlyThinking: accumReasoning.length > 0 && !hasSubstantiveText && !hasCompletedToolCall,
442
+ terminalFrameSeen: terminalFrameSeen(),
441
443
  };
442
444
  },
443
445
  sealForRetry() {
@@ -619,7 +621,7 @@ export function createOpenAIBrain(config = {}) {
619
621
  emptyNoFinish ||
620
622
  (toolError !== undefined && noUsableContent) ||
621
623
  (truncatedEmpty && !dynamicCut);
622
- const tornWithContent = !errored && opts?.truncated !== true && finishReason == null && !noUsableContent && !degenerate;
624
+ const tornWithContent = !errored && opts?.truncated !== true && !terminalFrameSeen() && !noUsableContent && !degenerate;
623
625
  const errorMessageOut = tornWithContent ? (errorMessage ? `${errorMessage}\n${STREAM_TORN_WARNING}` : STREAM_TORN_WARNING) : errorMessage;
624
626
  const errorKind = degenerate
625
627
  ? "degenerate"
@@ -194,11 +194,15 @@ export interface StreamParser {
194
194
  * - `hasOnlyThinking` — thinking streamed but nothing substantive (no non-blank text, no completed
195
195
  * tool call) → tier B (seal + whole-turn retry). A dangling tool-call accumulation alongside the
196
196
  * thinking does not veto this (nothing was admitted; the retry's snapshot replacement drops it).
197
+ * - `terminalFrameSeen` — the provider TERMINATED this stream (openai `finish_reason`, anthropic
198
+ * `message_delta.stop_reason`, responses `response.completed`/`response.incomplete`). A FACT, not a tier:
199
+ * the parser reports it off the same state its own finalize reads, and the tiering owns what it means.
197
200
  */
198
201
  snapshot(): {
199
202
  hasSubstantiveText: boolean;
200
203
  hasCompletedToolCall: boolean;
201
204
  hasOnlyThinking: boolean;
205
+ terminalFrameSeen: boolean;
202
206
  };
203
207
  /**
204
208
  * design/124 §0.5-2 tier B: seal the UI stream before a whole-turn retry — emit `thinking_end` for
@@ -647,6 +647,10 @@ export function runStreamingBrain(args) {
647
647
  return;
648
648
  }
649
649
  const snap = parser.snapshot();
650
+ if (snap.terminalFrameSeen) {
651
+ parser.finalize();
652
+ return;
653
+ }
650
654
  if (snap.hasSubstantiveText || snap.hasCompletedToolCall) {
651
655
  midstreamWarning =
652
656
  failure.kind === "stalled"
@@ -13,10 +13,12 @@ export declare function isAutoModeUnavailableCause(v: unknown): v is AutoModeUna
13
13
  /**
14
14
  * HOW a reply failed to carry a verdict — the closed set behind `parse_error.form`. `missing` = the reader found
15
15
  * NO answer (empty text, prose, an answer only inside a thinking block); `conflict` = it found TWO distinct
16
- * `<block>` answers (raw or thinking-stripped). The split exists for the classify leg's re-ask: a missing
17
- * answer is asked for again (CC 2.1.259 `Vrr` re-asks an unparseable reply), a conflict is not — it carries a
18
- * `yes`, and a second attempt that answered `no` would erase it. Not on any wire face; the trace frame and the
19
- * gate record keep the one word `parse_error`.
16
+ * `<block>` answers (raw or thinking-stripped). The split decides who gets a SECOND ATTEMPT at the same
17
+ * question, on both seats that offer one: a missing answer is asked for again (CC 2.1.259 `Vrr` re-asks an
18
+ * unparseable reply) and handed on to the next candidate ({@link rungDidNotRule}); a conflict gets neither —
19
+ * it carries a `yes`, and any second attempt that answered `no` would erase it. One rule, two seats: whatever
20
+ * a re-ask may not overwrite, a later rung may not overwrite either. Not on any wire face; the trace frame and
21
+ * the gate record keep the one word `parse_error`.
20
22
  */
21
23
  export declare const AUTO_MODE_PARSE_FAILURES: readonly ["missing", "conflict"];
22
24
  export type AutoModeParseFailure = (typeof AUTO_MODE_PARSE_FAILURES)[number];
@@ -26,8 +28,9 @@ export declare function isAutoModeParseFailure(v: unknown): v is AutoModeParseFa
26
28
  * WHY a classification moved DOWN the candidate ladder — the closed set behind
27
29
  * {@link AutoModeClassifierRound}'s `fallback.cause`, i.e. the ways a candidate produces NO RULING
28
30
  * ({@link rungDidNotRule}). Two words are the unavailable causes verbatim (the leg threw / rejected; the
29
- * round-trip cap elapsed); `parse_error` is the candidate that ANSWERED without a verdict. A ruling is not a
30
- * member: the ladder stops at a verdict either way.
31
+ * round-trip cap elapsed); `parse_error` is the candidate that ANSWERED with no answer in it at all (the
32
+ * `missing` form). A ruling is not a member, and neither is a CONTRADICTORY reply (`conflict`): the ladder
33
+ * stops at a rung that settled the classification, whichever way it settled it.
31
34
  */
32
35
  export declare const AUTO_MODE_FALLBACK_CAUSES: readonly ["error", "timeout", "parse_error"];
33
36
  export type AutoModeFallbackCause = (typeof AUTO_MODE_FALLBACK_CAUSES)[number];
@@ -65,8 +68,9 @@ export type AutoModeVerdict = ({
65
68
  } & AutoModeRoundAttributed)
66
69
  /** The model responded but not in the `<block>…` contract shape. Handled as a BLOCK with the CC
67
70
  * parse-failure sentence ({@link classifierParseFailureDenyMessage}), counted by the denial limit. `form`
68
- * says WHICH way the reply failed ({@link AutoModeParseFailure}): the classify leg re-asks a `missing`
69
- * reply once and never a `conflict`. */
71
+ * says WHICH way the reply failed ({@link AutoModeParseFailure}) and decides who may try the question a
72
+ * second time: the classify leg re-asks a `missing` reply once and the ladder hands it to the next rung;
73
+ * a `conflict` gets neither ({@link rungDidNotRule}). */
70
74
  | ({
71
75
  kind: "parse_error";
72
76
  raw: string;
@@ -100,20 +104,18 @@ export interface AutoModeClassifierRound {
100
104
  * the QUESTION (the transcript window every rung shares), not in a rung's answer.
101
105
  *
102
106
  * WHY IT EXISTS. The ladder advances on "the rung produced no ruling", and a hook that rejects looks exactly
103
- * like that. But a classification whose prompt could not be built fails IDENTICALLY on every rung (the build is one
104
- * shared promise, by contract — "the same prompt, re-asked" is literal), so the walk recorded a fallback
105
- * `{from: seat, to: main, cause: "error"}` and a deny sentence naming two models, for a classification in which NO
106
- * MODEL WAS EVER DISPATCHED: a transcript-store outage read, on every face a person can see, as a broken
107
- * classifier seat. The verdict is unchanged (fail-closed `unavailable`); what changes is that the record
108
- * stops attributing it to models that were never asked.
107
+ * like that. But a classification whose prompt could not be built fails IDENTICALLY on every rung (the build is
108
+ * one shared promise, by contract — "the same prompt, re-asked" is literal), so without this word the walk
109
+ * records a fallback and a deny sentence naming two models for a classification in which NO MODEL WAS EVER
110
+ * DISPATCHED. The verdict is unchanged (fail-closed `unavailable`); what this changes is that the record stops
111
+ * attributing it to models that were never asked.
109
112
  *
110
113
  * Carried as a `code` on the thrown error rather than a class so any classify hook — including one this
111
114
  * package never constructed — can raise it without importing a constructor.
112
115
  *
113
116
  * WHAT IT DOES NOT COVER, stated: a build that OUTRUNS a rung's cap is still recorded as that rung's
114
117
  * `timeout`. The cap bounds the whole classify call, assembly included, so at that point nothing distinguishes
115
- * a slow transcript store from a slow model. Moving the per-classification build outside the rung cap is a
116
- * seam change, filed for a later version.
118
+ * a slow transcript store from a slow model.
117
119
  */
118
120
  export declare const AUTO_MODE_ROUND_NOT_ASKED_CODE = "auto_mode.round_not_asked";
119
121
  /** Mint the marker above around whatever failed, keeping the original as `cause` for the host's own logs.
@@ -124,8 +126,25 @@ export declare const AUTO_MODE_ROUND_NOT_ASKED_CODE = "auto_mode.round_not_asked
124
126
  export declare function autoModeRoundNotAsked(reason: unknown): Error & {
125
127
  code: string;
126
128
  };
127
- /** Why a rung did not settle the classification, or `undefined` when it RULED and the ladder stops — the ONE
128
- * predicate behind the advance (there is no `if (fallback)` arm anywhere). */
129
+ /**
130
+ * Why a rung did not settle the classification, or `undefined` when it SETTLED it and the ladder stops — the
131
+ * ONE predicate behind the advance (there is no `if (fallback)` arm anywhere).
132
+ *
133
+ * A rung settles the classification two ways: it RULES (`allow` / `block`), or it answers with a
134
+ * CONTRADICTION (`parse_error` of form `conflict` — two distinct `<block>` answers in one reply). The second
135
+ * is not a ruling and stays the fail-closed deny it is; what it is NOT is an unanswered question. It holds a
136
+ * `yes`, so asking the SAME question again — of the same model as a re-ask, or of the next rung as a fallback
137
+ * — can only overwrite that `yes` with a `no` and allow a call the classifier already objected to. One rule
138
+ * for both second attempts: {@link AUTO_MODE_PARSE_FAILURES} names the form, the leg reads it for the re-ask
139
+ * and this predicate reads it for the walk. Only a reply with NO answer in it (`missing`) moves the ladder.
140
+ *
141
+ * @contract auto_mode.ladder.advances_on_no_evidence — the candidate ladder advances on exactly one
142
+ * condition, and it is the ABSENCE of evidence: the rung's leg threw or rejected, its round-trip cap elapsed,
143
+ * or its reply carried no answer at all. A rung that RULED and a rung whose reply CONTRADICTED itself both
144
+ * settle the classification where they stand — the contradiction as the fail-closed `parse_error` deny it
145
+ * already is, attributed to that rung and disclosing no fallback — so no later candidate is ever asked a
146
+ * question an earlier one has already answered `yes` to.
147
+ */
129
148
  export declare function rungDidNotRule(v: AutoModeVerdict): AutoModeFallbackCause | undefined;
130
149
  /**
131
150
  * The model-facing deny text for an `unavailable` verdict — CC 2.1.250 `x1t`, with the parenthetical `EIt`
@@ -56,7 +56,7 @@ export function rungDidNotRule(v) {
56
56
  if (v.kind === "unavailable")
57
57
  return v.cause;
58
58
  if (v.kind === "parse_error")
59
- return "parse_error";
59
+ return v.form === "conflict" ? undefined : "parse_error";
60
60
  return undefined;
61
61
  }
62
62
  export function classifierUnavailableDenyMessage(toolName, cause, attribution) {
@@ -0,0 +1,79 @@
1
+ /**
2
+ * The TIGHTEN-ONLY CLAMP SEATS an engine-built child spec carries from the run that builds it — and the
3
+ * ONE carrier every such constructor passes them through.
4
+ *
5
+ * Why a carrier and not a convention. The engine builds child specs in several places (the delegation
6
+ * lane, the observer sidecar, the wake/revival fold, the teacher's internal legs, the verify gate's two
7
+ * legs, the workflow lane), and each re-deriving per seat WHICH value is the tightening one lands a new
8
+ * seat one constructor at a time — the ones it does not reach staying invisible, since a `grep readFace`
9
+ * census cannot see a station that carries the face through a persisted seed or a synthesized fold.
10
+ *
11
+ * Two mechanical halves replace the census:
12
+ * · the COMPILER — {@link ChildClampSource} requires every seat as a PRESENT key (an `undefined` value
13
+ * is fine; a missing key is not), so adding a member to {@link CHILD_CLAMP_SEATS} reds `tsc` at every
14
+ * construction site until each one says what it carries there;
15
+ * · the SCAN — `scripts/verify-child-clamp-seats.mjs` reads {@link ENGINE_CHILD_SPEC_CONSTRUCTORS} and
16
+ * requires each registered constructor to go through this carrier, requires the carrier's own source
17
+ * to cover every seat, and requires every tighten-only seat `tightenTaskSpec` governs to be either
18
+ * carried here or listed in {@link CHILD_CLAMP_SEATS_NOT_CARRIED} with a reason.
19
+ *
20
+ * What "tighten-only" means for this seat list: the value that travels is the RESTRICTING one and only
21
+ * that one, so a parent that clamped nothing changes nothing about its children, and nothing a child can
22
+ * author lifts a clamp its parent ran under. That is why the carrier omits a key rather than writing a
23
+ * loosening value — a constructor may state its own stricter default and spread this after it.
24
+ */
25
+ import type { ReadDenyEntry } from "../tools/fs/read-deny.js";
26
+ import type { TaskSpec } from "./types.js";
27
+ /**
28
+ * The clamp seats, as string literals (the closed set the scan and the wire-manifest reader both parse —
29
+ * a spread or an identifier here reads as an empty set).
30
+ */
31
+ export declare const CHILD_CLAMP_SEATS: readonly ["handsReadOnly", "interactiveTools", "readFace", "writeFace", "readDenyPatterns"];
32
+ /** One clamp seat's name. */
33
+ export type ChildClampSeat = (typeof CHILD_CLAMP_SEATS)[number];
34
+ /**
35
+ * The seats `tightenTaskSpec` governs that this carrier deliberately does NOT carry, each with the reason
36
+ * — the "forgot" / "decided" distinction, kept where a new reader will trip over it. The scan requires
37
+ * every seat named in `tightenTaskSpec` to be in this table or in {@link CHILD_CLAMP_SEATS}.
38
+ */
39
+ export declare const CHILD_CLAMP_SEATS_NOT_CARRIED: Readonly<Record<string, string>>;
40
+ /**
41
+ * The engine-built child-spec constructors, as `<repo-relative file>#<name>` — the closed registry the
42
+ * scan enumerates. A constructor that builds a spec for a child RUN belongs here; the scan requires each
43
+ * row to resolve to exactly one declaration IN ITS OWN FILE that calls {@link childClampSeats}.
44
+ *
45
+ * Why the file is half the key, and why a shape the scan cannot read is a RED rather than an absence: a
46
+ * registry keyed on a bare NAME asks a syntactic scan to enumerate every shape that binds a callable, and
47
+ * that list has no end — every shape it misses leaves the real constructor invisible while a namesake
48
+ * answers for it.
49
+ */
50
+ export declare const ENGINE_CHILD_SPEC_CONSTRUCTORS: readonly ["src/agents/subagent.ts#buildChildSpec", "src/agents/subagent.ts#observerSpec", "src/agents/subagent.ts#resumeSpec", "src/agents/teacher.ts#inheritedDeploymentConfig", "src/agents/verify.ts#verifierSpec", "src/agents/verify.ts#fixLegSpec", "src/orchestration/workflow-primitives.ts#workflowChildClamps"];
51
+ /**
52
+ * The clamp source a construction site hands the carrier: EVERY seat as a present key. A site whose lane
53
+ * has no value for a seat writes `undefined` explicitly — which is the point, since that is a sentence
54
+ * a reader (and a diff) can see, unlike an absent key.
55
+ */
56
+ export interface ChildClampSource {
57
+ /** The write-hands clamp: only `true` travels. */
58
+ handsReadOnly: boolean | undefined;
59
+ /** The hard-headless clamp: only `false` travels. */
60
+ interactiveTools: boolean | undefined;
61
+ /** The read containment face: only `"roots"` travels. */
62
+ readFace: TaskSpec["readFace"] | undefined;
63
+ /** The write containment face: only `"roots"` travels. */
64
+ writeFace: TaskSpec["writeFace"] | undefined;
65
+ /** The deny-set additions: an add-only axis, so a non-empty list travels as a spawn-time copy. */
66
+ readDenyPatterns: readonly ReadDenyEntry[] | undefined;
67
+ }
68
+ /**
69
+ * Project a parent's clamps onto a child spec — the ONE place that decides, per seat, which value is the
70
+ * tightening one and how it is copied. Spread the result onto the child spec; a seat with nothing to
71
+ * clamp contributes NO KEY, so a constructor may state its own stricter value above the spread and keep
72
+ * it (a `false` from the parent can never lift it).
73
+ *
74
+ * @contract child_spec.clamps_travel_whole — every child spec the ENGINE builds carries the whole of its
75
+ * parent's tighten-only clamp seats (`CHILD_CLAMP_SEATS`) through this one carrier: only the restricting
76
+ * value of each seat travels, arrays are copied at spawn, and a seat the parent did not clamp adds no key
77
+ * — so a clamp can never end one level deep, and a new seat cannot reach some constructors and not others.
78
+ */
79
+ export declare function childClampSeats(source: ChildClampSource): Partial<TaskSpec>;
@@ -0,0 +1,29 @@
1
+ export const CHILD_CLAMP_SEATS = ["handsReadOnly", "interactiveTools", "readFace", "writeFace", "readDenyPatterns"];
2
+ export const CHILD_CLAMP_SEATS_NOT_CARRIED = Object.freeze({
3
+ toolPolicy: "not a clamp a child spec copies: a parent's denies reach its children through the ancestor-constraint chain the child's own prepare folds, not by putting the parent's policy object on the child seat",
4
+ shellGate: "travels on the inherited-gate lane (`InheritedGate.shellGate`, a trusted internals seat), not on the child spec — a spec-level copy would be a second source for the same clamp",
5
+ onAsk: "per-lane by construction: the delegation lane wraps the parent's approver with spawn provenance before it becomes the child's seat, and a lane with no approver must hand down none",
6
+ hooks: "a deployment seat, not an inherited clamp — `tightenTaskSpec` refuses to merge two hook sets rather than composing them, so there is no tightening value to carry",
7
+ excludeTools: "roster control rather than containment: the lanes that copy a parent's roster do so beside these seats, and the workflow lane authors its child roster from the governance baseline instead of the host's",
8
+ deferTools: "roster control, same reading as `excludeTools` above",
9
+ });
10
+ export const ENGINE_CHILD_SPEC_CONSTRUCTORS = [
11
+ "src/agents/subagent.ts#buildChildSpec",
12
+ "src/agents/subagent.ts#observerSpec",
13
+ "src/agents/subagent.ts#resumeSpec",
14
+ "src/agents/teacher.ts#inheritedDeploymentConfig",
15
+ "src/agents/verify.ts#verifierSpec",
16
+ "src/agents/verify.ts#fixLegSpec",
17
+ "src/orchestration/workflow-primitives.ts#workflowChildClamps",
18
+ ];
19
+ export function childClampSeats(source) {
20
+ return {
21
+ ...(source.handsReadOnly === true ? { handsReadOnly: true } : {}),
22
+ ...(source.interactiveTools === false ? { interactiveTools: false } : {}),
23
+ ...(source.readFace === "roots" ? { readFace: "roots" } : {}),
24
+ ...(source.writeFace === "roots" ? { writeFace: "roots" } : {}),
25
+ ...(source.readDenyPatterns !== undefined && source.readDenyPatterns.length > 0
26
+ ? { readDenyPatterns: [...source.readDenyPatterns] }
27
+ : {}),
28
+ };
29
+ }