@sema-agent/core 7.23.0 → 7.23.2

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 (132) hide show
  1. package/CHANGELOG.md +64 -0
  2. package/dist/agents/agent-transcript-tool.js +2 -1
  3. package/dist/agents/cascade.js +2 -1
  4. package/dist/agents/list-agents-tool.js +2 -1
  5. package/dist/agents/observer.js +3 -2
  6. package/dist/agents/peer-session-drain.js +2 -1
  7. package/dist/agents/retain-ledger.js +2 -1
  8. package/dist/agents/send-message-tool.js +11 -10
  9. package/dist/agents/subagent.js +18 -17
  10. package/dist/agents/teacher.js +5 -4
  11. package/dist/brain/circuit-breaker.js +2 -1
  12. package/dist/brain/degrading.js +2 -1
  13. package/dist/brain/errors.js +24 -15
  14. package/dist/brain/failover.js +2 -1
  15. package/dist/brain/stream-engine.js +2 -1
  16. package/dist/core/a2a.js +4 -3
  17. package/dist/core/artifact-host.js +2 -1
  18. package/dist/core/ask-question.js +3 -2
  19. package/dist/core/auto-promote.js +3 -2
  20. package/dist/core/gate-bounded-await.d.ts +1 -1
  21. package/dist/core/gate-bounded-await.js +3 -2
  22. package/dist/core/gate-crash-trace.d.ts +7 -0
  23. package/dist/core/gate-crash-trace.js +3 -0
  24. package/dist/core/gate-fold.js +6 -2
  25. package/dist/core/gate-lanes.js +6 -3
  26. package/dist/core/git-worktree-env.js +6 -5
  27. package/dist/core/hooks.js +6 -5
  28. package/dist/core/lsp-session.js +3 -2
  29. package/dist/core/mcp.js +4 -3
  30. package/dist/core/memory-admission.js +2 -1
  31. package/dist/core/memory-engine/consolidation-driver.js +5 -4
  32. package/dist/core/memory-engine/consolidation.js +2 -1
  33. package/dist/core/memory-engine/delegation-settlement.js +4 -3
  34. package/dist/core/memory-engine/distiller.js +8 -4
  35. package/dist/core/memory-engine/engine.js +63 -55
  36. package/dist/core/memory-engine/file-backend.js +11 -10
  37. package/dist/core/memory-engine/layout.js +5 -4
  38. package/dist/core/memory-engine/session-incarnation.js +2 -1
  39. package/dist/core/memory.js +2 -1
  40. package/dist/core/oracle-isolation.js +3 -2
  41. package/dist/core/permission-rule-store.js +2 -1
  42. package/dist/core/runner/announce-once-ledger.js +2 -1
  43. package/dist/core/runner/compaction-seams.js +3 -2
  44. package/dist/core/runner/gate-exit.d.ts +5 -5
  45. package/dist/core/runner/gate-exit.js +0 -3
  46. package/dist/core/runner/git-leg-delivery.js +4 -3
  47. package/dist/core/runner/git-status-frame.js +2 -1
  48. package/dist/core/runner/mcp-redial.js +2 -1
  49. package/dist/core/runner/memory-consolidation-driver.js +2 -1
  50. package/dist/core/runner/permission-rule-lanes.js +3 -2
  51. package/dist/core/runner/prepare-artifact.js +2 -1
  52. package/dist/core/runner/prepare-ask-lane.js +2 -1
  53. package/dist/core/runner/prepare-caps-and-workflow.js +4 -3
  54. package/dist/core/runner/prepare-inherited-gate.js +2 -1
  55. package/dist/core/runner/prepare-memory-engine-session.js +2 -1
  56. package/dist/core/runner/prepare-memory.js +5 -4
  57. package/dist/core/runner/prepare-policy-chain.js +3 -2
  58. package/dist/core/runner/prepare-project-context.js +3 -2
  59. package/dist/core/runner/prepare-protocol-tools.js +3 -2
  60. package/dist/core/runner/prepare-suspend-saga.js +2 -1
  61. package/dist/core/runner/prepare-task.js +6 -5
  62. package/dist/core/runner/prepare-turn-wiring.js +2 -1
  63. package/dist/core/runner/resume-apply.js +5 -4
  64. package/dist/core/runner/resume-preflight.js +3 -2
  65. package/dist/core/runner/run-git-lane.js +4 -3
  66. package/dist/core/runner/run-leg.js +4 -3
  67. package/dist/core/runner/run-recovery-lanes.js +3 -2
  68. package/dist/core/runner/run-settle-and-teardown.js +2 -1
  69. package/dist/core/runner/run-stop-and-final-verify.js +2 -1
  70. package/dist/core/runner/run-telemetry-and-budget-seats.js +2 -1
  71. package/dist/core/runner/run-terminal-adoption.js +6 -5
  72. package/dist/core/runner/run-turn-boundary.js +5 -4
  73. package/dist/core/runner/runtask.js +2 -1
  74. package/dist/core/runner/stream-settle-backstop.js +5 -4
  75. package/dist/core/runner/stream-steer-verb.js +2 -1
  76. package/dist/core/runner/strict-output-schema.js +2 -1
  77. package/dist/core/runner/teardown-bounded.js +2 -1
  78. package/dist/core/safe-notify.js +2 -1
  79. package/dist/core/skills-directory.js +2 -1
  80. package/dist/core/strategy-store.js +2 -1
  81. package/dist/core/task-registry-agent.js +2 -1
  82. package/dist/core/task-registry-monitor.js +2 -1
  83. package/dist/core/task-registry-workflow.js +2 -1
  84. package/dist/core/task-registry.js +3 -2
  85. package/dist/core/thrown-value.d.ts +79 -0
  86. package/dist/core/thrown-value.js +56 -0
  87. package/dist/core/tool-errors.d.ts +0 -6
  88. package/dist/core/tool-errors.js +36 -14
  89. package/dist/core/tool-model-gate.js +2 -1
  90. package/dist/core/tool-policy.js +6 -13
  91. package/dist/core/tool-result-store.js +2 -1
  92. package/dist/core/tool-spec.d.ts +12 -0
  93. package/dist/core/write-protect.js +4 -4
  94. package/dist/engine/compaction/compaction.js +3 -4
  95. package/dist/engine/harness/agent-harness.js +2 -1
  96. package/dist/engine/harness/types.js +8 -7
  97. package/dist/engine/loop/agent-loop.js +7 -6
  98. package/dist/engine/lsp/node-lsp-manager.js +5 -4
  99. package/dist/engine/lsp/stdio-lsp-transport.js +12 -5
  100. package/dist/orchestration/dev-vm-script-runner.js +2 -1
  101. package/dist/orchestration/run-workflow-tool.js +7 -6
  102. package/dist/orchestration/workflow-governance.js +2 -1
  103. package/dist/orchestration/workflow-sandbox-conformance.js +3 -2
  104. package/dist/orchestration/workflow-script-store.js +2 -1
  105. package/dist/orchestration/workflow.js +15 -14
  106. package/dist/server/http.js +3 -2
  107. package/dist/stores/file/adoption/adopt.js +3 -2
  108. package/dist/stores/file/fs-atomic.js +2 -1
  109. package/dist/stores/file/permission-rule-adopt.js +2 -1
  110. package/dist/stores/file/permission-rule-store.js +2 -1
  111. package/dist/stores/file/session-store.js +4 -3
  112. package/dist/stores/file/strategy-store.js +7 -6
  113. package/dist/tools/artifact/artifact-tool.js +3 -2
  114. package/dist/tools/artifact/conformance.js +4 -3
  115. package/dist/tools/fs/bash-lexer.d.ts +12 -5
  116. package/dist/tools/fs/bash-lexer.js +5 -2
  117. package/dist/tools/fs/bash-program-position.d.ts +18 -0
  118. package/dist/tools/fs/bash-program-position.js +13 -1
  119. package/dist/tools/fs/bash-readonly-classifier.d.ts +23 -7
  120. package/dist/tools/fs/bash-readonly-classifier.js +74 -41
  121. package/dist/tools/fs/fs-bash.d.ts +28 -0
  122. package/dist/tools/fs/fs-bash.js +31 -13
  123. package/dist/tools/fs/fs-write.js +3 -2
  124. package/dist/tools/fs/safety.js +2 -1
  125. package/dist/tools/fs/search.js +3 -2
  126. package/dist/tools/monitor.js +3 -2
  127. package/dist/tools/sql.js +2 -1
  128. package/dist/tools/web.js +5 -15
  129. package/dist/tools/worktree.js +12 -9
  130. package/package.json +2 -1
  131. package/wire-changes/core@7.23.1.json +205 -0
  132. package/wire-changes/core@7.23.2.json +42 -0
