@sema-agent/core 7.18.0 → 7.20.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 (137) hide show
  1. package/CHANGELOG.md +276 -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 +8 -3
  6. package/dist/brain/open-responses.js +12 -4
  7. package/dist/brain/openai.js +7 -2
  8. package/dist/brain/reasoning.d.ts +13 -13
  9. package/dist/brain/reasoning.js +2 -2
  10. package/dist/brain/stream-engine.d.ts +4 -0
  11. package/dist/brain/stream-engine.js +6 -1
  12. package/dist/brain/stream-shared.d.ts +3 -0
  13. package/dist/brain/stream-shared.js +1 -0
  14. package/dist/core/ask-origin.d.ts +16 -0
  15. package/dist/core/ask-origin.js +8 -0
  16. package/dist/core/auto-mode-arming.d.ts +6 -0
  17. package/dist/core/auto-mode-arming.js +5 -5
  18. package/dist/core/auto-mode-defaults.d.ts +43 -15
  19. package/dist/core/auto-mode-defaults.js +4 -1
  20. package/dist/core/auto-mode.d.ts +74 -26
  21. package/dist/core/auto-mode.js +80 -11
  22. package/dist/core/checkpoint-store.d.ts +7 -0
  23. package/dist/core/checkpoint-store.js +2 -2
  24. package/dist/core/child-clamp-seats.d.ts +79 -0
  25. package/dist/core/child-clamp-seats.js +29 -0
  26. package/dist/core/engine-notice.d.ts +89 -41
  27. package/dist/core/fs-write-gate-policy.d.ts +17 -9
  28. package/dist/core/fs-write-gate-policy.js +51 -4
  29. package/dist/core/gate-bounded-await.d.ts +38 -0
  30. package/dist/core/gate-bounded-await.js +53 -0
  31. package/dist/core/gate-fold.js +20 -48
  32. package/dist/core/gate-lanes.js +17 -9
  33. package/dist/core/gate-outcome.d.ts +34 -6
  34. package/dist/core/gate-outcome.js +7 -0
  35. package/dist/core/governance-codes.d.ts +16 -1
  36. package/dist/core/governance-codes.js +18 -0
  37. package/dist/core/hooks.d.ts +8 -8
  38. package/dist/core/memory-engine/engine.d.ts +14 -9
  39. package/dist/core/memory-engine/engine.js +21 -8
  40. package/dist/core/memory-engine/layout.d.ts +31 -0
  41. package/dist/core/memory-engine/layout.js +68 -3
  42. package/dist/core/memory-engine/types.d.ts +24 -0
  43. package/dist/core/memory.d.ts +19 -0
  44. package/dist/core/memory.js +12 -0
  45. package/dist/core/read-root-candidate.d.ts +12 -0
  46. package/dist/core/read-root-candidate.js +24 -0
  47. package/dist/core/remote-env.d.ts +32 -0
  48. package/dist/core/remote-env.js +4 -1
  49. package/dist/core/roles.d.ts +9 -1
  50. package/dist/core/roles.js +4 -0
  51. package/dist/core/runner/advertised-writable-dirs.d.ts +59 -1
  52. package/dist/core/runner/advertised-writable-dirs.js +51 -3
  53. package/dist/core/runner/announce-once-ledger.d.ts +15 -0
  54. package/dist/core/runner/announce-once-ledger.js +8 -0
  55. package/dist/core/runner/classifier-ladder.d.ts +154 -11
  56. package/dist/core/runner/classifier-ladder.js +170 -10
  57. package/dist/core/runner/contracts.d.ts +46 -1
  58. package/dist/core/runner/denial-limit-arms.d.ts +14 -1
  59. package/dist/core/runner/denial-limit-arms.js +7 -6
  60. package/dist/core/runner/env-capability-census.d.ts +30 -0
  61. package/dist/core/runner/env-capability-census.js +38 -0
  62. package/dist/core/runner/forward-sink.d.ts +32 -0
  63. package/dist/core/runner/forward-sink.js +41 -0
  64. package/dist/core/runner/gate-exit.d.ts +4 -0
  65. package/dist/core/runner/gate-exit.js +4 -1
  66. package/dist/core/runner/git-status-frame.d.ts +0 -9
  67. package/dist/core/runner/listing-fold.d.ts +57 -0
  68. package/dist/core/runner/listing-fold.js +75 -0
  69. package/dist/core/runner/prepare-ask-lane.js +1 -0
  70. package/dist/core/runner/prepare-caps-and-workflow.js +20 -31
  71. package/dist/core/runner/prepare-config-doors.d.ts +6 -0
  72. package/dist/core/runner/prepare-config-doors.js +8 -10
  73. package/dist/core/runner/prepare-execution-env.d.ts +1 -1
  74. package/dist/core/runner/prepare-execution-env.js +2 -0
  75. package/dist/core/runner/prepare-gate-stations.d.ts +8 -0
  76. package/dist/core/runner/prepare-gate-stations.js +6 -2
  77. package/dist/core/runner/prepare-hands-readface.d.ts +4 -0
  78. package/dist/core/runner/prepare-hands-readface.js +3 -5
  79. package/dist/core/runner/prepare-listings.d.ts +3 -0
  80. package/dist/core/runner/prepare-listings.js +4 -2
  81. package/dist/core/runner/prepare-memory.js +7 -1
  82. package/dist/core/runner/prepare-policy-chain.js +5 -5
  83. package/dist/core/runner/prepare-project-context.d.ts +5 -1
  84. package/dist/core/runner/prepare-project-context.js +3 -2
  85. package/dist/core/runner/prepare-prompt-inputs.d.ts +2 -2
  86. package/dist/core/runner/prepare-run-refs.d.ts +4 -2
  87. package/dist/core/runner/prepare-run-refs.js +5 -4
  88. package/dist/core/runner/prepare-task.js +8 -6
  89. package/dist/core/runner/resume-apply.js +3 -3
  90. package/dist/core/runner/run-clock-and-content.js +3 -7
  91. package/dist/core/runner/run-harness-handlers.js +10 -10
  92. package/dist/core/runner/run-leg.js +31 -16
  93. package/dist/core/runner/run-settle-and-teardown.js +2 -5
  94. package/dist/core/runner/run-turn-boundary.js +44 -25
  95. package/dist/core/runner/runtask.js +2 -1
  96. package/dist/core/runner/synthetic-tools.d.ts +41 -29
  97. package/dist/core/runner/synthetic-tools.js +25 -40
  98. package/dist/core/runner/task-root-canonical.d.ts +45 -0
  99. package/dist/core/runner/task-root-canonical.js +18 -0
  100. package/dist/core/runner/turn-attachments.d.ts +160 -53
  101. package/dist/core/runner/turn-attachments.js +153 -51
  102. package/dist/core/safe-notify.d.ts +5 -0
  103. package/dist/core/safe-notify.js +24 -2
  104. package/dist/core/sensitive-path-policy.d.ts +84 -17
  105. package/dist/core/sensitive-path-policy.js +37 -19
  106. package/dist/core/skill-listing-budget.d.ts +106 -0
  107. package/dist/core/skill-listing-budget.js +57 -0
  108. package/dist/core/skills-directory.js +17 -10
  109. package/dist/core/task-registry.js +2 -1
  110. package/dist/core/task-spec.d.ts +29 -2
  111. package/dist/core/tool-policy.d.ts +31 -5
  112. package/dist/core/tool-policy.js +45 -32
  113. package/dist/core/tool-spec.d.ts +37 -4
  114. package/dist/core/types.d.ts +1 -1
  115. package/dist/core/write-protect.d.ts +105 -20
  116. package/dist/core/write-protect.js +89 -32
  117. package/dist/index.d.ts +8 -7
  118. package/dist/index.js +6 -5
  119. package/dist/orchestration/workflow-primitives.js +10 -20
  120. package/dist/stores/file/memory-store.d.ts +5 -2
  121. package/dist/stores/file/memory-store.js +8 -4
  122. package/dist/tools/fs/bash-readonly-classifier.d.ts +40 -2
  123. package/dist/tools/fs/bash-readonly-classifier.js +376 -41
  124. package/dist/tools/fs/fs-bash.d.ts +1 -1
  125. package/dist/tools/fs/fs-bash.js +19 -28
  126. package/dist/tools/fs/fs-shared.d.ts +22 -0
  127. package/dist/tools/fs/fs-shared.js +30 -0
  128. package/dist/tools/fs/index.d.ts +1 -1
  129. package/dist/tools/fs/index.js +1 -1
  130. package/dist/tools/fs/read-deny.d.ts +4 -3
  131. package/dist/tools/fs/safety.d.ts +15 -0
  132. package/dist/tools/fs/safety.js +7 -0
  133. package/package.json +2 -1
  134. package/test/export-surface.snapshot.json +31 -5
  135. package/wire-changes/core@7.18.0.json +1273 -0
  136. package/wire-changes/core@7.19.0.json +457 -0
  137. package/wire-changes/core@7.20.0.json +568 -0
