@sema-agent/core 7.44.0 → 7.45.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 (75) hide show
  1. package/CHANGELOG.md +172 -0
  2. package/dist/agents/subagent.js +33 -14
  3. package/dist/agents/tool-filter.d.ts +65 -4
  4. package/dist/agents/tool-filter.js +127 -4
  5. package/dist/core/agent-definition.d.ts +2 -2
  6. package/dist/core/allow-entry.d.ts +25 -1
  7. package/dist/core/allow-entry.js +29 -4
  8. package/dist/core/ask-origin.d.ts +23 -4
  9. package/dist/core/ask-origin.js +10 -4
  10. package/dist/core/checkpoint-store.d.ts +2 -2
  11. package/dist/core/decision-receipt.js +1 -1
  12. package/dist/core/effective-path-target.d.ts +7 -6
  13. package/dist/core/effective-path-target.js +3 -7
  14. package/dist/core/engine-notice.d.ts +20 -3
  15. package/dist/core/engine-notice.js +12 -6
  16. package/dist/core/gate-lanes.js +2 -0
  17. package/dist/core/gate-outcome.d.ts +5 -0
  18. package/dist/core/gate-outcome.js +2 -0
  19. package/dist/core/hooks.d.ts +2 -0
  20. package/dist/core/memory-engine/content-origin.d.ts +0 -34
  21. package/dist/core/memory-engine/content-origin.js +9 -4
  22. package/dist/core/memory-engine/layout.d.ts +5 -1
  23. package/dist/core/memory-engine/layout.js +1 -0
  24. package/dist/core/permission-rules.d.ts +4 -0
  25. package/dist/core/permission-rules.js +5 -2
  26. package/dist/core/real-approval.d.ts +9 -6
  27. package/dist/core/real-approval.js +5 -5
  28. package/dist/core/runner/child-tree.d.ts +13 -0
  29. package/dist/core/runner/child-tree.js +3 -0
  30. package/dist/core/runner/contracts.d.ts +8 -2
  31. package/dist/core/runner/prepare-ask-lane.d.ts +2 -0
  32. package/dist/core/runner/prepare-ask-lane.js +2 -2
  33. package/dist/core/runner/prepare-config-doors.js +3 -1
  34. package/dist/core/runner/prepare-execution-env.js +2 -1
  35. package/dist/core/runner/prepare-gate-stations.d.ts +3 -0
  36. package/dist/core/runner/prepare-gate-stations.js +2 -1
  37. package/dist/core/runner/prepare-listings.d.ts +4 -4
  38. package/dist/core/runner/prepare-listings.js +2 -22
  39. package/dist/core/runner/prepare-park-ask.d.ts +2 -0
  40. package/dist/core/runner/prepare-park-ask.js +2 -2
  41. package/dist/core/runner/prepare-policy-chain.d.ts +2 -0
  42. package/dist/core/runner/prepare-policy-chain.js +4 -2
  43. package/dist/core/runner/prepare-task.js +10 -6
  44. package/dist/core/runner/resume-policy-outcome.js +2 -1
  45. package/dist/core/runner/run-leg.js +1 -1
  46. package/dist/core/runner/run-notification-lane.js +2 -2
  47. package/dist/core/runner/runtask.d.ts +2 -0
  48. package/dist/core/runner/runtask.js +2 -0
  49. package/dist/core/runner/stream-steer-verb.js +2 -2
  50. package/dist/core/runner/sub-agent-deny-lists.d.ts +32 -0
  51. package/dist/core/runner/sub-agent-deny-lists.js +121 -0
  52. package/dist/core/runner/synthetic-tools.d.ts +1 -1
  53. package/dist/core/runner/synthetic-tools.js +3 -2
  54. package/dist/core/session-policy-store.d.ts +6 -2
  55. package/dist/core/session-policy-store.js +2 -1
  56. package/dist/core/skill-grant.d.ts +10 -2
  57. package/dist/core/skill-grant.js +20 -4
  58. package/dist/core/task-notification.d.ts +12 -1
  59. package/dist/core/task-notification.js +13 -1
  60. package/dist/core/task-spec.d.ts +5 -3
  61. package/dist/core/tool-policy.d.ts +15 -4
  62. package/dist/core/tool-policy.js +9 -13
  63. package/dist/core/tool-spec.d.ts +20 -2
  64. package/dist/core/types.d.ts +1 -1
  65. package/dist/core/types.js +1 -1
  66. package/dist/index.d.ts +5 -3
  67. package/dist/index.js +5 -3
  68. package/dist/orchestration/workflow.js +2 -1
  69. package/dist/tools/fs/removal-targets.d.ts +82 -0
  70. package/dist/tools/fs/removal-targets.js +236 -0
  71. package/dist/tools/fs/shell-read-write-targets.d.ts +27 -0
  72. package/dist/tools/fs/shell-read-write-targets.js +1 -1
  73. package/package.json +1 -1
  74. package/test/export-surface.snapshot.json +33 -1
  75. package/wire-changes/core@7.45.0.json +1084 -0
