@sema-agent/core 7.53.0 → 7.54.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 (244) hide show
  1. package/CHANGELOG.md +277 -1
  2. package/LICENSE +3 -1
  3. package/dist/agents/peer-session-drain.d.ts +0 -1
  4. package/dist/agents/peer-session-drain.js +0 -2
  5. package/dist/agents/send-message-tool.js +3 -4
  6. package/dist/agents/subagent.d.ts +3 -5
  7. package/dist/agents/subagent.js +18 -11
  8. package/dist/agents/tool-filter.d.ts +7 -4
  9. package/dist/agents/tool-filter.js +7 -3
  10. package/dist/core/a2a.js +4 -2
  11. package/dist/core/allow-entry.d.ts +1 -0
  12. package/dist/core/allow-entry.js +1 -0
  13. package/dist/core/approval-object.d.ts +87 -0
  14. package/dist/core/approval-object.js +85 -0
  15. package/dist/core/arg-summary.d.ts +3 -2
  16. package/dist/core/arg-summary.js +17 -11
  17. package/dist/core/ask-origin.d.ts +43 -27
  18. package/dist/core/ask-origin.js +20 -11
  19. package/dist/core/ask-question.d.ts +0 -1
  20. package/dist/core/ask-question.js +4 -67
  21. package/dist/core/ask-unresolvable-notice.d.ts +3 -2
  22. package/dist/core/auto-compaction.d.ts +49 -50
  23. package/dist/core/auto-compaction.js +41 -51
  24. package/dist/core/auto-mode-prompt.d.ts +4 -2
  25. package/dist/core/auto-mode-prompt.js +4 -3
  26. package/dist/core/canonical-json.d.ts +69 -0
  27. package/dist/core/canonical-json.js +130 -8
  28. package/dist/core/checkpoint-requirements.d.ts +13 -7
  29. package/dist/core/checkpoint-requirements.js +4 -1
  30. package/dist/core/checkpoint-store.d.ts +33 -18
  31. package/dist/core/checkpoint-store.js +5 -4
  32. package/dist/core/child-clamp-seats.d.ts +45 -6
  33. package/dist/core/child-clamp-seats.js +56 -4
  34. package/dist/core/consolidate-scope.d.ts +1 -1
  35. package/dist/core/cross-boundary-taint.d.ts +213 -0
  36. package/dist/core/cross-boundary-taint.js +196 -0
  37. package/dist/core/decision-receipt.d.ts +44 -1
  38. package/dist/core/decision-receipt.js +16 -0
  39. package/dist/core/effective-path-target.d.ts +3 -0
  40. package/dist/core/effective-path-target.js +4 -0
  41. package/dist/core/engine-notice.d.ts +34 -2
  42. package/dist/core/engine-notice.js +17 -1
  43. package/dist/core/engine-storage-backend.d.ts +73 -0
  44. package/dist/core/engine-storage-backend.js +1 -0
  45. package/dist/core/file-history-store.d.ts +6 -2
  46. package/dist/core/file-history-store.js +1 -0
  47. package/dist/core/gate-fold.js +10 -2
  48. package/dist/core/gate-lanes.js +30 -21
  49. package/dist/core/gate-outcome.d.ts +7 -3
  50. package/dist/core/governance-codes.d.ts +7 -2
  51. package/dist/core/governance-codes.js +6 -0
  52. package/dist/core/hooks.d.ts +14 -14
  53. package/dist/core/hooks.js +1 -2
  54. package/dist/core/locked-config.d.ts +28 -1
  55. package/dist/core/locked-config.js +30 -3
  56. package/dist/core/lsp.d.ts +1 -1
  57. package/dist/core/mcp-server-spec.d.ts +66 -9
  58. package/dist/core/mcp-server-spec.js +52 -19
  59. package/dist/core/mcp.js +5 -4
  60. package/dist/core/memory-engine/engine.js +2 -1
  61. package/dist/core/memory-engine/export-bundle.d.ts +1 -1
  62. package/dist/core/memory-engine/export-bundle.js +1 -1
  63. package/dist/core/memory-engine/file-backend.d.ts +2 -6
  64. package/dist/core/memory-engine/file-backend.js +1 -12
  65. package/dist/core/memory.d.ts +3 -3
  66. package/dist/core/mode-policy.d.ts +13 -8
  67. package/dist/core/mode-policy.js +3 -2
  68. package/dist/core/mods/kernel/receive.d.ts +2 -1
  69. package/dist/core/mods/kernel/receive.js +7 -2
  70. package/dist/core/parent-spec-seat.d.ts +10 -13
  71. package/dist/core/parent-spec-seat.js +14 -6
  72. package/dist/core/pause-registry.d.ts +35 -1
  73. package/dist/core/pause-registry.js +10 -0
  74. package/dist/core/permission-rule-consent.d.ts +18 -5
  75. package/dist/core/permission-rule-consent.js +13 -8
  76. package/dist/core/permission-rule-model.d.ts +29 -6
  77. package/dist/core/permission-rule-model.js +6 -1
  78. package/dist/core/permission-rule-org.d.ts +37 -0
  79. package/dist/core/permission-rule-org.js +34 -42
  80. package/dist/core/permission-rule-session.js +2 -2
  81. package/dist/core/permission-rule-store.d.ts +8 -0
  82. package/dist/core/permission-rule-store.js +1 -0
  83. package/dist/core/permission-rule-sync.js +3 -2
  84. package/dist/core/read-only-shell.d.ts +1 -1
  85. package/dist/core/read-only-shell.js +30 -9
  86. package/dist/core/remote-env.d.ts +41 -1
  87. package/dist/core/remote-env.js +18 -0
  88. package/dist/core/resume-startup-cause.d.ts +18 -0
  89. package/dist/core/resume-startup-cause.js +12 -0
  90. package/dist/core/runner/approval-feedback.d.ts +46 -0
  91. package/dist/core/runner/approval-feedback.js +41 -0
  92. package/dist/core/runner/compaction-call-options.d.ts +3 -3
  93. package/dist/core/runner/compaction-call-options.js +2 -2
  94. package/dist/core/runner/contracts.d.ts +33 -41
  95. package/dist/core/runner/cross-boundary-lane.d.ts +87 -0
  96. package/dist/core/runner/cross-boundary-lane.js +190 -0
  97. package/dist/core/runner/decide-continuation.d.ts +4 -4
  98. package/dist/core/runner/decide-continuation.js +3 -1
  99. package/dist/core/runner/gate-exit.d.ts +32 -12
  100. package/dist/core/runner/gate-exit.js +75 -33
  101. package/dist/core/runner/inherited-ask-grants.js +2 -2
  102. package/dist/core/runner/person-path-rows.d.ts +2 -2
  103. package/dist/core/runner/prepare-acquire-reconcile.d.ts +3 -4
  104. package/dist/core/runner/prepare-acquire-reconcile.js +3 -3
  105. package/dist/core/runner/prepare-ask-lane.js +3 -3
  106. package/dist/core/runner/prepare-caps-and-workflow.d.ts +6 -5
  107. package/dist/core/runner/prepare-caps-and-workflow.js +3 -3
  108. package/dist/core/runner/prepare-config-doors.d.ts +12 -15
  109. package/dist/core/runner/prepare-config-doors.js +8 -3
  110. package/dist/core/runner/prepare-context-lane.js +6 -3
  111. package/dist/core/runner/prepare-delegation-surface.d.ts +3 -2
  112. package/dist/core/runner/prepare-delegation-surface.js +2 -3
  113. package/dist/core/runner/prepare-execution-env.d.ts +1 -2
  114. package/dist/core/runner/prepare-execution-env.js +2 -1
  115. package/dist/core/runner/prepare-gate-stations.d.ts +2 -0
  116. package/dist/core/runner/prepare-gate-stations.js +4 -3
  117. package/dist/core/runner/prepare-hands-readface.d.ts +2 -2
  118. package/dist/core/runner/prepare-hands-readface.js +1 -1
  119. package/dist/core/runner/prepare-inherited-gate.d.ts +2 -0
  120. package/dist/core/runner/prepare-inherited-gate.js +1 -0
  121. package/dist/core/runner/prepare-park-ask.d.ts +1 -1
  122. package/dist/core/runner/prepare-park-ask.js +9 -3
  123. package/dist/core/runner/prepare-policy-chain.js +2 -7
  124. package/dist/core/runner/prepare-safety-scan.d.ts +7 -2
  125. package/dist/core/runner/prepare-safety-scan.js +34 -27
  126. package/dist/core/runner/prepare-suspend-saga.d.ts +2 -2
  127. package/dist/core/runner/prepare-suspend-saga.js +6 -2
  128. package/dist/core/runner/prepare-task.js +13 -10
  129. package/dist/core/runner/prepare-turn-wiring.d.ts +10 -5
  130. package/dist/core/runner/prepare-turn-wiring.js +14 -49
  131. package/dist/core/runner/prepare-wiring-manifest.d.ts +2 -2
  132. package/dist/core/runner/prepare-wiring-manifest.js +2 -2
  133. package/dist/core/runner/prepare-workspace-restore.js +2 -2
  134. package/dist/core/runner/resume-admission.js +9 -9
  135. package/dist/core/runner/resume-apply.js +4 -4
  136. package/dist/core/runner/resume-policy-outcome.d.ts +2 -2
  137. package/dist/core/runner/resume-policy-outcome.js +18 -15
  138. package/dist/core/runner/resume-preflight.js +2 -2
  139. package/dist/core/runner/resume-review-outcome.js +3 -3
  140. package/dist/core/runner/run-leg.js +57 -33
  141. package/dist/core/runner/run-settle-and-teardown.js +2 -0
  142. package/dist/core/runner/run-terminal-adoption.js +15 -4
  143. package/dist/core/runner/session-file-state-replay.d.ts +5 -2
  144. package/dist/core/runner/session-file-state-replay.js +3 -2
  145. package/dist/core/runner/stream-steer-verb.js +2 -2
  146. package/dist/core/runner/tool-face-overlay.d.ts +24 -6
  147. package/dist/core/runner/tool-face-overlay.js +24 -15
  148. package/dist/core/runner-deps.d.ts +9 -9
  149. package/dist/core/session-store.d.ts +35 -16
  150. package/dist/core/session-store.js +20 -12
  151. package/dist/core/{shell-lexer.d.ts → shell-run-candidates.d.ts} +36 -12
  152. package/dist/core/{shell-lexer.js → shell-run-candidates.js} +145 -225
  153. package/dist/core/shell-wrapper-table.d.ts +5 -3
  154. package/dist/core/shell-wrapper-table.js +1 -1
  155. package/dist/core/store-contracts/checkpoint-store-contract.js +2 -2
  156. package/dist/core/store-contracts/session-store-contract.d.ts +49 -0
  157. package/dist/core/store-contracts/session-store-contract.js +639 -0
  158. package/dist/core/store-contracts/session-store-incarnation-contract.d.ts +12 -0
  159. package/dist/core/store-contracts/session-store-incarnation-contract.js +5 -2
  160. package/dist/core/task-event.d.ts +6 -4
  161. package/dist/core/task-registry-agent.js +1 -0
  162. package/dist/core/task-registry-shared.d.ts +5 -0
  163. package/dist/core/task-registry.d.ts +7 -0
  164. package/dist/core/task-registry.js +17 -0
  165. package/dist/core/task-result.d.ts +1 -1
  166. package/dist/core/task-spec.d.ts +6 -6
  167. package/dist/core/tighten-task-spec.d.ts +6 -4
  168. package/dist/core/tighten-task-spec.js +2 -2
  169. package/dist/core/tool-catalog-entries.d.ts +1 -1
  170. package/dist/core/tool-catalog-entries.js +52 -52
  171. package/dist/core/tool-face-reader.d.ts +49 -0
  172. package/dist/core/tool-face-reader.js +94 -0
  173. package/dist/core/tool-face.d.ts +17 -8
  174. package/dist/core/tool-face.js +1 -0
  175. package/dist/core/tool-policy.d.ts +64 -23
  176. package/dist/core/tool-policy.js +62 -56
  177. package/dist/core/tool-roster.d.ts +46 -28
  178. package/dist/core/tool-roster.js +91 -14
  179. package/dist/core/tool-spec-members.js +2 -2
  180. package/dist/core/tool-spec.d.ts +20 -4
  181. package/dist/core/tools.js +2 -0
  182. package/dist/core/trace.d.ts +24 -2
  183. package/dist/core/types.d.ts +2 -2
  184. package/dist/core/types.js +1 -1
  185. package/dist/core/untrusted-text.d.ts +7 -0
  186. package/dist/core/untrusted-text.js +3 -8
  187. package/dist/core/user-attachment.d.ts +43 -0
  188. package/dist/core/user-attachment.js +92 -15
  189. package/dist/engine/compaction/compaction.d.ts +2 -2
  190. package/dist/engine/compaction/utils.d.ts +2 -2
  191. package/dist/engine/harness/types.d.ts +7 -4
  192. package/dist/engine/llm/types.d.ts +7 -2
  193. package/dist/engine/llm/types.js +2 -1
  194. package/dist/engine/session/repo-utils.d.ts +5 -0
  195. package/dist/engine/session/repo-utils.js +9 -0
  196. package/dist/engine/session/storage-base.js +2 -2
  197. package/dist/index.d.ts +23 -9
  198. package/dist/index.js +19 -8
  199. package/dist/internal/llm.d.ts +1 -1
  200. package/dist/internal/llm.js +1 -1
  201. package/dist/orchestration/run-workflow-tool.d.ts +3 -0
  202. package/dist/orchestration/run-workflow-tool.js +1 -0
  203. package/dist/orchestration/workflow-governance.d.ts +1 -1
  204. package/dist/orchestration/workflow.d.ts +3 -0
  205. package/dist/orchestration/workflow.js +2 -1
  206. package/dist/prompt-assembly/catalog/schema.d.ts +3 -0
  207. package/dist/prompt-assembly/catalog/schema.js +3 -0
  208. package/dist/prompt-assembly/overlay.js +2 -2
  209. package/dist/prompt-assembly/select.js +2 -2
  210. package/dist/prompts/default.d.ts +1 -1
  211. package/dist/shell/lex.d.ts +87 -8
  212. package/dist/shell/lex.js +283 -49
  213. package/dist/stores/file/index.d.ts +8 -6
  214. package/dist/tools/artifact/artifact-tool.js +10 -0
  215. package/dist/tools/fs/bash-program-position.d.ts +3 -15
  216. package/dist/tools/fs/bash-program-position.js +3 -10
  217. package/dist/tools/fs/bash-readonly-classifier.js +2 -0
  218. package/dist/tools/fs/fs-bash.d.ts +3 -3
  219. package/dist/tools/fs/fs-bash.js +72 -101
  220. package/dist/tools/fs/fs-read.d.ts +12 -0
  221. package/dist/tools/fs/fs-read.js +10 -9
  222. package/dist/tools/fs/fs-shared.d.ts +27 -31
  223. package/dist/tools/fs/fs-shared.js +12 -15
  224. package/dist/tools/fs/fs-write.js +1 -1
  225. package/dist/tools/fs/index.js +2 -2
  226. package/dist/tools/fs/read-deny-home.d.ts +1 -1
  227. package/dist/tools/fs/read-deny-home.js +5 -4
  228. package/dist/tools/fs/read-deny.d.ts +16 -3
  229. package/dist/tools/fs/read-deny.js +12 -9
  230. package/dist/tools/fs/safety.d.ts +7 -6
  231. package/dist/tools/fs/shell-read-bindings.d.ts +5 -3
  232. package/dist/tools/fs/shell-read-bindings.js +15 -16
  233. package/dist/tools/fs/shell-read-device.js +1 -1
  234. package/dist/tools/fs/shell-read-plan.js +36 -47
  235. package/dist/tools/fs/shell-read-word.d.ts +1 -6
  236. package/dist/tools/fs/shell-read-word.js +2 -20
  237. package/dist/tools/fs/shell-reentry-text.js +1 -1
  238. package/dist/tools/web.d.ts +2 -2
  239. package/package.json +4 -1
  240. package/test/export-surface.snapshot.json +5654 -5520
  241. package/wire-changes/core@7.53.0.json +30 -8
  242. package/wire-changes/core@7.54.0.json +2870 -0
  243. package/dist/core/shell-scan.d.ts +0 -60
  244. package/dist/core/shell-scan.js +0 -183
