@sema-agent/core 7.40.1 → 7.41.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 (134) hide show
  1. package/CHANGELOG.md +188 -0
  2. package/dist/agents/agent-transcript-tool.d.ts +10 -2
  3. package/dist/agents/agent-transcript-tool.js +52 -26
  4. package/dist/agents/list-agents-tool.d.ts +17 -2
  5. package/dist/agents/list-agents-tool.js +28 -16
  6. package/dist/agents/peer-session-drain.d.ts +3 -1
  7. package/dist/agents/peer-session-drain.js +1 -0
  8. package/dist/agents/subagent-steps.d.ts +12 -2
  9. package/dist/agents/subagent-steps.js +8 -4
  10. package/dist/agents/subagent.d.ts +1 -17
  11. package/dist/agents/subagent.js +12 -24
  12. package/dist/core/agent-definition.d.ts +19 -40
  13. package/dist/core/agent-state.d.ts +91 -0
  14. package/dist/core/agent-state.js +79 -0
  15. package/dist/core/ask-origin.d.ts +28 -6
  16. package/dist/core/ask-origin.js +9 -5
  17. package/dist/core/ask-unresolvable-notice.d.ts +27 -4
  18. package/dist/core/ask-unresolvable-notice.js +8 -0
  19. package/dist/core/auto-promote.d.ts +14 -27
  20. package/dist/core/auto-promote.js +5 -5
  21. package/dist/core/checkpoint-store.d.ts +11 -12
  22. package/dist/core/decision-receipt.d.ts +5 -2
  23. package/dist/core/decision-receipt.js +1 -1
  24. package/dist/core/engine-notice.d.ts +7 -1
  25. package/dist/core/gate-crash-trace.d.ts +1 -1
  26. package/dist/core/gate-fold.js +7 -3
  27. package/dist/core/gate-lanes.js +22 -3
  28. package/dist/core/governance-codes.d.ts +3 -1
  29. package/dist/core/governance-codes.js +3 -0
  30. package/dist/core/hooks.d.ts +20 -6
  31. package/dist/core/hooks.js +28 -28
  32. package/dist/core/mcp-tool-meta.d.ts +24 -0
  33. package/dist/core/mcp-tool-meta.js +17 -0
  34. package/dist/core/mcp.d.ts +1 -6
  35. package/dist/core/mcp.js +6 -7
  36. package/dist/core/mode-policy.d.ts +25 -0
  37. package/dist/core/mode-policy.js +8 -0
  38. package/dist/core/protocol-table.d.ts +3 -0
  39. package/dist/core/protocol-table.js +3 -0
  40. package/dist/core/reminder-mint.d.ts +21 -1
  41. package/dist/core/reminder-mint.js +20 -0
  42. package/dist/core/retired-keys.d.ts +2 -1
  43. package/dist/core/retired-keys.js +13 -2
  44. package/dist/core/rule-denial-sentence.d.ts +2 -2
  45. package/dist/core/runner/call-hook-stations.d.ts +106 -0
  46. package/dist/core/runner/call-hook-stations.js +118 -0
  47. package/dist/core/runner/contracts.d.ts +35 -25
  48. package/dist/core/runner/gate-exit.d.ts +5 -1
  49. package/dist/core/runner/gate-exit.js +10 -6
  50. package/dist/core/runner/prepare-delegation-surface.d.ts +1 -0
  51. package/dist/core/runner/prepare-delegation-surface.js +4 -4
  52. package/dist/core/runner/prepare-gate-stations.d.ts +8 -3
  53. package/dist/core/runner/prepare-gate-stations.js +13 -70
  54. package/dist/core/runner/prepare-inherited-gate.d.ts +6 -3
  55. package/dist/core/runner/prepare-inherited-gate.js +5 -3
  56. package/dist/core/runner/prepare-park-ask.d.ts +4 -0
  57. package/dist/core/runner/prepare-park-ask.js +4 -1
  58. package/dist/core/runner/prepare-policy-chain.d.ts +6 -5
  59. package/dist/core/runner/prepare-policy-chain.js +14 -25
  60. package/dist/core/runner/prepare-project-context.d.ts +15 -16
  61. package/dist/core/runner/prepare-project-context.js +15 -11
  62. package/dist/core/runner/prepare-run-refs.d.ts +8 -8
  63. package/dist/core/runner/prepare-run-refs.js +3 -3
  64. package/dist/core/runner/prepare-safety-scan.js +3 -3
  65. package/dist/core/runner/prepare-suspend-saga.d.ts +2 -1
  66. package/dist/core/runner/prepare-suspend-saga.js +2 -1
  67. package/dist/core/runner/prepare-task.js +14 -12
  68. package/dist/core/runner/prepare-turn-wiring.d.ts +3 -1
  69. package/dist/core/runner/prepare-turn-wiring.js +16 -13
  70. package/dist/core/runner/resume-admission.js +2 -1
  71. package/dist/core/runner/resume-apply.js +12 -23
  72. package/dist/core/runner/resume-checkpoint-screen.js +3 -0
  73. package/dist/core/runner/run-leg.js +61 -47
  74. package/dist/core/runner/runtask.d.ts +1 -1
  75. package/dist/core/runner/session-rule-policy.d.ts +7 -19
  76. package/dist/core/runner/session-rule-policy.js +1 -2
  77. package/dist/core/runner/skill-base-dir.d.ts +10 -0
  78. package/dist/core/runner/skill-base-dir.js +4 -0
  79. package/dist/core/runner/synthetic-tools.d.ts +11 -17
  80. package/dist/core/runner/synthetic-tools.js +7 -8
  81. package/dist/core/safety-axis-vocab.d.ts +2 -2
  82. package/dist/core/safety-axis-vocab.js +2 -0
  83. package/dist/core/session-policy-store.d.ts +63 -11
  84. package/dist/core/session-policy-store.js +52 -6
  85. package/dist/core/skill-grant.d.ts +239 -0
  86. package/dist/core/skill-grant.js +313 -0
  87. package/dist/core/skills-directory.d.ts +5 -34
  88. package/dist/core/skills-directory.js +9 -58
  89. package/dist/core/task-registry-agent.d.ts +14 -2
  90. package/dist/core/task-registry-agent.js +23 -8
  91. package/dist/core/task-registry-shared.d.ts +6 -0
  92. package/dist/core/task-registry.d.ts +16 -0
  93. package/dist/core/task-registry.js +19 -6
  94. package/dist/core/task-spec.d.ts +10 -0
  95. package/dist/core/tool-face.d.ts +3 -2
  96. package/dist/core/tool-name-aliases.d.ts +38 -12
  97. package/dist/core/tool-name-aliases.js +16 -0
  98. package/dist/core/tool-name-readings.d.ts +6 -2
  99. package/dist/core/tool-name-readings.js +6 -4
  100. package/dist/core/tool-policy.d.ts +2 -2
  101. package/dist/core/tool-registry.d.ts +1 -4
  102. package/dist/core/tool-registry.js +0 -3
  103. package/dist/core/tool-spec.d.ts +11 -13
  104. package/dist/core/tools.d.ts +1 -1
  105. package/dist/core/trace.d.ts +15 -0
  106. package/dist/core/types.d.ts +1 -1
  107. package/dist/engine/harness/agent-harness.d.ts +12 -1
  108. package/dist/engine/harness/agent-harness.js +17 -13
  109. package/dist/engine/loop/agent-loop.d.ts +16 -1
  110. package/dist/engine/loop/agent-loop.js +9 -15
  111. package/dist/engine/loop/types.d.ts +2 -0
  112. package/dist/index.d.ts +9 -4
  113. package/dist/index.js +4 -2
  114. package/dist/stores/file/adoption/adopt.js +23 -16
  115. package/dist/stores/file/index.d.ts +8 -2
  116. package/dist/stores/file/index.js +2 -2
  117. package/dist/stores/file/mailbox-store.js +2 -2
  118. package/dist/stores/file/permission-rule-adopt.js +10 -1
  119. package/dist/stores/file/permission-rule-store.d.ts +6 -4
  120. package/dist/stores/file/permission-rule-store.js +127 -59
  121. package/dist/stores/file/session-policy-store.d.ts +86 -14
  122. package/dist/stores/file/session-policy-store.js +135 -118
  123. package/dist/stores/file/task-list-store.d.ts +5 -0
  124. package/dist/stores/file/task-list-store.js +18 -4
  125. package/dist/stores/file/usage-window-store.d.ts +9 -1
  126. package/dist/stores/file/usage-window-store.js +47 -33
  127. package/dist/tools/fs/safety.d.ts +1 -1
  128. package/package.json +1 -1
  129. package/test/export-surface.snapshot.json +45 -5
  130. package/wire-changes/core@7.41.0.json +1865 -0
  131. package/dist/core/runner/active-skill-scope.d.ts +0 -58
  132. package/dist/core/runner/active-skill-scope.js +0 -133
  133. package/dist/core/skill-tool-specifier.d.ts +0 -80
  134. package/dist/core/skill-tool-specifier.js +0 -57
