@sema-agent/core 7.42.1 → 7.44.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 (115) hide show
  1. package/CHANGELOG.md +173 -0
  2. package/dist/agents/send-message-tool.js +1 -0
  3. package/dist/agents/subagent.d.ts +3 -1
  4. package/dist/agents/subagent.js +5 -3
  5. package/dist/agents/teacher.js +1 -1
  6. package/dist/agents/verify.js +2 -0
  7. package/dist/brain/errors.d.ts +23 -2
  8. package/dist/brain/errors.js +26 -2
  9. package/dist/brain/stream-engine.d.ts +2 -1
  10. package/dist/brain/stream-engine.js +10 -9
  11. package/dist/core/allow-entry.d.ts +50 -31
  12. package/dist/core/allow-entry.js +100 -37
  13. package/dist/core/ask-origin.d.ts +16 -5
  14. package/dist/core/ask-origin.js +11 -3
  15. package/dist/core/ask-unresolvable-notice.d.ts +3 -2
  16. package/dist/core/ask-unresolvable-notice.js +1 -1
  17. package/dist/core/auto-mode.d.ts +5 -4
  18. package/dist/core/auto-mode.js +3 -1
  19. package/dist/core/checkpoint-store.d.ts +44 -20
  20. package/dist/core/checkpoint-store.js +10 -16
  21. package/dist/core/child-clamp-seats.d.ts +15 -1
  22. package/dist/core/child-clamp-seats.js +6 -2
  23. package/dist/core/classifier-unavailable-arm.d.ts +42 -23
  24. package/dist/core/classifier-unavailable-arm.js +14 -3
  25. package/dist/core/decision-receipt.js +2 -0
  26. package/dist/core/engine-notice.d.ts +16 -14
  27. package/dist/core/engine-notice.js +4 -3
  28. package/dist/core/gate-lanes.js +37 -29
  29. package/dist/core/governance-codes.d.ts +4 -3
  30. package/dist/core/governance-codes.js +1 -0
  31. package/dist/core/hooks.d.ts +6 -1
  32. package/dist/core/hooks.js +3 -0
  33. package/dist/core/memory-engine/engine.d.ts +2 -2
  34. package/dist/core/memory-engine/engine.js +7 -14
  35. package/dist/core/memory-engine/file-backend.d.ts +2 -2
  36. package/dist/core/memory-engine/file-backend.js +5 -7
  37. package/dist/core/memory-engine/layout.d.ts +37 -4
  38. package/dist/core/memory-engine/layout.js +34 -5
  39. package/dist/core/mode-policy.d.ts +5 -4
  40. package/dist/core/mode-policy.js +4 -0
  41. package/dist/core/permission-rule-consent.d.ts +3 -2
  42. package/dist/core/permission-rule-consent.js +25 -20
  43. package/dist/core/permission-rule-model.d.ts +47 -7
  44. package/dist/core/permission-rule-model.js +80 -6
  45. package/dist/core/permission-rules.d.ts +8 -2
  46. package/dist/core/permission-rules.js +26 -9
  47. package/dist/core/runner/classifier-ladder.d.ts +10 -8
  48. package/dist/core/runner/classifier-ladder.js +3 -1
  49. package/dist/core/runner/contracts.d.ts +1 -16
  50. package/dist/core/runner/denial-limit-arms.d.ts +25 -8
  51. package/dist/core/runner/denial-limit-arms.js +24 -10
  52. package/dist/core/runner/gate-exit.js +2 -1
  53. package/dist/core/runner/permission-rule-lanes.d.ts +2 -2
  54. package/dist/core/runner/permission-rule-lanes.js +8 -2
  55. package/dist/core/runner/prepare-ask-lane.d.ts +3 -0
  56. package/dist/core/runner/prepare-ask-lane.js +2 -1
  57. package/dist/core/runner/prepare-caps-and-workflow.d.ts +2 -3
  58. package/dist/core/runner/prepare-caps-and-workflow.js +3 -2
  59. package/dist/core/runner/prepare-config-doors.d.ts +3 -0
  60. package/dist/core/runner/prepare-config-doors.js +6 -5
  61. package/dist/core/runner/prepare-gate-stations.js +1 -1
  62. package/dist/core/runner/prepare-inherited-gate.d.ts +7 -5
  63. package/dist/core/runner/prepare-inherited-gate.js +5 -3
  64. package/dist/core/runner/prepare-park-ask.d.ts +4 -4
  65. package/dist/core/runner/prepare-park-ask.js +2 -4
  66. package/dist/core/runner/prepare-policy-chain.d.ts +7 -1
  67. package/dist/core/runner/prepare-policy-chain.js +59 -38
  68. package/dist/core/runner/prepare-project-context.js +6 -2
  69. package/dist/core/runner/prepare-task.js +6 -6
  70. package/dist/core/runner/prepare-wiring-manifest.d.ts +6 -5
  71. package/dist/core/runner/prepare-wiring-manifest.js +8 -9
  72. package/dist/core/runner/resume-checkpoint-screen.js +1 -1
  73. package/dist/core/runner/runtask.js +1 -5
  74. package/dist/core/runner/steer-admission.d.ts +10 -11
  75. package/dist/core/runner/steer-admission.js +6 -8
  76. package/dist/core/runner/stream-steer-verb.d.ts +5 -4
  77. package/dist/core/runner/stream-steer-verb.js +6 -8
  78. package/dist/core/runner/synthetic-tools.d.ts +3 -2
  79. package/dist/core/runner/synthetic-tools.js +1 -1
  80. package/dist/core/session-policy-store.d.ts +3 -3
  81. package/dist/core/session-policy-store.js +18 -4
  82. package/dist/core/skill-grant-binding.d.ts +44 -0
  83. package/dist/core/skill-grant-binding.js +22 -0
  84. package/dist/core/skill-grant.d.ts +59 -28
  85. package/dist/core/skill-grant.js +96 -23
  86. package/dist/core/steer-replay.d.ts +46 -0
  87. package/dist/core/steer-replay.js +22 -0
  88. package/dist/core/task-event.d.ts +3 -2
  89. package/dist/core/task-spec.d.ts +14 -1
  90. package/dist/core/task-stream.d.ts +12 -6
  91. package/dist/core/tighten-task-spec.d.ts +2 -0
  92. package/dist/core/tighten-task-spec.js +3 -0
  93. package/dist/core/tool-errors.d.ts +2 -2
  94. package/dist/core/tool-name-aliases.d.ts +35 -5
  95. package/dist/core/tool-name-aliases.js +29 -4
  96. package/dist/core/tool-policy.d.ts +10 -0
  97. package/dist/core/tool-policy.js +9 -2
  98. package/dist/core/tool-spec.d.ts +7 -0
  99. package/dist/core/types.d.ts +2 -2
  100. package/dist/core/types.js +1 -1
  101. package/dist/core/wiring-manifest.d.ts +56 -14
  102. package/dist/core/wiring-manifest.js +29 -4
  103. package/dist/index.d.ts +6 -4
  104. package/dist/index.js +5 -4
  105. package/dist/orchestration/run-workflow-tool.d.ts +4 -0
  106. package/dist/orchestration/run-workflow-tool.js +1 -1
  107. package/dist/orchestration/workflow-primitives.d.ts +3 -1
  108. package/dist/orchestration/workflow-primitives.js +6 -6
  109. package/dist/orchestration/workflow-types.d.ts +5 -0
  110. package/dist/orchestration/workflow.d.ts +3 -2
  111. package/dist/orchestration/workflow.js +14 -6
  112. package/package.json +1 -1
  113. package/test/export-surface.snapshot.json +21 -5
  114. package/wire-changes/core@7.43.0.json +985 -0
  115. package/wire-changes/core@7.44.0.json +469 -0
