@sema-agent/client-core 0.92.0 → 0.93.1

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 (125) hide show
  1. package/CHANGELOG.md +227 -0
  2. package/README.md +25 -16
  3. package/dist/adapt/arms.js +2 -0
  4. package/dist/adapt/toolCards.js +6 -4
  5. package/dist/adapt.d.ts +1 -1
  6. package/dist/adapt.js +1 -1
  7. package/dist/adapter/runStream.js +3 -1
  8. package/dist/agentsWireCaps.js +1 -1
  9. package/dist/approvalDecidedByCapability.d.ts +16 -0
  10. package/dist/approvalDecidedByCapability.js +47 -0
  11. package/dist/approvalsStreamLiveCapability.js +3 -3
  12. package/dist/askSurvivesStandingAnswer.d.ts +1 -1
  13. package/dist/centerWiringCapability.js +1 -1
  14. package/dist/cloudConfigWireCaps.js +5 -5
  15. package/dist/compensations.js +2 -2
  16. package/dist/decideReceipt.d.ts +2 -0
  17. package/dist/decideReceipt.js +34 -9
  18. package/dist/detachWire.d.ts +2 -14
  19. package/dist/detachWire.js +1 -47
  20. package/dist/deviceExecutorManagementCapability.d.ts +6 -2
  21. package/dist/deviceExecutorManagementCapability.js +61 -15
  22. package/dist/engineConfigDrift.d.ts +20 -0
  23. package/dist/engineConfigDrift.js +54 -0
  24. package/dist/engineErrorCodes.d.ts +3 -0
  25. package/dist/engineErrorCodes.js +12 -4
  26. package/dist/engineInlineTaskStats.d.ts +1 -0
  27. package/dist/engineNoticeCodes.d.ts +2 -2
  28. package/dist/engineWireFor.d.ts +22 -0
  29. package/dist/engineWireFor.js +154 -9
  30. package/dist/engineWireSdk.d.ts +0 -1
  31. package/dist/engineWireSdk.js +0 -3
  32. package/dist/executionLaneCapability.js +2 -2
  33. package/dist/fileHistoryCaptureCapability.d.ts +1 -1
  34. package/dist/fileHistoryCaptureCapability.js +1 -6
  35. package/dist/fileHistoryLegNotice.d.ts +1 -1
  36. package/dist/fileHistoryLegNotice.js +1 -6
  37. package/dist/gateVocabulary.d.ts +1 -5
  38. package/dist/gateVocabulary.js +4 -22
  39. package/dist/generated/engineFactTables.d.ts +4 -3
  40. package/dist/generated/engineFactTables.js +10 -0
  41. package/dist/generated/engineNoticeTables.d.ts +1 -1
  42. package/dist/generated/engineNoticeTables.js +1 -1
  43. package/dist/goalStopHook.d.ts +1 -8
  44. package/dist/goalStopHook.js +2 -58
  45. package/dist/headlessPermissionModeWire.d.ts +3 -2
  46. package/dist/headlessPermissionModeWire.js +46 -5
  47. package/dist/hitl/approvalResolution.d.ts +3 -2
  48. package/dist/hitl/approvalResolution.js +13 -0
  49. package/dist/hitl/askParkRowRouting.js +2 -2
  50. package/dist/hitl/frameRouter.js +7 -2
  51. package/dist/hitl/gateLedger.d.ts +1 -0
  52. package/dist/hitl/gateLedger.js +3 -0
  53. package/dist/hitl/hitlBridge.d.ts +1 -0
  54. package/dist/hitl/hitlBridge.js +6 -1
  55. package/dist/hitl/hitlHostSurface.d.ts +6 -2
  56. package/dist/hitl/hitlHostSurface.js +29 -3
  57. package/dist/hitl/parkResolver.js +22 -4
  58. package/dist/hitl/persistedRulesWire.js +29 -17
  59. package/dist/hitl/planReviewWire.d.ts +0 -2
  60. package/dist/hitl/planReviewWire.js +1 -18
  61. package/dist/hitl/sessionPolicyDeliverable.js +3 -3
  62. package/dist/hitl/sessionPolicyWire.d.ts +2 -0
  63. package/dist/hitl/sessionPolicyWire.js +7 -4
  64. package/dist/hitl/toolApprovalWire.d.ts +32 -7
  65. package/dist/hitl/toolApprovalWire.js +282 -59
  66. package/dist/hooksWireCaps.d.ts +1 -1
  67. package/dist/hooksWireCaps.js +1 -1
  68. package/dist/index.d.ts +7 -2
  69. package/dist/index.js +6 -2
  70. package/dist/inheritEnvWire.d.ts +1 -4
  71. package/dist/inheritEnvWire.js +0 -18
  72. package/dist/limitsWire.d.ts +0 -6
  73. package/dist/limitsWire.js +0 -38
  74. package/dist/mcpProbeCapability.js +1 -1
  75. package/dist/mcpProbeWire.js +10 -12
  76. package/dist/mcpReconnect.d.ts +5 -0
  77. package/dist/mcpReconnect.js +28 -0
  78. package/dist/memoryComplianceCapability.js +2 -2
  79. package/dist/memoryOriginCapability.js +2 -2
  80. package/dist/memoryVerbsWire.js +14 -13
  81. package/dist/ownKey.d.ts +2 -0
  82. package/dist/ownKey.js +15 -0
  83. package/dist/peerLaneCapability.js +2 -2
  84. package/dist/permissionModesCapability.d.ts +16 -0
  85. package/dist/permissionModesCapability.js +71 -0
  86. package/dist/permissionRuleIssue.js +1 -1
  87. package/dist/permissionRulesWriteCapability.js +2 -2
  88. package/dist/permissionWireCaps.d.ts +2 -0
  89. package/dist/permissionWireCaps.js +8 -1
  90. package/dist/permissionsAllowGrantCapability.js +7 -2
  91. package/dist/postureKnob.js +1 -1
  92. package/dist/protocolSegment.d.ts +2 -0
  93. package/dist/protocolSegment.js +3 -0
  94. package/dist/readFacePosture.js +1 -1
  95. package/dist/resumeRefusalCopy.d.ts +1 -0
  96. package/dist/resumeRefusalCopy.js +26 -3
  97. package/dist/retryStatus.js +2 -1
  98. package/dist/rewindArchiveCapability.js +2 -2
  99. package/dist/runTerminal.d.ts +16 -0
  100. package/dist/runTerminal.js +32 -3
  101. package/dist/seam.d.ts +1 -0
  102. package/dist/sessionPolicyCapability.js +1 -1
  103. package/dist/sessionSyncFileHistoryCapability.js +2 -2
  104. package/dist/skillToolListsCapability.js +1 -1
  105. package/dist/sqlEngineCapability.js +1 -1
  106. package/dist/steerPriorityCapability.js +1 -1
  107. package/dist/storePostureCapability.js +2 -2
  108. package/dist/supportFloor.d.ts +1 -0
  109. package/dist/supportFloor.js +13 -3
  110. package/dist/taskRequestWords.d.ts +4 -2
  111. package/dist/taskRequestWords.js +1 -1
  112. package/dist/toolHistoryBadTurn.js +1 -2
  113. package/dist/toolResult.d.ts +2 -1
  114. package/dist/toolResult.js +33 -3
  115. package/dist/userInvokedSkillsCapability.js +1 -1
  116. package/dist/webSearchBackendCapability.js +2 -2
  117. package/dist/webSearchWireCaps.d.ts +1 -1
  118. package/dist/webSearchWireCaps.js +40 -2
  119. package/dist/wireFailureShape.js +2 -2
  120. package/dist/wireRefusalCopy.d.ts +9 -3
  121. package/dist/wireRefusalCopy.js +41 -4
  122. package/dist/workflowsJournalCapability.js +1 -1
  123. package/dist/writeProtectionCapability.js +1 -1
  124. package/docs/INTEGRATION-CLIENTS.md +999 -15
  125. package/package.json +2 -2