package/dist/shell/lex.js CHANGED
@@ -2,13 +2,14 @@ import { shellWordsOf } from "./shell-words.js";
2
2
  export const SHELL_LEX_DIALECT = "bash";
3
3
  export const QUOTE_KINDS = Object.freeze(["'", '"', "$'", '$"', "\\"]);
4
4
  export const EXPANSION_KINDS = Object.freeze(["parameter", "command", "process", "arithmetic", "tilde", "brace", "glob"]);
5
- export const UNRESOLVED_REASONS = Object.freeze(["ansi_c_escape", "locale_string", "parameter_text", "subscript_text", "compound_array_value"]);
5
+ export const UNRESOLVED_REASONS = Object.freeze(["ansi_c_escape", "locale_string", "parameter_text", "subscript_text", "compound_array_value", "trailing_backslash"]);
6
6
  export const UNRESOLVED_SENTENCE = {
7
7
  ansi_c_escape: "an ANSI-C quoted word carries an escape this lexer does not fold",
8
8
  locale_string: "a locale-translated string carries text this lexer does not read",
9
9
  parameter_text: "a parameter expansion carries text this lexer does not read",
10
10
  subscript_text: "a subscript carries text this lexer does not read",
11
11
  compound_array_value: "a compound assignment value is a list this lexer does not read",
12
+ trailing_backslash: "a backslash ends the command, which the shell versions read differently (3.2 drops it, 5 keeps it)",
12
13
  };
