@sema-agent/core 5.37.0 → 5.38.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 (39) hide show
  1. package/CHANGELOG.md +67 -0
  2. package/dist/agents/subagent.js +6 -0
  3. package/dist/agents/teacher.js +3 -0
  4. package/dist/agents/team.d.ts +7 -1
  5. package/dist/agents/team.js +11 -9
  6. package/dist/agents/verify.js +3 -0
  7. package/dist/core/auto-mode-prompt-assets.d.ts +5 -3
  8. package/dist/core/auto-mode-prompt-assets.js +1 -1
  9. package/dist/core/checkpoint-store.d.ts +26 -1
  10. package/dist/core/hooks.d.ts +129 -2
  11. package/dist/core/hooks.js +20 -3
  12. package/dist/core/runner/prepare-config-doors.d.ts +17 -0
  13. package/dist/core/runner/prepare-config-doors.js +33 -2
  14. package/dist/core/runner/prepare-task.d.ts +17 -2
  15. package/dist/core/runner/prepare-task.js +124 -41
  16. package/dist/core/runner/runtask.js +46 -11
  17. package/dist/core/tool-model-gate.d.ts +125 -0
  18. package/dist/core/tool-model-gate.js +303 -0
  19. package/dist/core/tool-policy.d.ts +1 -1
  20. package/dist/core/types.d.ts +189 -1
  21. package/dist/core/types.js +21 -0
  22. package/dist/core/untrusted-text.d.ts +1 -1
  23. package/dist/index.d.ts +4 -3
  24. package/dist/index.js +2 -1
  25. package/dist/orchestration/builtin-workflows.d.ts +68 -6
  26. package/dist/orchestration/builtin-workflows.js +26 -9
  27. package/dist/orchestration/run-workflow-tool.d.ts +10 -1
  28. package/dist/orchestration/run-workflow-tool.js +70 -27
  29. package/dist/orchestration/workflow-script-store.d.ts +8 -3
  30. package/dist/prompts/coordinator.d.ts +4 -1
  31. package/dist/prompts/coordinator.js +8 -0
  32. package/dist/prompts/default.d.ts +14 -4
  33. package/dist/prompts/default.js +2 -1
  34. package/dist/scenarios/full-body.d.ts +5 -0
  35. package/dist/scenarios/full-body.js +8 -4
  36. package/dist/tools/fs/fs-shared.d.ts +3 -2
  37. package/dist/tools/fs/fs-shared.js +19 -9
  38. package/package.json +1 -1
  39. package/test/export-surface.snapshot.json +12 -1
@@ -29,7 +29,7 @@ import { inlineUntrusted } from "../untrusted-text.js";
29
29
  import { policyAskClassOf } from "../ask-class.js";
30
30
  import { emitTrace } from "../trace.js";
31
31
  import { createSessionRulePolicy } from "./session-rule-policy.js";
32
- import { cloneObserverInput, createHookEnvCapabilities, createPreToolUseConstraintPolicy, formatHookFeedback, persistedRuleMandateOf, runToolGate } from "../hooks.js";
32
+ import { cloneObserverInput, createHookEnvCapabilities, createPreToolUseConstraintPolicy, formatHookFeedback, mintHookInvocationIdentity, persistedRuleMandateOf, runToolGate } from "../hooks.js";
33
33
  import { orgRuleVerdictFor } from "../permission-rule-org.js";
34
34
  import { CacheBreakDetector, toolsToFingerprintInputs } from "../cache-break-detector.js";
35
35
  import { reservedCollisions, reservedFor } from "../../brain/request-params.js";