@@ -282,7 +282,7 @@ export type RealApprovalTableCoversEveryOrigin = AssertAllKeysHandled<Exclude<As
282
282
  * the ask's CLASS SET ({@link classifyAsk}, {@link MODE_ARBITRATION}) — never by this one word.
283
283
  *
284
284
  * - `operator_always` — the operator's `always` floor: per-call confirmation for shell commands.
285
- * - `tool_marks` — the tool's own egress / irreversibility declaration.
285
+ * - `tool_marks` — the tool's own egress / irreversibility / interaction declaration.
286
286
  * - `probe_mandate` — this call's reversibility probe declared the demotion STRUCTURAL.
287
287
  * - `probe_unanswered` — the probe was consulted and did not answer: the boundary is UNKNOWN.
288
288
  * - `write_protection` — the table covers this write's target. A write onto load-executed configuration
@@ -300,7 +300,7 @@ declare const MANDATE_DISCLOSURE: {
300
300
  readonly probe_mandate: "the reversibility check declared this call structurally gated (the built-in shell check raises this for a read outside the directories allowed for this session) — it is cleared by confirming this call, never by a standing allow rule";
301
301
  readonly probe_unanswered: "the reversibility check did not answer for this call (it timed out or failed), so whether the call reads outside the directories allowed for this session is unknown — it is cleared by confirming this call, never by a standing allow rule";
302
302
  readonly operator_always: "this deployment mandates per-call confirmation for shell commands (shellGate: always)";
303
- readonly tool_marks: "this tool carries egress/irreversibility marks (a mandated confirmation a rule cannot clear)";
303
+ readonly tool_marks: "this tool carries egress/irreversibility/interaction marks (a mandated confirmation a rule cannot clear)";
304
304
  readonly write_protection: "this write's target is on the deployment's write-protection table (the agent's own configuration is guarded one look per write) — it is cleared by confirming this call, never by a standing allow rule";
305
305
  readonly write_protection_unresolved: "the write-protection judge could not resolve this write's target, so whether it lands on the protected table is unknown — it is cleared by confirming this call: no standing rule and no automatic decider answers for it, though a wired approver seat can";
306
306
  };
@@ -346,6 +346,10 @@ export declare function persistedRuleMandateOf(marks: MandateMarks): PersistedRu
346
346
  * `irreversibility`) beside the two per-call facts the gate stamps. */