13
14
  export const PARSE_FAILURE_REASONS = Object.freeze([
14
15
  "unterminated_single_quote",
@@ -19,6 +20,7 @@ export const PARSE_FAILURE_REASONS = Object.freeze([
19
20
  "unterminated_command_substitution",
20
21
  "unterminated_process_substitution",
21
22
  "unterminated_parameter_expansion",
23
+ "unterminated_arithmetic_expansion",
22
24
  "unterminated_heredoc",
23
25
  "undelimited_substitution",
24
26
  "redirect_without_target",
@@ -34,6 +36,7 @@ export const PARSE_FAILURE_SENTENCE = {
34
36
  unterminated_command_substitution: "a command substitution never terminates",
35
37
  unterminated_process_substitution: "a process substitution never terminates",
36
38
  unterminated_parameter_expansion: "a parameter expansion never terminates",
39
+ unterminated_arithmetic_expansion: "an arithmetic expansion never terminates",
37
40
  unterminated_heredoc: "a here-document body never terminates",
38
41
  undelimited_substitution: "a command substitution carries a construct this lexer cannot delimit",
39
42
  redirect_without_target: "a redirection names no target",
@@ -71,13 +74,14 @@ export const SUBSTITUTION_PLACEHOLDER = " subst";
71
74
  export const SHELL_CONTROL_PREFIXES = shellWordsOf("control_prefix");
72
75
  const COMPOUND_OPERAND_COMMANDS = new Set(["alias", "declare", "eval", "export", "let", "local", "readonly", "typeset"]);
73
76
  const GROUP_WORDS = shellWordsOf("group");
77
+ const SCOPE_OPEN_WORDS = shellWordsOf("scope_open");
74
78
  const DEFINITION_WORDS = shellWordsOf("definition");
75
79
  const BACKGROUND_WORDS = shellWordsOf("background");
76
80
  const COMPOUND_OPEN_WORDS = shellWordsOf("compound_open");
77
81
  const CASE_ARM_WORDS = shellWordsOf("case_arms");
78
82
  const TIMER_WORDS = shellWordsOf("timer");
79
83
  export function dollarOpensQuote(text, at) {
80
- const next = text[at + 1];
84
+ const next = text[past(text, at + 1)];
81
85
  if (next !== "'" && next !== '"')
82
86
  return false;
83
87
  let run = 0;
@@ -218,7 +222,7 @@ function heredocHeaderAt(text, from, dash, op) {
218
222
  return fail("heredoc_without_delimiter", op);
219
223
  if (/[~[]/.test(raw))
220
224
  return "undelimited";
221
- const lexed = scan(raw, true);
225
+ const lexed = scan(raw, true, [firstSegment(raw)], 0);
222
226
  const tokens = "command" in lexed && lexed.command.segments.length === 1 ? lexed.command.segments[0].tokens : [];
223
227
  const word = tokens.length === 1 ? tokens[0] : undefined;
224
228
  if (word?.kind !== "word")
@@ -228,8 +232,9 @@ function heredocHeaderAt(text, from, dash, op) {
228
232
  }
229
233
  const BASH_READING = { heredoc: true, startComment: true };
230
234
  const BASH32_READINGS = [{ heredoc: false, startComment: true }, { heredoc: false, startComment: false }];
235
+ const ARITHMETIC_READING = { heredoc: false, startComment: true, arithmetic: true };
231
236
  const BODY_NESTING_MAX = 64;
232
- function walkBody(text, open, reading, inDouble, level = 0) {
237
+ function walkBody(text, open, reading, inDouble, level) {
233
238
  if (level > BODY_NESTING_MAX)
234
239
  return "undelimited";
235
240
  let depth = 0;
@@ -237,9 +242,10 @@ function walkBody(text, open, reading, inDouble, level = 0) {
237
242
  let wordStart = true;
238
243
  let word = "";
239
244
  let heredoc = false;
245
+ let innerClose;
240
246
  const owed = [];
241
247
  const nested = (at, dq) => {
242
- const inner = walkBody(text, at, reading, dq, level + 1);
248
+ const inner = walkBody(text, at, reading.arithmetic === true ? BASH_READING : reading, dq, level + 1);
243
249
  if (typeof inner === "object" && "end" in inner) {
244
250
  heredoc ||= inner.heredoc;
245
251
  return inner.end - 1;
@@ -278,6 +284,13 @@ function walkBody(text, open, reading, inDouble, level = 0) {
278
284
  continue;
279
285
  }
280
286
  if (c === "$" && text[k] === "{") {
287
+ if (opensCurrentShellSubstitution(text, k)) {
288
+ const run = currentShellSubstitutionEnd(text, k, level + 1);
289
+ if (typeof run !== "object" || !("end" in run))
290
+ return run;
291
+ j = run.end - 1;
292
+ continue;
293
+ }
281
294
  const close = braceExpansionEnd(text, k);
282
295
  if (close === undefined)
283
296
  return "unterminated";
@@ -356,7 +369,16 @@ function walkBody(text, open, reading, inDouble, level = 0) {
356
369
  word = "$";
357
370
  continue;
358
371
  }
359
- if (reading.heredoc && c === "$" && text[k] === "{") {
372
+ if ((reading.heredoc || reading.arithmetic === true) && c === "$" && text[k] === "{") {
373
+ if (opensCurrentShellSubstitution(text, k)) {
374
+ const run = currentShellSubstitutionEnd(text, k, level + 1);
375
+ if (typeof run !== "object" || !("end" in run))
376
+ return run;
377
+ j = run.end - 1;
378
+ wordStart = false;
379
+ word = "";
380
+ continue;
381
+ }
360
382
  const close = braceExpansionEnd(text, k);
361
383
  if (close === undefined)
362
384
  return "unterminated";
@@ -383,8 +405,10 @@ function walkBody(text, open, reading, inDouble, level = 0) {
383
405
  depth++;
384
406
  else if (c === ")") {
385
407
  depth--;
408
+ if (depth === 1)
409
+ innerClose ??= j;
386
410
  if (depth === 0)
387
- return owed.length > 0 ? "undelimited" : { end: j + 1, heredoc };
411
+ return owed.length > 0 ? "undelimited" : { end: j + 1, heredoc, ...(innerClose !== undefined ? { innerClose } : {}) };
388
412
  }
389
413
  if (!reading.heredoc) {
390
414
  wordStart = j === open ? reading.startComment && !inDouble && level === 0 : c === " " || c === "\t" || c === "\n";
@@ -398,18 +422,29 @@ function walkBody(text, open, reading, inDouble, level = 0) {
398
422
  }
399
423
  return "unterminated";
400
424
  }
401
- function substitutionBodyEnd(text, open, inDouble) {
402
- const read = walkBody(text, open, BASH_READING, inDouble);
425
+ function substitutionBodyEnd(text, open, inDouble, level) {
426
+ const arithmetic = arithmeticWalk(text, open, inDouble, level);
427
+ if (arithmetic !== undefined)
428
+ return { end: arithmetic.end, arithmetic: true };
429
+ const read = walkBody(text, open, BASH_READING, inDouble, level);
403
430
  if (typeof read !== "object" || !("end" in read))
404
431
  return read;
405
432
  if (!read.heredoc)
406
- return read.end;
433
+ return { end: read.end, arithmetic: false };
407
434
  for (const reading of BASH32_READINGS) {
408
- const counted = walkBody(text, open, reading, inDouble);
435
+ const counted = walkBody(text, open, reading, inDouble, level);
409
436
  if (counted !== "unterminated" && !(typeof counted === "object" && "end" in counted && counted.end === read.end))
410
437
  return "undelimited";
411
438
  }
412
- return read.end;
439
+ return { end: read.end, arithmetic: false };
440
+ }
441
+ function arithmeticWalk(text, open, inDouble, level) {
442
+ if (text[past(text, open + 1)] !== "(")
443
+ return undefined;
444
+ const read = walkBody(text, open, ARITHMETIC_READING, inDouble, level);
445
+ if (typeof read !== "object" || !("end" in read) || read.heredoc || read.innerClose === undefined)
446
+ return undefined;
447
+ return past(text, read.innerClose + 1) === read.end - 1 ? { end: read.end, heredoc: false } : undefined;
413
448
  }
414
449
  function braceExpansionEnd(text, open) {
415
450
  let quoted;
@@ -435,7 +470,47 @@ function braceExpansionEnd(text, open) {
435
470
  }
436
471
  return undefined;
437
472
  }
438
- function matchedParen(text, open) {
473
+ function opensCurrentShellSubstitution(text, open) {
474
+ const after = text[open + 1];
475
+ return after === "|" || after === " " || after === "\t" || after === "\n";
476
+ }
477
+ function currentShellSubstitutionEnd(text, open, level) {
478
+ if (level > BODY_NESTING_MAX)
479
+ return "undelimited";
480
+ const list = { from: open + (text[open + 1] === "|" ? 2 : 1), groups: 0 };
481
+ const read = scan(text, false, [firstSegment(text)], level, list);
482
+ if ("parseFailure" in read)
483
+ return read;
484
+ return list.closed === undefined ? "undelimited" : { end: list.closed, from: list.from };
485
+ }
486
+ function bracketExpansionEnd(text, open) {
487
+ let depth = 0;
488
+ let quoted;
489
+ for (let j = open; j < text.length; j++) {
490
+ const c = text[j];
491
+ if (quoted !== undefined) {
492
+ if (c === "\\" && quoted === '"')
493
+ j++;
494
+ else if (c === quoted)
495
+ quoted = undefined;
496
+ continue;
497
+ }
498
+ if (c === "\\") {
499
+ j++;
500
+ continue;
501
+ }
502
+ if (c === "'" || c === '"') {
503
+ quoted = c;
504
+ continue;
505
+ }
506
+ if (c === "[")
507
+ depth++;
508
+ else if (c === "]" && --depth === 0)
509
+ return j + 1;
510
+ }
511
+ return undefined;
512
+ }
513
+ function matchedParen(text, open, level) {
439
514
  let depth = 0;
440
515
  let quoted;
441
516
  for (let j = open; j < text.length; j++) {
@@ -463,6 +538,13 @@ function matchedParen(text, open) {
463
538
  continue;
464
539
  }
465
540
  if (c === "$" && text[j + 1] === "{") {
541
+ if (opensCurrentShellSubstitution(text, j + 1)) {
542
+ const run = currentShellSubstitutionEnd(text, j + 1, level + 1);
543
+ if (typeof run !== "object" || !("end" in run))
544
+ return run === "undelimited" ? fail("undelimited_substitution", j) : run;
545
+ j = run.end - 1;
546
+ continue;
547
+ }
466
548
  const close = braceExpansionEnd(text, j + 1);
467
549
  if (close === undefined)
468
550
  return undefined;
@@ -477,9 +559,12 @@ function matchedParen(text, open) {
477
559
  return undefined;
478
560
  }
479
561
  const UNREADABLE_EXPANSION_TEXT = /[$`()<>{\\]/;
480
- const EXPANSION_NAME = /^[#!]?([A-Za-z_][A-Za-z0-9_]*|\d+|[@*#?$!0-])/;
562
+ const PARAMETER = String.raw `(?:[A-Za-z_][A-Za-z0-9_]*|\d+|[*@#?$!-])`;
563
+ const PARAMETER_HEAD = new RegExp(String.raw `^[#!]?${PARAMETER}`);
564
+ const EXPANSION_NAME = new RegExp(String.raw `^[#!]?(${PARAMETER})`);
481
565
  const BARE_PARAMETER = /^(?:[A-Za-z_][A-Za-z0-9_]*|\d|[@*#?$!0-])/;
482
566
  const IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]*$/;
567
+ const PER_ELEMENT_BODY = /^!?@|\[@\]|^![^}]*@$/;
483
568
  const NAME_HEAD = /^[A-Za-z_][A-Za-z0-9_]*/;
484
569
  function backtickBody(source, open) {
485
570
  let body = "";
@@ -501,7 +586,7 @@ function backtickBody(source, open) {
501
586
  }
502
587
  return undefined;
503
588
  }
504
- export function substitutionsInHeredocBody(body) {
589
+ export function expansionsInHeredocBody(body) {
505
590
  const out = [];
506
591
  for (let i = 0; i < body.length; i++) {
507
592
  const ch = body[i];
@@ -513,20 +598,121 @@ export function substitutionsInHeredocBody(body) {
513
598
  const tick = backtickBody(body, i);
514
599
  if (tick === undefined)
515
600
  return undefined;
516
- out.push({ body: tick.body, kind: "command" });
601
+ out.push({ expansion: { kind: "command", yields: "one", body: tick.body } });
517
602
  i = tick.close;
518
603
  continue;
519
604
  }
520
- if (ch === "$" && body[i + 1] === "(") {
521
- const end = substitutionBodyEnd(body, i + 1, true);
522
- if (typeof end !== "number")
605
+ if (ch !== "$")
606
+ continue;
607
+ const open = body[i + 1];
608
+ if (open === "(") {
609
+ const close = substitutionBodyEnd(body, i + 1, true, 0);
610
+ if (typeof close !== "object" || !("end" in close))
611
+ return undefined;
612
+ out.push({ expansion: { kind: close.arithmetic ? "arithmetic" : "command", yields: "one", body: body.slice(i + 2, close.end - 1) } });
613
+ i = close.end - 1;
614
+ }
615
+ else if (open === "[") {
616
+ const end = bracketExpansionEnd(body, i + 1);
617
+ if (end === undefined)
523
618
  return undefined;
524
- out.push({ body: body.slice(i + 2, end - 1), kind: body[i + 2] === "(" ? "arithmetic" : "command" });
619
+ out.push({ expansion: { kind: "arithmetic", yields: "one", body: body.slice(i + 2, end - 1) } });
525
620
  i = end - 1;
526
621
  }
622
+ else if (open === "{") {
623
+ if (opensCurrentShellSubstitution(body, i + 1)) {
624
+ const run = currentShellSubstitutionEnd(body, i + 1, 1);
625
+ if (typeof run !== "object" || !("end" in run))
626
+ return undefined;
627
+ out.push({ expansion: { kind: "command", yields: "one", body: body.slice(run.from, run.end - 1) }, currentShell: true });
628
+ i = run.end - 1;
629
+ continue;
630
+ }
631
+ const end = braceExpansionEnd(body, i + 1);
632
+ if (end === undefined)
633
+ return undefined;
634
+ const text = body.slice(i + 2, end - 1);
635
+ const name = EXPANSION_NAME.exec(text)?.[1];
636
+ out.push({ expansion: { kind: "parameter", yields: "one", body: text, ...(name !== undefined ? { name } : {}) }, ...(UNREADABLE_EXPANSION_TEXT.test(text) ? { unresolved: "parameter_text" } : {}) });
637
+ i = end - 1;
638
+ }
639
+ else {
640
+ const head = BARE_PARAMETER.exec(body.slice(i + 1, i + 65))?.[0];
641
+ if (head === undefined)
642
+ continue;
643
+ out.push({ expansion: { kind: "parameter", yields: "one", name: head } });
644
+ i += head.length;
645
+ }
527
646
  }
528
647
  return out;
529
648
  }
649
+ export function substitutionsInHeredocBody(body) {
650
+ const all = expansionsInHeredocBody(body);
651
+ if (all === undefined)
652
+ return undefined;
653
+ return all.flatMap(({ expansion: e, currentShell }) => (e.kind === "command" || e.kind === "arithmetic" ? [{ body: e.body ?? "", kind: e.kind, ...(currentShell === true ? { currentShell } : {}) }] : []));
654
+ }
655
+ export function heredocBodyHidesRun(body) {
656
+ const all = expansionsInHeredocBody(body);
657
+ return all === undefined || /[<>]\(/.test(body) || all.some((x) => x.unresolved !== undefined || expansionHidesRun(x.expansion));
658
+ }
659
+ const SUBSTITUTION_OPENER = /\$\(|`|<\(|>\(|\$\[/;
660
+ export function parameterArithmeticTexts(body) {
661
+ const text = body.replace(/\\\n/g, "");
662
+ const head = PARAMETER_HEAD.exec(text);
663
+ if (head === null)
664
+ return {};
665
+ let rest = text.slice(head[0].length);
666
+ let subscript;
667
+ if (rest.startsWith("[")) {
668
+ const close = rest.indexOf("]");
669
+ subscript = rest.slice(1, close < 0 ? undefined : close);
670
+ rest = close < 0 ? "" : rest.slice(close + 1);
671
+ }
672
+ const offset = /^:(?![-=+?])(.*)$/s.exec(rest)?.[1];
673
+ return { ...(subscript !== undefined ? { subscript } : {}), ...(offset !== undefined ? { offset } : {}) };
674
+ }
675
+ function bodyEvaluatesText(body) {
676
+ const { subscript, offset } = parameterArithmeticTexts(body);
677
+ return (subscript !== undefined && /[A-Za-z_$]/.test(subscript)) || (offset !== undefined && /^[^}]*[A-Za-z_$]/.test(offset));
678
+ }
679
+ function nestedBodyEvaluatesText(text) {
680
+ for (let i = text.indexOf("${"); i >= 0; i = text.indexOf("${", i + 1))
681
+ if (bodyEvaluatesText(text.slice(i + 2)))
682
+ return true;
683
+ return false;
684
+ }
685
+ const TRANSFORM_FORM = new RegExp(String.raw `^${PARAMETER}(?:\[(?:'[^']*'|"(?:[^"\\]|\\.)*"|\\.|[^\]'"\\])*\])?@([A-Za-z])$`, "s");
686
+ const RESPELLING_TRANSFORMS = new Set(["Q", "E", "A", "K", "k", "a", "U", "u", "L"]);
687
+ const NAME_OR_KEY_LIST = /^![A-Za-z_][A-Za-z0-9_]*(?:[*@]|\[[@*]\])$/;
688
+ const PLAIN_ARITHMETIC = /^[\s\d+\-*/%&|^~!<>=?:,()]*$/;
689
+ export function expansionHidesRun(e) {
690
+ if (e.kind === "command" || e.kind === "process" || e.inner?.some(expansionHidesRun) === true)
691
+ return true;
692
+ const body = e.body?.replace(/\\\n/g, "");
693
+ if (body === undefined)
694
+ return false;
695
+ if (e.kind === "arithmetic")
696
+ return !PLAIN_ARITHMETIC.test(body);
697
+ return SUBSTITUTION_OPENER.test(body) || (e.kind === "parameter" && (bodyEvaluatesText(body) || parameterRereadsValue(body))) || nestedBodyEvaluatesText(body);
698
+ }
699
+ export function compoundValueHidesRun(w) {
700
+ return w.assignment?.form === "compound" && /\$(?!')|`|\[[^\]]*[$`A-Za-z_]/.test(w.assignment.value.raw);
701
+ }
702
+ export function transformEvaluates(body) {
703
+ const transform = TRANSFORM_FORM.exec(body)?.[1];
704
+ return transform !== undefined && !RESPELLING_TRANSFORMS.has(transform);
705
+ }
706
+ export function parameterRereadsValue(body) {
707
+ const text = body.replace(/\\\n/g, "");
708
+ return (text.startsWith("!") && !NAME_OR_KEY_LIST.test(text)) || transformEvaluates(text);
709
+ }
710
+ export function arithmeticTextIsPlain(text) {
711
+ return PLAIN_ARITHMETIC.test(text.replace(/\\\n/g, ""));
712
+ }
713
+ export function namesRedirectDescriptor(word, next) {
714
+ return next?.kind === "redirect" && word.expansions.length === 0 && word.quotedSpans.length === 0 && /^\{[A-Za-z_][A-Za-z0-9_]*\}$/.test(word.raw);
715
+ }
530
716
  export function literalTextOf(pieces) {
531
717
  return pieces.every((q) => q.literal !== undefined) ? pieces.map((q) => q.literal).join("") : undefined;
532
718
  }
@@ -717,7 +903,8 @@ class WordBuilder {
717
903
  const covering = this.pieces.find((p) => p.literal !== undefined && p.rawStart <= from && p.rawEnd >= to);
718
904
  if (covering === undefined)
719
905
  return;
720
- this.carve(from, to, { kind: "tilde", yields: "one", body: this.raw.slice(from, to) });
906
+ const body = this.raw.slice(from, to);
907
+ this.carve(from, to, { kind: "tilde", yields: /[*?[]/.test(body) ? "many" : "one", body });
721
908
  }
722
909
  carveBraceAndGlob() {
723
910
  const inExp = this.inExpansion();
@@ -816,6 +1003,14 @@ class WordBuilder {
816
1003
  k = close;
817
1004
  }
818
1005
  }
1006
+ const enclosed = this.pieces.filter((p) => p.expansion !== undefined);
1007
+ if (enclosed.length > 0) {
1008
+ for (const r of ranges) {
1009
+ const inner = enclosed.filter((p) => p.rawStart >= r.from && p.rawEnd <= r.to).map((p) => p.expansion);
1010
+ if (inner.length > 0)
1011
+ r.exp = { ...r.exp, inner };
1012
+ }
1013
+ }
819
1014
  this.carveAll(ranges);
820
1015
  }
821
1016
  readAssignment() {
@@ -955,9 +1150,8 @@ class WordBuilder {
955
1150
  };
956
1151
  }
957
1152
  }
958
- function scan(source, connectorsAreText) {
1153
+ function scan(source, connectorsAreText, segments, level, list) {
959
1154
  const split = !connectorsAreText;
960
- const segments = [{ start: 0, end: source.length, connector: "start", tokens: [], substitutions: [] }];
961
1155
  const current = () => segments[segments.length - 1];
962
1156
  const pendingHeredocs = [];
963
1157
  let pendingRedirect;
@@ -981,7 +1175,7 @@ function scan(source, connectorsAreText) {
981
1175
  const take = (ch, src) => w.emit(ch, ch, quotedBy(), src, src + 1);
982
1176
  const substitution = (from, to, body, kind) => {
983
1177
  const by = quotedBy();
984
- w.opaque(source.slice(from, to), SUBSTITUTION_PLACEHOLDER, by, from, to, { kind, yields: kind === "command" && by === undefined ? "many" : "one", body });
1178
+ w.opaque(source.slice(from, to), SUBSTITUTION_PLACEHOLDER, by, from, to, { kind, yields: kind !== "process" && by === undefined ? "many" : "one", body });
985
1179
  current().substitutions.push(body);
986
1180
  };
987
1181
  const resetPosition = () => { acceptable = true; commandWord = undefined; afterLeadingRedirect = false; assignmentSeen = false; subscriptsClosed = false; timeGrammar = undefined; definitionName = false; coprocWord = undefined; };
@@ -1038,6 +1232,8 @@ function scan(source, connectorsAreText) {
1038
1232
  return undefined;
1039
1233
  }
1040
1234
  if (acceptable && bare && (SHELL_CONTROL_PREFIXES.has(word.folded) || GROUP_WORDS.has(word.folded))) {
1235
+ if (list !== undefined && GROUP_WORDS.has(word.folded))
1236
+ list.groups += SCOPE_OPEN_WORDS.has(word.folded) ? 1 : -1;
1041
1237
  timeGrammar = TIMER_WORDS.has(word.folded) ? "opt" : undefined;
1042
1238
  coprocWord = undefined;
1043
1239
  return undefined;
@@ -1088,7 +1284,7 @@ function scan(source, connectorsAreText) {
1088
1284
  pendingHeredocs.length = 0;
1089
1285
  return read.resume;
1090
1286
  };
1091
- for (let i = 0; i < source.length; i++) {
1287
+ for (let i = list?.from ?? 0; i < source.length; i++) {
1092
1288
  if (!inSingle) {
1093
1289
  const j = past(source, i);
1094
1290
  if (j >= source.length)
@@ -1096,6 +1292,12 @@ function scan(source, connectorsAreText) {
1096
1292
  i = j;
1097
1293
  }
1098
1294
  const ch = source[i];
1295
+ if (list !== undefined && ch === "}" && list.groups === 0 && !inSingle && !inDouble && !w.started && acceptable && pendingRedirect === undefined && pendingHeredoc === undefined) {
1296
+ if (pendingHeredocs.length > 0)
1297
+ return fail("undelimited_substitution", i);
1298
+ list.closed = i + 1;
1299
+ return { command: { segments: [], substitutions: [] } };
1300
+ }
1099
1301
  if (ch === "$" && !inSingle && !inDouble && dollarOpensQuote(source, i)) {
1100
1302
  const q = next(i);
1101
1303
  if (source[q] === '"') {
@@ -1198,7 +1400,8 @@ function scan(source, connectorsAreText) {
1198
1400
  if (ch === "\\") {
1199
1401
  const escaped = source[i + 1];
1200
1402
  if (escaped === undefined) {
1201
- take(ch, i);
1403
+ w.mark("trailing_backslash", w.raw.length);
1404
+ w.opaque(ch, ch, "\\", i, i + 1, undefined);
1202
1405
  continue;
1203
1406
  }
1204
1407
  if (inDouble && !"$`\"\\".includes(escaped))
@@ -1217,15 +1420,31 @@ function scan(source, connectorsAreText) {
1217
1420
  continue;
1218
1421
  }
1219
1422
  if (ch === "$" && source[n1] === "(") {
1220
- const arithmetic = source[next(n1)] === "(";
1221
- const end = substitutionBodyEnd(source, n1, inDouble);
1222
- if (typeof end !== "number")
1223
- return end === "unterminated" ? fail("unterminated_command_substitution", i) : end === "undelimited" ? fail("undelimited_substitution", i) : end;
1224
- substitution(i, end, source.slice(n1 + 1, end - 1), arithmetic ? "arithmetic" : "command");
1423
+ const close = substitutionBodyEnd(source, n1, inDouble, level);
1424
+ if (typeof close !== "object" || !("end" in close))
1425
+ return close === "unterminated" ? fail("unterminated_command_substitution", i) : close === "undelimited" ? fail("undelimited_substitution", i) : close;
1426
+ substitution(i, close.end, source.slice(n1 + 1, close.end - 1), close.arithmetic ? "arithmetic" : "command");
1427
+ i = close.end - 1;
1428
+ continue;
1429
+ }
1430
+ if (ch === "$" && source[n1] === "[") {
1431
+ const end = bracketExpansionEnd(source, n1);
1432
+ if (end === undefined)
1433
+ return fail("unterminated_arithmetic_expansion", i);
1434
+ substitution(i, end, source.slice(n1 + 1, end - 1), "arithmetic");
1225
1435
  i = end - 1;
1226
1436
  continue;
1227
1437
  }
1228
1438
  if (ch === "$" && source[n1] === "{") {
1439
+ if (opensCurrentShellSubstitution(source, n1)) {
1440
+ const run = currentShellSubstitutionEnd(source, n1, level + 1);
1441
+ if (typeof run !== "object" || !("end" in run))
1442
+ return run === "undelimited" ? fail("undelimited_substitution", i) : run;
1443
+ substitution(i, run.end, source.slice(run.from, run.end - 1), "command");
1444
+ (current().currentShell ??= []).push(source.slice(run.from, run.end - 1));
1445
+ i = run.end - 1;
1446
+ continue;
1447
+ }
1229
1448
  const end = braceExpansionEnd(source, n1);
1230
1449
  if (end === undefined)
1231
1450
  return fail("unterminated_parameter_expansion", i);
@@ -1234,7 +1453,7 @@ function scan(source, connectorsAreText) {
1234
1453
  const name = EXPANSION_NAME.exec(body)?.[1];
1235
1454
  if (UNREADABLE_EXPANSION_TEXT.test(body))
1236
1455
  w.mark("parameter_text", w.raw.length);
1237
- w.opaque(text, text, quotedBy(), i, end, { kind: "parameter", yields: inDouble ? "one" : "many", body, ...(name !== undefined ? { name } : {}) });
1456
+ w.opaque(text, text, quotedBy(), i, end, { kind: "parameter", yields: inDouble && !PER_ELEMENT_BODY.test(body) ? "one" : "many", body, ...(name !== undefined ? { name } : {}) });
1238
1457
  i = end - 1;
1239
1458
  continue;
1240
1459
  }
@@ -1242,7 +1461,7 @@ function scan(source, connectorsAreText) {
1242
1461
  const head = BARE_PARAMETER.exec(source.slice(n1, n1 + 64))?.[0];
1243
1462
  if (head !== undefined) {
1244
1463
  const text = source.slice(i, n1 + head.length);
1245
- w.opaque(text, text, quotedBy(), i, n1 + head.length, { kind: "parameter", yields: inDouble ? "one" : "many", name: head });
1464
+ w.opaque(text, text, quotedBy(), i, n1 + head.length, { kind: "parameter", yields: inDouble && head !== "@" ? "one" : "many", name: head });
1246
1465
  i = n1 + head.length - 1;
1247
1466
  continue;
1248
1467
  }
@@ -1252,18 +1471,21 @@ function scan(source, connectorsAreText) {
1252
1471
  continue;
1253
1472
  }
1254
1473
  if ((ch === "<" || ch === ">") && source[n1] === "(") {
1255
- const end = substitutionBodyEnd(source, n1, false);
1256
- if (typeof end !== "number")
1257
- return end === "unterminated" ? fail("unterminated_process_substitution", i) : end === "undelimited" ? fail("undelimited_substitution", i) : end;
1258
- substitution(i, end, source.slice(n1 + 1, end - 1), "process");
1259
- i = end - 1;
1474
+ const close = substitutionBodyEnd(source, n1, false, level);
1475
+ if (typeof close !== "object" || !("end" in close))
1476
+ return close === "unterminated" ? fail("unterminated_process_substitution", i) : close === "undelimited" ? fail("undelimited_substitution", i) : close;
1477
+ substitution(i, close.end, source.slice(n1 + 1, close.end - 1), "process");
1478
+ i = close.end - 1;
1260
1479
  continue;
1261
1480
  }
1262
1481
  if (ch === "(" && w.started && w.isAssignmentHead() && (acceptable || (commandWord !== undefined && COMPOUND_OPERAND_COMMANDS.has(commandWord)))) {
1263
- const end = matchedParen(source, i);
1482
+ const end = matchedParen(source, i, level);
1264
1483
  if (end === undefined)
1265
1484
  return fail("unterminated_compound_value", i);
1266
- const inner = scan(source.slice(i + 1, end - 1), true);
1485
+ if (typeof end !== "number")
1486
+ return end;
1487
+ const value = source.slice(i + 1, end - 1);
1488
+ const inner = scan(value, true, [firstSegment(value)], level);
1267
1489
  if ("parseFailure" in inner)
1268
1490
  return fail(inner.parseFailure.reason, i + 1 + inner.parseFailure.at);
1269
1491
  const text = source.slice(i, end);
@@ -1271,6 +1493,9 @@ function scan(source, connectorsAreText) {
1271
1493
  w.opaque(text, text, undefined, i, end, undefined);
1272
1494
  w.compound = true;
1273
1495
  current().substitutions.push(...inner.command.substitutions);
1496
+ const runsHere = inner.command.segments.flatMap((s) => s.currentShell ?? []);
1497
+ if (runsHere.length > 0)
1498
+ (current().currentShell ??= []).push(...runsHere);
1274
1499
  i = end - 1;
1275
1500
  continue;
1276
1501
  }
@@ -1350,6 +1575,11 @@ function scan(source, connectorsAreText) {
1350
1575
  i = n1;
1351
1576
  continue;
1352
1577
  }
1578
+ if (ch === "<" && source[n1] === ">") {
1579
+ pendingRedirect = { op: "<>", ...written };
1580
+ i = n1;
1581
+ continue;
1582
+ }
1353
1583
  if (ch === ">" && (source[n1] === ">" || source[n1] === "|")) {
1354
1584
  pendingRedirect = { op: source[n1] === ">" ? ">>" : ">", ...written };
1355
1585
  i = n1;
@@ -1406,18 +1636,22 @@ function scan(source, connectorsAreText) {
1406
1636
  return tail;
1407
1637
  if (pendingHeredocs.length > 0)
1408
1638
  return fail("unterminated_heredoc", source.length);
1409
- const out = segments.map((s) => ({
1410
- text: source.slice(s.start, s.end),
1411
- connector: s.connector,
1412
- ...(s.pipesStderr ? { pipesStderr: true } : {}),
1413
- tokens: s.tokens,
1414
- substitutions: s.substitutions,
1415
- }));
1639
+ const out = segments.map((s) => segmentView(source, s));
1416
1640
  return { command: { segments: out, substitutions: out.flatMap((s) => s.substitutions) } };
1417
1641
  }
1642
+ const firstSegment = (source) => ({ start: 0, end: source.length, connector: "start", tokens: [], substitutions: [] });
1643
+ const segmentView = (source, s) => ({
1644
+ text: source.slice(s.start, s.end),
1645
+ connector: s.connector,
1646
+ ...(s.pipesStderr ? { pipesStderr: true } : {}),
1647
+ tokens: s.tokens,
1648
+ substitutions: s.substitutions,
1649
+ ...(s.currentShell !== undefined ? { currentShell: s.currentShell } : {}),
1650
+ });
1418
1651
  export function lexShell(source) {
1419
- const pass = scan(source, false);
1420
- return "parseFailure" in pass ? pass : pass.command;
1652
+ const segments = [firstSegment(source)];
1653
+ const pass = scan(source, false, segments, 0);
1654
+ return "parseFailure" in pass ? { ...pass, readBefore: segments.map((s) => segmentView(source, s)) } : pass.command;
1421
1655
  }
1422
1656
  export function lexShellText(source) {
1423
1657
  const lexed = lexShell(source);
@@ -14,6 +14,7 @@ import type { RosterStore } from "../../agents/roster-store.js";
14
14
  import type { SessionPolicyStore } from "../../core/session-policy-store.js";
15
15
  import type { WorkflowJournalStore } from "../../core/workflow-journal-store.js";
16
16
  import type { UsageWindowStore } from "../../core/usage-window-store.js";
17
+ import type { EngineStorageBackend } from "../../core/engine-storage-backend.js";
17
18
  export { FileCheckpointStore, type FileCheckpointStoreOptions } from "./checkpoint-store.js";
18
19
  export { FileMemoryStore } from "./memory-store.js";
19
20
  export { FileSessionRepo, type FileSessionRepoOptions } from "./session-store.js";
@@ -29,9 +30,10 @@ export { processFingerprint, isProcessLive, type ProcessFingerprint } from "./fs
29
30
  export { AdoptionError, assertAdoptionBootGate, readRootAdoptionFile, writeRootAdoptionFile, ROOT_ADOPTION_FILE, type AdoptionErrorCode, type AdoptionSource, type AdoptionReport, type AdoptionReceipt, type AdoptionLegReport, type AffectedDeploymentConfig, type RootAdoptionFile, } from "./adoption/marker.js";
30
31
  export { adoptLocalDataRoot, ackAdoptionConfig, witnessAdoptionConfig, listAdoptionQuarantine, readAdoptionStatus, type AdoptionStatus, type AdoptLocalDataRootOptions, type AdoptLocalDataRootResult, type AdoptionCarriageLeg, type AdoptionCarriageLegContext, type AdoptionConfigWitnessReceipt, } from "./adoption/adopt.js";
31
32
  /**
32
- * design/80 — the file-backed `StorageBackend` for the local (TOC) single-user binary: the 3rd store impl
33
- * alongside `InMemory*` and `Pg*`, selected purely by what the embedder injects into `RunnerDeps` (no engine
34
- * change). Construct one over a single data root and pass its four members straight into `RunnerDeps`:
33
+ * design/80 — the file-backed {@link EngineStorageBackend} for the local (TOC) single-user binary, selected
34
+ * purely by what the embedder injects into `RunnerDeps` (no engine change). Construct one over a single data
35
+ * root and pass its members into the `RunnerDeps` seats of the same names (every member of the contract is
36
+ * one; the class's extra members — `root`, `memoryStore`, `consolidationLock` — are not seats):
35
37
  *
36
38
  * ```ts
37
39
  * const backend = new FileStorageBackend(); // ~/.ai-agent (or $AGENT_DATA_DIR)
@@ -39,8 +41,8 @@ export { adoptLocalDataRoot, ackAdoptionConfig, witnessAdoptionConfig, listAdopt
39
41
  * brain,
40
42
  * sessionStore: backend.sessionStore,
41
43
  * checkpointStore: backend.checkpointStore,
42
- * memoryStore: backend.memoryStore,
43
44
  * toolResultStore: backend.toolResultStore,
45
+ * // …and the other seats the contract names (sessionPolicyStore, fileHistoryStore, …)
44
46
  * });
45
47
  * // …on shutdown:
46
48
  * await backend.dispose();
@@ -122,8 +124,8 @@ export type FileStorageCorruptReadInfo = CorruptReadInfo & {
122
124
  /** Present only when the read was keyed by a principal. */
123
125
  principal?: string;
124
126
  };
125
- export declare class FileStorageBackend {
126
- /** The resolved, canonical data root all four stores live under. */
127
+ export declare class FileStorageBackend implements EngineStorageBackend {
128
+ /** The resolved, canonical data root every store of this backend lives under. */
127
129
  readonly root: string;
128
130
  readonly sessionStore: SessionStore;
129
131
  readonly checkpointStore: CheckpointStore;
@@ -3,6 +3,11 @@ import { deliverEngineNotice } from "../../core/engine-notice.js";
3
3
  import { createHash } from "node:crypto";
4
4
  import { Type } from "typebox";
5
5
  import { ARTIFACT_ACTION_CONCURRENCY_SAFE, ARTIFACT_COMMIT_THROW_OUTCOME, ARTIFACT_FAILURE_RESULT_CODE, ARTIFACT_HOST_ERROR_CODE, ARTIFACT_LIST_SCOPES, ARTIFACT_PLANE_CONTENT_ORIGIN, ARTIFACT_PUBLISH_CONTEXTS, } from "../../core/artifact-host.js";
6
+ import { BOUNDARY_LEGS } from "../../core/cross-boundary-taint.js";
7
+ const ARTIFACT_ACTION_LEGS = {
8
+ hosted: { publish: ["env", "outside_write"], list: ["outside_read"], read: ["outside_read"], list_files: ["outside_read"], read_file: ["outside_read"] },
9
+ "local-stub": { publish: ["env"], list: [], read: [], list_files: [], read_file: [] },
10
+ };
6
11
  import { ARTIFACT_TOOL_NAME, toolFace } from "../../core/tool-catalog-entries.js";
7
12
  import { resolveAsk } from "../../core/tool-policy.js";
8
13
  import { NOT_BINDABLE } from "../../core/skill-grant-binding.js";
@@ -448,6 +453,11 @@ export function createArtifactTool(opts) {
448
453
  parameters: buildSchema(caps),
449
454
  prepareArguments: withDefaultAction,
450
455
  isConcurrencySafe: (args) => isRecord(args) && typeof args["action"] === "string" && ARTIFACT_ACTION_CONCURRENCY_SAFE[args["action"]] === true,
456
+ boundaryLegsFor: (args) => {
457
+ const action = isRecord(args) ? (args["action"] ?? "publish") : undefined;
458
+ const legs = ARTIFACT_ACTION_LEGS[caps.plane];
459
+ return { legs: typeof action === "string" && Object.hasOwn(legs, action) ? legs[action] : BOUNDARY_LEGS };
460
+ },
451
461
  execute: async (args, ctx) => {
452
462
  const a = withDefaultAction(args);
453
463
  const action = a.action;