@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
@@ -1,62 +1,74 @@
1
+ import { arithmeticTextIsPlain, compoundValueHidesRun, expansionHidesRun, heredocBodyHidesRun, lexShell, namesRedirectDescriptor, PARSE_FAILURE_SENTENCE, } from "../shell/lex.js";
1
2
  import { shellWordsOf } from "../shell/shell-words.js";
3
+ import { isLauncherAssignmentOperand } from "../tools/fs/bash-program-position.js";
4
+ import { extglobOpenerIn } from "../tools/fs/bash-readonly-classifier.js";
2
5
  import { evaluatesOperand, SHELL_WRAPPER_TABLE } from "./shell-wrapper-table.js";
3
- import { arithmeticHidesRun, carriesSubstitution, freshWord, matchOp, readDoubleQuoted, readExpansion, skipBalanced, skipHeredocBodies } from "./shell-scan.js";
4
6
  export { SHELL_WRAPPER_TABLE } from "./shell-wrapper-table.js";
5
7
  const WRAPPERS = new Map(Object.entries(SHELL_WRAPPER_TABLE));
6
8
  const PREFIX_KEYWORDS = shellWordsOf("stripped_prefix");
7
9
  const CLOSING_KEYWORDS = shellWordsOf("stripped_standalone");
8
10
  const OPAQUE_KEYWORDS = shellWordsOf("opaque");
9
- const ASSIGNMENT_RE = /^[A-Za-z_][A-Za-z0-9_]*(?:\[[^\]]*\])?\+?=/;
11
+ const CONTROL_PREFIXES = shellWordsOf("control_prefix");
10
12
  const DURATION_RE = /^\d+(?:\.\d+)?[smhd]?$/;
11
- const REDIRECT_OPS = ["<<<", "<<-", "<<", "<>", "<&", "<", ">>", ">|", ">&", "&>>", "&>", ">"];
12
- const CONNECTOR_OPS = ["&&", "||", "|&", "|", ";;&", ";&", ";;", ";", "&"];
13
- const ALL_OPS = [...CONNECTOR_OPS, ...REDIRECT_OPS];
14
13
  export const MAX_SHELL_READ_CHARS = 10_000;