@@ -1,80 +1,24 @@
1
1
  import { hostEnv } from './hostEnv.js';
2
2
  import { envFlagOff } from './envFlag.js';
3
- import { engineCapString } from './engineCapsCache.js';
4
3
  let wireSessionStopHookPrompt = null;
5
- let wireSessionStopHookCcSemantics = false;
6
4
  export const GOAL_STOP_HOOK_WIRE_ENV = 'SEMA_GOAL_STOP_HOOK_WIRE';
7
- export const CC_STOP_SEMANTICS_MIN_SERVER = '1.279.3';
8
- function parseSemverCore(v) {
9
- const m = /^(\d+)\.(\d+)\.(\d+)$/.exec(v.trim());
10
- return m ? [Number(m[1]), Number(m[2]), Number(m[3])] : null;
11
- }
12
- export function ccStopSemanticsFromVersion(version) {
13
- if (typeof version !== 'string' || version.length === 0)
14
- return false;
15
- const got = parseSemverCore(version);
16
- const min = parseSemverCore(CC_STOP_SEMANTICS_MIN_SERVER);
17
- if (got === null || min === null)
18
- return false;
19
- const [gotMajor, gotMinor, gotPatch] = got;
20
- const [minMajor, minMinor, minPatch] = min;
21
- if (gotMajor !== minMajor)
22
- return gotMajor > minMajor;
23
- if (gotMinor !== minMinor)
24
- return gotMinor > minMinor;
25
- if (gotPatch !== minPatch)
26
- return gotPatch > minPatch;
27
- return true;
28
- }
29
- export function engineCcStopSemantics(env = hostEnv()) {
30
- const base = env.SEMA_LIVE_BASEURL;
31
- if (!base)
32
- return false;
33
- return ccStopSemanticsFromVersion(engineCapString(base, 'version'));
34
- }
35
5
  export function isGoalStopHookWireArmed(env = hostEnv()) {
36
6
  return !envFlagOff(env[GOAL_STOP_HOOK_WIRE_ENV]);
37
7
  }