@@ -92,13 +92,104 @@ import { countElicitOptIns, deriveWiringManifest, resolveAskSeamForm, resolveDec
92
92
  import { durableParkGapFor } from "../park-selfcheck.js";
93
93
  import { GLOBAL_USAGE_KEY, usageRetryAfterMs } from "../usage-window-store.js";
94
94
  import { deliverEngineNotice } from "../types.js";
95
- const announcedMaterializeEnv = new Set();
95
+ let announcedMaterializeEnvBySink = new WeakMap();
96
+ const announcedMaterializeEnvConsole = new Set();
97
+ function materializeEnvLedger(onNotice) {
98
+ if (typeof onNotice !== "function")
99
+ return announcedMaterializeEnvConsole;
100
+ let lines = announcedMaterializeEnvBySink.get(onNotice);
101
+ if (lines === undefined) {
102
+ lines = new Set();
103
+ announcedMaterializeEnvBySink.set(onNotice, lines);
104
+ }
105
+ return lines;
106
+ }
96
107
  export function __resetMaterializeEnvAnnouncements() {
97
- announcedMaterializeEnv.clear();
108
+ announcedMaterializeEnvBySink = new WeakMap();
109
+ announcedMaterializeEnvConsole.clear();
98
110
  }
99
111
  function emitMaterializeEnvNotice(onNotice, message, detail) {
112
+ const ledger = materializeEnvLedger(onNotice);
113
+ if (ledger.has(message))
114
+ return;
115
+ ledger.add(message);
100
116
  deliverEngineNotice(onNotice, { code: "config.materialize_env_discarded", message, detail });
101
117
  }
118
+ function warnCompactionWindowHazard(tracer, spec, model, compModel, hostTaskId) {
119
+ if (compModel === undefined)
120
+ return;
121
+ const compWindow = compModel.contextTokens ?? compModel.contextWindow;
122
+ const mainWindow = model.autoCompactTokens ?? model.contextTokens ?? model.contextWindow;
123
+ if (compWindow > 0 && mainWindow > 0 && compWindow < mainWindow) {
124
+ const merged = { ...DEFAULT_COMPACTION_SETTINGS, ...spec.compaction };
125
+ const sanitized = sanitizeCompactionSettings(merged, mainWindow);
126
+ const tolerance = sanitized.clampTolerance ?? DEFAULT_CLAMP_TOLERANCE;
127
+ const headroom = Math.max(0, compWindow - Math.max(Math.floor(0.8 * summaryOutputBudgetTokens(compModel, sanitized)), 2048) - 512);
128
+ emitTrace(tracer, () => ({
129
+ kind: "compaction.window_config_warning",
130
+ version: 1,
131
+ taskId: hostTaskId,
132
+ compactionModelWindow: compWindow,
133
+ mainModelWindow: mainWindow,
134
+ ...(tolerance < 1 ? { fallbackAt: Math.floor(headroom / (1 - tolerance)) } : {}),
135
+ ts: Date.now(),
136
+ }));
137
+ }
138
+ }
139
+ let announcedToolModelGateBySink = new WeakMap();
140
+ const announcedToolModelGateConsole = new Set();
141
+ function toolModelGateLedger(onNotice) {
142
+ if (typeof onNotice !== "function")
143
+ return announcedToolModelGateConsole;
144
+ let lines = announcedToolModelGateBySink.get(onNotice);
145
+ if (lines === undefined) {
146
+ lines = new Set();
147
+ announcedToolModelGateBySink.set(onNotice, lines);
148
+ }
149
+ return lines;
150
+ }
151
+ export function __resetToolModelGateAnnouncements() {
152
+ announcedToolModelGateBySink = new WeakMap();
153
+ announcedToolModelGateConsole.clear();
154
+ }
155
+ function announceToolModelGate(onNotice, modelId, gate) {
156
+ const ledger = toolModelGateLedger(onNotice);
157
+ for (const [gateClass, removed] of gate.removedByClass) {
158
+ const line = `Model gate: default-mounted tool(s) ${removed.map((n) => JSON.stringify(n)).join(", ")} (class ${JSON.stringify(gateClass)}) ` +
159
+ `were not mounted for model ${JSON.stringify(modelId)} — the gate table marks this model as managing multi-step work without the scaffold. ` +
160
+ `Explicitly composed tools are exempt; restore via TaskSpec.restoreGatedTools, SEMA_TOOL_MODEL_GATE=off, or RunnerDeps.toolModelGate: false.`;
161
+ if (!ledger.has(line)) {
162
+ ledger.add(line);
163
+ deliverEngineNotice(onNotice, {
164
+ code: "config.tool_model_gate_removed",
165
+ message: line,
166
+ detail: {
167
+ modelId,
168
+ gateClass,
169
+ removed: [...removed],
170
+ restore: { spec: "TaskSpec.restoreGatedTools", env: "SEMA_TOOL_MODEL_GATE=off", deps: "RunnerDeps.toolModelGate: false" },
171
+ },
172
+ });
173
+ }
174
+ }
175
+ for (const gateClass of gate.unknownClasses) {
176
+ const line = `ToolSpec.modelGate names gate class ${JSON.stringify(gateClass)}, which no row of the merged gate table defines — the tag is inert ` +
177
+ `(fail-open: the tool stays mounted). Fix the tag, or define the class via RunnerDeps.toolModelGate.classes.`;
178
+ if (!ledger.has(line)) {
179
+ ledger.add(line);
180
+ deliverEngineNotice(onNotice, { code: "config.tool_model_gate_unknown_class", message: line, detail: { gateClass } });
181
+ }
182
+ }
183
+ if (gate.discardedEnvRaw !== undefined) {
184
+ const line = `SEMA_TOOL_MODEL_GATE=${JSON.stringify(gate.discardedEnvRaw)} is not in the closed set (on|1|true|off|0|false, case-insensitive) — ` +
185
+ `not in force on this task (nothing the model gate would remove), but a task where the gate WOULD trim a default-mounted tool ` +
186
+ `will refuse to prepare under it (config.tool_model_gate_env_invalid). Fix or unset the flag.`;
187
+ if (!ledger.has(line)) {
188
+ ledger.add(line);
189
+ deliverEngineNotice(onNotice, { code: "config.tool_model_gate_env_invalid", message: line, detail: { raw: gate.discardedEnvRaw } });
190
+ }
191
+ }
192
+ }
102
193
  const readFaceClampAnnouncedSinks = new WeakSet();
103
194
  let readFaceClampConsoleAnnounced = false;
104
195
  export function __resetReadFaceClampAnnouncement() {
@@ -153,8 +244,9 @@ export { rebaseWorkspacePath, rebaseWorkspacePathAcross } from "./prepare-worksp
153
244
  function hasConversationContent(branch) {
154
245
  return branch.some((e) => e.type === "message" || e.type === "custom_message" || e.type === "compaction");
155
246
  }
156
- function isDelegatedNonForkChild(internals) {
157
- return internals?.isDelegatedChild === true && internals?.insideFork !== true;
247
+ function effectiveDelegationFacts(internals, seedIsDelegatedChild) {
248
+ const isDelegatedChild = internals?.isDelegatedChild !== undefined ? internals.isDelegatedChild === true : seedIsDelegatedChild === true;
249
+ return { isDelegatedChild, isNonForkChild: isDelegatedChild && internals?.insideFork !== true };
158
250
  }
159
251
  export function batchContextAt(messages, currentId) {
160
252
  let batch = [];
@@ -238,6 +330,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
238
330
  const doors = prepareConfigDoors({ spec, deps, sessions, resume, internals });
239
331
  spec = doors.spec;
240
332
  const { toolFaceSnapshot, promptProfile, lockedPreflight, resolvedInteractionPosture, resolvedRole, model, thinking, compModel, fableMitigations, usageWindows, brainCallGuardrailRef, brainCallGuardrailMs } = doors;
333
+ announceToolModelGate(deps.onNotice, model.id, doors.modelGate);
241
334
  const { toolEffects, egressTools, irreversibleTools, irreversibilityTier, axisExplicitNegatives, reversibilityProbes, ownToolNames } = prepareSafetyScan({ spec, deps });
242
335
  let shellGatedBash = false;
243
336
  let shellGatedMonitor = false;
@@ -249,25 +342,8 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
249
342
  const { acquired, session, conflictRef, wakeRecovered, resumeAtBeforeParentId } = await prepareAcquireReconcile({ sessions, spec, resume, toolEffects, ...(() => { const g = durableParkGapFor(deps, spec); return g !== undefined ? { durableParkGap: g } : {}; })() });
250
343
  const sessionId = acquired.sessionId;
251
344
  const hostTaskId = spec.taskId ?? sessionId;
252
- if (compModel !== undefined) {
253
- const compWindow = compModel.contextTokens ?? compModel.contextWindow;
254
- const mainWindow = model.autoCompactTokens ?? model.contextTokens ?? model.contextWindow;
255
- if (compWindow > 0 && mainWindow > 0 && compWindow < mainWindow) {
256
- const merged = { ...DEFAULT_COMPACTION_SETTINGS, ...spec.compaction };
257
- const sanitized = sanitizeCompactionSettings(merged, mainWindow);
258
- const tolerance = sanitized.clampTolerance ?? DEFAULT_CLAMP_TOLERANCE;
259
- const headroom = Math.max(0, compWindow - Math.max(Math.floor(0.8 * summaryOutputBudgetTokens(compModel, sanitized)), 2048) - 512);
260
- emitTrace(deps.tracer, () => ({
261
- kind: "compaction.window_config_warning",
262
- version: 1,
263
- taskId: hostTaskId,
264
- compactionModelWindow: compWindow,
265
- mainModelWindow: mainWindow,
266
- ...(tolerance < 1 ? { fallbackAt: Math.floor(headroom / (1 - tolerance)) } : {}),
267
- ts: Date.now(),
268
- }));
269
- }
270
- }
345
+ const delegation = effectiveDelegationFacts(internals, resume?.seed.isDelegatedChild);
346
+ warnCompactionWindowHazard(deps.tracer, spec, model, compModel, hostTaskId);
271
347
  const taskScope = internals?.registryScope ?? spec.principal ?? "default";
272
348
  internals?.peerSelfRef?.addAxis("s", sessionId);
273
349
  internals?.peerSelfRef?.addAxis("t", hostTaskId);
@@ -705,6 +781,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
705
781
  excludeTools: toolFaceSnapshot.exclude,
706
782
  deferTools: toolFaceSnapshot.defer,
707
783
  alwaysLoadTools: toolFaceSnapshot.alwaysLoad,
784
+ ...(toolFaceSnapshot.restoreGated !== undefined ? { restoreGatedTools: toolFaceSnapshot.restoreGated } : {}),
708
785
  promptProfile,
709
786
  ...(spec.additionalDirectories !== undefined ? { additionalDirectories: Object.freeze([...spec.additionalDirectories]) } : {}),
710
787
  ...(spec.additionalReadDirectories !== undefined ? { additionalReadDirectories: Object.freeze([...spec.additionalReadDirectories]) } : {}),
@@ -876,6 +953,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
876
953
  parentExcludeTools: toolFaceSnapshot.exclude,
877
954
  parentDeferTools: toolFaceSnapshot.defer,
878
955
  parentAlwaysLoadTools: toolFaceSnapshot.alwaysLoad,
956
+ ...(toolFaceSnapshot.restoreGated !== undefined ? { parentRestoreGatedTools: toolFaceSnapshot.restoreGated } : {}),
879
957
  parentPromptProfile: promptProfile,
880
958
  models: deps.models,
881
959
  agents: deps.agents,
@@ -1581,7 +1659,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
1581
1659
  orgMemoryDenied: complianceDenies.has("org_memory_mount"),
1582
1660
  complianceDegraded,
1583
1661
  parentAdmittedOrgScopes: foldAdmissionFreeze({
1584
- delegated: internals?.isDelegatedChild === true ||
1662
+ delegated: delegation.isDelegatedChild ||
1585
1663
  internals?.inheritedGate !== undefined ||
1586
1664
  (resume?.seed.inheritedGate !== undefined &&
1587
1665
  (resume.seed.inheritedGate.ancestorRules !== undefined ||
@@ -1650,7 +1728,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
1650
1728
  loaded = await Promise.resolve(deps.loadProjectMemory({
1651
1729
  cwd: taskRootFinal,
1652
1730
  handsEnabled,
1653
- isSubagent: isDelegatedNonForkChild(internals),
1731
+ isSubagent: delegation.isNonForkChild,
1654
1732
  ...(internals?.agentName ? { agentName: internals.agentName } : {}),
1655
1733
  sessionId,
1656
1734
  phase: projectMemoryPhase,
@@ -1775,7 +1853,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
1775
1853
  awarenessEnabled: thinking !== undefined && ULTRA_REASONING_TIERS.has(thinking),
1776
1854
  worktreeIsolated: internals?.isolation === "worktree" && ownedEnv !== undefined,
1777
1855
  withinTaskCompactionEnabled: (spec.compaction?.enabled ?? true) && (spec.compaction?.withinTask ?? true),
1778
- isSubagent: isDelegatedNonForkChild(internals),
1856
+ isSubagent: delegation.isNonForkChild,
1779
1857
  };
1780
1858
  const userSystemPrompt = spec.systemPrompt ?? resolvedRole.systemPrompt ?? internals?.defaultSystemPrompt;
1781
1859
  const userAppendSystemPrompt = spec.appendSystemPrompt;
@@ -2209,10 +2287,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
2209
2287
  const raw = process.env.SEMA_TOOL_MATERIALIZE_STRATEGY;
2210
2288
  if (raw !== undefined && raw !== "swap" && raw !== "static" && deferred.size === 0) {
2211
2289
  const line = `SEMA_TOOL_MATERIALIZE_STRATEGY=${JSON.stringify(raw)} is not "swap" or "static" — inert on this task (no deferred tools), but a deferring task WITHOUT an explicit spec strategy will refuse to prepare under it (an explicit legal spec outranks and discards it, loudly). Fix or unset the flag.`;
2212
- if (!announcedMaterializeEnv.has(line)) {
2213
- announcedMaterializeEnv.add(line);
2214
- emitMaterializeEnvNotice(deps.onNotice, line, { raw });
2215
- }
2290
+ emitMaterializeEnvNotice(deps.onNotice, line, { raw });
2216
2291
  }
2217
2292
  }
2218
2293
  if (deferred.size > 0) {
@@ -2231,10 +2306,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
2231
2306
  }
2232
2307
  if (envStrategyInvalid) {
2233
2308
  const line = `SEMA_TOOL_MATERIALIZE_STRATEGY=${JSON.stringify(rawEnvStrategy)} was ignored — not "swap" or "static", and the task spec pins toolMaterializeStrategy=${JSON.stringify(spec.toolMaterializeStrategy)} which outranks it. Fix or unset the env flag.`;
2234
- if (!announcedMaterializeEnv.has(line)) {
2235
- announcedMaterializeEnv.add(line);
2236
- emitMaterializeEnvNotice(deps.onNotice, line, { raw: rawEnvStrategy, specStrategy: spec.toolMaterializeStrategy });
2237
- }
2309
+ emitMaterializeEnvNotice(deps.onNotice, line, { raw: rawEnvStrategy, specStrategy: spec.toolMaterializeStrategy });
2238
2310
  }
2239
2311
  const envStrategy = envStrategyInvalid ? undefined : rawEnvStrategy;
2240
2312
  const requestedStrategy = spec.toolMaterializeStrategy ?? envStrategy ?? "swap";
@@ -2785,7 +2857,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
2785
2857
  ...((internals?.explicitAgentName ?? internals?.agentName) !== undefined
2786
2858
  ? { sourceAgentName: internals?.explicitAgentName ?? internals?.agentName }
2787
2859
  : {}),
2788
- ...(internals?.isDelegatedChild === true ? { isDelegatedChild: true } : {}),
2860
+ ...(delegation.isDelegatedChild ? { isDelegatedChild: true } : {}),
2789
2861
  });
2790
2862
  const riskAxesOf = (toolName) => {
2791
2863
  const tier = irreversibilityTier.get(toolName);
@@ -3347,6 +3419,15 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
3347
3419
  retentionPolicyWired: deps.retentionPolicy !== undefined,
3348
3420
  });
3349
3421
  const parkLaneArmed = wiringManifest.parkLane.effective === true;
3422
+ const hookIdentity = mintHookInvocationIdentity({
3423
+ sessionId,
3424
+ taskId: spec.taskId ?? sessionId,
3425
+ legKind: wiringManifest.leg.kind,
3426
+ isDelegatedChild: delegation.isDelegatedChild,
3427
+ ...(internals?.insideFork === true ? { insideFork: true } : {}),
3428
+ ...(internals?.agentName !== undefined ? { agentName: internals.agentName } : {}),
3429
+ ...(internals?.parentToolCallId !== undefined ? { parentToolCallId: internals.parentToolCallId } : {}),
3430
+ });
3350
3431
  const hookContextConsumerWired = hooks?.preToolUse !== undefined || hooks?.postToolUse !== undefined || hooks?.postToolUseFailure !== undefined;
3351
3432
  const hookEnvFace = hookContextConsumerWired && (ownedEnv ?? deps.executionEnv) != null ? createHookEnvCapabilities(executionEnv) : undefined;
3352
3433
  if (effectivePolicy || hooks?.preToolUse || egressTools.size > 0 || irreversibleTools.size > 0 || resourceSuspendEligible || platformSuspendArmed || spec.enablePlanMode === true) {
@@ -3556,6 +3637,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
3556
3637
  : undefined,
3557
3638
  gitAnnouncement: gitStatusRef.announced !== undefined ? { ...gitStatusRef.announced } : undefined,
3558
3639
  delegationProvenance: internals?.delegationProvenance !== undefined ? { ...internals.delegationProvenance.ref.current } : undefined,
3640
+ isDelegatedChild: hookIdentity.isDelegatedChild ? true : undefined,
3559
3641
  });
3560
3642
  const commitSuspendSaga = async (token, cp, remoteEnv, remoteHandle) => {
3561
3643
  if (!checkpointStore)
@@ -4155,7 +4237,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
4155
4237
  if (blockedTracked)
4156
4238
  blockedToolCalls.add(e.toolCallId);
4157
4239
  if (notifyPermissionDenied) {
4158
- await notifyPermissionDenied({ toolName: e.toolName, input: cloneObserverInput(e.input), toolCallId: e.toolCallId, reason: complianceDeny, source: "safety" });
4240
+ await notifyPermissionDenied({ toolName: e.toolName, input: cloneObserverInput(e.input), toolCallId: e.toolCallId, reason: complianceDeny, source: "safety", identity: hookIdentity });
4159
4241
  }
4160
4242
  return { block: true, reason: formatHookFeedback(complianceDeny), preToolContext: [] };
4161
4243
  }
@@ -4166,7 +4248,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
4166
4248
  const planDenyReason = `Plan mode is active — "${e.toolName}" is a write/mutating tool and is read-only-blocked. ` +
4167
4249
  `Research with read-only tools, then call ${PRESENT_PLAN_TOOL_NAME} with your plan to get it approved before acting.`;
4168
4250
  if (notifyPermissionDenied) {
4169
- await notifyPermissionDenied({ toolName: e.toolName, input: cloneObserverInput(e.input), toolCallId: e.toolCallId, reason: planDenyReason, source: "planMode" });
4251
+ await notifyPermissionDenied({ toolName: e.toolName, input: cloneObserverInput(e.input), toolCallId: e.toolCallId, reason: planDenyReason, source: "planMode", identity: hookIdentity });
4170
4252
  }
4171
4253
  return {
4172
4254
  block: true,
@@ -4179,6 +4261,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
4179
4261
  result = await runToolGate({
4180
4262
  onNotifyError: (f) => emitTrace(deps.tracer, () => ({ kind: "observer.notify_failed", version: 1, taskId: spec.taskId ?? sessionId, site: f.site, message: f.error.message, ts: Date.now() })),
4181
4263
  event: e,
4264
+ identity: hookIdentity,
4182
4265
  preToolUse: ownGatePreToolUse,
4183
4266
  ...(hookEnvFace !== undefined ? { hookEnv: hookEnvFace } : {}),
4184
4267
  adjudicate,
@@ -4291,7 +4374,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
4291
4374
  isInterrupt: abortController.signal.aborted || spec.signal?.aborted === true,
4292
4375
  content: e.content.map((c) => ({ ...c })),
4293
4376
  details: clonedDetails,
4294
- }, { toolCallId: e.toolCallId, toolName: e.toolName, ...(hookEnvFace !== undefined ? { env: hookEnvFace } : {}) });
4377
+ }, { toolCallId: e.toolCallId, toolName: e.toolName, ...(hookEnvFace !== undefined ? { env: hookEnvFace } : {}), identity: hookIdentity });
4295
4378
  if (patch?.additionalContext) {
4296
4379
  content = [...content, { type: "text", text: formatHookFeedback(patch.additionalContext) }];
4297
4380
  changed = true;
@@ -4299,7 +4382,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
4299
4382
  }
4300
4383
  }
4301
4384
  else if (hooks?.postToolUse) {
4302
- const patch = await hooks.postToolUse(e.toolName, e.input, { content: e.content, details: e.details, isError: e.isError }, { toolCallId: e.toolCallId, toolName: e.toolName, ...(hookEnvFace !== undefined ? { env: hookEnvFace } : {}) });
4385
+ const patch = await hooks.postToolUse(e.toolName, e.input, { content: e.content, details: e.details, isError: e.isError }, { toolCallId: e.toolCallId, toolName: e.toolName, ...(hookEnvFace !== undefined ? { env: hookEnvFace } : {}), identity: hookIdentity });
4303
4386
  if (patch?.updatedOutput) {
4304
4387
  content = patch.updatedOutput;
4305
4388
  changed = true;
@@ -4586,7 +4669,7 @@ export async function prepareTask(spec, deps, sessions, resume, internals, runne
4586
4669
  const effectiveReadFaceObserved = carrierReadFace();
4587
4670
  const effectiveReadDenyObserved = readDenyAdditionsNormalized.length > 0 ? readDenyAdditionsNormalized.map((e) => ({ ...e })) : undefined;
4588
4671
  const preparedHolder = {};
4589
- const buildPrepared = () => ({ harness, session, sessionId, taskRootPath: taskRootFinal, model, thinking, compModel, mcp: mcp, ...(a2a !== undefined && a2a.tools.length > 0 ? { a2a } : {}), blockedRef, outputRef, abortController, conflictRef, blockedToolCalls, approvalSettlement, nestedStats, ...(rewindNotes.length > 0 ? { rewindNotes } : {}), ...(effectiveReadFaceObserved !== undefined ? { effectiveReadFace: effectiveReadFaceObserved } : {}), ...(effectiveReadDenyObserved !== undefined ? { effectiveReadDenyPatterns: effectiveReadDenyObserved } : {}), effectiveMemoryScopes: memoryEffectiveScopes, cwdRef: handsCwdRef, ...(worktreeSessionRef !== undefined ? { worktreeSessionRef } : {}), ...(workspaceStateSettle !== undefined ? { workspaceStateSettle } : {}), denyNarrowingPolicy, ...(basePolicyForResumeEdit !== undefined ? { basePolicyForResumeEdit } : {}), ...(permissionRuleOrgLane !== undefined ? { permissionRuleOrg: permissionRuleOrgLane } : {}), releaseSignal, settleContentAskBindings, cacheBreakDetector, cacheFingerprint, wiringManifest, promptManifest, epochDeclaredSections, activeTools, ...(deferred.size > 0 ? { deferredToolNames: deferred } : {}), toolMaterializeStatic, deferDirectCall, ...(staticFaceForRef.current !== undefined ? { staticFaceFor: staticFaceForRef.current } : {}), ownedEnv, suspendRef, suspendProgressRef, reviewRef, remoteEnvFailures, reviewRequestRef, suspendLoopRef, suspendForResource, ...(suspendForPlatformLimit !== undefined ? { suspendForPlatformLimit } : {}), ...(envLifetimeSuspendAt !== undefined ? { envLifetimeSuspendAt } : {}), ...(usageGovernance !== undefined ? { usageGovernance } : {}), callIssuedAtRef, brainCallGuardrailRef, suspendForReview, resourceLedger: priorLedger, liveSpendRef, humanReviewRef, now, tools, toolEffects, wakeRecovered, promptOverheadTokens, lastBrainContext, readTaskFile, recentlyReadFiles, normalizeAttachmentPath, isDedupStubResult, ...(onCompactionApplied ? { onCompactionApplied } : {}), compactionReuseRef, trimPressureRef, ...(memoryEngineSession ? { memoryEngineSession } : {}), ...(subagentRetain ? { subagentRetain } : {}), ...(lspDiagnostics && nudgeLspOnEdit ? { lspDiagnostics: { registry: lspDiagnostics, nudge: nudgeLspOnEdit, runIdent: lspRunIdent } } : {}), planModeRef, ...(dateChange ? { dateChange } : {}), ...(instructionSources ? { instructionSources } : {}), ...(workflowSizeGuideline ? { workflowSizeGuideline } : {}), ...(detectExternalChanges ? { detectExternalChanges } : {}), ...(toolsDeltaRef ? { toolsDeltaRef } : {}), ...(agentListing ? { agentListing } : {}), ...(skillsListing ? { skillsListing } : {}), announcedListingsRef, gitStatusRef, listBackgroundTasks, ...(turnSnapshotRef.current !== undefined ? { turnSnapshot: turnSnapshotRef.current } : {}), ...(centerCompactionCandidate !== undefined ? { centerCompactionCandidate } : {}) });
4672
+ const buildPrepared = () => ({ harness, session, sessionId, taskRootPath: taskRootFinal, model, thinking, compModel, mcp: mcp, ...(a2a !== undefined && a2a.tools.length > 0 ? { a2a } : {}), blockedRef, outputRef, abortController, conflictRef, blockedToolCalls, approvalSettlement, nestedStats, ...(rewindNotes.length > 0 ? { rewindNotes } : {}), ...(effectiveReadFaceObserved !== undefined ? { effectiveReadFace: effectiveReadFaceObserved } : {}), ...(effectiveReadDenyObserved !== undefined ? { effectiveReadDenyPatterns: effectiveReadDenyObserved } : {}), effectiveMemoryScopes: memoryEffectiveScopes, cwdRef: handsCwdRef, ...(worktreeSessionRef !== undefined ? { worktreeSessionRef } : {}), ...(workspaceStateSettle !== undefined ? { workspaceStateSettle } : {}), denyNarrowingPolicy, ...(basePolicyForResumeEdit !== undefined ? { basePolicyForResumeEdit } : {}), ...(permissionRuleOrgLane !== undefined ? { permissionRuleOrg: permissionRuleOrgLane } : {}), releaseSignal, settleContentAskBindings, cacheBreakDetector, cacheFingerprint, wiringManifest, promptManifest, epochDeclaredSections, activeTools, ...(deferred.size > 0 ? { deferredToolNames: deferred } : {}), toolMaterializeStatic, deferDirectCall, ...(staticFaceForRef.current !== undefined ? { staticFaceFor: staticFaceForRef.current } : {}), ownedEnv, suspendRef, suspendProgressRef, reviewRef, remoteEnvFailures, reviewRequestRef, suspendLoopRef, suspendForResource, ...(suspendForPlatformLimit !== undefined ? { suspendForPlatformLimit } : {}), ...(envLifetimeSuspendAt !== undefined ? { envLifetimeSuspendAt } : {}), ...(usageGovernance !== undefined ? { usageGovernance } : {}), callIssuedAtRef, brainCallGuardrailRef, suspendForReview, resourceLedger: priorLedger, liveSpendRef, humanReviewRef, now, tools, toolEffects, wakeRecovered, promptOverheadTokens, lastBrainContext, readTaskFile, recentlyReadFiles, normalizeAttachmentPath, isDedupStubResult, ...(onCompactionApplied ? { onCompactionApplied } : {}), compactionReuseRef, trimPressureRef, ...(memoryEngineSession ? { memoryEngineSession } : {}), ...(subagentRetain ? { subagentRetain } : {}), ...(lspDiagnostics && nudgeLspOnEdit ? { lspDiagnostics: { registry: lspDiagnostics, nudge: nudgeLspOnEdit, runIdent: lspRunIdent } } : {}), planModeRef, ...(dateChange ? { dateChange } : {}), ...(instructionSources ? { instructionSources } : {}), ...(workflowSizeGuideline ? { workflowSizeGuideline } : {}), ...(detectExternalChanges ? { detectExternalChanges } : {}), ...(toolsDeltaRef ? { toolsDeltaRef } : {}), ...(agentListing ? { agentListing } : {}), ...(skillsListing ? { skillsListing } : {}), announcedListingsRef, gitStatusRef, listBackgroundTasks, hookIdentity, ...(turnSnapshotRef.current !== undefined ? { turnSnapshot: turnSnapshotRef.current } : {}), ...(centerCompactionCandidate !== undefined ? { centerCompactionCandidate } : {}) });
4590
4673
  const prepared = buildPrepared();
4591
4674
  preparedHolder.current = prepared;
4592
4675
  return prepared;
@@ -1,5 +1,6 @@
1
1
  import { persistedReadDenyEntryProblem } from "../../tools/fs/read-deny.js";
2
2
  import { createSafeNotifier, observeThenableRejection } from "../safe-notify.js";
3
+ import { deliverDelegationLifecycle } from "../types.js";
3
4
  import { AgentHarness, DEFAULT_COMPACTION_SETTINGS, uuidv7 } from "../../internal/harness.js";
4
5
  import { snapshotActorAssertion } from "../../internal/llm.js";
5
6
  import { CheckpointError, BINDING_CHECKPOINT_VERSION, checkpointVersionOf, F012_CHECKPOINT_VERSION, MAX_SUPPORTED_CHECKPOINT_VERSION, REAL_APPROVAL_CHECKPOINT_VERSION, FACE_CHECKPOINT_VERSION, remainingBudgetMicroUsd, readPendingSteerQueue, remainingTokens, LEGACY_PENDING_STEER_INPUT_ID, MAX_STEER_INPUT_ID_CHARS, validatePendingSteer, winnerFromOutcome, } from "../checkpoint-store.js";
@@ -757,7 +758,7 @@ function makeTurnBoundary(prepared, stats, rs, deps) {
757
758
  const injectedThisTurn = finalVerifyInjectedThisTurn ? "final_verification" : undefined;
758
759
  if (!boundarySteered && !prepared.abortController.signal.aborted) {
759
760
  try {
760
- const r = await postToolBatchHook(batch, injectedThisTurn !== undefined ? { injectedThisTurn } : undefined);
761
+ const r = await postToolBatchHook(batch, injectedThisTurn !== undefined ? { injectedThisTurn } : undefined, { identity: prepared.hookIdentity });
761
762
  if (r?.additionalContext && injectedThisTurn === undefined) {
762
763
  const body = sanitizeUntrustedText(r.additionalContext);
763
764
  const budget = ATTACHMENT_BYTE_CAP - boundaryAttachmentBytes;
@@ -1484,12 +1485,12 @@ export class Runner {
1484
1485
  ...(typeof h.preToolUse === "function" ? { preToolUse: (t, i, c) => h.preToolUse(t, i, c) } : {}),
1485
1486
  ...(typeof h.preToolUse === "function" && h.preToolUseObservational === true ? { preToolUseObservational: true } : {}),
1486
1487
  ...(typeof h.postToolUse === "function" ? { postToolUse: (t, i, o, c) => h.postToolUse(t, i, o, c) } : {}),
1487
- ...(typeof h.userPromptSubmit === "function" ? { userPromptSubmit: (p) => h.userPromptSubmit(p) } : {}),
1488
+ ...(typeof h.userPromptSubmit === "function" ? { userPromptSubmit: (p, c) => h.userPromptSubmit(p, c) } : {}),
1488
1489
  ...(typeof h.stop === "function" ? { stop: (c) => h.stop(c) } : {}),
1489
1490
  ...(typeof h.postToolUseFailure === "function"
1490
1491
  ? { postToolUseFailure: (t, i, f, c) => h.postToolUseFailure(t, i, f, c) }
1491
1492
  : {}),
1492
- ...(typeof h.postToolBatch === "function" ? { postToolBatch: (b) => h.postToolBatch(b) } : {}),
1493
+ ...(typeof h.postToolBatch === "function" ? { postToolBatch: (b, m, c) => h.postToolBatch(b, m, c) } : {}),
1493
1494
  ...(typeof h.preCompact === "function" ? { preCompact: (c) => h.preCompact(c) } : {}),
1494
1495
  ...(typeof h.postCompact === "function" ? { postCompact: (c) => h.postCompact(c) } : {}),
1495
1496
  ...(typeof h.stopFailure === "function" ? { stopFailure: (c) => h.stopFailure(c) } : {}),
@@ -1679,6 +1680,13 @@ export class Runner {
1679
1680
  durationMs: Date.now() - runStartedAt,
1680
1681
  ts: Date.now(),
1681
1682
  }));
1683
+ const owedTerminal = taskIdRef.delegationTerminalOwed;
1684
+ if (owedTerminal !== undefined) {
1685
+ taskIdRef.delegationTerminalOwed = undefined;
1686
+ deliverDelegationLifecycle(this.deps.onDelegationLifecycle, { phase: "terminal", identity: owedTerminal, status: "failed", turns: 0, ...(code !== undefined ? { errorCode: code } : {}) }, createSafeNotifier({
1687
+ onError: (f) => console.warn(`[sema-core] ${f.site}: delegation-lifecycle observer threw (contained; further failures counted, not re-disclosed): ${f.error.message}`),
1688
+ }), "runtask.onDelegationLifecycle");
1689
+ }
1682
1690
  queue.push({ type: "done", result: resultValue });
1683
1691
  queue.close();
1684
1692
  });
@@ -2037,6 +2045,20 @@ export class Runner {
2037
2045
  const ident = () => parentToolCallId !== undefined ? { eventId: uuidv7(), parentToolCallId, sourceTaskId: runSourceTaskId } : { eventId: uuidv7() };
2038
2046
  notificationIdent = ident;
2039
2047
  queue.push({ type: "wiring_manifest", manifest: prepared.wiringManifest, ...ident() });
2048
+ const delegationLifecycleNotifier = createSafeNotifier({
2049
+ onError: (f) => console.warn(`[sema-core] ${f.site}: delegation-lifecycle observer threw (contained; further failures counted, not re-disclosed): ${f.error.message}`),
2050
+ });
2051
+ const emitDelegationLifecycle = (event) => {
2052
+ if (!prepared.hookIdentity.isDelegatedChild)
2053
+ return;
2054
+ if (this.deps.onDelegationLifecycle === undefined)
2055
+ return;
2056
+ deliverDelegationLifecycle(this.deps.onDelegationLifecycle, event, delegationLifecycleNotifier, "runtask.onDelegationLifecycle");
2057
+ };
2058
+ emitDelegationLifecycle({ phase: "spawn", identity: prepared.hookIdentity });
2059
+ if (taskIdRef !== undefined && prepared.hookIdentity.isDelegatedChild && this.deps.onDelegationLifecycle !== undefined) {
2060
+ taskIdRef.delegationTerminalOwed = prepared.hookIdentity;
2061
+ }
2040
2062
  manualCompactRef.emitMooted = (reason) => {
2041
2063
  queue.push({ type: "compaction_outcome", outcome: "mooted", trigger: "manual", reason, ...ident() });
2042
2064
  };
@@ -2729,6 +2751,7 @@ export class Runner {
2729
2751
  stopHookActive: consecutiveBlocks > 0,
2730
2752
  consecutiveBlocks,
2731
2753
  getBranch: () => prepared.session.getBranch(),
2754
+ identity: prepared.hookIdentity,
2732
2755
  });
2733
2756
  }
2734
2757
  catch (err) {
@@ -2804,7 +2827,7 @@ export class Runner {
2804
2827
  ...this.seamCCompactionOptions(prepared),
2805
2828
  ...gitRestateOption(prepared),
2806
2829
  ...windowSafetyOptions(prepared.harness.getModel()),
2807
- ...this.compactionHookOptions(spec, prepared.sessionId, "forced"),
2830
+ ...this.compactionHookOptions(spec, prepared.sessionId, "forced", prepared.hookIdentity),
2808
2831
  });
2809
2832
  if (comp.compacted) {
2810
2833
  compactionBreaker.failures = 0;
@@ -2869,7 +2892,7 @@ export class Runner {
2869
2892
  runnerHooks: {
2870
2893
  onError: this.deps.onError,
2871
2894
  seamCCompactionOptions: (p) => this.seamCCompactionOptions(p),
2872
- compactionHookOptions: (s, sid, trig) => this.compactionHookOptions(s, sid, trig),
2895
+ compactionHookOptions: (s, sid, trig) => this.compactionHookOptions(s, sid, trig, prepared.hookIdentity),
2873
2896
  recordCompactionReuse: (p, c) => this.recordCompactionReuse(p, c),
2874
2897
  },
2875
2898
  });
@@ -3099,7 +3122,7 @@ export class Runner {
3099
3122
  const userPromptSubmit = (spec.hooks ?? this.deps.hooks)?.userPromptSubmit;
3100
3123
  if (userPromptSubmit) {
3101
3124
  try {
3102
- const decision = await userPromptSubmit(spec.objective);
3125
+ const decision = await userPromptSubmit(spec.objective, { identity: prepared.hookIdentity });
3103
3126
  if (decision?.block) {
3104
3127
  prepared.blockedRef.reason = formatHookFeedback(decision.block);
3105
3128
  promptBlocked = true;
@@ -3503,6 +3526,7 @@ export class Runner {
3503
3526
  result.errorCode !== "conflict") {
3504
3527
  try {
3505
3528
  await stopFailureHook({
3529
+ identity: prepared.hookIdentity,
3506
3530
  error: result.errorMessage ?? "model error",
3507
3531
  ...(result.errorCode !== undefined ? { errorKind: result.errorCode } : {}),
3508
3532
  turns: stats.turns,
@@ -3516,6 +3540,15 @@ export class Runner {
3516
3540
  }
3517
3541
  }
3518
3542
  }
3543
+ emitDelegationLifecycle({
3544
+ phase: "terminal",
3545
+ identity: prepared.hookIdentity,
3546
+ status: result.status,
3547
+ turns: stats.turns,
3548
+ ...(result.errorCode !== undefined ? { errorCode: result.errorCode } : {}),
3549
+ });
3550
+ if (taskIdRef !== undefined)
3551
+ taskIdRef.delegationTerminalOwed = undefined;
3519
3552
  if (rs.degrade.degraded)
3520
3553
  result.degraded = rs.degrade.degraded;
3521
3554
  if (prepared.outputRef.set)
@@ -4503,15 +4536,17 @@ export class Runner {
4503
4536
  consecutiveProviderReuse: prepared.compactionReuseRef.consecutive,
4504
4537
  };
4505
4538
  }
4506
- compactionHookOptions(spec, sessionId, trigger) {
4539
+ compactionHookOptions(spec, sessionId, trigger, identity) {
4507
4540
  const hooks = spec.hooks ?? this.deps.hooks;
4508
4541
  const pre = hooks?.preCompact;
4509
4542
  const post = hooks?.postCompact;
4543
+ const withIdentity = (ctx) => identity !== undefined ? { ...ctx, identity } : ctx;
4510
4544
  return {
4511
4545
  trigger,
4512
4546
  ...(pre
4513
4547
  ? {
4514
- preCompact: async (ctx) => {
4548
+ preCompact: async (rawCtx) => {
4549
+ const ctx = withIdentity(rawCtx);
4515
4550
  const report = (err) => {
4516
4551
  try {
4517
4552
  this.deps.onError?.(err, { phase: "hook", sessionId });
@@ -4535,9 +4570,9 @@ export class Runner {
4535
4570
  : {}),
4536
4571
  ...(post
4537
4572
  ? {
4538
- postCompact: async (ctx) => {
4573
+ postCompact: async (rawCtx) => {
4539
4574
  try {
4540
- await post.call(hooks, ctx);
4575
+ await post.call(hooks, withIdentity(rawCtx));
4541
4576
  }
4542
4577
  catch (err) {
4543
4578
  try {
@@ -4581,7 +4616,7 @@ export class Runner {
4581
4616
  ...(prepared.onCompactionApplied ? { onApplied: prepared.onCompactionApplied } : {}),
4582
4617
  ...this.seamCCompactionOptions(prepared),
4583
4618
  ...gitRestateOption(prepared),
4584
- ...this.compactionHookOptions(spec, prepared.sessionId, "auto"),
4619
+ ...this.compactionHookOptions(spec, prepared.sessionId, "auto", prepared.hookIdentity),
4585
4620
  });
4586
4621
  this.recordCompactionReuse(prepared, finishComp);
4587
4622
  if (finishComp.unevaluableWindow) {
@@ -0,0 +1,125 @@
1
+ /**
2
+ * design/277 — the tool-registration MODEL GATE (CC 233 counterpart, first formalized there as a
3
+ * family+version-floor table over the todo/task-board tool family).
4
+ *
5
+ * WHAT THE GATE IS: a DEFAULT-MOUNT trim, never a capability ban. A {@link ToolSpec.modelGate}
6
+ * tag declares "this entry is a default-mounted scaffold of the named class"; when the task's
7
+ * RESOLVED model id matches the class's rule below, that entry is dropped from the roster at
8
+ * prepare (true unmount — the schema never reaches `tools[]`). An EXPLICITLY composed tool is
9
+ * never tagged (the bundle only stamps its default arms), so "the user asked for it" is the
10
+ * first restore channel by construction. Restore channels beyond that:
11
+ * `TaskSpec.restoreGatedTools` (per task-tree), env `SEMA_TOOL_MODEL_GATE=off` (process),
12
+ * `RunnerDeps.toolModelGate: false` (deployment).
13
+ *
14
+ * FAIL-OPEN, BY CONTRACT (not by accident): sema is BYOM — model ids are an OPEN set, and this
15
+ * table encodes POSITIVE knowledge ("this family at this version and above manages multi-step
16
+ * work without the scaffold"). An id the table knows nothing about (deepseek-* / gpt-* / qwen-* / any
17
+ * custom name) is NOT gated: no knowledge ⇒ no trim ⇒ tool stays. The matcher's three pass-through
18
+ * edges (family absent, id shape unmatched, version below floor) all point the same way, and it is
19
+ * the only polarity compatible with BYOM — the reverse would trim every non-claude deployment's
20
+ * default face. The engine does NOT guarantee a canonical id either: a deployment-authored
21
+ * `Model.id` (a raw Model object, or a catalog key shadowing the tier expansion) that is not in
22
+ * canonical `claude-<family>-<version>` shape simply falls off the regex and stays open — the
23
+ * explicit-id discipline's deployment-side duty. To gate a non-claude strong model, write the
24
+ * exact id into a {@link ToolModelGateRule.modelIds} row (the only non-claude channel; no prefix
25
+ * or wildcard syntax exists).
26
+ *
27
+ * DIVERGENCE (registered, deliberate): CC judges the MAIN-LOOP canonical model once per process;
28
+ * sema judges the PER-TASK resolved model on every prepare — BYOM per-task models are first-class,
29
+ * so a delegated child on a different model re-judges under its own (strong parent / weak child ⇒
30
+ * the child gets the scaffold back). And the env valve's polarity is reversed (CC: enable the
31
+ * tools; sema: disable the GATE) because sema's class vocabulary is open — a per-family enable
32
+ * env would have to grow with every class.
33
+ */
34
+ import type { ToolSpec } from "./types.js";
35
+ /** The shared tail extractor (BYOM ids may carry provider prefixes — "openrouter/anthropic/claude-fable-5"):
36
+ * boundary-aware LAST path segment, case-folded. Single source for every model-family/model-id
37
+ * comparison site (`isFableFamilyModelId` consumes it too — the prompt-shape axis, semantically
38
+ * independent but sharing the one tail-extraction posture). */
39
+ export declare function modelIdTail(id: string): string;
40
+ /** One gate class's matching rule (both axes optional; a rule with BOTH axes empty gates nothing —
41
+ * the per-class OFF shape a deployment reaches by explicitly clearing the axes). */
42
+ export interface ToolModelGateRule {
43
+ /** claude-syntax family+floor rows — meaningful only for ids whose TAIL SEGMENT matches
44
+ * `claude-<letters>-<digits(-digits)*>`. Version tuples compare positionally, missing positions
45
+ * read 0, tuple ≥ floor ⇒ gated (CC 233 comparator, verbatim semantics). Any other id shape
46
+ * falls through this axis entirely (fail-open). */
47
+ floors?: ReadonlyArray<readonly [family: string, floor: ReadonlyArray<number>]>;
48
+ /** Exact-id rows (explicit-id discipline): the resolved `Model.id`'s tail segment must equal the
49
+ * row case-insensitively. The ONLY channel by which a BYOM deployment gates its own non-claude
50
+ * strong model — deliberately no prefix/wildcard syntax (a substring guess against an open id
51
+ * set is how the `deepseek-chat` alias downgrade class of accident happens). */
52
+ modelIds?: ReadonlyArray<string>;
53
+ }
54
+ /**
55
+ * The built-in gate table: gate-class → rule. CC 233 z_S floors, verbatim (evidence D2; the
56
+ * `mythos` row rides per the anchor). v1 vocabulary = ONE class, `"task-scaffold"` — the
57
+ * TodoWrite/TaskCreate/TaskGet/TaskUpdate/TaskList default bundle (`assembleCodeTools`). One class
58
+ * is evidence discipline, not mechanism limit: a family enters this table only with positive
59
+ * "strong models don't need it" evidence (a CC anchor or deployment measurement); classes are an
60
+ * open vocabulary and a deployment adds its own via `RunnerDeps.toolModelGate.classes`.
61
+ */
62
+ export declare const TOOL_MODEL_GATE_CLASSES: Readonly<Record<string, ToolModelGateRule>>;
63
+ /**
64
+ * Pure matcher: is `modelId` gated under `rule`? Every unmatched shape returns `false` (the
65
+ * fail-open contract in the module header). Exported for deployment pre-flight ("would my model
66
+ * lose the scaffold?") — the engine's own decision point calls this same function.
67
+ */
68
+ export declare function isModelGatedForClass(modelId: string, rule: ToolModelGateRule): boolean;
69
+ /**
70
+ * #123 value screen for `TaskSpec.restoreGatedTools` (same posture as `assertReadFaceValue`: the
71
+ * VALUE is screened at the door, unconditionally — a garbage restore list must refuse loudly on
72
+ * every leg, never be silently read as "no restore" in the narrowing direction). Legal: absent,
73
+ * literal `true` (restore every class), or an array of strings (wire names; unknown names are
74
+ * inert by contract, like `excludeTools`).
75
+ */
76
+ export declare function assertRestoreGatedToolsValue(value: unknown): asserts value is true | readonly string[] | undefined;
77
+ /** Doors-facing decision (built + applied inside `prepareConfigDoors`' synchronous stretch —
78
+ * decision and application share one atomic window, no drift gap). */
79
+ export interface ToolModelGateDecision {
80
+ /** Present ⇔ at least one entry was removed: the fresh, PRIVATE survivor array `spec.tools` is
81
+ * rebound to (a caller mutating its original live array afterwards can neither re-add a removed
82
+ * entry nor displace anything — there are no indices left to misalign). `undefined` = no
83
+ * removal, the spec is left untouched. */
84
+ survivors: ToolSpec[] | undefined;
85
+ /** gate-class → removed wire names (sorted, unique) — the removal-notice payload. */
86
+ removedByClass: ReadonlyMap<string, readonly string[]>;
87
+ /** Stamped classes with no row in the merged table — announce-once material (the loud half of
88
+ * fail-open: a tag typo must not silently become "never gated" with nobody told). */
89
+ unknownClasses: readonly string[];
90
+ /** An env value outside the closed set that was NOT in force (nothing this prepare would gate) —
91
+ * discarded-value announce material. In-force garbage never lands here: it throws. */
92
+ discardedEnvRaw: string | undefined;
93
+ }
94
+ /**
95
+ * The single decision point, called from `prepareConfigDoors` with the model id read ONCE into the
96
+ * decision (the Model object is mutable; decision and later reads must not diverge) and the frozen
97
+ * restore snapshot (never the live spec).
98
+ *
99
+ * Channel semantics (a UNION of loosening channels — any hit ⇒ don't trim; the order below is
100
+ * evaluation order, not priority):
101
+ * - `depsSeat === false` — deployment kill switch; nothing is scanned.
102
+ * - env `SEMA_TOOL_MODEL_GATE` `off|0|false` (case-folded) — process kill switch; `on|1|true` is
103
+ * the explicit default (no-op). A value outside the closed set follows the loud-bad-value
104
+ * dialect split (#123, the SEMA_TOOL_MATERIALIZE_STRATEGY precedent): where the value is IN
105
+ * FORCE — its reading would change this prepare's outcome, i.e. after folding the other
106
+ * channels there remains a stamped entry the gate would remove — the prepare REFUSES
107
+ * (`config.tool_model_gate_env_invalid`; a mistyped `off` must not silently select the
108
+ * narrowing arm). Everywhere else it is a DISCARDED value: announced once per process, never a
109
+ * veto of the configuration that outranks it.
110
+ * - `restoreGated === true` — every class exempt for this task; an array exempts the WHOLE class
111
+ * of any stamped tool it names (CC "opting into any tool of the family restores the family",
112
+ * judged against the FULL stamp set — a name `excludeTools` will later remove still selects its
113
+ * class here, while the exclusion itself stands downstream; unknown names inert).
114
+ * - a stamped class with no merged row ⇒ kept + reported in `unknownClasses` (fail-open, loud).
115
+ * - a merged rule with both axes empty ⇒ kept, silent (the documented per-class OFF).
116
+ * - the exclusion valve (`excludeTools`, applied downstream at the roster splice) ALWAYS wins in
117
+ * the result: no restore channel resurrects an excluded name.
118
+ */
119
+ export declare function applyToolModelGate(input: {
120
+ tools: ReadonlyArray<ToolSpec> | undefined;
121
+ modelId: string;
122
+ depsSeat: unknown;
123
+ restoreGated: true | readonly string[] | undefined;
124
+ envRaw: string | undefined;
125
+ }): ToolModelGateDecision;