@@ -1,12 +1,135 @@
1
+ import { parsePermissionRule } from "../core/permission-rule-syntax.js";
2
+ export function readDeniedEntry(entry) {
3
+ if (typeof entry !== "string")
4
+ return { kind: "not_a_string", type: valueTypeLabel(entry) };
5
+ if (entry !== entry.trim())
6
+ return { kind: "name", name: entry, content: false };
7
+ const parsed = parsePermissionRule(entry);
8
+ return { kind: "name", name: parsed.toolName, content: parsed.ruleContent !== undefined };
9
+ }
10
+ export function valueTypeLabel(value) {
11
+ if (value === null)
12
+ return "null";
13
+ switch (typeof value) {
14
+ case "string":
15
+ return "a string";
16
+ case "undefined":
17
+ return "undefined";
18
+ case "number":
19
+ return "a number";
20
+ case "boolean":
21
+ return "a boolean";
22
+ case "bigint":
23
+ return "a bigint";
24
+ case "symbol":
25
+ return "a symbol";
26
+ case "function":
27
+ return "a function";
28
+ default:
29
+ try {
30
+ return Array.isArray(value) ? "an array" : "an object";
31
+ }
32
+ catch {
33
+ return "an object";
34
+ }
35
+ }
36
+ }
37
+ const isList = (value) => Array.isArray(value);
38
+ const UNREADABLE_LIST = Object.freeze({});
39
+ export function readToolNameList(value) {
40
+ if (value === UNREADABLE_LIST)
41
+ return { kind: "unreadable" };
42
+ let list;
43
+ try {
44
+ list = isList(value) ? value : undefined;
45
+ }
46
+ catch {
47
+ return { kind: "not_a_list", type: "an object" };
48
+ }
49
+ if (list === undefined)
50
+ return { kind: "not_a_list", type: valueTypeLabel(value) };
51
+ try {
52
+ const entries = [];
53
+ for (let i = 0; i < list.length; i++)
54
+ entries.push(list[i]);
55
+ return { kind: "entries", entries };
56
+ }
57
+ catch {
58
+ return { kind: "unreadable" };
59
+ }
60
+ }
61
+ export function snapshotToolNameList(value) {
62
+ const read = readToolNameList(value);
63
+ return read.kind === "entries" ? read.entries : read.kind === "unreadable" ? UNREADABLE_LIST : value;
64
+ }
65
+ const LIST_NOUN = { allowed: "allowed tools", denied: "denied tools" };
66
+ const LIST_NAME = { allowed: "an allowed-tools list", denied: "a denied-tools list" };
67
+ export function notAStringSentence(where, side, index, type) {
68
+ return `${where} declares in its ${LIST_NOUN[side]}, as entry ${index + 1}, a value that is ${type}, not a tool name — ${LIST_NAME[side]} holds tool names, each a string. Fix the entry.`;
69
+ }
70
+ export function notAListSentence(where, side, list) {
71
+ return list.kind === "not_a_list"
72
+ ? `${where} declares in its ${LIST_NOUN[side]} a value that is ${list.type}, not a list of tool names. Fix the list.`
73
+ : `${where} declares in its ${LIST_NOUN[side]} a list whose entries cannot be read (an accessor or a proxy trap threw). Fix the list.`;
74
+ }
75
+ export function toolNameListFlaw(where, side, value) {
76
+ if (value === undefined || value === null)
77
+ return undefined;
78
+ const list = readToolNameList(value);
79
+ if (list.kind !== "entries")
80
+ return notAListSentence(where, side, list);
81
+ for (let i = 0; i < list.entries.length; i++) {
82
+ const entry = list.entries[i];
83
+ if (typeof entry !== "string")
84
+ return notAStringSentence(where, side, i, valueTypeLabel(entry));
85
+ }
86
+ return undefined;
87
+ }
88
+ export function snapshotDefinitionLists(def) {
89
+ const copy = { ...def };
90
+ if (copy.allowTools !== undefined)
91
+ copy.allowTools = snapshotToolNameList(copy.allowTools);
92
+ if (copy.denyTools !== undefined)
93
+ copy.denyTools = snapshotToolNameList(copy.denyTools);
94
+ return copy;
95
+ }
96
+ export function allowedToolNames(allowTools) {
97
+ if (allowTools === undefined || allowTools === null)
98
+ return undefined;
99
+ const list = readToolNameList(allowTools);
100
+ if (list.kind !== "entries")
101
+ return new Set();
102
+ if (list.entries.includes("*"))
103
+ return undefined;
104
+ const names = new Set();
105
+ for (const entry of list.entries)
106
+ if (typeof entry === "string")
107
+ names.add(entry);
108
+ return names;
109
+ }
110
+ function deniedNames(denyTools) {
111
+ const names = new Set();
112
+ const list = readToolNameList(denyTools);
113
+ if (list.kind !== "entries")
114
+ return names;
115
+ for (const entry of list.entries) {
116
+ const read = readDeniedEntry(entry);
117
+ if (read.kind === "name")
118
+ names.add(read.name);
119
+ }
120
+ return names;
121
+ }
1
122
  export function toolNameAllowed(name, allowTools, denyTools) {
2
- const allow = allowTools !== undefined && !allowTools.includes("*") ? new Set(allowTools) : undefined;
3
- const deny = denyTools && denyTools.length > 0 ? new Set(denyTools) : undefined;
123
+ const allow = allowedToolNames(allowTools);
124
+ const denied = deniedNames(denyTools);
125
+ const deny = denied.size > 0 ? denied : undefined;
4
126
  const n = name;
5
127
  return (!allow || allow.has(n)) && (!deny || !deny.has(n));
6
128
  }