@@ -1,5 +1,19 @@
1
- /** Hard cap on one classification round-trip when `AutoModeDeciderOptions.timeoutMs` is omitted. */
2
- export declare const AUTO_MODE_DEFAULT_TIMEOUT_MS = 15000;
1
+ /**
2
+ * Hard cap on one classification RUNG (both attempts) when `AutoModeDeciderOptions.timeoutMs` is omitted — CC
3
+ * 2.1.259's per-attempt classifier base (`j7e = 60000`; 2.1.250 `oue`, the same number), which upstream
4
+ * stretches to 120 s with prompt size and sema leaves to the deployment knob. The 15 s that stood here was
5
+ * never upstream's: live 2026-09-15 a thinking-by-default gateway answers in 5–165 s (Qwen 3.8 p50 11–24 s,
6
+ * kimi and GLM p50 ~10 s), so 15 s turned most classifications into `unavailable (timed out)`. Worst-case
7
+ * walk = this × rungs (2) = 120 s.
8
+ */
9
+ export declare const AUTO_MODE_DEFAULT_TIMEOUT_MS = 60000;
10
+ /**
11
+ * What an arming RECORDING that carries no `timeoutMs` meant when it was written: the default of its day. Every
12
+ * recipe minted since the deadline moved to 60 s MATERIALISES its bound, so an absent recorded knob dates the
13
+ * recording — the fold reads it as the 15 s it ran under, never as the current default
14
+ * (`auto_mode.arming.absent_timeout_reads_recorded_default`, on the fold).
15
+ */
16
+ export declare const AUTO_MODE_RECORDED_ABSENT_TIMEOUT_MS = 15000;
3
17
  /** Newest transcript entries in the classify window when `AutoModeWindowOptions.maxEntries` is omitted. */