14
+ const LEXED_WORDS = new WeakMap();
15
+ export function lexedWordOf(w) {
16
+ return LEXED_WORDS.get(w);
17
+ }
18
+ function wordOf(w) {
19
+ let expands = false;
20
+ const widen = (to) => { if (expands !== "many")
21
+ expands = to; };
22
+ let text = "";
23
+ for (const p of w.pieces) {
24
+ const e = p.expansion;
25
+ if (p.quotedBy === "$'" || (e === undefined && p.literal === undefined)) {
26
+ widen("many");
27
+ continue;
28
+ }
29
+ if (e === undefined) {
30
+ text += p.literal ?? "";
31
+ continue;
32
+ }
33
+ if (e.kind === "tilde") {
34
+ if (p === w.pieces[0])
35
+ widen(e.yields);
36
+ text += e.body ?? "";
37
+ }
38
+ else if (e.kind === "brace") {
39
+ widen("many");
40
+ text += `{${e.body ?? ""}}`;
41
+ }
42
+ else {
43
+ widen(e.yields);
44
+ text += e.pattern?.folded ?? "";
45
+ }
46
+ }
47
+ const out = { text, raw: w.raw, expands };
48
+ LEXED_WORDS.set(out, w);
49
+ return out;
50
+ }
51
+ const literalIn = (w, set) => w !== undefined && w.expands === false && set.has(w.text);
52
+ const bareAssignment = (w) => lexedWordOf(w)?.assignment?.bare === true;
53
+ const hidesRun = (w) => w.expansions.some((p) => p.expansion !== undefined && expansionHidesRun(p.expansion)) || (w.assignment?.subscript !== undefined && !arithmeticTextIsPlain(w.assignment.subscript.raw)) || compoundValueHidesRun(w);
54
+ const CASE_ARM_UNREAD = "a `case` arm terminator outside a `case` this lane reads";
55
+ const EXTGLOB_UNREAD = "an extended pattern (`?(…)` `*(…)` `+(…)` `@(…)` `!(…)`) is one this reader does not expand";
15
56
  export function readShellCommand(source) {
16
57
  if (source.length > MAX_SHELL_READ_CHARS)
17
58
  return { segments: [{ argv: [], redirections: [], peeled: [], unreadable: `the command is longer than ${MAX_SHELL_READ_CHARS} characters — not read` }], connectors: [], grouped: false, backgrounded: false, strayRedirection: false };
59
+ const lexed = lexShell(source);
60
+ const failed = "parseFailure" in lexed ? lexed : undefined;
61
+ const read = "parseFailure" in lexed ? lexed.readBefore : lexed.segments;
18
62
  const segments = [];
19
63
  const connectors = [];
20
64
  let grouped = false;
21
65
  let backgrounded = false;
22
66
  let strayRedirection = false;
23
67
  let seg = { words: [], redirections: [] };
24
- let word = freshWord();
25
- let pendingRedirect;
26
- let heredocs = [];
27
- let badDelimiter = false;
28
68
  let afterBoundary = true;
29
69
  let lastConnector;
30
- let tail;
31
- const n = source.length;
32
- let i = 0;
33
- const endWord = () => {
34
- if (!word.started)
35
- return;
36
- if (word.braceExpands)
37
- word.expands = "many";
38
- if (word.substitution)
39
- seg.substitution = true;
40
- if (pendingRedirect !== undefined) {
41
- if (pendingRedirect === "<<" || pendingRedirect === "<<-") {
42
- if (word.expands !== false)
43
- badDelimiter = true;
44
- heredocs.push({ delimiter: word.text, quoted: word.raw !== word.text, dash: pendingRedirect === "<<-" });
45
- }
46
- seg.redirections.push({ op: pendingRedirect, target: publicWord(word) });
47
- pendingRedirect = undefined;
48
- }
49
- else {
50
- seg.words.push(word);
51
- }
52
- word = freshWord();
53
- };
70
+ let arithmetic;
54
71
  const finishSegment = (connector) => {
55
- endWord();
56
- if (pendingRedirect !== undefined) {
57
- seg.unreadable ??= `the redirection \`${pendingRedirect}\` has no operand — a syntax error`;
58
- pendingRedirect = undefined;
59
- }
60
72
  let built = buildSegment(seg);
61
73
  if (built === "keyworded")
62
74
  grouped = true;
@@ -86,214 +98,121 @@ export function readShellCommand(source) {
86
98
  afterBoundary = false;
87
99
  };
88
100
  const boundary = () => { finishSegment(undefined); grouped = true; afterBoundary = true; lastConnector = undefined; };
89
- while (i < n && tail === undefined) {
90
- const ch = source[i];
91
- if (ch === " " || ch === "\t") {
92
- endWord();
93
- i++;
94
- continue;
95
- }
96
- if (ch === "\n") {
97
- endWord();
98
- if (badDelimiter) {
99
- tail = "a here-document delimiter spelled with an expansion";
100
- break;
101
- }
102
- if (heredocs.length === 0) {
103
- finishSegment("\n");
104
- i++;
105
- continue;
106
- }
107
- const skipped = skipHeredocBodies(source, i + 1, heredocs);
108
- heredocs = [];
109
- if (skipped === undefined) {
110
- tail = "a here-document body never reaches its delimiter";
111
- break;
101
+ const word = (w) => { if (hidesRun(w))
102
+ seg.substitution = true; return wordOf(w); };
103
+ const readTokens = (s) => {
104
+ const tokens = s.tokens;
105
+ const shown = tokens.flatMap((t) => (t.kind === "word" ? t.expansions : t.kind === "redirect" ? t.target?.expansions ?? [] : [])).filter((p) => p.expansion?.kind === "command" || p.expansion?.kind === "process" || p.expansion?.kind === "arithmetic").length;
106
+ if (s.substitutions.length > shown)
107
+ seg.substitution = true;
108
+ let definitionClose = false;
109
+ tokens.forEach((t, i) => {
110
+ if (arithmetic !== undefined) {
111
+ if (t.kind === "group") {
112
+ arithmetic.depth += t.op === "(" ? 1 : -1;
113
+ if (arithmetic.depth === 0)
114
+ arithmetic = undefined;
115
+ return;
116
+ }
117
+ const w = t.kind === "word" ? t : t.kind === "redirect" ? t.target : undefined;
118
+ if (w === undefined || hidesRun(w) || !arithmeticTextIsPlain(w.raw) || t.kind === "heredoc")
119
+ seg.substitution = true;
120
+ if (t.kind === "redirect" && t.dup !== undefined && !arithmeticTextIsPlain(t.dup))
121
+ seg.substitution = true;
122
+ return;
112
123
  }
113
- if (skipped.substitution)
114
- seg.substitution = true;
115
- finishSegment("\n");
116
- i = skipped.end;
117
- continue;
118
- }
119
- if (ch === "#" && !word.started) {
120
- while (i < n && source[i] !== "\n")
121
- i++;
122
- continue;
123
- }
124
- if (ch === "\\") {
125
- if (i + 1 >= n) {
126
- tail = "a trailing backslash — the line never ends";
127
- break;
124
+ if (t.kind === "word") {
125
+ if (!namesRedirectDescriptor(t, tokens[i + 1]))
126
+ seg.words.push(word(t));
127
+ return;
128
128
  }
129
- if (source[i + 1] === "\n") {
130
- i += 2;
131
- continue;
129
+ if (t.kind === "redirect") {
130
+ const target = t.target !== undefined ? word(t.target) : { text: t.dup ?? "", raw: t.dup ?? "", expands: false };
131
+ seg.redirections.push({ op: t.op, target });
132
+ return;
132
133
  }
133
- word.started = true;
134
- word.text += source[i + 1];
135
- word.raw += source.slice(i, i + 2);
136
- i += 2;
137
- continue;
138
- }
139
- if (ch === "'") {
140
- const close = source.indexOf("'", i + 1);
141
- if (close < 0) {
142
- tail = "an unterminated single quote";
143
- break;
134
+ if (t.kind === "heredoc") {
135
+ seg.redirections.push({ op: "<<", target: { text: t.delimiter, raw: t.delimiter, expands: false } });
136
+ if (!t.quoted && heredocBodyHidesRun(t.body))
137
+ seg.substitution = true;
138
+ return;
144
139
  }
145
- word.started = true;
146
- word.text += source.slice(i + 1, close);
147
- word.raw += source.slice(i, close + 1);
148
- i = close + 1;
149
- continue;
150
- }
151
- let dq = i + 1;
152
- if (ch === "$")
153
- while (source[dq] === "\\" && source[dq + 1] === "\n")
154
- dq += 2;
155
- if (ch === '"' || (ch === "$" && source[dq] === '"')) {
156
- const open = ch === "$" ? dq + 1 : i + 1;
157
- const end = readDoubleQuoted(source, open, word);
158
- if (end === undefined) {
159
- tail = "an unterminated double quote";
160
- break;
140
+ const next = tokens[i + 1];
141
+ if (definitionClose) {
142
+ definitionClose = false;
143
+ return;
161
144
  }
162
- word.raw += source.slice(i, end);
163
- i = end;
164
- continue;
165
- }
166
- if (ch === "$" || ch === "`" || ((ch === "<" || ch === ">") && source[i + 1] === "(")) {
167
- const end = readExpansion(source, i, word);
168
- if (end === undefined) {
169
- tail = "an unterminated substitution";
170
- break;
145
+ if (t.op === "(" && next?.kind === "group" && next.op === "(" && seg.redirections.length === 0 && seg.words.every((w) => literalIn(w, CONTROL_PREFIXES) || literalIn(w, CLOSING_KEYWORDS) || (w.expands === false && w.text === "-p"))) {
146
+ arithmetic = { depth: 1 };
147
+ return;
171
148
  }
172
- if (end === i + 1) {
173
- word.started = true;
174
- word.text += ch;
175
- word.raw += ch;
176
- i = end;
177
- continue;
149
+ if (t.op === "(" && next?.kind === "group" && next.op === ")" && seg.words.length === 1 && seg.words[0].expands === false && seg.redirections.length === 0) {
150
+ seg.words.pop();
151
+ definitionClose = true;
178
152
  }
179
- word.started = true;
180
- word.expands = ch === "$" || ch === "`" ? "many" : word.expands || "one";
181
- word.raw += source.slice(i, end);
182
- i = end;
183
- continue;
184
- }
185
- if (ch === "~" && !word.started)
186
- word.expands = "one";
187
- const op = matchOp(source, i, ALL_OPS);
188
- if (op !== undefined) {
189
- if (CONNECTOR_OPS.includes(op)) {
190
- if (op === ";;" || op === ";&" || op === ";;&") {
191
- endWord();
192
- seg.unreadable ??= "a `case` arm terminator outside a `case` this lane reads";
193
- i += op.length;
194
- continue;
195
- }
196
- if (op === "&")
197
- backgrounded = true;
198
- finishSegment(op);
199
- i += op.length;
153
+ boundary();
154
+ });
155
+ };
156
+ for (let k = 0; k < read.length; k++) {
157
+ const s = read[k];
158
+ if (k > 0) {
159
+ const armEnd = caseArmEnd(read, k);
160
+ if (armEnd !== undefined) {
161
+ seg.unreadable ??= CASE_ARM_UNREAD;
162
+ k = armEnd;
163
+ readTokens(read[k]);
200
164
  continue;
201
165
  }
202
- if (word.started && word.raw === word.text && (/^\d+$/.test(word.text) || /^\{[A-Za-z_][A-Za-z0-9_]*\}$/.test(word.text)))
203
- word = freshWord();
204
- else
205
- endWord();
206
- if (pendingRedirect !== undefined)
207
- seg.unreadable ??= `the redirection \`${pendingRedirect}\` has no operand — a syntax error`;
208
- pendingRedirect = op;
209
- i += op.length;
210
- continue;
211
- }
212
- if (ch === "(" && word.started && "@?*+!".includes(word.raw.slice(-1)) && word.raw !== "!") {
213
- const end = skipBalanced(source, i, "(", ")");
214
- if (end === undefined) {
215
- tail = "an unterminated extended pattern (`@(…)`)";
216
- break;
217
- }
218
- if (carriesSubstitution(source.slice(i + 1, end - 1)))
219
- word.substitution = true;
220
- word.expands = "many";
221
- word.text += source.slice(i, end);
222
- word.raw += source.slice(i, end);
223
- i = end;
224
- continue;
225
- }
226
- if (ch === "(" && source[i + 1] === "(" && (!word.started || word.raw === "!") && pendingRedirect === undefined && seg.words.every((w) => w.expands === false && (shellWordsOf("control_prefix").has(w.text) || CLOSING_KEYWORDS.has(w.text) || w.text === "-p"))) {
227
- endWord();
228
- const end = skipBalanced(source, i, "(", ")");
229
- if (end === undefined) {
230
- tail = "an unterminated arithmetic command";
231
- break;
166
+ const connector = s.pipesStderr === true ? "|&" : s.connector;
167
+ if (arithmetic !== undefined) {
168
+ if (connector === ";")
169
+ seg.substitution = true;
232
170
  }
233
- if (arithmeticHidesRun(source.slice(i + 2, end - 1)))
234
- seg.substitution = true;
235
- i = end;
236
- continue;
237
- }
238
- if (ch === "(" || ch === ")") {
239
- if (ch === "(" && word.started && !word.expands && seg.words.length === 0 && pendingRedirect === undefined) {
240
- let j = i + 1;
241
- while (j < n && (source[j] === " " || source[j] === "\t"))
242
- j++;
243
- if (source[j] === ")") {
244
- word = freshWord();
245
- boundary();
246
- i = j + 1;
247
- continue;
248
- }
171
+ else {
172
+ if (connector === "&")
173
+ backgrounded = true;
174
+ finishSegment(connector);
249
175
  }
250
- boundary();
251
- i++;
252
- continue;
253
176
  }
254
- if (ch === "*" || ch === "?" || (ch === "]" && word.bracketOpen))
255
- word.expands = "many";
256
- if (ch === "[")
257
- word.bracketOpen = true;
258
- if (ch === "{")
259
- word.braceOpen = true;
260
- if (word.braceOpen && (ch === "," || (ch === "." && source[i + 1] === ".")))
261
- word.braceExpands = true;
262
- if (ch === "}" && !word.braceExpands)
263
- word.braceOpen = false;
264
- word.started = true;
265
- word.text += ch;
266
- word.raw += ch;
267
- i++;
177
+ readTokens(s);
268
178
  }
269
- if (tail !== undefined) {
270
- endWord();
271
- seg.unreadable ??= tail;
272
- finishSegment(undefined);
179
+ if (failed !== undefined) {
180
+ seg.unreadable ??= PARSE_FAILURE_SENTENCE[failed.parseFailure.reason];
181
+ }
182
+ else if (arithmetic !== undefined) {
183
+ seg.unreadable ??= "an unterminated arithmetic command";
273
184
  }
274
185
  else {
275
- if (heredocs.length > 0)
276
- seg.unreadable ??= "a here-document with no body";
277
- const dangling = seg.words.length === 0 && !word.started && lastConnector !== undefined && lastConnector !== ";" && lastConnector !== "&" && lastConnector !== "\n";
186
+ const dangling = seg.words.length === 0 && seg.redirections.length === 0 && lastConnector !== undefined && lastConnector !== ";" && lastConnector !== "&" && lastConnector !== "\n";
278
187
  if (dangling)
279
188
  seg.unreadable ??= `the connector \`${lastConnector}\` has no command after it — a syntax error`;
280
- finishSegment(undefined);
281
189
  }
190
+ finishSegment(undefined);
191
+ if (failed === undefined && "segments" in lexed && extglobOpenerIn(lexed, source))
192
+ return { segments: segments.map((s) => (s.unreadable !== undefined ? s : { ...s, peelUnreadable: s.peelUnreadable ?? EXTGLOB_UNREAD })), connectors, grouped, backgrounded, strayRedirection };
282
193
  return { segments, connectors, grouped, backgrounded, strayRedirection };
283
194
  }
284
- const publicWord = ({ text, raw, expands }) => ({ text, raw, expands });
195
+ function caseArmEnd(read, k) {
196
+ const arm = (j) => read[j].connector === ";" && read[j].text === "" && read[j].tokens.length === 0;
197
+ const next = read[k + 1];
198
+ if (!arm(k) || next === undefined || (next.connector !== ";" && next.connector !== "&"))
199
+ return undefined;
200
+ const third = read[k + 2];
201
+ return next.connector === ";" && arm(k + 1) && third?.connector === "&" ? k + 2 : k + 1;
202
+ }
285
203
  const SUBSTITUTION_UNREAD = "a command substitution runs a program this reader does not read";
286
204
  const MAX_WRAPPER_LAYERS = 8;
287
205
  export function isFullyReadable(shape) {
288
206
  return shape.segments.every((s) => s.unreadable === undefined && s.peelUnreadable === undefined && [s.argv, ...s.peeled].every((argv) => argv[0]?.expands === false));
289
207
  }
208
+ const operandsOf = (run) => run.map((w) => ({ text: w.text, raw: w.raw, assignment: lexedWordOf(w)?.assignment }));
290
209
  function buildSegment(seg) {
291
210
  if (seg.unreadable !== undefined)
292
- return { argv: seg.words.map(publicWord), redirections: seg.redirections, peeled: [], unreadable: seg.unreadable };
211
+ return { argv: seg.words, redirections: seg.redirections, peeled: [], unreadable: seg.unreadable };
293
212
  const carried = () => (seg.substitution === true ? { argv: [], redirections: seg.redirections, peeled: [], peelUnreadable: SUBSTITUTION_UNREAD } : keyworded ? "keyworded" : "empty");
294
213
  let keyworded = false;
295
214
  let k0 = 0;
296
- while (k0 < seg.words.length && seg.words[k0].expands === false && (PREFIX_KEYWORDS.has(seg.words[k0].text) || CLOSING_KEYWORDS.has(seg.words[k0].text))) {
215
+ while (k0 < seg.words.length && (literalIn(seg.words[k0], PREFIX_KEYWORDS) || literalIn(seg.words[k0], CLOSING_KEYWORDS))) {
297
216
  k0++;
298
217
  keyworded = true;
299
218
  }
@@ -301,24 +220,24 @@ function buildSegment(seg) {
301
220
  const head = words[0];
302
221
  if (head === undefined)
303
222
  return carried();
304
- if (evaluatesOperand(words))
223
+ if (evaluatesOperand(operandsOf(words)))
305
224
  seg.substitution = true;
306
225
  if (head.expands === false && (head.text === "[[" || head.text === "["))
307
226
  return carried();
308
- if (head.expands === false && OPAQUE_KEYWORDS.has(head.text))
309
- return { argv: words.map(publicWord), redirections: seg.redirections, peeled: [], unreadable: `a \`${head.text}\` control structure — its commands need a syntax tree` };
310
- const argv = words.map(publicWord);
227
+ if (literalIn(head, OPAQUE_KEYWORDS))
228
+ return { argv: words, redirections: seg.redirections, peeled: [], unreadable: `a \`${head.text}\` control structure — its commands need a syntax tree` };
229
+ const argv = words;
311
230
  const peeled = [];
312
231
  let peelUnreadable;
313
232
  let k = 0;
314
- while (k < argv.length && ASSIGNMENT_RE.test(argv[k].raw))
233
+ while (k < argv.length && bareAssignment(argv[k]))
315
234
  k++;
316
235
  let run = argv.slice(k);
317
236
  if (k > 0 && run.length > 0)
318
237
  peeled.push(run);
319
238
  for (let layer = 0; run.length > 0; layer++) {
320
239
  let j = 0;
321
- while (j < run.length && ((run[j].text === "!" && run[j].expands === false) || ASSIGNMENT_RE.test(run[j].raw)))
240
+ while (j < run.length && ((run[j].text === "!" && run[j].expands === false) || bareAssignment(run[j])))
322
241
  j++;
323
242
  if (j > 0) {
324
243
  run = run.slice(j);
@@ -344,7 +263,7 @@ function buildSegment(seg) {
344
263
  run = next;
345
264
  peeled.push(run);
346
265
  }
347
- if (seg.substitution === true || [argv, ...peeled].some((c) => c[0]?.expands === false && evaluatesOperand(c)))
266
+ if (seg.substitution === true || [argv, ...peeled].some((c) => c[0]?.expands === false && evaluatesOperand(operandsOf(c))))
348
267
  peelUnreadable ??= SUBSTITUTION_UNREAD;
349
268
  return { argv, redirections: seg.redirections, peeled, ...(peelUnreadable !== undefined ? { peelUnreadable } : {}) };
350
269
  }
@@ -364,6 +283,7 @@ function unwrap(words) {
364
283
  const flags = new Set(row.flags);
365
284
  const attached = new Set(row.attached ?? []);
366
285
  const commandString = new Set(row.commandString);
286
+ const assignmentOperand = (w) => row.assignments === true && w.expands === false && isLauncherAssignmentOperand(w.text);
367
287
  let positionalTaken = row.positional === undefined;
368
288
  let durationTaken = row.duration !== true;
369
289
  let replaceToken;
@@ -405,7 +325,7 @@ function unwrap(words) {
405
325
  return r;
406
326
  i++;
407
327
  }
408
- while (row.assignments === true && i < words.length && words[i].expands === false && ASSIGNMENT_RE.test(words[i].text))
328
+ while (i < words.length && assignmentOperand(words[i]))
409
329
  i++;
410
330
  break;
411
331
  }
@@ -464,7 +384,7 @@ function unwrap(words) {
464
384
  i++;
465
385
  continue;
466
386
  }
467
- if (row.assignments === true && ASSIGNMENT_RE.test(t)) {
387
+ if (assignmentOperand(w)) {
468
388
  i++;
469
389
  continue;
470
390
  }
@@ -1,5 +1,6 @@
1
+ import type { AssignmentFact } from "../shell/lex.js";
1
2
  /**
2
- * The wrapper programs the tightening reader ({@link import("./shell-lexer.js").readShellCommand}) peels to
3
+ * The wrapper programs the tightening reader ({@link import("./shell-run-candidates.js").readShellCommand}) peels to
3
4
  * reach the program they run — upstream's three tables (its light strip on every arm, its full strip on
4
5
  * the deny/ask arms, and its `xargs` clause inlined in the matcher) as ONE closed table. Peeling is
5
6
  * ADDITIVE on the reading side: the segment keeps its spelled argv as one run candidate and adds one
@@ -395,13 +396,14 @@ export interface ShellWrapperRow {
395
396
  * subscript (`arr[a]`, quoted, bare, attached `-varr[a]`, or `arr[a]=1`) is evaluated as arithmetic —
396
397
  * recursively, like `$((a))` — when the builtin reads or writes it (`printf -v 'arr[a]' …`, `unset arr[a]`,
397
398
  * `declare arr[a]=1`, `read arr[a]`, `[[ -v arr[a] ]]`, `test -v arr[a]`), and so is the value of an
398
- * INTEGER-attribute assignment (`declare -i x=a`). The lexer marks such a run's hidden run at every peel
399
+ * INTEGER-attribute assignment (`declare -i x=a`). The tightening reader marks such a run's hidden run at every peel
399
400
  * candidate (`command printf -v …`). An operand that is WHOLLY an expansion is not judged here (`unset "$X"`
400
401
  * — one more indirection; the price of reading it would be a prompt on every `export FOO="$BAR"`), nor is
401
402
  * an arithmetic comparison's operand (`[[ $n -gt 0 ]]` evaluates `$n` — the same price). */
402
403
  export declare const VARIABLE_OPERAND_BUILTINS: ReadonlySet<string>;
403
- /** Does this run of a variable-operand builtin evaluate an operand arithmetically? Linear in the words. */
404
+ /** Does this run of a variable-operand builtin evaluate an operand arithmetically? `assignment`: the lexer's fact on the word. */
404
405
  export declare function evaluatesOperand(run: readonly {
405
406
  readonly text: string;
406
407
  readonly raw: string;
408
+ readonly assignment?: AssignmentFact | undefined;
407
409
  }[]): boolean;
@@ -67,7 +67,7 @@ export function evaluatesOperand(run) {
67
67
  if (!VARIABLE_OPERAND_BUILTINS.has(run[0]?.text ?? ""))
68
68
  return false;
69
69
  const integer = run.some((w) => /^-[A-Za-z]*i/.test(w.text));
70
- return run.some((w) => namesArrayElement(w.text) || namesArrayElement(w.raw) || (integer && /^[A-Za-z_][A-Za-z0-9_]*\+?=.*[A-Za-z_$]/.test(w.text)));
70
+ return run.some((w) => namesArrayElement(w.text) || namesArrayElement(w.raw) || (integer && w.assignment !== undefined && /[A-Za-z_$]/.test(w.assignment.value.folded)));
71
71
  }
72
72
  function namesArrayElement(word) {
73
73
  for (let open = word.indexOf("["); open > 0; open = word.indexOf("[", open + 1)) {
@@ -308,7 +308,7 @@ export async function checkpointStoreContract(make, runAssertion) {
308
308
  const stored = (await store.get(bitless.token)).pendingAction;
309
309
  assert.equal("hasBidiControls" in stored, false, "projection backfill must not write back to the row");
310
310
  });
311
- run("design/388 B18 × B6 identity + face group: a row's eight optional members (capabilityId/contractId/shapeDigest/implementationRevision/rosterDigest + family/pathTarget/names) survive the round-trip verbatim; an absent group stays absent", async () => {
311
+ run("the approval object's members: a row's identity + face group and its targets (capabilityId/contractId/shapeDigest/implementationRevision + family/pathTarget/names + targets) survive the round-trip verbatim; an absent group stays absent", async () => {
312
312
  const store = make();
313
313
  const carried = createCheckpointFixture({ token: mintCheckpointToken(), sessionId: "id-carried" });
314
314
  const group = {
@@ -316,10 +316,10 @@ export async function checkpointStoreContract(make, runAssertion) {
316
316
  contractId: "acme.write@2",
317
317
  shapeDigest: "sha256:0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
318
318
  implementationRevision: "r7",
319
- rosterDigest: "sha256:fedcba9876543210fedcba9876543210fedcba9876543210fedcba9876543210",
320
319
  family: "file-write",
321
320
  pathTarget: { param: "destination", aliases: ["path"], access: "create", skillScopeEligible: true },
322
321
  names: ["AcmeWrite", "acme_write"],
322
+ targets: { protective: "/srv/work/out.txt", declared: "/srv/work/out.txt" },
323
323
  };
324
324
  Object.assign(carried.pendingAction, group);
325
325
  const bare = createCheckpointFixture({ token: mintCheckpointToken(), sessionId: "id-bare" });
@@ -0,0 +1,49 @@
1
+ import { type SessionStore } from "../session.js";
2
+ import { type ContractAssertionRunner } from "./contract-harness.js";
3
+ import { type SessionStoreIncarnationContractHooks } from "./session-store-incarnation-contract.js";
4
+ /**
5
+ * The cross-backend contract for {@link SessionStore} as the interface states it TODAY — what a store must
6
+ * do so that the engine, a deployment's authorizer and a shell read the same answers off it whether the
7
+ * store is the bundled `TtlSessionStore` (over an in-memory or file repo) or a backend that implements the
8
+ * interface directly (a SQL store). vitest-free (see `contract-harness.ts`); every case builds a FRESH store
9
+ * through `make()` and disposes it.
10
+ *
11
+ * Each case pins one sentence of the interface (`core/session.ts`) or of the session-tree storage it hands
12
+ * out (`SessionStorage`, whose path / leaf / write-CAS rules every store's sessions carry). Optional faces
13
+ * are judged only when present; a group whose face is absent passes WITHOUT asserting and says so through
14
+ * `onOptionalMember` — a case name cannot carry "absent" (the default runner drops names, and a green
15
+ * absent run is byte-identical to a green verified one), so a consumer that relies on a face asserts the
16
+ * report, never the green.
17
+ *
18
+ * What the kit cannot see from outside, it does not claim: idle eviction and `pin` (the interface has no
19
+ * clock seam), what `release` does to an ordinary session's live set (`size` is best-effort and a durable
20
+ * `list` may still show the history), a durable backend's crash recovery, and a delete that fails half-way.
21
+ * One stated obligation is not a case yet because the bundled store does not keep it: an ownership row
22
+ * outliving its session across a restart (`TtlSessionStore` holds owners in process memory).
23
+ *
24
+ * Semantics since @sema-agent/core 7.55.0.
25
+ */
26
+ export interface SessionStoreContractHooks {
27
+ /** The same hook as the incarnation kit's ({@link SessionStoreIncarnationContractHooks.deleteSession}): one
28
+ * function serves both kits. */
29
+ deleteSession: SessionStoreIncarnationContractHooks["deleteSession"];
30
+ /** A SECOND, independent store instance over the same rows as `store` — what a restarted process
31
+ * constructs. After calling it the kit no longer uses `store`, so a backend that holds an exclusive lock
32
+ * may release `store`'s first (the kit still disposes `store` at the end of the case: that dispose must
33
+ * then be harmless). The kit disposes the returned instance. */
34
+ reopen: (store: SessionStore) => SessionStore | Promise<SessionStore>;
35
+ /** A second LIVE instance over the same rows, used CONCURRENTLY with `store` — what another replica
36
+ * constructs. Absent ⇒ the replica case is reported absent (a backend that is single-process by
37
+ * contract has no replica). The kit disposes it. */
38
+ replica?: (store: SessionStore) => SessionStore;
39
+ runAssertion?: ContractAssertionRunner;
40
+ /** Optional faces (plus the replica hook, and `requireExisting` — absent = the store rejects the option, its other
41
+ * legal mode) a run verified or found absent; reported per judging case, after its assertions held. */
42
+ onOptionalMember?: (report: {
43
+ member: "requireExisting" | "forget" | "fork" | "list" | "noteTaskRun" | "listPlaced" | "placements" | "ownership" | "incarnationOf" | "replica";
44
+ status: "verified" | "absent";
45
+ }) => void;
46
+ }
47
+ /** A hooks record the kit cannot run with is refused SYNCHRONOUSLY, before any case is registered, so a
48
+ * `void sessionStoreContract(…)` registration fails at collection instead of as an unhandled rejection. */
49
+ export declare function sessionStoreContract(make: () => SessionStore, hooks: SessionStoreContractHooks): Promise<void>;