@@ -1,7 +1,6 @@
1
1
  import { hasWin32AliasedComponent, isAbsoluteForFamily, isAbsolutePathForm, isBlockedDevicePath, isShellRootedSpellingUnmapped, joinForFamily, nativeUncSpellingOf, normalizeAbsPathLexically, win32NamespaceScreen, pathFamilyOf, withinAnyRoot } from "./safety.js";
2
2
  import { lexShellText, literalTildeSpelling, quoteMask, substitutionsInHeredocBody, SUBSTITUTION_PLACEHOLDER, wordsOf, } from "./bash-lexer.js";
3
- import { ASSIGNMENT_WORD, COMMAND_LAUNCHERS, NOT_AUTO_ALLOWED, SHELL_BLOCK_WORDS, argvReentryUnjudged, isBareControlWord, programBasename, programPositionRefusalSentence, readProgramPosition, } from "./bash-program-position.js";
4
- export { NOT_AUTO_ALLOWED } from "./bash-program-position.js";
3
+ import { ASSIGNMENT_WORD, COMMAND_LAUNCHERS, NOT_AUTO_ALLOWED, SHELL_BLOCK_WORDS, argvReentryUnjudged, isBareControlWord, programPositionRunsShell, shellScanDeclineOf, programBasename, programPositionRefusalSentence, readProgramPosition, } from "./bash-program-position.js";
5
4
  export const BASH_READONLY_DEFAULT_ALLOW = [
6
5
  "ls", "cat", "head", "tail", "wc", "pwd", "echo", "whoami", "uname",
7
6
  "grep", "cut", "tr", "basename", "dirname", "stat", "du", "df", "which",
@@ -52,7 +51,8 @@ export function coarseReadonlyCheck(command, allow, options) {
52
51
  return `command "${parsed.name}" is not in the read-only allowlist`;
53
52
  return undefined;
54
53
  }
55
- const SHELL_SEGMENT_HARD_REJECT = /[<>$()`\n\r\\]/;
54
+ const SHELL_SEGMENT_QUOTE_BLIND_REJECT = /[<>()`\n\r\\]/;
55
+ const SHELL_EXPANSION_CHARS = /[$]/;
56
56
  export function carriesShellRedirection(text) {
57
57
  return SHELL_REDIRECTION_OPERATORS.test(text);
58
58
  }
@@ -78,10 +78,25 @@ function rejectedSansRedirection(source) {
78
78
  if (redirectionPrecedesConnector(source)) {
79
79
  return "a redirection followed by `|`, `&` or `;` is not allowed — the operator's own characters cannot be told apart from a command boundary";
80
80
  }
81
+ return hardRejectReason(source, true);
82
+ }
83
+ function hardRejectReason(source, tolerateRedirection) {
84
+ const reason = tolerateRedirection
85
+ ? "command/variable substitution, subshells, escapes, and line breaks are not allowed"
86
+ : "redirection, command/variable substitution, subshells, escapes, and line breaks are not allowed";
81
87
  for (const ch of source) {
82
- if (SHELL_SEGMENT_HARD_REJECT.test(ch) && !SHELL_REDIRECTION_OPERATORS.test(ch)) {
83
- return "command/variable substitution, subshells, escapes, and line breaks are not allowed";
84
- }
88
+ if (SHELL_SEGMENT_QUOTE_BLIND_REJECT.test(ch) && !(tolerateRedirection && SHELL_REDIRECTION_OPERATORS.test(ch)))
89
+ return reason;
90
+ }
91
+ if (!SHELL_EXPANSION_CHARS.test(source))
92
+ return undefined;
93
+ const mask = quoteMask(source);
94
+ for (let i = 0; i < source.length; i++) {
95
+ if (!SHELL_EXPANSION_CHARS.test(source[i]))
96
+ continue;
97
+ if (mask.balanced && mask.openedBy[i] === "'")
98
+ continue;
99
+ return reason;
85
100
  }
86
101
  return undefined;
87
102
  }
@@ -91,8 +106,10 @@ export function splitShellCompoundSegments(source, options) {
91
106
  if (refused !== undefined)
92
107
  return { reject: refused };
93
108
  }
94
- else if (SHELL_SEGMENT_HARD_REJECT.test(source)) {
95
- return { reject: "redirection, command/variable substitution, subshells, escapes, and line breaks are not allowed" };
109
+ else {
110
+ const refused = hardRejectReason(source, false);
111
+ if (refused !== undefined)
112
+ return { reject: refused };
96
113
  }
97
114
  const scanned = options?.trailingTerminator !== "keep" ? source.replace(/;\s*$/, "") : source;
98
115
  const lexed = lexShellText(scanned, { mode: "strict" });
@@ -239,8 +256,10 @@ function tokensMoveBase(toks) {
239
256
  return pos.refusal !== undefined || pos.program?.basename === "cd";
240
257
  }
241
258
  const SHELL_REENTRY_PROGRAM_RE = /^(?:(?:ba|z|da|k|fi|a|mk|ya|pdk|c|tc)?sh|eval|source|\.|parallel)$/;
242
- export function refusedCommandMayRead(command, allow, boundary) {
243
- return !refusedCommandExempt(command, allow, boundary, 0);
259
+ export function refusedCommandMayRead(command, allow, boundary, declinesAreEvidence) {
260
+ const scan = { declined: false };
261
+ const exempt = refusedCommandExempt(command, allow, boundary, declinesAreEvidence, scan, 0);
262
+ return { mayRead: !exempt, declined: scan.declined };
244
263
  }
245
264
  const STDIN_ONLY_READER_FLAGS = new Map([
246
265
  ["cat", /^-[AbeEnstTuv]+$/],
@@ -262,7 +281,7 @@ function whitelistSegment(segment) {
262
281
  redirects: tokens.filter((t) => t.kind === "redirect"),
263
282
  };
264
283
  }
265
- function refusedCommandExempt(command, allow, boundary, depth, baseMovedOutside = false) {
284
+ function refusedCommandExempt(command, allow, boundary, declinesAreEvidence, scan, depth, baseMovedOutside = false) {
266
285
  if (depth > 4)
267
286
  return false;
268
287
  const lexed = lexShellText(command, { mode: "lenient" });
@@ -282,12 +301,23 @@ function refusedCommandExempt(command, allow, boundary, depth, baseMovedOutside
282
301
  }
283
302
  segments.push(whitelistSegment(segment));
284
303
  }
304
+ let declined;
305
+ for (const { words } of segments) {
306
+ const pos = readProgramPosition(words);
307
+ if (programPositionRunsShell(pos, SHELL_REENTRY_PROGRAM_RE))
308
+ return false;
309
+ const decline = shellScanDeclineOf(pos, XARGS_TARGETS_ACCEPTED_UNJUDGED);
310
+ if (decline !== undefined)
311
+ scan.declined = true;
312
+ if (decline !== undefined && declinesAreEvidence.has(decline))
313
+ return false;
314
+ declined ??= decline;
315
+ }
316
+ if (declined !== undefined)
317
+ return true;
285
318
  const baseMoved = baseMovedOutside ||
286
319
  segments.some(({ words }) => {
287
- const pos = readProgramPosition(words);
288
- if (pos.refusal !== undefined)
289
- return true;
290
- const runs = pos.program?.basename;
320
+ const runs = readProgramPosition(words).program?.basename;
291
321
  return runs === "cd" || (runs !== undefined && UNMODELLED_BASE_MOVERS.has(runs));
292
322
  });
293
323
  for (const body of bodies) {
@@ -296,7 +326,7 @@ function refusedCommandExempt(command, allow, boundary, depth, baseMovedOutside
296
326
  (walked.undecidedPaths?.length ?? 0) === 0 && (walked.unresolvedOperands?.length ?? 0) === 0 && (walked.recursiveReadPaths?.length ?? 0) === 0;
297
327
  if (fullyGreen)
298
328
  continue;
299
- if (!refusedCommandExempt(body, allow, boundary, depth + 1, baseMoved))
329
+ if (!refusedCommandExempt(body, allow, boundary, declinesAreEvidence, scan, depth + 1, baseMoved))
300
330
  return false;
301
331
  }
302
332
  const stdinFileJudgedInside = (word) => {
@@ -353,8 +383,6 @@ function refusedCommandExempt(command, allow, boundary, depth, baseMovedOutside
353
383
  if (r.op === "<" && !stdinFileJudgedInside(r.target?.folded))
354
384
  return false;
355
385
  const pos = readProgramPosition(words);
356
- if (pos.refusal !== undefined)
357
- return false;
358
386
  for (const k of words.folded.keys()) {
359
387
  const word = words.folded[k];
360
388
  if (ASSIGNMENT_WORD.test(word) && /\$\(|`/.test(word) && (pos.assignments.includes(k) || pos.launchers.some((l) => l.index < k)))
@@ -365,24 +393,16 @@ function refusedCommandExempt(command, allow, boundary, depth, baseMovedOutside
365
393
  if (program === undefined) {
366
394
  if (pos.launchers.length === 0)
367
395
  continue;
368
- if (argvReentryUnjudged(pos, XARGS_TARGETS_ACCEPTED_UNJUDGED))
369
- return false;
370
396
  if (!unlistedSegmentJudgedInside(words, pos.launchers[0].index, pos.assignments))
371
397
  return false;
372
398
  continue;
373
399
  }
374
400
  if (program.name.includes(SUBSTITUTION_PLACEHOLDER) || /[$`]/.test(program.name))
375
401
  return false;
376
- if (SHELL_REENTRY_PROGRAM_RE.test(program.basename))
377
- return false;
378
- if (pos.launchers.length > 0) {
379
- if (argvReentryUnjudged(pos, XARGS_TARGETS_ACCEPTED_UNJUDGED))
402
+ if (pos.launchers.length > 0 && !listed(program.basename)) {
403
+ if (!unlistedSegmentJudgedInside(words, pos.launchers[0].index, pos.assignments))
380
404
  return false;
381
- if (!listed(program.basename)) {
382
- if (!unlistedSegmentJudgedInside(words, pos.launchers[0].index, pos.assignments))
383
- return false;
384
- continue;
385
- }
405
+ continue;
386
406
  }
387
407
  const head = program.basename;
388
408
  if (head === "cd" || UNMODELLED_BASE_MOVERS.has(head))
@@ -455,12 +475,12 @@ function longOptionNameOf(tok) {
455
475
  function isLongOptionAbbrevOf(name, full) {
456
476
  return full.startsWith(name);
457
477
  }
458
- function isGrepPatternFlagToken(tok) {
478
+ function suppliesFirstOperandSlot(slot, tok) {
459
479
  if (tok.startsWith("--")) {
460
480
  const long = longOptionNameOf(tok);
461
- return long !== undefined && (isLongOptionAbbrevOf(long, "regexp") || isLongOptionAbbrevOf(long, "file"));
481
+ return long !== undefined && slot.suppliedByLongNames.some((full) => isLongOptionAbbrevOf(long, full));
462
482
  }
463
- return /^-[A-Za-z0-9]*[ef]/.test(tok);
483
+ return new RegExp(`^-[A-Za-z0-9]*[${slot.suppliedByLetters}]`).test(tok);
464
484
  }
465
485
  function isGrepClusterFileStdin(tok) {
466
486
  if (tok.startsWith("--") || !/^-[A-Za-z0-9]/.test(tok))
@@ -534,6 +554,7 @@ const VERB_OPTION_SLOTS = {
534
554
  regexp: "pattern",
535
555
  },
536
556
  longOptionalValue: { color: "label", colour: "label", context: "count" },
557
+ firstOperand: { kind: "pattern", suppliedByLetters: "ef", suppliedByLongNames: ["regexp", "file"] },
537
558
  longPlain: [
538
559
  "basic-regexp", "byte-offset", "count", "dereference-recursive", "extended-regexp",
539
560
  "files-with-matches", "files-without-match", "fixed-strings", "help", "ignore-case", "initial-tab",
@@ -579,6 +600,7 @@ const VERB_OPTION_SLOTS = {
579
600
  shortPlain: "nrEszu",
580
601
  longValue: { expression: "script", file: "path", "line-length": "count" },
581
602
  longOptionalValue: { "in-place": "label" },
603
+ firstOperand: { kind: "script", suppliedByLetters: "ef", suppliedByLongNames: ["expression", "file"] },
582
604
  longPlain: [
583
605
  "debug", "follow-symlinks", "help", "null-data", "posix", "quiet", "regexp-extended", "sandbox",
584
606
  "separate", "silent", "unbuffered", "version", "zero-terminated",
@@ -1670,7 +1692,8 @@ function collectSegmentBoundaryFindings(tokens, boundary) {
1670
1692
  }
1671
1693
  else {
1672
1694
  const eoo = args.indexOf("--");
1673
- const patternSuppliedByFlag = name === "grep" && (eoo === -1 ? args : args.slice(0, eoo)).some(isGrepPatternFlagToken);
1695
+ const firstOperandSlot = VERB_OPTION_SLOTS[name]?.firstOperand;
1696
+ const firstOperandSuppliedByFlag = firstOperandSlot !== undefined && (eoo === -1 ? args : args.slice(0, eoo)).some((t) => suppliesFirstOperandSlot(firstOperandSlot, t));
1674
1697
  let sawOperand = false;
1675
1698
  let sawArgvOperand = false;
1676
1699
  let endOfOptions = false;
@@ -1718,12 +1741,12 @@ function collectSegmentBoundaryFindings(tokens, boundary) {
1718
1741
  continue;
1719
1742
  }
1720
1743
  }
1721
- const isGrepPatternSlot = name === "grep" && !patternSuppliedByFlag && !sawOperand;
1744
+ const inFirstOperandSlot = firstOperandSlot !== undefined && !firstOperandSuppliedByFlag && !sawOperand;
1722
1745
  sawOperand = true;
1723
1746
  if (k >= argvOperandsStartAt)
1724
1747
  sawArgvOperand = true;
1725
1748
  if (t === "-") {
1726
- if (isGrepPatternSlot)
1749
+ if (inFirstOperandSlot)
1727
1750
  continue;
1728
1751
  const m = RECURSIVE_READ_FORMS[slotVerb];
1729
1752
  if (m === undefined || m.dashIsStdin === true) {
@@ -1734,8 +1757,14 @@ function collectSegmentBoundaryFindings(tokens, boundary) {
1734
1757
  positionalOperands.push(t);
1735
1758
  continue;
1736
1759
  }
1737
- if (isGrepPatternSlot)
1760
+ if (inFirstOperandSlot) {
1761
+ const named = slotValueCandidates(t, firstOperandSlot.kind);
1762
+ for (const c of named)
1763
+ judgeOperand(c, argGlobs(k));
1764
+ if (named.length > 0 && k >= argvOperandsStartAt)
1765
+ positionalOperands.push(t);
1738
1766
  continue;
1767
+ }
1739
1768
  if (k >= argvOperandsStartAt)
1740
1769
  positionalOperands.push(t);
1741
1770
  judgeOperand(t, argGlobs(k));
@@ -1987,7 +2016,8 @@ export function classifyCompoundReadonlyDetailed(command, allow, boundary, opts)
1987
2016
  if (!(pipeFed[si] ?? false)) {
1988
2017
  const floor = STDIN_FILE_FLOOR[name];
1989
2018
  const restArgs = toks.slice(1);
1990
- const grepPatternSuppliedByFlag = name === "grep" && restArgs.some(isGrepPatternFlagToken);
2019
+ const floorFirstOperandSlot = VERB_OPTION_SLOTS[name]?.firstOperand;
2020
+ const floorSlotSuppliedByFlag = floorFirstOperandSlot !== undefined && restArgs.some((t) => suppliesFirstOperandSlot(floorFirstOperandSlot, t));
1991
2021
  let sawNonFlagOperand = false;
1992
2022
  let hasStdinDash = false;
1993
2023
  let operandCount = 0;
@@ -2013,10 +2043,10 @@ export function classifyCompoundReadonlyDetailed(command, allow, boundary, opts)
2013
2043
  if (t.startsWith("-") && t !== "-")
2014
2044
  continue;
2015
2045
  }
2016
- const isGrepPatternPosition = name === "grep" && !grepPatternSuppliedByFlag && !sawNonFlagOperand;
2046
+ const inFirstOperandSlot = floorFirstOperandSlot !== undefined && !floorSlotSuppliedByFlag && !sawNonFlagOperand;
2017
2047
  sawNonFlagOperand = true;
2018
2048
  operandCount++;
2019
- if (isGrepPatternPosition)
2049
+ if (inFirstOperandSlot)
2020
2050
  continue;
2021
2051
  if (t === "-") {
2022
2052
  hasStdinDash = true;
@@ -2329,10 +2359,13 @@ export function classifyOutOfRootReadGate(command, allow, boundary) {
2329
2359
  if (baseMoved)
2330
2360
  return { gated: true, paths: [] };
2331
2361
  const verdict = evaluateReadBoundary(readers, containmentOnly, false, readerReach);
2332
- return verdict.outOfRootRead === true ? { gated: true, paths: verdict.outOfRootPaths ?? [] } : NOT_GATED;
2362
+ const denied = boundary.denyMatch === undefined ? false : evaluateReadBoundary(readers, boundary, false, readerReach).readDenied === true;
2363
+ if (verdict.outOfRootRead === true)
2364
+ return { gated: true, paths: verdict.outOfRootPaths ?? [], ...(denied ? { denied: true } : {}) };
2365
+ return denied ? { gated: false, paths: [], denied: true } : NOT_GATED;
2333
2366
  };
2334
2367
  const direct = judge(command);
2335
- if (direct.gated)
2368
+ if (direct.gated || direct.denied === true)
2336
2369
  return direct;
2337
2370
  const shape = POLL_LOOP_SHAPE.exec(command.trim());
2338
2371
  return shape !== null ? judge(shape[3]) : NOT_GATED;
@@ -3,6 +3,7 @@ import { type TaskRegistry } from "../../core/task-registry.js";
3
3
  import { type CwdRef } from "./fs-shared.js";
4
4
  import type { ReversibilityVerdict } from "../../core/types.js";
5
5
  import { type BashReadonlyRootBoundary } from "./bash-readonly-classifier.js";
6
+ import { type ShellScanDecline } from "./bash-program-position.js";
6
7
  /**
7
8
  * A parsed-command classifier for the `bash` tool, exposed as a
8
9
  * `ToolSpec.reversibilityProbe`. A full shell is treated as egress+irreversible by DEFAULT; this probe is the
@@ -60,6 +61,33 @@ import { type BashReadonlyRootBoundary } from "./bash-readonly-classifier.js";
60
61
  * shape classifier here changes what happens to the residual shell risk, never that reading.
61
62
  */
62
63
  export declare function bashReversibilityProbe(allow?: readonly string[], boundary?: BashReadonlyRootBoundary | (() => BashReadonlyRootBoundary | undefined)): (args: unknown) => ReversibilityVerdict;
64
+ /**
65
+ * ONE rule, TWO TABLES: which {@link ShellScanDecline} a SEAT reads as read evidence. A decline names no word,
66
+ * so it is never the boundary DECLARING anything about a path.
67
+ *
68
+ * The READ-BOUNDARY seat is what the default/off doctrine and a bypass mode mount ALONE: there "not mandated"
69
+ * is VOUCHED, so a decline must count as evidence or a read nobody judged runs silently (`<out-of-root> |
70
+ * xargs cat` at zero asks — a closed P0). It keeps both. The CLASSIFY seat composes the same boundary with the
71
+ * SHAPE classifier, which refuses every one of these shapes: what survives is a CARD, and the only question
72
+ * this table answers is who may retire it. It keeps NEITHER — an ANSWERER may (a person, the auto-mode
73
+ * classifier, a person's stored allow rule) — empty by the argument, not by omission.
74
+ *
75
+ * WHAT AN EMPTY TABLE DOES NOT BUY, corrected where it was once claimed: "the shape classifier refuses these
76
+ * shapes anyway, so the only question left is the stored answer" was true of the PROBE and false of the OUTCOME
77
+ * — the allow layer's other members are not that classifier, and both cleared exactly these cards at ZERO asks. An
78
+ * empty table says only "no MANDATE HERE"; that the boundary DECLINED is a separate fact, carried on every ask this
79
+ * seat raises ({@link BoundaryAnswer}'s weaker rung), standing every ANSWERLESS arm down while every answerer's lane stays open.
80
+ * THE COST, stated rather than mitigated: once that ask is retired BY AN ANSWERER, an out-of-root or deny-listed
81
+ * read HIDDEN inside such a command is not asked again. A command's words are not the argv its programs receive (a
82
+ * stream is re-split by its consumer, an unread option may move the working directory first, stdin text is a
83
+ * command line nobody parsed), so a scan of them cannot be the guarantee it was claimed to be. "A read the boundary
84
+ * objects to is never retired by a stored rule" does NOT hold on the classify seat for this class, and holds
85
+ * unchanged elsewhere. One word-level check survives on both seats and is not part of the trade: a token the scan
86
+ * DID read naming a path outside the roots still gates the call ({@link classifyOutOfRootReadGate}).
87
+ */
88
+ export declare const BOUNDARY_SEAT_DECLINE_EVIDENCE: ReadonlySet<ShellScanDecline>;
89
+ /** The classify doctrine's table — see above; empty by the argument. */
90
+ export declare const CLASSIFY_SEAT_DECLINE_EVIDENCE: ReadonlySet<ShellScanDecline>;
63
91
  /**
64
92
  * The READ-BOUNDARY seat of the full shell: a `ToolSpec.reversibilityProbe` that answers exactly ONE
65
93
  * question — does the deployment's read boundary object to where this command reads? — and nothing about
@@ -13,8 +13,10 @@ import { boundaryIsTheSandbox, canonicalPathIsAuthoritative, hasDestroy, isIsola
13
13
  import { ghRateLimitHint } from "./gh-rate-limit.js";
14
14
  import { resolveBashTimeoutCaps, bashTimeoutCapsSec, bashTimeoutArgRefusal, bashTimeoutParamDescription, envErrorDetail, bashMaxOutputChars, clipShellOutput, writeShellOverflowFile, createShellOverflowSpoolFence, shellRecoveryHint, CWD_SENTINEL, stripCwdSentinel, BASH_READONLY_CONFINEMENT_NOTE, } from "./fs-shared.js";
15
15
  import { PROBE_CAUSE_PATH_MAX, inlineUntrusted } from "../../core/untrusted-text.js";
16
- import { bashClassifyDefaultAllow, BASH_READONLY_DEFAULT_ALLOW, coarseReadonlyCheck, classifyBoundedReadonlyPollLoopDetailed, classifyCompoundReadonlyDetailed, classifyOutOfRootReadGate, refusedCommandMayRead, classifySimpleCommandReadBoundary, NOT_AUTO_ALLOWED, } from "./bash-readonly-classifier.js";
16
+ import { bashClassifyDefaultAllow, BASH_READONLY_DEFAULT_ALLOW, coarseReadonlyCheck, classifyBoundedReadonlyPollLoopDetailed, classifyCompoundReadonlyDetailed, classifyOutOfRootReadGate, refusedCommandMayRead, classifySimpleCommandReadBoundary, } from "./bash-readonly-classifier.js";
17
+ import { NOT_AUTO_ALLOWED, SHELL_SCAN_DECLINES } from "./bash-program-position.js";
17
18
  import { toolFace } from "../../core/tool-catalog-entries.js";
19
+ import { thrownText } from "../../core/thrown-value.js";
18
20
  const RECURSIVE_CAUSE_MAX_PATHS = 3;
19
21
  const RECURSIVE_READ_CAUSE_CODE = "shell.recursive_read_unbounded";
20
22
  function operandFamily(paths) {
@@ -27,8 +29,8 @@ function operandFamily(paths) {
27
29
  export function bashReversibilityProbe(allow, boundary) {
28
30
  const allowSet = new Set(allow ?? bashClassifyDefaultAllow());
29
31
  const judge = (args, command, resolved, offers) => {
30
- const boundaryGate = (verdict) => readBoundaryMandate(command, allowSet, resolved, verdict);
31
- const outOfRootGate = () => readBoundaryMandate(command, allowSet, resolved, {});
32
+ const boundaryGate = (verdict) => readBoundaryMandate(command, allowSet, resolved, verdict, CLASSIFY_SEAT_DECLINE_EVIDENCE);
33
+ const outOfRootGate = () => readBoundaryMandate(command, allowSet, resolved, {}, CLASSIFY_SEAT_DECLINE_EVIDENCE);
32
34
  const backgrounded = isBackgroundShellCall(args);
33
35
  const detailed = classifyCompoundReadonlyDetailed(command, allowSet, resolved);
34
36
  if (backgrounded)
@@ -46,7 +48,8 @@ export function bashReversibilityProbe(allow, boundary) {
46
48
  if (loop.reason === undefined)
47
49
  return { reversible: true };
48
50
  const seatOffer = (verdict) => (offers ? readRootCandidateOf(verdict) : {});
49
- if (boundaryDeclared(loop) || unjudgedRead(command, allowSet, resolved, loop))
51
+ const loopScan = refusedCommandScan(command, allowSet, resolved, loop, CLASSIFY_SEAT_DECLINE_EVIDENCE);
52
+ if (boundaryDeclared(loop) || unjudgedRead(command, allowSet, resolved, loop, loopScan))
50
53
  return { reversible: false, mandated: true, ...seatOffer(loop) };
51
54
  const refusedCause = recursiveReadCause(detailed);
52
55
  return { reversible: false, ...boundaryGate(detailed), ...seatOffer(detailed), ...(refusedCause !== undefined ? { cause: refusedCause } : {}) };
@@ -86,24 +89,39 @@ function shellCommandOf(args) {
86
89
  function isBackgroundShellCall(args) {
87
90
  return args?.run_in_background === true;
88
91
  }
89
- function readBoundaryMandate(command, allowSet, resolved, verdict) {
92
+ export const BOUNDARY_SEAT_DECLINE_EVIDENCE = new Set(SHELL_SCAN_DECLINES);
93
+ export const CLASSIFY_SEAT_DECLINE_EVIDENCE = new Set();
94
+ function readBoundaryMandate(command, allowSet, resolved, verdict, declineEvidence) {
90
95
  if (resolved === undefined)
91
96
  return {};
92
- if (denyJudgeSpoke(verdict) || unjudgedRead(command, allowSet, resolved, verdict))
97
+ const scan = refusedCommandScan(command, allowSet, resolved, verdict, declineEvidence);
98
+ if (denyJudgeSpoke(verdict) || unjudgedRead(command, allowSet, resolved, verdict, scan))
93
99
  return { mandated: true };
94
- return classifyOutOfRootReadGate(command, allowSet, resolved).gated ? { mandated: true } : {};
100
+ const named = classifyOutOfRootReadGate(command, allowSet, resolved);
101
+ if (named.gated || named.denied === true)
102
+ return { mandated: true };
103
+ return declinedRung(scan);
104
+ }
105
+ function declinedRung(scan) {
106
+ return scan.declined ? { boundaryDeclined: true } : {};
107
+ }
108
+ function refusedCommandScan(command, allowSet, resolved, verdict, declineEvidence) {
109
+ return verdict.refusedWhole === true ? refusedCommandMayRead(command, allowSet, resolved, declineEvidence) : { mayRead: false, declined: false };
95
110
  }
96
111
  function boundaryUnjudged(verdict) {
97
112
  return verdict.refusedWhole === true || (verdict.unresolvedOperands !== undefined && verdict.unresolvedOperands.length > 0);
98
113
  }
99
- function unjudgedRead(command, allowSet, resolved, verdict) {
114
+ function unjudgedRead(command, allowSet, resolved, verdict, scan) {
100
115
  if (verdict.unresolvedOperands !== undefined && verdict.unresolvedOperands.length > 0)
101
116
  return true;
102
117
  if (verdict.refusedWhole !== true)
103
118
  return false;
104
- if (refusedCommandMayRead(command, allowSet, resolved))
119
+ if (scan.mayRead)
105
120
  return true;
106
- return resolved !== undefined && classifyOutOfRootReadGate(command, allowSet, resolved).gated;
121
+ if (resolved === undefined)
122
+ return false;
123
+ const named = classifyOutOfRootReadGate(command, allowSet, resolved);
124
+ return named.gated || named.denied === true;
107
125
  }
108
126
  function recursiveReadCause(detailed) {
109
127
  const recursive = detailed.recursiveReadPaths;
@@ -121,7 +139,7 @@ export function bashReadBoundaryProbe(boundary) {
121
139
  const allowSet = new Set(bashClassifyDefaultAllow());
122
140
  const judge = (command, resolved, offers) => {
123
141
  const seatVerdict = (verdict) => {
124
- if (boundaryDeclared(verdict) || unjudgedRead(command, allowSet, resolved, verdict)) {
142
+ if (boundaryDeclared(verdict) || unjudgedRead(command, allowSet, resolved, verdict, refusedCommandScan(command, allowSet, resolved, verdict, BOUNDARY_SEAT_DECLINE_EVIDENCE))) {
125
143
  const cause = recursiveReadCause(verdict);
126
144
  return { reversible: false, mandated: true, ...(cause !== undefined ? { cause } : {}) };
127
145
  }
@@ -145,7 +163,7 @@ export function bashReadBoundaryProbe(boundary) {
145
163
  const offerFrom = (verdict) => (offers ? readRootCandidateOf(verdict) : {});
146
164
  if (mandated !== undefined)
147
165
  return { ...mandated, ...offerFrom(detailed), ...offerFrom(loop) };
148
- if (readBoundaryMandate(command, allowSet, resolved, detailed).mandated === true)
166
+ if (readBoundaryMandate(command, allowSet, resolved, detailed, BOUNDARY_SEAT_DECLINE_EVIDENCE).mandated === true)
149
167
  return { reversible: false, mandated: true };
150
168
  return compoundVerdict ?? loopVerdict ?? { reversible: true };
151
169
  };
@@ -719,7 +737,7 @@ async function adoptBackgroundShell(req) {
719
737
  return { ok: true, taskId, outputFile };
720
738
  }
721
739
  catch (e) {
722
- const reason = e instanceof Error ? e.message : String(e);
740
+ const reason = thrownText(e);
723
741
  let kill;
724
742
  try {
725
743
  kill = await env.killBackground(shellId);
@@ -5,6 +5,7 @@ import { sha256, resolveKey, violationText, violationDetails, requireRead, check
5
5
  import { decodeTextBytes, encodeTextForFile, normalizeEditText, normalizeFileText } from "./encoding.js";
6
6
  import { MAX_EDIT_BYTES, decodeEditBytes, tooLargeToEditMessage, truncatedUtf16BodyMessage, persistedTextOf, notReadRefusalText, enoentMessage, FILE_STATE_TRAILER, FILE_PATH_PARAMS, ipynbRedirect, countLines, applyRecordedEdit, } from "./fs-shared.js";
7
7
  import { toolFace } from "../../core/tool-catalog-entries.js";
8
+ import { thrownText } from "../../core/thrown-value.js";
8
9
  async function envFinalWrite(env, key, content, signal, opts) {
9
10
  const fireEdited = () => {
10
11
  if (opts?.edited?.hook === undefined)
@@ -66,7 +67,7 @@ async function trackToolEdit(hook, tool, path, key, signal) {
66
67
  res = await hook({ tool, path, key, ...(signal !== undefined ? { signal } : {}) });
67
68
  }
68
69
  catch (err) {
69
- return { refusal: `Error (${tool}): the file-history first-touch backup failed (${clipHookText(err instanceof Error ? err.message : String(err))}) — nothing was written.` };
70
+ return { refusal: `Error (${tool}): the file-history first-touch backup failed (${clipHookText(thrownText(err))}) — nothing was written.` };
70
71
  }
71
72
  if (res.ok === false)
72
73
  return { refusal: clipHookText(res.refusal) };
@@ -80,7 +81,7 @@ async function gateToolWrite(hook, tool, path, key, content) {
80
81
  res = await hook({ tool, path, key, content });
81
82
  }
82
83
  catch (err) {
83
- return `Error (${tool}): the write gate failed (${clipHookText(err instanceof Error ? err.message : String(err))}) — nothing was written.`;
84
+ return `Error (${tool}): the write gate failed (${clipHookText(thrownText(err))}) — nothing was written.`;
84
85
  }
85
86
  if (res !== undefined && res.ok === false)
86
87
  return `Error (${tool}): write rejected by the write gate (${res.code}): ${clipHookText(res.reason)}`;
@@ -2,6 +2,7 @@ import { createHash } from "node:crypto";
2
2
  import { sliceHeadSafe } from "../../core/surrogate-safe-slice.js";
3
3
  import { pathTargetValue, protectivePathTargetOf } from "../../core/tool-registry.js";
4
4
  import { withGateTimeout, DEFAULT_GATE_AWAIT_TIMEOUT_MS } from "../../core/gate-bounded-await.js";
5
+ import { thrownText } from "../../core/thrown-value.js";
5
6
  export function fileArgPath(args) {
6
7
  const a = args;
7
8
  const fp = a?.file_path;
@@ -524,7 +525,7 @@ export async function canonicalizeTarget(env, path, signal, family, baseCwd) {
524
525
  catch (err) {
525
526
  if (seenThrow && err === thrownByProbe)
526
527
  throw err;
527
- return { ok: false, message: `cannot resolve path "${path}": ${err instanceof Error ? err.message : String(err)}` };
528
+ return { ok: false, message: `cannot resolve path "${path}": ${thrownText(err)}` };
528
529
  }
529
530
  }
530
531
  async function probeRealTarget(env, path, target, family, signal) {
@@ -1,6 +1,7 @@
1
1
  import { delimitUntrusted } from "../../core/untrusted-text.js";
2
2
  import { MAX_EXEC_OUTPUT_BYTES } from "../../core/exec-output-tail.js";
3
3
  import { hasBinaryExtension, isAbsoluteForFamily, isWinFormPath, joinForFamily, pathFamilyOf } from "./safety.js";
4
+ import { thrownText } from "../../core/thrown-value.js";
4
5
  const WALK_MAX_FILES = 5000;
5
6
  const WALK_MAX_DEPTH = 32;
6
7
  const GREP_DEFAULT_CAP = 250;
@@ -772,7 +773,7 @@ export async function jsGrepDetailed(env, root, p, signal, guards, deny, denyOut
772
773
  test = compilePattern(p);
773
774
  }
774
775
  catch (e) {
775
- return { text: `Error (grep): invalid regex: ${e instanceof Error ? e.message : String(e)}` };
776
+ return { text: `Error (grep): invalid regex: ${thrownText(e)}` };
776
777
  }
777
778
  const ignore = await buildIgnore(env, root, signal);
778
779
  const rootPrefix = root.replace(/[\\/]+$/, "") + (root.includes("\\") ? "\\" : "/");
@@ -912,7 +913,7 @@ export async function jsGrepDetailed(env, root, p, signal, guards, deny, denyOut
912
913
  catch (e) {
913
914
  if (e instanceof GrepBudgetExceeded)
914
915
  return { text: budgetError(shownPath, monotonicNow() - fileMatchStart) };
915
- return { text: `Error (grep): invalid regex: ${e instanceof Error ? e.message : String(e)}` };
916
+ return { text: `Error (grep): invalid regex: ${thrownText(e)}` };
916
917
  }
917
918
  fileCount = spans.length;
918
919
  if (mode === "content") {
@@ -3,6 +3,7 @@ import { defineTool, errorResult } from "../core/tools.js";
3
3
  import { hasBackgroundShell } from "../core/background-shell.js";
4
4
  import { MONITOR_DEFAULT_TIMEOUT_MS, MONITOR_MAX_TIMEOUT_MS, } from "../core/task-registry.js";
5
5
  import { toolFace } from "../core/tool-catalog-entries.js";
6
+ import { thrownText } from "../core/thrown-value.js";
6
7
  const MONITOR_DESCRIPTION = "Run a shell command in the background and watch its stdout as a stream of events.\n" +
7
8
  "\n" +
8
9
  "- Each line the command writes to stdout becomes a notification event delivered to you between turns; " +
@@ -98,7 +99,7 @@ export function createMonitorTool(env, opts) {
98
99
  });
99
100
  }
100
101
  catch (e) {
101
- const why = e instanceof Error ? e.message : String(e);
102
+ const why = thrownText(e);
102
103
  let killDetail;
103
104
  try {
104
105
  const kill = await env.killBackground(spawn.value.shellId);
@@ -106,7 +107,7 @@ export function createMonitorTool(env, opts) {
106
107
  killDetail = `${kill.error.code}: ${kill.error.message}`;
107
108
  }
108
109
  catch (killErr) {
109
- killDetail = killErr instanceof Error ? killErr.message : String(killErr);
110
+ killDetail = thrownText(killErr);
110
111
  }
111
112
  return errorResult(killDetail === undefined
112
113
  ? `Error (Monitor): the watch process was started but could not be registered (${why}); it has been terminated.`
package/dist/tools/sql.js CHANGED
@@ -1,4 +1,5 @@
1
1
  import { Type } from "typebox";
2
+ import { thrownText } from "../core/thrown-value.js";
2
3
  const FORBIDDEN = /\b(insert|update|delete|drop|alter|create|truncate|grant|revoke|merge|replace|attach|detach|pragma|vacuum|exec|execute|call|copy|lock|reindex)\b/i;
3
4
  class SqlValidationError extends Error {
4
5
  }
@@ -82,7 +83,7 @@ export function createSqlTool(opts) {
82
83
  validateReadOnlySql(sql, opts.allowTables);
83
84
  }
84
85
  catch (e) {
85
- const msg = e instanceof Error ? e.message : String(e);
86
+ const msg = thrownText(e);
86
87
  return { content: `SQL rejected: ${msg}`, details: { error: msg }, terminate: false };
87
88
  }
88
89
  const rows = await opts.query(stripTrailingSemicolon(sql.trim()));
package/dist/tools/web.js CHANGED
@@ -8,6 +8,7 @@ import { DEFAULT_CHARS_PER_TOKEN } from "../internal/harness.js";
8
8
  import { binaryMagicFormat } from "./fs/safety.js";
9
9
  import { pdfMagicMatches } from "./fs/pdf.js";
10
10
  import { toolFace } from "../core/tool-catalog-entries.js";
11
+ import { thrownText } from "../core/thrown-value.js";
11
12
  const MAX_REDIRECTS = 10;
12
13
  const MAX_URL_LENGTH = 2048;
13
14
  const WEBFETCH_ACCEPT = "text/markdown, text/html, */*";
@@ -447,7 +448,7 @@ export function webFetchToolSpec(config = {}) {
447
448
  };
448
449
  }
449
450
  return {
450
- content: `Error (WebFetch): request failed: ${e instanceof Error ? e.message : String(e)}`,
451
+ content: `Error (WebFetch): request failed: ${thrownText(e)}`,
451
452
  details: errDetails,
452
453
  isError: true,
453
454
  };
@@ -594,7 +595,7 @@ export function webFetchToolSpec(config = {}) {
594
595
  catch (e) {
595
596
  out = text;
596
597
  note =
597
- `[note: summarization failed (${inlineUntrusted(e instanceof Error ? e.message : String(e), 160)}) — ` +
598
+ `[note: summarization failed (${inlineUntrusted(thrownText(e), 160)}) — ` +
598
599
  `raw page content follows; the requested analysis ("${inlineUntrusted(prompt, 120)}") was NOT applied]`;
599
600
  }
600
601
  }
@@ -910,7 +911,7 @@ export function createWebSearchTool(config) {
910
911
  return abortedFrame();
911
912
  if (ac.signal.aborted)
912
913
  return timedOutFrame();
913
- const msg = redactSecrets(e instanceof Error ? e.message : String(e)).trim();
914
+ const msg = redactSecrets(thrownText(e)).trim();
914
915
  const headline = "Error (WebSearch): the search backend failed.";
915
916
  const verdict = classifySearchFailure(msg);
916
917
  const composed = (msg ? `${headline} The backend's error text follows:\n\n${delimitUntrusted("WebSearch backend error", msg, SEARCH_ERROR_EXCERPT_CHARS)}` : headline) + `\n\n${verdict.hint}`;
@@ -1029,18 +1030,7 @@ export async function probeSearchBackend(search, options) {
1029
1030
  return { ok: true, results: results.length };
1030
1031
  }
1031
1032
  catch (e) {
1032
- let text;
1033
- if (e instanceof Error)
1034
- text = e.message;
1035
- else {
1036
- try {
1037
- text = String(e);
1038
- }
1039
- catch {
1040
- text = Object.prototype.toString.call(e);
1041
- }
1042
- }
1043
- return { ok: false, error: text };
1033
+ return { ok: false, error: thrownText(e) };
1044
1034
  }
1045
1035
  finally {
1046
1036
  clearTimeout(timer);
@@ -3,6 +3,7 @@ import { Type } from "typebox";
3
3
  import { defineTool, errorResult } from "../core/tools.js";
4
4
  import { addWorktree, pruneWorktrees, WORKTREE_PARENT } from "../core/git-worktree-env.js";
5
5
  import { toolFace } from "../core/tool-catalog-entries.js";
6
+ import { thrownText } from "../core/thrown-value.js";
6
7
  const ENTER_DESCRIPTION = "Use this tool ONLY when explicitly instructed to work in a worktree — either by the user directly, or by " +
7
8
  "project instructions. This tool creates an isolated git worktree and switches the current session into it.\n" +
8
9
  "\n" +
@@ -67,14 +68,16 @@ const EXIT_DESCRIPTION = "Exit a worktree session created by EnterWorktree and r
67
68
  "- Once exited, EnterWorktree can be called again to create a fresh worktree.";
68
69
  const shq = (s) => `'${s.replace(/'/g, "'\\''")}'`;
69
70
  function reasonOf(e) {
70
- if (e instanceof Error)
71
- return e.message;
72
- if (typeof e === "object" && e !== null) {
73
- const m = e.message;
74
- if (typeof m === "string" && m !== "")
75
- return m;
71
+ try {
72
+ if (typeof e === "object" && e !== null && !(e instanceof Error)) {
73
+ const m = e.message;
74
+ if (typeof m === "string" && m !== "")
75
+ return m;
76
+ }
77
+ }
78
+ catch {
76
79
  }
77
- return String(e);
80
+ return thrownText(e);
78
81
  }
79
82
  async function resolveBaseCommit(env, repoRoot, baseRef) {
80
83
  const rev = async (ref) => {
@@ -147,7 +150,7 @@ export function createWorktreeTools(env, opts) {
147
150
  try {
148
151
  const probe = await env.exec("git rev-parse --is-inside-work-tree", { cwd: repoRoot });
149
152
  if (!probe.ok || probe.value.exitCode !== 0) {
150
- const detail = probe.ok ? (probe.value.stderr || probe.value.stdout).trim().split("\n")[0] : String(probe.error);
153
+ const detail = probe.ok ? (probe.value.stderr || probe.value.stdout).trim().split("\n")[0] : thrownText(probe.error);
151
154
  return errorResult(`Error (EnterWorktree): the working directory ${repoRoot} is not a git repository` +
152
155
  `${detail ? ` (${detail})` : ""}. This deployment has no VCS-agnostic worktree extension point, ` +
153
156
  "so worktree isolation is unavailable here.");
@@ -214,7 +217,7 @@ export function createWorktreeTools(env, opts) {
214
217
  createdDir = worktreeDir;
215
218
  }
216
219
  catch (e) {
217
- return errorResult(`Error (EnterWorktree): ${e instanceof Error ? e.message : String(e)}`);
220
+ return errorResult(`Error (EnterWorktree): ${thrownText(e)}`);
218
221
  }
219
222
  session.current = {
220
223
  worktreeDir: createdDir,