4
18
  export declare const AUTO_MODE_DEFAULT_WINDOW_MAX_ENTRIES = 40;
5
19
  /** Per-entry excerpt cap when `AutoModeWindowOptions.maxCharsPerEntry` is omitted. */
@@ -17,25 +31,39 @@ export declare const AUTO_MODE_DEFAULT_WINDOW_MAX_CHARS = 2000;
17
31
  * parses as a block (the parser tolerates a missing tail), a truncated allow does not exist (it is
18
32
  * seven tokens), and the cap's job is to make a model that ignores the "no preamble" instruction —
19
33
  * or that reasons in-text — cost a bounded, quickly-detected contract failure instead of a slow one.
20
- * Paired with `reasoning:"off"` at the call: the cap hosts the ANSWER, so it applies only where the
21
- * wire can say off (`thinkingOffExpressible`); a model that thinks regardless keeps its own budget.
34
+ * Paired with `reasoning:"off"` at the call: the cap hosts the ANSWER, so it stands alone only where the
35
+ * wire SPELLS off (`thinkingOffSpelled`); every other seat adds a thinking allowance to it.
22
36
  */
23
37
  export declare const AUTO_MODE_CLASSIFIER_MAX_TOKENS = 256;
24
38
  /**
25
- * The output-budget ALLOWANCE for a classifier request that must think: added to the verdict cap when the
26
- * seat asks for `reasoning:"low"` instead of `"off"` — a model whose wire cannot say off (`reasoning:true`
27
- * with no off spelling on an effort-only wire, or the anthropic adaptive family), or an unknown model whose
28
- * capped off-request came back EMPTY at the cap (it reasoned anyway). The seat then pays for the lowest
29
- * reasoning tier plus the verdict, never for an open-ended budget.
39
+ * The output-budget ALLOWANCE for a classifier request whose thinking the wire could not turn OFF: added to
40
+ * the verdict cap when the seat asks for `reasoning:"low"` instead of `"off"` — a model whose wire has no off
41
+ * spelling (`reasoning:true` on an effort-only wire with no declared off, the anthropic adaptive family) and a
42
+ * model that declares NO reasoning (the wire then carries nothing and the request is the gateway's default,
43
+ * which on a default-on gateway is a think). The seat pays for the least the wire can grade plus the verdict,
44
+ * never for an open-ended budget.
30
45
  *
31
- * 2048 is anchored twice: CC 2.1.250 `E4` adds exactly this many tokens to the verdict cap for a model in
32
- * its `rejects_disabled_thinking` family (`BHe`); and it is 2 × `MIN_THINKING_TOKENS` (1024), so on the
33
- * anthropic BUDGET path a HARD cap of 256 + 2048 clears the cap-wins skip (`budgetCapSkipsThinking`) and
34
- * hosts a legal `low` budget (`1024 ≤ budget_tokens < max_tokens`) without the brain having to raise a cap
35
- * it refuses to raise. A model that spends more than this on thinking still answers nothing — that reply
36
- * is the contract failure it always was.
46
+ * 2048 is anchored twice: CC 2.1.259 `MU` adds exactly this many tokens to the verdict cap for a model in
47
+ * its `rejects_disabled_thinking` family (2.1.250 `E4`/`BHe`); and it is 2 × `MIN_THINKING_TOKENS` (1024), so
48
+ * on the anthropic BUDGET path a HARD cap of 256 + 2048 clears the cap-wins skip (`budgetCapSkipsThinking`)
49
+ * and hosts a legal `low` budget (`1024 ≤ budget_tokens < max_tokens`) without the brain having to raise a
50
+ * cap it refuses to raise. Live 2026-09-15: deepseek's default think fits it ~90–100% of the time; the tail
51
+ * is the re-ask seat's ({@link AUTO_MODE_CLASSIFIER_THINK_BUDGET_TOKENS}). What a cap can and cannot promise is
52
+ * the seat's contract (`classifier.budget_is_room`, on `classifierRungSeat`).
37
53
  */