7
129
  export function resolveToolSubset(pool, allowTools, denyTools) {
8
- const allow = allowTools !== undefined && !allowTools.includes("*") ? new Set(allowTools) : undefined;
9
- const deny = denyTools && denyTools.length > 0 ? new Set(denyTools) : undefined;
130
+ const allow = allowedToolNames(allowTools);
131
+ const denied = deniedNames(denyTools);
132
+ const deny = denied.size > 0 ? denied : undefined;
10
133
  if (!allow && !deny)
11
134
  return [...pool];
12
135
  return pool.filter((t) => {
@@ -33,7 +33,7 @@ export interface AgentDefinition {
33
33
  * the delegation tool's name cannot re-delegate (CC Explore/Plan `disallowedTools` include Agent,
34
34
  * pretty.js:409189/486948). */
35
35
  allowTools?: string[];
36
- /** Tool denylist (a deny always wins). See {@link allowTools} for the per-agent delegation-tool note. */
36
+ /** Tool denylist (a deny always wins), tool NAMES: `Tool(content)` hides the whole `Tool`. See {@link allowTools} for the per-agent delegation-tool note. */
37
37
  denyTools?: string[];
38
38
  /**
39
39
  * F7 (CC 2.1.198 parity, per-agent `skills` — 锚 pretty.js:422126 `e.skills ?? []`, GA/ungated in 198:
@@ -116,7 +116,7 @@ export interface SkillSpec {
116
116
  * either list asks once (`Execute skill: <name>`, the card listing what it releases); a skill the person invoked by
117
117
  * name ({@link import("./task-spec.js").TaskSpec.userInvokedSkills}) grants without asking. The grant holds for that
118
118
  * run (a steer it takes in does not end it) and the next runTask starts without it. An entry this grammar reads as nothing — `*` (it voids an allow list; on a deny list it is inert and the rest of the
119
- * list still refuses), `mcp__*`, a write path, a domain — grants nothing and is announced at the start of the run (`permission.allowed_tool_not_granted`).
119
+ * list still refuses), `mcp__*`, a write path, a domain — grants nothing and is announced at the start of the run (`permission.allowed_tool_not_granted`). A table whose value is not a list (a string, an object…) reads as one such entry; `null` is no table.
120
120
  */
121
121
  allowedTools?: string[];
122
122
  /** The tools refused for the rest of the run this skill is activated in — the frontmatter `disallowed-tools` list,
@@ -3,17 +3,33 @@ import { type ToolCallRequest } from "./tool-policy.js";
3
3
  import { type AllowRuleMember, type AllowRuleShadow } from "./allow-rule-shadow.js";
4
4
  import { type PersistedRule } from "./permission-rule-model.js";
5
5
  import { type AskClass, type AssertAllKeysHandled, type PermissionMode, type PersistedRuleMandate } from "./ask-origin.js";
6
+ import { type ToolNameListReading } from "../agents/tool-filter.js";
6
7
  import { type EngineNotice } from "./engine-notice.js";
7
8
  /** Why an entry grants (or refuses) nothing — the closed set the start-of-run notice names, for every list. `tool_name`: the
8
9
  * entry's tool name is one the name judgment reads as stale against the run's roster (`verdict` says which); `near_miss`: the
9
10
  * run mounts no tool of that exact name but mounts one that differs only by case (`currentName` is its spelling). */
10
11
  export declare const ALLOW_ENTRY_INERT_REASONS: readonly ["wildcard_all", "namespace_wildcard", "path_pattern", "domain", "content_unsupported", "grammar", "tool_name", "near_miss"];
11
12
  export type AllowEntryInertReason = (typeof ALLOW_ENTRY_INERT_REASONS)[number];
12
- /** The facts beside a `tool_name` / `near_miss` reason: the judgment's word, and the spelling to write instead when there is one. */
13
+ /** The facts beside a reason: for `tool_name` / `near_miss` the judgment's word and the spelling to write instead when there is
14
+ * one; for a list member that is not a string (`grammar`) the TYPE of the value (`a number`, `null`, `an object`…) — `entry` then
15
+ * holds {@link nonStringMemberLabel}, never the value. */
13
16
  export interface AllowEntryInertDetail {
14
17
  readonly verdict?: Exclude<RuleToolNameVerdict["kind"], "current">;
15
18
  readonly currentName?: string;
19
+ readonly valueType?: string;
16
20
  }
21
+ /**
22
+ * How a list member that is not a string is named wherever a list's entries are named (the inert record's `entry`, the Skill
23
+ * card's binding): its TYPE, in a spelling no readable entry can have — a readable entry's name holds no whitespace and a
24
+ * name-only entry holds no parenthesis, and this label holds a space and none. It can equal only another entry that reads as
25
+ * nothing, so a binding that holds it never stands for an entry that grants or refuses.
26
+ */
27
+ export declare function nonStringMemberLabel(value: unknown): string;
28
+ /** How a tool-name list's VALUE that is not a list stands as one entry (the same kind of spelling as {@link nonStringMemberLabel}: it
29
+ * reads as `grammar`): its TYPE, or that its entries could not be read — never the value. */
30
+ export declare function nonListValueLabel(read: Exclude<ToolNameListReading, {
31
+ kind: "entries";
32
+ }>): string;
17
33
  /** One entry a list reads as nothing. */
18
34
  export type AllowEntryInert = {
19
35
  readonly entry: string;
@@ -189,6 +205,7 @@ declare const SHADOW_REASON: {
189
205
  readonly content_question: "the call is the engine's own question to a person";
190
206
  readonly hook: "a PreToolUse hook asks about this call — the deployment's own screening is answered by a person";
191
207
  readonly ancestor: "the task that delegated this one owns this question";
208
+ readonly dangerous_delete: "this removal targets a protected location (the filesystem root, a top-level directory, a home directory, or a workspace directory or one of its ancestors) — it is cleared by confirming this call, never by an allow rule";
192
209
  readonly unverifiable_delete: "this removal could not be verified safe — it is cleared by confirming this call, never by an allow rule";
193
210
  readonly transcript_write: "this write targets the session transcript — it is cleared by confirming this call, never by an allow rule";
194
211
  readonly org_rule: "an organization policy rule requires approval for this call";
@@ -244,6 +261,13 @@ export type AllowEntryInertNote = ({
244
261
  } & AllowEntryInert) | ({
245
262
  readonly source: AllowEntrySource;
246
263
  } & AllowEntryInert);
264
+ /**
265
+ * THE ONE WRITER of a list entry into a sentence (the start-of-run notice, a skill's refusal and its decisions, a sub-agent
266
+ * list's refusals and advisories): the entry as a JSON string — quotes, backslashes and the usual escapes — with every
267
+ * character that prints as nothing, or as a space, written as `\uXXXX` (`\u{…}` beyond the BMP) and a run of spaces kept
268
+ * space by space, so what a person reads is what the engine read. Capped on what is printed.
269
+ */
270
+ export declare function renderListEntry(entry: string): string;
247
271
  /**
248
272
  * One entry that reads as nothing, as a person reads it: the entry as written (quoted, so a surrounding space shows) and why
249
273
  * — the reason word, with the spelling to write when the reading has one. The one phrasing the start-of-run notice and a
@@ -9,12 +9,19 @@ import { pathRuleLaneAnswer } from "./runner/permission-rule-lanes.js";
9
9
  import { splitShellCompoundSegments } from "../tools/fs/bash-readonly-classifier.js";
10
10
  import { mandateDisclosureOf } from "./ask-origin.js";
11
11
  import { inlineUntrusted } from "./untrusted-text.js";
12
+ import { valueTypeLabel } from "../agents/tool-filter.js";
12
13
  import { deliverEngineNotice } from "./engine-notice.js";
13
14
  export const ALLOW_ENTRY_INERT_REASONS = Object.freeze(["wildcard_all", "namespace_wildcard", "path_pattern", "domain", "content_unsupported", "grammar", "tool_name", "near_miss"]);
15
+ export function nonStringMemberLabel(value) {
16
+ return `<not a string: ${valueTypeLabel(value)}>`;
17
+ }
18
+ export function nonListValueLabel(read) {
19
+ return read.kind === "not_a_list" ? `<not a list: ${read.type}>` : "<a list whose entries cannot be read>";
20
+ }
14
21
  function ruleRowOf(parsed) {
15
22
  return { ...parsed, scope: { kind: "global" }, adds: [{ dot: { actor: "list", counter: 1 }, origin: "user", createdAt: "" }] };
16
23
  }
17
- const TOOL_NAME_SPELLING = /^[^\s()]+$/;
24
+ const TOOL_NAME_SPELLING = /^[^\s()\p{Cc}\p{Cf}]+$/u;
18
25
  function mountedSpellingOf(name, roster) {
19
26
  const folded = name.toLowerCase();
20
27
  for (const mounted of roster)
@@ -36,6 +43,8 @@ function toolNameReading(name, roster) {
36
43
  function readAllowEntry(raw, behavior, roster) {
37
44
  const inert = (reason) => ({ inert: reason });
38
45
  const row = (compiled) => ({ compiled });
46
+ if (typeof raw !== "string")
47
+ return { inert: "grammar", detail: { valueType: valueTypeLabel(raw) } };
39
48
  if (raw !== raw.trim())
40
49
  return inert("grammar");
41
50
  const parsed = parsePermissionRule(raw);
@@ -94,11 +103,11 @@ export function compileAllowList(entries, behavior, roster) {
94
103
  const rows = [];
95
104
  const inert = [];
96
105
  for (const entry of entries) {
97
- const read = voided ? { inert: "wildcard_all" } : readAllowEntry(entry, behavior, roster);
106
+ const read = typeof entry !== "string" || !voided ? readAllowEntry(entry, behavior, roster) : { inert: "wildcard_all" };
98
107
  if ("compiled" in read)
99
108
  rows.push({ entry, compiled: read.compiled });
100
109
  else
101
- inert.push({ entry, reason: read.inert, ...read.detail });
110
+ inert.push({ entry: typeof entry === "string" ? entry : nonStringMemberLabel(entry), reason: read.inert, ...read.detail });
102
111
  }
103
112
  return { rows, inert };
104
113
  }
@@ -307,6 +316,7 @@ const SHADOW_REASON = {
307
316
  content_question: "the call is the engine's own question to a person",
308
317
  hook: "a PreToolUse hook asks about this call — the deployment's own screening is answered by a person",
309
318
  ancestor: "the task that delegated this one owns this question",
319
+ dangerous_delete: "this removal targets a protected location (the filesystem root, a top-level directory, a home directory, or a workspace directory or one of its ancestors) — it is cleared by confirming this call, never by an allow rule",
310
320
  unverifiable_delete: "this removal could not be verified safe — it is cleared by confirming this call, never by an allow rule",
311
321
  transcript_write: "this write targets the session transcript — it is cleared by confirming this call, never by an allow rule",
312
322
  org_rule: "an organization policy rule requires approval for this call",
@@ -366,7 +376,22 @@ export function cardDisclosureOf(decision, fallbackMessage) {
366
376
  }
367
377
  export const ALLOWED_TOOL_NOT_GRANTED_CODE = "permission.allowed_tool_not_granted";
368
378
  const NOTICE_ENTRIES_SHOWN = 5;
379
+ const UNSEEN_IN_ENTRY = /[\p{Cc}\p{Cf}\p{Zl}\p{Zp}\p{Zs}]/gu;
380
+ const ENTRY_RENDER_MAX = 122;
381
+ function escapedCodePoint(ch) {
382
+ const cp = ch.codePointAt(0) ?? 0;
383
+ const hex = cp.toString(16).padStart(4, "0");
384
+ return cp > 0xffff ? `\\u{${hex}}` : `\\u${hex}`;
385
+ }
386
+ export function renderListEntry(entry) {
387
+ const quoted = JSON.stringify(entry)
388
+ .replace(/ {2,}/g, (run) => "\\u0020".repeat(run.length))
389
+ .replace(UNSEEN_IN_ENTRY, (ch) => (ch === " " ? ch : escapedCodePoint(ch)));
390
+ return inlineUntrusted(quoted, ENTRY_RENDER_MAX);
391
+ }
369
392
  export function inertEntryPhrase(e) {
393
+ if (e.valueType !== undefined)
394
+ return `a value that is ${e.valueType} (grammar: not a string)`;
370
395
  const spelling = e.currentName !== undefined ? inlineUntrusted(e.currentName, 120) : undefined;
371
396
  const why = (() => {
372
397
  if (e.reason === "near_miss")
@@ -386,7 +411,7 @@ export function inertEntryPhrase(e) {
386
411
  }
387
412
  }
388
413
  })();
389
- return `${inlineUntrusted(JSON.stringify(e.entry), 122)} (${why})`;
414
+ return `${renderListEntry(e.entry)} (${why})`;
390
415
  }
391
416
  export function allowedToolsNotGrantedNotice(inert, sessionId) {
392
417
  const where = (e) => (e.source === "skill" ? `skill ${inlineUntrusted(e.skill, 80)} ${e.list}` : e.source === "request" ? "this run's allowedTools" : "the deployment's allowedTools");
@@ -25,6 +25,7 @@
25
25
  * the ask). Both stay where they are and are consumed beside the origin, not folded into it.
26
26
  */
27
27
  import type { PermissionResult } from "./tool-policy.js";
28
+ import type { ContainmentFace } from "../tools/fs/containment-face.js";
28
29
  /**
29
30
  * The closed set, in AUTHORITY order — the order {@link askOriginOf} resolves a fold in when several
30
31
  * facts hold on one call (a hook ask folded over a policy ask, an org stamp over either). The order
@@ -448,8 +449,12 @@ export type ToolMarkSource = "deployment" | "mcp_annotation" | "a2a_default";
448
449
  * `classifier_no_ruling` is a classifier station's question to a person because the auto-mode classification did not rule
449
450
  * (`classifierNoRuling` — it could not run, or replied with no verdict): the denial-limit fallback's row (only a person
450
451
  * answers it; the classifier is never asked again).
452
+ * `dangerous_delete` is a removal whose target is a protected location (`dangerousRemovalOf`: the filesystem root, a
453
+ * top-level directory, a home directory, or a workspace directory — the task root, the working directory, an additional
454
+ * working directory — or an ancestor of one): the reference implementation's dangerous-removal breaker, which no mode,
455
+ * allow rule or classifier answers. `unverifiable_delete` is its sibling for a target the command text cannot resolve.
451
456
  */
452
- 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", "classifier_no_ruling", "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"];
457
+ export declare const ASK_CLASSES: readonly ["content_question", "hook", "ancestor", "dangerous_delete", "unverifiable_delete", "transcript_write", "org_rule", "governance_unavailable", "rule_unreadable", "ask_rule", "denial_limit_fallback", "classifier_no_ruling", "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"];
453
458
  export type AskClass = (typeof ASK_CLASSES)[number];
454
459
  /** Membership test for {@link AskClass} — the word crosses the wire on `tool_end.gate.disposition`. */
455
460
  export declare function isAskClass(v: unknown): v is AskClass;
@@ -457,7 +462,8 @@ export declare function isAskClass(v: unknown): v is AskClass;
457
462
  export interface ModeArbitrationRow {
458
463
  /** The ask still reaches a person under `bypassPermissions` (the reference implementation's `bypassImmune`). */
459
464
  readonly bypassImmune: boolean;
460
- /** The auto-mode classifier may answer it. */
465
+ /** The auto-mode classifier may answer it — as the class stands on its own: the folded row of an ask
466
+ * ({@link foldArbitrationRow}) also takes the read face the ask was raised under. */
461
467
  readonly classifierEligible: boolean;
462
468
  /** The sandbox-admission leg may admit it (containment holds the effect). */
463
469
  readonly sandboxAdmission: boolean;
@@ -473,6 +479,8 @@ export interface ModeArbitrationRow {
473
479
  * sandbox columns are the fold of the two former per-word lane tables (only `probe` moved: the classifier may
474
480
  * now answer it — the read boundary's unestablishable spelling is fed to it as a deny-table row instead), and
475
481
  * `ruleMayClear` restates {@link ruleClearanceOf}'s conclusion per class (a real-approval bit is never rule-clearable).
482
+ * `probe`'s `classifierEligible` is the OPEN read face's reading: a narrowed (`roots`) face withholds the classifier from the class,
483
+ * folded where the ask's row is ({@link foldArbitrationRow}).
476
484
  * `mode` is the permission mode's OWN question (`plan`'s two sentences, `decisionReason: "mode"`): the reference
477
485
  * implementation puts it to a person past every allow rule and never to an automatic lane, and its bypass conversion
478
486
  * releases it like any other non-immune ask. `unstated` is the reference implementation's passthrough conversion: an allow
@@ -503,6 +511,13 @@ export declare const MODE_ARBITRATION: {
503
511
  readonly ruleMayClear: false;
504
512
  readonly hookAskFloor: false;
505
513
  };
514
+ readonly dangerous_delete: {
515
+ readonly bypassImmune: true;
516
+ readonly classifierEligible: false;
517
+ readonly sandboxAdmission: false;
518
+ readonly ruleMayClear: false;
519
+ readonly hookAskFloor: true;
520
+ };
506
521
  readonly unverifiable_delete: {
507
522
  readonly bypassImmune: true;
508
523
  readonly classifierEligible: true;
@@ -683,6 +698,8 @@ export interface AskClassFacts extends AskOriginFacts {
683
698
  readonly toolMarkSources?: ReadonlySet<ToolMarkSource>;
684
699
  readonly a2aIsolatePeers?: boolean;
685
700
  readonly ancestorClasses?: ReadonlySet<AskClass>;
701
+ /** The read face the leg this ask was raised on resolved ({@link ContainmentFace}); absent ⇒ none resolved (a leg with no hands). */
702
+ readonly readFace?: ContainmentFace;
686
703
  }
687
704
  /** The classification the arbitration reads: the whole set, the disclosed word (the set's first member in
688
705
  * {@link ASK_CLASSES} order) and the row folded across the set. */
@@ -718,8 +735,10 @@ export declare function decisionAskClassesOf(d: Extract<PermissionResult, {
718
735
  }>): Set<AskClass>;
719
736
  /** Fold a class set onto one row: `bypassImmune` and `hookAskFloor` by OR (any immune member keeps the ask; any floored
720
737
  * member floors a hook's ask), the three permissive switches by AND (one refusing member withholds the lane). An empty
721
- * set is `ordinary`. */
722
- export declare function foldArbitrationRow(classes: ReadonlySet<AskClass>): ModeArbitrationRow;
738
+ * set is `ordinary`. `readFace` is the face the ask was raised under: `roots` takes the classifier's lane from a set that
739
+ * holds a class the narrowed face withholds from it — the one place the face enters, so every reader of `classifierEligible`
740
+ * reads it already folded. */
741
+ export declare function foldArbitrationRow(classes: ReadonlySet<AskClass>, readFace?: ContainmentFace): ModeArbitrationRow;
723
742
  /**
724
743
  * The ONE class derivation: the decision half ({@link decisionAskClassesOf}) joined with the gate's own facts —
725
744
  * the question tool, an ancestor's mark, the org layer's two arms, the persisted lane's unreadable call, the tool
@@ -170,6 +170,7 @@ export const ASK_CLASSES = Object.freeze([
170
170
  "content_question",
171
171
  "hook",
172
172
  "ancestor",
173
+ "dangerous_delete",
173
174
  "unverifiable_delete",
174
175
  "transcript_write",
175
176
  "org_rule",
@@ -200,6 +201,7 @@ export const MODE_ARBITRATION = {
200
201
  content_question: { bypassImmune: true, classifierEligible: false, sandboxAdmission: false, ruleMayClear: false, hookAskFloor: true },
201
202
  hook: { bypassImmune: true, classifierEligible: false, sandboxAdmission: false, ruleMayClear: false, hookAskFloor: false },
202
203
  ancestor: { bypassImmune: true, classifierEligible: false, sandboxAdmission: false, ruleMayClear: false, hookAskFloor: false },
204
+ dangerous_delete: { bypassImmune: true, classifierEligible: false, sandboxAdmission: false, ruleMayClear: false, hookAskFloor: true },
203
205
  unverifiable_delete: { bypassImmune: true, classifierEligible: true, sandboxAdmission: false, ruleMayClear: false, hookAskFloor: true },
204
206
  transcript_write: { bypassImmune: false, classifierEligible: true, sandboxAdmission: false, ruleMayClear: false, hookAskFloor: true },
205
207
  org_rule: { bypassImmune: true, classifierEligible: false, sandboxAdmission: false, ruleMayClear: false, hookAskFloor: false },
@@ -222,7 +224,8 @@ export const MODE_ARBITRATION = {
222
224
  unstated: { bypassImmune: false, classifierEligible: true, sandboxAdmission: false, ruleMayClear: true, hookAskFloor: false },
223
225
  ordinary: { bypassImmune: false, classifierEligible: true, sandboxAdmission: true, ruleMayClear: true, hookAskFloor: false },
224
226
  };
225
- export const RRA_EXPLAINERS = new Set(["unverifiable_delete", "transcript_write", "org_rule", "governance_unavailable", "rule_unreadable", "denial_limit_fallback", "classifier_no_ruling", "ancestor", "artifact_shared_read", "requires_user_interaction"]);
227
+ const CLASSIFIER_WITHHELD_ON_ROOTS = new Set(["probe"]);
228
+ export const RRA_EXPLAINERS = new Set(["dangerous_delete", "unverifiable_delete", "transcript_write", "org_rule", "governance_unavailable", "rule_unreadable", "denial_limit_fallback", "classifier_no_ruling", "ancestor", "artifact_shared_read", "requires_user_interaction"]);
226
229
  export const RELEASABLE_ASK_CLASSES = ASK_CLASSES.filter((c) => !MODE_ARBITRATION[c].bypassImmune);
227
230
  export const MODE_RELEASE_REASONS = Object.freeze(["rule_unreadable"]);
228
231
  const MODE_RELEASE_REASON_SET = new Set(MODE_RELEASE_REASONS);
@@ -243,6 +246,8 @@ export function screenAskClassMembers(askClass, classes) {
243
246
  }
244
247
  export function decisionAskClassesOf(d) {
245
248
  const out = new Set();
249
+ if (d.guard === "dangerous_delete")
250
+ out.add("dangerous_delete");
246
251
  if (d.guard === "unverifiable_delete")
247
252
  out.add("unverifiable_delete");
248
253
  if (d.guard === "transcript_write")
@@ -273,10 +278,11 @@ export function decisionAskClassesOf(d) {
273
278
  out.add(c);
274
279
  return out;
275
280
  }
276
- export function foldArbitrationRow(classes) {
281
+ export function foldArbitrationRow(classes, readFace) {
277
282
  if (classes.size === 0)
278
283
  return MODE_ARBITRATION.ordinary;
279
- let bypassImmune = false, classifierEligible = true, sandboxAdmission = true, ruleMayClear = true, hookAskFloor = false;
284
+ const withheld = readFace === "roots" && [...classes].some((c) => CLASSIFIER_WITHHELD_ON_ROOTS.has(c));
285
+ let bypassImmune = false, classifierEligible = !withheld, sandboxAdmission = true, ruleMayClear = true, hookAskFloor = false;
280
286
  for (const c of classes) {
281
287
  const row = MODE_ARBITRATION[c];
282
288
  bypassImmune ||= row.bypassImmune;
@@ -334,7 +340,7 @@ export function classifyAsk(d, facts) {
334
340
  if (classes.size === 0)
335
341
  classes.add("ordinary");
336
342
  const askClass = ASK_CLASSES.find((c) => classes.has(c));
337
- return { classes, askClass, row: foldArbitrationRow(classes) };
343
+ return { classes, askClass, row: foldArbitrationRow(classes, facts.readFace) };
338
344
  }
339
345
  export function headlessDenialMessage(toolName, askMessage) {
340
346
  return `Permission for this tool use was denied: it requires interactive approval, and permission prompts are not available in this session (nobody can be asked to approve "${toolName}" in this run — 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}`;
@@ -1,4 +1,4 @@
1
- import type { GateOutcome } from "./gate-outcome.js";
1
+ import type { GateOutcome, HostDecider } from "./gate-outcome.js";
2
2
  import { type ExecutionOutcomeRecordWord } from "./checkpoint-execution-record.js";
3
3
  import type { CheckpointPause } from "./pause-registry.js";
4
4
  import type { SystemInjectionPriority } from "./task-notification.js";
@@ -625,7 +625,7 @@ export type ResumeOutcome = {
625
625
  * authorization; a replay states its own facts.
626
626
  */
627
627
  hostDecision: {
628
- decidedBy: "person" | "sla_timeout" | "policy";
628
+ decidedBy: HostDecider;
629
629
  approver?: string;
630
630
  };
631
631
  /**
@@ -11,7 +11,7 @@ export const refusedOutOfContract = (message) => {
11
11
  export const isEngineRefusal = (d) => typeof d === "object" && d !== null && ENGINE_REFUSALS.has(d);
12
12
  const DECISION_REASONS = Object.freeze(["rule", "mode", "hook", "safety", "classifier", "persisted_rule", "sandbox", "org_rule", "org_unavailable", "read_only", "skill", "deployment_rule", "request_rule"]);
13
13
  const DECISION_ACTIONS = Object.keys({ allow: 0, deny: 0, ask: 0 });
14
- const GUARDS = Object.keys({ unverifiable_delete: 0, transcript_write: 0, artifact_shared_read: 0 });
14
+ const GUARDS = Object.keys({ dangerous_delete: 0, unverifiable_delete: 0, transcript_write: 0, artifact_shared_read: 0 });
15
15
  const RULE_UNREADABLE_WORDS = Object.keys({ content_deny: 0, content_ask: 0, persisted_call: 0 });
16
16
  const AUTHORED_BY_WORDS = Object.keys({ deployment: 0 });
17
17
  const memberOf = (words) => {
@@ -28,8 +28,8 @@
28
28
  *
29
29
  * Nothing here judges anything: it answers "which absolute path is this call about". Containment, the
30
30
  * sensitive-path deny face and the rule precedence walk are unchanged and still run where they ran.
31
- * `undefined` means "this call has no path this reading can locate" — the documented fail-toward-asking
32
- * posture of both lanes (no refusal on the tightening side, no grant on the allow side).
31
+ * `undefined` means "this call names no path" (no declaration, a non-string slot, an absent slot that targets
32
+ * nothing); a path the call does name whose base this reading lacks is `unreadable` — never a guess, never silence.
33
33
  *
34
34
  * RepoMap shares the search tools' root-relative `path` semantics but declares NO path target at all (no
35
35
  * rule family reaches it, and the rule grammar is derived from the catalog's path targets), so it has no
@@ -71,8 +71,8 @@ export declare function splitAbsoluteGlobPattern(pattern: string, family: PathFa
71
71
  } | undefined;
72
72
  /**
73
73
  * The reading of a call's path slot under ONE declaration: `path` = the absolute target's identity; `unreadable`
74
- * = the slot spells a target but the base it needs is not a usable base of this tree (fed to TIGHTENING rows
75
- * only, as the ask a person clears — never to the allow arm); `refused` = a namespace refusal of the
74
+ * = the slot spells a target (or targets its base) but the base it needs is not supplied or not a usable base of
75
+ * this tree (fed to TIGHTENING rows only, as the ask a person clears — never to the allow arm); `refused` = a namespace refusal of the
76
76
  * spelling itself (a rule speaks about no such spelling: no verdict, no ask — the fs throat delivers the
77
77
  * refusal with the same word); `undefined` = no target in this reading.
78
78
  */
@@ -97,8 +97,9 @@ export type PathTargetReading = {
97
97
  * ③ otherwise a `patternParam`'s ABSOLUTE pattern supplies its own base directory;
98
98
  * ④ otherwise the base itself (the tool searches all of it).
99
99
  *
100
- * A base the deployment did not supply yields `undefined`: a relative spelling with nothing to resolve
101
- * against names no file, and claiming it names `/<spelling>` was a fabrication no fence should judge.
100
+ * A base the caller did not supply yields `unreadable`: a relative spelling with nothing to resolve against
101
+ * names a file this reading cannot locate — claiming it names `/<spelling>` would be a fabrication, and
102
+ * claiming it names nothing would let every rule about that file stay silent.
102
103
  */
103
104
  export declare function effectivePathTargetOf(call: ToolCallLike, pt: ToolCallPathTarget | undefined, bases: ToolPathBases): PathTargetReading;
104
105
  /**
@@ -31,10 +31,8 @@ export function effectivePathTargetOf(call, pt, bases) {
31
31
  if (spelling.spelled !== undefined)
32
32
  return resolveSpelling(spelling.spelled, spelling.base, bases);
33
33
  const declaredBase = spelling.base;
34
- if (declaredBase === undefined)
35
- return undefined;
36
34
  const family = pathFamilyOf(bases);
37
- return isAbsoluteForFamily(family, declaredBase) ? readingOf(pathIdentityOf(declaredBase, family, bases.root ?? bases.cwd)) : unreadableBase(declaredBase);
35
+ return declaredBase !== undefined && isAbsoluteForFamily(family, declaredBase) ? readingOf(pathIdentityOf(declaredBase, family, bases.root ?? bases.cwd)) : unreadableBase(declaredBase);
38
36
  }
39
37
  export function effectivePathSpellingOf(call, pt, bases) {
40
38
  if (pt === undefined)
@@ -63,9 +61,7 @@ function resolveSpelling(spelled, declaredBase, bases) {
63
61
  const tree = bases.root ?? bases.cwd;
64
62
  if (isAbsoluteForFamily(family, screened.spelled))
65
63
  return readingOf(pathIdentityOf(screened.spelled, family, tree));
66
- if (declaredBase === undefined || family === undefined)
67
- return undefined;
68
- if (!isAbsoluteForFamily(family, declaredBase))
64
+ if (declaredBase === undefined || family === undefined || !isAbsoluteForFamily(family, declaredBase))
69
65
  return unreadableBase(declaredBase);
70
66
  return readingOf(pathIdentityOf(joinForFamily(family, declaredBase, screened.spelled), family, tree));
71
67
  }
@@ -75,5 +71,5 @@ function readingOf(r) {
75
71
  return r.reason === "relative" || r.reason === "unanchored" ? undefined : { kind: "refused", reason: r.reason };
76
72
  }
77
73
  function unreadableBase(base) {
78
- return { kind: "unreadable", reason: `the base this call resolves against (${JSON.stringify(base)}) is not a fully-qualified path of this tree's family` };
74
+ return { kind: "unreadable", reason: base === undefined ? "the call's path is relative to a base that was not supplied" : `the base this call resolves against (${JSON.stringify(base)}) is not a fully-qualified path of this tree's family` };
79
75
  }
@@ -394,8 +394,9 @@ export interface EngineNotice {
394
394
  * frame's two count keys). They are NOT redelivered (a steer aimed at a finished run must not
395
395
  * fire at the next one — unlike ENGINE notes, which pend per session); the loud half of the
396
396
  * #257 contract's "accepted = enqueued, not consumed" sentence;
397
- * `detail: { steer, cause, taskId?, sessionId?, runId? }` / `{ followUp, cause, taskId?, sessionId?, runId? }`
398
- * (`cause` on both rows, closed `USER_INPUT_UNDRAINED_CAUSES`: `"leg_end"` — this engine's arm, the
397
+ * `detail: { <countKey>, cause, taskId?, sessionId?, runId? }` (the `code` and the `countKey` of each family are the
398
+ * table {@link USER_INPUT_UNDRAINED_FAMILIES}, keyed by the lane the input was queued on — `injectionLaneOf` — and the
399
+ * minter writes exactly those; `cause` on both rows, closed `USER_INPUT_UNDRAINED_CAUSES`: `"leg_end"` — this engine's arm, the
399
400
  * leg ended with it still owed; `"crashed"` — minted by the SERVING LAYER, not this engine, before the
400
401
  * session's next run, for inputs it had accepted whose consumption was not confirmed before the process died: their queue was the
401
402
  * serving layer's, so only it can count them, and it picks the family by the LANE the input was queued on (a `later`
@@ -404,7 +405,7 @@ export interface EngineNotice {
404
405
  * per-run identity, the key this account is actually about — see
405
406
  * {@link undrainedUserInputNotices}; all omitted when the caller has none, never fabricated). A delegated child
406
407
  * leg is announced on its PARENT's session as well — the direct parent only — as a second row per family,
407
- * `detail: { steer | followUp, cause, taskId?, sessionId: <the parent's>, childSessionId }` (no `runId`: nothing
408
+ * `detail: { <countKey>, cause, taskId?, sessionId: <the parent's>, childSessionId }` (no `runId`: nothing
408
409
  * carries the parent's), message tail `(a child leg, <childSessionId>)`; `childSessionId` present = a child reported it.
409
410
  * Per-run, at most once per family (the terminal sweep is a single site) — which is why
410
411
  * `taskId`/`sessionId` cannot key it: both answer the same value for two runs of one session.
@@ -971,6 +972,22 @@ export declare function settleOwedUserInput(account: OwedUserInputAccount, rowRe
971
972
  */
972
973
  export declare const USER_INPUT_UNDRAINED_CAUSES: readonly ["leg_end", "crashed"];
973
974
  export type UserInputUndrainedCause = (typeof USER_INPUT_UNDRAINED_CAUSES)[number];
975
+ /**
976
+ * The two families of the stranded-input account, keyed by the lane the input was queued on (`injectionLaneOf` maps a steer's
977
+ * tier to it): the notice `code` the family is announced under, and the `detail` key that carries its count
978
+ * (`detail[countKey]`, beside `cause`). {@link undrainedUserInputNotices} writes its rows with exactly these two spellings, so a
979
+ * serving layer that mints the `crashed` arm picks its family by lane and spells code and count key the way the engine does.
980
+ */
981
+ export declare const USER_INPUT_UNDRAINED_FAMILIES: Readonly<{
982
+ readonly steer: Readonly<{
983
+ readonly code: "task.user_steer_undrained";
984
+ readonly countKey: "steer";
985
+ }>;
986
+ readonly followUp: Readonly<{
987
+ readonly code: "task.user_followup_undrained";
988
+ readonly countKey: "followUp";
989
+ }>;
990
+ }>;
974
991
  /** Where a child leg's account is ALSO announced: its spawner's session, and that run's task id when the spawner carried one
975
992
  * (it never carries the run id, and the row never invents one). */
976
993
  interface ParentLegIds {
@@ -52,6 +52,12 @@ export function settleOwedUserInput(account, rowReturned, ids, onNotice) {
52
52
  }
53
53
  const SETTLED_OWED_ACCOUNTS = new WeakSet();
54
54
  export const USER_INPUT_UNDRAINED_CAUSES = Object.freeze(["leg_end", "crashed"]);
55
+ const STEER_UNDRAINED = "task.user_steer_undrained";
56
+ const FOLLOWUP_UNDRAINED = "task.user_followup_undrained";
57
+ export const USER_INPUT_UNDRAINED_FAMILIES = Object.freeze({
58
+ steer: Object.freeze({ code: STEER_UNDRAINED, countKey: "steer" }),
59
+ followUp: Object.freeze({ code: FOLLOWUP_UNDRAINED, countKey: "followUp" }),
60
+ });
55
61
  export function parentLegIds(internals) {
56
62
  const parentSessionId = internals?.parentSessionId;
57
63
  if (typeof parentSessionId !== "string" || parentSessionId === "")
@@ -73,17 +79,17 @@ export function undrainedUserInputNotices(counts, taskId, sessionId, runId, pare
73
79
  const cause = "leg_end";
74
80
  if (counts.steer > 0) {
75
81
  const message = `${counts.steer} user steer(s) ${tail}`;
76
- const count = { steer: counts.steer, cause };
77
- own.push({ code: "task.user_steer_undrained", message, detail: { ...count, ...tid, ...sid, ...rid } });
82
+ const count = { [USER_INPUT_UNDRAINED_FAMILIES.steer.countKey]: counts.steer, cause };
83
+ own.push({ code: STEER_UNDRAINED, message, detail: { ...count, ...tid, ...sid, ...rid } });
78
84
  if (toParent)
79
- copies.push({ code: "task.user_steer_undrained", message: `${message}${copyTail}`, detail: { ...count, ...ptid, sessionId: parentSession, childSessionId: sessionId } });
85
+ copies.push({ code: STEER_UNDRAINED, message: `${message}${copyTail}`, detail: { ...count, ...ptid, sessionId: parentSession, childSessionId: sessionId } });
80
86
  }
81
87
  if (counts.followUp > 0) {
82
88
  const message = `${counts.followUp} user follow-up(s) ${tail}`;
83
- const count = { followUp: counts.followUp, cause };
84
- own.push({ code: "task.user_followup_undrained", message, detail: { ...count, ...tid, ...sid, ...rid } });
89
+ const count = { [USER_INPUT_UNDRAINED_FAMILIES.followUp.countKey]: counts.followUp, cause };
90
+ own.push({ code: FOLLOWUP_UNDRAINED, message, detail: { ...count, ...tid, ...sid, ...rid } });
85
91
  if (toParent)
86
- copies.push({ code: "task.user_followup_undrained", message: `${message}${copyTail}`, detail: { ...count, ...ptid, sessionId: parentSession, childSessionId: sessionId } });
92
+ copies.push({ code: FOLLOWUP_UNDRAINED, message: `${message}${copyTail}`, detail: { ...count, ...ptid, sessionId: parentSession, childSessionId: sessionId } });
87
93
  }
88
94
  return [...own, ...copies];
89
95
  }
@@ -40,6 +40,7 @@ export async function runGateLanes(pass) {
40
40
  writeProtected: pass.writeProtected === true,
41
41
  writeProtectionUnresolved: pass.writeProtectionUnresolved === true,
42
42
  });
43
+ const readFaceNow = input.readFace?.();
43
44
  const askOriginFacts = (org, ruleCallUnreadable) => ({
44
45
  contentQuestion: heldBy("lanes.contentQuestionClass", QUESTION_TOOL_NAMES, pass.req),
45
46
  markedUnresolvable: markedUnresolvable(),
@@ -49,6 +50,7 @@ export async function runGateLanes(pass) {
49
50
  mandateMarks: mandateMarksNow(),
50
51
  ...(input.toolMarkSources !== undefined ? { toolMarkSources: input.toolMarkSources } : {}),
51
52
  ...(input.a2aIsolatePeers === true ? { a2aIsolatePeers: true } : {}),
53
+ ...(readFaceNow !== undefined ? { readFace: readFaceNow } : {}),
52
54
  });
53
55
  const classifyStanding = (unstamped, facts) => (pass.askClassification = classifyAsk(unstamped, facts));
54
56
  let orgTightenCount = 0;