@@ -2,7 +2,8 @@ import type { PermissionResult, ToolCallRequest } from "./tool-policy.js";
2
2
  import type { PermissionMode } from "./ask-origin.js";
3
3
  import { type PermissionSeatInputs, type PermissionSeatReading } from "./mode-policy.js";
4
4
  import type { AgentTool } from "../internal/harness-types.js";
5
- import { type AllowEntryGateSeats, type AllowEntryInertReason, type AllowEntryJudgeContext, type CompiledAllowEntry } from "./allow-entry.js";
5
+ import type { SkillGrantBindingMembers } from "./skill-grant-binding.js";
6
+ import { type AllowEntryGateSeats, type AllowEntryInert, type AllowEntryJudgeContext, type CompiledAllowEntry } from "./allow-entry.js";
6
7
  /** Who wrote a row: the `Skill` tool on a successful activation, prepare for a skill the person invoked by name, or a
7
8
  * delegated child's seed from its parent. */
8
9
  export declare const SKILL_GRANT_ORIGINS: readonly ["model", "user", "inherited"];
@@ -25,12 +26,12 @@ export interface SkillGrantRow {
25
26
  readonly compiled: CompiledAllowEntry;
26
27
  }
27
28
  /** An entry the grammar reads as granting or refusing nothing. */
28
- export interface SkillInertEntry {
29
+ export type SkillInertEntry = {
29
30
  readonly skill: string;
30
31
  readonly list: "allowed-tools" | "disallowed-tools";
31
- readonly entry: string;
32
- readonly reason: AllowEntryInertReason;
33
- }
32
+ } & AllowEntryInert;
33
+ /** The names the run mounts, read when a list is — a skill is activated later than prepare, and the roster is read as it stands. */
34
+ export type MountedToolNames = () => ReadonlySet<string>;
34
35
  /** The two lists a skill declares (the shape of a `SkillSpec`, read by name). */