38
54
  export declare const AUTO_MODE_CLASSIFIER_LOW_REASONING_BUDGET_TOKENS = 2048;
55
+ /**
56
+ * The RE-ASK seat's allowance — the room a rung gets ONCE after its first attempt produced no verdict: CC
57
+ * 2.1.259's stage-2 classifier cap (`max_tokens: 8192 + bn`). Live 2026-09-15 the thinking tail that overruns
58
+ * 2048 (deepseek 1–2 of 20, some past 4096; Qwen up to 6k) sits under it. One re-ask, never a loop.
59
+ */
60
+ export declare const AUTO_MODE_CLASSIFIER_THINK_BUDGET_TOKENS = 8192;
61
+ /**
62
+ * How many CONSECUTIVE no-verdict classifications on ONE model mint `classifier.contract_broken` (once per run
63
+ * per model): one is a fluke, two is a pattern, and at three the denial limit already hands the user a card —
64
+ * the operator line lands before that one.
65
+ */
66
+ export declare const AUTO_MODE_CLASSIFIER_CONTRACT_BROKEN_AFTER = 2;
39
67
  /** The sentinel a deployment puts INSIDE a paired rule list to splice the CC default rules back in at
40
68
  * that position (CC `XYt = "$defaults"`). It lives here, beside the other defaults, because the #503
41
69
  * recipe canonicalizer needs its VALUE and must not load the assembly face (and its SHA-locked assets)
@@ -1,8 +1,11 @@
1
- export const AUTO_MODE_DEFAULT_TIMEOUT_MS = 15_000;
1
+ export const AUTO_MODE_DEFAULT_TIMEOUT_MS = 60_000;
2
+ export const AUTO_MODE_RECORDED_ABSENT_TIMEOUT_MS = 15_000;
2
3
  export const AUTO_MODE_DEFAULT_WINDOW_MAX_ENTRIES = 40;
3
4
  export const AUTO_MODE_DEFAULT_WINDOW_MAX_CHARS = 2_000;
4
5
  export const AUTO_MODE_CLASSIFIER_MAX_TOKENS = 256;
5
6
  export const AUTO_MODE_CLASSIFIER_LOW_REASONING_BUDGET_TOKENS = 2048;
7
+ export const AUTO_MODE_CLASSIFIER_THINK_BUDGET_TOKENS = 8192;
8
+ export const AUTO_MODE_CLASSIFIER_CONTRACT_BROKEN_AFTER = 2;
6
9
  export const AUTO_MODE_DEFAULTS_SENTINEL = "$defaults";
7
10
  export const AUTO_MODE_DENIAL_LIMIT_DEFAULTS = Object.freeze({ maxConsecutive: 3, maxTotal: 20 });
8
11
  export const AUTO_MODE_DENIAL_AUTO_DENY_DEFAULT_MS = 120_000;
@@ -10,12 +10,27 @@ export declare const AUTO_MODE_UNAVAILABLE_CAUSES: readonly ["error", "timeout"]
10
10
  export type AutoModeUnavailableCause = (typeof AUTO_MODE_UNAVAILABLE_CAUSES)[number];
11
11
  /** Whether a value is a member of the unavailable-cause set — the screen a row reader applies to a stored word. */
12
12
  export declare function isAutoModeUnavailableCause(v: unknown): v is AutoModeUnavailableCause;
13
+ /**
14
+ * HOW a reply failed to carry a verdict — the closed set behind `parse_error.form`. `missing` = the reader found
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 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`.
22
+ */
23
+ export declare const AUTO_MODE_PARSE_FAILURES: readonly ["missing", "conflict"];
24
+ export type AutoModeParseFailure = (typeof AUTO_MODE_PARSE_FAILURES)[number];
25
+ /** Whether a value is a member of the parse-failure set. */
26
+ export declare function isAutoModeParseFailure(v: unknown): v is AutoModeParseFailure;
13
27
  /**
14
28
  * WHY a classification moved DOWN the candidate ladder — the closed set behind
15
29
  * {@link AutoModeClassifierRound}'s `fallback.cause`, i.e. the ways a candidate produces NO RULING
16
30
  * ({@link rungDidNotRule}). Two words are the unavailable causes verbatim (the leg threw / rejected; the
17
- * round-trip cap elapsed); `parse_error` is the candidate that ANSWERED without a verdict. A ruling is not a
18
- * 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.
19
34
  */