347
347
  export interface MandateMarks {
348
348
  egress?: boolean;
349
+ /** The tool's server declared that a call must be put to a person
350
+ * ({@link import("../engine/loop/types.js").AgentTool.mcpRequiresUserInteraction}): a mark of the tool's own, like `egress` — no
351
+ * persisted allow rule or standing grant clears the ask, and the class `requires_user_interaction` rides every ask on the call. */
352
+ requiresUserInteraction?: boolean;
349
353
  shellGated?: boolean;
350
354
  irreversibility?: "never" | "maybe" | "always";
351
355
  /** The surviving ask's engine-stamped `probeMandated` — see
@@ -436,10 +440,13 @@ export type ToolMarkSource = "deployment" | "mcp_annotation" | "a2a_default";
436
440
  * most-immune-first order; every lane reads the folded row ({@link foldArbitrationRow}), never the word.
437
441
  * `shell_floor` is the operator's `shellGate: "always"` floor on a shell-running seat — the question the `operator_always`
438
442
  * mandate word discloses, as a class of its own so a set that holds another forced question still holds it.
443
+ * `requires_user_interaction` is the SERVER's declaration on the called tool (`_meta["anthropic/requiresUserInteraction"] === true`): a
444
+ * permission question, unlike `content_question` (the reserved question tool's content-routing signal) — it carries that class's arbitration
445
+ * row cell by cell, because the reference implementation judges the declaration ahead of its allow-rule lookup and its bypass release.
439
446
  * `unstated` is the mode's answer for a call nobody stated: a tool whose permission seat is `passthrough` (it states no
440
447
  * permission of its own — a remote peer's tool) reached the end of the fold with an allow no layer stated.
441
448
  */
442
- export declare const ASK_CLASSES: readonly ["content_question", "hook", "ancestor", "unverifiable_delete", "transcript_write", "org_rule", "governance_unavailable", "rule_unreadable", "ask_rule", "denial_limit_fallback", "operator_forced", "shell_floor", "write_protection_unresolved", "probe", "write_protection", "tool_marks_mcp", "tool_marks_a2a", "policy_rra", "artifact_shared_read", "mode", "unstated", "ordinary"];
449
+ export declare const ASK_CLASSES: readonly ["content_question", "hook", "ancestor", "unverifiable_delete", "transcript_write", "org_rule", "governance_unavailable", "rule_unreadable", "ask_rule", "denial_limit_fallback", "requires_user_interaction", "operator_forced", "shell_floor", "write_protection_unresolved", "probe", "write_protection", "tool_marks_mcp", "tool_marks_a2a", "policy_rra", "artifact_shared_read", "mode", "unstated", "ordinary"];
443
450
  export type AskClass = (typeof ASK_CLASSES)[number];
444
451
  /** Membership test for {@link AskClass} — the word crosses the wire on `tool_end.gate.disposition`. */
445
452
  export declare function isAskClass(v: unknown): v is AskClass;
@@ -542,6 +549,13 @@ export declare const MODE_ARBITRATION: {
542
549
  readonly ruleMayClear: false;
543
550
  readonly hookAskFloor: false;
544
551
  };
552
+ readonly requires_user_interaction: {
553
+ readonly bypassImmune: true;
554
+ readonly classifierEligible: false;
555
+ readonly sandboxAdmission: false;
556
+ readonly ruleMayClear: false;
557
+ readonly hookAskFloor: true;
558
+ };
545
559
  readonly operator_forced: {
546
560
  readonly bypassImmune: true;
547
561
  readonly classifierEligible: true;
@@ -631,7 +645,8 @@ export declare const MODE_ARBITRATION: {
631
645
  export type ArbitrationTableCoversEveryClass = AssertAllKeysHandled<Exclude<AskClass, keyof typeof MODE_ARBITRATION>>;
632
646
  /** The classes whose own mint puts the `requiresRealApproval` bit on the ask — a bit one of them explains is not
633
647
  * the bare bit `policy_rra` names. The mark classes (probe / write protection / tool marks / operator forced / the
634
- * shell floor) mint no bit and explain none. */
648
+ * shell floor) mint no bit and explain none — except `requires_user_interaction`, whose declaration the origin stamp
649
+ * turns into the bit on every ask of the tool. */
635
650
  export declare const RRA_EXPLAINERS: ReadonlySet<AskClass>;
636
651
  /** The classes a permission mode can RELEASE (the `bypassImmune: false` rows) — the only words that can appear on
637
652
  * `GateDisposition.released.askClass`; every other class reaches a person under every mode. */
@@ -709,6 +724,10 @@ export declare function classifyAsk(d: Extract<PermissionResult, {
709
724
  * (2.1.280) — the call did NOT run because this session has no approval surface — with the run's own fact beside it
710
725
  * and the ask's prompt as what was requested. The gate appends the caller-side remedy. */
711
726
  export declare function headlessDenialMessage(toolName: string, askMessage: string): string;
727
+ /** The state of the approver seat a remedy is written for: `absent` — no approver is wired for the leg; `unavailable` — one is
728
+ * wired and answered that nobody is reachable for this ask. Absent in the input reads as `absent` (the posture the
729
+ * function was written for). */
730
+ export type HeadlessApproverSeat = "absent" | "unavailable";
712
731
  /**
713
732
  * The ONE sentence a headless auto-deny (an ask that reached its final deny because no approver is wired) adds
714
733
  * after its refusal: the caller-side change that would have let this call run. Read off the effective mode, the
@@ -720,8 +739,10 @@ export declare function headlessDenialMessage(toolName: string, askMessage: stri
720
739
  * · the same ask under `acceptEdits` — the target lies outside the working directories: add its directory to
721
740
  * `additionalDirectories`;
722
741
  * · any other releasable ask — `bypassPermissions` releases it (disclosed on the call).
723
- * Every arm also names the approver seat. The refusal's tool text and the `delegation.ask_unresolvable` notice
724
- * both carry this sentence — they call this function, neither spells it.
742
+ * Every arm also names the approver, in the clause of the seat's own state ({@link HeadlessApproverSeat}): none wired ⇒
743
+ * wire one; one wired that answered `unavailable` for this ask ⇒ make a person reachable through it (a wired approver is
744
+ * never told to be wired). The refusal's tool text and the `delegation.ask_unresolvable` notice both carry this sentence —
745
+ * they call this function, neither spells it.
725
746
  */
726
747
  export declare function headlessApprovalRemedy(input: {
727
748
  readonly mode: PermissionMode | undefined;
@@ -730,6 +751,7 @@ export declare function headlessApprovalRemedy(input: {
730
751
  readonly unplacedOptions?: {
731
752
  readonly program: "cp" | "mv";
732
753
  };
754
+ readonly approverSeat?: HeadlessApproverSeat;
733
755
  }): string;
734
756
  /** The reference implementation's sentence (2.1.280) for a `cp` / `mv` the mode will not answer because it carries option
735
757
  * words — the approval card's message under `acceptEdits` and the head of the headless remedy. */
@@ -16,7 +16,7 @@ export function stampAskOrigin(decision, facts, classification) {
16
16
  const classed = askClassMembersOf(classification);
17
17
  const { mandate: _selfDeclared, ...unstamped } = decision;
18
18
  const clearance = ruleClearanceOf(unstamped, facts.mandateMarks);
19
- const mandated = { ...unstamped, ...(clearance.mandated ? { mandated: true } : {}), ...(clearance.mandate !== undefined ? { mandate: clearance.mandate } : {}) };
19
+ const mandated = { ...unstamped, ...(clearance.mandated ? { mandated: true } : {}), ...(clearance.mandate !== undefined ? { mandate: clearance.mandate } : {}), ...(facts.mandateMarks.requiresUserInteraction === true ? { requiresRealApproval: true } : {}) };
20
20
  if (mandated.probeReadRootCandidate === undefined || TIGHTEN_ONLY_ORIGINS.has(origin))
21
21
  return { ...mandated, origin, ...classed };
22
22
  const { probeReadRootCandidate: _withdrawn, ...kept } = mandated;
@@ -112,7 +112,7 @@ const MANDATE_DISCLOSURE = {
112
112
  probe_mandate: "the reversibility check declared this call structurally gated (the built-in shell check raises this for a read outside the directories allowed for this session) — it is cleared by confirming this call, never by a standing allow rule",
113
113
  probe_unanswered: "the reversibility check did not answer for this call (it timed out or failed), so whether the call reads outside the directories allowed for this session is unknown — it is cleared by confirming this call, never by a standing allow rule",
114
114
  operator_always: "this deployment mandates per-call confirmation for shell commands (shellGate: always)",
115
- tool_marks: "this tool carries egress/irreversibility marks (a mandated confirmation a rule cannot clear)",
115
+ tool_marks: "this tool carries egress/irreversibility/interaction marks (a mandated confirmation a rule cannot clear)",
116
116
  write_protection: "this write's target is on the deployment's write-protection table (the agent's own configuration is guarded one look per write) — it is cleared by confirming this call, never by a standing allow rule",
117
117
  write_protection_unresolved: "the write-protection judge could not resolve this write's target, so whether it lands on the protected table is unknown — it is cleared by confirming this call: no standing rule and no automatic decider answers for it, though a wired approver seat can",
118
118
  };
@@ -127,7 +127,7 @@ export function persistedRuleMandateOf(marks) {
127
127
  return "write_protection_unresolved";
128
128
  if (marks.writeProtected === true)
129
129
  return "write_protection";
130
- return marks.egress === true ? "tool_marks" : word;
130
+ return marks.egress === true || marks.requiresUserInteraction === true ? "tool_marks" : word;
131
131
  }
132
132
  function irreversibilityMandateOf(marks) {
133
133
  return marks.probeMandated === true
@@ -177,6 +177,7 @@ export const ASK_CLASSES = Object.freeze([
177
177
  "rule_unreadable",
178
178
  "ask_rule",
179
179
  "denial_limit_fallback",
180
+ "requires_user_interaction",
180
181
  "operator_forced",
181
182
  "shell_floor",
182
183
  "write_protection_unresolved",
@@ -205,6 +206,7 @@ export const MODE_ARBITRATION = {
205
206
  rule_unreadable: { bypassImmune: false, classifierEligible: false, sandboxAdmission: false, ruleMayClear: false, hookAskFloor: false },
206
207
  ask_rule: { bypassImmune: true, classifierEligible: false, sandboxAdmission: false, ruleMayClear: false, hookAskFloor: true },
207
208
  denial_limit_fallback: { bypassImmune: true, classifierEligible: false, sandboxAdmission: false, ruleMayClear: false, hookAskFloor: false },
209
+ requires_user_interaction: { bypassImmune: true, classifierEligible: false, sandboxAdmission: false, ruleMayClear: false, hookAskFloor: true },
208
210
  operator_forced: { bypassImmune: true, classifierEligible: true, sandboxAdmission: true, ruleMayClear: false, hookAskFloor: false },
209
211
  shell_floor: { bypassImmune: true, classifierEligible: true, sandboxAdmission: true, ruleMayClear: false, hookAskFloor: false },
210
212
  write_protection_unresolved: { bypassImmune: true, classifierEligible: false, sandboxAdmission: true, ruleMayClear: false, hookAskFloor: true },
@@ -218,7 +220,7 @@ export const MODE_ARBITRATION = {
218
220
  unstated: { bypassImmune: false, classifierEligible: true, sandboxAdmission: false, ruleMayClear: true, hookAskFloor: false },
219
221
  ordinary: { bypassImmune: false, classifierEligible: true, sandboxAdmission: true, ruleMayClear: true, hookAskFloor: false },
220
222
  };
221
- export const RRA_EXPLAINERS = new Set(["unverifiable_delete", "transcript_write", "org_rule", "governance_unavailable", "rule_unreadable", "denial_limit_fallback", "ancestor", "artifact_shared_read"]);
223
+ export const RRA_EXPLAINERS = new Set(["unverifiable_delete", "transcript_write", "org_rule", "governance_unavailable", "rule_unreadable", "denial_limit_fallback", "ancestor", "artifact_shared_read", "requires_user_interaction"]);
222
224
  export const RELEASABLE_ASK_CLASSES = ASK_CLASSES.filter((c) => !MODE_ARBITRATION[c].bypassImmune);
223
225
  export const MODE_RELEASE_REASONS = Object.freeze(["rule_unreadable"]);
224
226
  const MODE_RELEASE_REASON_SET = new Set(MODE_RELEASE_REASONS);
@@ -295,6 +297,8 @@ export function classifyAsk(d, facts) {
295
297
  if (facts.ruleCallUnreadable === true)
296
298
  classes.add("rule_unreadable");
297
299
  const marks = facts.mandateMarks;
300
+ if (marks.requiresUserInteraction === true)
301
+ classes.add("requires_user_interaction");
298
302
  if (marks.shellGated === true && marks.irreversibility === "always")
299
303
  classes.add("shell_floor");
300
304
  const toolMarked = marks.shellGated !== true && (marks.egress === true || marks.irreversibility === "always" || marks.irreversibility === "maybe");
@@ -332,7 +336,7 @@ export function headlessDenialMessage(toolName, askMessage) {
332
336
  return `Permission for this tool use was denied: it requires interactive approval, and permission prompts are not available in this session (no approver is wired for "${toolName}" — headless auto-deny). The action was NOT performed. Do not claim it succeeded, and do not retry it in this session — report the limitation to the user, or suggest an alternative. What was requested: ${askMessage}`;
333
337
  }
334
338
  export function headlessApprovalRemedy(input) {
335
- const approver = "wire an approver (`onAsk`) to put it to a person";
339
+ const approver = input.approverSeat === "unavailable" ? "make a person reachable through the approver that is wired (it answered `unavailable` for this ask)" : "wire an approver (`onAsk`) to put it to a person";
336
340
  const c = input.classification;
337
341
  if (input.mode === "acceptEdits" && input.unplacedOptions !== undefined && (c === undefined || (c.classes.size === 1 && c.classes.has("ordinary"))))
338
342
  return `${fileCommandOptionsSentence(input.unplacedOptions.program)} Spell the ${input.unplacedOptions.program} without option words for the mode to answer it, or ${approver}`;
@@ -27,6 +27,7 @@
27
27
  * is pure.
28
28
  */
29
29
  import type { EngineNotice } from "./engine-notice.js";
30
+ import type { AssertAllKeysHandled, HeadlessApproverSeat } from "./ask-origin.js";
30
31
  /** WHY the ask could not be resolved — the closed set (see the module note for each word). */
31
32
  export declare const ASK_UNRESOLVABLE_CAUSES: readonly ["approver_unavailable", "no_approver_wired", "mandate_unreconstructible"];
32
33
  export type AskUnresolvableCause = (typeof ASK_UNRESOLVABLE_CAUSES)[number];
@@ -45,10 +46,31 @@ export interface AskUnresolvableFacts {
45
46
  /** Whether a durable approval park lane was ARMED for this run (`true`: the park had its one attempt and
46
47
  * did not take the call — it declined or failed; `false`: no park lane at all, the usual shape). */
47
48
  readonly parkLaneExisted: boolean;
48
- /** `no_approver_wired` only: the caller-side change that would have let the call run, as the gate minted it for the
49
- * refusal's tool text (`headlessApprovalRemedy`, ask-origin.ts) — carried verbatim, never re-composed here. */
49
+ /** Present for the causes that say nobody could be asked ({@link askUnresolvableRemedySeat}): the caller-side change that
50
+ * would have let the call run, as the gate minted it for the refusal's tool text (`headlessApprovalRemedy`, ask-origin.ts)
51
+ * — carried verbatim, never re-composed here. */
50
52
  readonly remedy?: string;
51
53
  }
54
+ /**
55
+ * Which causes take the caller-side remedy (`headlessApprovalRemedy`, ask-origin.ts), and in which seat's clause — the table IS
56
+ * the rule, and it is total over the closed set (a cause with no row is a red build: {@link CauseRemedySeatCoversEveryCause}).
57
+ * The two causes that say NOBODY COULD BE ASKED take it — no approver is wired (`absent`: the clause says to wire one), or the
58
+ * wired approver reported nobody reachable (`unavailable`: the clause says to make a person reachable through it, never to
59
+ * wire what is wired): one fact, one sentence about the way out, whichever way nobody was there. The inherited-mandate cause
60
+ * takes none (`undefined`): its recovery is a park facility on this leg, which neither a permission mode nor an `onAsk` here
61
+ * supplies, so the remedy's arms would name a change that does not release the call. (The settlement-axis table
62
+ * `SETTLEMENT_NOBODY_JUDGED` keeps `approver_unavailable` false because there a park route may still take the ask; this table is
63
+ * read at the deny exit, where every park route has had its attempt.)
64
+ */
65
+ declare const CAUSE_REMEDY_SEAT: {
66
+ readonly approver_unavailable: "unavailable";
67
+ readonly no_approver_wired: "absent";
68
+ readonly mandate_unreconstructible: undefined;
69
+ };
70
+ export type CauseRemedySeatCoversEveryCause = AssertAllKeysHandled<Exclude<AskUnresolvableCause, keyof typeof CAUSE_REMEDY_SEAT>>;
71
+ /** The seat whose clause the refusal for `cause` carries its remedy in, or `undefined` when the cause takes no remedy
72
+ * ({@link CAUSE_REMEDY_SEAT}); read once at the deny exit, which hands the sentence to the tool text and the notice alike. */
73
+ export declare function askUnresolvableRemedySeat(cause: AskUnresolvableCause): HeadlessApproverSeat | undefined;
52
74
  /**
53
75
  * Compose the `delegation.ask_unresolvable` notice from the gate's deny-exit facts.
54
76
  *
@@ -58,7 +80,8 @@ export interface AskUnresolvableFacts {
58
80
  * an empty session or tool name, or a cause outside the closed set, is refused at the mint with a
59
81
  * `TypeError` rather than shaped into a notice that would route nowhere or say nothing.
60
82
  *
61
- * `detail: { sessionId, toolName, toolCallId, cause, parkLaneExisted, remedy? }` — `remedy` rides a headless
62
- * auto-deny only (the sentence the refusal's tool text carries); `sessionId` is lifted onto the typed key by {@link import("./engine-notice.js").deliverEngineNotice}, the one throat.
83
+ * `detail: { sessionId, toolName, toolCallId, cause, parkLaneExisted, remedy? }` — `remedy` rides the causes that say nobody
84
+ * could be asked ({@link askUnresolvableRemedySeat}: the sentence the refusal's tool text carries); `sessionId` is lifted onto the typed key by {@link import("./engine-notice.js").deliverEngineNotice}, the one throat.
63
85
  */
64
86
  export declare function askUnresolvableNotice(facts: AskUnresolvableFacts): EngineNotice;
87
+ export {};
@@ -9,6 +9,14 @@ const CAUSE_CLAUSE = {
9
9
  no_approver_wired: "no approver (or question face) is wired for this run — the headless posture",
10
10
  mandate_unreconstructible: "an inherited durable-approval mandate binds this ask and this leg has no park facility to honor it",
11
11
  };
12
+ const CAUSE_REMEDY_SEAT = {
13
+ approver_unavailable: "unavailable",
14
+ no_approver_wired: "absent",
15
+ mandate_unreconstructible: undefined,
16
+ };
17
+ export function askUnresolvableRemedySeat(cause) {
18
+ return CAUSE_REMEDY_SEAT[cause];
19
+ }
12
20
  export function askUnresolvableNotice(facts) {
13
21
  const { sessionId, toolName, toolCallId, cause, parkLaneExisted, remedy } = facts;
14
22
  if (typeof sessionId !== "string" || sessionId.length === 0) {
@@ -1,39 +1,25 @@
1
1
  import type { OracleIsolationVerdict } from "./oracle-isolation.js";
2
- import type { SkillManifest } from "./types.js";
2
+ import type { SkillSpec } from "./types.js";
3
3
  /**
4
4
  * The terminal of {@link decideAutoPromote}.
5
5
  * - `"auto_promote"` — the ONLY exit that skips human review. Reachable only when all four prior guards
6
- * (tripwire / irreversible / statistics / concurrence) clear AND all four gates are present+passing.
6
+ * (tripwire / irreversible / statistics / concurrence) clear AND all three gates are present+passing.
7
7
  * - `"candidate_only"` — surface the artifact as a candidate; the SOLE shortfall is the oracle isolation class
8
8
  * being downgraded to `in_process_probe_only` (oracle-isolation.ts:34) with everything else passing.
9
9
  * - `"needs_human"` — fail-closed default: escalate to a human (any guard tripped, any other gate failing).
10
10
  */
11
11
  export type AutoPromoteTerminal = "auto_promote" | "candidate_only" | "needs_human";
12
12
  /**
13
- * Gate-2 + Gate-3 lineage-bound handle (裁决净增③ / P0#3 / P1 lineage-bind). `gate2Verdict` and `manifest`
14
- * MUST come from the SAME atomic `appendPromotable` write (memory.ts:295) — this struct bundles them into one
15
- * handle so the decision can never pair a verdict with a divergent manifest ("assume two booleans are
16
- * co-sourced" is the bug this prevents). `lineageId` keys the active-skill-scope frame (active-skill-scope.ts).
13
+ * Gate-2 handle (裁决净增③): the verdict the promoted artifact's OWN promotable write landed with, held in one place
14
+ * so the decision reads one verdict and never a loose boolean. (It used to carry the artifact's lineage id as the scope
15
+ * key of the retired per-skill narrowing; with that gone the lineage keys nothing the fold reads.)
17
16
  */
18
17
  export interface PromotedRecord {
19
- /** Stable lineage id of the promoted artifact; the Gate-3 scope key. */
20
- lineageId: string;
21
18
  /**
22
19
  * Gate-2 chokepoint result: the promotable write landed WITH a positive utility verdict (memory.ts:240),
23
20
  * or it was absent/ungated. Only `"present_positive"` is gate-passing; `"absent_or_ungated"` ⇒ needs_human.
24
21
  */
25
22
  gate2Verdict: "present_positive" | "absent_or_ungated";
26
- /** Gate-3 capability manifest — co-located with `gate2Verdict` in the same store record, never diverging. */
27
- manifest: SkillManifest;
28
- /**
29
- * Gate-3 scope is armed AND has propagated across the subagent boundary (design/79 §4, design/77 §3/§7).
30
- * `true` ONLY when either (a) the manifest lists NO subagent-spawn tool, or (b) its scope has propagated to
31
- * spawned children. The skill→subagent manifest-scope propagation just landed in this branch (commit
32
- * `04f5104`); for an artifact that lists a subagent-spawn tool whose scope has NOT propagated, this is
33
- * `false` → the decision falls to `needs_human` (裁决净增⑤ / P1). A bare in-process `gate4Armed`-style
34
- * boolean is intentionally NOT enough here: the propagation hole is encoded structurally into this flag.
35
- */
36
- gate3Scoped: boolean;
37
23
  }
38
24
  /**
39
25
  * Inputs to {@link decideAutoPromote}. Every field is a PRESENCE/boolean/set the profile has already computed —
@@ -44,7 +30,7 @@ export interface PromotedRecord {
44
30
  export interface AutoPromoteInputs {
45
31
  /** Gate-1 verdict (oracle-isolation.ts:37); the caller re-asserted it before calling this function. */
46
32
  oracle: OracleIsolationVerdict;
47
- /** Gate-2 + Gate-3 lineage-bound single handle (净增③). */
33
+ /** Gate-2 lineage-bound single handle (净增③). */
48
34
  record: PromotedRecord;
49
35
  /** Gate-4: the irreversibility tighten is in place (or the artifact touches no irreversible tool). */
50
36
  gate4Armed: boolean;
@@ -79,9 +65,9 @@ export interface AutoPromoteInputs {
79
65
  * 2. `touchesIrreversibleTool` → needs_human (§6 permanent human door — 净增①; even if all else passes)
80
66
  * 3. `!statisticsPresent` → needs_human (statistics must have RAN — core-required; thresholds are profile)
81
67
  * 4. `concurringSignals.size < 2` → needs_human (≥2 DISTINCT KIND mechanical signals concur — 净增②)
82
- * 5. the four-gate present-passing check; if not all pass, the SOLE-shortfall arm:
68
+ * 5. the three-gate present-passing check; if not all pass, the SOLE-shortfall arm:
83
69
  * `in_process_probe_only` oracle (and every other gate passing) → `candidate_only`; otherwise needs_human.
84
- * 6. all four gates present+passing (with guards clear) → `auto_promote` (the only auto exit).
70
+ * 6. all three gates present+passing (with guards clear) → `auto_promote` (the only auto exit).
85
71
  */
86
72
  export declare function decideAutoPromote(i: AutoPromoteInputs): AutoPromoteTerminal;
87
73
  /**
@@ -104,13 +90,13 @@ export declare const FROZEN_DENYLIST_FLOOR: ReadonlySet<string>;
104
90
  /**
105
91
  * Build a tripwire verdict over a promoted artifact's text. **ESCALATE-ONLY** (design/79 §3,守 design/76
106
92
  * §5.1 r3): it can ONLY ever raise `escalate:true`; a clean artifact yields `{escalate:false, reasons:[]}` and
107
- * NEVER clears anything — the REAL containment is Gate-3 runtime deny-narrowing (exhaustive at call time), not
108
- * this advisory. Two mechanical scans, both escalate-only:
93
+ * NEVER clears anything — it contains nothing itself: the real protection is the out-of-process boundary and the
94
+ * permission gate at call time, not this advisory. Two mechanical scans, both escalate-only:
109
95
  *
110
96
  * 1. **symbol-mention drift** via {@link extractSymbols} (repo-map.ts). extractSymbols is a v1 heuristic:
111
97
  * column-0 / source-only, it skips indented lines and is structurally INCAPABLE of being a containment
112
98
  * proof (it can't enumerate everything the artifact can call). So it is used ONLY to ESCALATE — if a
113
- * surfaced top-level symbol names a tool/capability that is NOT in `declared.allowTools`, escalate.
99
+ * surfaced top-level symbol names a tool/capability that is NOT in `declared.allowedTools`, escalate.
114
100
  * 2. **raw-byte denylist** over the artifact's RAW bytes INCLUDING markdown bytes — {@link FROZEN_DENYLIST_FLOOR}
115
101
  * UNIONed with `profileTokens` (add-only). A substring hit → escalate, with the matched token in `reasons`.
116
102
  *
@@ -119,7 +105,8 @@ export declare const FROZEN_DENYLIST_FLOOR: ReadonlySet<string>;
119
105
  * contribution, never block" case, which is the caller's concern, not a throw inside the scan.)
120
106
  *
121
107
  * @param artifactText the artifact's raw text (markdown body included).
122
- * @param declared the artifact's declared manifest — its `allowTools` is the symbol-drift baseline.
108
+ * @param declared the artifact's declared tool list (the `SkillSpec` member the loader copies) — its `allowedTools`
109
+ * entries, each read as a permission rule for the tool it names, are the symbol-drift baseline.
123
110
  * @param profileTokens optional ADD-ONLY extra denylist tokens (UNIONed onto the frozen floor; cannot remove).
124
111
  */
125
- export declare function deriveTripwire(artifactText: string, declared: SkillManifest, profileTokens?: ReadonlySet<string>): TripwireResult;
112
+ export declare function deriveTripwire(artifactText: string, declared: Pick<SkillSpec, "allowedTools">, profileTokens?: ReadonlySet<string>): TripwireResult;
@@ -1,5 +1,5 @@
1
1
  import { extractSymbols } from "../tools/fs/repo-map.js";
2
- import { parseSkillToolEntry } from "./skill-tool-specifier.js";
2
+ import { parsePermissionRule } from "./permission-rules.js";
3
3
  import { thrownText } from "./thrown-value.js";
4
4
  export function decideAutoPromote(i) {
5
5
  if (i.tripwire.escalate)
@@ -12,10 +12,10 @@ export function decideAutoPromote(i) {
12
12
  return "needs_human";
13
13
  const g1ok = i.oracle.protected && i.oracle.isolationClass === "out_of_process";
14
14
  const g2ok = i.record.gate2Verdict === "present_positive";
15
- const allGatesPresentPassing = g1ok && g2ok && i.record.gate3Scoped && i.gate4Armed;
15
+ const allGatesPresentPassing = g1ok && g2ok && i.gate4Armed;
16
16
  if (!allGatesPresentPassing) {
17
17
  return (i.oracle.protected && i.oracle.isolationClass === "in_process_probe_only"
18
- && g2ok && i.record.gate3Scoped && i.gate4Armed)
18
+ && g2ok && i.gate4Armed)
19
19
  ? "candidate_only"
20
20
  : "needs_human";
21
21
  }
@@ -60,12 +60,12 @@ export function deriveTripwire(artifactText, declared, profileTokens) {
60
60
  const reasons = [];
61
61
  let escalate = false;
62
62
  try {
63
- const allow = new Set((declared.allowTools ?? []).map((entry) => parseSkillToolEntry(entry).name));
63
+ const allow = new Set((declared.allowedTools ?? []).map((entry) => parsePermissionRule(entry).toolName));
64
64
  const symbols = extractSymbols(artifactText);
65
65
  for (const sym of symbols) {
66
66
  if (!allow.has(sym)) {
67
67
  escalate = true;
68
- reasons.push(`symbol-drift: surfaced symbol "${sym}" not in declared.allowTools`);
68
+ reasons.push(`symbol-drift: surfaced symbol "${sym}" not in declared.allowedTools`);
69
69
  }
70
70
  }
71
71
  }
@@ -56,12 +56,6 @@ export declare function mintCheckpointId(): string;
56
56
  * that {@link ResumeOutcome} must match at the resume entry (council #3: prevents a `task_done` gate
57
57
  * being resumed with a `policy_ask` outcome → type confusion / corruption).
58
58
  */
59
- /** design/74: why a resource-slice suspend fired. `"budget"` covers cost OR tokens (both are checked by
60
- * `overBudget`); `"walltime"` is the soft slice deadline; `"turns"` the slice turn cap.
61
- * design/80 Seam #2: `"preempt"` is NOT a resource limit — it is an EXTERNAL scheduler yield (the scheduler
62
- * raised `spec.preemptSignal` to free resources for a higher-priority task). It rides the SAME durable-suspend
63
- * mechanism: the gate.kind stays `resource_limit` (the mechanism), so the resume path is byte-identical to a
64
- * budget/turns/walltime resume (continue the work, no decision); `"preempt"` is only the CAUSE. */
65
59
  /**
66
60
  * Why a `resource_limit` suspend was minted. design/164 split the single `budget` member: a token
67
61
  * window and a $ window are different allowances with different owners, and a scheduler reading a
@@ -80,6 +74,7 @@ export declare function mintCheckpointId(): string;
80
74
  * turn boundary the attribution order is
81
75
  * `preempt > env_lifetime > usage_window > budget_tokens > budget_cost > turns > walltime`
82
76
  * (external causes above self-chosen allowances; preempt keeps the top seat it always had).
77
+ * `"preempt"` is a scheduler's yield (`spec.preemptSignal`), not a limit, riding the same suspend: its resume continues the work.
83
78
  *
84
79
  * **Consumers pinning the closed set again**: a `switch` that was exhaustive over the five 5.x members
85
80
  * now falls through on these two.
@@ -854,6 +849,8 @@ export type PendingAction = {
854
849
  * pre-1a.2 checkpoint (deserialized without it), in which case the resume skips the hash check.
855
850
  */
856
851
  boundInputHash: string;
852
+ /** A parked `Skill` call's binding to its card — the skill's name and its two lists ({@link import("./skill-grant.js").skillGrantDigest}, judged by {@link import("./skill-grant.js").parkedSkillApprovalRefusal}); only that row carries it. */
853
+ boundGrantDigest?: string;
857
854
  /**
858
855
  * design/388 §2.5 boundary ② (B18; additive — no checkpoint-version bump, the `previewWithheld`
859
856
  * precedent: an optional group an older reader ignores): the IDENTITY of the row the park minted
@@ -1014,6 +1011,8 @@ export interface CheckpointState {
1014
1011
  writeFace?: {
1015
1012
  face: "open" | "roots";
1016
1013
  };
1014
+ /** The grant rows the run grew ({@link import("./skill-grant.js").frozenSkillGrants}); absent = none, damaged = refused pre-CAS. */
1015
+ skillGrants?: import("./skill-grant.js").SkillGrantRecord[];
1017
1016
  /**
1018
1017
  * design/319 (A ticket) — the session's reminder provenance mark
1019
1018
  * ({@link import("./reminder-mint.js").mintReminderMark}), persisted so a durable resume keeps
@@ -2034,14 +2033,14 @@ export declare function summarizeCheckpoint(cp: Checkpoint): CheckpointSummary;
2034
2033
  * steer seat, which no worker version reads and the host decides itself, or a retired mode-seed member of
2035
2034
  * `inheritedGate`, which an engine at 7.38.x or earlier still reads once).
2036
2035
  * · `invalid_outcome` pre-CAS row-integrity arms: `"real_approval_damaged"`, `"real_approval_forged"`,
2037
- * `"read_face_damaged"`, `"read_face_forged"`, `"write_face_damaged"`, `"constraint_chain_missing"` (all terminal for the row's
2036
+ * `"read_face_damaged"`, `"read_face_forged"`, `"write_face_damaged"`, `"skill_grants_damaged"`, `"constraint_chain_missing"` (all terminal for the row's
2038
2037
  * current bytes — no worker version redeems a damaged row).
2039
2038
  */
2040
2039
  export type CheckpointErrorReason = "version_newer" | "env_factory_missing" | "governed_unwired" | "origin_missing" | "ledger_event_unknown" | "seat_retired" | "real_approval_damaged" | "real_approval_forged" | "read_face_damaged" | "read_face_forged"
2041
2040
  /** `state.writeFace` is present but not a well-formed section. No `_forged` twin: the section is
2042
2041
  * legal-and-absent at every version, so there is no version to judge a fabrication against — and a
2043
2042
  * fabricated "open" still needs the LIVE resolution to say open too. */
2044
- | "write_face_damaged" | "constraint_chain_missing";
2043
+ | "write_face_damaged" | "skill_grants_damaged" | "constraint_chain_missing";
2045
2044
  /**
2046
2045
  * The codes a RESUME's pre-claim configuration screen carries as `detail.code` of a `checkpoint.config_refused` — the four
2047
2046
  * words prepare's mode, output-style and recorded-mode doors throw, held unchanged so one refusal reads the same on a fresh
@@ -2219,7 +2218,7 @@ export declare class CheckpointError extends Error {
2219
2218
  * members. `field` names WHICH part of the decision payload was rejected on a
2220
2219
  * `checkpoint.invalid_outcome` — `"boundCallId"` (the action you decided on has been replaced:
2221
2220
  * re-fetch the pending list), `"boundInputHash"` (the input you reviewed has changed under the same
2222
- * action: re-review), or `"answer"` (the content-ask answer is missing, or was attached to a
2221
+ * action: re-review), `"boundGrantDigest"` (a parked `Skill` approval does not cover the skill the decision selects — another skill, or the card's with other lists: deny, re-activate), or `"answer"` (the content-ask answer is missing, or was attached to a
2223
2222
  * decision that cannot carry one: re-decide with a well-formed answer — ruled 2026-08-04), or
2224
2223
  * `"hostDecision"` (the decision facts are missing, name a decider outside `person`/`sla_timeout`/`policy`,
2225
2224
  * pair the sweep's `sla_timeout` with an allow, or try to supply the minted word), or — design/252 G-7 — `"approver"` (the attribution is not a
@@ -2231,7 +2230,7 @@ export declare class CheckpointError extends Error {
2231
2230
  * 2026-08-03, cross-repo HITL discussion). Additive and optional — `code` remains the only REQUIRED
2232
2231
  * discriminant. */
2233
2232
  readonly detail?: {
2234
- field?: "boundCallId" | "boundInputHash" | "answer" | "hostDecision" | "approver" | "taskConfig" | "inheritedGate";
2233
+ field?: "boundCallId" | "boundInputHash" | "boundGrantDigest" | "answer" | "hostDecision" | "approver" | "taskConfig" | "inheritedGate";
2235
2234
  /** On `checkpoint.config_refused` — prepare's own code for the refusal ({@link CheckpointConfigRefusalCode}), the
2236
2235
  * word a fresh run of the same bad config reports as its `errorCode`. Absent on every other code. */
2237
2236
  code?: CheckpointConfigRefusalCode;
@@ -2411,7 +2410,7 @@ export declare class CheckpointError extends Error {
2411
2410
  * members. `field` names WHICH part of the decision payload was rejected on a
2412
2411
  * `checkpoint.invalid_outcome` — `"boundCallId"` (the action you decided on has been replaced:
2413
2412
  * re-fetch the pending list), `"boundInputHash"` (the input you reviewed has changed under the same
2414
- * action: re-review), or `"answer"` (the content-ask answer is missing, or was attached to a
2413
+ * action: re-review), `"boundGrantDigest"` (a parked `Skill` approval does not cover the skill the decision selects — another skill, or the card's with other lists: deny, re-activate), or `"answer"` (the content-ask answer is missing, or was attached to a
2415
2414
  * decision that cannot carry one: re-decide with a well-formed answer — ruled 2026-08-04), or
2416
2415
  * `"hostDecision"` (the decision facts are missing, name a decider outside `person`/`sla_timeout`/`policy`,
2417
2416
  * pair the sweep's `sla_timeout` with an allow, or try to supply the minted word), or — design/252 G-7 — `"approver"` (the attribution is not a
@@ -2423,7 +2422,7 @@ export declare class CheckpointError extends Error {
2423
2422
  * 2026-08-03, cross-repo HITL discussion). Additive and optional — `code` remains the only REQUIRED
2424
2423
  * discriminant. */
2425
2424
  detail?: {
2426
- field?: "boundCallId" | "boundInputHash" | "answer" | "hostDecision" | "approver" | "taskConfig" | "inheritedGate";
2425
+ field?: "boundCallId" | "boundInputHash" | "boundGrantDigest" | "answer" | "hostDecision" | "approver" | "taskConfig" | "inheritedGate";
2427
2426
  /** On `checkpoint.config_refused` — prepare's own code for the refusal ({@link CheckpointConfigRefusalCode}), the
2428
2427
  * word a fresh run of the same bad config reports as its `errorCode`. Absent on every other code. */
2429
2428
  code?: CheckpointConfigRefusalCode;
@@ -4,8 +4,11 @@ export declare const refusedOutOfContract: (message: string) => PermissionResult
4
4
  * `receiveHookResult` is one of two things, and this tells them apart. */
5
5
  export declare const isEngineRefusal: (d: unknown) => boolean;
6
6
  /** A decision's `decisionReason` — the closed set; the type is derived FROM this list, so a word added to either side is a
7
- * compile error rather than a drift between the type and the screen. Module-private (the package publishes the type). */
8
- declare const DECISION_REASONS: readonly ["rule", "mode", "hook", "safety", "classifier", "persisted_rule", "sandbox", "org_rule", "org_unavailable", "read_only"];
7
+ * compile error rather than a drift between the type and the screen. Module-private (the package publishes the type).
8
+ * `skill`: an active skill's list spoke — the allow layer's skill member cleared the call (`allowed-tools`), or this
9
+ * run's deny row refused it (`disallowed-tools`); distinct from `rule` (a deployment policy) and `persisted_rule` (a
10
+ * person's stored row) because the authority is the skill the run activated. */
11
+ declare const DECISION_REASONS: readonly ["rule", "mode", "hook", "safety", "classifier", "persisted_rule", "sandbox", "org_rule", "org_unavailable", "read_only", "skill"];
9
12
  export type DecisionReason = (typeof DECISION_REASONS)[number];
10
13
  export declare const RETIRED_WINDOW_MEMBER_DENY_MESSAGE: string;
11
14
  export declare const carriesRetiredWindow: (fallback: unknown) => boolean;
@@ -8,7 +8,7 @@ export const refusedOutOfContract = (message) => {
8
8
  return refused;
9
9
  };
10
10
  export const isEngineRefusal = (d) => typeof d === "object" && d !== null && ENGINE_REFUSALS.has(d);
11
- const DECISION_REASONS = Object.freeze(["rule", "mode", "hook", "safety", "classifier", "persisted_rule", "sandbox", "org_rule", "org_unavailable", "read_only"]);
11
+ const DECISION_REASONS = Object.freeze(["rule", "mode", "hook", "safety", "classifier", "persisted_rule", "sandbox", "org_rule", "org_unavailable", "read_only", "skill"]);
12
12
  const DECISION_ACTIONS = Object.keys({ allow: 0, deny: 0, ask: 0 });
13
13
  const GUARDS = Object.keys({ unverifiable_delete: 0, transcript_write: 0, artifact_shared_read: 0 });
14
14
  const RULE_UNREADABLE_WORDS = Object.keys({ content_deny: 0, content_ask: 0, persisted_call: 0 });
@@ -57,6 +57,11 @@ export interface EngineNotice {
57
57
  * at least one type is listed name-only (`nameOnly`) or not at all (`dropped`). Audience `operator`.
58
58
  * `detail: { budgetBytes, total, kept, nameOnly: [name], dropped: [name], sessionId }`; the same
59
59
  * firing rule as its skills sibling below (once per delivered frame, on the carrying message's receipt).
60
+ * - `"skills.allowed_tool_not_granted"` — a loaded skill's `allowed-tools` / `disallowed-tools` holds entries that grant
61
+ * (or refuse) nothing: `*` (it voids an allow list; on a deny list it reads as nothing and the rest still refuses), `mcp__*` (no server named), a write-path or domain
62
+ * entry, a shape the rule grammar does not read. The run goes on and asks for those tools as usual. Audience
63
+ * `operator`; once per prepared task, only when such an entry exists. `detail: { sessionId, entries: [{ skill, list,
64
+ * entry, reason }] }` (`reason` ∈ `wildcard_all` / `namespace_wildcard` / `path_pattern` / `domain` / `content_unsupported` / `grammar`).
60
65
  * - `"skills.listing_truncated"` — the `<skills>` disclosure block did not fit its byte budget, so
61
66
  * at least one skill is listed without its description (`nameOnly`) or, in the pathological case,
62
67
  * not listed at all (`dropped`); the `Skill` tool still serves every one of them. Audience
@@ -775,7 +780,8 @@ export interface EngineNotice {
775
780
  * fail-closed sentence as the tool result); this is its person-facing half, minted at the gate's ONE
776
781
  * deny exit, once per tool call, by `ask-unresolvable-notice.ts`. `detail: { sessionId, toolName,
777
782
  * toolCallId, cause, parkLaneExisted, remedy? }` — `parkLaneExisted` says whether a park lane was armed and
778
- * declined/failed (`true`) or none existed (`false`, the usual shape); `remedy` (the headless auto-deny only)
783
+ * declined/failed (`true`) or none existed (`false`, the usual shape); `remedy` (the two causes that say nobody
784
+ * could be asked — `approver_unavailable` and `no_approver_wired`; never `mandate_unreconstructible`)
779
785
  * is the one sentence naming the caller-side change that would have let the call run, as the tool text carries it. Audience `"user"`: the refused
780
786
  * work is the session user's own; the operator hears it through the sink like every code.
781
787
  *
@@ -4,4 +4,4 @@ import type { SafeNotifier } from "./safe-notify.js";
4
4
  * throwing sink alter the fail-closed outcome — the same swallow posture as every other observer here.
5
5
  * Shared by the gate's dispatch (the phase-1 crash arm, the seat-bound refusal), the reversibility probe's
6
6
  * two failure arms, the approved-edit re-screen and the bounded-await deadline. */
7
- export declare function traceHookCrash(input: ToolGateInput, err: unknown, notifier: SafeNotifier): void;
7
+ export declare function traceHookCrash(input: Pick<ToolGateInput, "onHookError">, err: unknown, notifier: SafeNotifier): void;
@@ -8,7 +8,7 @@ import { writeProtectionMandated, writeProtectionTightenDecision, writeProtectio
8
8
  import { createGateDeadline, awaitWriteProtectionJudgment, gateWaitFailureOf, withGateTimeout } from "./gate-bounded-await.js";
9
9
  import { thrownText } from "./thrown-value.js";
10
10
  import { refuseOutOfContractDecision } from "./tool-policy.js";
11
- import { asksUnstated, unstatedAskMessage } from "./mode-policy.js";
11
+ import { asksUnstated, permissionSeatAt, unstatedAskMessage } from "./mode-policy.js";
12
12
  export async function runGateFold(pass) {
13
13
  const { input, toolName, toolCallId, callSignal, ledger, notifier, adjudicate } = pass;
14
14
  const boundedAwaitDeadlineMs = createGateDeadline(input, notifier);
@@ -69,6 +69,10 @@ export async function runGateFold(pass) {
69
69
  applyShellReadVerdict(pass, verdict, toolName, input.shellGated === true);
70
70
  }
71
71
  }
72
+ if (input.requiresUserInteraction === true && pass.decision.action === "allow") {
73
+ pass.decision = { action: "ask", message: markTightenMessage(toolName, "interaction"), decisionReason: "safety" };
74
+ pass.tightenedBy = "safety";
75
+ }
72
76
  if (input.egress && pass.decision.action === "allow") {
73
77
  pass.decision = {
74
78
  action: "ask",
@@ -169,8 +173,8 @@ export async function runGateFold(pass) {
169
173
  pass.deniedBy = "write_protection";
170
174
  }
171
175
  }
172
- if (asksUnstated(pass.decision, input.permissionSeat, input.permissionMode?.(), pass.hookAllowed === true && pass.decision.updatedInput === undefined)) {
173
- pass.decision = { action: "ask", message: unstatedAskMessage(toolName), ...(pass.policyRewrite !== undefined ? { updatedInput: pass.policyRewrite } : {}) };
176
+ if (asksUnstated(pass.decision, permissionSeatAt(input, pass.policyRewrite !== undefined ? pass.policyRewrite : pass.currentInput)?.seat, input.permissionMode?.(), pass.hookAllowed === true && pass.decision.updatedInput === undefined)) {
177
+ pass.decision = { action: "ask", message: permissionSeatAt(input, pass.policyRewrite !== undefined ? pass.policyRewrite : pass.currentInput)?.question ?? unstatedAskMessage(toolName), ...(pass.policyRewrite !== undefined ? { updatedInput: pass.policyRewrite } : {}) };
174
178
  pass.tightenedBy = "unstated";
175
179
  }
176
180
  return await pass.afterFold(pass);