35
36
  export interface SkillGrantSource {
36
37
  readonly name: string;
@@ -45,23 +46,19 @@ export interface SkillGrantSource {
45
46
  * the person's invoked names are read through it ({@link seedSkillGrants}).
46
47
  */
47
48
  export declare function skillNamedBy<T extends Pick<SkillGrantSource, "name">>(args: unknown, skills: readonly T[]): T | undefined;
48
- /**
49
- * The digest of what a Skill card disclosed — `Execute skill: <name>` and the lists approving it releases: the skill's name
50
- * as the table spells it ({@link skillNamedBy} resolved it, so it equals the call's `skill` exactly) and its two lists in the
51
- * rule grammar's canonical spelling (`Tool()` / `Tool(*)` read as the bare name), de-duplicated and sorted, hashed as the
52
- * approval binding is hashed (`boundInputHashOf`). `undefined` — no skill named — digests as a nameless skill declaring
53
- * nothing (activating it grants nothing). Any change to the name or to either list changes it; the body does not. The read
54
- * point a parked Skill approval binds to (stored beside the row's input binding, compared before the resumed activation).
55
- */
49
+ /** The digest of what a Skill card disclosed — `Execute skill: <name>` and the lists approving it releases ({@link skillGrantBinding}). The read point a parked Skill approval binds to (stored beside the row's input binding, compared before the resumed activation). */
56
50
  export declare function skillGrantDigest(skill: Pick<SkillGrantSource, "name" | "allowedTools" | "disallowedTools"> | undefined): string;
57
51
  /**
58
- * The digest a parked call to the engine's `Skill` tool binds — written by the park beside the row's input binding — or
59
- * undefined for any other call, and when no skill is loaded (no engine `Skill` tool is mounted, so a call by that name is
60
- * not an activation).
52
+ * The card members of a call to the engine's `Skill` tool — the ONE factory every ask spreads them from: the live
53
+ * `AskRequest` an approver is handed, an ancestor's, the parked row and (through the row) the inbox projection.
54
+ * `boundGrants` is the skill's name and canonical lists, `boundGrantDigest` its hash — the same value, so what a person was
55
+ * shown and what an approving resume is judged by cannot differ. The binding is a property of the CALL, whoever asked and
56
+ * however the question was worded. Empty for any other call, and when no skill is loaded (no engine `Skill` tool is
57
+ * mounted, so a call by that name is not an activation).
61
58
  */
62
- export declare function skillCallGrantDigest(call: {
59
+ export declare function skillGrantBindingMembers(call: {
63
60
  readonly toolName: string;
64
- }, args: unknown, skills: readonly SkillGrantSource[], skillToolName: string): string | undefined;
61
+ }, args: unknown, skills: readonly SkillGrantSource[], skillToolName: string): SkillGrantBindingMembers;
65
62
  /**
66
63
  * The approving resume of a parked call, judged against the card it was asked under. A row is a `Skill` approval when it
67
64
  * carries a digest (the park said so), or when the resume would run the engine's `Skill` tool on it (skills loaded, the
@@ -87,10 +84,10 @@ export declare function declaresSkillLists(skill: Pick<SkillGrantSource, "allowe
87
84
  /**
88
85
  * Compile one skill's two lists into rows (`origin` stamped on each) and the entries that read as nothing. An allow list
89
86
  * holding `*` voids that list whole (every other entry on it is reported too: the list grants nothing — the safe direction).
90
- * A `*` on the deny list reads as nothing by itself and the rest of that list still refuses: a deny list never loses an
91
- * entry to a wildcard beside it.
87
+ * A `*` on the deny list reads as nothing by itself, like any deny entry that does — and a skill whose deny list holds one
88
+ * is not written at all ({@link SkillGrantTable.activate}).
92
89
  */
93
- export declare function compileSkillGrant(skill: SkillGrantSource, origin: SkillGrantOrigin): {
90
+ export declare function compileSkillGrant(skill: SkillGrantSource, origin: SkillGrantOrigin, roster?: ReadonlySet<string>): {
94
91
  rows: SkillGrantRow[];
95
92
  inert: SkillInertEntry[];
96
93
  };
@@ -98,8 +95,10 @@ export declare function compileSkillGrant(skill: SkillGrantSource, origin: Skill
98
95
  export interface SkillGrantHit {
99
96
  readonly skill: string;
100
97
  readonly entries: readonly string[];
98
+ /** On a refusal only: the call activates `skill`, whose `disallowed-tools` entries `entries` read as nothing (why, each). */
99
+ readonly readsAsNothing?: readonly AllowEntryInert[];
101
100
  }
102
- /** The refusal a deny row writes. */
101
+ /** The refusal a deny hit writes: a deny row's, or an activation's whose deny entries read as nothing. */
103
102
  export declare function skillDenyDecision(hit: SkillGrantHit): PermissionResult;
104
103
  /** The run's deny rows over `req` through `lane` — the refusal they write, or undefined (no lane, no row). The one
105
104
  * reading the gate's regular decision, its approved-edit round and the resumed execution boundary share. */
@@ -111,12 +110,13 @@ export declare function skillAllowDecision(hit: SkillGrantHit, updatedInput: unk
111
110
  * releases anything — the entries it releases for the rest of the run in this mode (an entry that grants nothing, or
112
111
  * stands down in this mode, is not named). Deployment text, sanitized and capped.
113
112
  */
114
- export declare function skillActivationQuestion(skill: SkillGrantSource, mode: PermissionMode | undefined): string;
113
+ export declare function skillActivationQuestion(skill: SkillGrantSource, mode: PermissionMode | undefined, roster?: ReadonlySet<string>): string;
115
114
  /** The face the gate and the resumed execution boundary read the table through, bound to the leg's call facts. */
116
115
  export interface SkillGrantLane {
117
116
  /** The allow layer's skill member: what grants this call in this mode, or undefined. */
118
117
  allowing(req: ToolCallRequest, mode: PermissionMode | undefined): SkillGrantHit | undefined;
119
- /** The run's deny rows: what refuses this call, or undefined. */
118
+ /** What refuses this call, or undefined: the run's deny rows, else — for a call to the `Skill` tool — a named skill whose
119
+ * `disallowed-tools` holds an entry that reads as nothing (`readsAsNothing`). */
120
120
  denying(req: ToolCallRequest): SkillGrantHit | undefined;
121
121
  }
122
122
  /** The gate inputs the run's skill rows ride (a member group of `ToolGateInput`). */
@@ -141,7 +141,7 @@ export interface SkillGateSeats {
141
141
  /** The `Skill` tool's per-call seat over the loaded skills: a skill that declares a list states no permission of its own. */
142
142
  export type SkillSeatReader = (args: unknown, mode: PermissionMode | undefined) => PermissionSeatReading | undefined;
143
143
  /** The reader over these skills (`undefined` when none is loaded): `Execute skill: <name>` for a skill declaring a list. */
144
- export declare function skillSeatReader(skills: readonly SkillGrantSource[]): SkillSeatReader | undefined;
144
+ export declare function skillSeatReader(skills: readonly SkillGrantSource[], mounted?: MountedToolNames): SkillSeatReader | undefined;
145
145
  /**
146
146
  * The gate's seat inputs for one call, composed in ONE place: the mounted instance's static seat (`calledName`), the `Skill`
147
147
  * tool's per-call reader in the leg's live mode (only when the call's wire name is the engine's `Skill` — `skillToolName`),
@@ -162,7 +162,8 @@ export declare function skillGateWiring(opts: {
162
162
  * parent's capture, held until this child ends, and a resumed leg's frozen rows, which keep the word `model` where the
163
163
  * model wrote them) and the skills the person invoked by name (`user` — the invocation is the statement, so no card). Every invoked
164
164
  * name must be one of the loaded skills; anything else (or a value that is not a list of names) refuses the run loudly
165
- * (`config.user_invoked_skill_unknown`), never dropped. Returns the entries the grammar reads as nothing, across every
165
+ * (`config.user_invoked_skill_unknown`), never dropped; an invoked skill, or a carried deny record, whose deny entry reads as
166
+ * nothing refuses it too ({@link SKILL_DENY_UNREADABLE_CODE}). Returns the entries the grammar reads as nothing, across every
166
167
  * loaded skill — the start-of-run notice's content.
167
168
  */
168
169
  export declare function seedSkillGrants(table: SkillGrantTable, opts: {
@@ -170,23 +171,50 @@ export declare function seedSkillGrants(table: SkillGrantTable, opts: {
170
171
  userInvokedSkills: unknown;
171
172
  inherited: readonly SkillGrantRecord[] | undefined;
172
173
  }): readonly SkillInertEntry[];
174
+ /**
175
+ * The user-invoked half of {@link seedSkillGrants}, read where no roster exists yet — a resume's pre-claim screen, over the
176
+ * re-supplied config: the invoked names seeded exactly as prepare seeds them, on a table told no mounted names, so only what
177
+ * every roster refuses alike refuses here (a name the config's skills do not carry; a deny entry the grammar alone reads as
178
+ * nothing). A name the leg's roster reads as stale or a near miss is prepare's to refuse; any other throw (a value prepare
179
+ * reports its own way) is left to prepare.
180
+ */
181
+ export declare function screenUserInvokedSkills(config: {
182
+ readonly skills?: readonly SkillGrantSource[];
183
+ readonly userInvokedSkills?: unknown;
184
+ }): void;
173
185
  /** The per-task table. Identity is the contract: the `Skill` tool, prepare's seeds, the tool ctx's snapshot and the
174
186
  * lane all hold this one object. */
175
187
  export declare class SkillGrantTable {
176
188
  private rowsHeld;
189
+ private readonly rowKeys;
190
+ private mounted;
191
+ private activations;
192
+ /** State where the run's mounted names are read (prepare, once the roster is assembled): a skill's entries are read against
193
+ * them when seeded and whenever the skill is activated. A table never told reads the grammar alone. */
194
+ bindMountedNames(mounted: MountedToolNames): void;
195
+ /** State the skills the run's `Skill` tool activates (the list it serves) and its wire name, so a call naming one whose deny
196
+ * entries read as nothing is refused where the deny rows are read. A table never told refuses such a skill at activation only. */
197
+ bindSkills(skills: readonly SkillGrantSource[], skillToolName: string): void;
198
+ /** The names the run mounts as they stand, or undefined when none were stated. */
199
+ mountedNames(): ReadonlySet<string> | undefined;
177
200
  /** The table never holds the same row twice (skill, origin, list, entry) — one rule for both writers: a re-activation, a
178
201
  * capture re-supplied beside the frozen rows that already carry it. Keyed on the ROW, not the skill: a definition that
179
202
  * changed across a park writes its new rows when it is activated again. */
180
203
  private hold;
181
- /** Write a skill's rows (rows already held are not held twice). Returns the entries that read as nothing. */
204
+ /** Write a skill's rows (rows already held are not held twice) — or none, refused with {@link SKILL_DENY_UNREADABLE_CODE},
205
+ * when its `disallowed-tools` holds an entry that reads as nothing. Returns the allow entries that read as nothing. */
182
206
  activate(skill: SkillGrantSource, origin: SkillGrantOrigin): readonly SkillInertEntry[];
183
207
  /** Seed portable records (a delegated child's capture, a resumed leg's frozen rows): a record the model wrote keeps that
184
- * word, every other record seeds under `origin` — `user` is written only from this leg's own spec, never seeded. */
208
+ * word, every other record seeds under `origin` — `user` is written only from this leg's own spec, never seeded. Each
209
+ * record is read as its writer read it, against the leg's roster: an allow record that reads as nothing grants nothing
210
+ * (not held); a deny record that does refuses the seed ({@link SKILL_DENY_UNREADABLE_CODE}) and nothing is held. */
185
211
  seed(records: readonly SkillGrantRecord[], origin: SkillGrantOrigin): void;
186
212
  /** The rows as portable records (a copy). */
187
213
  records(): readonly SkillGrantRecord[];
188
214
  /** The lane over this table, bound to the leg's call facts. */
189
215
  lane(ctx: AllowEntryJudgeContext): SkillGrantLane;
216
+ /** A call to the `Skill` tool naming a skill whose deny entries read as nothing, read as the activation would read it. */
217
+ private activationRefused;
190
218
  }
191
219
  /** Read a delegated child's capture ({@link readCarriedRecords}): `undefined` refuses the delegation. */
192
220
  export declare function readSkillGrantCapture(snapshot: readonly unknown[] | undefined): readonly SkillGrantRecord[] | undefined;
@@ -198,3 +226,6 @@ export declare function frozenSkillGrants(table: Pick<SkillGrantTable, "records"
198
226
  export declare function readFrozenSkillGrants(section: unknown): readonly SkillGrantRecord[] | undefined;
199
227
  /** The refusal for a `TaskSpec.userInvokedSkills` name the run does not carry (or a value that is not a list of names). */
200
228
  export declare const USER_INVOKED_SKILL_UNKNOWN_CODE = "config.user_invoked_skill_unknown";
229
+ /** The refusal for a skill whose `disallowed-tools` holds an entry that reads as nothing: the activation, or the leg that
230
+ * would carry the skill's deny record, does not happen. */
231
+ export declare const SKILL_DENY_UNREADABLE_CODE = "config.skill_deny_unreadable";
@@ -3,25 +3,32 @@ import { parsePermissionRule } from "./permission-rules.js";
3
3
  import { mountedToolPermissionSeat } from "./mode-policy.js";
4
4
  import { inlineUntrusted } from "./untrusted-text.js";
5
5
  import { boundInputHashOf } from "./canonical-json.js";
6
- import { allowingRows, autoStripped, compileAllowEntry, compileAllowList, judgeAllowEntries } from "./allow-entry.js";
6
+ import { allowingRows, autoStripped, compileAllowEntry, compileAllowList, inertEntryPhrase, judgeAllowEntries } from "./allow-entry.js";
7
7
  export const SKILL_GRANT_ORIGINS = Object.freeze(["model", "user", "inherited"]);
8
8
  const FROZEN_SKILL_GRANT_ORIGINS = new Set(["model", "inherited"]);
9
9
  export function skillNamedBy(args, skills) {
10
10
  const named = args?.skill;
11
11
  return typeof named === "string" ? skills.find((s) => s.name === named) : undefined;
12
12
  }
13
- export function skillGrantDigest(skill) {
13
+ function skillGrantBinding(skill) {
14
14
  const canonical = (entries) => [...new Set((entries ?? []).map((e) => {
15
15
  const p = parsePermissionRule(e);
16
16
  return p.ruleContent === undefined ? p.toolName : `${p.toolName}(${p.ruleContent})`;
17
17
  }))].sort();
18
- return boundInputHashOf({ skill: skill?.name ?? null, allowedTools: canonical(skill?.allowedTools), disallowedTools: canonical(skill?.disallowedTools) });
18
+ const boundGrants = { skill: skill?.name ?? null, allowedTools: canonical(skill?.allowedTools), disallowedTools: canonical(skill?.disallowedTools) };
19
+ return { boundGrantDigest: boundInputHashOf(boundGrants), boundGrants };
20
+ }
21
+ export function skillGrantDigest(skill) {
22
+ return skillGrantBinding(skill).boundGrantDigest;
19
23
  }
20
- export function skillCallGrantDigest(call, args, skills, skillToolName) {
21
- return skills.length > 0 && wireOf("skillGrant.binding", call) === skillToolName ? skillGrantDigest(skillNamedBy(args, skills)) : undefined;
24
+ export function skillGrantBindingMembers(call, args, skills, skillToolName) {
25
+ return isSkillActivationCall(call, skills, skillToolName) ? skillGrantBinding(skillNamedBy(args, skills)) : {};
26
+ }
27
+ function isSkillActivationCall(call, skills, skillToolName) {
28
+ return skills.length > 0 && wireOf("skillGrant.binding", call) === skillToolName;
22
29
  }
23
30
  export function parkedSkillApprovalRefusal(row, args, skills, skillToolName, site = "claim") {
24
- if (row.boundGrantDigest === undefined && skillCallGrantDigest(row, args, skills, skillToolName) === undefined)
31
+ if (row.boundGrantDigest === undefined && skillGrantBindingMembers(row, args, skills, skillToolName).boundGrantDigest === undefined)
25
32
  return undefined;
26
33
  if (skillGrantDigest(skillNamedBy(args, skills)) === row.boundGrantDigest)
27
34
  return undefined;
@@ -43,18 +50,18 @@ export function detachSkillLists(skills) {
43
50
  export function declaresSkillLists(skill) {
44
51
  return (skill.allowedTools?.length ?? 0) > 0 || (skill.disallowedTools?.length ?? 0) > 0;
45
52
  }
46
- export function compileSkillGrant(skill, origin) {
53
+ export function compileSkillGrant(skill, origin, roster) {
47
54
  const rows = [];
48
55
  const inert = [];
49
56
  for (const [list, behavior, entries] of [
50
57
  ["allowed-tools", "allow", skill.allowedTools ?? []],
51
58
  ["disallowed-tools", "deny", skill.disallowedTools ?? []],
52
59
  ]) {
53
- const compiled = compileAllowList(entries, behavior);
60
+ const compiled = compileAllowList(entries, behavior, roster);
54
61
  for (const r of compiled.rows)
55
62
  rows.push({ skill: skill.name, origin, behavior, entry: r.entry, compiled: r.compiled });
56
63
  for (const i of compiled.inert)
57
- inert.push({ skill: skill.name, list, entry: i.entry, reason: i.reason });
64
+ inert.push({ skill: skill.name, list, ...i });
58
65
  }
59
66
  return { rows, inert };
60
67
  }
@@ -65,10 +72,15 @@ function judge(rows, req, behavior, ctx) {
65
72
  export function skillDenyDecision(hit) {
66
73
  return {
67
74
  action: "deny",
68
- message: `the active skill "${inlineUntrusted(hit.skill, 120)}" lists this tool in disallowed-tools (${hit.entries.map((e) => inlineUntrusted(e, 200)).join(", ")}) — refused for the rest of this run`,
75
+ message: hit.readsAsNothing !== undefined
76
+ ? skillDenyUnreadable(hit.skill, hit.readsAsNothing, "activation").message
77
+ : `the active skill "${inlineUntrusted(hit.skill, 120)}" lists this tool in disallowed-tools (${hit.entries.map((e) => inlineUntrusted(e, 200)).join(", ")}) — refused for the rest of this run`,
69
78
  decisionReason: "skill",
70
79
  };
71
80
  }
81
+ function unreadDenyOf(inert) {
82
+ return inert.filter((i) => i.list === "disallowed-tools");
83
+ }
72
84
  export function skillRowRefusal(lane, req) {
73
85
  const hit = lane?.denying(req);
74
86
  return hit === undefined ? undefined : skillDenyDecision(hit);
@@ -83,21 +95,21 @@ export function skillAllowDecision(hit, updatedInput) {
83
95
  }
84
96
  const CARD_ENTRIES_MAX = 10;
85
97
  const CARD_ENTRY_MAX_CHARS = 120;
86
- export function skillActivationQuestion(skill, mode) {
98
+ export function skillActivationQuestion(skill, mode, roster) {
87
99
  const head = `Execute skill: ${inlineUntrusted(skill.name, 200)}`;
88
- const granting = compileSkillGrant(skill, "model").rows.filter((r) => r.behavior === "allow" && !(mode === "auto" && autoStripped(r.compiled, undefined)));
100
+ const granting = compileSkillGrant(skill, "model", roster).rows.filter((r) => r.behavior === "allow" && !(mode === "auto" && autoStripped(r.compiled, undefined)));
89
101
  if (granting.length === 0)
90
102
  return head;
91
103
  const shown = granting.slice(0, CARD_ENTRIES_MAX).map((r) => inlineUntrusted(r.entry, CARD_ENTRY_MAX_CHARS));
92
104
  const rest = granting.length - shown.length;
93
105
  return `${head}\nApproving also lets these tools be used without asking for the rest of this run: ${shown.join(", ")}${rest > 0 ? ` and ${rest} more` : ""}`;
94
106
  }
95
- export function skillSeatReader(skills) {
107
+ export function skillSeatReader(skills, mounted) {
96
108
  if (skills.length === 0)
97
109
  return undefined;
98
110
  return (args, mode) => {
99
111
  const skill = skillNamedBy(args, skills);
100
- return skill !== undefined && declaresSkillLists(skill) ? { seat: "passthrough", question: skillActivationQuestion(skill, mode) } : undefined;
112
+ return skill !== undefined && declaresSkillLists(skill) ? { seat: "passthrough", question: skillActivationQuestion(skill, mode, mounted?.()) } : undefined;
101
113
  };
102
114
  }
103
115
  export function skillGateWiring(opts) {
@@ -123,27 +135,68 @@ export function seedSkillGrants(table, opts) {
123
135
  for (const name of invoked)
124
136
  table.activate(named(name), "user");
125
137
  }
126
- return opts.skills.flatMap((s) => compileSkillGrant(s, "model").inert);
138
+ const roster = table.mountedNames();
139
+ return opts.skills.flatMap((s) => compileSkillGrant(s, "model", roster).inert);
140
+ }
141
+ export function screenUserInvokedSkills(config) {
142
+ if (config.userInvokedSkills === undefined)
143
+ return;
144
+ try {
145
+ seedSkillGrants(new SkillGrantTable(), { skills: config.skills ?? [], userInvokedSkills: config.userInvokedSkills, inherited: [] });
146
+ }
147
+ catch (e) {
148
+ const code = e?.code;
149
+ if (code === USER_INVOKED_SKILL_UNKNOWN_CODE || code === SKILL_DENY_UNREADABLE_CODE)
150
+ throw e;
151
+ }
127
152
  }
128
153
  export class SkillGrantTable {
129
154
  rowsHeld = [];
155
+ rowKeys = new Set();
156
+ mounted;
157
+ activations;
158
+ bindMountedNames(mounted) {
159
+ this.mounted = mounted;
160
+ }
161
+ bindSkills(skills, skillToolName) {
162
+ this.activations = { skills, skillToolName };
163
+ }
164
+ mountedNames() {
165
+ return this.mounted?.();
166
+ }
130
167
  hold(row) {
131
- if (!this.rowsHeld.some((r) => r.skill === row.skill && r.origin === row.origin && r.behavior === row.behavior && r.entry === row.entry))
132
- this.rowsHeld.push(row);
168
+ const key = JSON.stringify([row.skill, row.origin, row.behavior, row.entry]);
169
+ if (this.rowKeys.has(key))
170
+ return;
171
+ this.rowKeys.add(key);
172
+ this.rowsHeld.push(row);
133
173
  }
134
174
  activate(skill, origin) {
135
- const { rows, inert } = compileSkillGrant(skill, origin);
175
+ const { rows, inert } = compileSkillGrant(skill, origin, this.mountedNames());
176
+ const unreadDeny = unreadDenyOf(inert);
177
+ if (unreadDeny.length > 0)
178
+ throw skillDenyUnreadable(skill.name, unreadDeny, "activation");
136
179
  for (const row of rows)
137
180
  this.hold(row);
138
181
  return inert;
139
182
  }
140
183
  seed(records, origin) {
184
+ const roster = this.mountedNames();
185
+ const rows = [];
186
+ const unreadDeny = [];
141
187
  for (const r of records) {
142
- const compiled = compileAllowEntry(r.entry, r.behavior);
143
- if (typeof compiled === "string")
144
- continue;
145
- this.hold({ skill: r.skill, origin: r.origin === "model" ? "model" : origin, behavior: r.behavior, entry: r.entry, compiled });
188
+ const read = compileAllowList([r.entry], r.behavior, roster);
189
+ const row = read.rows[0];
190
+ if (row !== undefined)
191
+ rows.push({ skill: r.skill, origin: r.origin === "model" ? "model" : origin, behavior: r.behavior, entry: r.entry, compiled: row.compiled });
192
+ else if (r.behavior === "deny")
193
+ unreadDeny.push(...read.inert.map((i) => ({ skill: r.skill, ...i })));
146
194
  }
195
+ const first = unreadDeny[0];
196
+ if (first !== undefined)
197
+ throw skillDenyUnreadable(first.skill, unreadDeny.filter((i) => i.skill === first.skill), "carried");
198
+ for (const row of rows)
199
+ this.hold(row);
147
200
  }
148
201
  records() {
149
202
  return this.rowsHeld.map((r) => ({ skill: r.skill, behavior: r.behavior, entry: r.entry, origin: r.origin }));
@@ -151,9 +204,17 @@ export class SkillGrantTable {
151
204
  lane(ctx) {
152
205
  return {
153
206
  allowing: (req, mode) => judge(allowingRows(this.rowsHeld, req, mode), req, "allow", ctx),
154
- denying: (req) => judge(this.rowsHeld, req, "deny", ctx),
207
+ denying: (req) => judge(this.rowsHeld, req, "deny", ctx) ?? this.activationRefused(req),
155
208
  };
156
209
  }
210
+ activationRefused(req) {
211
+ const at = this.activations;
212
+ if (at === undefined || !isSkillActivationCall(req, at.skills, at.skillToolName))
213
+ return undefined;
214
+ const skill = skillNamedBy(req.args, at.skills);
215
+ const unread = skill === undefined ? [] : unreadDenyOf(compileSkillGrant(skill, "model", this.mountedNames()).inert);
216
+ return skill === undefined || unread.length === 0 ? undefined : { skill: skill.name, entries: unread.map((i) => i.entry), readsAsNothing: unread };
217
+ }
157
218
  }
158
219
  function readCarriedRecords(raw, carrier) {
159
220
  if (raw === undefined)
@@ -190,3 +251,15 @@ export function readFrozenSkillGrants(section) {
190
251
  return readCarriedRecords(section, "frozen");
191
252
  }
192
253
  export const USER_INVOKED_SKILL_UNKNOWN_CODE = "config.user_invoked_skill_unknown";
254
+ export const SKILL_DENY_UNREADABLE_CODE = "config.skill_deny_unreadable";
255
+ function skillDenyUnreadable(skill, entries, site) {
256
+ const listed = entries.map(inertEntryPhrase).join(", ");
257
+ const one = entries.length === 1;
258
+ const name = inlineUntrusted(skill, 120);
259
+ const message = site === "activation"
260
+ ? `Skill "${name}" was not activated: its disallowed-tools entr${one ? `y ${listed} reads` : `ies ${listed} read`} as nothing — a refusal this engine cannot read is not dropped, so the activation does not happen (its instructions are not loaded and nothing it lists is granted). Spell each entry as the tool is mounted, or remove it, in its definition.`
261
+ : `the grant rows carried for Skill "${name}" (a park's frozen rows, or a parent's capture) hold the disallowed-tools entr${one ? `y ${listed}, which reads` : `ies ${listed}, which read`} as nothing for this run — a refusal this engine cannot read is not dropped, so this leg does not start without it. A parked task an earlier release wrote can be finished on a worker of that release; otherwise correct the entry in its disallowed-tools and submit the task again.`;
262
+ const e = new Error(message);
263
+ e.code = SKILL_DENY_UNREADABLE_CODE;
264
+ return e;
265
+ }
@@ -0,0 +1,46 @@
1
+ /**
2
+ * Is a repeat under an already-used steer `inputId` the same instruction? ONE answer for both seats that hold a
3
+ * steer under its key — the parked queue (`appendPendingSteer`) and the live verb's accepted-input domain — and that
4
+ * one answer feeds the replay decision, the refusal's machine-readable `detail.differs` and its sentence.
5
+ *
6
+ * Both seats compare the record the CALLER sent (text, trusted, tier, actor) — never the delivered bytes: the live
7
+ * delivery folds `trusted` (the framing) and the actor (the speaker label) into its payload, so comparing payloads
8
+ * would report one changed facet as two.
9
+ */
10
+ import type { ActorAssertion } from "../internal/llm.js";
11
+ import { type SystemInjectionPriority } from "./task-notification.js";
12
+ /** The facets a steer's replay identity is made of, in the order a refusal lists them. Closed. */
13
+ export declare const STEER_REPLAY_FACETS: readonly ["text", "trusted", "priority", "actor"];
14
+ export type SteerReplayFacet = (typeof STEER_REPLAY_FACETS)[number];
15
+ /** What a seat remembers a steer by: the caller's own statement, as validated. The parked entry and the live park
16
+ * record both are this shape. An absent `priority` reads as `"next"` ({@link steerPriorityOf}). */
17
+ export interface SteerReplayRecord {
18
+ readonly text: string;
19
+ readonly trusted: boolean;
20
+ readonly priority?: SystemInjectionPriority;
21
+ readonly actor?: ActorAssertion;
22
+ }
23
+ /** The facets `repeat` differs from `held` in, in {@link STEER_REPLAY_FACETS} order. Empty = the idempotent replay. */
24
+ export declare function steerReplayDiffers(held: SteerReplayRecord, repeat: SteerReplayRecord): SteerReplayFacet[];
25
+ /** `steering.duplicate_input_id`'s machine-readable half: the facets, by name only (never the values). Always
26
+ * present on that code, never empty. */
27
+ export interface SteerReplayRefusalDetail {
28
+ readonly differs: readonly SteerReplayFacet[];
29
+ }
30
+ /** Which seat refuses: the parked queue names the key it holds; the live verb answers about the caller's own key. */
31
+ export type SteerReplaySeat = {
32
+ readonly seat: "parked";
33
+ readonly inputId: string;
34
+ } | {
35
+ readonly seat: "accepted";
36
+ };
37
+ /**
38
+ * The refusal for a repeat that is not a replay, or `undefined` for one that is. When `text` differs the repeat is
39
+ * another instruction and the caller is told to take a fresh key. When only the tier / trust / attribution differ it
40
+ * is the SAME instruction, already accepted, whose standing receipt those facets do not change — a fresh key would
41
+ * queue it a second time, so the sentence says so instead of advising one.
42
+ */
43
+ export declare function steerReplayRefusal(held: SteerReplayRecord, repeat: SteerReplayRecord, where: SteerReplaySeat): {
44
+ message: string;
45
+ detail: SteerReplayRefusalDetail;
46
+ } | undefined;
@@ -0,0 +1,22 @@
1
+ import { steerPriorityOf } from "./task-notification.js";
2
+ export const STEER_REPLAY_FACETS = Object.freeze(["text", "trusted", "priority", "actor"]);
3
+ const FACET_IS_SAME = {
4
+ text: (a, b) => a.text === b.text,
5
+ trusted: (a, b) => a.trusted === b.trusted,
6
+ priority: (a, b) => steerPriorityOf(a) === steerPriorityOf(b),
7
+ actor: (a, b) => a.actor?.id === b.actor?.id && a.actor?.hostAsserted === b.actor?.hostAsserted && a.actor?.issuer === b.actor?.issuer,
8
+ };
9
+ export function steerReplayDiffers(held, repeat) {
10
+ return STEER_REPLAY_FACETS.filter((facet) => !FACET_IS_SAME[facet](held, repeat));
11
+ }
12
+ export function steerReplayRefusal(held, repeat, where) {
13
+ const differs = steerReplayDiffers(held, repeat);
14
+ if (differs.length === 0)
15
+ return undefined;
16
+ const seat = where.seat === "parked" ? `parked under inputId "${where.inputId}"` : "accepted under this inputId";
17
+ const listed = differs.join(", ");
18
+ const message = differs.includes("text")
19
+ ? `a different steering instruction is already ${seat} (this repeat differs in: ${listed}) — re-issue this one with a fresh inputId (an identical payload would have been an idempotent retry)`
20
+ : `the same steering instruction is already ${seat}; this repeat differs only in: ${listed}, which cannot change after acceptance — sending it again under another inputId would queue it a second time; to confirm the standing receipt, replay it with its original values`;
21
+ return { message, detail: { differs } };
22
+ }
@@ -94,8 +94,9 @@ export type BrainStatusPhase = "rate_limited" | "retrying" | "reconnecting" | "c
94
94
  * {@link BrainStatus.errorStatus}; this bucket is the reason a consumer RENDERS and the only carrier for the
95
95
  * transport classes, which have no status at all.) Values are about the SHAPE of the failure, never its
96
96
  * provider taxonomy:
97
- * - `connect_refused` — the attempt got a definite negative about the target itself (nothing accepts
98
- * at that address, or the name has no address). This is the class the SHORT retry lane serves.
97
+ * - `connect_refused` — the attempt got a definite negative about the way to the target (nothing accepts
98
+ * at that address, the name has no address, or the proxy on the way refused the tunnel). This is the class
99
+ * the SHORT retry lane serves.
99
100
  * - `transport` — any other transport-level failure: a connect timeout, a reset, a mid-stream
100
101
  * tear. No verdict about the target; the full ladder applies.
101
102
  * - `stall` — no failure was observed on the link at all: THIS process cancelled its own
@@ -110,6 +110,18 @@ export interface TaskSpec {
110
110
  * explicit opt-out, which covers both arms.
111
111
  */
112
112
  interactionPosture?: "interactive" | "headless";
113
+ /**
114
+ * The run's declaration that NOBODY on it can be asked to approve a call — read by the approval seat's one resolution
115
+ * ({@link import("./wiring-manifest.js").resolveAskSeat}). `"none"`: an approver callback the spec or the deployment wires
116
+ * is not this run's approver (never called; the manifest reports `ask.form: "declared_none"`); with the deployment's park
117
+ * facility armed an ask parks for a person to answer later (`park_only`), without one it is refused as nobody's. It
118
+ * approves and releases nothing; a blanket `"allow"` / `"deny"` seat stays a policy; under `dontAsk` the mode refuses
119
+ * first. Absent = today's reading. Not checkpointed (a resumed leg holds it only when its taskConfig supplies it again);
120
+ * children inherit it (the clamp seats). `interactionPosture: "interactive"` beside it is refused
121
+ * (`config.interaction_posture`); a value outside the closed set too (`config.approval_reach_invalid`). Independent of
122
+ * {@link interactiveTools} — a content question still goes to `onQuestion`.
123
+ */
124
+ approvalReach?: import("./wiring-manifest.js").ApprovalReach;
113
125
  /**
114
126
  * design/173 §8.2 — the EXPLICIT knob (default off) letting a posture-`"interactive"` run
115
127
  * CONTINUE with the synthetic self-answer instruction after all on BOTH hardened question arms
@@ -562,7 +574,8 @@ export interface TaskSpec {
562
574
  * projections and the deny-narrowing layers still apply regardless).
563
575
  */
564
576
  basePolicyForResumeEdit?: import("./tool-policy.js").ToolPolicy;
565
- /** How `ask` decisions resolve for this task (headless auto-deny by default). Overrides `RunnerDeps.onAsk`. */
577
+ /** How `ask` decisions resolve for this task (headless auto-deny by default). Overrides `RunnerDeps.onAsk` when present.
578
+ * A callback here is set aside when the run declares {@link approvalReach} `"none"`. */
566
579
  onAsk?: import("./tool-policy.js").OnAsk;
567
580
  /** Content-ask seam (design/64 §5): routes an AskUserQuestion tool call to a real human/UI. When set, the
568
581
  * AskUserQuestion tool is mounted (one of the delivery faces in the criterion — see
@@ -43,15 +43,20 @@ export interface TaskStream extends AsyncIterable<TaskEvent> {
43
43
  * that arrives while the task is LIVE was the only one that injected twice.
44
44
  * - **Absent ⇒ nothing changes**: a uuidv7 is minted for the event, nothing is recorded for the call,
45
45
  * and the delivered bytes are what they always were. Every pre-existing caller is on this arm.
46
- * - **Replay ⇒ idempotent no-op**: re-steering an id this stream already ACCEPTED, with an identical
47
- * payload (same text, same `trusted`, same `actor`), injects nothing and emits no second
46
+ * - **Replay ⇒ idempotent no-op**: re-steering an id this stream already ACCEPTED, with the same
47
+ * steer (same text, `trusted`, tier — absent reads `next` — and `actor`), injects nothing and emits no second
48
48
  * `human_input` frame. What is remembered is exactly what QUEUED: a delivery that was refused
49
49
  * (not running) can be retried under its own id, and a call whose DELIVERED payload is
50
50
  * whitespace-only — which the harness discards without minting a frame; note a `trusted` steer is
51
51
  * wrapped first, so it queues even for blank text — reserves nothing, leaving that id usable.
52
- * - **Same id, DIFFERENT instruction ⇒ typed throw** `steering.duplicate_input_id`: a key is not
52
+ * - **Same id, not the same steer ⇒ typed throw** `steering.duplicate_input_id`: a key is not
53
53
  * evidence of a replay, and two callers colliding on one id must not silently lose the second
54
- * (the parked leg's `appendPendingSteer` refuses it identically). Re-issue under a fresh id.
54
+ * (the parked leg's `appendPendingSteer` judges and refuses it identically). The error carries
55
+ * `detail.differs` — the facets that differ (closed `STEER_REPLAY_FACETS`: `text`, `trusted`,
56
+ * `priority`, `actor`), compared on what the caller SENT, never on the framed bytes. With `text`
57
+ * among them it is another instruction: re-issue it under a fresh id. Without, it is the accepted one
58
+ * re-sent at another tier / trust / attribution, which acceptance does not change: a fresh id would
59
+ * queue it twice, so replay it with its original values to confirm the receipt.
55
60
  * - **Bad value ⇒ typed throw** `steering.invalid_content`, never a silent fallback to "no id": the
56
61
  * value domain is the parked leg's (a non-empty string of at most `MAX_STEER_INPUT_ID_CHARS`
57
62
  * characters), so one key is accepted or
@@ -107,8 +112,9 @@ export interface TaskStream extends AsyncIterable<TaskEvent> {
107
112
  * relaying steer must gate this option at its own face — relay IS authorization.
108
113
  * An unknown value throws typed `steering.invalid_content` (bad-value loudness). The replay
109
114
  * identity (`inputId` idempotency) includes the NORMALIZED tier: the same text replayed under
110
- * the same id at a different tier refuses `steering.duplicate_input_id` — an "idempotent
111
- * success" that silently skipped the interrupt would be a disposition lie.
115
+ * the same id at a different tier refuses `steering.duplicate_input_id` (`detail.differs:
116
+ * ["priority"]`) — an "idempotent success" that silently skipped the interrupt would be a
117
+ * disposition lie.
112
118
  *
113
119
  * design/373 §4.3 (D2, ruling of 2026-08-24) — a deployment `userPromptSubmit` hook SCREENS this verb:
114
120
  * the steer face is a SERVICE entrance (third-party callers reach a running run through it), so
@@ -13,6 +13,8 @@ export declare class TaskSpecTightenError extends Error {
13
13
  * would drop the base policy entirely.
14
14
  * - **`handsReadOnly`** — `true` is stricter. `base:true` + `override:false` ⇒ **throw** (loosening). Otherwise
15
15
  * the stricter (`true` if either is true) wins.
16
+ * - **`approvalReach`** — `"none"` (nobody can be asked) is the only word; a base declaration survives an override that
17
+ * omits it or writes `undefined`.
16
18
  * - **`shellGate`** — one value (`"always"`, the operator floor). Either side set ⇒ the floor stands; it cannot
17
19
  * be loosened because absence is the only other state and an override cannot unset a base value.
18
20
  * - **`onAsk`** — no safe automatic merge (the approver and the policy must be coordinated). Both set to
@@ -47,6 +47,9 @@ export function tightenTaskSpec(base, overrides) {
47
47
  if (base.handsReadOnly !== undefined || overrides.handsReadOnly !== undefined) {
48
48
  merged.handsReadOnly = base.handsReadOnly === true || overrides.handsReadOnly === true;
49
49
  }
50
+ if (base.approvalReach !== undefined || overrides.approvalReach !== undefined) {
51
+ merged.approvalReach = base.approvalReach === "none" || overrides.approvalReach === "none" ? "none" : (overrides.approvalReach ?? base.approvalReach);
52
+ }
50
53
  if (base.permissionMode !== undefined && overrides.permissionMode !== undefined) {
51
54
  const { effective, clamped } = clampPermissionMode(overrides.permissionMode, base.permissionMode);
52
55
  if (clamped) {
@@ -123,8 +123,8 @@ export type WorkerErrorClass = "budget" | "limit" | "output" | "suspend" | "revi
123
123
  * The `limits.` namespace splits into the `budget` and `limit` classes by exact code — see
124
124
  * {@link EXACT_CODE_CLASS}. Operation-level dotted codes that are surfaced to a *caller* and never become a
125
125
  * task outcome are deliberately OUT of scope: `steering.*` (`steering.not_running`/`steering.invalid_content`/
126
- * `steering.duplicate_input_id`/`steering.blocked_by_hook`, rejected to the `steer()` caller — the fourth also
127
- * rides the resume face as a `CheckpointError` code) and `mcp.*` (`mcp.server_unavailable`, an `onWarn` warning code) — neither
126
+ * `steering.duplicate_input_id`/`steering.blocked_by_hook`, rejected to the `steer()` caller — the third also
127
+ * rides the parked queue's `setPendingSteer` face and the fourth the resume face, as `CheckpointError` codes) and `mcp.*` (`mcp.server_unavailable`, an `onWarn` warning code) — neither
128
128
  * reaches `TaskResult.errorCode`, so a caller will never pass them here. A genuinely unmapped code → `"unknown"` (which therefore means
129
129
  * "known-but-foreign or no code", e.g. a leaked fs/Node code, NOT "an OUR terminal class we forgot to add").
130
130
  */