38
- export function setWireSessionStopHook(prompt, opts) {
8
+ export function setWireSessionStopHook(prompt) {
39
9
  wireSessionStopHookPrompt = prompt && prompt.length > 0 ? prompt : null;
40
- wireSessionStopHookCcSemantics = wireSessionStopHookPrompt === null ? false : opts?.ccSemantics === true;
41
10
  }
42
11
  export function getWireSessionStopHook() {
43
12
  return wireSessionStopHookPrompt;
44
13
  }
45
- export function getWireSessionStopHookCcSemantics() {
46
- return wireSessionStopHookCcSemantics;
47
- }
48
- export function buildGoalStopHookPrompt(condition) {
49
- const reason = `[session goal] Before you stop: verify that this goal actually holds — ${condition}. ` +
50
- `If it does hold, say so plainly and finish. If it does not, keep working toward it and do not ` +
51
- `pause to ask the user what to do.`;
52
- return ('You are the Stop gate for an autonomous coding agent session. The user set this session goal, ' +
53
- 'which the agent is supposed to satisfy before it stops:\n' +
54
- `<goal>\n${condition}\n</goal>\n` +
55
- 'Stop-hook input JSON: $ARGUMENTS\n' +
56
- 'Reply with RAW JSON only — no prose, no explanation, no code fences.\n' +
57
- 'If the input JSON has "stop_hook_active" set to true, this gate already fired once for this turn: ' +
58
- 'reply with exactly {"continue":true} so the agent may stop.\n' +
59
- 'Otherwise reply with exactly:\n' +
60
- JSON.stringify({ decision: 'block', reason }));
61
- }
62
14
  export function goalStopHookMatcher(managedOnly) {
63
15
  if (wireSessionStopHookPrompt === null || managedOnly || !isGoalStopHookWireArmed())
64
16
  return null;
65
17
  return {
66
18
  matcher: '',
67
- hooks: [
68
- {
69
- type: 'prompt',
70
- prompt: wireSessionStopHookCcSemantics
71
- ? wireSessionStopHookPrompt
72
- : buildGoalStopHookPrompt(wireSessionStopHookPrompt),
73
- },
74
- ],
19
+ hooks: [{ type: 'prompt', prompt: wireSessionStopHookPrompt }],
75
20
  };
76
21
  }
77
22
  export function __resetGoalStopHookForTests() {
78
23
  wireSessionStopHookPrompt = null;
79
- wireSessionStopHookCcSemantics = false;
80
24
  }
@@ -1,13 +1,14 @@
1
1
  import { type PermissionMode, type PermissionModeField } from './permissionWireCaps.js';
2
+ import { type PermissionModesReading } from './permissionModesCapability.js';
2
3
  import { type EnvLike } from './hostEnv.js';
3
4
  export declare const HEADLESS_PERMISSION_MODE_ENV = "SEMA_HEADLESS_PERMISSION_MODE";
4
5
  export declare const HEADLESS_PERMISSION_WIRE_MODES: readonly PermissionMode[];
5
- export declare function normalizeShellPermissionModeForWire(mode: string | undefined): string | undefined;
6
+ export declare function normalizeShellPermissionModeForWire(mode: string | undefined, permissionModes?: PermissionModesReading): string | undefined;
6
7
  export type HeadlessPermissionResolution = {
7
8
  fields: PermissionModeField;
8
9
  source: 'flag' | 'env' | 'default' | 'absent';
9
10
  warning?: string;
10
11
  };
11
12
  export declare function permissionModeFromArgv(argv: readonly string[]): string | undefined;
12
- export declare function resolveHeadlessPermissionMode(argv: readonly string[], env?: EnvLike): HeadlessPermissionResolution;
13
+ export declare function resolveHeadlessPermissionMode(argv: readonly string[], env?: EnvLike, permissionModes?: PermissionModesReading): HeadlessPermissionResolution;
13
14
  export declare function _resetHeadlessPermissionModeWireForTest(): void;
@@ -1,16 +1,20 @@
1
1
  import { permissionModeField } from './permissionWireCaps.js';
2
+ import { permissionModeAdmitted } from './permissionModesCapability.js';
2
3
  import { hostEnv } from './hostEnv.js';
3
4
  import { lastFlagValue } from './argvFlagValue.js';
4
5
  import { PERMISSION_MODE_WORDS } from './taskRequestWords.js';
6
+ import { readOwnDataValue } from './ownKey.js';
5
7
  export const HEADLESS_PERMISSION_MODE_ENV = 'SEMA_HEADLESS_PERMISSION_MODE';
6
8
  export const HEADLESS_PERMISSION_WIRE_MODES = [...PERMISSION_MODE_WORDS];
7
9
  const SHELL_INTERNAL_MODE_TO_WIRE = new Map([
8
10
  ['manual', 'default'],
9
11
  ['dontAsk', 'default'],
10
12
  ]);
11
- export function normalizeShellPermissionModeForWire(mode) {
13
+ export function normalizeShellPermissionModeForWire(mode, permissionModes) {
12
14
  if (mode === undefined)
13
15
  return undefined;
16
+ if (mode === 'dontAsk' && permissionModeAdmitted(permissionModes, mode))
17
+ return mode;
14
18
  return SHELL_INTERNAL_MODE_TO_WIRE.get(mode) ?? mode;
15
19
  }
16
20
  export function permissionModeFromArgv(argv) {
@@ -27,19 +31,32 @@ export function permissionModeFromArgv(argv) {
27
31
  return undefined;
28
32
  }
29
33
  let warnedInvalidEnv = false;
30
- export function resolveHeadlessPermissionMode(argv, env = hostEnv()) {
34
+ export function resolveHeadlessPermissionMode(argv, env = hostEnv(), permissionModes) {
31
35
  const flagMode = permissionModeFromArgv(argv);
32
36
  if (flagMode !== undefined) {
33
- const normalized = normalizeShellPermissionModeForWire(flagMode);
34
- return { fields: permissionModeField(normalized, { stampDefault: true }), source: 'flag' };
37
+ const snap = flagMode === 'dontAsk' ? dontAskSnapshot(permissionModes) : undefined;
38
+ const normalized = normalizeShellPermissionModeForWire(flagMode, snap);
39
+ return { fields: permissionModeField(normalized, { stampDefault: true, permissionModes: snap }), source: 'flag' };
35
40
  }
36
41
  const raw = env[HEADLESS_PERMISSION_MODE_ENV]?.trim();
37
42
  if (raw === undefined || raw === '') {
38
43
  return { fields: permissionModeField('default', { stampDefault: true }), source: 'default' };
39
44
  }
40
45
  if (HEADLESS_PERMISSION_WIRE_MODES.includes(raw)) {
46
+ const snap = raw === 'dontAsk' ? dontAskSnapshot(permissionModes) : undefined;
47
+ if (raw === 'dontAsk' && snap === undefined) {
48
+ const downgraded = {
49
+ fields: permissionModeField('default', { stampDefault: true }),
50
+ source: 'env',
51
+ };
52
+ if (!warnedInvalidEnv) {
53
+ warnedInvalidEnv = true;
54
+ downgraded.warning = `sema: ${HEADLESS_PERMISSION_MODE_ENV}="dontAsk" is only sent once this client knows the engine accepts that mode, and ${dontAskWithheldClause(permissionModes)} — this run uses "default"`;
55
+ }
56
+ return downgraded;
57
+ }
41
58
  return {
42
- fields: permissionModeField(raw, { stampDefault: true }),
59
+ fields: permissionModeField(raw, { stampDefault: true, permissionModes: snap }),
43
60
  source: 'env',
44
61
  };
45
62
  }
@@ -53,6 +70,30 @@ export function resolveHeadlessPermissionMode(argv, env = hostEnv()) {
53
70
  }
54
71
  return resolution;
55
72
  }
73
+ function dontAskSnapshot(reading) {
74
+ return permissionModeAdmitted(reading, 'dontAsk') ? { kind: 'present', modes: Object.freeze(['dontAsk']) } : undefined;
75
+ }
76
+ function dontAskWithheldClause(reading) {
77
+ if (reading === undefined)
78
+ return 'no such reading was supplied for this run';
79
+ let kind;
80
+ try {
81
+ kind = typeof reading === 'object' && reading !== null ? readOwnDataValue(reading, 'kind') : undefined;
82
+ }
83
+ catch {
84
+ kind = undefined;
85
+ }
86
+ switch (kind) {
87
+ case 'unobserved':
88
+ return "this engine's capabilities have not been read yet (wait for them before the first request)";
89
+ case 'not_reported':
90
+ return 'this engine does not report which modes it accepts';
91
+ case 'present':
92
+ return "this engine's list of accepted modes does not include it";
93
+ default:
94
+ return 'the reading supplied for it could not be read';
95
+ }
96
+ }
56
97
  export function _resetHeadlessPermissionModeWireForTest() {
57
98
  warnedInvalidEnv = false;
58
99
  }
@@ -1,9 +1,9 @@
1
1
  import type { DecideReceiptView } from '../decideReceipt.js';
2
2
  import type { HitlSafetyCode } from './hitlBridge.js';
3
- import type { FsApprovalOutcome, ToolApprovalFrameOutcome, ToolApprovalRespondAckView, ToolApprovalRespondDecision } from './toolApprovalWire.js';
3
+ import type { FsApprovalOutcome, ToolApprovalFrameOutcome, ToolApprovalRespondAckView, ToolApprovalRespondDecision, ToolApprovalRespondParkingView } from './toolApprovalWire.js';
4
4
  export declare const APPROVAL_NOT_SENT_CAUSES: readonly ["retracted", "edit_refused", "no_pending", "fetch_failed", "no_input", "card_unavailable"];
5
5
  export type ApprovalNotSentCause = (typeof APPROVAL_NOT_SENT_CAUSES)[number];
6
- export declare const APPROVAL_UNSETTLED_CAUSES: readonly ["engine_refused", "decide_failed", "transport_exhausted", "safety_stop", "respond_failed", "interrupted", "orchestration", "unreadable"];
6
+ export declare const APPROVAL_UNSETTLED_CAUSES: readonly ["engine_refused", "decide_failed", "transport_exhausted", "safety_stop", "respond_failed", "parking_in_progress", "interrupted", "orchestration", "unreadable"];
7
7
  export type ApprovalUnsettledCause = (typeof APPROVAL_UNSETTLED_CAUSES)[number];
8
8
  export interface ApprovalResolutionDecided {
9
9
  kind: 'decided';
@@ -26,6 +26,7 @@ export interface ApprovalResolutionUnsettled {
26
26
  errorCode?: string;
27
27
  status?: number;
28
28
  detail?: string;
29
+ parking?: ToolApprovalRespondParkingView;
29
30
  }
30
31
  export type ApprovalResolution = ApprovalResolutionDecided | ApprovalResolutionNotSent | ApprovalResolutionUnsettled;
31
32
  export type ApprovalLegOutcome = FsApprovalOutcome | ToolApprovalFrameOutcome;
@@ -12,6 +12,7 @@ export const APPROVAL_UNSETTLED_CAUSES = Object.freeze([
12
12
  'transport_exhausted',
13
13
  'safety_stop',
14
14
  'respond_failed',
15
+ 'parking_in_progress',
15
16
  'interrupted',
16
17
  'orchestration',
17
18
  'unreadable',
@@ -98,11 +99,23 @@ function fsLegResolution(outcome, kind) {
98
99
  return { kind: 'unsettled', cause: 'orchestration', ...call, ...detail };
99
100
  return { kind: 'unsettled', cause: 'unreadable', ...call, ...detail };
100
101
  }
102
+ function parkingViewOf(v) {
103
+ const approvalId = str(pick(v, 'approvalId'));
104
+ if (approvalId === undefined)
105
+ return undefined;
106
+ const hint = pick(v, 'retryAfterMs');
107
+ const retryAfterMs = typeof hint === 'number' && Number.isFinite(hint) && hint >= 0 ? hint : undefined;
108
+ const decidePath = str(pick(v, 'decidePath'));
109
+ return { approvalId, ...(retryAfterMs !== undefined ? { retryAfterMs } : {}), ...(decidePath !== undefined ? { decidePath } : {}) };
110
+ }
101
111
  function frameLegResolution(outcome) {
102
112
  if (pick(outcome, 'retracted') === true)
103
113
  return { kind: 'not_sent', cause: 'retracted' };
104
114
  if (pick(outcome, 'editRefused') === true)
105
115
  return { kind: 'not_sent', cause: 'edit_refused' };
116
+ const parking = parkingViewOf(pick(outcome, 'parking'));
117
+ if (parking !== undefined)
118
+ return { kind: 'unsettled', cause: 'parking_in_progress', parking };
106
119
  if (pick(outcome, 'decision') === 'unresolved') {
107
120
  const refusal = pick(outcome, 'respondRefusal');
108
121
  return {
@@ -15,9 +15,9 @@ export function classifyAskParkRows(rows, taskId, deps) {
15
15
  kind: 'unborn',
16
16
  reason: rows.length === 0
17
17
  ?
18
- `no pending row at all for task ${taskId} — either an orphan park (a seat held with no decision outstanding) or the next gate's row is not minted yet`
18
+ `no pending row at all for task ${taskId} — either an orphan park (a seat held with no decision outstanding) or the next gate's row is not minted yet; this client cannot tell which`
19
19
  : soleUnowned
20
- ? `the only pending row (task ${String(sole?.taskId)} / session ${String(sole?.sessionId)}) could not be proven to belong to this client (not in its own-run ledger, session id absent or different) — refusing to surface an approval that may be another session's`
20
+ ? `the only pending row (task ${String(sole?.taskId)} / session ${String(sole?.sessionId)}) could not be proven to belong to this client (not in its own-run ledger, session id absent or different; this client cannot tell which) — refusing to surface an approval that may be another session's`
21
21
  : `${String(rows.length)} pending rows but none for task ${taskId} — ambiguous queue, refusing to surface an unrelated row`,
22
22
  };
23
23
  }
@@ -66,6 +66,11 @@ function isGatedToolName(name) {
66
66
  function isFsOrShellToolName(name) {
67
67
  return typeof name === 'string' && (toolNameIsFsWrite(name) || toolNameIsShellExec(name));
68
68
  }
69
+ function parkFamilyEvidence(led, gateToolName, gatedCallId) {
70
+ if (!isFsOrShellToolName(gateToolName))
71
+ return false;
72
+ return gatedCallId === undefined || led.isFsOrShellGatedCall(gatedCallId);
73
+ }
69
74
  const ENGINE_GATE_PARKED_ERROR_CODE = GATE_PARKED_ERROR_CODE;
70
75
  function isEngineAbortToolEnd(ev) {
71
76
  if (ev.errorCode === ENGINE_GATE_PARKED_ERROR_CODE)
@@ -353,7 +358,7 @@ function routeSuspended(ev, ctx) {
353
358
  if (ev.gate &&
354
359
  isToolApprovalGate(ev.gate) &&
355
360
  ctx.taskId.current) {
356
- ctx.led.noteParkGate({ fsOrShellFamily: isFsOrShellToolName(ev.gate.toolName) });
361
+ ctx.led.noteParkGate({ fsOrShellFamily: parkFamilyEvidence(ctx.led, ev.gate.toolName, gatedCallId) });
357
362
  return { kind: 'park', gate: 'fs', ...(gatedCallId !== undefined ? { gatedCallId } : {}) };
358
363
  }
359
364
  return { kind: 'yield', events: [ev] };
@@ -364,7 +369,7 @@ function routeDone(ev, ctx) {
364
369
  const park = parkGateOf(ev.result);
365
370
  if (park !== null) {
366
371
  ctx.taskId.current = ctx.taskId.current ?? park.taskId;
367
- led.noteParkGate({ fsOrShellFamily: park.gate === 'fs' && isFsOrShellToolName(park.toolName) });
372
+ led.noteParkGate({ fsOrShellFamily: park.gate === 'fs' && parkFamilyEvidence(led, park.toolName, gatedCallId) });
368
373
  return { kind: 'park', gate: park.gate, pendingDone: ev, ...(gatedCallId !== undefined ? { gatedCallId } : {}) };
369
374
  }
370
375
  const events = [...flushHeldWithInterruptRewrite(led, { terminal: ev, signal: ctx.signal })];
@@ -46,6 +46,7 @@ export interface GateLedger {
46
46
  lastPendingFsCall(): string | undefined;
47
47
  noteFsOrShellGate(callId: string): void;
48
48
  lastFsOrShellGatedCallId(): string | undefined;
49
+ isFsOrShellGatedCall(callId: string): boolean;
49
50
  noteParkGate(evidence: {
50
51
  fsOrShellFamily: boolean;
51
52
  }): void;
@@ -235,6 +235,9 @@ export function createGateLedger() {
235
235
  lastFsOrShellGatedCallId() {
236
236
  return lastFsOrShellGatedCall;
237
237
  },
238
+ isFsOrShellGatedCall(callId) {
239
+ return fsOrShellGatedCalls.has(callId);
240
+ },
238
241
  noteParkGate(evidence) {
239
242
  if (batchParkCount > 0 && !heldSinceLastPark)
240
243
  return;
@@ -56,6 +56,7 @@ export interface HitlDecideDenyOutcome {
56
56
  decision: 'deny';
57
57
  reason?: string | undefined;
58
58
  settledBy?: 'policy' | undefined;
59
+ decidedBy?: 'policy' | undefined;
59
60
  }
60
61
  export declare class HitlSafetyError extends Error {
61
62
  readonly code: HitlSafetyCode;
@@ -2,6 +2,7 @@ import { askParkForeignGateKind } from '../adapter/activeRunSelfHeal.js';
2
2
  import { eventSeq } from '../adapter/types.js';
3
3
  import { hostLog } from '../host.js';
4
4
  import { abortableSleep } from '../abortableSleep.js';
5
+ import { readOwnDataValue } from '../ownKey.js';
5
6
  import { suspendedReopenOf } from './suspendedReopen.js';
6
7
  export const DEFAULT_DENY_REASON = 'The user rejected this tool use';
7
8
  const MAX_DENY_REASON_CHARS = 4096;
@@ -291,7 +292,11 @@ export class HitlBridge {
291
292
  decision: 'deny',
292
293
  ...this.bindingOf(pending),
293
294
  ...(outcome.reason !== undefined ? { reason: outcome.reason } : {}),
294
- ...(outcome.settledBy === 'policy' ? { settledBy: 'policy' } : {}),
295
+ ...(readOwnDataValue(outcome, 'decidedBy') === 'policy'
296
+ ? { decidedBy: 'policy' }
297
+ : readOwnDataValue(outcome, 'settledBy') === 'policy'
298
+ ? { settledBy: 'policy' }
299
+ : {}),
295
300
  };
296
301
  return this.decideRaw(pending.sessionId, decision, opts);
297
302
  }
@@ -28,12 +28,16 @@ export declare const REMEMBER_NOT_APPLIED_WARN_TEXT = "this session-wide allow w
28
28
  export declare const EDIT_NOT_FORWARDED_WARN_TEXT = "your edited tool input was NOT forwarded \u2014 the tool is running with the ORIGINAL input you saw before editing";
29
29
  export declare function surfaceRememberNotApplied(): void;
30
30
  export declare function surfaceEditNotForwarded(): void;
31
+ export declare const APPROVAL_PARKING_WARN_TEXT = "This approval is being parked; answer it again on the parked card when it appears.";
32
+ export declare function approvalParkingNoticeText(retryAfterMs?: number): string;
33
+ export declare function surfaceApprovalParking(retryAfterMs?: number): void;
31
34
  export declare const REPLAYED_APPROVAL_LIST_UNREADABLE_WARN_TEXT: string;
32
35
  export declare function surfaceReplayedApprovalListUnreadable(): void;
33
36
  export declare const EDIT_REFUSED_ON_BLIND_ASK_WARN_TEXT: string;
34
37
  export declare function surfaceEditRefusedOnBlindAsk(): void;
35
38
  export declare const RULE_NOT_SENT_WARN_TEXT = "your \"don't ask again\" choice was NOT sent to this engine \u2014 support for that rule form could not be confirmed. The approval itself went through, but you will be asked again";
36
39
  export declare function surfaceRuleArmNotSent(): void;
37
- export declare const RULE_NOT_SENT_REJECTED_WARN_TEXT = "your \"don't ask again\" choice was NOT sent \u2014 it did not pass the client-side check (the card returned a rule the engine never offered, an index that is not a batch offer, or two conflicting choices at once). The approval itself went through, but you will be asked again";
38
- export declare function surfaceRuleArmRejected(): void;
40
+ export type RuleArmRejectedWhy = 'both_arms' | 'index_not_a_batch_offer' | 'text_not_offered';
41
+ export declare const RULE_NOT_SENT_REJECTED_WARN_TEXT: string;
42
+ export declare function surfaceRuleArmRejected(why?: RuleArmRejectedWhy): void;
39
43
  export declare function observeCancelByDeny(settle: Promise<unknown>, taskId: string): void;
@@ -72,6 +72,17 @@ export function surfaceRememberNotApplied() {
72
72
  export function surfaceEditNotForwarded() {
73
73
  surfaceSelfClearingWarn(EDIT_NOT_FORWARDED_KEY, EDIT_NOT_FORWARDED_WARN_TEXT, EDIT_NOT_FORWARDED_TIMEOUT_MS);
74
74
  }
75
+ export const APPROVAL_PARKING_WARN_TEXT = 'This approval is being parked; answer it again on the parked card when it appears.';
76
+ const APPROVAL_PARKING_KEY = 'hitl-approval-parking';
77
+ const APPROVAL_PARKING_TIMEOUT_MS = 15_000;
78
+ export function approvalParkingNoticeText(retryAfterMs) {
79
+ if (retryAfterMs === undefined)
80
+ return APPROVAL_PARKING_WARN_TEXT;
81
+ return `${APPROVAL_PARKING_WARN_TEXT} (the engine suggested waiting about ${Math.ceil(retryAfterMs / 100) / 10} s)`;
82
+ }
83
+ export function surfaceApprovalParking(retryAfterMs) {
84
+ surfaceSelfClearingWarn(APPROVAL_PARKING_KEY, approvalParkingNoticeText(retryAfterMs), APPROVAL_PARKING_TIMEOUT_MS);
85
+ }
75
86
  export const REPLAYED_APPROVAL_LIST_UNREADABLE_WARN_TEXT = "Could not read the pending-approval list, so an approval request read back from this run's event log was not raised as a question, " +
76
87
  'and no decision was sent for it.';
77
88
  const REPLAYED_APPROVAL_LIST_UNREADABLE_KEY = 'hitl-replayed-approval-list-unreadable';
@@ -91,11 +102,26 @@ const RULE_NOT_SENT_TIMEOUT_MS = 10_000;
91
102
  export function surfaceRuleArmNotSent() {
92
103
  surfaceSelfClearingWarn(RULE_NOT_SENT_KEY, RULE_NOT_SENT_WARN_TEXT, RULE_NOT_SENT_TIMEOUT_MS);
93
104
  }
94
- export const RULE_NOT_SENT_REJECTED_WARN_TEXT = 'your "don\'t ask again" choice was NOT sent — it did not pass the client-side check (the card returned a rule the engine never offered, an index that is not a batch offer, or two conflicting choices at once). The approval itself went through, but you will be asked again';
105
+ function ruleNotSentRejectedReason(why) {
106
+ switch (why) {
107
+ case 'both_arms':
108
+ return 'the card returned two conflicting choices at once, a rule text and a batch index';
109
+ case 'index_not_a_batch_offer':
110
+ return 'the card returned an index that is not a batch offer';
111
+ case 'text_not_offered':
112
+ return 'the card returned a rule text that is not one of the single rules the engine offered';
113
+ default:
114
+ return 'the card returned a rule text that is not one of the single rules the engine offered, an index that is not a batch offer, or two conflicting choices at once; this client cannot tell which';
115
+ }
116
+ }
117
+ function ruleNotSentRejectedText(why) {
118
+ return `your "don't ask again" choice was NOT sent — it did not pass the client-side check (${ruleNotSentRejectedReason(why)}). The approval itself went through, but you will be asked again`;
119
+ }
120
+ export const RULE_NOT_SENT_REJECTED_WARN_TEXT = ruleNotSentRejectedText();
95
121
  const RULE_NOT_SENT_REJECTED_KEY = 'hitl-rule-arm-rejected';
96
122
  const RULE_NOT_SENT_REJECTED_TIMEOUT_MS = 10_000;
97
- export function surfaceRuleArmRejected() {
98
- surfaceSelfClearingWarn(RULE_NOT_SENT_REJECTED_KEY, RULE_NOT_SENT_REJECTED_WARN_TEXT, RULE_NOT_SENT_REJECTED_TIMEOUT_MS);
123
+ export function surfaceRuleArmRejected(why) {
124
+ surfaceSelfClearingWarn(RULE_NOT_SENT_REJECTED_KEY, ruleNotSentRejectedText(why), RULE_NOT_SENT_REJECTED_TIMEOUT_MS);
99
125
  }
100
126
  function isNoPendingError(e) {
101
127
  if (e instanceof HitlSafetyError)
@@ -1,6 +1,6 @@
1
- import { putOwnKey } from '../ownKey.js';
1
+ import { putOwnKey, readOwnDataValue } from '../ownKey.js';
2
2
  import { DecideTransportRetryExhaustedError, HitlBridge, HitlSafetyError, NO_QUESTION_OVERLAY_REASON, findPendingForTask, isNoDecisionSurfaceReason, readDecideCurrentPending, readWireErrorCode, } from './hitlBridge.js';
3
- import { ASK_GONE, CHECKPOINT_UNSUPPORTED_VERSION, PARKED_RESUME_STARTUP_FAILED, RESUME_POLICY_FOLD_CODE, isDecideParkMovedCode, refusalSaysStillPending } from '../engineErrorCodes.js';
3
+ import { ASK_GONE, CHECKPOINT_UNSUPPORTED_VERSION, DECIDE_WORKFLOW_PARK_FORKED, DECIDE_WORKFLOW_PARKS_UNREADABLE, PARKED_RESUME_STARTUP_FAILED, RESUME_POLICY_FOLD_CODE, isDecideParkMovedCode, refusalSaysStillPending } from '../engineErrorCodes.js';
4
4
  import { publishQuestionFrame, registerLocalQuestionResponder, hasQuestionOverlay, } from '../liveQuestionStore.js';
5
5
  import { hostLog } from '../host.js';
6
6
  import { surfaceFsApprovalAndDecide } from './toolApprovalWire.js';
@@ -63,7 +63,11 @@ function alreadyResolvedFailureVerdict(outcome) {
63
63
  return isAlreadyResolvedGateReason(outcome.reason);
64
64
  }
65
65
  export function isGateStandingErrorCode(code, retriable) {
66
- return code === RESUME_POLICY_FOLD_CODE || code === 'corrupt' || (code === CHECKPOINT_UNSUPPORTED_VERSION && retriable === true);
66
+ return (code === RESUME_POLICY_FOLD_CODE ||
67
+ code === 'corrupt' ||
68
+ (code === CHECKPOINT_UNSUPPORTED_VERSION && retriable === true) ||
69
+ code === DECIDE_WORKFLOW_PARKS_UNREADABLE ||
70
+ code === DECIDE_WORKFLOW_PARK_FORKED);
67
71
  }
68
72
  export function isCodeClassifiedGateFailure(code, retriable) {
69
73
  return isGateStandingErrorCode(code, retriable) || code === PARKED_RESUME_STARTUP_FAILED;
@@ -192,10 +196,18 @@ function isFetchStepNoPending(outcome) {
192
196
  async function surfaceParkGate(park, ctx, gatedCallId, onPresented) {
193
197
  const { deps, led, taskId } = ctx;
194
198
  if (park.gate === 'fs') {
199
+ let decidedByReading;
200
+ try {
201
+ decidedByReading = readOwnDataValue(deps.approvalLane, 'approvalDecidedBy');
202
+ }
203
+ catch {
204
+ decidedByReading = undefined;
205
+ }
195
206
  const outcome = await surfaceFsApprovalAndDecide({
196
207
  client: deps.client,
197
208
  ...(onPresented ? { onPresented } : {}),
198
209
  ...(deps.onDecideOperation ? { onDecideOperation: deps.onDecideOperation } : {}),
210
+ ...(decidedByReading !== undefined ? { approvalDecidedBy: decidedByReading } : {}),
199
211
  }, taskId, argsByCallOf(led), ctx.signal, gatedCallId);
200
212
  const outcomeDenied = 'denied' in outcome ? outcome.denied : undefined;
201
213
  if (outcome.kind === 'decided' && outcomeDenied === true && outcome.gatedCallId) {
@@ -210,6 +222,12 @@ async function surfaceParkGate(park, ctx, gatedCallId, onPresented) {
210
222
  }
211
223
  return surfaceGateAndDecide(deps, taskId, argsByCallOf(led), ctx.signal, gatedCallId, onPresented);
212
224
  }
225
+ function decidedIdentityCandidate(led, park) {
226
+ const id = park.gatedCallId;
227
+ if (id !== undefined)
228
+ return id;
229
+ return led.lastFsOrShellGatedCallId();
230
+ }
213
231
  export async function resolvePark(park, ctx) {
214
232
  const { led, taskId } = ctx;
215
233
  let outcome;
@@ -245,7 +263,7 @@ export async function resolvePark(park, ctx) {
245
263
  }
246
264
  else {
247
265
  if (park.gate === 'fs')
248
- candidateGatedCallId = led.lastFsOrShellGatedCallId();
266
+ candidateGatedCallId = decidedIdentityCandidate(led, park);
249
267
  outcome = await surfaceParkGate(park, ctx, park.gatedCallId, witness);
250
268
  }
251
269
  const alreadyDecidedById = outcome.kind === 'failed' &&
@@ -88,6 +88,10 @@ function rowStr(row, key) {
88
88
  function persistedRuleIdentityRowReadable(row) {
89
89
  return rowStr(row, 'rule') !== undefined && rowStr(row, 'scope') !== undefined;
90
90
  }
91
+ function persistedRuleIdentityRowWhy(row) {
92
+ const bad = ['rule', 'scope'].filter((k) => rowStr(row, k) === undefined);
93
+ return `${bad.join(' and ')} ${bad.length === 1 ? 'is' : 'are'} missing or not a non-empty string`;
94
+ }
91
95
  export function persistedRuleBehaviorOf(rule) {
92
96
  const b = rowStr(rule, 'behavior');
93
97
  return b !== undefined && BEHAVIOR_SET.has(b) ? b : undefined;
@@ -148,16 +152,22 @@ export async function listAllPersistedRules(facade, params = {}, opts) {
148
152
  }
149
153
  return { ok: false, failure };
150
154
  }
151
- if (typeof res !== 'object' ||
152
- res === null ||
153
- !Array.isArray(res.rules) ||
154
- typeof res.rev !== 'number' ||
155
- !Number.isFinite(res.rev)) {
155
+ const pageProblems = [];
156
+ if (typeof res !== 'object' || res === null) {
157
+ pageProblems.push('the page is not an object');
158
+ }
159
+ else {
160
+ if (!Array.isArray(res.rules))
161
+ pageProblems.push('rules is missing or not an array');
162
+ if (typeof res.rev !== 'number' || !Number.isFinite(res.rev))
163
+ pageProblems.push('rev is missing or not a finite number');
164
+ }
165
+ if (pageProblems.length > 0) {
156
166
  return {
157
167
  ok: false,
158
168
  failure: {
159
169
  kind: 'error',
160
- message: 'rule list page was malformed (missing/ill-typed rules array or rev on a 2xx) — refusing to certify it as a complete governance list',
170
+ message: `rule list page was malformed (${pageProblems.join('; ')}) on a 2xx — refusing to certify it as a complete governance list`,
161
171
  },
162
172
  };
163
173
  }
@@ -176,7 +186,7 @@ export async function listAllPersistedRules(facade, params = {}, opts) {
176
186
  ok: false,
177
187
  failure: {
178
188
  kind: 'error',
179
- message: 'rule list page carried a malformed rule row (missing/ill-typed rule or scope) — refusing to present a list that hides or garbles live rules',
189
+ message: `rule list page carried a malformed rule row (${persistedRuleIdentityRowWhy(r)}) — refusing to present a list that hides or garbles live rules`,
180
190
  },
181
191
  };
182
192
  }
@@ -348,24 +358,26 @@ export async function writePersistedRule(facade, input, opts) {
348
358
  catch (e) {
349
359
  return writeOutcomeFromFailure(classifyRulesFailure(e));
350
360
  }
351
- const malformed = () => ({
361
+ const malformed = (what) => ({
352
362
  status: 'unknown',
353
363
  why: 'malformed_result',
354
- message: 'the rule write answered 2xx with a body this client cannot read (status, rev, liveness or the written row) — refusing to certify it as a standing rule; re-list the rules to reconcile',
364
+ message: `the rule write answered 2xx with a body this client cannot read (${what}) — refusing to certify it as a standing rule; re-list the rules to reconcile`,
355
365
  });
356
366
  if (res === null || typeof res !== 'object' || Array.isArray(res))
357
- return malformed();
367
+ return malformed('the body is not an object');
358
368
  const snap = certificationSnapshot(res);
359
369
  if (snap === null)
360
- return malformed();
370
+ return malformed('a field of the body is an inherited or accessor property, or threw when read');
361
371
  const { status, rev, hasLiveness, liveness, hasRow, row } = snap;
362
- if ((status !== 'persisted' && status !== 'no-op') || typeof rev !== 'number' || !Number.isFinite(rev))
363
- return malformed();
372
+ const statusOk = status === 'persisted' || status === 'no-op';
373
+ const revOk = typeof rev === 'number' && Number.isFinite(rev);
374
+ if (!statusOk || !revOk)
375
+ return malformed([...(statusOk ? [] : ['status is not persisted or no-op']), ...(revOk ? [] : ['rev is not a finite number'])].join('; '));
364
376
  const wrote = status === 'persisted' ? 'persisted' : 'no_op';
365
377
  if (hasLiveness) {
366
378
  if (liveness === 'no') {
367
379
  if (hasRow)
368
- return malformed();
380
+ return malformed('liveness and the written row contradict each other');
369
381
  return {
370
382
  status: 'revoked',
371
383
  wrote,
@@ -375,7 +387,7 @@ export async function writePersistedRule(facade, input, opts) {
375
387
  }
376
388
  if (liveness === 'unknown') {
377
389
  if (hasRow)
378
- return malformed();
390
+ return malformed('liveness and the written row contradict each other');
379
391
  return {
380
392
  status: 'unverified',
381
393
  wrote,
@@ -384,10 +396,10 @@ export async function writePersistedRule(facade, input, opts) {
384
396
  };
385
397
  }
386
398
  if (liveness !== 'yes')
387
- return malformed();
399
+ return malformed('liveness is not one of yes, no or unknown');
388
400
  }
389
401
  if (!persistedRuleIdentityRowReadable(row))
390
- return malformed();
402
+ return malformed(`the written row is unreadable (${persistedRuleIdentityRowWhy(row)})`);
391
403
  const landed = row;
392
404
  if (landed.behavior !== behavior || landed.rule !== rule || landed.scope !== scope) {
393
405
  return {
@@ -6,8 +6,6 @@ export declare const PLAN_REVIEW_APPROVE_LABEL = "Yes, approve and run the plan"
6
6
  export declare const PLAN_REVIEW_REJECT_LABEL = "No, reject it (keep planning)";
7
7
  export declare const PLAN_REVIEW_APPROVE_AUTO_LABEL = "Yes, and auto-accept edits";
8
8
  export declare const PLAN_REVIEW_APPROVE_MANUAL_LABEL = "Yes, and manually approve edits";
9
- export declare const PLAN_REVIEW_MODE_AFTER_MIN_ENGINE: readonly [number, number, number];
10
- export declare function versionSupportsPlanReviewModeAfter(v: string | undefined): 'supported' | 'unsupported' | 'unknown';
11
9
  export declare function planReviewDecisionFromAnswer(answer: QuestionAnswer | null | undefined, shownLabels?: readonly string[]): 'approve' | 'reject' | 'dismissed';
12
10
  export type PlanReviewChoice = {
13
11
  decision: 'approve';
@@ -9,7 +9,6 @@ import { decideFailureNote } from '../decideFailureNote.js';
9
9
  import { checkpointUnsupportedVersionContent, checkpointUnsupportedVersionFromError } from '../resumeRefusalCopy.js';
10
10
  import { REQUEST_FIELD_CONFLICT, isUnavailableFaceCode } from '../engineErrorCodes.js';
11
11
  import { denyReasonForWire, isPlanReviewModeAfter } from './hitlBridge.js';
12
- import { engineCapString } from '../engineCapsCache.js';
13
12
  import { planReviewQuestionId, REOPEN_ID_TAIL, taskIdFromPlanReviewQuestionId } from './gateIdentity.js';
14
13
  import { notePlanReviewAnswered, notePlanReviewAnsweredFor, planReviewArmedKeyFor, registerArmedGateFor, waitForGateArmedFor, wasGateArmedFor, } from './armedGateRegistry.js';
15
14
  import { createSessionSlot, DEFAULT_SESSION_KEY } from '../sessionSlot.js';
@@ -20,20 +19,6 @@ export const PLAN_REVIEW_APPROVE_LABEL = 'Yes, approve and run the plan';
20
19
  export const PLAN_REVIEW_REJECT_LABEL = 'No, reject it (keep planning)';
21
20
  export const PLAN_REVIEW_APPROVE_AUTO_LABEL = 'Yes, and auto-accept edits';
22
21
  export const PLAN_REVIEW_APPROVE_MANUAL_LABEL = 'Yes, and manually approve edits';
23
- export const PLAN_REVIEW_MODE_AFTER_MIN_ENGINE = Object.freeze([7, 86, 0]);
24
- export function versionSupportsPlanReviewModeAfter(v) {
25
- const mm = v ? /^(0|[1-9]\d*)\.(0|[1-9]\d*)\.(0|[1-9]\d*)$/.exec(v) : null;
26
- if (!mm)
27
- return 'unknown';
28
- const got = [Number(mm[1]), Number(mm[2]), Number(mm[3])];
29
- for (let i = 0; i < 3; i++) {
30
- const g = got[i];
31
- const need = PLAN_REVIEW_MODE_AFTER_MIN_ENGINE[i];
32
- if (g !== need)
33
- return g > need ? 'supported' : 'unsupported';
34
- }
35
- return 'supported';
36
- }
37
22
  const APPROVE_LABEL = PLAN_REVIEW_APPROVE_LABEL;
38
23
  const REJECT_LABEL = PLAN_REVIEW_REJECT_LABEL;
39
24
  const LEGACY_TWO_CHOICE_LABELS = Object.freeze([PLAN_REVIEW_APPROVE_LABEL, PLAN_REVIEW_REJECT_LABEL]);
@@ -266,9 +251,7 @@ export function armPlanReviewApproval(result, sessionKey, opts) {
266
251
  }
267
252
  armedPlanReviewTaskIds.add(taskId);
268
253
  armedTaskId = taskId;
269
- const capsBaseUrl = wire !== undefined ? wire.capsBaseUrl : engineWireTarget()?.baseUrl;
270
- const offerModeAfter = opts?.submittedInPlanMode === true &&
271
- versionSupportsPlanReviewModeAfter(engineCapString(capsBaseUrl, 'version')) === 'supported';
254
+ const offerModeAfter = opts?.submittedInPlanMode === true;
272
255
  if (offerModeAfter)
273
256
  offerModeAfterByTask.set(taskId, true);
274
257
  else
@@ -109,10 +109,10 @@ export function sessionPolicyDeliverable(behavior, rules, opts) {
109
109
  }
110
110
  if (why === undefined && COVERING_NAMESPACE_PREFIXES.some((p) => r.startsWith(p))) {
111
111
  const segment = protocolSegmentOf(r);
112
- if (segment.kind === 'unknown')
113
- return unreadable();
114
- if (segment.kind === 'unminted')
112
+ if (segment.kind === 'empty' || segment.kind === 'unminted')
115
113
  why = 'not_a_tool_name';
114
+ else if (segment.kind === 'unknown')
115
+ return unreadable();
116
116
  }
117
117
  if (why === undefined && mounted !== undefined && !mounted.has(r))
118
118
  why = 'not_in_roster';