20
35
  export declare const AUTO_MODE_FALLBACK_CAUSES: readonly ["error", "timeout", "parse_error"];
21
36
  export type AutoModeFallbackCause = (typeof AUTO_MODE_FALLBACK_CAUSES)[number];
@@ -52,10 +67,14 @@ export type AutoModeVerdict = ({
52
67
  cause: AutoModeUnavailableCause;
53
68
  } & AutoModeRoundAttributed)
54
69
  /** The model responded but not in the `<block>…` contract shape. Handled as a BLOCK with the CC
55
- * parse-failure sentence ({@link CLASSIFIER_PARSE_FAILURE_DENY_MESSAGE}), counted by the denial limit. */
70
+ * parse-failure sentence ({@link classifierParseFailureDenyMessage}), counted by the denial limit. `form`
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}). */
56
74
  | ({
57
75
  kind: "parse_error";
58
76
  raw: string;
77
+ form: AutoModeParseFailure;
59
78
  } & AutoModeRoundAttributed);
60
79
  /** The member every verdict arm shares: WHICH MODEL this classification asked and which one answered. */
61
80
  export interface AutoModeRoundAttributed {
@@ -85,20 +104,18 @@ export interface AutoModeClassifierRound {
85
104
  * the QUESTION (the transcript window every rung shares), not in a rung's answer.
86
105
  *
87
106
  * WHY IT EXISTS. The ladder advances on "the rung produced no ruling", and a hook that rejects looks exactly
88
- * like that. But a classification whose prompt could not be built fails IDENTICALLY on every rung (the build is one
89
- * shared promise, by contract — "the same prompt, re-asked" is literal), so the walk recorded a fallback
90
- * `{from: seat, to: main, cause: "error"}` and a deny sentence naming two models, for a classification in which NO
91
- * MODEL WAS EVER DISPATCHED: a transcript-store outage read, on every face a person can see, as a broken
92
- * classifier seat. The verdict is unchanged (fail-closed `unavailable`); what changes is that the record
93
- * 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.
94
112
  *
95
113
  * Carried as a `code` on the thrown error rather than a class so any classify hook — including one this
96
114
  * package never constructed — can raise it without importing a constructor.
97
115
  *
98
116
  * WHAT IT DOES NOT COVER, stated: a build that OUTRUNS a rung's cap is still recorded as that rung's
99
117
  * `timeout`. The cap bounds the whole classify call, assembly included, so at that point nothing distinguishes
100
- * a slow transcript store from a slow model. Moving the per-classification build outside the rung cap is a
101
- * seam change, filed for a later version.
118
+ * a slow transcript store from a slow model.
102
119
  */
103
120
  export declare const AUTO_MODE_ROUND_NOT_ASKED_CODE = "auto_mode.round_not_asked";
104
121
  /** Mint the marker above around whatever failed, keeping the original as `cause` for the host's own logs.
@@ -109,8 +126,25 @@ export declare const AUTO_MODE_ROUND_NOT_ASKED_CODE = "auto_mode.round_not_asked
109
126
  export declare function autoModeRoundNotAsked(reason: unknown): Error & {
110
127
  code: string;
111
128
  };
112
- /** Why a rung did not settle the classification, or `undefined` when it RULED and the ladder stops — the ONE
113
- * 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
+ */
114
148
  export declare function rungDidNotRule(v: AutoModeVerdict): AutoModeFallbackCause | undefined;
115
149
  /**
116
150
  * The model-facing deny text for an `unavailable` verdict — CC 2.1.250 `x1t`, with the parenthetical `EIt`
@@ -121,23 +155,35 @@ export declare function rungDidNotRule(v: AutoModeVerdict): AutoModeFallbackCaus
121
155
  * NOT move keeps the CC text byte for byte.
122
156
  */
123
157
  export declare function classifierUnavailableDenyMessage(toolName: string, cause: AutoModeUnavailableCause, attribution?: AutoModeClassifierRound): string;
124
- /** The model-facing deny text for a `parse_error` verdict — CC 2.1.250 `Ure` over the R3t stem: the classifier
125
- * answered, but not with a verdict, and auto mode blocks what it could not evaluate. */
126
- export declare const CLASSIFIER_PARSE_FAILURE_DENY_MESSAGE = "Auto mode could not evaluate this action and is blocking it for safety \u2014 the classifier's reply carried no verdict.";
127
158
  /**
128
- * Parse the classifier's output contract — the CC 2.1.250 verdict reader (`wCe`, its thinking strip
129
- * `xCe`, its reason reader `yCe`) ported as is:
159
+ * The model-facing deny text for a `parse_error` verdict — CC 2.1.259 `i7t` stem ("Auto mode could not evaluate
160
+ * this action and is blocking it for safety"; 2.1.250 `Ure`): the classifier answered, but not with a verdict,
161
+ * and auto mode blocks what it could not evaluate. With a candidate ladder wired the tail NAMES the model whose
162
+ * reply carried no verdict (`attribution.modelUsed`) — a declared departure from upstream, whose card names no
163
+ * model: the person reading this card is the one who can change the catalog, and on a single-model deployment
164
+ * the name is the whole diagnosis. Without attribution the sentence is unchanged. The stem is a stable prefix
165
+ * either way; one writer for both deny sites.
166
+ */
167
+ export declare function classifierParseFailureDenyMessage(attribution?: AutoModeClassifierRound): string;
168
+ /**
169
+ * Parse the classifier's output contract — the CC 2.1.259 verdict reader (`MTt`, its thinking strip `QTt`, its
170
+ * reason reader `TTt`; unchanged from 2.1.250 `wCe`/`xCe`/`yCe`), plus ONE arm of sema's own:
130
171
  * blocked: `<block>yes</block><category>Rule Name</category><reason>[Rule Name] …</reason>`
131
172
  * allowed: `<block>no</block>`
132
173
  *
133
174
  * The verdict is the set of `<block>yes|no` hits, NOT a prefix match:
134
175
  * 1. CONFLICT, read on the RAW text (thinking included): `<block>(yes|no)\b` collected over the whole
135
- * response — more than one distinct answer ⇒ no verdict (`parse_error`). A scratch-space "yes"
136
- * contradicting a final "no" is a contradiction, not an allow.
137
- * 2. HITS, read on the text with `<thinking>` blocks removed (closed ones anywhere, then an unclosed
138
- * trailing one — a reasoning model's scratch space is not part of the verdict): `<block>(yes|no)\b`
139
- * with the closing tag OPTIONAL, at ANY position. Zero hits ⇒ `parse_error` (a reply that only
140
- * answered inside its thinking has no verdict); the one distinct answer ⇒ `no` = allow, `yes` = block.
176
+ * response — more than one distinct answer ⇒ no verdict (`parse_error`, form `conflict`). A scratch-space
177
+ * "yes" contradicting a final "no" is a contradiction, not an allow.
178
+ * 2. HITS, read on the text with thinking blocks removed ({@link stripThinking}): `<block>(yes|no)\b` with
179
+ * the closing tag OPTIONAL, at ANY position. Two distinct answers ⇒ `conflict`; the one distinct answer ⇒
180
+ * `no` = allow, `yes` = block.
181
+ * 3. BARE ANSWER (sema, ruled 2026-09-15 — small open models drop the tag; recorded as a divergence; a caller
182
+ * that knows the reply is not a complete answer passes `bareAnswer: false` and this arm is closed): zero
183
+ * hits AND the whole thinking-stripped text is an answer by {@link bareAnswerOf} ⇒ that word is the
184
+ * verdict (a bare `yes` blocks with empty category/reason) — unless a `<block>` answer inside the
185
+ * thinking disagrees with it (`conflict`). Zero hits and anything else — prose, a question with its
186
+ * answer, a reply that only answered inside its thinking — ⇒ `parse_error`, form `missing`.
141
187
  * So a preamble before the tag, prose after `<block>no`, a missing `</block>` and a reply that repeats
142
188
  * its answer all read as the answer they carry; the tag must be spelled `<block>yes` / `<block>no` with
143
189
  * no whitespace inside (the word-boundary form — `<block>yesterday` is not a hit) and case does not matter.
@@ -149,11 +195,13 @@ export declare const CLASSIFIER_PARSE_FAILURE_DENY_MESSAGE = "Auto mode could no
149
195
  * line from the tool name in that case. Downgrading a stated block to `parse_error` would lose the
150
196
  * classifier's explicit verdict on the exact calls it flagged.
151
197
  *
152
- * `parse_error` is fail-closed: the gate handles it as a BLOCK (CC 2.1.250's own outcome for a reply with no
198
+ * `parse_error` is fail-closed: the gate handles it as a BLOCK (CC 2.1.259's own outcome for a reply with no
153
199
  * verdict) that the denial limit counts like any other — never a silent allow, and never a question handed
154
200
  * to a person on the classifier's behalf.
155
201
  */
156
- export declare function parseAutoModeResponse(text: string): AutoModeVerdict;
202
+ export declare function parseAutoModeResponse(text: string, options?: {
203
+ bareAnswer?: boolean;
204
+ }): AutoModeVerdict;
157
205
  /** What the decider hands the deployment's `classify` hook (the assembled prompt is the hook's job
158
206
  * when it owns assembly; when using {@link buildAutoModePrompt} the decider passes its output). */
159
207
  export interface AutoModeClassifyInput {
@@ -5,6 +5,11 @@ const AUTO_MODE_UNAVAILABLE_CAUSE_SET = new Set(AUTO_MODE_UNAVAILABLE_CAUSES);
5
5
  export function isAutoModeUnavailableCause(v) {
6
6
  return AUTO_MODE_UNAVAILABLE_CAUSE_SET.has(v);
7
7
  }
8
+ export const AUTO_MODE_PARSE_FAILURES = ["missing", "conflict"];
9
+ const AUTO_MODE_PARSE_FAILURE_SET = new Set(AUTO_MODE_PARSE_FAILURES);
10
+ export function isAutoModeParseFailure(v) {
11
+ return AUTO_MODE_PARSE_FAILURE_SET.has(v);
12
+ }
8
13
  export const AUTO_MODE_FALLBACK_CAUSES = ["error", "timeout", "parse_error"];
9
14
  const AUTO_MODE_FALLBACK_CAUSE_SET = new Set(AUTO_MODE_FALLBACK_CAUSES);
10
15
  export function isAutoModeFallbackCause(v) {
@@ -51,7 +56,7 @@ export function rungDidNotRule(v) {
51
56
  if (v.kind === "unavailable")
52
57
  return v.cause;
53
58
  if (v.kind === "parse_error")
54
- return "parse_error";
59
+ return v.form === "conflict" ? undefined : "parse_error";
55
60
  return undefined;
56
61
  }
57
62
  export function classifierUnavailableDenyMessage(toolName, cause, attribution) {
@@ -64,19 +69,83 @@ export function classifierUnavailableDenyMessage(toolName, cause, attribution) {
64
69
  "If it keeps failing, continue with other tasks that don't require this action and come back to it later. " +
65
70
  "Note: reading files, searching code, and other read-only operations do not require the classifier and can still be used.");
66
71
  }
67
- export const CLASSIFIER_PARSE_FAILURE_DENY_MESSAGE = "Auto mode could not evaluate this action and is blocking it for safety — the classifier's reply carried no verdict.";
68
- export function parseAutoModeResponse(text) {
72
+ export function classifierParseFailureDenyMessage(attribution) {
73
+ const stem = "Auto mode could not evaluate this action and is blocking it for safety";
74
+ return attribution === undefined ? `${stem} — the classifier's reply carried no verdict.` : `${stem} — the classifier model "${attribution.modelUsed}" replied without a verdict.`;
75
+ }
76
+ function stripThinking(text) {
77
+ return text.replace(/<(thinking|think)>[\s\S]*?<\/\1>/gi, "").replace(/<(thinking|think)>[\s\S]*$/i, "");
78
+ }
79
+ const BARE_ANSWER_WRAPPERS = [
80
+ ["**", "**"],
81
+ ["__", "__"],
82
+ ["*", "*"],
83
+ ["_", "_"],
84
+ ["`", "`"],
85
+ ['"', '"'],
86
+ ["'", "'"],
87
+ ["«", "»"],
88
+ ];
89
+ function unwrapBareAnswer(text) {
90
+ let s = text;
91
+ for (;;) {
92
+ const before = s;
93
+ s = s.trim().replace(/[.!]+$/, "").trim();
94
+ for (const [open, close] of BARE_ANSWER_WRAPPERS) {
95
+ if (s.length > open.length + close.length && s.startsWith(open) && s.endsWith(close)) {
96
+ s = s.slice(open.length, s.length - close.length);
97
+ break;
98
+ }
99
+ }
100
+ if (s === before)
101
+ return s;
102
+ }
103
+ }
104
+ function bareAnswerOf(text) {
105
+ const visible = text.replace(/[\u200b-\u200d\ufeff]/g, "").trim();
106
+ if (visible.includes("```") || visible.includes("\n"))
107
+ return undefined;
108
+ let s = unwrapBareAnswer(visible);
109
+ const labelled = labelledAnswer(s);
110
+ if (labelled !== undefined)
111
+ s = unwrapBareAnswer(labelled);
112
+ const word = /^(yes|no)$/i.exec(s)?.[1]?.toLowerCase();
113
+ return word === "yes" || word === "no" ? word : undefined;
114
+ }
115
+ function labelledAnswer(s) {
116
+ const label = "(?:final[ \\t]+)?(?:block|answer|verdict|decision)";
117
+ for (const [open, close] of [["", ""], ...BARE_ANSWER_WRAPPERS]) {
118
+ const re = new RegExp(`^${escapeRe(open)}${label}${escapeRe(close)}[ \\t]*[:=][ \\t]*(.*)$`, "i");
119
+ const m = re.exec(s);
120
+ if (m !== null)
121
+ return m[1];
122
+ }
123
+ return undefined;
124
+ }
125
+ function escapeRe(s) {
126
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
127
+ }
128
+ export function parseAutoModeResponse(text, options) {
69
129
  const rawAnswers = new Set([...text.matchAll(/<block>(yes|no)\b/gi)].map((m) => m[1].toLowerCase()));
70
130
  if (rawAnswers.size > 1)
71
- return { kind: "parse_error", raw: text };
72
- const stripped = text.replace(/<thinking>[\s\S]*?<\/thinking>/g, "").replace(/<thinking>[\s\S]*$/, "");
131
+ return { kind: "parse_error", raw: text, form: "conflict" };
132
+ const stripped = stripThinking(text);
73
133
  const hits = [...stripped.matchAll(/<block>(yes|no)\b(<\/block>)?/gi)];
74
- if (hits.length === 0)
75
- return { kind: "parse_error", raw: text };
76
- const answers = new Set(hits.map((m) => m[1].toLowerCase()));
77
- if (answers.size !== 1)
78
- return { kind: "parse_error", raw: text };
79
- if (!answers.has("yes"))
134
+ let answer;
135
+ if (hits.length > 0) {
136
+ const answers = new Set(hits.map((m) => m[1].toLowerCase()));
137
+ if (answers.size !== 1)
138
+ return { kind: "parse_error", raw: text, form: "conflict" };
139
+ answer = answers.has("yes") ? "yes" : "no";
140
+ }
141
+ else {
142
+ answer = options?.bareAnswer === false ? undefined : bareAnswerOf(stripped);
143
+ if (answer === undefined)
144
+ return { kind: "parse_error", raw: text, form: "missing" };
145
+ if (rawAnswers.size > 0 && !rawAnswers.has(answer))
146
+ return { kind: "parse_error", raw: text, form: "conflict" };
147
+ }
148
+ if (answer === "no")
80
149
  return { kind: "allow" };
81
150
  const category = /<category>([\s\S]*?)<\/category>/i.exec(stripped)?.[1]?.trim() ?? "";
82
151
  const reason = /<reason>([\s\S]*?)<\/reason>/.exec(stripped)?.[1]?.trim() ?? "";
@@ -369,6 +369,13 @@ export interface PendingSteerInput {
369
369
  inputId?: string;
370
370
  priority?: SystemInjectionPriority;
371
371
  }
372
+ /** True ONLY for a PLAIN data record (not null / array / class-instance / proxy-with-odd-prototype) — the only
373
+ * shape we safely digest. Adversarial args (a proxy, a getter that throws) must never crash the mint, so this +
374
+ * {@link safeDataValue}/{@link safeKeys} make {@link buildRiskDescriptor} fully TOTAL. */
375
+ export declare function isPlainRecord(x: unknown): x is Record<string, unknown>;
376
+ /** Read `obj[key]` as a DATA property only — NEVER invokes a getter/accessor (so a throwing getter can't escape
377
+ * AND a non-deterministic getter can't vary the digest); a trapped descriptor read ⇒ undefined. */
378
+ export declare function safeDataValue(obj: Record<string, unknown>, key: string): unknown;
372
379
  /**
373
380
  * design/80 §D-E: build the DETERMINISTIC, REDACTED {@link RiskDescriptor} for an escalation checkpoint at
374
381
  * MINT. Pure — a function ONLY of (`toolName`, `args`, the D-2 `safety` axis, the `shellGated` flag); it reads
@@ -73,7 +73,7 @@ export const MAX_PENDING_STEER_CHARS = 16_000;
73
73
  export const PENDING_STEER_QUEUE_BYTE_BUDGET_BYTES = 48_000;
74
74
  export const MAX_PENDING_STEER_ENTRIES = Math.floor(PENDING_STEER_QUEUE_BYTE_BUDGET_BYTES / MAX_PENDING_STEER_CHARS);
75
75
  export const PENDING_STEER_FROZEN_FIELDS = ["text", "trusted", "actor", "seq", "inputId", "priority"];
76
- function isPlainRecord(x) {
76
+ export function isPlainRecord(x) {
77
77
  try {
78
78
  if (x === null || typeof x !== "object" || Array.isArray(x))
79
79
  return false;
@@ -84,7 +84,7 @@ function isPlainRecord(x) {
84
84
  return false;
85
85
  }
86
86
  }
87
- function safeDataValue(obj, key) {
87
+ export function safeDataValue(obj, key) {
88
88
  try {
89
89
  const d = Object.getOwnPropertyDescriptor(obj, key);
90
90
  return d && "value" in d ? d.value : undefined;
@@ -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
+ }